From d980126473d31057e40510cee262d328215aadc7 Mon Sep 17 00:00:00 2001 From: Simon Cornet Date: Wed, 10 Dec 2025 23:32:27 +0100 Subject: [PATCH] feat: use go-git instead of "system git" --- cmd/gogitlabber/git.go | 239 ++++++++++++++++++-------------- cmd/gogitlabber/gitea.go | 5 +- cmd/gogitlabber/gitlab.go | 5 +- cmd/gogitlabber/input.go | 28 ++-- cmd/gogitlabber/main.go | 24 ++-- cmd/gogitlabber/output.go | 8 +- cmd/gogitlabber/requirements.go | 15 -- go.mod | 27 +++- go.sum | 93 +++++++++++++ 9 files changed, 282 insertions(+), 162 deletions(-) delete mode 100644 cmd/gogitlabber/requirements.go diff --git a/cmd/gogitlabber/git.go b/cmd/gogitlabber/git.go index 70194fd..601e71b 100644 --- a/cmd/gogitlabber/git.go +++ b/cmd/gogitlabber/git.go @@ -2,11 +2,12 @@ package main import ( "fmt" - "os/exec" + "os" "path/filepath" - "strings" "sync" + "github.com/go-git/go-git/v6" + "github.com/go-git/go-git/v6/plumbing/transport/http" "github.com/scornet256/go-logger" ) @@ -31,7 +32,6 @@ type GitStats struct { // increment counters func (stats *GitStats) IncrementCounter(operation string, repoPath string) { - stats.mu.Lock() defer stats.mu.Unlock() @@ -54,9 +54,8 @@ func (stats *GitStats) IncrementCounter(operation string, repoPath string) { // concurrent git operations func CheckoutRepositories(repositories []Repository, stats *GitStats) { - var wg sync.WaitGroup - semaphore := make(chan struct{}, config.Concurrency) + semaphore := make(chan struct{}, globalConfig.Concurrency) for _, repo := range repositories { wg.Add(1) @@ -79,67 +78,67 @@ func CheckoutRepositories(repositories []Repository, stats *GitStats) { // manage single repo func processRepository(repo Repository) GitOperationResult { repoName := string(repo.PathWithNamespace) - repoDestination := filepath.Join(config.Destination, repoName) + repoDestination := filepath.Join(globalConfig.Destination, repoName) logger.Print("Starting on repository: "+repoName, nil) - // check repo status - status, err := checkRepositoryStatus(repoDestination) + // check if repo exists + _, err := git.PlainOpen(repoDestination) if err != nil { + if err == git.ErrRepositoryNotExists { + // repo doesn't exist, clone it + gitURL := buildGitURL(repoName) + return cloneRepository(repoName, repoDestination, gitURL) + } return GitOperationResult{ RepoName: repoName, Operation: "error", - Error: fmt.Errorf("checking repository status: %w", err), + Error: fmt.Errorf("opening repository: %w", err), } } - gitURL := buildGitURL(repoName) - - switch { - case strings.Contains(status, "No such file or directory"): - return cloneRepository(repoName, repoDestination, gitURL) - case strings.Contains(status, gitURL): - return pullRepository(repoName, repoDestination) - default: - return GitOperationResult{ - RepoName: repoName, - Operation: "error", - Error: fmt.Errorf("unexpected repository status: %s", status), - } - } + // repo exists, pull it + return pullRepository(repoName, repoDestination) } -// check repo status -func checkRepositoryStatus(repoDestination string) (string, error) { - cmd := exec.Command("git", "-C", repoDestination, "remote", "-v") - output, err := cmd.CombinedOutput() - - // If directory doesn't exist, that's expected for new clones - if err != nil && strings.Contains(string(output), "No such file or directory") { - return string(output), nil - } - - return string(output), err -} - -// craft git url with auth +// craft git url without auth (auth handled separately) func buildGitURL(repoName string) string { - return fmt.Sprintf("https://%s-token:%s@%s/%s.git", - config.GitBackend, config.GitToken, config.GitHost, repoName) + return fmt.Sprintf("https://%s/%s.git", globalConfig.GitHost, repoName) +} + +// create auth method +func getAuth() *http.BasicAuth { + return &http.BasicAuth{ + Username: globalConfig.GitBackend + "-token", + Password: globalConfig.GitToken, + } } // clone new repository func cloneRepository(repoName, repoDestination, gitURL string) GitOperationResult { logger.Print("Cloning repository: "+repoName, nil) - cmd := exec.Command("git", "clone", gitURL, repoDestination) - output, err := cmd.CombinedOutput() + // ensure parent directory exists + parentDir := filepath.Dir(repoDestination) + if err := os.MkdirAll(parentDir, 0755); err != nil { + return GitOperationResult{ + RepoName: repoName, + Operation: "error", + Error: fmt.Errorf("creating parent directory: %w", err), + } + } + + _, err := git.PlainClone(repoDestination, &git.CloneOptions{ + URL: gitURL, + Auth: getAuth(), + Progress: nil, + }) if err != nil { return GitOperationResult{ RepoName: repoName, Operation: "error", - Error: fmt.Errorf("cloning repository: %w, output: %s", err, string(output)), + Error: fmt.Errorf("cloning repository: %w", err), } } @@ -158,27 +157,77 @@ func cloneRepository(repoName, repoDestination, gitURL string) GitOperationResul func pullRepository(repoName, repoDestination string) GitOperationResult { logger.Print("Pulling repository: "+repoName, nil) - // Find remote - remote, err := findRemote(repoDestination) + // open repository + repo, err := git.PlainOpen(repoDestination) if err != nil { return GitOperationResult{ RepoName: repoName, Operation: "error", - Error: fmt.Errorf("finding remote: %w", err), + Error: fmt.Errorf("opening repository: %w", err), + } + } + + // update remote URL with current token (in case token changed) + gitURL := buildGitURL(repoName) + if err := updateRemoteURL(repoDestination, gitURL); err != nil { + logger.Print("WARNING: failed to update remote URL: "+err.Error(), nil) + } + + // get worktree + worktree, err := repo.Worktree() + if err != nil { + return GitOperationResult{ + RepoName: repoName, + Operation: "error", + Error: fmt.Errorf("getting worktree: %w", err), + } + } + + // check for uncommitted/unstaged changes + status, err := worktree.Status() + if err != nil { + return GitOperationResult{ + RepoName: repoName, + Operation: "error", + Error: fmt.Errorf("checking status: %w", err), + } + } + + if !status.IsClean() { + // determine error type + errorType := "unstaged" + for _, s := range status { + if s.Staging != git.Unmodified { + errorType = "uncommitted" + break + } + } + + return GitOperationResult{ + RepoName: repoName, + Operation: "error", + Error: fmt.Errorf("repository has local changes"), + ErrorType: errorType, } } // pull changes - cmd := exec.Command("git", "-C", repoDestination, "pull", remote) - output, err := cmd.CombinedOutput() + err = worktree.Pull(&git.PullOptions{ + Auth: getAuth(), + Progress: nil, + }) if err != nil { - errorType := classifyPullError(string(output)) - return GitOperationResult{ - RepoName: repoName, - Operation: "error", - Error: fmt.Errorf("pulling repository: %w, output: %s", err, string(output)), - ErrorType: errorType, + if err == git.NoErrAlreadyUpToDate { + // not an error, just already up to date + logger.Print("Repository already up to date: "+repoName, nil) + } else { + return GitOperationResult{ + RepoName: repoName, + Operation: "error", + Error: fmt.Errorf("pulling repository: %w", err), + ErrorType: "other", + } } } @@ -193,73 +242,51 @@ func pullRepository(repoName, repoDestination string) GitOperationResult { } } -// find remote for repo -func findRemote(repoDestination string) (string, error) { - cmd := exec.Command("git", "-C", repoDestination, "remote", "show") - output, err := cmd.CombinedOutput() - if err != nil { - return "", fmt.Errorf("getting remote: %w", err) - } - - remotes := strings.Split(strings.TrimSpace(string(output)), "\n") - if len(remotes) == 0 { - return "", fmt.Errorf("no remotes found") - } - - return remotes[0], nil -} - -// manage pull error -func classifyPullError(output string) string { - switch { - case strings.Contains(output, "You have unstaged changes"): - return "unstaged" - case strings.Contains(output, "Your index contains uncommitted changes"): - return "uncommitted" - default: - return "other" - } -} - // set git user config func setGitUserConfig(repoName, repoDestination string) error { - - // git user name - if err := setGitUserName(repoName, repoDestination); err != nil { - return fmt.Errorf("setting username: %w", err) + repo, err := git.PlainOpen(repoDestination) + if err != nil { + return fmt.Errorf("opening repository: %w", err) } - // git user mail - if err := setGitUserEmail(repoName, repoDestination); err != nil { - return fmt.Errorf("setting email: %w", err) + cfg, err := repo.Config() + if err != nil { + return fmt.Errorf("getting config: %w", err) } + cfg.User.Name = globalConfig.GitUserName + cfg.User.Email = globalConfig.GitUserMail + + if err := repo.SetConfig(cfg); err != nil { + return fmt.Errorf("setting config: %w", err) + } + + logger.Print("Set git user config for: "+repoName, nil) return nil } -// set git user name -func setGitUserName(repoName, repoDestination string) error { - - cmd := exec.Command("git", "-C", repoDestination, "config", "user.name", config.GitUserName) - output, err := cmd.CombinedOutput() +// update remote URL with current token +func updateRemoteURL(repoDestination, gitURL string) error { + repo, err := git.PlainOpen(repoDestination) if err != nil { - return fmt.Errorf("setting git username: %w, output: %s", err, string(output)) + return fmt.Errorf("opening repository: %w", err) } - logger.Print("Set git username for: "+repoName, nil) - return nil -} - -// set git user mail -func setGitUserEmail(repoName, repoDestination string) error { - - cmd := exec.Command("git", "-C", repoDestination, "config", "user.email", config.GitUserMail) - output, err := cmd.CombinedOutput() + cfg, err := repo.Config() if err != nil { - return fmt.Errorf("setting git email: %w, output: %s", err, string(output)) + return fmt.Errorf("getting config: %w", err) + } + + // update first remote's URL + for name := range cfg.Remotes { + cfg.Remotes[name].URLs = []string{gitURL} + break + } + + if err := repo.SetConfig(cfg); err != nil { + return fmt.Errorf("setting config: %w", err) } - logger.Print("Set git email for: "+repoName, nil) return nil } @@ -291,7 +318,7 @@ func handleResult(result GitOperationResult, stats *GitStats) { } // update progress bar - if !config.Debug { + if !globalConfig.Debug { _ = bar.Add(1) } } diff --git a/cmd/gogitlabber/gitea.go b/cmd/gogitlabber/gitea.go index f2ff802..4e4a62a 100644 --- a/cmd/gogitlabber/gitea.go +++ b/cmd/gogitlabber/gitea.go @@ -47,11 +47,10 @@ func NewGiteaClient(baseURL, token string) *GiteaClient { // fetch gitea repos func FetchRepositoriesGitea() ([]Repository, error) { - client := NewGiteaClient(config.GitHost, config.GitToken) - + client := NewGiteaClient(globalConfig.GitHost, globalConfig.GitToken) options := GiteaAPIOptions{ Visibility: "all", - IncludeArchived: config.IncludeArchived, + IncludeArchived: globalConfig.IncludeArchived, Sort: "alpha", Limit: 100, Page: 1, diff --git a/cmd/gogitlabber/gitlab.go b/cmd/gogitlabber/gitlab.go index 5ce630b..c3a213e 100644 --- a/cmd/gogitlabber/gitlab.go +++ b/cmd/gogitlabber/gitlab.go @@ -63,11 +63,10 @@ func NewGitLabClient(baseURL, token string) *GitLabClient { // fetch gitlab repos func FetchRepositoriesGitLab() ([]Repository, error) { - client := NewGitLabClient(config.GitHost, config.GitToken) - + client := NewGitLabClient(globalConfig.GitHost, globalConfig.GitToken) options := GitLabAPIOptions{ Membership: true, - IncludeArchived: config.IncludeArchived, + IncludeArchived: globalConfig.IncludeArchived, OrderBy: "name", Sort: "asc", PerPage: 100, diff --git a/cmd/gogitlabber/input.go b/cmd/gogitlabber/input.go index ca3540e..783826f 100644 --- a/cmd/gogitlabber/input.go +++ b/cmd/gogitlabber/input.go @@ -63,8 +63,8 @@ func expandPath(path string) string { // loadconfig from yaml file func loadConfig(configPath string) (*Config, error) { - config := &Config{} - config.setDefaults() + cfg := &Config{} + cfg.setDefaults() // check if config file exists if _, err := os.Stat(configPath); os.IsNotExist(err) { @@ -78,11 +78,11 @@ func loadConfig(configPath string) (*Config, error) { } // parse yaml - if err := yaml.Unmarshal(data, config); err != nil { + if err := yaml.Unmarshal(data, cfg); err != nil { return nil, fmt.Errorf("failed to parse config file: %w", err) } - return config, nil + return cfg, nil } // validateConfig validates the configuration values @@ -147,11 +147,6 @@ func manageArguments() *Config { flag.Parse() - // override debug setting if flag is set - if *debugFlag { - config.Debug = true - } - if *versionFlag { fmt.Println(version) os.Exit(0) @@ -160,23 +155,28 @@ func manageArguments() *Config { configPath := *configFileFlag // Load configuration from YAML file - config, err := loadConfig(configPath) + cfg, err := loadConfig(configPath) if err != nil { flag.Usage() logger.Fatal("Configuration error: "+err.Error(), nil) } + // override debug setting if flag is set + if *debugFlag { + cfg.Debug = true + } + // Process configuration - config.processConfig() + cfg.processConfig() // Validate configuration - if err := config.validateConfig(); err != nil { + if err := cfg.validateConfig(); err != nil { flag.Usage() logger.Fatal("Configuration validation error: "+err.Error(), nil) } // Log configuration - config.logConfig(configPath) + cfg.logConfig(configPath) - return config + return cfg } diff --git a/cmd/gogitlabber/main.go b/cmd/gogitlabber/main.go index b786652..51e5fea 100644 --- a/cmd/gogitlabber/main.go +++ b/cmd/gogitlabber/main.go @@ -8,7 +8,7 @@ import ( // version var version string -var config *Config +var globalConfig *Config // repository data type Repository struct { @@ -19,37 +19,31 @@ type Repository struct { func main() { // set app version - version = "2.2.2" + version = "3.0.0" // set appname for logger logger.SetAppName("gogitlabber") // manage all argument magic and load configuration - config = manageArguments() + globalConfig = manageArguments() // set debugging - logger.SetDebug(config.Debug) - - // check for git - err := verifyGitAvailable() - if err != nil { - logger.Fatal("VALIDATION: git not found in path", err) - } - logger.Print("VALIDATION: git found in path", nil) + logger.SetDebug(globalConfig.Debug) // validate git backend is set - if config.GitBackend == "" { + if globalConfig.GitBackend == "" { logger.Fatal("Configuration error: git_backend is required (gitlab|gitea)", nil) } // make initial progressbar - if !config.Debug { + if !globalConfig.Debug { progressBar() } // fetch repository information var repositories []Repository - switch config.GitBackend { + var err error + switch globalConfig.GitBackend { case "gitea": repositories, err = FetchRepositoriesGitea() if err != nil { @@ -61,7 +55,7 @@ func main() { logger.Fatal("Fetching repositories failed", err) } default: - logger.Fatal(fmt.Sprintf("Unsupported git backend: %s (supported: gitlab|gitea)", config.GitBackend), nil) + logger.Fatal(fmt.Sprintf("Unsupported git backend: %s (supported: gitlab|gitea)", globalConfig.GitBackend), nil) } // manage found repositories diff --git a/cmd/gogitlabber/output.go b/cmd/gogitlabber/output.go index 9200035..eb465fe 100644 --- a/cmd/gogitlabber/output.go +++ b/cmd/gogitlabber/output.go @@ -36,7 +36,7 @@ func progressBar() { // update progressbar func updateProgressBar(repoCount int) error { - if config.Debug { + if globalConfig.Debug { return nil // Skip progress bar in debug mode } logger.Print("Resetting progress bar", nil) @@ -68,7 +68,7 @@ func printPullErrorUnstaged(stats *GitStats) { if len(stats.pullErrorMsgUnstaged) > 0 { fmt.Println("Repositories with unstaged changes:") for _, repo := range stats.pullErrorMsgUnstaged { - fmt.Printf("❕ %s has unstaged changes.\n", repo) + fmt.Printf("• %s has unstaged changes.\n", repo) } fmt.Println() } @@ -79,7 +79,7 @@ func printPullErrorUncommitted(stats *GitStats) { if len(stats.pullErrorMsgUncommitted) > 0 { fmt.Println("Repositories with uncommitted changes:") for _, repo := range stats.pullErrorMsgUncommitted { - fmt.Printf("❕ %s has uncommitted changes.\n", repo) + fmt.Printf("• %s has uncommitted changes.\n", repo) } fmt.Println() } @@ -97,7 +97,7 @@ func printGeneralErrors(stats *GitStats) { if len(stats.generalErrors) > 0 { fmt.Println("Repositories with errors:") for _, repo := range stats.generalErrors { - fmt.Printf("❌ %s failed to process.\n", repo) + fmt.Printf("✗ %s failed to process.\n", repo) } fmt.Println() } diff --git a/cmd/gogitlabber/requirements.go b/cmd/gogitlabber/requirements.go deleted file mode 100644 index 25a3951..0000000 --- a/cmd/gogitlabber/requirements.go +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - "fmt" - "os/exec" -) - -// verify if git is available -func verifyGitAvailable() error { - _, err := exec.LookPath("git") - if err != nil { - return fmt.Errorf("git is not installed or not in PATH: %w", err) - } - return nil -} diff --git a/go.mod b/go.mod index cd29e95..873c597 100644 --- a/go.mod +++ b/go.mod @@ -9,10 +9,33 @@ require ( ) require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/gcfg/v2 v2.0.2 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 // indirect + github.com/go-git/go-git/v5 v5.16.4 // indirect + github.com/go-git/go-git/v6 v6.0.0-20251210072406-9b5f6428e1da // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.4.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 648e786..4c54202 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,63 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo= +github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0 h1:eY5aB2GXiVdgTueBcqsBt53WuJTRZAuCdIS/86Pcq5c= +github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k= +github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= +github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-git/go-git/v6 v6.0.0-20251210072406-9b5f6428e1da h1:ch21RnknyB1dYlWSpomdW3pXNcQZJtrtDi8wz5up31s= +github.com/go-git/go-git/v6 v6.0.0-20251210072406-9b5f6428e1da/go.mod h1:XY/p4VJq0DwOVAAs+58NpHcQrqwHDEzMv4g8MBK7ZVA= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= +github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= +github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -20,13 +68,58 @@ github.com/scornet256/go-logger v0.0.0-20250306113004-0062c214ab34 h1:icrxu4GofO github.com/scornet256/go-logger v0.0.0-20250306113004-0062c214ab34/go.mod h1:GptTzXTPlyNj2mZjhRyWfmP4EDb1Ca2osDpooBy6MmI= github.com/scornet256/go-logger v0.0.2 h1:8k+PciBU7kZjRPAdb03zTQjnLnDaM+GDrlIOa/ur6/Y= github.com/scornet256/go-logger v0.0.2/go.mod h1:GptTzXTPlyNj2mZjhRyWfmP4EDb1Ca2osDpooBy6MmI= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=