Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,10 +518,11 @@ ignore the relevant context while writing the upstream commit message. Braid
does not generate an upstream subject for you, and leaving the guidance in place
does not add it to the final commit message.

This guidance is best-effort. If Braid cannot compute it safely, or if
`core.commentChar` is set to `auto`, Braid prints a warning and opens the editor
without the provenance block; the push still proceeds through the normal commit
and push checks.
This guidance is best-effort. If a historical upstream revision is unavailable
locally, Braid prints an informational message unless `--quiet` is active and
opens the editor without the provenance block. Other failures that prevent Braid
from computing the guidance safely, including `core.commentChar=auto`, print a
warning. The push still proceeds through the normal commit and push checks.

To prefill the editor with a generated draft message, set
`BRAID_PUSH_COMMIT_MESSAGE_COMMAND` to a trusted local POSIX shell command. Empty
Expand Down
1 change: 1 addition & 0 deletions internal/command/preflight.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ type PushGit interface {
ConfigGet(context.Context, ...string) (string, bool, error)
CoreCommentChar(context.Context) (string, bool, error)
ShellPath(context.Context) (string, error)
ResolveRevision(context.Context, string) (string, bool, error)
FirstParentCommits(context.Context, string) ([]string, error)
LogCommitsTouchingPath(context.Context, string, string) ([]gitexec.Commit, error)
ShowFile(context.Context, string, string) ([]byte, bool, error)
Expand Down
13 changes: 12 additions & 1 deletion internal/command/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ func (h PushHandler) push(ctx context.Context, repo RepoContext, git PushGit, m
if commitMessage == "" {
provenance, provenanceOK, provenanceErr = buildPushProvenance(ctx, git, m)
if provenanceErr != nil {
warnPushProvenance(stderr, provenanceErr)
reportPushProvenanceFailure(stderr, global.Quiet, provenanceErr)
}
messageGeneration, err = resolvePushMessageGeneration(ctx, git, messageGeneration)
if err != nil {
Expand Down Expand Up @@ -502,6 +502,17 @@ func warnPushProvenance(stderr io.Writer, err error) {
_, _ = fmt.Fprintf(stderr, "Braid: warning: push provenance guidance skipped: %v\n", err)
}

func reportPushProvenanceFailure(stderr io.Writer, quiet bool, err error) {
var unavailable *pushProvenanceRevisionUnavailableError
if errors.As(err, &unavailable) {
if !quiet {
_, _ = fmt.Fprintf(stderr, "Braid: push provenance guidance unavailable: %v\n", err)
}
return
}
warnPushProvenance(stderr, err)
}

func writeAlternates(ctx context.Context, source PushGit, tempDir, sourceWorkDir string) error {
objectsPath, err := source.RepoFilePath(ctx, "objects")
if err != nil {
Expand Down
23 changes: 21 additions & 2 deletions internal/command/push_provenance.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ type pushProvenanceWindow struct {
NoCleanAnchor bool
}

type pushProvenanceRevisionUnavailableError struct {
Source string
Revision string
}

func (e *pushProvenanceRevisionUnavailableError) Error() string {
return fmt.Sprintf("historical upstream revision %s for source :%s is unavailable locally", e.Revision, e.Source)
}

func buildPushProvenance(ctx context.Context, git PushGit, m source.SourceMirror) (pushProvenance, bool, error) {
byHash := map[string]pushProvenanceCommit{}
noAnchor := false
Expand Down Expand Up @@ -165,10 +174,20 @@ func mirrorCleanAtCommit(ctx context.Context, git PushGit, commit string, m sour
}

func recordedMirrorItem(ctx context.Context, git PushGit, m source.SourceMirror) (gitexec.TreeItem, error) {
revision, ok, err := git.ResolveRevision(ctx, m.Revision+"^{commit}")
if err != nil {
return gitexec.TreeItem{}, err
}
if !ok {
return gitexec.TreeItem{}, &pushProvenanceRevisionUnavailableError{
Source: m.Name,
Revision: m.Revision,
}
}
if m.UpstreamPath == "" {
return git.TreeItem(ctx, m.Revision)
return git.TreeItem(ctx, revision)
}
return git.LsTreeItem(ctx, m.Revision, m.UpstreamPath)
return git.LsTreeItem(ctx, revision, m.UpstreamPath)
}

func collectPushProvenanceCommits(ctx context.Context, git PushGit, current source.SourceMirror, revisionRange string) ([]pushProvenanceCommit, int, error) {
Expand Down
43 changes: 43 additions & 0 deletions internal/command/push_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,49 @@ func TestPushCommandProvenanceSurvivesUpdateAndExcludesBraidCommit(t *testing.T)
assertFile(t, upstream, "remote.txt", "remote change\n")
}

func TestPushCommandHistoricalRevisionUnavailableIsInformational(t *testing.T) {
upstream := testutil.InitRepo(t)
testutil.WriteFile(t, upstream, "local.txt", "base local\n")
testutil.WriteFile(t, upstream, "remote.txt", "base remote\n")
historicalRevision := testutil.CommitAll(t, upstream, "base")

repo := initDownstream(t)
runCommandOK(t, repo, []string{"add", upstream, "vendor/basic"})
testutil.WriteFile(t, repo, "vendor/basic/local.txt", "local change\n")
commitAllWithMessage(t, repo, "local before update")
testutil.WriteFile(t, upstream, "remote.txt", "remote change\n")
currentRevision := testutil.CommitAll(t, upstream, "remote update")
runCommandOK(t, repo, []string{"update", "vendor/basic"})

parent := t.TempDir()
clone := filepath.Join(parent, "clone")
testutil.Git(t, parent, "clone", "--no-local", repo, clone)
testutil.Git(t, clone, "fetch", "--depth=1", upstream, currentRevision)
git := gitexec.New(clone, false, nil)
if resolved, ok, err := git.ResolveRevision(context.Background(), historicalRevision+"^{commit}"); err != nil {
t.Fatalf("inspect historical revision: %v", err)
} else if ok {
t.Fatalf("historical revision unexpectedly available at %s", resolved)
}

_, ok, provenanceErr := buildPushProvenance(context.Background(), git, loadMirror(t, clone, "vendor/basic"))
if provenanceErr == nil {
t.Fatal("buildPushProvenance returned nil error, want unavailable historical revision")
}
if ok {
t.Fatal("buildPushProvenance returned ok, want false")
}

var stderr bytes.Buffer
reportPushProvenanceFailure(&stderr, false, provenanceErr)
assertContains(t, stderr.String(), "Braid: push provenance guidance unavailable: historical upstream revision "+historicalRevision+" for source :001 is unavailable locally")
assertNotContains(t, stderr.String(), "Braid: warning: push provenance guidance skipped")

stderr.Reset()
reportPushProvenanceFailure(&stderr, true, provenanceErr)
assertNotContains(t, stderr.String(), "push provenance guidance")
}

func TestPushCommandProvenanceCapShowsNewestTwentyFiveChronologically(t *testing.T) {
upstream := testutil.InitRepo(t)
testutil.WriteFile(t, upstream, "README.md", "base\n")
Expand Down
8 changes: 8 additions & 0 deletions internal/gitexec/gitexec.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,14 @@ func (g Git) RevParse(ctx context.Context, rev string) (string, error) {
return g.Output(ctx, "rev-parse", rev)
}

func (g Git) ResolveRevision(ctx context.Context, rev string) (string, bool, error) {
resolved, err := g.optionalRevParse(ctx, rev)
if err != nil {
return "", false, err
}
return resolved, resolved != "", nil
}

func (g Git) Head(ctx context.Context) (string, error) {
return g.RevParse(ctx, "HEAD")
}
Expand Down
11 changes: 11 additions & 0 deletions internal/gitexec/gitexec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,17 @@ func TestHistoryHelpersReadCommitsFilesAndTrees(t *testing.T) {
head := realGitOutput(t, repo, "rev-parse", "HEAD")

git := New(repo, false, nil)
resolved, ok, err := git.ResolveRevision(context.Background(), "HEAD^{commit}")
if err != nil {
t.Fatalf("ResolveRevision HEAD returned error: %v", err)
}
if !ok || resolved != head {
t.Fatalf("ResolveRevision HEAD = %q, %v, want %q, true", resolved, ok, head)
}
if resolved, ok, err := git.ResolveRevision(context.Background(), "refs/heads/missing^{commit}"); err != nil || ok || resolved != "" {
t.Fatalf("ResolveRevision missing = %q, %v, %v, want empty, false, nil", resolved, ok, err)
}

commits, err := git.FirstParentCommits(context.Background(), "HEAD")
if err != nil {
t.Fatalf("FirstParentCommits returned error: %v", err)
Expand Down