package knotmirror import ( "context" "errors" "fmt" "os/exec" "regexp" "strings" "github.com/go-git/go-git/v5" gitconfig "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing/transport" ) type GitMirrorClient interface { Clone(ctx context.Context, path, url string) error Fetch(ctx context.Context, path, url string) error } type CliGitMirrorClient struct{} var _ GitMirrorClient = new(CliGitMirrorClient) func (c *CliGitMirrorClient) Clone(ctx context.Context, path, url string) error { cmd := exec.CommandContext(ctx, "git", "clone", "--mirror", url, path) if out, err := cmd.CombinedOutput(); err != nil { if ctx.Err() != nil { return ctx.Err() } msg := string(out) if classification := classifyError(msg); classification != nil { return classification } return fmt.Errorf("cloning repo: %w\n%s", err, msg) } return nil } func (c *CliGitMirrorClient) Fetch(ctx context.Context, path, url string) error { cmd := exec.CommandContext(ctx, "git", "-C", path, "fetch", "--prune", "origin") if out, err := cmd.CombinedOutput(); err != nil { if ctx.Err() != nil { return ctx.Err() } return fmt.Errorf("fetching repo: %w\n%s", err, string(out)) } return nil } var ( ErrDNSFailure = errors.New("git: dns failure (could not resolve host)") ErrCertExpired = errors.New("git: certificate has expired") ErrRepoNotFound = errors.New("git: repository not found") ) var ( reDNS = regexp.MustCompile(`Could not resolve host:`) reCertExpired = regexp.MustCompile(`SSL certificate OpenSSL verify result: certificate has expired`) reRepoNotFound = regexp.MustCompile(`repository '.*' not found`) ) func classifyError(stderr string) error { msg := strings.TrimSpace(stderr) switch { case reDNS.MatchString(msg): return ErrDNSFailure case reCertExpired.MatchString(msg): return ErrCertExpired case reRepoNotFound.MatchString(msg): return ErrRepoNotFound } return nil } type GoGitMirrorClient struct{} var _ GitMirrorClient = new(GoGitMirrorClient) func (c *GoGitMirrorClient) Clone(ctx context.Context, path string, url string) error { _, err := git.PlainCloneContext(ctx, path, true, &git.CloneOptions{ URL: url, Mirror: true, }) if err != nil && !errors.Is(err, transport.ErrEmptyRemoteRepository) { return fmt.Errorf("cloning repo: %w", err) } return nil } func (c *GoGitMirrorClient) Fetch(ctx context.Context, path string, url string) error { gr, err := git.PlainOpen(path) if err != nil { return fmt.Errorf("opening local repo: %w", err) } if err := gr.FetchContext(ctx, &git.FetchOptions{ RemoteURL: url, RefSpecs: []gitconfig.RefSpec{gitconfig.RefSpec("+refs/*:refs/*")}, Force: true, Prune: true, }); err != nil { return fmt.Errorf("fetching reppo: %w", err) } return nil }