Skip to content

Commit 143fdec

Browse files
authored
feat: adds function for checking out a git ref (#160)
1 parent 8770d46 commit 143fdec

File tree

2 files changed

+171
-5
lines changed

2 files changed

+171
-5
lines changed

lib/tools/git/repo/repo.go

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"log/slog"
77
"os"
88
"path/filepath"
9+
"strings"
910
"time"
1011

1112
gg "github.com/go-git/go-git/v5"
@@ -43,18 +44,62 @@ type GitRepo struct {
4344
worktree *gg.Worktree
4445
}
4546

47+
// CheckoutBranch checks out a branch with the given name.
48+
func (g *GitRepo) CheckoutBranch(branch string) error {
49+
return g.worktree.Checkout(&gg.CheckoutOptions{
50+
Branch: plumbing.NewBranchReferenceName(branch),
51+
})
52+
}
53+
4654
// CheckoutCommit checks out a commit with the given hash.
4755
func (g *GitRepo) CheckoutCommit(hash string) error {
4856
return g.worktree.Checkout(&gg.CheckoutOptions{
4957
Hash: plumbing.NewHash(hash),
5058
})
5159
}
5260

53-
// CheckoutBranch checks out a branch with the given name.
54-
func (g *GitRepo) CheckoutBranch(branch string) error {
55-
return g.worktree.Checkout(&gg.CheckoutOptions{
56-
Branch: plumbing.NewBranchReferenceName(branch),
57-
})
61+
func (g *GitRepo) CheckoutRef(reference string) error {
62+
// Initialize checkout options
63+
checkoutOpts := &gg.CheckoutOptions{
64+
Force: true,
65+
}
66+
67+
// Try as a commit hash first
68+
hash := plumbing.NewHash(reference)
69+
if !hash.IsZero() {
70+
_, err := g.raw.CommitObject(hash)
71+
if err == nil {
72+
checkoutOpts.Hash = hash
73+
return g.worktree.Checkout(checkoutOpts)
74+
}
75+
}
76+
77+
// Try as a local branch
78+
branchRefName := plumbing.NewBranchReferenceName(reference)
79+
_, err := g.raw.Reference(branchRefName, true)
80+
if err == nil {
81+
checkoutOpts.Branch = branchRefName
82+
return g.worktree.Checkout(checkoutOpts)
83+
}
84+
85+
// Try as a tag
86+
tagRefName := plumbing.NewTagReferenceName(reference)
87+
tagRef, err := g.raw.Reference(tagRefName, true)
88+
if err == nil {
89+
tagObj, err := g.raw.TagObject(tagRef.Hash())
90+
if err == nil {
91+
commit, err := tagObj.Commit()
92+
if err == nil {
93+
checkoutOpts.Hash = commit.Hash
94+
return g.worktree.Checkout(checkoutOpts)
95+
}
96+
} else if strings.Contains(err.Error(), "not a tag object") {
97+
checkoutOpts.Hash = tagRef.Hash()
98+
return g.worktree.Checkout(checkoutOpts)
99+
}
100+
}
101+
102+
return fmt.Errorf("reference not found: %s is not a valid commit hash, branch, or tag", reference)
58103
}
59104

60105
// Clone loads a repository from a git remote.
@@ -104,6 +149,36 @@ func (g *GitRepo) Commit(msg string) (plumbing.Hash, error) {
104149
return hash, nil
105150
}
106151

152+
// CreateTag creates a tag with the given name and message.
153+
func (g *GitRepo) CreateTag(name, hash, message string) error {
154+
commitHash := plumbing.NewHash(hash)
155+
if commitHash.IsZero() {
156+
return fmt.Errorf("invalid commit hash: %s", hash)
157+
}
158+
159+
_, err := g.raw.CommitObject(commitHash)
160+
if err != nil {
161+
return fmt.Errorf("failed to find commit %s: %w", hash, err)
162+
}
163+
164+
author, email := g.getAuthor()
165+
opts := &gg.CreateTagOptions{
166+
Message: message,
167+
Tagger: &object.Signature{
168+
Name: author,
169+
Email: email,
170+
When: time.Now(),
171+
},
172+
}
173+
174+
_, err = g.raw.CreateTag(name, commitHash, opts)
175+
if err != nil {
176+
return fmt.Errorf("failed to create tag %s: %w", name, err)
177+
}
178+
179+
return nil
180+
}
181+
107182
// Exists checks if a file exists in the repository.
108183
func (g *GitRepo) Exists(path string) (bool, error) {
109184
_, err := g.wfs.Stat(path)

lib/tools/git/repo/repo_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,66 @@ func TestGitRepoClone(t *testing.T) {
5555
assert.Equal(t, commit.Message, "test")
5656
}
5757

58+
func TestGitRepoCheckoutRef(t *testing.T) {
59+
t.Run("valid commit", func(t *testing.T) {
60+
repo := newGitRepo(t)
61+
oldHead, err := repo.raw.Head()
62+
require.NoError(t, err)
63+
64+
require.NoError(t, repo.wfs.WriteFile("new.txt", []byte("test"), 0644))
65+
require.NoError(t, repo.StageFile("new.txt"))
66+
hash, err := repo.Commit("test")
67+
require.NoError(t, err)
68+
newHead, err := repo.raw.CommitObject(hash)
69+
require.NoError(t, err)
70+
require.NotEqual(t, oldHead.Hash(), newHead.Hash)
71+
72+
assert.NoError(t, repo.CheckoutRef(oldHead.Hash().String()))
73+
head, err := repo.raw.Head()
74+
require.NoError(t, err)
75+
assert.Equal(t, head.Hash(), oldHead.Hash())
76+
})
77+
78+
t.Run("valid branch", func(t *testing.T) {
79+
repo := newGitRepo(t)
80+
require.NoError(t, repo.NewBranch("test"))
81+
require.NoError(t, repo.wfs.WriteFile("new.txt", []byte("test"), 0644))
82+
require.NoError(t, repo.StageFile("new.txt"))
83+
_, err := repo.Commit("test")
84+
require.NoError(t, err)
85+
86+
assert.NoError(t, repo.CheckoutRef("master"))
87+
branch, err := repo.GetCurrentBranch()
88+
require.NoError(t, err)
89+
assert.Equal(t, branch, "master")
90+
})
91+
92+
t.Run("valid tag", func(t *testing.T) {
93+
repo := newGitRepo(t)
94+
head, err := repo.raw.Head()
95+
require.NoError(t, err)
96+
require.NoError(t, repo.CreateTag("v1.0.0", head.Hash().String(), "test tag"))
97+
98+
require.NoError(t, repo.wfs.WriteFile("new.txt", []byte("test"), 0644))
99+
require.NoError(t, repo.StageFile("new.txt"))
100+
_, err = repo.Commit("test")
101+
require.NoError(t, err)
102+
103+
assert.NoError(t, repo.CheckoutRef("v1.0.0"))
104+
105+
newHead, err := repo.raw.Head()
106+
require.NoError(t, err)
107+
assert.Equal(t, head.Hash().String(), newHead.Hash().String())
108+
})
109+
110+
t.Run("invalid ref", func(t *testing.T) {
111+
repo := newGitRepo(t)
112+
113+
err := repo.CheckoutRef("invalid")
114+
assert.Error(t, err)
115+
})
116+
}
117+
58118
func TestGitRepoCommit(t *testing.T) {
59119
t.Run("succcess", func(t *testing.T) {
60120
repo := newGitRepo(t)
@@ -73,6 +133,37 @@ func TestGitRepoCommit(t *testing.T) {
73133
})
74134
}
75135

136+
func TestGitRepoCreateTag(t *testing.T) {
137+
t.Run("create annotated tag", func(t *testing.T) {
138+
repo := newGitRepo(t)
139+
140+
require.NoError(t, repo.wfs.WriteFile("new.txt", []byte("contesttent"), 0644))
141+
require.NoError(t, repo.StageFile("new.txt"))
142+
commitHash, err := repo.Commit("test")
143+
require.NoError(t, err)
144+
145+
err = repo.CreateTag("v1.0.0", commitHash.String(), "message")
146+
assert.NoError(t, err)
147+
148+
tag, err := repo.raw.Tag("v1.0.0")
149+
require.NoError(t, err)
150+
151+
tagObj, err := repo.raw.TagObject(tag.Hash())
152+
require.NoError(t, err, "Should be able to get a tag object for an annotated tag")
153+
assert.Equal(t, "message\n", tagObj.Message)
154+
155+
commit, err := tagObj.Commit()
156+
require.NoError(t, err)
157+
assert.Equal(t, commitHash.String(), commit.Hash.String())
158+
})
159+
160+
t.Run("invalid commit hash", func(t *testing.T) {
161+
repo := newGitRepo(t)
162+
err := repo.CreateTag("invalid-tag", "not-a-hash", "")
163+
assert.Error(t, err)
164+
})
165+
}
166+
76167
func TestGitRepoFetch(t *testing.T) {
77168
var opts *gg.FetchOptions
78169
repo := newGitRepo(t)

0 commit comments

Comments
 (0)