package git import ( "context" "errors" "io" "path/filepath" "strings" "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/object" "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 CommitLogs map[string]*object.Commit 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, CommitLogs: make(map[string]*object.Commit), 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) LoadLogs() error { commitIter, err := p.repository.Log(&git.LogOptions{}) if err != nil { return err } return commitIter.ForEach(func(c *object.Commit) error { p.CommitLogs[c.Message] = c return nil }) } func (p *Project) CommitExists(commitmsg string) bool { if _, ok := p.CommitLogs[commitmsg]; ok { return true } return false } func (p *Project) HasChanges() bool { localBranchRef, err := p.repository.Head() if err != nil { return false } remoteBranchRef, err := p.repository.Reference(plumbing.NewRemoteReferenceName("origin", p.Branch), true) if errors.Is(err, plumbing.ErrReferenceNotFound) { return true } else if err != nil { return false } return localBranchRef.Hash() != remoteBranchRef.Hash() } 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) && !errors.Is(err, git.NoErrAlreadyUpToDate) && 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) } func (p *Project) ListJSONFiles(directory string) ([]string, error) { files, err := p.fs.ReadDir(directory) if err != nil { return nil, err } var allFiles []string for _, file := range files { if file.IsDir() { var childFiles []string childFiles, err = p.ListJSONFiles(filepath.Join(directory, file.Name())) if err != nil { return nil, err } allFiles = append(allFiles, childFiles...) } else if strings.HasSuffix(file.Name(), ".json") { allFiles = append(allFiles, filepath.Join(directory, file.Name())) } } return allFiles, nil } func (p *Project) ReadFile(filepath string) ([]byte, error) { file, err := p.fs.Open(filepath) if err != nil { return nil, err } defer file.Close() return io.ReadAll(file) }