mirror of
https://github.com/gabrie30/ghorg.git
synced 2026-01-21 16:21:43 +01:00
- Add --sync-default-branch flag to clone command for enabling sync feature via CLI - Add syncDefaultBranch variable and flag handler in cmd package - Refactor git/sync.go to use colorlog consistently throughout - Convert all fmt.Printf debug statements to colorlog.PrintInfo/PrintSuccess/PrintError - Remove all GHORG_DEBUG environment variable checks - Return formatted errors with context using fmt.Errorf for better error messages - Add comprehensive tests for sync-default-branch flag - Test default value is false - Test flag sets environment variable correctly - All existing tests continue to pass
160 lines
5.8 KiB
Go
160 lines
5.8 KiB
Go
// Package git provides Git repository synchronization functionality for ghorg.
|
|
//
|
|
// For comprehensive documentation on sync functionality, safety philosophy,
|
|
// configuration options, and troubleshooting, see README.md in this directory.
|
|
package git
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/gabrie30/ghorg/colorlog"
|
|
"github.com/gabrie30/ghorg/scm"
|
|
)
|
|
|
|
// SyncDefaultBranch synchronizes the local default branch with the remote
|
|
// It checks for local changes and unpushed commits before performing the sync
|
|
func (g GitClient) SyncDefaultBranch(repo scm.Repo) error {
|
|
// Check if sync is disabled via configuration
|
|
// GHORG_SYNC_DEFAULT_BRANCH defaults to false (sync disabled by default)
|
|
syncEnabled := os.Getenv("GHORG_SYNC_DEFAULT_BRANCH")
|
|
if syncEnabled != "true" {
|
|
m := fmt.Sprintf("Skipping sync for %s: GHORG_SYNC_DEFAULT_BRANCH is not set to true\n", repo.Name)
|
|
colorlog.PrintInfo(m)
|
|
return nil
|
|
}
|
|
|
|
// First check if the remote exists and is accessible
|
|
_, err := g.GetRemoteURL(repo, "origin")
|
|
if err != nil {
|
|
// Remote doesn't exist or isn't accessible, skip sync
|
|
return nil
|
|
}
|
|
|
|
// Check if the working directory has any uncommitted changes
|
|
hasWorkingDirChanges, err := g.HasLocalChanges(repo)
|
|
if err != nil {
|
|
m := fmt.Sprintf("Failed to check working directory status for %s: %v", repo.Name, err)
|
|
colorlog.PrintError(m)
|
|
return fmt.Errorf("failed to check working directory status: %w", err)
|
|
}
|
|
|
|
// Check if the current branch has unpushed commits
|
|
hasUnpushedCommits, err := g.HasUnpushedCommits(repo)
|
|
if err != nil {
|
|
m := fmt.Sprintf("Failed to check for unpushed commits for %s: %v", repo.Name, err)
|
|
colorlog.PrintError(m)
|
|
return fmt.Errorf("failed to check for unpushed commits: %w", err)
|
|
}
|
|
|
|
// Check if we're on the correct branch
|
|
currentBranch, err := g.GetCurrentBranch(repo)
|
|
if err != nil {
|
|
m := fmt.Sprintf("Failed to get current branch for %s: %v", repo.Name, err)
|
|
colorlog.PrintError(m)
|
|
return fmt.Errorf("failed to get current branch: %w", err)
|
|
}
|
|
|
|
// Check if current branch has commits not on the default branch (divergent development)
|
|
hasCommitsNotOnDefault, err := g.HasCommitsNotOnDefaultBranch(repo, currentBranch)
|
|
if err != nil {
|
|
m := fmt.Sprintf("Failed to check for commits not on default branch for %s: %v", repo.Name, err)
|
|
colorlog.PrintError(m)
|
|
return fmt.Errorf("failed to check for commits not on default branch: %w", err)
|
|
}
|
|
|
|
// Check if the default branch is behind HEAD (missing commits from current branch)
|
|
isDefaultBehindHead, err := g.IsDefaultBranchBehindHead(repo, currentBranch)
|
|
if err != nil {
|
|
m := fmt.Sprintf("Failed to check if default branch is behind HEAD for %s: %v", repo.Name, err)
|
|
colorlog.PrintError(m)
|
|
return fmt.Errorf("failed to check if default branch is behind HEAD: %w", err)
|
|
}
|
|
|
|
// Only sync if:
|
|
// 1. Working directory is clean (no uncommitted changes)
|
|
// 2. No unpushed commits on the current branch
|
|
// 3. Either:
|
|
// a. No commits on current branch that aren't on the default branch, OR
|
|
// b. Default branch is behind HEAD (we can fast-forward merge)
|
|
// 4. We're on the target branch or can safely switch to it
|
|
if hasWorkingDirChanges {
|
|
m := fmt.Sprintf("Skipping sync for %s: working directory has uncommitted changes\n", repo.Name)
|
|
colorlog.PrintInfo(m)
|
|
return nil
|
|
}
|
|
|
|
if hasUnpushedCommits {
|
|
m := fmt.Sprintf("Skipping sync for %s: branch has unpushed commits\n", repo.Name)
|
|
colorlog.PrintInfo(m)
|
|
return nil
|
|
}
|
|
|
|
// Allow sync if default branch is behind HEAD (can fast-forward merge)
|
|
// or if there are no commits not on default branch
|
|
if hasCommitsNotOnDefault && !isDefaultBehindHead {
|
|
m := fmt.Sprintf("Skipping sync for %s: current branch has commits not on default branch and default is not behind\n", repo.Name)
|
|
colorlog.PrintInfo(m)
|
|
return nil
|
|
}
|
|
|
|
// Switch to the target branch if we're not already on it
|
|
if currentBranch != repo.CloneBranch {
|
|
err := g.Checkout(repo)
|
|
if err != nil {
|
|
m := fmt.Sprintf("Could not checkout %s for %s: %v", repo.CloneBranch, repo.Name, err)
|
|
colorlog.PrintError(m)
|
|
return nil // Don't fail, just skip sync
|
|
}
|
|
}
|
|
|
|
// Fetch the latest changes from the remote
|
|
err = g.FetchCloneBranch(repo)
|
|
if err != nil {
|
|
m := fmt.Sprintf("Failed to fetch default branch for %s: %v", repo.Name, err)
|
|
colorlog.PrintError(m)
|
|
return fmt.Errorf("failed to fetch default branch: %w", err)
|
|
}
|
|
|
|
// If the default branch is behind HEAD and we have commits to merge,
|
|
// perform a fast-forward merge
|
|
if isDefaultBehindHead && hasCommitsNotOnDefault {
|
|
m := fmt.Sprintf("Default branch is behind HEAD for %s, performing fast-forward merge", repo.Name)
|
|
colorlog.PrintInfo(m)
|
|
|
|
err = g.MergeIntoDefaultBranch(repo, currentBranch)
|
|
if err != nil {
|
|
m := fmt.Sprintf("Failed to merge into default branch for %s: %v", repo.Name, err)
|
|
colorlog.PrintError(m)
|
|
return fmt.Errorf("failed to merge into default branch: %w", err)
|
|
}
|
|
|
|
m = fmt.Sprintf("Successfully updated default branch %s by merging %s for %s", repo.CloneBranch, currentBranch, repo.Name)
|
|
colorlog.PrintSuccess(m)
|
|
return nil
|
|
}
|
|
|
|
// Update the local branch reference to match the remote
|
|
refName := fmt.Sprintf("refs/heads/%s", repo.CloneBranch)
|
|
commitRef := fmt.Sprintf("refs/remotes/origin/%s", repo.CloneBranch)
|
|
err = g.UpdateRef(repo, refName, commitRef)
|
|
if err != nil {
|
|
m := fmt.Sprintf("Failed to update branch reference for %s: %v", repo.Name, err)
|
|
colorlog.PrintError(m)
|
|
return fmt.Errorf("failed to update branch reference: %w", err)
|
|
}
|
|
|
|
// Reset the working directory to match the updated branch
|
|
err = g.Reset(repo)
|
|
if err != nil {
|
|
m := fmt.Sprintf("Failed to reset working directory to remote branch for %s: %v", repo.Name, err)
|
|
colorlog.PrintError(m)
|
|
return fmt.Errorf("failed to reset working directory to remote branch: %w", err)
|
|
}
|
|
|
|
m := fmt.Sprintf("Successfully updated default branch %s for %s", repo.CloneBranch, repo.Name)
|
|
colorlog.PrintSuccess(m)
|
|
|
|
return nil
|
|
}
|