summaryrefslogtreecommitdiff
path: root/internal/sync/sync.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-06-23 23:26:52 +0300
committerPaul Buetow <paul@buetow.org>2025-06-23 23:26:52 +0300
commit006724744a943aad877a92406a5e2b4d5d12acd3 (patch)
treece79e6481d3a9ae38bebf3a7acd1d3a7edd520a8 /internal/sync/sync.go
parent125e2a2c50bcb3eaa5dfb8802c6de3b2f406b3d2 (diff)
Add GitHub repository creation and improve error handling
- Add --create-github-repos flag to automatically create missing GitHub repositories - Implement GitHub API client with token support from config/env/file - Add Codeberg API integration to sync all public repositories - Make sync operations stop on first error for better debugging - Support GitHub repo creation for all sync commands (--sync, --sync-all, --sync-codeberg-public) - Add comprehensive error messages and debug logging 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'internal/sync/sync.go')
-rw-r--r--internal/sync/sync.go113
1 files changed, 62 insertions, 51 deletions
diff --git a/internal/sync/sync.go b/internal/sync/sync.go
index e35dad7..7f9b81e 100644
--- a/internal/sync/sync.go
+++ b/internal/sync/sync.go
@@ -17,6 +17,7 @@ type Syncer struct {
repoName string
}
+// CLAUDE: Is there a reason, we return a pointer to Syncer?
// New creates a new Syncer instance
func New(cfg *config.Config, workDir string) *Syncer {
return &Syncer{
@@ -28,7 +29,7 @@ func New(cfg *config.Config, workDir string) *Syncer {
// SyncRepository synchronizes a repository across all configured organizations
func (s *Syncer) SyncRepository(repoName string) error {
s.repoName = repoName
-
+
// Create work directory if it doesn't exist
if err := os.MkdirAll(s.workDir, 0755); err != nil {
return fmt.Errorf("failed to create work directory: %w", err)
@@ -49,19 +50,19 @@ func (s *Syncer) SyncRepository(repoName string) error {
if len(s.config.Organizations) == 0 {
return fmt.Errorf("no organizations configured")
}
-
+
firstOrg := &s.config.Organizations[0]
if err := s.cloneRepository(firstOrg, repoPath); err != nil {
return fmt.Errorf("failed to clone repository: %w", err)
}
-
+
// Rename origin to the proper remote name
firstRemoteName := s.getRemoteName(firstOrg)
cmd := exec.Command("git", "-C", repoPath, "remote", "rename", "origin", firstRemoteName)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to rename origin remote: %w", err)
}
-
+
// Add other organizations as remotes
for i := 1; i < len(s.config.Organizations); i++ {
org := &s.config.Organizations[i]
@@ -72,12 +73,12 @@ func (s *Syncer) SyncRepository(repoName string) error {
} else {
// Repository exists, ensure all remotes are configured
fmt.Printf("Using existing repository at %s\n", repoPath)
-
+
// Check and add any missing remotes
for i := range s.config.Organizations {
org := &s.config.Organizations[i]
remoteName := s.getRemoteName(org)
-
+
// Check if remote exists
cmd := exec.Command("git", "-C", repoPath, "remote", "get-url", remoteName)
if err := cmd.Run(); err != nil {
@@ -95,7 +96,7 @@ func (s *Syncer) SyncRepository(repoName string) error {
return fmt.Errorf("failed to get current directory: %w", err)
}
defer os.Chdir(originalDir)
-
+
if err := os.Chdir(repoPath); err != nil {
return fmt.Errorf("failed to change to repository directory: %w", err)
}
@@ -135,24 +136,24 @@ func (s *Syncer) cloneRepository(org *config.Organization, repoPath string) erro
// For SSH URLs, the format is: git@host:org/repo.git
cloneURL = fmt.Sprintf("%s/%s.git", org.GetGitURL(), s.repoName)
}
-
+
fmt.Printf("Cloning from %s...\n", cloneURL)
-
+
cmd := exec.Command("git", "clone", cloneURL, repoPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
-
+
if err := cmd.Run(); err != nil {
return err
}
-
+
return nil
}
// addRemote adds a remote to the repository
func (s *Syncer) addRemote(repoPath string, org *config.Organization) error {
remoteName := s.getRemoteName(org)
-
+
// For file:// URLs, we need special handling
var remoteURL string
if strings.HasPrefix(org.Host, "file://") {
@@ -160,18 +161,19 @@ func (s *Syncer) addRemote(repoPath string, org *config.Organization) error {
} else {
remoteURL = fmt.Sprintf("%s/%s.git", org.GetGitURL(), s.repoName)
}
-
+
fmt.Printf("Adding remote %s: %s\n", remoteName, remoteURL)
-
+
cmd := exec.Command("git", "-C", repoPath, "remote", "add", remoteName, remoteURL)
if err := cmd.Run(); err != nil {
return err
}
-
+
return nil
}
// fetchAll fetches from all remotes
+// Note: We use individual fetches instead of --all to handle missing repositories gracefully
func (s *Syncer) fetchAll() error {
// First, check which remotes actually exist
cmd := exec.Command("git", "remote", "-v")
@@ -201,14 +203,14 @@ func (s *Syncer) fetchAll() error {
if err != nil {
// Check if it's because the repository doesn't exist
if strings.Contains(string(output), "does not appear to be a git repository") ||
- strings.Contains(string(output), "Could not read from remote repository") {
+ strings.Contains(string(output), "Could not read from remote repository") {
fmt.Printf(" Warning: Remote repository %s does not exist yet\n", remote)
continue
}
return fmt.Errorf("failed to fetch from %s: %w\n%s", remote, err, string(output))
}
}
-
+
return nil
}
@@ -219,16 +221,16 @@ func (s *Syncer) getAllBranches() ([]string, error) {
if err != nil {
return nil, err
}
-
+
branchMap := make(map[string]bool)
lines := strings.Split(string(output), "\n")
-
+
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.Contains(line, "->") {
continue
}
-
+
// Extract branch name from remote/branch format
parts := strings.SplitN(line, "/", 2)
if len(parts) == 2 {
@@ -236,13 +238,13 @@ func (s *Syncer) getAllBranches() ([]string, error) {
branchMap[branch] = true
}
}
-
+
// Convert map to slice
branches := make([]string, 0, len(branchMap))
for branch := range branchMap {
branches = append(branches, branch)
}
-
+
return branches, nil
}
@@ -255,7 +257,7 @@ func (s *Syncer) syncBranch(branch string, remotes map[string]*config.Organizati
// Track which remotes have this branch
remotesWithBranch := make(map[string]bool)
-
+
// Check which remotes have this branch
for remoteName := range remotes {
if s.remoteBranchExists(remoteName, branch) {
@@ -263,45 +265,52 @@ func (s *Syncer) syncBranch(branch string, remotes map[string]*config.Organizati
}
}
- // If no remotes have this branch, skip it
+ // If no remotes have this branch, it means it's a local branch that needs to be pushed
if len(remotesWithBranch) == 0 {
- fmt.Printf(" Branch %s not found on any remote, skipping\n", branch)
- return nil
- }
+ fmt.Printf(" Branch %s is local only, will push to all remotes\n", branch)
+ } else {
+ // Merge changes from all remotes that have this branch
+ for remoteName := range remotesWithBranch {
+ fmt.Printf(" Merging from %s/%s...\n", remoteName, branch)
- // Merge changes from all remotes that have this branch
- for remoteName := range remotesWithBranch {
- fmt.Printf(" Merging from %s/%s...\n", remoteName, branch)
-
- cmd := exec.Command("git", "merge", fmt.Sprintf("%s/%s", remoteName, branch), "--no-edit")
- output, err := cmd.CombinedOutput()
-
- if err != nil {
- // Check if it's a merge conflict
- if strings.Contains(string(output), "CONFLICT") {
- return fmt.Errorf("merge conflict detected when merging %s/%s. Please resolve manually", remoteName, branch)
+ cmd := exec.Command("git", "merge", fmt.Sprintf("%s/%s", remoteName, branch), "--no-edit")
+ output, err := cmd.CombinedOutput()
+
+ if err != nil {
+ // Check if it's a merge conflict
+ if strings.Contains(string(output), "CONFLICT") {
+ return fmt.Errorf("merge conflict detected when merging %s/%s. Please resolve manually", remoteName, branch)
+ }
+ return fmt.Errorf("failed to merge %s/%s: %w\n%s", remoteName, branch, err, string(output))
}
- return fmt.Errorf("failed to merge %s/%s: %w\n%s", remoteName, branch, err, string(output))
}
}
// Push to all remotes
for remoteName, org := range remotes {
- fmt.Printf(" Pushing to %s (%s)...\n", remoteName, org.Host)
-
+ // Check if this remote has the branch
+ remoteHasBranch := remotesWithBranch[remoteName]
+
+ if !remoteHasBranch {
+ fmt.Printf(" Creating branch on %s (%s)...\n", remoteName, org.Host)
+ } else {
+ fmt.Printf(" Pushing to %s (%s)...\n", remoteName, org.Host)
+ }
+
cmd := exec.Command("git", "push", remoteName, branch)
output, err := cmd.CombinedOutput()
-
+
if err != nil {
outputStr := string(output)
// Check if it's because the repository doesn't exist
if strings.Contains(outputStr, "does not appear to be a git repository") ||
- strings.Contains(outputStr, "Could not read from remote repository") {
- fmt.Printf(" Note: Remote repository %s does not exist - creating it first would be needed\n", remoteName)
- fmt.Printf(" Skipping push to %s (repository must be created manually)\n", remoteName)
+ strings.Contains(outputStr, "Could not read from remote repository") {
+ fmt.Printf(" Note: Remote repository %s does not exist - must be created manually\n", remoteName)
+ fmt.Printf(" Skipping push to %s\n", remoteName)
continue
}
// Check if it's because the branch doesn't exist on the remote
+ // This shouldn't happen with our logic, but keep it as a fallback
if strings.Contains(outputStr, "error: src refspec") {
fmt.Printf(" Creating new branch on %s\n", remoteName)
// Try again with -u flag to set upstream
@@ -312,6 +321,8 @@ func (s *Syncer) syncBranch(branch string, remotes map[string]*config.Organizati
} else {
return fmt.Errorf("failed to push to %s: %w\n%s", remoteName, err, outputStr)
}
+ } else if !remoteHasBranch {
+ fmt.Printf(" Successfully created branch %s on %s\n", branch, remoteName)
}
}
@@ -325,18 +336,18 @@ func (s *Syncer) checkoutBranch(branch string) error {
if err := cmd.Run(); err == nil {
return nil
}
-
+
// If that fails, create a new branch tracking the first remote that has it
for i := range s.config.Organizations {
org := &s.config.Organizations[i]
remoteName := s.getRemoteName(org)
-
+
if s.remoteBranchExists(remoteName, branch) {
cmd = exec.Command("git", "checkout", "-b", branch, fmt.Sprintf("%s/%s", remoteName, branch))
return cmd.Run()
}
}
-
+
return fmt.Errorf("branch %s not found on any remote", branch)
}
@@ -359,7 +370,7 @@ func (s *Syncer) getRemoteName(org *config.Organization) string {
host = strings.ReplaceAll(host, ":", "_")
host = strings.ReplaceAll(host, ".", "_")
host = strings.ReplaceAll(host, "/", "_")
-
+
// For file URLs, create a simpler name
if strings.HasPrefix(org.Host, "file://") {
// Get the last part of the path
@@ -368,6 +379,6 @@ func (s *Syncer) getRemoteName(org *config.Organization) string {
return parts[len(parts)-1]
}
}
-
+
return host
-} \ No newline at end of file
+}