Skip to content

Commit d1b57e4

Browse files
Simon Prochazkafallion
authored andcommitted
fix: handle detached head cases
1 parent 1970a4f commit d1b57e4

10 files changed

+127
-8
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
testdata/commits_on_branch/
22
testdata/git_tags/
33
testdata/annotated_git_tags_mix/
4+
testdata/detached_head/

branch_diff_commits.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func (g *Git) BranchDiffCommits(branchA string, branchB string) ([]Hash, error)
1414
// The ^branchB syntax excludes all commits reachable from branchB
1515
output, err := g.runGitCommand("log", "--format=%H", branchA, "^"+branchB)
1616
if err != nil {
17-
return nil, fmt.Errorf("Failed comparing branches %v and %v: %v", branchA, branchB, err)
17+
return nil, fmt.Errorf("failed comparing branches %v and %v: %v", branchA, branchB, err)
1818
}
1919

2020
var diffCommits []Hash

branch_diff_commits_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,25 @@ func TestBranchDiffCommitsWithMasterMerge(t *testing.T) {
3737
assert.Equal(t, err, nil)
3838

3939
}
40+
41+
func TestBranchDiffCommitsDetachedHead(t *testing.T) {
42+
testGit, err := OpenGit("./testdata/detached_head")
43+
assert.NoError(t, err)
44+
45+
// Verify we're in detached HEAD state
46+
currentBranch, err := testGit.CurrentBranch()
47+
assert.NoError(t, err)
48+
assert.Equal(t, "HEAD", currentBranch.Name())
49+
50+
// BranchDiffCommits should work even in detached HEAD state
51+
// Compare origin/master (which is ahead) to HEAD (which is at second commit)
52+
commits, err := testGit.BranchDiffCommits("origin/master", "HEAD")
53+
54+
assert.NoError(t, err)
55+
// origin/master has one commit ahead of HEAD (the third commit)
56+
assert.Equal(t, 1, len(commits))
57+
58+
commit, err := testGit.Commit(commits[0])
59+
assert.NoError(t, err)
60+
assert.Equal(t, "third commit\n", commit.Message)
61+
}

commits_between_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,34 @@ func TestToFromEqual(t *testing.T) {
6868
assert.Equal(t, 0, len(commits))
6969
assert.NoError(t, err)
7070
}
71+
72+
func TestCommitsBetweenDetachedHead(t *testing.T) {
73+
testGit, err := OpenGit("./testdata/detached_head")
74+
assert.NoError(t, err)
75+
76+
// Verify we're in detached HEAD state
77+
currentBranch, err := testGit.CurrentBranch()
78+
assert.NoError(t, err)
79+
assert.Equal(t, "HEAD", currentBranch.Name())
80+
81+
// Get HEAD commit hash (second commit)
82+
headHash := currentBranch.Hash()
83+
84+
// Get origin/master commit hash (third commit)
85+
masterHashStr, err := testGit.runGitCommand("rev-parse", "origin/master")
86+
assert.NoError(t, err)
87+
masterHash, err := NewHash(masterHashStr)
88+
assert.NoError(t, err)
89+
90+
// CommitsBetween should work even in detached HEAD state
91+
// Get commits between HEAD (second commit) and origin/master (third commit)
92+
commits, err := testGit.CommitsBetween(masterHash, headHash)
93+
94+
assert.NoError(t, err)
95+
// Should have 1 commit (the third commit)
96+
assert.Equal(t, 1, len(commits))
97+
98+
commit, err := testGit.Commit(commits[0])
99+
assert.NoError(t, err)
100+
assert.Equal(t, "third commit\n", commit.Message)
101+
}

