Skip to content

Commit 2790f9f

Browse files
authored
feat(Provider): Add ResolveSourceIdentity() to the source provider interface (#45)
1 parent 3c58863 commit 2790f9f

File tree

8 files changed

+796
-26
lines changed

8 files changed

+796
-26
lines changed

internal/providers/sourceproviders/fedorasourceprovider.go

Lines changed: 109 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -221,54 +221,142 @@ func (g *FedoraSourcesProviderImpl) renameSpecIfNeeded(dir, upstreamName, compon
221221
return nil
222222
}
223223

224-
// checkoutTargetCommit determines the appropriate commit to use and checks it out.
225-
// Priority order:
226-
// 1. Explicit upstream commit hash - specified per-component via upstream-commit
227-
// 2. Upstream distro snapshot - snapshot time from the provider's resolved distro
228-
// 3. Default - use current HEAD (no checkout needed)
224+
// checkoutTargetCommit resolves the effective commit via [resolveEffectiveCommitHash]
225+
// and checks it out in the cloned repository.
229226
func (g *FedoraSourcesProviderImpl) checkoutTargetCommit(
230227
ctx context.Context,
231228
upstreamCommit string,
232229
repoDir string,
233230
) error {
231+
commitHash, err := g.resolveEffectiveCommitHash(ctx, repoDir, upstreamCommit, slog.LevelInfo)
232+
if err != nil {
233+
return err
234+
}
235+
236+
if err := g.gitProvider.Checkout(ctx, repoDir, commitHash); err != nil {
237+
return fmt.Errorf("failed to checkout commit %#q:\n%w", commitHash, err)
238+
}
239+
240+
return nil
241+
}
242+
243+
// ResolveIdentity implements [SourceIdentityProvider] by resolving the upstream
244+
// commit hash for the component. All resolution priority logic is in
245+
// [resolveEffectiveCommitHash], called via [resolveCommit].
246+
func (g *FedoraSourcesProviderImpl) ResolveIdentity(
247+
ctx context.Context,
248+
component components.Component,
249+
) (string, error) {
250+
if component.GetName() == "" {
251+
return "", errors.New("component name cannot be empty")
252+
}
253+
254+
upstreamName := component.GetConfig().Spec.UpstreamName
255+
if upstreamName == "" {
256+
upstreamName = component.GetName()
257+
}
258+
259+
gitRepoURL := strings.ReplaceAll(g.distroGitBaseURI, "$pkg", upstreamName)
260+
261+
return g.resolveCommit(ctx, gitRepoURL, upstreamName, component.GetConfig().Spec.UpstreamCommit)
262+
}
263+
264+
// resolveCommit determines the effective commit via [resolveEffectiveCommitHash].
265+
// For pinned commits (case 1), it returns immediately without cloning. For snapshot
266+
// and HEAD cases, it performs a metadata-only clone to resolve the commit hash.
267+
func (g *FedoraSourcesProviderImpl) resolveCommit(
268+
ctx context.Context, gitRepoURL string, upstreamName string, upstreamCommit string,
269+
) (string, error) {
234270
// Case 1: Explicit upstream commit hash specified per-component
235271
if upstreamCommit != "" {
236-
slog.Info("Using explicit upstream commit hash",
237-
"commitHash", upstreamCommit)
272+
return g.resolveEffectiveCommitHash(ctx, "", upstreamCommit, slog.LevelDebug)
273+
}
238274

239-
if err := g.gitProvider.Checkout(ctx, repoDir, upstreamCommit); err != nil {
240-
return fmt.Errorf("failed to checkout upstream commit %#q:\n%w", upstreamCommit, err)
275+
// Cases 2 & 3: need a metadata-only clone to resolve snapshot or HEAD commit.
276+
tempDir, err := fileutils.MkdirTempInTempDir(g.fs, "azldev-identity-snapshot-")
277+
if err != nil {
278+
return "", fmt.Errorf("creating temp directory for snapshot clone:\n%w", err)
279+
}
280+
281+
defer func() {
282+
if removeErr := g.fs.RemoveAll(tempDir); removeErr != nil {
283+
slog.Debug("Failed to clean up snapshot clone temp directory",
284+
"path", tempDir, "error", removeErr)
241285
}
286+
}()
242287

243-
return nil
288+
// Clone a single branch to resolve the snapshot commit. We use a full
289+
// (non-shallow) clone because not all git servers support --shallow-since
290+
// (e.g., Pagure returns "the remote end hung up unexpectedly").
291+
err = retry.Do(ctx, g.retryConfig, func() error {
292+
_ = g.fs.RemoveAll(tempDir)
293+
_ = fileutils.MkdirAll(g.fs, tempDir)
294+
295+
return g.gitProvider.Clone(ctx, gitRepoURL, tempDir,
296+
git.WithGitBranch(g.distroGitBranch),
297+
git.WithMetadataOnly(),
298+
git.WithQuiet(),
299+
)
300+
})
301+
if err != nil {
302+
return "", fmt.Errorf("partial clone for identity of %#q:\n%w", upstreamName, err)
303+
}
304+
305+
commitHash, err := g.resolveEffectiveCommitHash(ctx, tempDir, "", slog.LevelDebug)
306+
if err != nil {
307+
return "", fmt.Errorf("resolving commit for %#q:\n%w", upstreamName, err)
308+
}
309+
310+
return commitHash, nil
311+
}
312+
313+
// resolveEffectiveCommitHash is the single source of truth for which commit a
314+
// component should use from a cloned repository.
315+
//
316+
// Priority:
317+
// 1. Explicit upstream commit hash (pinned per-component).
318+
// 2. Snapshot time — commit immediately before the snapshot date.
319+
// 3. Default — current HEAD.
320+
func (g *FedoraSourcesProviderImpl) resolveEffectiveCommitHash(
321+
ctx context.Context,
322+
repoDir string,
323+
upstreamCommit string,
324+
logLevel slog.Level,
325+
) (string, error) {
326+
// Case 1: Explicit upstream commit hash specified per-component.
327+
if upstreamCommit != "" {
328+
slog.Log(ctx, logLevel, "Using explicit upstream commit hash", "commitHash", upstreamCommit)
329+
330+
return upstreamCommit, nil
244331
}
245332

246-
// Case 2: Provider has a snapshot time configured from the resolved distro
333+
// Case 2: Provider has a snapshot time configured from the resolved distro.
247334
if g.snapshotTime != "" {
248335
snapshotDateTime, err := time.Parse(time.RFC3339, g.snapshotTime)
249336
if err != nil {
250-
return fmt.Errorf("invalid snapshot time %#q:\n%w", g.snapshotTime, err)
337+
return "", fmt.Errorf("invalid snapshot time %#q:\n%w", g.snapshotTime, err)
251338
}
252339

253340
commitHash, err := g.gitProvider.GetCommitHashBeforeDate(ctx, repoDir, snapshotDateTime)
254341
if err != nil {
255-
return fmt.Errorf("failed to get commit hash for snapshot time %s:\n%w",
342+
return "", fmt.Errorf("resolving commit for snapshot time %s:\n%w",
256343
snapshotDateTime.Format(time.RFC3339), err)
257344
}
258345

259-
slog.Info("Using upstream distro snapshot time",
346+
slog.Log(ctx, logLevel, "Using upstream distro snapshot time",
260347
"snapshotDateTime", snapshotDateTime.Format(time.RFC3339),
261348
"commitHash", commitHash)
262349

263-
if err := g.gitProvider.Checkout(ctx, repoDir, commitHash); err != nil {
264-
return fmt.Errorf("failed to checkout snapshot commit %#q:\n%w", commitHash, err)
265-
}
350+
return commitHash, nil
351+
}
266352

267-
return nil
353+
// Case 3: Default — use current HEAD.
354+
commitHash, err := g.gitProvider.GetCurrentCommit(ctx, repoDir)
355+
if err != nil {
356+
return "", fmt.Errorf("resolving current HEAD commit:\n%w", err)
268357
}
269358

270-
// Case 3: Default - use current HEAD (already checked out by clone)
271-
slog.Info("Using current HEAD (no snapshot time configured)")
359+
slog.Log(ctx, logLevel, "Using current HEAD", "commitHash", commitHash)
272360

273-
return nil
361+
return commitHash, nil
274362
}

internal/providers/sourceproviders/fedorasourceprovider_test.go

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,14 @@ func TestGetComponentFromGit(t *testing.T) {
169169
return fileutils.WriteFile(env.FS(), sourcesPath, []byte(sourcesContent), fileperms.PublicFile)
170170
})
171171

172+
mockGitProvider.EXPECT().
173+
GetCurrentCommit(gomock.Any(), gomock.Any()).
174+
Return("head123abc", nil)
175+
176+
mockGitProvider.EXPECT().
177+
Checkout(gomock.Any(), gomock.Any(), "head123abc").
178+
Return(nil)
179+
172180
provider, err := sourceproviders.NewFedoraSourcesProviderImpl(
173181
env.FS(),
174182
env.DryRunnable,
@@ -255,6 +263,14 @@ func TestGetComponentFromGit(t *testing.T) {
255263
return fileutils.WriteFile(env.FS(), sourcesPath, []byte(sourcesContent), fileperms.PublicFile)
256264
})
257265

266+
mockGitProvider.EXPECT().
267+
GetCurrentCommit(gomock.Any(), gomock.Any()).
268+
Return("head123abc", nil)
269+
270+
mockGitProvider.EXPECT().
271+
Checkout(gomock.Any(), gomock.Any(), "head123abc").
272+
Return(nil)
273+
258274
const testDestDir = "/output-preexisting"
259275

260276
provider, err := sourceproviders.NewFedoraSourcesProviderImpl(
@@ -323,6 +339,14 @@ func TestGetComponentFromGit(t *testing.T) {
323339
return fileutils.WriteFile(env.FS(), specPath, []byte("Name: "+testPackageName), fileperms.PublicFile)
324340
})
325341

342+
mockGitProvider.EXPECT().
343+
GetCurrentCommit(gomock.Any(), gomock.Any()).
344+
Return("head123abc", nil)
345+
346+
mockGitProvider.EXPECT().
347+
Checkout(gomock.Any(), gomock.Any(), "head123abc").
348+
Return(nil)
349+
326350
provider, err := sourceproviders.NewFedoraSourcesProviderImpl(
327351
env.FS(),
328352
env.DryRunnable,
@@ -437,6 +461,14 @@ func TestGetComponentFromGit(t *testing.T) {
437461
Clone(gomock.Any(), repoURL, gomock.Any(), gomock.Any()).
438462
Return(nil)
439463

464+
mockGitProvider.EXPECT().
465+
GetCurrentCommit(gomock.Any(), gomock.Any()).
466+
Return("head123abc", nil)
467+
468+
mockGitProvider.EXPECT().
469+
Checkout(gomock.Any(), gomock.Any(), "head123abc").
470+
Return(nil)
471+
440472
// But extractor fails - note it receives the component name, not destDir
441473
extractorError := errors.New("extraction failed")
442474
mockExtractor.EXPECT().
@@ -490,6 +522,14 @@ func TestGetComponentFromGit(t *testing.T) {
490522
)
491523
})
492524

525+
mockGitProvider.EXPECT().
526+
GetCurrentCommit(gomock.Any(), gomock.Any()).
527+
Return("head123abc", nil)
528+
529+
mockGitProvider.EXPECT().
530+
Checkout(gomock.Any(), gomock.Any(), "head123abc").
531+
Return(nil)
532+
493533
// Extractor succeeds
494534
mockExtractor.EXPECT().
495535
ExtractSourcesFromRepo(gomock.Any(), gomock.Any(), upstreamName, gomock.Any(), gomock.Any()).
@@ -544,6 +584,14 @@ func TestGetComponentFromGit(t *testing.T) {
544584
Clone(gomock.Any(), upstreamRepoURL, gomock.Any(), gomock.Any()).
545585
Return(nil)
546586

587+
mockGitProvider.EXPECT().
588+
GetCurrentCommit(gomock.Any(), gomock.Any()).
589+
Return("head123abc", nil)
590+
591+
mockGitProvider.EXPECT().
592+
Checkout(gomock.Any(), gomock.Any(), "head123abc").
593+
Return(nil)
594+
547595
// Extractor succeeds
548596
mockExtractor.EXPECT().
549597
ExtractSourcesFromRepo(gomock.Any(), gomock.Any(), upstreamName, gomock.Any(), gomock.Any()).
@@ -652,7 +700,17 @@ func TestCheckoutTargetCommit(t *testing.T) {
652700
return fileutils.WriteFile(env.FS(), specPath, []byte("Name: "+testPackageName), fileperms.PublicFile)
653701
})
654702

655-
// Should NOT call GetCommitHashBeforeDate or Checkout - uses HEAD from clone
703+
headCommitHash := "head123abc"
704+
705+
// GetCurrentCommit is called to resolve HEAD
706+
mockGitProvider.EXPECT().
707+
GetCurrentCommit(gomock.Any(), gomock.Any()).
708+
Return(headCommitHash, nil)
709+
710+
// Then checkout that commit
711+
mockGitProvider.EXPECT().
712+
Checkout(gomock.Any(), gomock.Any(), headCommitHash).
713+
Return(nil)
656714

657715
// Extractor succeeds
658716
mockExtractor.EXPECT().
@@ -700,7 +758,7 @@ func TestCheckoutTargetCommit(t *testing.T) {
700758

701759
err = provider.GetComponent(context.Background(), mockComponent, destDir)
702760
require.Error(t, err)
703-
assert.Contains(t, err.Error(), "failed to get commit hash for snapshot time")
761+
assert.Contains(t, err.Error(), "resolving commit for snapshot time")
704762
assert.ErrorIs(t, err, hashError)
705763
})
706764

@@ -747,7 +805,7 @@ func TestCheckoutTargetCommit(t *testing.T) {
747805

748806
err = provider.GetComponent(context.Background(), mockComponent, destDir)
749807
require.Error(t, err)
750-
assert.Contains(t, err.Error(), "failed to checkout snapshot commit")
808+
assert.Contains(t, err.Error(), "failed to checkout commit")
751809
assert.ErrorIs(t, err, checkoutError)
752810
})
753811

@@ -929,7 +987,7 @@ func TestCheckoutTargetCommit_UpstreamCommit(t *testing.T) {
929987

930988
err = provider.GetComponent(context.Background(), mockComponent, destDir)
931989
require.Error(t, err)
932-
assert.Contains(t, err.Error(), "failed to checkout upstream commit")
990+
assert.Contains(t, err.Error(), "failed to checkout commit")
933991
assert.ErrorIs(t, err, checkoutError)
934992
})
935993
}

0 commit comments

Comments
 (0)