2020-10-20 09:05:17 +02:00
|
|
|
package git
|
|
|
|
|
|
|
|
import (
|
2021-02-15 13:34:19 +02:00
|
|
|
"github.com/SAP/jenkins-library/pkg/log"
|
2020-10-20 09:05:17 +02:00
|
|
|
"github.com/go-git/go-git/v5"
|
|
|
|
"github.com/go-git/go-git/v5/plumbing"
|
2020-10-27 14:45:34 +02:00
|
|
|
"github.com/go-git/go-git/v5/plumbing/object"
|
2020-10-20 09:05:17 +02:00
|
|
|
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
|
|
|
"github.com/pkg/errors"
|
2020-10-27 14:45:34 +02:00
|
|
|
"time"
|
2020-10-20 09:05:17 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// utilsWorkTree interface abstraction of git.Worktree to enable tests
|
|
|
|
type utilsWorkTree interface {
|
|
|
|
Add(path string) (plumbing.Hash, error)
|
|
|
|
Commit(msg string, opts *git.CommitOptions) (plumbing.Hash, error)
|
|
|
|
Checkout(opts *git.CheckoutOptions) error
|
|
|
|
}
|
|
|
|
|
|
|
|
// utilsRepository interface abstraction of git.Repository to enable tests
|
|
|
|
type utilsRepository interface {
|
|
|
|
Worktree() (*git.Worktree, error)
|
|
|
|
Push(o *git.PushOptions) error
|
|
|
|
}
|
|
|
|
|
|
|
|
// utilsGit interface abstraction of git to enable tests
|
|
|
|
type utilsGit interface {
|
|
|
|
plainClone(path string, isBare bool, o *git.CloneOptions) (*git.Repository, error)
|
2021-02-15 13:34:19 +02:00
|
|
|
plainOpen(path string) (*git.Repository, error)
|
2020-10-20 09:05:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// CommitSingleFile Commits the file located in the relative file path with the commitMessage to the given worktree.
|
|
|
|
// In case of errors, the error is returned. In the successful case the commit is provided.
|
2020-10-27 14:45:34 +02:00
|
|
|
func CommitSingleFile(filePath, commitMessage, author string, worktree *git.Worktree) (plumbing.Hash, error) {
|
|
|
|
return commitSingleFile(filePath, commitMessage, author, worktree)
|
2020-10-20 09:05:17 +02:00
|
|
|
}
|
|
|
|
|
2020-10-27 14:45:34 +02:00
|
|
|
func commitSingleFile(filePath, commitMessage, author string, worktree utilsWorkTree) (plumbing.Hash, error) {
|
2020-10-20 09:05:17 +02:00
|
|
|
_, err := worktree.Add(filePath)
|
|
|
|
if err != nil {
|
|
|
|
return [20]byte{}, errors.Wrap(err, "failed to add file to git")
|
|
|
|
}
|
|
|
|
|
2020-10-27 14:45:34 +02:00
|
|
|
commit, err := worktree.Commit(commitMessage, &git.CommitOptions{
|
|
|
|
All: true,
|
|
|
|
Author: &object.Signature{Name: author, When: time.Now()},
|
|
|
|
})
|
2020-10-20 09:05:17 +02:00
|
|
|
if err != nil {
|
|
|
|
return [20]byte{}, errors.Wrap(err, "failed to commit file")
|
|
|
|
}
|
|
|
|
|
|
|
|
return commit, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// PushChangesToRepository Pushes all committed changes in the repository to the remote repository
|
|
|
|
func PushChangesToRepository(username, password string, repository *git.Repository) error {
|
|
|
|
return pushChangesToRepository(username, password, repository)
|
|
|
|
}
|
|
|
|
|
|
|
|
func pushChangesToRepository(username, password string, repository utilsRepository) error {
|
|
|
|
pushOptions := &git.PushOptions{
|
|
|
|
Auth: &http.BasicAuth{Username: username, Password: password},
|
|
|
|
}
|
|
|
|
err := repository.Push(pushOptions)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to push commit")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// PlainClone Clones a non-bare repository to the provided directory
|
|
|
|
func PlainClone(username, password, serverURL, directory string) (*git.Repository, error) {
|
|
|
|
abstractedGit := &abstractionGit{}
|
|
|
|
return plainClone(username, password, serverURL, directory, abstractedGit)
|
|
|
|
}
|
|
|
|
|
|
|
|
func plainClone(username, password, serverURL, directory string, abstractionGit utilsGit) (*git.Repository, error) {
|
|
|
|
gitCloneOptions := git.CloneOptions{
|
|
|
|
Auth: &http.BasicAuth{Username: username, Password: password},
|
|
|
|
URL: serverURL,
|
|
|
|
}
|
|
|
|
repository, err := abstractionGit.plainClone(directory, false, &gitCloneOptions)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "failed to clone git")
|
|
|
|
}
|
|
|
|
return repository, nil
|
|
|
|
}
|
|
|
|
|
2021-02-15 13:34:19 +02:00
|
|
|
// PlainOpen opens a git repository from the given path
|
|
|
|
func PlainOpen(path string) (*git.Repository, error) {
|
|
|
|
abstractedGit := &abstractionGit{}
|
|
|
|
return plainOpen(path, abstractedGit)
|
|
|
|
}
|
|
|
|
|
|
|
|
func plainOpen(path string, abstractionGit utilsGit) (*git.Repository, error) {
|
|
|
|
log.Entry().Infof("Opening git repo at '%s'", path)
|
|
|
|
r, err := abstractionGit.plainOpen(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "Unable to open git repository at '%s'", path)
|
|
|
|
}
|
|
|
|
return r, nil
|
|
|
|
}
|
|
|
|
|
2020-10-20 09:05:17 +02:00
|
|
|
// ChangeBranch checkout the provided branch.
|
|
|
|
// It will create a new branch if the branch does not exist yet.
|
2020-11-03 19:29:46 +02:00
|
|
|
// It will return an error if no branch name if provided
|
2020-10-20 09:05:17 +02:00
|
|
|
func ChangeBranch(branchName string, worktree *git.Worktree) error {
|
|
|
|
return changeBranch(branchName, worktree)
|
|
|
|
}
|
|
|
|
|
|
|
|
func changeBranch(branchName string, worktree utilsWorkTree) error {
|
|
|
|
if branchName == "" {
|
2020-11-03 19:29:46 +02:00
|
|
|
return errors.New("no branch name provided")
|
2020-10-20 09:05:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
var checkoutOptions = &git.CheckoutOptions{}
|
|
|
|
checkoutOptions.Branch = plumbing.NewBranchReferenceName(branchName)
|
|
|
|
checkoutOptions.Create = false
|
|
|
|
err := worktree.Checkout(checkoutOptions)
|
|
|
|
if err != nil {
|
|
|
|
// branch might not exist, try to create branch
|
|
|
|
checkoutOptions.Create = true
|
|
|
|
err = worktree.Checkout(checkoutOptions)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to checkout branch")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-01-29 13:36:15 +02:00
|
|
|
// LogRange Returns a CommitIterator providing all commits reachable from 'to', but
|
|
|
|
// not reachable by 'from'.
|
|
|
|
func LogRange(repo *git.Repository, from, to string) (object.CommitIter, error) {
|
|
|
|
|
|
|
|
cTo, err := getCommitObject(to, repo)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "Cannot provide log range (to: '%s' not found)", to)
|
|
|
|
}
|
|
|
|
cFrom, err := getCommitObject(from, repo)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "Cannot provide log range (from: '%s' not found)", from)
|
|
|
|
}
|
|
|
|
ignore := []plumbing.Hash{}
|
|
|
|
err = object.NewCommitPreorderIter(
|
|
|
|
cFrom,
|
|
|
|
map[plumbing.Hash]bool{},
|
|
|
|
[]plumbing.Hash{},
|
|
|
|
).ForEach(func(c *object.Commit) error {
|
|
|
|
ignore = append(ignore, c.ID())
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "Cannot provide log range")
|
|
|
|
}
|
|
|
|
|
|
|
|
return object.NewCommitPreorderIter(cTo, map[plumbing.Hash]bool{}, ignore), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getCommitObject(ref string, repo *git.Repository) (*object.Commit, error) {
|
|
|
|
if len(ref) == 0 {
|
|
|
|
// with go-git v5.1.0 we panic otherwise inside ResolveRevision
|
|
|
|
return nil, errors.New("Cannot get a commit for an empty ref")
|
|
|
|
}
|
|
|
|
r, err := repo.ResolveRevision(plumbing.Revision(ref))
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "Trouble resolving '%s'", ref)
|
|
|
|
}
|
|
|
|
c, err := repo.CommitObject(*r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "Trouble resolving '%s'", ref)
|
|
|
|
}
|
|
|
|
return c, nil
|
|
|
|
}
|
|
|
|
|
2020-10-20 09:05:17 +02:00
|
|
|
type abstractionGit struct{}
|
|
|
|
|
|
|
|
func (abstractionGit) plainClone(path string, isBare bool, o *git.CloneOptions) (*git.Repository, error) {
|
|
|
|
return git.PlainClone(path, isBare, o)
|
|
|
|
}
|
2021-02-15 13:34:19 +02:00
|
|
|
|
|
|
|
func (abstractionGit) plainOpen(path string) (*git.Repository, error) {
|
|
|
|
return git.PlainOpen(path)
|
|
|
|
}
|