commits_on_branch_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,32 @@ func TestCommitsOnBranch(t *testing.T) {
2727

2828
assert.Equal(t, "test commit on master\n", lastCommit.Message)
2929
}
30+
31+
func TestCommitsOnBranchDetachedHead(t *testing.T) {
32+
testGit, err := OpenGit("./testdata/detached_head")
33+
assert.NoError(t, err)
34+
35+
// Verify we're in detached HEAD state
36+
currentBranch, err := testGit.CurrentBranch()
37+
assert.NoError(t, err)
38+
assert.Equal(t, "HEAD", currentBranch.Name())
39+
40+
// Get HEAD commit hash
41+
headHash := currentBranch.Hash()
42+
43+
// CommitsOnBranch should work even in detached HEAD state
44+
// It takes a commit hash, so HEAD state doesn't matter
45+
commits, err := testGit.CommitsOnBranch(headHash)
46+
47+
assert.NoError(t, err)
48+
// Should have 2 commits (second commit and first commit)
49+
assert.Equal(t, 2, len(commits))
50+
51+
commit, err := testGit.Commit(commits[0])
52+
assert.NoError(t, err)
53+
assert.Equal(t, "second commit\n", commit.Message)
54+
55+
lastCommit, err := testGit.Commit(commits[1])
56+
assert.NoError(t, err)
57+
assert.Equal(t, "first commit\n", lastCommit.Message)
58+
}

current_branch.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
package git
22

3-
// CurrentBranch returns the reference HEAD is at right now
3+
// CurrentBranch returns the reference HEAD is at right now.
4+
// In detached HEAD state, it returns a reference with name "HEAD".
45
func (g *Git) CurrentBranch() (*Reference, error) {
5-
// Get the symbolic ref name
6-
refName, err := g.runGitCommand("symbolic-ref", "HEAD")
6+
// Get the commit hash first (works in both normal and detached HEAD state)
7+
hashStr, err := g.runGitCommand("rev-parse", "HEAD")
78
if err != nil {
89
return nil, err
910
}
1011

11-
// Get the commit hash
12-
hashStr, err := g.runGitCommand("rev-parse", "HEAD")
12+
hash, err := NewHash(hashStr)
1313
if err != nil {
1414
return nil, err
1515
}
1616

17-
hash, err := NewHash(hashStr)
17+
// Try to get the symbolic ref name (fails in detached HEAD state)
18+
refName, err := g.runGitCommand("symbolic-ref", "HEAD")
1819
if err != nil {
19-
return nil, err
20+
// In detached HEAD state, use "HEAD" as the reference name
21+
refName = "HEAD"
2022
}
2123

2224
return NewReference(refName, hash), nil

current_branch_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,16 @@ func TestCurrentBranch(t *testing.T) {
1818
assert.NoError(t, err)
1919
assert.Equal(t, "refs/heads/my-branch", currentBranch.Name())
2020
}
21+
22+
func TestCurrentBranchDetachedHead(t *testing.T) {
23+
testGit, err := OpenGit("./testdata/detached_head")
24+
assert.NoError(t, err)
25+
26+
currentBranch, err := testGit.CurrentBranch()
27+
28+
assert.NoError(t, err)
29+
assert.Equal(t, "HEAD", currentBranch.Name())
30+
31+
// Verify it has a valid commit hash
32+
assert.NotEmpty(t, currentBranch.Hash().String())
33+
}

latest_commit_on_branch_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,19 @@ func TestLatestCommitOnBranch(t *testing.T) {
1919
assert.Equal(t, "third commit on new branch\n", commit.Message)
2020
assert.Equal(t, err, nil)
2121
}
22+
23+
func TestLatestCommitOnBranchDetachedHead(t *testing.T) {
24+
testGit, err := OpenGit("./testdata/detached_head")
25+
assert.NoError(t, err)
26+
27+
// Verify we're in detached HEAD state
28+
currentBranch, err := testGit.CurrentBranch()
29+
assert.NoError(t, err)
30+
assert.Equal(t, "HEAD", currentBranch.Name())
31+
32+
// LatestCommitOnBranch should work even in detached HEAD state
33+
commit, err := testGit.LatestCommitOnBranch("origin/master")
34+
35+
assert.NoError(t, err)
36+
assert.Equal(t, "third commit\n", commit.Message)
37+
}

testdata/detached_head.bundle

830 Bytes
Binary file not shown.

testdata/setup_test_repos.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,8 @@ echo 'testdata/annotated_git_tags_mix/ directory does not exist at the root; cre
1616
rm -rf annotated_git_tags_mix
1717
git clone annotated_git_tags_mix.bundle
1818
echo 'done'
19+
20+
echo 'testdata/detached_head/ directory does not exist at the root; creating...'
21+
rm -rf detached_head
22+
git clone detached_head.bundle
23+
echo 'done'

0 commit comments

Comments
 (0)