diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/cli/description_sync.go | 71 | ||||
| -rw-r--r-- | internal/cli/description_sync_test.go | 57 | ||||
| -rw-r--r-- | internal/cli/sync_handlers.go | 14 | ||||
| -rw-r--r-- | internal/cli/sync_handlers_test.go | 19 | ||||
| -rw-r--r-- | internal/config/config.go | 17 | ||||
| -rw-r--r-- | internal/config/config_test.go | 22 | ||||
| -rw-r--r-- | internal/showcase/showcase.go | 12 | ||||
| -rw-r--r-- | internal/showcase/showcase_test.go | 19 | ||||
| -rw-r--r-- | internal/sync/backup_test.go | 63 | ||||
| -rw-r--r-- | internal/sync/branch_sync.go | 22 | ||||
| -rw-r--r-- | internal/sync/git_operations.go | 52 | ||||
| -rw-r--r-- | internal/sync/repository_setup.go | 12 | ||||
| -rw-r--r-- | internal/sync/sync.go | 72 | ||||
| -rw-r--r-- | internal/version/version.go | 2 |
14 files changed, 415 insertions, 39 deletions
diff --git a/internal/cli/description_sync.go b/internal/cli/description_sync.go index 4904b56..317bcdf 100644 --- a/internal/cli/description_sync.go +++ b/internal/cli/description_sync.go @@ -2,6 +2,10 @@ package cli import ( "fmt" + "os" + "os/exec" + "path" + "path/filepath" "strings" "codeberg.org/snonux/gitsyncer/internal/codeberg" @@ -105,8 +109,75 @@ func syncRepoDescriptions(cfg *config.Config, dryRun bool, repoName, knownCBDesc } } + syncBackupDescriptions(cfg, dryRun, repoName, canonical) + // Update cache if cache != nil { cache[repoName] = canonical } } + +func syncBackupDescriptions(cfg *config.Config, dryRun bool, repoName, canonical string) { + if cfg == nil || canonical == "" { + return + } + + for i := range cfg.Organizations { + org := &cfg.Organizations[i] + if !org.BackupLocation { + continue + } + + supported, err := syncBackupDescription(org, repoName, canonical, dryRun) + if err != nil { + fmt.Printf(" Warning: Failed to update backup description on %s: %v\n", org.Host, err) + continue + } + if supported && !dryRun { + fmt.Printf(" Updated backup description for %s on %s\n", repoName, org.Host) + } + } +} + +func syncBackupDescription(org *config.Organization, repoName, description string, dryRun bool) (bool, error) { + if org == nil || !org.BackupLocation { + return false, nil + } + + description = strings.TrimSpace(description) + if description == "" { + return false, nil + } + + if strings.HasPrefix(org.Host, "file://") { + descriptionPath := filepath.Join(strings.TrimPrefix(org.Host, "file://"), repoName+".git", "description") + if dryRun { + fmt.Printf(" [DRY RUN] Would update backup description for %s on %s -> %q\n", repoName, org.Host, description) + return true, nil + } + return true, os.WriteFile(descriptionPath, []byte(description+"\n"), 0644) + } + + if strings.TrimSpace(org.DescriptionSyncHost) == "" || strings.TrimSpace(org.DescriptionSyncRoot) == "" { + return false, nil + } + + descriptionPath := path.Join(org.DescriptionSyncRoot, repoName+".git", "description") + if dryRun { + fmt.Printf(" [DRY RUN] Would update backup description for %s on %s -> %q\n", repoName, org.Host, description) + return true, nil + } + + cmd := exec.Command("ssh", org.DescriptionSyncHost, fmt.Sprintf("cat > %s", shellSingleQuote(descriptionPath))) + cmd.Stdin = strings.NewReader(description + "\n") + output, err := cmd.CombinedOutput() + if err != nil { + return true, fmt.Errorf("ssh write failed: %w\n%s", err, strings.TrimSpace(string(output))) + } + + return true, nil +} + +func shellSingleQuote(value string) string { + return "'" + strings.ReplaceAll(value, "'", `'\''`) + "'" +} diff --git a/internal/cli/description_sync_test.go b/internal/cli/description_sync_test.go new file mode 100644 index 0000000..8f92349 --- /dev/null +++ b/internal/cli/description_sync_test.go @@ -0,0 +1,57 @@ +package cli + +import ( + "os" + "path/filepath" + "testing" + + "codeberg.org/snonux/gitsyncer/internal/config" +) + +func TestSyncBackupDescription_FileURLWritesDescription(t *testing.T) { + t.Parallel() + + rootDir := t.TempDir() + repoDir := filepath.Join(rootDir, "sample.git") + if err := os.MkdirAll(repoDir, 0755); err != nil { + t.Fatalf("mkdir repo dir: %v", err) + } + + org := &config.Organization{ + Host: "file://" + rootDir, + BackupLocation: true, + } + + supported, err := syncBackupDescription(org, "sample", "Sample description", false) + if err != nil { + t.Fatalf("syncBackupDescription() error = %v", err) + } + if !supported { + t.Fatal("expected file backup description sync to be supported") + } + + content, err := os.ReadFile(filepath.Join(repoDir, "description")) + if err != nil { + t.Fatalf("read description: %v", err) + } + if string(content) != "Sample description\n" { + t.Fatalf("description = %q, want %q", string(content), "Sample description\n") + } +} + +func TestSyncBackupDescription_SSHWithoutDescriptionSyncConfigIsUnsupported(t *testing.T) { + t.Parallel() + + org := &config.Organization{ + Host: "ssh://git@example.com/repos", + BackupLocation: true, + } + + supported, err := syncBackupDescription(org, "sample", "Sample description", false) + if err != nil { + t.Fatalf("syncBackupDescription() error = %v", err) + } + if supported { + t.Fatal("expected SSH backup description sync without config to be unsupported") + } +} diff --git a/internal/cli/sync_handlers.go b/internal/cli/sync_handlers.go index 6a96b92..e791b45 100644 --- a/internal/cli/sync_handlers.go +++ b/internal/cli/sync_handlers.go @@ -12,6 +12,14 @@ import ( "codeberg.org/snonux/gitsyncer/internal/sync" ) +func shouldEnableBackupSync(flags *Flags) bool { + if flags == nil { + return false + } + + return flags.Backup || flags.FullSync +} + // HandleSync handles syncing a single repository func HandleSync(cfg *config.Config, flags *Flags) int { stateManager, syncState, err := loadSyncState(flags.WorkDir) @@ -50,7 +58,7 @@ func HandleSync(cfg *config.Config, flags *Flags) int { } syncer := sync.New(cfg, flags.WorkDir) - syncer.SetBackupEnabled(flags.Backup) + syncer.SetBackupEnabled(shouldEnableBackupSync(flags)) if err := syncer.SyncRepository(flags.SyncRepo); err != nil { fmt.Printf("ERROR: Sync failed: %v\n", err) return 1 @@ -107,7 +115,7 @@ func HandleSyncAll(cfg *config.Config, flags *Flags) int { } syncer := sync.New(cfg, flags.WorkDir) - syncer.SetBackupEnabled(flags.Backup) + syncer.SetBackupEnabled(shouldEnableBackupSync(flags)) successCount := 0 // Load descriptions cache descCache := loadDescriptionCache(flags.WorkDir) @@ -421,7 +429,7 @@ func newSyncExecution(cfg *config.Config, flags *Flags) *syncExecution { descCache: loadDescriptionCache(flags.WorkDir), syncer: sync.New(cfg, flags.WorkDir), } - execution.syncer.SetBackupEnabled(flags.Backup) + execution.syncer.SetBackupEnabled(shouldEnableBackupSync(flags)) manager, st, err := loadSyncState(flags.WorkDir) if err != nil { diff --git a/internal/cli/sync_handlers_test.go b/internal/cli/sync_handlers_test.go new file mode 100644 index 0000000..2eaad67 --- /dev/null +++ b/internal/cli/sync_handlers_test.go @@ -0,0 +1,19 @@ +package cli + +import "testing" + +func TestShouldEnableBackupSync_FullSyncImplicitlyEnablesBackup(t *testing.T) { + t.Parallel() + + if !shouldEnableBackupSync(&Flags{FullSync: true}) { + t.Fatal("expected full sync to enable backup sync implicitly") + } + + if !shouldEnableBackupSync(&Flags{Backup: true}) { + t.Fatal("expected explicit --backup to enable backup sync") + } + + if shouldEnableBackupSync(&Flags{}) { + t.Fatal("did not expect backup sync to be enabled by default") + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 4e40cdf..02f2fee 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,11 +10,13 @@ import ( // Organization represents a git organization with its host and name type Organization struct { - Host string `json:"host"` - Name string `json:"name"` - GitHubToken string `json:"github_token,omitempty"` - CodebergToken string `json:"codeberg_token,omitempty"` - BackupLocation bool `json:"backupLocation,omitempty"` // Mark this as a backup-only destination + Host string `json:"host"` + Name string `json:"name"` + GitHubToken string `json:"github_token,omitempty"` + CodebergToken string `json:"codeberg_token,omitempty"` + BackupLocation bool `json:"backupLocation,omitempty"` // Mark this as a backup-only destination + DescriptionSyncHost string `json:"descriptionSyncHost,omitempty"` // SSH host with shell access for updating backup descriptions + DescriptionSyncRoot string `json:"descriptionSyncRoot,omitempty"` // Filesystem path on DescriptionSyncHost where bare repos live } // Config holds the application configuration @@ -101,6 +103,11 @@ func (c *Config) Validate() error { if org.Name == "" && !strings.HasPrefix(org.Host, "file://") && !org.IsSSH() { return fmt.Errorf("organization %d: missing name", i) } + hasDescriptionSyncHost := strings.TrimSpace(org.DescriptionSyncHost) != "" + hasDescriptionSyncRoot := strings.TrimSpace(org.DescriptionSyncRoot) != "" + if hasDescriptionSyncHost != hasDescriptionSyncRoot { + return fmt.Errorf("organization %d: descriptionSyncHost and descriptionSyncRoot must be set together", i) + } } for repo, branch := range c.ShowcaseStatsBranches { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index db70457..30b59df 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -25,3 +25,25 @@ func TestValidate_ShowcaseStatsBranchesRejectsEmptyBranch(t *testing.T) { t.Fatalf("Validate() error = %q, want showcase_stats_branches context", err) } } + +func TestValidate_DescriptionSyncFieldsMustBePaired(t *testing.T) { + t.Parallel() + + cfg := &Config{ + Organizations: []Organization{ + { + Host: "ssh://git@example.com/repos", + BackupLocation: true, + DescriptionSyncHost: "root@example.com", + }, + }, + } + + err := cfg.Validate() + if err == nil { + t.Fatal("Validate() error = nil, want description sync validation error") + } + if !strings.Contains(err.Error(), "descriptionSyncHost") { + t.Fatalf("Validate() error = %q, want descriptionSyncHost context", err) + } +} diff --git a/internal/showcase/showcase.go b/internal/showcase/showcase.go index 9cf43a6..c6154f9 100644 --- a/internal/showcase/showcase.go +++ b/internal/showcase/showcase.go @@ -27,6 +27,7 @@ type ProjectSummary struct { Summary string CodebergURL string GitHubURL string + CgitURL string Metadata *RepoMetadata RankHistory []RepoRankHistory // Latest 5 weekly rank points, newest first Images []string // Relative paths to images in showcase directory @@ -669,9 +670,10 @@ func (g *Generator) getRepositories() ([]string, error) { return repos, nil } -func (g *Generator) buildProjectLinks(repoName string) (string, string) { +func (g *Generator) buildProjectLinks(repoName string) (string, string, string) { codebergURL := "" githubURL := "" + cgitURL := fmt.Sprintf("https://cgit.f3s.buetow.org/%s/", repoName) if codebergOrg := g.config.FindCodebergOrg(); codebergOrg != nil { codebergURL = fmt.Sprintf("https://codeberg.org/%s/%s", codebergOrg.Name, repoName) @@ -681,7 +683,7 @@ func (g *Generator) buildProjectLinks(repoName string) (string, string) { githubURL = fmt.Sprintf("https://github.com/%s/%s", githubOrg.Name, repoName) } - return codebergURL, githubURL + return codebergURL, githubURL, cgitURL } func (g *Generator) prepareStatsRepoPath(repoName, repoPath string) (string, func() error, error) { @@ -833,7 +835,7 @@ func (g *Generator) generateProjectSummary(repoName string, forceRegenerate bool summary = sanitizeSummaryForGemtext(summary) // Build URLs - codebergURL, githubURL := g.buildProjectLinks(repoName) + codebergURL, githubURL, cgitURL := g.buildProjectLinks(repoName) // Always extract images from README (not cached) fmt.Printf("Extracting images from README...\n") @@ -865,6 +867,7 @@ func (g *Generator) generateProjectSummary(repoName string, forceRegenerate bool Summary: summary, CodebergURL: codebergURL, GitHubURL: githubURL, + CgitURL: cgitURL, Metadata: metadata, Images: images, CodeSnippet: codeSnippet, @@ -1078,6 +1081,9 @@ func (g *Generator) formatGemtext(summaries []ProjectSummary) string { if summary.GitHubURL != "" { builder.WriteString(fmt.Sprintf("=> %s View on GitHub\n", summary.GitHubURL)) } + if summary.CgitURL != "" { + builder.WriteString(fmt.Sprintf("=> %s View in cgit\n", summary.CgitURL)) + } } diff --git a/internal/showcase/showcase_test.go b/internal/showcase/showcase_test.go index fa18799..cd571f8 100644 --- a/internal/showcase/showcase_test.go +++ b/internal/showcase/showcase_test.go @@ -148,6 +148,25 @@ func TestFormatGemtext_SanitizesMarkdownHeadingsInSummary(t *testing.T) { } } +func TestFormatGemtext_IncludesCgitLink(t *testing.T) { + t.Parallel() + + g := &Generator{config: &config.Config{}} + content := g.formatGemtext([]ProjectSummary{ + { + Name: "cpuinfo", + Summary: "summary", + CodebergURL: "https://codeberg.org/snonux/cpuinfo", + GitHubURL: "https://github.com/snonux/cpuinfo", + CgitURL: "https://cgit.f3s.buetow.org/cpuinfo/", + }, + }) + + if !strings.Contains(content, "=> https://cgit.f3s.buetow.org/cpuinfo/ View in cgit\n") { + t.Fatalf("cgit link was not rendered: %s", content) + } +} + func TestFindReadmeContent_UsesRepoPathWithoutChangingCWD(t *testing.T) { t.Parallel() diff --git a/internal/sync/backup_test.go b/internal/sync/backup_test.go new file mode 100644 index 0000000..9bdbff3 --- /dev/null +++ b/internal/sync/backup_test.go @@ -0,0 +1,63 @@ +package sync + +import ( + "errors" + "testing" + + "codeberg.org/snonux/gitsyncer/internal/config" +) + +func TestHandlePushError_DisablesBackupForSession(t *testing.T) { + resetBackupSessionState() + t.Cleanup(resetBackupSessionState) + + syncer := &Syncer{} + syncer.SetBackupEnabled(true) + + err := syncer.handlePushError("backup", &config.Organization{BackupLocation: true}, errors.New("dial tcp: connection refused")) + if err != nil { + t.Fatalf("expected backup push failure to be downgraded, got %v", err) + } + if syncer.backupActive() { + t.Fatal("expected backup sync to be disabled for the remainder of the session") + } +} + +func TestHandlePushError_PropagatesPrimaryRemoteFailure(t *testing.T) { + resetBackupSessionState() + t.Cleanup(resetBackupSessionState) + + syncer := &Syncer{} + syncer.SetBackupEnabled(true) + + pushErr := errors.New("push rejected") + err := syncer.handlePushError("origin", &config.Organization{}, pushErr) + if !errors.Is(err, pushErr) { + t.Fatalf("expected primary remote error to be returned, got %v", err) + } +} + +func TestParseSSHLocation_SupportsSSHURLWithPort(t *testing.T) { + t.Parallel() + + userHost, sshArgs, basePath, err := parseSSHLocation("ssh://git@r0:30022/repos") + if err != nil { + t.Fatalf("parseSSHLocation() error = %v", err) + } + if userHost != "git@r0" { + t.Fatalf("userHost = %q, want %q", userHost, "git@r0") + } + if basePath != "/repos" { + t.Fatalf("basePath = %q, want %q", basePath, "/repos") + } + + wantArgs := []string{"-p", "30022", "git@r0"} + if len(sshArgs) != len(wantArgs) { + t.Fatalf("sshArgs = %#v, want %#v", sshArgs, wantArgs) + } + for i := range wantArgs { + if sshArgs[i] != wantArgs[i] { + t.Fatalf("sshArgs = %#v, want %#v", sshArgs, wantArgs) + } + } +} diff --git a/internal/sync/branch_sync.go b/internal/sync/branch_sync.go index a667ee2..a053e78 100644 --- a/internal/sync/branch_sync.go +++ b/internal/sync/branch_sync.go @@ -40,9 +40,27 @@ func mergeFromRemotes(repoPath, branch string, remotesWithBranch map[string]bool return nil } +// handlePushError decides whether a push error should stop sync or only disable backup for this session. +func (s *Syncer) handlePushError(remoteName string, org *config.Organization, err error) error { + if err == nil { + return nil + } + + if org != nil && org.BackupLocation { + s.disableBackupForSession(remoteName, err) + return nil + } + + return err +} + // pushToAllRemotes pushes the branch to all configured remotes -func pushToAllRemotes(repoPath, branch string, remotes map[string]*config.Organization, remotesWithBranch map[string]bool) error { +func (s *Syncer) pushToAllRemotes(repoPath, branch string, remotes map[string]*config.Organization, remotesWithBranch map[string]bool) error { for remoteName, org := range remotes { + if org.BackupLocation && !s.backupActive() { + continue + } + // Check if this remote has the branch remoteHasBranch := remotesWithBranch[remoteName] @@ -52,7 +70,7 @@ func pushToAllRemotes(repoPath, branch string, remotes map[string]*config.Organi fmt.Printf(" Pushing to %s (%s)...\n", remoteName, org.Host) } - if err := pushBranchWithBackupSupport(repoPath, remoteName, branch, remoteHasBranch, org); err != nil { + if err := s.handlePushError(remoteName, org, pushBranchWithBackupSupport(repoPath, remoteName, branch, remoteHasBranch, org)); err != nil { return err } } diff --git a/internal/sync/git_operations.go b/internal/sync/git_operations.go index 62a530b..6619349 100644 --- a/internal/sync/git_operations.go +++ b/internal/sync/git_operations.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "fmt" + "net/url" "os/exec" "regexp" "strings" @@ -245,23 +246,19 @@ func getAllUniqueBranches(output []byte) []string { // 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, sshArgs, basePath, err := parseSSHLocation(sshHost) + if err != nil { + return err } - userHost := parts[0] - basePath := parts[1] - // Full path to the repository - fullRepoPath := fmt.Sprintf("%s/%s.git", basePath, repoPath) + fullRepoPath := strings.TrimRight(basePath, "/") + "/" + repoPath + ".git" 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) + commands := fmt.Sprintf("mkdir -p %q && cd %q && git init --bare", fullRepoPath, fullRepoPath) + cmd := exec.Command("ssh", append(sshArgs, commands)...) output, err := cmd.CombinedOutput() if err != nil { @@ -272,6 +269,41 @@ func createSSHBareRepository(sshHost, repoPath string) error { return nil } +func parseSSHLocation(sshHost string) (string, []string, string, error) { + if strings.HasPrefix(sshHost, "ssh://") { + parsed, err := url.Parse(sshHost) + if err != nil { + return "", nil, "", fmt.Errorf("invalid SSH host format: %w", err) + } + + host := parsed.Hostname() + if host == "" || parsed.Path == "" { + return "", nil, "", fmt.Errorf("invalid SSH host format: %s", sshHost) + } + + userHost := host + if parsed.User != nil && parsed.User.Username() != "" { + userHost = parsed.User.Username() + "@" + host + } + + sshArgs := make([]string, 0, 3) + if port := parsed.Port(); port != "" { + sshArgs = append(sshArgs, "-p", port) + } + sshArgs = append(sshArgs, userHost) + + return userHost, sshArgs, parsed.Path, nil + } + + parts := strings.SplitN(sshHost, ":", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", nil, "", fmt.Errorf("invalid SSH host format: %s", sshHost) + } + + userHost := parts[0] + return userHost, []string{userHost}, parts[1], nil +} + // pushBranchWithBackupSupport pushes a branch to a remote, creating SSH repos if needed func pushBranchWithBackupSupport(repoPath, remoteName, branch string, remoteHasBranch bool, org *config.Organization) error { cmd := gitCommand(repoPath, "push", remoteName, branch, "--tags") diff --git a/internal/sync/repository_setup.go b/internal/sync/repository_setup.go index e255274..9bb0c2f 100644 --- a/internal/sync/repository_setup.go +++ b/internal/sync/repository_setup.go @@ -55,8 +55,8 @@ func (s *Syncer) setupNewRepository(repoPath string) error { } org := &s.config.Organizations[i] - // Skip backup locations if backup is not enabled - if org.BackupLocation && !s.backupEnabled { + // Skip backup locations unless backup sync is currently active. + if org.BackupLocation && !s.backupActive() { continue } @@ -76,8 +76,8 @@ func (s *Syncer) setupExistingRepository(repoPath string) error { for i := range s.config.Organizations { org := &s.config.Organizations[i] - // Skip backup locations if backup is not enabled - if org.BackupLocation && !s.backupEnabled { + // Skip backup locations unless backup sync is currently active. + if org.BackupLocation && !s.backupActive() { continue } @@ -102,8 +102,8 @@ func (s *Syncer) getRemotesMap() 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 { + // Skip backup locations unless backup sync is currently active. + if org.BackupLocation && !s.backupActive() { continue } diff --git a/internal/sync/sync.go b/internal/sync/sync.go index ba03e0f..e0a8215 100644 --- a/internal/sync/sync.go +++ b/internal/sync/sync.go @@ -6,10 +6,19 @@ import ( "os/exec" "path/filepath" "strings" + stdsync "sync" "codeberg.org/snonux/gitsyncer/internal/config" ) +type backupSessionState struct { + mu stdsync.Mutex + disabled bool + reason string +} + +var currentBackupSession backupSessionState + // Syncer handles repository synchronization between organizations type Syncer struct { config *config.Config @@ -45,6 +54,55 @@ func (s *Syncer) SetBackupEnabled(enabled bool) { s.backupEnabled = enabled } +func (s *Syncer) backupActive() bool { + if !s.backupEnabled { + return false + } + + disabled, _ := currentBackupSession.status() + return !disabled +} + +func (s *Syncer) disableBackupForSession(remoteName string, err error) { + if !s.backupEnabled { + return + } + + reason := fmt.Sprintf("%s: %v", remoteName, err) + if currentBackupSession.disable(reason) { + fmt.Printf("Warning: Backup sync to %s failed: %v\n", remoteName, err) + fmt.Println("Warning: Disabling backup sync for the remainder of this session.") + } +} + +func (b *backupSessionState) disable(reason string) bool { + b.mu.Lock() + defer b.mu.Unlock() + + if b.disabled { + return false + } + + b.disabled = true + b.reason = reason + return true +} + +func (b *backupSessionState) status() (bool, string) { + b.mu.Lock() + defer b.mu.Unlock() + + return b.disabled, b.reason +} + +func resetBackupSessionState() { + currentBackupSession.mu.Lock() + defer currentBackupSession.mu.Unlock() + + currentBackupSession.disabled = false + currentBackupSession.reason = "" +} + // SyncRepository synchronizes a repository across all configured organizations func (s *Syncer) SyncRepository(repoName string) error { s.repoName = repoName @@ -234,7 +292,7 @@ func (s *Syncer) fetchAll() error { for remote := range remotes { // Check if this remote is a backup location if org, exists := allOrgsMap[remote]; exists && org.BackupLocation { - if !s.backupEnabled { + if !s.backupActive() { // Silently skip - don't even print a message since backup is not enabled continue } @@ -260,13 +318,9 @@ 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 + // Backup remotes are push-only and must never influence branch discovery. + filteredOutput := s.filterBackupBranches(output) + return getAllUniqueBranches(filteredOutput), nil } // syncBranch synchronizes a specific branch across all remotes @@ -296,7 +350,7 @@ func (s *Syncer) syncBranch(branch string, remotes map[string]*config.Organizati } // Push to all remotes - return pushToAllRemotes(repoPath, branch, remotes, remotesWithBranch) + return s.pushToAllRemotes(repoPath, branch, remotes, remotesWithBranch) } // handleWorkingDirectoryState checks for conflicts and stashes changes if needed diff --git a/internal/version/version.go b/internal/version/version.go index afd3d4e..189f928 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -7,7 +7,7 @@ import ( var ( // Version is the current version of gitsyncer - Version = "0.16.0" + Version = "0.17.0" // GitCommit is the git commit hash at build time GitCommit = "unknown" |
