Resolve committed module identity from Git objects
This commit is contained in:
@@ -75,22 +75,84 @@ func ResolveSelection(repositoryDir, moduleDir string) (Selection, error) {
|
||||
// FromCommitted derives identity from the selected module declaration at a
|
||||
// captured commit. The selected module must contain its own go.mod.
|
||||
func FromCommitted(ctx context.Context, repositoryDir, moduleDir, version, commit string) (Identity, error) {
|
||||
selection, err := ResolveSelection(repositoryDir, moduleDir)
|
||||
resolver, err := committedPaths(repositoryDir, commit)
|
||||
if err != nil {
|
||||
return Identity{}, err
|
||||
return Identity{Version: version}, err
|
||||
}
|
||||
return FromCommittedSelection(ctx, selection, version, commit)
|
||||
selection, err := selectCommittedDirectory(ctx, resolver, moduleDir)
|
||||
if err != nil {
|
||||
return Identity{Selection: selection, Version: version}, err
|
||||
}
|
||||
return fromCommittedSelection(ctx, selection, version, commit, resolver)
|
||||
}
|
||||
|
||||
// ResolveCommittedSelection resolves directory selection without reading the
|
||||
// module declaration or requiring the directory to be materialized.
|
||||
func ResolveCommittedSelection(ctx context.Context, repositoryDir, moduleDir, commit string) (Selection, error) {
|
||||
resolver, err := committedPaths(repositoryDir, commit)
|
||||
if err != nil {
|
||||
return Selection{}, err
|
||||
}
|
||||
return selectCommittedDirectory(ctx, resolver, moduleDir)
|
||||
}
|
||||
|
||||
func committedPaths(repositoryDir, commit string) (git.PathResolver, error) {
|
||||
repositoryAbs, err := filepath.Abs(repositoryDir)
|
||||
if err != nil {
|
||||
return git.PathResolver{}, err
|
||||
}
|
||||
repositoryAbs, err = filepath.EvalSymlinks(repositoryAbs)
|
||||
if err != nil {
|
||||
return git.PathResolver{}, err
|
||||
}
|
||||
return git.CommittedPaths(repositoryAbs, commit), nil
|
||||
}
|
||||
|
||||
func selectCommittedDirectory(ctx context.Context, resolver git.PathResolver, moduleDir string) (Selection, error) {
|
||||
selection := Selection{RepositoryDir: resolver.RepositoryDir}
|
||||
if moduleDir == "" {
|
||||
moduleDir = "."
|
||||
}
|
||||
if filepath.IsAbs(moduleDir) {
|
||||
return selection, fmt.Errorf("module directory must be repository-relative: %q", moduleDir)
|
||||
}
|
||||
directory, err := resolver.ResolveDestination(ctx, "go.mod", filepath.ToSlash(moduleDir))
|
||||
if err != nil {
|
||||
return selection, fmt.Errorf("resolve committed module directory: %w", err)
|
||||
}
|
||||
if directory.Kind != git.DestinationDirectory {
|
||||
return selection, fmt.Errorf("selected committed module directory %q resolves to %s at %q", moduleDir, directory.Kind, directory.Path)
|
||||
}
|
||||
selection.ModuleDir = filepath.Join(resolver.RepositoryDir, filepath.FromSlash(directory.Path))
|
||||
selection.ModuleSubdir = directory.Path
|
||||
return selection, nil
|
||||
}
|
||||
|
||||
// FromCommittedSelection derives identity from a previously resolved module
|
||||
// selection and the selected module declaration at a captured commit.
|
||||
func FromCommittedSelection(ctx context.Context, selection Selection, version, commit string) (Identity, error) {
|
||||
return fromCommittedSelection(ctx, selection, version, commit, git.CommittedPaths(selection.RepositoryDir, commit))
|
||||
}
|
||||
|
||||
func fromCommittedSelection(ctx context.Context, selection Selection, version, commit string, resolver git.PathResolver) (Identity, error) {
|
||||
identity := Identity{Selection: selection, Version: version}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return Identity{}, fmt.Errorf("resolve committed release identity: %w", err)
|
||||
return identity, fmt.Errorf("resolve committed release identity: %w", err)
|
||||
}
|
||||
declaration, err := git.ReadCommitFile(ctx, selection.RepositoryDir, commit, moduleFilePath(selection.ModuleSubdir))
|
||||
declarationPath, err := resolver.ResolveDestination(ctx, "go.mod", moduleFilePath(selection.ModuleSubdir))
|
||||
if err != nil {
|
||||
return Identity{}, fmt.Errorf("read selected module declaration at %s: %w", commit, err)
|
||||
return identity, fmt.Errorf("resolve selected module declaration at %s: %w", commit, err)
|
||||
}
|
||||
if declarationPath.Kind != git.DestinationEntry {
|
||||
return identity, fmt.Errorf("selected module declaration resolves to %s at %q", declarationPath.Kind, declarationPath.Path)
|
||||
}
|
||||
entry, _, err := resolver.Lookup(ctx, declarationPath.Path)
|
||||
if err != nil {
|
||||
return identity, err
|
||||
}
|
||||
declaration, err := git.ReadBlob(ctx, selection.RepositoryDir, entry.ObjectID)
|
||||
if err != nil {
|
||||
return identity, fmt.Errorf("read selected module declaration at %s: %w", commit, err)
|
||||
}
|
||||
return Derive(selection, declaration, version)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package releaseidentity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -134,6 +135,85 @@ func TestCommittedAndWorkingTreeDeclarationsUseTheirOwnSources(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommittedIdentityFollowsCapturedDeclarationAndDirectoryLinks(t *testing.T) {
|
||||
repository := t.TempDir()
|
||||
gitIdentityRun(t, repository, "init", "-q")
|
||||
gitIdentityRun(t, repository, "config", "user.name", "ReleaseKit test")
|
||||
gitIdentityRun(t, repository, "config", "user.email", "releasekit@example.invalid")
|
||||
moduleDir := filepath.Join(repository, "nested")
|
||||
if err := os.Mkdir(moduleDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
declaration := filepath.Join(repository, "module.txt")
|
||||
if err := os.WriteFile(declaration, []byte("module example.com/committed\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink("../module.txt", filepath.Join(moduleDir, "go.mod")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink("nested", filepath.Join(repository, "selected")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gitIdentityRun(t, repository, "add", ".")
|
||||
gitIdentityRun(t, repository, "commit", "-qm", "Add linked declaration")
|
||||
commit := strings.TrimSpace(gitIdentityOutput(t, repository, "rev-parse", "HEAD"))
|
||||
if err := os.WriteFile(declaration, []byte("module example.com/working\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
committed, err := FromCommitted(t.Context(), repository, "selected", "v1.2.3", commit)
|
||||
if err != nil || committed.ModulePath != "example.com/committed" || committed.ModuleDir != moduleDir || committed.Tag != "nested/v1.2.3" {
|
||||
t.Fatalf("committed linked identity = %#v, error = %v", committed, err)
|
||||
}
|
||||
working, err := FromWorkingTree(t.Context(), repository, "selected", "v1.2.3")
|
||||
if err != nil || working.ModulePath != "example.com/working" || working.ModuleDir != moduleDir {
|
||||
t.Fatalf("working linked identity = %#v, error = %v", working, err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
if _, err := FromCommitted(ctx, repository, "selected", "v1.2.3", commit); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("canceled committed identity error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommittedIdentityPreservesReferenceFailures(t *testing.T) {
|
||||
for _, mode := range []string{"cycle", "outside", "missing", "unavailable blob"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
repository := t.TempDir()
|
||||
gitIdentityRun(t, repository, "init", "-q")
|
||||
gitIdentityRun(t, repository, "config", "user.name", "ReleaseKit test")
|
||||
gitIdentityRun(t, repository, "config", "user.email", "releasekit@example.invalid")
|
||||
target, wantCause := "go.mod", "symlink-cycle"
|
||||
switch mode {
|
||||
case "outside":
|
||||
target, wantCause = "../module.txt", "outside-checkout"
|
||||
case "missing":
|
||||
target, wantCause = "missing.mod", "missing"
|
||||
case "unavailable blob":
|
||||
target, wantCause = "module.txt", "read selected module declaration"
|
||||
if err := os.WriteFile(filepath.Join(repository, target), []byte("module example.com/linked\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := os.Symlink(target, filepath.Join(repository, "go.mod")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gitIdentityRun(t, repository, "add", ".")
|
||||
gitIdentityRun(t, repository, "commit", "-qm", "Add reference fixture")
|
||||
commit := strings.TrimSpace(gitIdentityOutput(t, repository, "rev-parse", "HEAD"))
|
||||
if mode == "unavailable blob" {
|
||||
object := strings.TrimSpace(gitIdentityOutput(t, repository, "rev-parse", "HEAD:module.txt"))
|
||||
if err := os.Remove(filepath.Join(repository, ".git", "objects", object[:2], object[2:])); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
identity, err := FromCommitted(t.Context(), repository, ".", "v1.2.3", commit)
|
||||
if err == nil || !strings.Contains(err.Error(), wantCause) || identity.RepositoryDir != repository || identity.ModuleDir != repository {
|
||||
t.Fatalf("identity = %#v, error = %v; want %s", identity, err, wantCause)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectedNestedModuleDoesNotRequireRootDeclaration(t *testing.T) {
|
||||
repository := t.TempDir()
|
||||
moduleDir := filepath.Join(repository, "nested")
|
||||
|
||||
Reference in New Issue
Block a user