package git import ( "context" "errors" "io" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/go-git/go-git/v5/storage/memory" ) type ProjectOption func(*Project) func WithBasicAuth(user, pass string) ProjectOption { return func(p *Project) { p.auth = &http.BasicAuth{ Username: user, Password: pass, } } } func WithBranch(branch string) ProjectOption { return func(p *Project) { p.Branch = branch } } func WithOutputWriter(o io.Writer) ProjectOption { return func(p *Project) { p.writer = o } } type Project struct { Branch string Force bool RepoURL string auth transport.AuthMethod fs billy.Filesystem storer *memory.Storage repository *git.Repository worktree *git.Worktree writer io.Writer } func NewProject(url string, options ...ProjectOption) *Project { project := &Project{ RepoURL: url, fs: memfs.New(), storer: memory.NewStorage(), repository: nil, } for _, option := range options { option(project) } return project } func (p *Project) Checkout() error { branchRef := plumbing.NewBranchReferenceName(p.Branch) _, err := p.repository.Reference(branchRef, true) if errors.Is(err, plumbing.ErrReferenceNotFound) { var headRef *plumbing.Reference headRef, err = p.repository.Head() if err != nil { return err } ref := plumbing.NewHashReference(branchRef, headRef.Hash()) if err = p.repository.Storer.SetReference(ref); err != nil { return err } } else if err != nil { return err } p.worktree, err = p.repository.Worktree() if err != nil { return err } checkoutOpts := git.CheckoutOptions{ Branch: branchRef, Create: false, } if err = checkoutOpts.Validate(); err != nil { return err } return p.worktree.Checkout(&checkoutOpts) } func (p *Project) Clone(ctx context.Context) error { cloneOpts := git.CloneOptions{ URL: p.RepoURL, RecurseSubmodules: git.DefaultSubmoduleRecursionDepth, } if p.auth != nil { cloneOpts.Auth = p.auth } if p.writer != nil { cloneOpts.Progress = p.writer } if err := cloneOpts.Validate(); err != nil { return err } var err error p.repository, err = git.CloneContext( ctx, p.storer, p.fs, &cloneOpts, ) if err != nil { return err } return nil } func (p *Project) HasChanges() bool { remoteBranchRef := plumbing.NewRemoteReferenceName("origin", p.Branch) remoteBranch, err := p.repository.Reference(remoteBranchRef, true) if errors.Is(err, plumbing.ErrReferenceNotFound) { return true } else if err != nil { return false } localBranchRef, err := p.repository.Reference(plumbing.NewBranchReferenceName(p.Branch), true) if err != nil { return false } if localBranchRef.Hash() != remoteBranch.Hash() { return true } return false } func (p *Project) Pull(ctx context.Context) error { pullOpts := git.PullOptions{ ReferenceName: plumbing.NewBranchReferenceName(p.Branch), } if p.writer != nil { pullOpts.Progress = p.writer } if err := pullOpts.Validate(); err != nil { return err } err := p.worktree.PullContext(ctx, &pullOpts) if !errors.Is(err, plumbing.ErrReferenceNotFound) && err != nil { return err } return nil } func (p *Project) Push(ctx context.Context) error { pushOpts := git.PushOptions{ RemoteName: "origin", } if p.auth != nil { pushOpts.Auth = p.auth } if p.writer != nil { pushOpts.Progress = p.writer } if err := pushOpts.Validate(); err != nil { return err } return p.repository.PushContext(ctx, &pushOpts) }