Skip to content

Commit 854ff0c

Browse files
committed
rework git
1 parent fed781b commit 854ff0c

File tree

8 files changed

+277
-48
lines changed

8 files changed

+277
-48
lines changed

internal/app/azldev/cmds/component/identity.go

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func identityOnAppInit(_ *azldev.App, parentCmd *cobra.Command) {
2828
parentCmd.AddCommand(NewComponentIdentityCommand())
2929
}
3030

31-
// Constructs a [cobra.Command] for "component identity" CLI subcommand.
31+
// NewComponentIdentityCommand constructs a [cobra.Command] for "component identity" CLI subcommand.
3232
func NewComponentIdentityCommand() *cobra.Command {
3333
options := &IdentityComponentOptions{}
3434

@@ -116,6 +116,10 @@ func computeIdentitiesParallel(
116116
"count", len(comps))
117117
defer progressEvent.End()
118118

119+
// Create a cancellable child env so we can stop remaining goroutines on first error.
120+
workerEnv, cancel := env.WithCancel()
121+
defer cancel()
122+
119123
type indexedResult struct {
120124
index int
121125
result ComponentIdentityResult
@@ -137,14 +141,14 @@ func computeIdentitiesParallel(
137141
select {
138142
case semaphore <- struct{}{}:
139143
defer func() { <-semaphore }()
140-
case <-env.Done():
141-
resultsChan <- indexedResult{index: compIdx, err: env.Err()}
144+
case <-workerEnv.Done():
145+
resultsChan <- indexedResult{index: compIdx, err: workerEnv.Err()}
142146

143147
return
144148
}
145149

146150
result, computeErr := computeSingleIdentity(
147-
env, comp, distroRef,
151+
workerEnv, comp, distroRef,
148152
)
149153

150154
resultsChan <- indexedResult{index: compIdx, result: result, err: computeErr}
@@ -163,15 +167,28 @@ func computeIdentitiesParallel(
163167
var completed int64
164168

165169
total := int64(len(comps))
170+
var firstErr error
166171

167172
for indexed := range resultsChan {
168173
if indexed.err != nil {
169-
return nil, indexed.err
174+
if firstErr == nil {
175+
firstErr = indexed.err
176+
cancel()
177+
}
178+
179+
// Drain remaining results so the closer goroutine can finish.
180+
continue
170181
}
171182

172-
results[indexed.index] = indexed.result
173-
completed++
174-
progressEvent.SetProgress(completed, total)
183+
if firstErr == nil {
184+
results[indexed.index] = indexed.result
185+
completed++
186+
progressEvent.SetProgress(completed, total)
187+
}
188+
}
189+
190+
if firstErr != nil {
191+
return nil, firstErr
175192
}
176193

177194
return results, nil
@@ -188,7 +205,7 @@ func computeSingleIdentity(
188205
componentName := comp.GetName()
189206

190207
identityOpts := fingerprint.IdentityOptions{
191-
AffectsCommitCount: countAffectsCommits(config, componentName),
208+
AffectsCommitCount: countAffectsCommits(env, config, componentName),
192209
}
193210

194211
// Resolve source identity, selecting the appropriate method based on source type (local vs. upstream etc.).
@@ -236,9 +253,9 @@ func resolveDistroForIdentity(
236253
// countAffectsCommits counts the number of "Affects: <componentName>" commits in the
237254
// project repo. Returns 0 if the count cannot be determined (e.g., no git repo).
238255
func countAffectsCommits(
239-
config *projectconfig.ComponentConfig, componentName string,
256+
env *azldev.Env, config *projectconfig.ComponentConfig, componentName string,
240257
) int {
241-
commits, err := sources.CountAffectsCommits(config, componentName)
258+
commits, err := sources.CountAffectsCommits(env.Context(), config, componentName)
242259
if err != nil {
243260
slog.Debug("Could not count Affects commits; defaulting to 0",
244261
"component", componentName, "error", err)
@@ -251,8 +268,14 @@ func countAffectsCommits(
251268

252269
// resolveSourceIdentityForComponent returns a deterministic identity string for the
253270
// component's source. Local components are handled directly (spec directory hash) to
254-
// avoid constructing a full source manager. Upstream components go through the source
255-
// manager which handles provider dispatch and retry.
271+
// avoid constructing a full [sourceproviders.SourceManager]. This avoids the overhead of
272+
// resolving a distro and creating upstream providers that won't be used, and ensures
273+
// local-only projects (with no distro configured) work without error.
274+
// Upstream components go through the source manager which handles provider dispatch
275+
// and retry.
276+
//
277+
// NOTE: This logic mirrors [sourceproviders.sourceManager.ResolveSourceIdentity] for the
278+
// local path. Changes to source type dispatch must be kept in sync.
256279
func resolveSourceIdentityForComponent(
257280
env *azldev.Env, comp components.Component,
258281
) (string, error) {

internal/app/azldev/core/sources/sourceprep.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ func (p *sourcePreparerImpl) PrepareSources(
157157

158158
// Record the changes as synthetic git history when dist-git creation is enabled.
159159
if p.withGitRepo {
160-
if err := p.trySyntheticHistory(component, outputDir); err != nil {
160+
if err := p.trySyntheticHistory(ctx, component, outputDir); err != nil {
161161
return fmt.Errorf("failed to generate synthetic history for component %#q:\n%w",
162162
component.GetName(), err)
163163
}
@@ -296,13 +296,14 @@ func initSourcesRepo(sourcesDirPath string) (*gogit.Repository, error) {
296296
// Returns nil when there are no Affects commits to apply.
297297
// Returns a non-nil error if history generation fails.
298298
func (p *sourcePreparerImpl) trySyntheticHistory(
299+
ctx context.Context,
299300
component components.Component,
300301
sourcesDirPath string,
301302
) error {
302303
config := component.GetConfig()
303304

304305
// Build commit metadata from Affects commits.
305-
commits, err := buildSyntheticCommits(config, component.GetName())
306+
commits, err := buildSyntheticCommits(ctx, config, component.GetName())
306307
if err != nil {
307308
return fmt.Errorf("failed to build synthetic commits:\n%w", err)
308309
}

internal/app/azldev/core/sources/synthistory.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package sources
55

66
import (
7+
"context"
78
"errors"
89
"fmt"
910
"log/slog"
@@ -43,8 +44,8 @@ type CommitMetadata struct {
4344

4445
// FindAffectsCommits walks the git log from HEAD and returns metadata for all commits
4546
// whose message contains "Affects: <componentName>". Results are sorted chronologically
46-
// (oldest first).
47-
func FindAffectsCommits(repo *gogit.Repository, componentName string) ([]CommitMetadata, error) {
47+
// (oldest first). The ctx parameter is checked between commits for early cancellation.
48+
func FindAffectsCommits(ctx context.Context, repo *gogit.Repository, componentName string) ([]CommitMetadata, error) {
4849
head, err := repo.Head()
4950
if err != nil {
5051
return nil, fmt.Errorf("failed to get HEAD reference:\n%w", err)
@@ -60,6 +61,10 @@ func FindAffectsCommits(repo *gogit.Repository, componentName string) ([]CommitM
6061
re := regexp.MustCompile(affectsRegexPattern + regexp.QuoteMeta(componentName) + `(?:$|\s|[,;])`)
6162

6263
err = commitIter.ForEach(func(commit *object.Commit) error {
64+
if ctx.Err() != nil {
65+
return ctx.Err()
66+
}
67+
6368
if re.MatchString(commit.Message) {
6469
matches = append(matches, CommitMetadata{
6570
Hash: commit.Hash.String(),
@@ -86,7 +91,7 @@ func FindAffectsCommits(repo *gogit.Repository, componentName string) ([]CommitM
8691
// project repository that contains the component's config file. Returns 0 and an error
8792
// if the repository cannot be opened or the config file path is unavailable.
8893
func CountAffectsCommits(
89-
config *projectconfig.ComponentConfig, componentName string,
94+
ctx context.Context, config *projectconfig.ComponentConfig, componentName string,
9095
) (int, error) {
9196
configFilePath, err := resolveConfigFilePath(config, componentName)
9297
if err != nil {
@@ -98,7 +103,7 @@ func CountAffectsCommits(
98103
return 0, fmt.Errorf("opening project repo:\n%w", err)
99104
}
100105

101-
commits, err := FindAffectsCommits(projectRepo, componentName)
106+
commits, err := FindAffectsCommits(ctx, projectRepo, componentName)
102107
if err != nil {
103108
return 0, fmt.Errorf("finding Affects commits:\n%w", err)
104109
}
@@ -164,7 +169,7 @@ func CommitSyntheticHistory(
164169
//
165170
// Returns nil when no matching commits are found.
166171
func buildSyntheticCommits(
167-
config *projectconfig.ComponentConfig, componentName string,
172+
ctx context.Context, config *projectconfig.ComponentConfig, componentName string,
168173
) ([]CommitMetadata, error) {
169174
configFilePath, err := resolveConfigFilePath(config, componentName)
170175
if err != nil {
@@ -180,7 +185,7 @@ func buildSyntheticCommits(
180185
return nil, err
181186
}
182187

183-
affectsCommits, err := FindAffectsCommits(projectRepo, componentName)
188+
affectsCommits, err := FindAffectsCommits(ctx, projectRepo, componentName)
184189
if err != nil {
185190
return nil, fmt.Errorf("failed to find Affects commits for component %#q:\n%w", componentName, err)
186191
}

internal/app/azldev/core/sources/synthistory_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func TestFindAffectsCommits(t *testing.T) {
8181
"Charlie", "charlie@example.com",
8282
time.Date(2025, 3, 1, 10, 0, 0, 0, time.UTC))
8383

84-
results, err := sources.FindAffectsCommits(repo, "curl")
84+
results, err := sources.FindAffectsCommits(t.Context(), repo, "curl")
8585
require.NoError(t, err)
8686

8787
// Expect 2 matching commits, oldest first.
@@ -107,7 +107,7 @@ func TestFindAffectsCommits_NoMatches(t *testing.T) {
107107
"Alice", "alice@example.com",
108108
time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC))
109109

110-
results, err := sources.FindAffectsCommits(repo, "curl")
110+
results, err := sources.FindAffectsCommits(t.Context(), repo, "curl")
111111
require.NoError(t, err)
112112
assert.Empty(t, results)
113113
}
@@ -130,13 +130,13 @@ func TestFindAffectsCommits_MultipleComponents(t *testing.T) {
130130
"Charlie", "charlie@example.com",
131131
time.Date(2025, 3, 1, 10, 0, 0, 0, time.UTC))
132132

133-
curlResults, err := sources.FindAffectsCommits(repo, "curl")
133+
curlResults, err := sources.FindAffectsCommits(t.Context(), repo, "curl")
134134
require.NoError(t, err)
135135
require.Len(t, curlResults, 2, "curl should match 2 commits")
136136
assert.Equal(t, "Alice", curlResults[0].Author)
137137
assert.Equal(t, "Charlie", curlResults[1].Author)
138138

139-
wgetResults, err := sources.FindAffectsCommits(repo, "wget")
139+
wgetResults, err := sources.FindAffectsCommits(t.Context(), repo, "wget")
140140
require.NoError(t, err)
141141
require.Len(t, wgetResults, 2, "wget should match 2 commits")
142142
assert.Equal(t, "Bob", wgetResults[0].Author)
@@ -158,13 +158,13 @@ func TestFindAffectsCommits_NoSubstringMatch(t *testing.T) {
158158
time.Date(2025, 2, 1, 10, 0, 0, 0, time.UTC))
159159

160160
// Searching for "curl" matches only Bob's commit (exact component name).
161-
curlResults, err := sources.FindAffectsCommits(repo, "curl")
161+
curlResults, err := sources.FindAffectsCommits(t.Context(), repo, "curl")
162162
require.NoError(t, err)
163163
require.Len(t, curlResults, 1, "exact match should not include curl-minimal commit")
164164
assert.Equal(t, "Bob", curlResults[0].Author)
165165

166166
// Searching for "curl-minimal" matches only Alice's commit.
167-
minimalResults, err := sources.FindAffectsCommits(repo, "curl-minimal")
167+
minimalResults, err := sources.FindAffectsCommits(t.Context(), repo, "curl-minimal")
168168
require.NoError(t, err)
169169
require.Len(t, minimalResults, 1)
170170
assert.Equal(t, "Alice", minimalResults[0].Author)
@@ -179,7 +179,7 @@ func TestFindAffectsCommits_AffectsInSubject(t *testing.T) {
179179
"Alice", "alice@example.com",
180180
time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC))
181181

182-
results, err := sources.FindAffectsCommits(repo, "curl")
182+
results, err := sources.FindAffectsCommits(t.Context(), repo, "curl")
183183
require.NoError(t, err)
184184
require.Len(t, results, 1)
185185
assert.Equal(t, "Alice", results[0].Author)
@@ -204,13 +204,13 @@ func TestFindAffectsCommits_CaseSensitive(t *testing.T) {
204204
time.Date(2025, 3, 1, 10, 0, 0, 0, time.UTC))
205205

206206
// Matching is case-sensitive: searching for "kernel" only matches the exact-case commit.
207-
results, err := sources.FindAffectsCommits(repo, "kernel")
207+
results, err := sources.FindAffectsCommits(t.Context(), repo, "kernel")
208208
require.NoError(t, err)
209209
require.Len(t, results, 1)
210210
assert.Equal(t, "Charlie", results[0].Author)
211211

212212
// Searching for "Kernel" matches only Alice's commit (exact case on component name).
213-
results, err = sources.FindAffectsCommits(repo, "Kernel")
213+
results, err = sources.FindAffectsCommits(t.Context(), repo, "Kernel")
214214
require.NoError(t, err)
215215
require.Len(t, results, 1)
216216
assert.Equal(t, "Alice", results[0].Author)

internal/app/azldev/env.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,19 @@ func (env *Env) Context() context.Context {
211211
return env.ctx
212212
}
213213

214+
// WithCancel returns a shallow copy of the [Env] with a child [context.Context]
215+
// derived from [context.WithCancel]. The returned [Env] shares all features
216+
// (FS, config, event listener, cmd factory, etc.) with the original but has an
217+
// independently cancellable context. The caller must call the returned
218+
// [context.CancelFunc] when done.
219+
func (env *Env) WithCancel() (*Env, context.CancelFunc) {
220+
childCtx, cancel := context.WithCancel(env.ctx)
221+
childEnv := *env
222+
childEnv.ctx = childCtx
223+
224+
return &childEnv, cancel
225+
}
226+
214227
// ConfirmAutoResolution prompts the user to confirm auto-resolution of a problem. The provided
215228
// text is displayed to the user as explanation.
216229
func (env *Env) ConfirmAutoResolution(text string) bool {

internal/providers/sourceproviders/fedorasourceprovider.go

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ func (g *FedoraSourcesProviderImpl) GetComponent(
124124
defer fileutils.RemoveAllAndUpdateErrorIfNil(g.fs, tempDir, &err)
125125

126126
// Clone the repository to temp directory with retry for transient network failures.
127+
// NOTE: We intentionally do a full clone here (no --shallow-since) because downstream
128+
// consumers need complete history — rpmautospec's %autorelease counts commits to compute
129+
// the release number etc.
127130
err = retry.Do(ctx, g.retryConfig, func() error {
128131
// Clean up temp directory contents from any prior failed clone attempt.
129132
_ = g.fs.RemoveAll(tempDir)
@@ -276,10 +279,8 @@ func (g *FedoraSourcesProviderImpl) checkoutTargetCommit(
276279
// ResolveSourceIdentity implements [SourceIdentityProvider] by resolving the upstream
277280
// commit hash for the component. Resolution priority matches [checkoutTargetCommit]:
278281
// 1. Explicit upstream commit hash (pinned per-component) — returned directly.
279-
// 2. Otherwise — query HEAD of the dist-git branch via ls-remote.
280-
//
281-
// Snapshot-time resolution is not supported here because it requires a full clone
282-
// to walk git history. Snapshot changes are tracked separately via the config hash.
282+
// 2. Snapshot time — shallow clone + rev-list to find the commit at the snapshot date.
283+
// 3. Default — query HEAD of the dist-git branch via ls-remote.
283284
func (g *FedoraSourcesProviderImpl) ResolveSourceIdentity(
284285
ctx context.Context,
285286
component components.Component,
@@ -293,14 +294,78 @@ func (g *FedoraSourcesProviderImpl) ResolveSourceIdentity(
293294
return pinnedCommit, nil
294295
}
295296

296-
// Case 2: Query HEAD of the dist-git branch.
297297
upstreamName := component.GetConfig().Spec.UpstreamName
298298
if upstreamName == "" {
299299
upstreamName = component.GetName()
300300
}
301301

302302
gitRepoURL := strings.ReplaceAll(g.distroGitBaseURI, "$pkg", upstreamName)
303303

304+
// Case 2: Snapshot time — need a shallow clone to resolve the commit at that date.
305+
if g.snapshotTime != "" {
306+
return g.resolveSnapshotCommit(ctx, gitRepoURL, upstreamName)
307+
}
308+
309+
// Case 3: No snapshot — query HEAD of the dist-git branch via ls-remote.
310+
return g.resolveBranchHead(ctx, gitRepoURL, upstreamName)
311+
}
312+
313+
// resolveSnapshotCommit clones the branch with --shallow-since and resolves the
314+
// commit at (or before) the configured snapshot time.
315+
func (g *FedoraSourcesProviderImpl) resolveSnapshotCommit(
316+
ctx context.Context, gitRepoURL string, upstreamName string,
317+
) (string, error) {
318+
snapshotDateTime, err := time.Parse(time.RFC3339, g.snapshotTime)
319+
if err != nil {
320+
return "", fmt.Errorf("invalid snapshot time %#q:\n%w", g.snapshotTime, err)
321+
}
322+
323+
tempDir, err := fileutils.MkdirTempInTempDir(g.fs, "azldev-identity-snapshot-")
324+
if err != nil {
325+
return "", fmt.Errorf("creating temp directory for snapshot clone:\n%w", err)
326+
}
327+
328+
defer func() {
329+
if removeErr := g.fs.RemoveAll(tempDir); removeErr != nil {
330+
slog.Debug("Failed to clean up snapshot clone temp directory",
331+
"path", tempDir, "error", removeErr)
332+
}
333+
}()
334+
335+
// Clone a single branch to resolve the snapshot commit. We use a full
336+
// (non-shallow) clone because not all git servers support --shallow-since
337+
// (e.g., Pagure returns "the remote end hung up unexpectedly").
338+
err = retry.Do(ctx, g.retryConfig, func() error {
339+
_ = g.fs.RemoveAll(tempDir)
340+
_ = fileutils.MkdirAll(g.fs, tempDir)
341+
342+
return g.gitProvider.Clone(ctx, gitRepoURL, tempDir,
343+
git.WithGitBranch(g.distroGitBranch),
344+
git.WithQuiet(),
345+
)
346+
})
347+
if err != nil {
348+
return "", fmt.Errorf("shallow clone for snapshot identity of %#q:\n%w", upstreamName, err)
349+
}
350+
351+
commitHash, err := g.gitProvider.GetCommitHashBeforeDate(ctx, tempDir, snapshotDateTime)
352+
if err != nil {
353+
return "", fmt.Errorf("resolving snapshot commit for %#q at %s:\n%w",
354+
upstreamName, snapshotDateTime.Format(time.RFC3339), err)
355+
}
356+
357+
slog.Debug("Resolved snapshot commit for identity",
358+
"component", upstreamName,
359+
"snapshot", g.snapshotTime,
360+
"commit", commitHash)
361+
362+
return commitHash, nil
363+
}
364+
365+
// resolveBranchHead queries the HEAD commit of the dist-git branch via ls-remote.
366+
func (g *FedoraSourcesProviderImpl) resolveBranchHead(
367+
ctx context.Context, gitRepoURL string, upstreamName string,
368+
) (string, error) {
304369
var commitHash string
305370

306371
retryErr := retry.Do(ctx, g.retryConfig, func() error {

0 commit comments

Comments
 (0)