diff --git a/pkg/git/commit.go b/pkg/git/commit.go index 50793cd..49ed170 100644 --- a/pkg/git/commit.go +++ b/pkg/git/commit.go @@ -2,15 +2,13 @@ package git import ( "errors" - "os" + "fmt" + "path/filepath" + "strings" "time" "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/armor" - "github.com/go-git/go-billy/v5" "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/config" - "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" ) @@ -36,10 +34,16 @@ func WithCommitter(name, email string) CommitOption { } } -func WithFileContent(content []byte, file billy.File) CommitOption { +func WithFileContent(content []byte, filename, folder string) CommitOption { return func(c *Commit) { c.Content = content - c.File = file + c.Filename = filepath.Join(folder, fmt.Sprintf("%s.json", filename)) + } +} + +func WithSigner(signKey SignKey) CommitOption { + return func(c *Commit) { + c.signKey = signKey.entity } } @@ -47,9 +51,11 @@ type Commit struct { Author *object.Signature Committer *object.Signature Content []byte - File billy.File + Filename string + KeyFile string project *Project + signKey *openpgp.Entity } func (p *Project) NewCommit(options ...CommitOption) *Commit { @@ -63,64 +69,11 @@ func (p *Project) NewCommit(options ...CommitOption) *Commit { } func (c *Commit) Create(msg string) error { - var ( - signer *openpgp.Entity - ) - - if c.project.KeyFile != "" { - file, err := os.Open(c.project.KeyFile) - if err != nil { - return err - } - defer file.Close() - - block, err := armor.Decode(file) - if err != nil { - return err - } - - entityList, err := openpgp.ReadKeyRing(block.Body) - if err != nil || len(entityList) < 1 { - return err - } - - signer = entityList[0] - } - - worktree, err := c.project.repository.Worktree() - if err != nil { + if err := c.addContent(); err != nil { return err } - _, err = c.project.repository.Branch(c.project.Branch) - if errors.Is(err, git.ErrBranchNotFound) { - err = c.project.repository.CreateBranch( - &config.Branch{Name: c.project.Branch, Remote: c.project.Branch}, - ) - if err != nil { - return err - } - } - if err != nil { - return err - } - - err = worktree.Checkout(&git.CheckoutOptions{ - Branch: plumbing.ReferenceName(c.project.Branch), - }) - if err != nil { - return err - } - - if _, err = c.File.Write(c.Content); err != nil { - return err - } - - if err = c.File.Close(); err != nil { - return err - } - - if _, err = worktree.Add(c.File.Name()); err != nil { + if _, err := c.project.worktree.Add(c.Filename); err != nil { return err } @@ -130,25 +83,41 @@ func (c *Commit) Create(msg string) error { commitOpts.Committer = c.Committer } - if signer != nil { - commitOpts.SignKey = signer + if c.signKey != nil { + commitOpts.SignKey = c.signKey } - _, err = worktree.Commit(msg, &commitOpts) - return err -} - -func (c *Commit) Push() error { - origin, err := c.project.repository.Remote("origin") + _, err := c.project.worktree.Commit(msg, &commitOpts) if err != nil { return err } - pushOpts := git.PushOptions{} + return nil +} - if c.project.auth != nil { - pushOpts.Auth = c.project.auth +func (c *Commit) Exists(uid string, id uint) bool { + commitIter, err := c.project.repository.Log(&git.LogOptions{}) + if err != nil { + return false } - return origin.Push(&pushOpts) + err = commitIter.ForEach(func(commit *object.Commit) error { + if strings.Contains(commit.Message, fmt.Sprintf("Update %s", uid)) && + strings.Contains(commit.Message, fmt.Sprintf("version %d", id)) { + return errors.New("version already committed") + } + return nil + }) + return err != nil +} + +func (c *Commit) addContent() error { + file, err := c.project.worktree.Filesystem.Create(c.Filename) + if err != nil { + return err + } + defer file.Close() + + _, err = file.Write(c.Content) + return err } diff --git a/pkg/git/project.go b/pkg/git/project.go index ea823b2..d10a145 100644 --- a/pkg/git/project.go +++ b/pkg/git/project.go @@ -1,9 +1,13 @@ package git import ( + "context" + "errors" + "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" @@ -26,28 +30,21 @@ func WithBranch(branch string) ProjectOption { } } -func WithKey(path string) ProjectOption { - return func(p *Project) { - p.KeyFile = path - } -} - type Project struct { Branch string + Force bool RepoURL string - KeyFile string auth transport.AuthMethod fs billy.Filesystem storer *memory.Storage repository *git.Repository + worktree *git.Worktree } func NewProject(url string, options ...ProjectOption) *Project { project := &Project{ - Branch: "", RepoURL: url, - KeyFile: "", fs: memfs.New(), storer: memory.NewStorage(), repository: nil, @@ -60,20 +57,120 @@ func NewProject(url string, options ...ProjectOption) *Project { return project } -func (p *Project) Clone() error { +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, + URL: p.RepoURL, + RecurseSubmodules: git.DefaultSubmoduleRecursionDepth, } if p.auth != nil { cloneOpts.Auth = p.auth } + if err := cloneOpts.Validate(); err != nil { + return err + } + var err error - p.repository, err = git.Clone( + p.repository, err = git.CloneContext( + ctx, p.storer, p.fs, &cloneOpts, ) - return err + 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 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 err := pushOpts.Validate(); err != nil { + return err + } + + return p.repository.PushContext(ctx, &pushOpts) } diff --git a/pkg/git/sign_key.go b/pkg/git/sign_key.go new file mode 100644 index 0000000..5ac350b --- /dev/null +++ b/pkg/git/sign_key.go @@ -0,0 +1,35 @@ +package git + +import ( + "os" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" +) + +type SignKey struct { + KeyFile string + + entity *openpgp.Entity +} + +func (s *SignKey) ReadKeyFile() error { + file, err := os.Open(s.KeyFile) + if err != nil { + return err + } + defer file.Close() + + block, err := armor.Decode(file) + if err != nil { + return err + } + + entityList, err := openpgp.ReadKeyRing(block.Body) + if err != nil || len(entityList) < 1 { + return err + } + + s.entity = entityList[0] + return nil +}