package providers import ( "errors" "fmt" "os" "sync" "time" "github.com/Theodor-Springmann-Stiftung/kgpz_web/helpers/logging" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" ) var InvalidBranchError = errors.New("The currently checked out branch does not match the requested branch. Please checkout the correct branch first.") var InvalidStateError = errors.New("The GitProvider is not in a valid state. Fix the issues or continue without Git data.") var NoURLProvidedError = errors.New("No URL provided for GitProvider.") var NoPathProvidedError = errors.New("No path or branch provided for GitProvider.") // NOTE: GitProvider does not open any worktree files, it only // - reads in information from the repo, given a path // - clones a repo, given an URL & a path // - pulls a repo, given a path // In case of success it updates it's state: the commit hash and date. Then it closes the repo. type GitProvider struct { mu sync.Mutex URL string Path string Branch string Commit string Date time.Time } func NewGitProvider(url string, path string, branch string) (*GitProvider, error) { // TODO: check if directory is empty // TODO: force clone if _, err := os.Stat(path); err == nil { return GitProviderFromPath(path, branch) } return GitProviderFromURL(url, path, branch) } func GitProviderFromPath(path string, branch string) (*GitProvider, error) { if branch == "" || path == "" { return nil, NoPathProvidedError } gp := GitProvider{Path: path, Branch: branch} if err := gp.Read(); err != nil { return nil, err } return &gp, nil } func GitProviderFromURL(url string, path string, branch string) (*GitProvider, error) { if url == "" { return nil, NoURLProvidedError } if branch == "" || path == "" { return nil, NoPathProvidedError } gp := GitProvider{URL: url, Path: path, Branch: branch} if err := gp.Clone(); err != nil { return nil, err } return &gp, nil } // Returs true if the repo was updated remotely, false otherwise func (g *GitProvider) Pull() (error, bool) { g.mu.Lock() defer g.mu.Unlock() branch := plumbing.NewBranchReferenceName(g.Branch) repo, err := git.PlainOpen(g.Path) if err != nil { logging.Error(err, "Error opening repository") return err, false } wt, err := repo.Worktree() if err != nil { logging.Error(err, "Error getting worktree") return err, false } if err := wt.Pull(&git.PullOptions{ RemoteName: "origin", ReferenceName: branch, Progress: os.Stdout, }); err != nil { if err == git.NoErrAlreadyUpToDate { return nil, false } logging.Error(err, "Error pulling repository") return err, false } defer wt.Clean(&git.CleanOptions{Dir: true}) oldCommit := g.Commit if err := g.setValues(repo); err != nil { logging.Error(err, "Error setting values for new commit") return err, false } if oldCommit == g.Commit { return nil, false } return nil, true } func (g *GitProvider) Clone() error { if g.URL == "" { return NoURLProvidedError } g.mu.Lock() defer g.mu.Unlock() branch := plumbing.NewBranchReferenceName(g.Branch) repo, err := git.PlainClone(g.Path, false, &git.CloneOptions{ URL: g.URL, Progress: os.Stdout, }) if err != nil { return err } wt, err := repo.Worktree() if err != nil { return err } defer wt.Clean(&git.CleanOptions{Dir: true}) if err := wt.Checkout(&git.CheckoutOptions{ Branch: branch, Force: true, }); err != nil { return err } return g.setValues(repo) } // Implement String Interface func (g *GitProvider) String() string { return fmt.Sprintf("GitProvider\nURL: %s\nPath: %s\nBranch: %s\nCommit: %s\nDate: %s\n", g.URL, g.Path, g.Branch, g.Commit, g.Date) } func (g *GitProvider) setValues(repo *git.Repository) error { log, err := repo.Log(&git.LogOptions{}) if err != nil { return err } defer log.Close() commit, err := log.Next() if err != nil { return err } g.Commit = commit.Hash.String() g.Date = commit.Author.When return nil } func (g *GitProvider) Read() error { g.mu.Lock() defer g.mu.Unlock() repo, err := git.PlainOpen(g.Path) if err != nil { return err } if err := g.ValidateBranch(repo); err != nil { branch := plumbing.NewBranchReferenceName(g.Branch) wt, err := repo.Worktree() if err != nil { return err } defer wt.Clean(&git.CleanOptions{Dir: true}) if err := wt.Checkout(&git.CheckoutOptions{ Branch: branch, Force: true, }); err != nil { return err } if err := g.ValidateBranch(repo); err != nil { return err } } return g.setValues(repo) } func (g *GitProvider) Validate() error { repo, err := git.PlainOpen(g.Path) if err != nil { return err } if err := g.ValidateBranch(repo); err != nil { return err } if err := g.ValidateCommit(); err != nil { return err } return nil } func (g *GitProvider) ValidateBranch(repo *git.Repository) error { head, err := repo.Head() if err != nil { return err } cbranch := head.Name().Short() if cbranch != g.Branch { return InvalidBranchError } return nil } func (g *GitProvider) Wait() { g.mu.Lock() defer g.mu.Unlock() } func (g *GitProvider) ValidateCommit() error { if g.Commit == "" || g.Date.IsZero() { return InvalidStateError } return nil }