diff --git a/.gitignore b/.gitignore index 252c155..1b2cac1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ testdata/commits_on_branch/ testdata/git_tags/ testdata/annotated_git_tags_mix/ +testdata/detached_head/ \ No newline at end of file diff --git a/branch_diff_commits.go b/branch_diff_commits.go index de871c5..899d2eb 100644 --- a/branch_diff_commits.go +++ b/branch_diff_commits.go @@ -14,7 +14,7 @@ func (g *Git) BranchDiffCommits(branchA string, branchB string) ([]Hash, error) // The ^branchB syntax excludes all commits reachable from branchB output, err := g.runGitCommand("log", "--format=%H", branchA, "^"+branchB) if err != nil { - return nil, fmt.Errorf("Failed comparing branches %v and %v: %v", branchA, branchB, err) + return nil, fmt.Errorf("failed comparing branches %v and %v: %v", branchA, branchB, err) } var diffCommits []Hash diff --git a/branch_diff_commits_test.go b/branch_diff_commits_test.go index 654189d..ed3f228 100644 --- a/branch_diff_commits_test.go +++ b/branch_diff_commits_test.go @@ -37,3 +37,25 @@ func TestBranchDiffCommitsWithMasterMerge(t *testing.T) { assert.Equal(t, err, nil) } + +func TestBranchDiffCommitsDetachedHead(t *testing.T) { + testGit, err := OpenGit("./testdata/detached_head") + assert.NoError(t, err) + + // Verify we're in detached HEAD state + currentBranch, err := testGit.CurrentBranch() + assert.NoError(t, err) + assert.Equal(t, "HEAD", currentBranch.Name()) + + // BranchDiffCommits should work even in detached HEAD state + // Compare origin/master (which is ahead) to HEAD (which is at second commit) + commits, err := testGit.BranchDiffCommits("origin/master", "HEAD") + + assert.NoError(t, err) + // origin/master has one commit ahead of HEAD (the third commit) + assert.Equal(t, 1, len(commits)) + + commit, err := testGit.Commit(commits[0]) + assert.NoError(t, err) + assert.Equal(t, "third commit\n", commit.Message) +} diff --git a/commits_between_test.go b/commits_between_test.go index 5e1739f..02eb999 100644 --- a/commits_between_test.go +++ b/commits_between_test.go @@ -68,3 +68,34 @@ func TestToFromEqual(t *testing.T) { assert.Equal(t, 0, len(commits)) assert.NoError(t, err) } + +func TestCommitsBetweenDetachedHead(t *testing.T) { + testGit, err := OpenGit("./testdata/detached_head") + assert.NoError(t, err) + + // Verify we're in detached HEAD state + currentBranch, err := testGit.CurrentBranch() + assert.NoError(t, err) + assert.Equal(t, "HEAD", currentBranch.Name()) + + // Get HEAD commit hash (second commit) + headHash := currentBranch.Hash() + + // Get origin/master commit hash (third commit) + masterHashStr, err := testGit.runGitCommand("rev-parse", "origin/master") + assert.NoError(t, err) + masterHash, err := NewHash(masterHashStr) + assert.NoError(t, err) + + // CommitsBetween should work even in detached HEAD state + // Get commits between HEAD (second commit) and origin/master (third commit) + commits, err := testGit.CommitsBetween(masterHash, headHash) + + assert.NoError(t, err) + // Should have 1 commit (the third commit) + assert.Equal(t, 1, len(commits)) + + commit, err := testGit.Commit(commits[0]) + assert.NoError(t, err) + assert.Equal(t, "third commit\n", commit.Message) +} diff --git a/commits_on_branch_test.go b/commits_on_branch_test.go index 6d4c262..1d20965 100644 --- a/commits_on_branch_test.go +++ b/commits_on_branch_test.go @@ -27,3 +27,32 @@ func TestCommitsOnBranch(t *testing.T) { assert.Equal(t, "test commit on master\n", lastCommit.Message) } + +func TestCommitsOnBranchDetachedHead(t *testing.T) { + testGit, err := OpenGit("./testdata/detached_head") + assert.NoError(t, err) + + // Verify we're in detached HEAD state + currentBranch, err := testGit.CurrentBranch() + assert.NoError(t, err) + assert.Equal(t, "HEAD", currentBranch.Name()) + + // Get HEAD commit hash + headHash := currentBranch.Hash() + + // CommitsOnBranch should work even in detached HEAD state + // It takes a commit hash, so HEAD state doesn't matter + commits, err := testGit.CommitsOnBranch(headHash) + + assert.NoError(t, err) + // Should have 2 commits (second commit and first commit) + assert.Equal(t, 2, len(commits)) + + commit, err := testGit.Commit(commits[0]) + assert.NoError(t, err) + assert.Equal(t, "second commit\n", commit.Message) + + lastCommit, err := testGit.Commit(commits[1]) + assert.NoError(t, err) + assert.Equal(t, "first commit\n", lastCommit.Message) +} diff --git a/current_branch.go b/current_branch.go index 71263bf..bf6f44a 100644 --- a/current_branch.go +++ b/current_branch.go @@ -1,22 +1,24 @@ package git -// CurrentBranch returns the reference HEAD is at right now +// CurrentBranch returns the reference HEAD is at right now. +// In detached HEAD state, it returns a reference with name "HEAD". func (g *Git) CurrentBranch() (*Reference, error) { - // Get the symbolic ref name - refName, err := g.runGitCommand("symbolic-ref", "HEAD") + // Get the commit hash first (works in both normal and detached HEAD state) + hashStr, err := g.runGitCommand("rev-parse", "HEAD") if err != nil { return nil, err } - // Get the commit hash - hashStr, err := g.runGitCommand("rev-parse", "HEAD") + hash, err := NewHash(hashStr) if err != nil { return nil, err } - hash, err := NewHash(hashStr) + // Try to get the symbolic ref name (fails in detached HEAD state) + refName, err := g.runGitCommand("symbolic-ref", "HEAD") if err != nil { - return nil, err + // In detached HEAD state, use "HEAD" as the reference name + refName = "HEAD" } return NewReference(refName, hash), nil diff --git a/current_branch_test.go b/current_branch_test.go index d8a15c3..2b7adc5 100644 --- a/current_branch_test.go +++ b/current_branch_test.go @@ -18,3 +18,16 @@ func TestCurrentBranch(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "refs/heads/my-branch", currentBranch.Name()) } + +func TestCurrentBranchDetachedHead(t *testing.T) { + testGit, err := OpenGit("./testdata/detached_head") + assert.NoError(t, err) + + currentBranch, err := testGit.CurrentBranch() + + assert.NoError(t, err) + assert.Equal(t, "HEAD", currentBranch.Name()) + + // Verify it has a valid commit hash + assert.NotEmpty(t, currentBranch.Hash().String()) +} diff --git a/latest_commit_on_branch_test.go b/latest_commit_on_branch_test.go index 13a0df0..fb7e77b 100644 --- a/latest_commit_on_branch_test.go +++ b/latest_commit_on_branch_test.go @@ -19,3 +19,19 @@ func TestLatestCommitOnBranch(t *testing.T) { assert.Equal(t, "third commit on new branch\n", commit.Message) assert.Equal(t, err, nil) } + +func TestLatestCommitOnBranchDetachedHead(t *testing.T) { + testGit, err := OpenGit("./testdata/detached_head") + assert.NoError(t, err) + + // Verify we're in detached HEAD state + currentBranch, err := testGit.CurrentBranch() + assert.NoError(t, err) + assert.Equal(t, "HEAD", currentBranch.Name()) + + // LatestCommitOnBranch should work even in detached HEAD state + commit, err := testGit.LatestCommitOnBranch("origin/master") + + assert.NoError(t, err) + assert.Equal(t, "third commit\n", commit.Message) +} diff --git a/testdata/detached_head.bundle b/testdata/detached_head.bundle new file mode 100644 index 0000000..9b34214 Binary files /dev/null and b/testdata/detached_head.bundle differ diff --git a/testdata/setup_test_repos.sh b/testdata/setup_test_repos.sh index b77f418..4aec273 100644 --- a/testdata/setup_test_repos.sh +++ b/testdata/setup_test_repos.sh @@ -16,3 +16,8 @@ echo 'testdata/annotated_git_tags_mix/ directory does not exist at the root; cre rm -rf annotated_git_tags_mix git clone annotated_git_tags_mix.bundle echo 'done' + +echo 'testdata/detached_head/ directory does not exist at the root; creating...' +rm -rf detached_head +git clone detached_head.bundle +echo 'done'