Skip to content

Commit 60a7b8b

Browse files
Copilotddstreet
authored andcommitted
feat(sources): derive synthetic history from upstream-commit config
Add the lock-file-free source of synthetic dist-git history. Instead of walking a component's lock file for input-fingerprint changes, the new discovery path walks the generated upstream-commit TOML that pinned the component and records the pin held at each commit that touched it, including the pin carried by a commit that deletes the file. Discovery is the only part that differs: the recorded change data is identical, so interleaving and replay are shared with the default mode. No fork point is persisted in this mode, so the upstream walk is bounded by the repository's first-parent root rather than by an import commit. Preparers select the source with the new WithoutLockfileHistory option, which is rejected together with dirty detection because there is no fingerprint to compare a working tree against. Component build, render, and prepare-sources now assemble their git-repo preparer options through a single mode-aware helper. Refs: microsoft#323
1 parent 3d8739b commit 60a7b8b

8 files changed

Lines changed: 444 additions & 34 deletions

File tree

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -269,10 +269,7 @@ func buildComponent(
269269

270270
var preparerOpts []sources.PreparerOption
271271
if !options.WithoutGitRepo {
272-
preparerOpts = append(preparerOpts,
273-
sources.WithGitRepo(env, env.LockReader(), distro.Version.ReleaseVer),
274-
sources.WithDirtyDetection(),
275-
)
272+
preparerOpts = append(preparerOpts, gitRepoPreparerOptions(env, distro)...)
276273
}
277274

278275
preparerOpts = append(preparerOpts,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
package component
5+
6+
import (
7+
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev"
8+
"github.com/microsoft/azure-linux-dev-tools/internal/app/azldev/core/sources"
9+
"github.com/microsoft/azure-linux-dev-tools/internal/providers/sourceproviders"
10+
)
11+
12+
// gitRepoPreparerOptions returns the [sources.PreparerOption] values that enable
13+
// synthetic dist-git history for the current mode.
14+
//
15+
// By default, history is derived from the component's lock file, and working-tree
16+
// changes are detected by comparing input fingerprints. In lock-file-free mode it
17+
// is derived from the component's generated upstream-commit TOML, which has no
18+
// fingerprint to compare against, so only committed changes are represented.
19+
func gitRepoPreparerOptions(
20+
env *azldev.Env, distro sourceproviders.ResolvedDistro,
21+
) []sources.PreparerOption {
22+
if env.WithoutLockfile() {
23+
return []sources.PreparerOption{
24+
sources.WithGitRepo(env, nil, ""),
25+
sources.WithoutLockfileHistory(),
26+
}
27+
}
28+
29+
return []sources.PreparerOption{
30+
sources.WithGitRepo(env, env.LockReader(), distro.Version.ReleaseVer),
31+
sources.WithDirtyDetection(),
32+
}
33+
}

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,7 @@ func buildPreparerOptions(
155155
var opts []sources.PreparerOption
156156

157157
if !options.WithoutGitRepo && !options.SkipOverlays {
158-
opts = append(opts,
159-
sources.WithGitRepo(env, env.LockReader(), distro.Version.ReleaseVer),
160-
sources.WithDirtyDetection(),
161-
)
158+
opts = append(opts, gitRepoPreparerOptions(env, distro)...)
162159
}
163160

164161
opts = append(opts,

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -523,13 +523,11 @@ func prepareComponentSources(
523523
// rpmautospec can expand %autorelease and %autochangelog correctly.
524524
// WithSkipLookaside avoids expensive tarball downloads — only spec +
525525
// sidecar files are needed for rendering.
526-
preparerOpts := []sources.PreparerOption{
527-
sources.WithGitRepo(env, env.LockReader(), distro.Version.ReleaseVer),
528-
sources.WithDirtyDetection(),
526+
preparerOpts := append(gitRepoPreparerOptions(env, distro),
529527
sources.WithSkipLookaside(),
530528
sources.WithUpstreamProvenance(sources.FedoraDistTag(distro.Ref.Name, distro.Version.ReleaseVer)),
531529
sources.WithMockProcessor(mockProcessor),
532-
}
530+
)
533531

534532
preparer, err := sources.NewPreparer(sourceManager, env.FS(), env, env, preparerOpts...)
535533
if err != nil {

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

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@ func WithDirtyDetection() PreparerOption {
9898
}
9999
}
100100

101+
// WithoutLockfileHistory returns a [PreparerOption] that derives synthetic history
102+
// from the component's generated upstream-commit TOML instead of its lock file.
103+
// It selects the behavior of the global '--without-lockfile' flag; the lock reader
104+
// and release version passed to [WithGitRepo] are unused in that mode.
105+
func WithoutLockfileHistory() PreparerOption {
106+
return func(p *sourcePreparerImpl) {
107+
p.withoutLockfile = true
108+
}
109+
}
110+
101111
// WithSkipLookaside returns a [PreparerOption] that skips all lookaside cache
102112
// downloads during source preparation. This includes both explicit source file
103113
// downloads ([SourceManager.FetchFiles]) and lookaside extraction during
@@ -184,6 +194,11 @@ type sourcePreparerImpl struct {
184194
// synthetic history generation. Set via [WithDirtyDetection].
185195
dirtyDetection bool
186196

197+
// withoutLockfile, when true, derives synthetic history from the generated
198+
// upstream-commit TOML rather than the lock file. Set via
199+
// [WithoutLockfileHistory].
200+
withoutLockfile bool
201+
187202
// releaseVer is the per-component resolved distro release version, not the
188203
// project default. Set via [WithGitRepo].
189204
releaseVer string
@@ -244,6 +259,11 @@ func NewPreparer(
244259
"dirty detection compares fingerprints against committed lock files in the git history")
245260
}
246261

262+
if impl.dirtyDetection && impl.withoutLockfile {
263+
return nil, errors.New("WithDirtyDetection is incompatible with WithoutLockfileHistory; " +
264+
"there is no lock file to compare fingerprints against")
265+
}
266+
247267
return impl, nil
248268
}
249269

@@ -500,26 +520,9 @@ func (p *sourcePreparerImpl) trySyntheticHistory(
500520
config := component.GetConfig()
501521
componentName := component.GetName()
502522

503-
// Compute the current fingerprint for uncommitted-change detection.
504-
// Only computed when dirty detection is enabled (e.g., build, render).
505-
// An empty fingerprint skips dirty detection in buildSyntheticCommits.
506-
var currentFingerprint string
507-
508-
if p.dirtyDetection {
509-
var fpErr error
510-
511-
currentFingerprint, fpErr = computeCurrentFingerprint(p.fs, fingerprintConfig, p.releaseVer)
512-
if fpErr != nil {
513-
return fmt.Errorf("dirty detection failed for component %#q:\n%w", componentName, fpErr)
514-
}
515-
}
516-
517-
changes, importCommit, err := buildSyntheticCommits(
518-
ctx, p.cmdFactory, config, componentName, p.lockReader.LockDir(),
519-
currentFingerprint,
520-
)
523+
changes, importCommit, err := p.findSyntheticChanges(ctx, config, fingerprintConfig, componentName)
521524
if err != nil {
522-
return fmt.Errorf("failed to build synthetic commits:\n%w", err)
525+
return err
523526
}
524527

525528
if len(changes) == 0 {
@@ -570,6 +573,52 @@ func (p *sourcePreparerImpl) trySyntheticHistory(
570573
return nil
571574
}
572575

576+
// findSyntheticChanges discovers the component changes that synthetic history
577+
// should represent, using the source selected by the preparer's mode: the
578+
// component's lock file by default, or its generated upstream-commit TOML when
579+
// [WithoutLockfileHistory] is set. The returned import commit bounds the upstream
580+
// walk; it is always empty in lock-file-free mode, where no fork point is
581+
// persisted and the repository's first-parent root bounds the walk instead.
582+
func (p *sourcePreparerImpl) findSyntheticChanges(
583+
ctx context.Context,
584+
config *projectconfig.ComponentConfig,
585+
fingerprintConfig *projectconfig.ComponentConfig,
586+
componentName string,
587+
) (changes []FingerprintChange, importCommit string, err error) {
588+
if p.withoutLockfile {
589+
changes, err = buildUpstreamCommitSyntheticCommits(ctx, p.cmdFactory, config, componentName)
590+
if err != nil {
591+
return nil, "", fmt.Errorf("failed to build synthetic commits:\n%w", err)
592+
}
593+
594+
return changes, "", nil
595+
}
596+
597+
// Compute the current fingerprint for uncommitted-change detection.
598+
// Only computed when dirty detection is enabled (e.g., build, render).
599+
// An empty fingerprint skips dirty detection in buildSyntheticCommits.
600+
var currentFingerprint string
601+
602+
if p.dirtyDetection {
603+
var fpErr error
604+
605+
currentFingerprint, fpErr = computeCurrentFingerprint(p.fs, fingerprintConfig, p.releaseVer)
606+
if fpErr != nil {
607+
return nil, "", fmt.Errorf("dirty detection failed for component %#q:\n%w", componentName, fpErr)
608+
}
609+
}
610+
611+
changes, importCommit, err = buildSyntheticCommits(
612+
ctx, p.cmdFactory, config, componentName, p.lockReader.LockDir(),
613+
currentFingerprint,
614+
)
615+
if err != nil {
616+
return nil, "", fmt.Errorf("failed to build synthetic commits:\n%w", err)
617+
}
618+
619+
return changes, importCommit, nil
620+
}
621+
573622
// computeCurrentFingerprint computes the current input fingerprint for a
574623
// component from its resolved config. Returns ("", nil) for local components
575624
// or when the source identity cannot be determined — dirty detection is

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ type FingerprintChange struct {
4242
UpstreamCommit string
4343
}
4444

45+
// UpstreamCommitChange records a project commit that changed a component's
46+
// configured upstream commit. Lock-file-free mode discovers changes from the
47+
// generated upstream-commit TOML instead of a lock file's fingerprint, but the
48+
// recorded data and the replay that consumes it are identical.
49+
type UpstreamCommitChange = FingerprintChange
50+
4551
// interleavedEntry represents a single commit in the rebuilt dist-git history.
4652
// Exactly one of upstreamCommit or syntheticChange is non-nil.
4753
type interleavedEntry struct {
@@ -585,15 +591,29 @@ func openProjectRepo(
585591
config *projectconfig.ComponentConfig,
586592
componentName string,
587593
) (*gogit.Repository, string, error) {
588-
if config.SourceConfigFile == nil || config.SourceConfigFile.SourcePath() == "" {
594+
var configFilePath string
595+
if config.SourceConfigFile != nil {
596+
configFilePath = config.SourceConfigFile.SourcePath()
597+
}
598+
599+
return openProjectRepoForConfigFile(configFilePath, componentName)
600+
}
601+
602+
// openProjectRepoForConfigFile opens the git repository containing configFilePath
603+
// and returns both the [gogit.Repository] and the worktree root directory. Returns
604+
// (nil, "", nil) when the path is empty, indicating that synthetic commits should
605+
// be skipped.
606+
func openProjectRepoForConfigFile(
607+
configFilePath string,
608+
componentName string,
609+
) (*gogit.Repository, string, error) {
610+
if configFilePath == "" {
589611
slog.Debug("Cannot resolve config file for synthetic commits; skipping",
590612
"component", componentName)
591613

592614
return nil, "", nil
593615
}
594616

595-
configFilePath := config.SourceConfigFile.SourcePath()
596-
597617
repo, err := git.OpenProjectRepo(filepath.Dir(configFilePath))
598618
if err != nil {
599619
return nil, "", fmt.Errorf("failed to find project repository for config file %#q:\n%w",

0 commit comments

Comments
 (0)