Skip to content

Commit eaf900a

Browse files
authored
feat(git): Add GetCurrentCommit function and WithMetadataOnly option (#44)
1 parent cbb7393 commit eaf900a

File tree

3 files changed

+149
-13
lines changed

3 files changed

+149
-13
lines changed

internal/utils/git/git.go

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ type GitProvider interface {
2424
Checkout(ctx context.Context, repoDir string, commitHash string) error
2525
// GetCommitHashBeforeDate returns the commit hash at or before the specified date in the repository.
2626
GetCommitHashBeforeDate(ctx context.Context, repoDir string, dateTime time.Time) (string, error)
27+
// GetCurrentCommit returns the current commit hash of the repository at the given directory, regardless of the date.
28+
GetCurrentCommit(ctx context.Context, repoDir string) (string, error)
2729
}
2830

2931
type GitProviderImpl struct {
@@ -33,7 +35,20 @@ type GitProviderImpl struct {
3335

3436
var _ GitProvider = (*GitProviderImpl)(nil)
3537

36-
type GitOptions func() []string
38+
// GitOptions is a functional option that configures a clone operation.
39+
// Options may add CLI arguments and/or request post-clone actions.
40+
type GitOptions func(opts *cloneOptions)
41+
42+
// cloneOptions holds the resolved configuration for a clone operation,
43+
// including any post-clone actions.
44+
type cloneOptions struct {
45+
// args are the CLI arguments to pass to 'git clone'.
46+
args []string
47+
// quiet suppresses event emission during the clone. Use this for
48+
// internal clones (e.g., identity resolution) that run concurrently
49+
// and would otherwise produce misleading nested output.
50+
quiet bool
51+
}
3752

3853
func NewGitProviderImpl(eventListener opctx.EventListener, cmdFactory opctx.CmdFactory) (*GitProviderImpl, error) {
3954
if eventListener == nil {
@@ -64,13 +79,10 @@ func (g *GitProviderImpl) Clone(ctx context.Context, repoURL, destDir string, op
6479
return errors.New("destination directory cannot be empty")
6580
}
6681

67-
args := []string{"clone"}
68-
69-
// Add options before URL and destination
70-
for _, opt := range options {
71-
args = append(args, opt()...)
72-
}
82+
// Resolve options into args and post-clone actions.
83+
resolved := resolveCloneOptions(options)
7384

85+
args := append([]string{"clone"}, resolved.args...)
7486
args = append(args, repoURL, destDir)
7587

7688
cmd := exec.CommandContext(ctx, "git", args...)
@@ -80,9 +92,10 @@ func (g *GitProviderImpl) Clone(ctx context.Context, repoURL, destDir string, op
8092
return fmt.Errorf("failed to create git command:\n%w", err)
8193
}
8294

83-
event := g.eventListener.StartEvent("Cloning git repo", "repoURL", repoURL)
84-
85-
defer event.End()
95+
if !resolved.quiet {
96+
event := g.eventListener.StartEvent("Cloning git repo", "repoURL", repoURL)
97+
defer event.End()
98+
}
8699

87100
err = wrappedCmd.Run(ctx)
88101
if err != nil {
@@ -163,9 +176,49 @@ func (g *GitProviderImpl) GetCommitHashBeforeDate(
163176
return output, nil
164177
}
165178

166-
// WithGitBranch returns a GitOptions that specifies the branch to clone.
179+
// GetCurrentCommit returns the current commit hash of the repository at the given directory, regardless of the date.
180+
func (g *GitProviderImpl) GetCurrentCommit(ctx context.Context, repoDir string) (string, error) {
181+
// Pass zero time to get the current commit
182+
return g.GetCommitHashBeforeDate(ctx, repoDir, time.Time{})
183+
}
184+
185+
// resolveCloneOptions collects all [GitOptions] into a [cloneOptions] struct.
186+
func resolveCloneOptions(options []GitOptions) cloneOptions {
187+
var resolved cloneOptions
188+
189+
for _, opt := range options {
190+
if opt == nil {
191+
continue
192+
}
193+
194+
opt(&resolved)
195+
}
196+
197+
return resolved
198+
}
199+
200+
// WithGitBranch returns a [GitOptions] that specifies the branch to clone.
167201
func WithGitBranch(branch string) GitOptions {
168-
return func() []string {
169-
return []string{"--branch", branch}
202+
return func(opts *cloneOptions) {
203+
opts.args = append(opts.args, "--branch", branch)
204+
}
205+
}
206+
207+
// WithQuiet returns a [GitOptions] that suppresses event emission during
208+
// the clone. Use this for internal operations (e.g., identity resolution)
209+
// that run concurrently and would produce misleading nested log output.
210+
func WithQuiet() GitOptions {
211+
return func(opts *cloneOptions) {
212+
opts.quiet = true
213+
}
214+
}
215+
216+
// WithMetadataOnly returns a [GitOptions] that performs a blobless partial clone
217+
// (--filter=blob:none --no-checkout). Only git metadata is fetched; no working-tree
218+
// files are checked out.
219+
func WithMetadataOnly() GitOptions {
220+
return func(opts *cloneOptions) {
221+
opts.args = append(opts.args, "--filter=blob:none")
222+
opts.args = append(opts.args, "--no-checkout")
170223
}
171224
}

internal/utils/git/git_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,71 @@ func TestCloneNonExistentRepo(t *testing.T) {
161161
require.Error(t, err)
162162
assert.Contains(t, err.Error(), errMsgCloneFailed)
163163
}
164+
165+
func TestGetCurrentCommit(t *testing.T) {
166+
ctrl := gomock.NewController(t)
167+
168+
cmdFactory, err := externalcmd.NewCmdFactory(
169+
opctx_test.NewNoOpMockDryRunnable(ctrl),
170+
opctx_test.NewNoOpMockEventListener(ctrl),
171+
)
172+
require.NoError(t, err)
173+
174+
provider, err := git.NewGitProviderImpl(opctx_test.NewNoOpMockEventListener(ctrl), cmdFactory)
175+
require.NoError(t, err)
176+
177+
destDir := filepath.Join(t.TempDir(), testRepoSubDir)
178+
179+
err = provider.Clone(context.Background(), testRepoURL, destDir)
180+
require.NoError(t, err)
181+
182+
commitHash, err := provider.GetCurrentCommit(t.Context(), destDir)
183+
require.NoError(t, err)
184+
185+
// A full SHA-1 hash is 40 hex characters.
186+
assert.Len(t, commitHash, 40)
187+
assert.Regexp(t, `^[0-9a-f]{40}$`, commitHash)
188+
}
189+
190+
func TestGetCurrentCommitEmptyRepoDir(t *testing.T) {
191+
ctrl := gomock.NewController(t)
192+
193+
provider, err := git.NewGitProviderImpl(
194+
opctx_test.NewMockEventListener(ctrl),
195+
opctx_test.NewMockCmdFactory(ctrl),
196+
)
197+
require.NoError(t, err)
198+
199+
_, err = provider.GetCurrentCommit(context.Background(), "")
200+
require.Error(t, err)
201+
assert.Contains(t, err.Error(), "repository directory cannot be empty")
202+
}
203+
204+
func TestCloneWithMetadataOnly(t *testing.T) {
205+
ctrl := gomock.NewController(t)
206+
207+
cmdFactory, err := externalcmd.NewCmdFactory(
208+
opctx_test.NewNoOpMockDryRunnable(ctrl),
209+
opctx_test.NewNoOpMockEventListener(ctrl),
210+
)
211+
require.NoError(t, err)
212+
213+
provider, err := git.NewGitProviderImpl(opctx_test.NewNoOpMockEventListener(ctrl), cmdFactory)
214+
require.NoError(t, err)
215+
216+
destDir := filepath.Join(t.TempDir(), testRepoSubDir)
217+
218+
err = provider.Clone(
219+
t.Context(),
220+
testRepoURL,
221+
destDir,
222+
git.WithMetadataOnly(),
223+
)
224+
225+
require.NoError(t, err)
226+
227+
// Git metadata should exist.
228+
assert.DirExists(t, filepath.Join(destDir, testGitDir))
229+
// --no-checkout means the working tree file should NOT be present.
230+
assert.NoFileExists(t, filepath.Join(destDir, testRepoReadmeFile))
231+
}

internal/utils/git/git_test/git_mocks.go

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)