// SPDX-License-Identifier: AGPL-3.0-only package provenance import ( "context" "errors" "fmt" "path/filepath" "strconv" "strings" "gamertan.com/tend/internal/process" ) type Source struct { Commit string Epoch int64 } func Inspect(ctx context.Context, runner process.Runner, dir, branch string) (Source, error) { gitDir, err := gitPath(ctx, runner, dir, "--git-dir") if err != nil { return Source{}, fmt.Errorf("inspect Git directory: %w", err) } commonDir, err := gitPath(ctx, runner, dir, "--git-common-dir") if err != nil { return Source{}, fmt.Errorf("inspect Git common directory: %w", err) } if gitDir != commonDir { return Source{}, errors.New("release packaging does not yet support linked Git worktrees; use a clean standalone clone of the exact pushed commit") } status, err := runner.Run(ctx, dir, nil, "git", "status", "--porcelain=v1", "--untracked-files=all") if err != nil { return Source{}, err } if len(status) != 0 { return Source{}, errors.New("source checkout is not clean") } commitOut, err := runner.Run(ctx, dir, nil, "git", "rev-parse", "HEAD") if err != nil { return Source{}, err } commit := strings.TrimSpace(string(commitOut)) if len(commit) != 40 { return Source{}, errors.New("source commit is not a full SHA-1 object id") } remoteOut, err := runner.Run(ctx, dir, nil, "git", "ls-remote", "--exit-code", "origin", "refs/heads/"+branch) if err != nil { return Source{}, fmt.Errorf("verify pushed commit: %w", err) } fields := strings.Fields(string(remoteOut)) if len(fields) != 2 || fields[0] != commit || fields[1] != "refs/heads/"+branch { return Source{}, errors.New("HEAD is not the exact pushed branch commit") } epochOut, err := runner.Run(ctx, dir, nil, "git", "show", "-s", "--format=%ct", commit) if err != nil { return Source{}, err } epoch, err := strconv.ParseInt(strings.TrimSpace(string(epochOut)), 10, 64) if err != nil || epoch <= 0 { return Source{}, errors.New("commit timestamp is invalid") } return Source{Commit: commit, Epoch: epoch}, nil } func gitPath(ctx context.Context, runner process.Runner, dir, argument string) (string, error) { out, err := runner.Run(ctx, dir, nil, "git", "rev-parse", argument) if err != nil { return "", err } path := strings.TrimSpace(string(out)) if path == "" { return "", errors.New("Git returned an empty path") } if !filepath.IsAbs(path) { path = filepath.Join(dir, path) } return filepath.Clean(path), nil }