diff options
Diffstat (limited to 'internal/sync')
| -rw-r--r-- | internal/sync/branch_sync.go | 8 | ||||
| -rw-r--r-- | internal/sync/git_operations.go | 117 | ||||
| -rw-r--r-- | internal/sync/repository_setup.go | 39 | ||||
| -rw-r--r-- | internal/sync/sync.go | 65 |
4 files changed, 225 insertions, 4 deletions
diff --git a/internal/sync/branch_sync.go b/internal/sync/branch_sync.go index 1bf8b79..02f8964 100644 --- a/internal/sync/branch_sync.go +++ b/internal/sync/branch_sync.go @@ -10,7 +10,11 @@ import ( func (s *Syncer) trackRemotesWithBranch(branch string, remotes map[string]*config.Organization) map[string]bool { remotesWithBranch := make(map[string]bool) - for remoteName := range remotes { + for remoteName, org := range remotes { + // Skip checking backup locations as we don't sync from them + if org.BackupLocation { + continue + } if s.remoteBranchExists(remoteName, branch) { remotesWithBranch[remoteName] = true } @@ -48,7 +52,7 @@ func pushToAllRemotes(branch string, remotes map[string]*config.Organization, re fmt.Printf(" Pushing to %s (%s)...\n", remoteName, org.Host) } - if err := pushBranch(remoteName, branch, remoteHasBranch); err != nil { + if err := pushBranchWithBackupSupport(remoteName, branch, remoteHasBranch, org); err != nil { return err } } diff --git a/internal/sync/git_operations.go b/internal/sync/git_operations.go index 7664113..3bd6618 100644 --- a/internal/sync/git_operations.go +++ b/internal/sync/git_operations.go @@ -7,6 +7,8 @@ import ( "os/exec" "regexp" "strings" + + "codeberg.org/snonux/gitsyncer/internal/config" ) // checkForMergeConflicts checks if the repository has merge conflicts @@ -233,3 +235,118 @@ func getAllUniqueBranches(output []byte) []string { return branches } + +// createSSHBareRepository creates a bare repository on an SSH server +func createSSHBareRepository(sshHost, repoPath string) error { + // Extract user@host and path components + parts := strings.Split(sshHost, ":") + if len(parts) != 2 { + return fmt.Errorf("invalid SSH host format: %s", sshHost) + } + + userHost := parts[0] + basePath := parts[1] + + // Full path to the repository + fullRepoPath := fmt.Sprintf("%s/%s.git", basePath, repoPath) + + fmt.Printf("Creating bare repository at %s:%s\n", userHost, fullRepoPath) + + // Create the repository directory and initialize as bare + commands := fmt.Sprintf("mkdir -p %s && cd %s && git init --bare", fullRepoPath, fullRepoPath) + cmd := exec.Command("ssh", userHost, commands) + output, err := cmd.CombinedOutput() + + if err != nil { + return fmt.Errorf("failed to create bare repository: %w\n%s", err, string(output)) + } + + fmt.Printf("Successfully created bare repository at %s:%s\n", userHost, fullRepoPath) + return nil +} + +// pushBranchWithBackupSupport pushes a branch to a remote, creating SSH repos if needed +func pushBranchWithBackupSupport(remoteName, branch string, remoteHasBranch bool, org *config.Organization) error { + cmd := exec.Command("git", "push", remoteName, branch, "--tags") + output, err := cmd.CombinedOutput() + + if err != nil { + outputStr := string(output) + // Check if it's because the repository doesn't exist + if isRepositoryMissing(outputStr) { + // If it's an SSH backup location, try to create the repository + if org.BackupLocation && org.IsSSH() { + // Get the repository name from the remote URL + remoteURL, err := getRemoteURL(remoteName) + if err != nil { + return fmt.Errorf("failed to get remote URL: %w", err) + } + + // Extract repo name from URL + repoName := extractRepoName(remoteURL) + if repoName == "" { + return fmt.Errorf("failed to extract repository name from URL: %s", remoteURL) + } + + // Create the bare repository + if err := createSSHBareRepository(org.Host, repoName); err != nil { + return fmt.Errorf("failed to create SSH repository: %w", err) + } + + // Try pushing again + cmd = exec.Command("git", "push", remoteName, branch, "--tags") + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to push after creating repository: %w", err) + } + fmt.Printf(" Successfully pushed to newly created backup repository\n") + return nil + } + + fmt.Printf(" Note: Remote repository %s does not exist - must be created manually\n", remoteName) + fmt.Printf(" Skipping push to %s\n", remoteName) + return nil // Not an error, just skip + } + + // Check if it's because the branch doesn't exist on the remote + if isBranchMissing(outputStr) { + fmt.Printf(" Creating new branch on %s\n", remoteName) + // Try again with -u flag to set upstream + cmd = exec.Command("git", "push", "-u", remoteName, branch, "--tags") + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to push to %s: %w", remoteName, err) + } + return nil + } + + return fmt.Errorf("failed to push to %s: %w\n%s", remoteName, err, outputStr) + } + + if !remoteHasBranch { + fmt.Printf(" Successfully created branch %s on %s\n", branch, remoteName) + } + + return nil +} + +// getRemoteURL gets the URL for a given remote +func getRemoteURL(remoteName string) (string, error) { + cmd := exec.Command("git", "remote", "get-url", remoteName) + output, err := cmd.Output() + if err != nil { + return "", err + } + return strings.TrimSpace(string(output)), nil +} + +// extractRepoName extracts the repository name from a git URL +func extractRepoName(url string) string { + // Remove .git suffix if present + url = strings.TrimSuffix(url, ".git") + + // Extract the last component of the path + parts := strings.Split(url, "/") + if len(parts) > 0 { + return parts[len(parts)-1] + } + return "" +} diff --git a/internal/sync/repository_setup.go b/internal/sync/repository_setup.go index 15d436e..3ebafbd 100644 --- a/internal/sync/repository_setup.go +++ b/internal/sync/repository_setup.go @@ -22,7 +22,21 @@ func (s *Syncer) setupNewRepository(repoPath string) error { return fmt.Errorf("no organizations configured") } - firstOrg := &s.config.Organizations[0] + // Find first non-backup organization to clone from + var firstOrg *config.Organization + var firstOrgIndex int + for i := range s.config.Organizations { + if !s.config.Organizations[i].BackupLocation { + firstOrg = &s.config.Organizations[i] + firstOrgIndex = i + break + } + } + + if firstOrg == nil { + return fmt.Errorf("no non-backup organizations configured to clone from") + } + if err := s.cloneRepository(firstOrg, repoPath); err != nil { return fmt.Errorf("failed to clone repository: %w", err) } @@ -35,8 +49,17 @@ func (s *Syncer) setupNewRepository(repoPath string) error { } // Add other organizations as remotes - for i := 1; i < len(s.config.Organizations); i++ { + for i := range s.config.Organizations { + if i == firstOrgIndex { + continue // Skip the first org we already cloned from + } org := &s.config.Organizations[i] + + // Skip backup locations if backup is not enabled + if org.BackupLocation && !s.backupEnabled { + continue + } + if err := s.addRemote(repoPath, org); err != nil { return fmt.Errorf("failed to add remote %s: %w", s.getRemoteName(org), err) } @@ -52,6 +75,12 @@ func (s *Syncer) setupExistingRepository(repoPath string) error { // Check and add any missing remotes for i := range s.config.Organizations { org := &s.config.Organizations[i] + + // Skip backup locations if backup is not enabled + if org.BackupLocation && !s.backupEnabled { + continue + } + remoteName := s.getRemoteName(org) // Check if remote exists @@ -86,6 +115,12 @@ func (s *Syncer) getRemotesMap() map[string]*config.Organization { remotes := make(map[string]*config.Organization) for i := range s.config.Organizations { org := &s.config.Organizations[i] + + // Skip backup locations if backup is not enabled + if org.BackupLocation && !s.backupEnabled { + continue + } + remoteName := s.getRemoteName(org) remotes[remoteName] = org } diff --git a/internal/sync/sync.go b/internal/sync/sync.go index b413318..af0feba 100644 --- a/internal/sync/sync.go +++ b/internal/sync/sync.go @@ -17,6 +17,7 @@ type Syncer struct { repoName string abandonedReports map[string]*AbandonedBranchReport // Collects reports across repos branchFilter *BranchFilter // Filter for excluding branches + backupEnabled bool // Whether to sync to backup locations } // CLAUDE: Is there a reason, we return a pointer to Syncer? @@ -35,9 +36,15 @@ func New(cfg *config.Config, workDir string) *Syncer { workDir: workDir, abandonedReports: make(map[string]*AbandonedBranchReport), branchFilter: branchFilter, + backupEnabled: false, // Default to false, will be set via SetBackupEnabled } } +// SetBackupEnabled enables or disables syncing to backup locations +func (s *Syncer) SetBackupEnabled(enabled bool) { + s.backupEnabled = enabled +} + // SyncRepository synchronizes a repository across all configured organizations func (s *Syncer) SyncRepository(repoName string) error { s.repoName = repoName @@ -109,11 +116,19 @@ func (s *Syncer) SyncRepository(repoName string) error { // cloneRepository clones a repository from an organization func (s *Syncer) cloneRepository(org *config.Organization, repoPath string) error { + // Skip cloning from backup locations + if org.BackupLocation { + return fmt.Errorf("cannot clone from backup location %s", org.Host) + } + // For file:// URLs, we need special handling var cloneURL string if strings.HasPrefix(org.Host, "file://") { // For local file paths, the format is: file:///path/to/repo.git cloneURL = fmt.Sprintf("%s/%s.git", org.Host, s.repoName) + } else if org.IsSSH() && org.Name == "" { + // For SSH backup locations: user@host:path/repo.git + cloneURL = fmt.Sprintf("%s/%s.git", org.Host, s.repoName) } else { // For SSH URLs, the format is: git@host:org/repo.git cloneURL = fmt.Sprintf("%s/%s.git", org.GetGitURL(), s.repoName) @@ -140,6 +155,9 @@ func (s *Syncer) addRemote(repoPath string, org *config.Organization) error { var remoteURL string if strings.HasPrefix(org.Host, "file://") { remoteURL = fmt.Sprintf("%s/%s.git", org.Host, s.repoName) + } else if org.IsSSH() && org.Name == "" { + // For SSH backup locations: user@host:path/repo.git + remoteURL = fmt.Sprintf("%s/%s.git", org.Host, s.repoName) } else { remoteURL = fmt.Sprintf("%s/%s.git", org.GetGitURL(), s.repoName) } @@ -163,8 +181,17 @@ func (s *Syncer) fetchAll() error { return err } + // Get remotes map to check if it's a backup location + remotesMap := s.getRemotesMap() + // Fetch from each remote for remote := range remotes { + // Skip backup locations - we don't fetch from them + if org, exists := remotesMap[remote]; exists && org.BackupLocation { + fmt.Printf("Skipping fetch from backup location %s\n", remote) + continue + } + if err := fetchRemote(remote); err != nil { return err } @@ -181,6 +208,12 @@ func (s *Syncer) getAllBranches() ([]string, error) { return nil, err } + // If backup is disabled, filter out branches from backup locations + if !s.backupEnabled { + filteredOutput := s.filterBackupBranches(output) + return getAllUniqueBranches(filteredOutput), nil + } + return getAllUniqueBranches(output), nil } @@ -287,3 +320,35 @@ func (s *Syncer) getRemoteName(org *config.Organization) string { return host } + +// filterBackupBranches filters out branches from backup locations +func (s *Syncer) filterBackupBranches(output []byte) []byte { + lines := strings.Split(string(output), "\n") + var filtered []string + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + // Check if this branch is from a backup remote + isBackup := false + for i := range s.config.Organizations { + org := &s.config.Organizations[i] + if org.BackupLocation { + remoteName := s.getRemoteName(org) + if strings.HasPrefix(line, remoteName+"/") { + isBackup = true + break + } + } + } + + if !isBackup { + filtered = append(filtered, line) + } + } + + return []byte(strings.Join(filtered, "\n")) +} |
