summaryrefslogtreecommitdiff
path: root/internal/sync
diff options
context:
space:
mode:
Diffstat (limited to 'internal/sync')
-rw-r--r--internal/sync/branch_sync.go8
-rw-r--r--internal/sync/git_operations.go117
-rw-r--r--internal/sync/repository_setup.go39
-rw-r--r--internal/sync/sync.go65
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"))
+}