diff --git a/pkg/git/commit.go b/pkg/git/commit.go new file mode 100644 index 0000000..50793cd --- /dev/null +++ b/pkg/git/commit.go @@ -0,0 +1,154 @@ +package git + +import ( + "errors" + "os" + "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" +) + +type CommitOption func(*Commit) + +func WithAuthor(name, email string) CommitOption { + return func(c *Commit) { + c.Author = &object.Signature{ + Name: name, + Email: email, + When: time.Now(), + } + } +} + +func WithCommitter(name, email string) CommitOption { + return func(c *Commit) { + c.Committer = &object.Signature{ + Name: name, + Email: email, + When: time.Now(), + } + } +} + +func WithFileContent(content []byte, file billy.File) CommitOption { + return func(c *Commit) { + c.Content = content + c.File = file + } +} + +type Commit struct { + Author *object.Signature + Committer *object.Signature + Content []byte + File billy.File + + project *Project +} + +func (p *Project) NewCommit(options ...CommitOption) *Commit { + commit := &Commit{project: p} + + for _, option := range options { + option(commit) + } + + return 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 { + 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 { + return err + } + + commitOpts := git.CommitOptions{Author: c.Author} + + if c.Committer != nil { + commitOpts.Committer = c.Committer + } + + if signer != nil { + commitOpts.SignKey = signer + } + + _, err = worktree.Commit(msg, &commitOpts) + return err +} + +func (c *Commit) Push() error { + origin, err := c.project.repository.Remote("origin") + if err != nil { + return err + } + + pushOpts := git.PushOptions{} + + if c.project.auth != nil { + pushOpts.Auth = c.project.auth + } + + return origin.Push(&pushOpts) +} diff --git a/pkg/git/git.go b/pkg/git/git.go deleted file mode 100644 index b2b5df1..0000000 --- a/pkg/git/git.go +++ /dev/null @@ -1,241 +0,0 @@ -package git - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "time" - - "git.ar21.de/yolokube/grafana-backuper/pkg/grafana" - "github.com/ProtonMail/go-crypto/openpgp" - "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/http" - "github.com/go-git/go-git/v5/storage/memory" -) - -var ( - fs billy.Filesystem - storer *memory.Storage -) - -type Payload struct { - Author *object.Signature - Committer *object.Signature - Content []byte - Dashboard grafana.FoundBoard - DashboardInfo grafana.BoardProperties - Directory string - KeyFile string - Repository *git.Repository - Version grafana.DashboardVersion -} - -func NewPayload(keyFile string) *Payload { - fs = memfs.New() - storer = memory.NewStorage() - - return &Payload{ - Author: nil, - Committer: nil, - Content: []byte{}, - Dashboard: grafana.FoundBoard{}, - DashboardInfo: grafana.BoardProperties{}, - Directory: "", - KeyFile: keyFile, - Repository: nil, - Version: grafana.DashboardVersion{}, - } -} - -func (p *Payload) AddAuthor(name, email string) { - p.Author = &object.Signature{ - Name: name, - Email: email, - When: time.Now(), - } -} - -func (p *Payload) UpdateAuthor(timestamp time.Time) { - p.Author.When = timestamp -} - -func (p *Payload) AddCommitter(name, email string) { - p.Committer = &object.Signature{ - Name: name, - Email: email, - When: time.Now(), - } -} - -func (p *Payload) UpdateCommitter() { - p.Committer.When = time.Now() -} - -func (p *Payload) UpdateContent(content []byte) { - p.Content = content -} - -func (p *Payload) UpdateDashboard(dashboard grafana.FoundBoard) { - p.Dashboard = dashboard -} - -func (p *Payload) UpdateDashboardInfo(dashboardInfo grafana.BoardProperties) { - p.DashboardInfo = dashboardInfo -} - -func (p *Payload) UpdateVersion(version grafana.DashboardVersion) { - p.Version = version -} - -func (p *Payload) GetRepo(repoURL, user, password string) (err error) { - p.Repository, err = git.Clone( - storer, - fs, - &git.CloneOptions{ - Auth: genAuth(user, password), - URL: repoURL, - Progress: os.Stdout, - }, - ) - - p.Directory = filepath.Base(repoURL) - - return -} - -func (p *Payload) IsVersionCommitted(branch string) bool { - refName := plumbing.NewBranchReferenceName(branch) - - ref, err := p.Repository.Reference(refName, false) - if err != nil { - return false - } - - commitIter, err := p.Repository.Log(&git.LogOptions{From: ref.Hash()}) - if err != nil { - return false - } - - err = commitIter.ForEach(func(commit *object.Commit) error { - if strings.Contains(commit.Message, fmt.Sprintf("Update %s", p.Version.DashboardUID)) && strings.Contains(commit.Message, fmt.Sprintf("version %d", p.Version.ID)) { - return fmt.Errorf("version already committed") - } - return nil - }) - - return err != nil -} - -func genAuth(user, password string) *http.BasicAuth { - return &http.BasicAuth{ - Username: user, - Password: password, - } -} - -func commitDashboard(repo *git.Repository, content []byte, commitMsg, dashboardTitle, folderTitle, gitRepoDirectory, keyFile string, author, committer *object.Signature) (err error) { - var ( - file billy.File - signer *openpgp.Entity - worktree *git.Worktree - ) - - if strings.TrimSpace(keyFile) != "" { - signer, err = getSigner(keyFile) - if err != nil { - return - } - } - - worktree, err = repo.Worktree() - if err != nil { - return - } - - if err = fs.MkdirAll(folderTitle, 0755); err != nil { - return - } - - filePath := filepath.Join(folderTitle, fmt.Sprintf("%s.json", dashboardTitle)) - if file, err = fs.Create(filePath); err != nil { - return - } - - if _, err = file.Write(content); err != nil { - return - } - - if err = file.Close(); err != nil { - return - } - - if _, err = worktree.Add(filePath); err != nil { - return - } - - _, err = worktree.Commit( - commitMsg, - &git.CommitOptions{ - Author: author, - Committer: committer, - SignKey: signer, - }, - ) - - return -} - -func (p *Payload) CreateCommit() error { - var commitmsg string - - if p.Version.Message != "" { - commitmsg = fmt.Sprintf( - "%s: Update %s to version %d => %s", - p.Dashboard.Title, - p.Version.DashboardUID, - p.Version.ID, - p.Version.Message, - ) - } else { - commitmsg = fmt.Sprintf( - "%s: Update %s to version %d", - p.Dashboard.Title, - p.Version.DashboardUID, - p.Version.ID, - ) - } - - p.UpdateAuthor(p.Version.Created) - p.UpdateCommitter() - - return commitDashboard( - p.Repository, - p.Content, - commitmsg, - p.Dashboard.Title, - p.DashboardInfo.FolderTitle, - p.Directory, - p.KeyFile, - p.Author, - p.Committer, - ) -} - -func (p *Payload) PushToRemote(user, password string) error { - origin, err := p.Repository.Remote("origin") - if err != nil { - return err - } - - return origin.Push( - &git.PushOptions{ - Auth: genAuth(user, password), - Progress: os.Stdout, - }, - ) -} diff --git a/pkg/git/project.go b/pkg/git/project.go new file mode 100644 index 0000000..ea823b2 --- /dev/null +++ b/pkg/git/project.go @@ -0,0 +1,79 @@ +package git + +import ( + "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/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 WithKey(path string) ProjectOption { + return func(p *Project) { + p.KeyFile = path + } +} + +type Project struct { + Branch string + RepoURL string + KeyFile string + + auth transport.AuthMethod + fs billy.Filesystem + storer *memory.Storage + repository *git.Repository +} + +func NewProject(url string, options ...ProjectOption) *Project { + project := &Project{ + Branch: "", + RepoURL: url, + KeyFile: "", + fs: memfs.New(), + storer: memory.NewStorage(), + repository: nil, + } + + for _, option := range options { + option(project) + } + + return project +} + +func (p *Project) Clone() error { + cloneOpts := git.CloneOptions{ + URL: p.RepoURL, + } + + if p.auth != nil { + cloneOpts.Auth = p.auth + } + + var err error + p.repository, err = git.Clone( + p.storer, + p.fs, + &cloneOpts, + ) + return err +} diff --git a/pkg/git/signer.go b/pkg/git/signer.go deleted file mode 100644 index 1834d06..0000000 --- a/pkg/git/signer.go +++ /dev/null @@ -1,28 +0,0 @@ -package git - -import ( - "os" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/armor" -) - -func getSigner(keyFile string) (*openpgp.Entity, error) { - file, err := os.Open(keyFile) - if err != nil { - return nil, err - } - defer file.Close() - - block, err := armor.Decode(file) - if err != nil { - return nil, err - } - - entityList, err := openpgp.ReadKeyRing(block.Body) - if err != nil { - return nil, err - } - - return entityList[0], nil -}