Feature/allow worktrees (#1530)

* Use go-git to find remote URL

* Use go-git package to resolve HEAD revision (commit sha1)

* Use go-git to find checked-out reference

* Remove unused functions
This commit is contained in:
Alexandre Lavigne 2023-01-19 07:29:23 +01:00 committed by GitHub
parent 3e23b4fbb5
commit 7ebcc1c816
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 95 additions and 120 deletions

View file

@ -7,7 +7,6 @@ import (
"io" "io"
"os" "os"
"path" "path"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
@ -17,8 +16,8 @@ import (
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/storer"
"github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-ini/ini"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -55,41 +54,40 @@ func (e *Error) Commit() string {
// FindGitRevision get the current git revision // FindGitRevision get the current git revision
func FindGitRevision(ctx context.Context, file string) (shortSha string, sha string, err error) { func FindGitRevision(ctx context.Context, file string) (shortSha string, sha string, err error) {
logger := common.Logger(ctx) logger := common.Logger(ctx)
gitDir, err := findGitDirectory(file)
gitDir, err := git.PlainOpenWithOptions(
file,
&git.PlainOpenOptions{
DetectDotGit: true,
EnableDotGitCommonDir: true,
},
)
if err != nil {
logger.WithError(err).Error("path", file, "not located inside a git repository")
return "", "", err
}
head, err := gitDir.Reference(plumbing.HEAD, true)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
bts, err := os.ReadFile(filepath.Join(gitDir, "HEAD")) if head.Hash().IsZero() {
if err != nil { return "", "", fmt.Errorf("HEAD sha1 could not be resolved")
return "", "", err
} }
var ref = strings.TrimSpace(strings.TrimPrefix(string(bts), "ref:")) hash := head.Hash().String()
var refBuf []byte
if strings.HasPrefix(ref, "refs/") {
// load commitid ref
refBuf, err = os.ReadFile(filepath.Join(gitDir, ref))
if err != nil {
return "", "", err
}
} else {
refBuf = []byte(ref)
}
logger.Debugf("Found revision: %s", refBuf) logger.Debugf("Found revision: %s", hash)
return string(refBuf[:7]), strings.TrimSpace(string(refBuf)), nil return hash[:7], strings.TrimSpace(hash), nil
} }
// FindGitRef get the current git ref // FindGitRef get the current git ref
func FindGitRef(ctx context.Context, file string) (string, error) { func FindGitRef(ctx context.Context, file string) (string, error) {
logger := common.Logger(ctx) logger := common.Logger(ctx)
gitDir, err := findGitDirectory(file)
if err != nil {
return "", err
}
logger.Debugf("Loading revision from git directory '%s'", gitDir)
logger.Debugf("Loading revision from git directory")
_, ref, err := FindGitRevision(ctx, file) _, ref, err := FindGitRevision(ctx, file)
if err != nil { if err != nil {
return "", err return "", err
@ -100,28 +98,58 @@ func FindGitRef(ctx context.Context, file string) (string, error) {
// Prefer the git library to iterate over the references and find a matching tag or branch. // Prefer the git library to iterate over the references and find a matching tag or branch.
var refTag = "" var refTag = ""
var refBranch = "" var refBranch = ""
r, err := git.PlainOpen(filepath.Join(gitDir, "..")) repo, err := git.PlainOpenWithOptions(
if err == nil { file,
iter, err := r.References() &git.PlainOpenOptions{
if err == nil { DetectDotGit: true,
for { EnableDotGitCommonDir: true,
r, err := iter.Next() },
if r == nil || err != nil { )
break
} if err != nil {
// logger.Debugf("Reference: name=%s sha=%s", r.Name().String(), r.Hash().String()) return "", err
if r.Hash().String() == ref {
if r.Name().IsTag() {
refTag = r.Name().String()
}
if r.Name().IsBranch() {
refBranch = r.Name().String()
}
}
}
iter.Close()
}
} }
iter, err := repo.References()
if err != nil {
return "", err
}
// find the reference that matches the revision's has
err = iter.ForEach(func(r *plumbing.Reference) error {
/* tags and branches will have the same hash
* when a user checks out a tag, it is not mentioned explicitly
* in the go-git package, we must identify the revision
* then check if any tag matches that revision,
* if so then we checked out a tag
* else we look for branches and if matches,
* it means we checked out a branch
*
* If a branches matches first we must continue and check all tags (all references)
* in case we match with a tag later in the interation
*/
if r.Hash().String() == ref {
if r.Name().IsTag() {
refTag = r.Name().String()
}
if r.Name().IsBranch() {
refBranch = r.Name().String()
}
}
// we found what we where looking for
if refTag != "" && refBranch != "" {
return storer.ErrStop
}
return nil
})
if err != nil {
return "", err
}
// order matters here see above comment.
if refTag != "" { if refTag != "" {
return refTag, nil return refTag, nil
} }
@ -129,39 +157,7 @@ func FindGitRef(ctx context.Context, file string) (string, error) {
return refBranch, nil return refBranch, nil
} }
// If the above doesn't work, fall back to the old way return "", fmt.Errorf("failed to identify reference (tag/branch) for the checked-out revision '%s'", ref)
// try tags first
tag, err := findGitPrettyRef(ctx, ref, gitDir, "refs/tags")
if err != nil || tag != "" {
return tag, err
}
// and then branches
return findGitPrettyRef(ctx, ref, gitDir, "refs/heads")
}
func findGitPrettyRef(ctx context.Context, head, root, sub string) (string, error) {
var name string
var err = filepath.Walk(filepath.Join(root, sub), func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if name != "" || info.IsDir() {
return nil
}
var bts []byte
if bts, err = os.ReadFile(path); err != nil {
return err
}
var pointsTo = strings.TrimSpace(string(bts))
if head == pointsTo {
// On Windows paths are separated with backslash character so they should be replaced to provide proper git refs format
name = strings.TrimPrefix(strings.ReplaceAll(strings.Replace(path, root, "", 1), `\`, `/`), "/")
common.Logger(ctx).Debugf("HEAD matches %s", name)
}
return nil
})
return name, err
} }
// FindGithubRepo get the repo // FindGithubRepo get the repo
@ -179,26 +175,27 @@ func FindGithubRepo(ctx context.Context, file, githubInstance, remoteName string
} }
func findGitRemoteURL(ctx context.Context, file, remoteName string) (string, error) { func findGitRemoteURL(ctx context.Context, file, remoteName string) (string, error) {
gitDir, err := findGitDirectory(file) repo, err := git.PlainOpenWithOptions(
file,
&git.PlainOpenOptions{
DetectDotGit: true,
EnableDotGitCommonDir: true,
},
)
if err != nil { if err != nil {
return "", err return "", err
} }
common.Logger(ctx).Debugf("Loading slug from git directory '%s'", gitDir)
gitconfig, err := ini.InsensitiveLoad(fmt.Sprintf("%s/config", gitDir)) remote, err := repo.Remote(remoteName)
if err != nil { if err != nil {
return "", err return "", err
} }
remote, err := gitconfig.GetSection(fmt.Sprintf(`remote "%s"`, remoteName))
if err != nil { if len(remote.Config().URLs) < 1 {
return "", err return "", fmt.Errorf("remote '%s' exists but has no URL", remoteName)
} }
urlKey, err := remote.GetKey("url")
if err != nil { return remote.Config().URLs[0], nil
return "", err
}
url := urlKey.String()
return url, nil
} }
func findGitSlug(url string, githubInstance string) (string, string, error) { func findGitSlug(url string, githubInstance string) (string, string, error) {
@ -222,35 +219,6 @@ func findGitSlug(url string, githubInstance string) (string, string, error) {
return "", url, nil return "", url, nil
} }
func findGitDirectory(fromFile string) (string, error) {
absPath, err := filepath.Abs(fromFile)
if err != nil {
return "", err
}
fi, err := os.Stat(absPath)
if err != nil {
return "", err
}
var dir string
if fi.Mode().IsDir() {
dir = absPath
} else {
dir = filepath.Dir(absPath)
}
gitPath := filepath.Join(dir, ".git")
fi, err = os.Stat(gitPath)
if err == nil && fi.Mode().IsDir() {
return gitPath, nil
} else if dir == "/" || dir == "C:\\" || dir == "c:\\" {
return "", &Error{err: ErrNoRepo}
}
return findGitDirectory(filepath.Dir(dir))
}
// NewGitCloneExecutorInput the input for the NewGitCloneExecutor // NewGitCloneExecutorInput the input for the NewGitCloneExecutor
type NewGitCloneExecutorInput struct { type NewGitCloneExecutorInput struct {
URL string URL string

View file

@ -82,12 +82,19 @@ func TestFindGitRemoteURL(t *testing.T) {
assert.NoError(err) assert.NoError(err)
remoteURL := "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/my-repo-name" remoteURL := "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/my-repo-name"
err = gitCmd("config", "-f", fmt.Sprintf("%s/.git/config", basedir), "--add", "remote.origin.url", remoteURL) err = gitCmd("-C", basedir, "remote", "add", "origin", remoteURL)
assert.NoError(err) assert.NoError(err)
u, err := findGitRemoteURL(context.Background(), basedir, "origin") u, err := findGitRemoteURL(context.Background(), basedir, "origin")
assert.NoError(err) assert.NoError(err)
assert.Equal(remoteURL, u) assert.Equal(remoteURL, u)
remoteURL = "git@github.com/AwesomeOwner/MyAwesomeRepo.git"
err = gitCmd("-C", basedir, "remote", "add", "upstream", remoteURL)
assert.NoError(err)
u, err = findGitRemoteURL(context.Background(), basedir, "upstream")
assert.NoError(err)
assert.Equal(remoteURL, u)
} }
func TestGitFindRef(t *testing.T) { func TestGitFindRef(t *testing.T) {