diff options
author | James Fargher <jfargher@gitlab.com> | 2021-08-31 01:33:50 +0300 |
---|---|---|
committer | James Fargher <jfargher@gitlab.com> | 2021-09-20 00:58:00 +0300 |
commit | 3060e008e31b1e227986c81e3c9197e3b484af9d (patch) | |
tree | 1aa6cf5f5ae2ddec0ef8aaf01dff15c39494f7da | |
parent | c8f1f9ba89901b08514242dabcde049a61b4c58f (diff) |
Implement GetDefaultBranch in localrepo
This logic is required in a few different places and currently lives
within the refs service code. To allow better code sharing the ref
service code has been extracted.
-rw-r--r-- | internal/git/localrepo/refs.go | 91 | ||||
-rw-r--r-- | internal/git/localrepo/refs_test.go | 109 |
2 files changed, 200 insertions, 0 deletions
diff --git a/internal/git/localrepo/refs.go b/internal/git/localrepo/refs.go index ac9c69fd1..3f6b87e89 100644 --- a/internal/git/localrepo/refs.go +++ b/internal/git/localrepo/refs.go @@ -266,3 +266,94 @@ func (repo *Repo) GetRemoteReferences(ctx context.Context, remote string, opts . return refs, nil } + +// GetDefaultBranchOptions are options passed to GetDefaultBranch +type GetDefaultBranchOptions struct { + // HeadReference allows specifying the HEAD symbolic reference instead of + // looking it up if it is already known + HeadReference git.ReferenceName +} + +// GetDefaultBranch determines the default branch name +func (repo *Repo) GetDefaultBranch(ctx context.Context, opts *GetDefaultBranchOptions) (git.Reference, error) { + if opts == nil { + opts = &GetDefaultBranchOptions{} + } + + branches, err := repo.GetBranches(ctx) + if err != nil { + return git.Reference{}, err + } + switch len(branches) { + case 0: + return git.Reference{}, nil + case 1: + return branches[0], nil + } + + if len(opts.HeadReference) == 0 { + var err error + opts.HeadReference, err = repo.headReference(ctx) + if err != nil { + return git.Reference{}, err + } + } + + var defaultRef, legacyDefaultRef git.Reference + for _, branch := range branches { + if len(opts.HeadReference) != 0 && opts.HeadReference == branch.Name { + return branch, nil + } + + if string(git.DefaultRef) == branch.Name.String() { + defaultRef = branch + } + + if string(git.LegacyDefaultRef) == branch.Name.String() { + legacyDefaultRef = branch + } + } + + if len(defaultRef.Name) != 0 { + return defaultRef, nil + } + + if len(legacyDefaultRef.Name) != 0 { + return legacyDefaultRef, nil + } + + // If all else fails, return the first branch name + return branches[0], nil +} + +func (repo *Repo) headReference(ctx context.Context) (git.ReferenceName, error) { + var headRef []byte + + cmd, err := repo.Exec(ctx, git.SubCmd{ + Name: "rev-parse", + Flags: []git.Option{git.Flag{Name: "--symbolic-full-name"}}, + Args: []string{"HEAD"}, + }) + if err != nil { + return "", err + } + + scanner := bufio.NewScanner(cmd) + scanner.Scan() + if err := scanner.Err(); err != nil { + return "", err + } + headRef = scanner.Bytes() + + if err := cmd.Wait(); err != nil { + // If the ref pointed at by HEAD doesn't exist, the rev-parse fails + // returning the string `"HEAD"`, so we return `nil` without error. + if bytes.Equal(headRef, []byte("HEAD")) { + return "", nil + } + + return "", err + } + + return git.ReferenceName(headRef), nil +} diff --git a/internal/git/localrepo/refs_test.go b/internal/git/localrepo/refs_test.go index a99384e42..8a4fbdc98 100644 --- a/internal/git/localrepo/refs_test.go +++ b/internal/git/localrepo/refs_test.go @@ -454,3 +454,112 @@ func TestRepo_UpdateRef(t *testing.T) { }) } } + +func TestGetDefaultBranch(t *testing.T) { + const testOID = "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863" + + for _, tc := range []struct { + desc string + repo func(t *testing.T) *Repo + opts *GetDefaultBranchOptions + expectedName git.ReferenceName + expectedTarget string + }{ + { + desc: "default ref", + repo: func(t *testing.T) *Repo { + repo, repoPath := setupRepo(t, true) + oid := gittest.WriteCommit(t, repo.cfg, repoPath, gittest.WithParents(), gittest.WithBranch("apple")) + gittest.WriteCommit(t, repo.cfg, repoPath, gittest.WithParents(oid), gittest.WithBranch("main")) + return repo + }, + expectedName: git.ReferenceName(git.DefaultRef), + }, + { + desc: "legacy default ref", + repo: func(t *testing.T) *Repo { + repo, repoPath := setupRepo(t, true) + oid := gittest.WriteCommit(t, repo.cfg, repoPath, gittest.WithParents(), gittest.WithBranch("apple")) + gittest.WriteCommit(t, repo.cfg, repoPath, gittest.WithParents(oid), gittest.WithBranch("master")) + return repo + }, + expectedName: git.ReferenceName(git.LegacyDefaultRef), + }, + { + desc: "no branches", + repo: func(t *testing.T) *Repo { + repo, _ := setupRepo(t, true) + return repo + }, + }, + { + desc: "one branch", + repo: func(t *testing.T) *Repo { + repo, repoPath := setupRepo(t, true) + gittest.WriteCommit(t, repo.cfg, repoPath, gittest.WithParents(), gittest.WithBranch("apple")) + return repo + }, + expectedName: git.NewReferenceNameFromBranchName("apple"), + }, + { + desc: "no default branches", + repo: func(t *testing.T) *Repo { + repo, repoPath := setupRepo(t, true) + oid := gittest.WriteCommit(t, repo.cfg, repoPath, gittest.WithParents(), gittest.WithBranch("apple")) + gittest.WriteCommit(t, repo.cfg, repoPath, gittest.WithParents(oid), gittest.WithBranch("banana")) + return repo + }, + expectedName: git.NewReferenceNameFromBranchName("apple"), + }, + { + desc: "test repo default", + repo: func(t *testing.T) *Repo { + repo, _ := setupRepo(t, false) + return repo + }, + expectedName: git.ReferenceName(git.LegacyDefaultRef), + }, + { + desc: "test repo HEAD set", + repo: func(t *testing.T) *Repo { + repo, repoPath := setupRepo(t, false) + gittest.Exec(t, repo.cfg, "-C", repoPath, "update-ref", "refs/heads/feature", testOID) + gittest.Exec(t, repo.cfg, "-C", repoPath, "symbolic-ref", "HEAD", "refs/heads/feature") + return repo + }, + expectedName: git.NewReferenceNameFromBranchName("feature"), + expectedTarget: testOID, + }, + { + desc: "test repo HEAD provided", + repo: func(t *testing.T) *Repo { + repo, repoPath := setupRepo(t, false) + gittest.Exec(t, repo.cfg, "-C", repoPath, "update-ref", "refs/heads/feature", testOID) + return repo + }, + opts: &GetDefaultBranchOptions{ + HeadReference: "refs/heads/feature", + }, + expectedName: git.NewReferenceNameFromBranchName("feature"), + expectedTarget: testOID, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + branch, err := tc.repo(t).GetDefaultBranch(ctx, tc.opts) + require.NoError(t, err) + if len(tc.expectedName) != 0 { + require.Equal(t, tc.expectedName, branch.Name) + } + if len(tc.expectedTarget) != 0 { + require.Equal(t, tc.expectedTarget, branch.Target) + } + if len(tc.expectedName) == 0 && len(tc.expectedTarget) == 0 { + require.Empty(t, branch.Name) + require.Empty(t, branch.Target) + } + }) + } +} |