package git import ( "context" "errors" "fmt" "os" "os/signal" "strings" "coopcloud.tech/abra/pkg/i18n" "coopcloud.tech/abra/pkg/log" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" ) // gitCloneIgnoreErr checks whether we can ignore a git clone error or not. func gitCloneIgnoreErr(err error) bool { if strings.Contains(err.Error(), "authentication required") { return true } if strings.Contains(err.Error(), "remote repository is empty") { return true } return false } // Clone runs a git clone which accounts for different default branches. This // function respects Ctrl+C (SIGINT) calls from the user, cancelling the // context and deleting the (typically) half-baked clone of the repository. // This avoids broken state for future clone / recipe ops. func Clone(dir, url string) error { ctx := context.Background() ctx, cancelCtx := context.WithCancel(ctx) sigIntCh := make(chan os.Signal, 1) signal.Notify(sigIntCh, os.Interrupt) defer func() { signal.Stop(sigIntCh) cancelCtx() }() errCh := make(chan error) go func() { if _, err := os.Stat(dir); os.IsNotExist(err) { log.Debug(i18n.G("git clone: %s", url)) _, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{ URL: url, Tags: git.AllTags, ReferenceName: plumbing.ReferenceName("refs/heads/main"), SingleBranch: true, }) if err != nil && gitCloneIgnoreErr(err) { log.Debug(i18n.G("git clone: %s cloned successfully", dir)) errCh <- nil } if err := ctx.Err(); err != nil { errCh <- errors.New(i18n.G("git clone %s: cancelled due to interrupt", dir)) } if err != nil { log.Debug(i18n.G("git clone: main branch failed, attempting master branch")) _, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{ URL: url, Tags: git.AllTags, ReferenceName: plumbing.ReferenceName("refs/heads/master"), SingleBranch: true, }) if err != nil && gitCloneIgnoreErr(err) { log.Debug(i18n.G("git clone: %s cloned successfully", dir)) errCh <- nil } if err != nil { errCh <- err } } log.Debug(i18n.G("git clone: %s cloned successfully", dir)) } else { log.Debug(i18n.G("git clone: %s already exists", dir)) } errCh <- nil }() select { case <-sigIntCh: cancelCtx() fmt.Println() // NOTE(d1): newline after ^C if err := os.RemoveAll(dir); err != nil { return errors.New(i18n.G("unable to clean up git clone of %s: %s", dir, err)) } return errors.New(i18n.G("git clone %s: cancelled due to interrupt", dir)) case err := <-errCh: return err } return nil }