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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ Show local changes as a Git diff:

```bash
braid diff
braid diff --sync-push-only
braid diff vendor/rails
braid diff :rails
braid diff vendor/rails -- --stat
Expand All @@ -309,6 +310,11 @@ braid diff vendor/rails -- --cached
A mirror-path selector diffs only that local mirror. A `:source` selector diffs
every mirror belonging to the source.

`--sync-push-only` limits the command to sources configured with
`"sync_push": true`. Ineligible sources are skipped quietly, including when
explicitly selected. The option filters sources only; each selected mirror
retains the normal `braid diff` comparison behavior.

Arguments after `--` are passed to `git diff`. This is useful for generating
patches, limiting output, or checking staged changes only.

Expand Down Expand Up @@ -663,7 +669,7 @@ includes it.
| --- | --- |
| `add [--no-commit]` | Add a source with mirrors, or add mirrors to an existing `:source`. |
| `status` | Show whether mirrors have remote, local, or removal changes. |
| `diff` | Show local mirror changes, with Git diff arguments after `--`. |
| `diff [--sync-push-only]` | Show local mirror changes, optionally limited to sources that participate in sync pushes, with Git diff arguments after `--`. |
| `pull [--no-commit]` | Pull one source atomically, or every eligible source. |
| `push` | Push a source's committed mirror changes upstream as one commit. |
| `sync [selector...] [--pull-only] [--autostash] [--keep]` | Push then pull selected sources. |
Expand Down
5 changes: 5 additions & 0 deletions integration/completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ func TestExecutableBashCompletion(t *testing.T) {

noRepoMirrors := completeExecutable(t, env, root, braid, "status", "")
assertCompletionCandidate(t, noRepoMirrors, "help")

diffOptions := completeExecutable(t, env, root, braid, "diff", "")
assertCompletionCandidate(t, diffOptions, "--sync-push-only")
diffOptionsAfterSelector := completeExecutable(t, env, root, braid, "diff", "mirror", "")
assertCompletionCandidate(t, diffOptionsAfterSelector, "--sync-push-only")
}

func TestExecutableBashCompletionCoversEveryCommandAndOptionPosition(t *testing.T) {
Expand Down
35 changes: 35 additions & 0 deletions integration/lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,41 @@ func TestExecutablePrimaryLifecycle(t *testing.T) {
assertClean(t, env, downstream)
}

func TestExecutableDiffSyncPushOnly(t *testing.T) {
root := t.TempDir()
env := newProcessEnv(t, root)
braid := braidBinary(t)

upstreamEnabled := filepath.Join(root, "upstream-enabled")
initRepo(t, env, upstreamEnabled)
writeFile(t, upstreamEnabled, "README.md", "enabled base\n")
commitAll(t, env, upstreamEnabled, "enabled")
upstreamDisabled := filepath.Join(root, "upstream-disabled")
initRepo(t, env, upstreamDisabled)
writeFile(t, upstreamDisabled, "README.md", "disabled base\n")
commitAll(t, env, upstreamDisabled, "disabled")

downstream := filepath.Join(root, "downstream")
initRepo(t, env, downstream)
writeFile(t, downstream, "README.md", "downstream\n")
commitAll(t, env, downstream, "downstream")
assertResult(t, runBraid(t, env, downstream, braid, "--quiet", "add", upstreamEnabled, "vendor/enabled", "--sync-push"), 0, "", "")
assertResult(t, runBraid(t, env, downstream, braid, "--quiet", "add", upstreamDisabled, "vendor/disabled"), 0, "", "")
writeFile(t, downstream, "vendor/enabled/README.md", "enabled changed\n")
writeFile(t, downstream, "vendor/disabled/README.md", "disabled changed\n")

filtered := runBraid(t, env, downstream, braid, "diff", "--sync-push-only")
assertExit(t, filtered, 0)
assertEmpty(t, "filtered diff stderr", filtered.stderr)
assertContains(t, filtered.stdout, "Braid: Diffing mirror vendor/enabled")
assertContains(t, filtered.stdout, "enabled changed")
assertNotContains(t, filtered.stdout, "vendor/disabled")
assertNotContains(t, filtered.stdout, "disabled changed")

skipped := runBraid(t, env, downstream, braid, "diff", "vendor/disabled", "--sync-push-only")
assertResult(t, skipped, 0, "", "")
}

func TestUpgradeConfigCommitAndNoCommit(t *testing.T) {
for _, noCommit := range []bool{false, true} {
t.Run(map[bool]string{false: "commit", true: "no-commit"}[noCommit], func(t *testing.T) {
Expand Down
8 changes: 5 additions & 3 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,10 @@ type RemoveOptions struct {
}

type DiffOptions struct {
LocalPath string
Keep bool
GitDiffArgs []string
LocalPath string
Keep bool
SyncPushOnly bool
GitDiffArgs []string
}

type PushOptions struct {
Expand Down Expand Up @@ -423,6 +424,7 @@ func parseDiff(args []string, options *DiffOptions) error {
options.LocalPath = normalizeLocalPathArg(parsed.positionals[0])
}
options.Keep = parsed.has("--keep")
options.SyncPushOnly = parsed.has("--sync-push-only")
options.GitDiffArgs = parsed.passthrough
return nil
}
Expand Down
20 changes: 18 additions & 2 deletions internal/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,18 @@ func TestParseCommands(t *testing.T) {
},
{
name: "diff passthrough",
args: []string{"--no-cache", "--quiet", "diff", "vendor/repo", "--", "--stat", "weird;path"},
args: []string{"--no-cache", "--quiet", "diff", "vendor/repo", "--sync-push-only", "--", "--stat", "weird;path"},
want: Invocation{
Global: GlobalOptions{NoCache: true, Quiet: true},
Command: CommandDiff,
Diff: DiffOptions{LocalPath: "vendor/repo", GitDiffArgs: []string{"--stat", "weird;path"}},
Diff: DiffOptions{LocalPath: "vendor/repo", SyncPushOnly: true, GitDiffArgs: []string{"--stat", "weird;path"}},
},
},
{
name: "diff sync push only before selector",
args: []string{"diff", "--sync-push-only", "vendor/repo"},
want: Invocation{Command: CommandDiff, Diff: DiffOptions{LocalPath: "vendor/repo", SyncPushOnly: true}},
},
{
name: "diff verbose passthrough",
args: []string{"diff", "vendor/repo", "--", "--verbose"},
Expand All @@ -87,6 +92,14 @@ func TestParseCommands(t *testing.T) {
Diff: DiffOptions{LocalPath: "vendor/repo", GitDiffArgs: []string{"--verbose"}},
},
},
{
name: "diff sync push only text after separator is passthrough",
args: []string{"diff", "vendor/repo", "--", "--sync-push-only"},
want: Invocation{
Command: CommandDiff,
Diff: DiffOptions{LocalPath: "vendor/repo", GitDiffArgs: []string{"--sync-push-only"}},
},
},
{
name: "push",
args: []string{"-v", "push", "vendor/repo", "-b", "main", "--keep"},
Expand Down Expand Up @@ -336,6 +349,9 @@ func TestUsageDocumentsVerboseAsGlobalOnly(t *testing.T) {
if got, want := CommandUsage(CommandRemove), "usage: braid remove <local_path|:source> [--keep] [--no-commit]\n"; got != want {
t.Fatalf("CommandUsage(remove) = %q, want %q", got, want)
}
if got, want := CommandUsage(CommandDiff), "usage: braid diff [local_path|:source] [--keep] [--sync-push-only] [-- <git_diff_arg>...]\n"; got != want {
t.Fatalf("CommandUsage(diff) = %q, want %q", got, want)
}
if got, want := CommandUsage(CommandPush), "usage: braid push <local_path|:source> [--branch|-b <branch>] [--message|-m <message>] [--keep]\n"; got != want {
t.Fatalf("CommandUsage(push) = %q, want %q", got, want)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ var commandSpecs = []CommandSpec{
},
{
Command: CommandDiff, Name: "diff", Summary: "Show local mirror changes",
Options: []OptionSpec{{Long: "--keep"}},
Options: []OptionSpec{{Long: "--keep"}, {Long: "--sync-push-only"}},
Positionals: []PositionalSpec{{Name: "local_path|:source", Usage: "[local_path|:source]", Completion: CompletionMirror}},
Passthrough: &PassthroughSpec{Usage: "[-- <git_diff_arg>...]"},
},
Expand Down
5 changes: 5 additions & 0 deletions internal/command/completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ func TestCompleteCommandOptions(t *testing.T) {
candidates = completeCandidates(t, dir, "remove", "")
assertCandidate(t, candidates, "--no-commit")

candidates = completeCandidates(t, dir, "diff", "")
assertCandidate(t, candidates, "--sync-push-only")
candidates = completeCandidates(t, dir, "diff", "mirror", "")
assertCandidate(t, candidates, "--sync-push-only")

candidates = completeCandidates(t, dir, "pull", "--no-commit", "--")
assertNoCandidate(t, candidates, "--no-commit")

Expand Down
41 changes: 24 additions & 17 deletions internal/command/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,35 +33,42 @@ func (h DiffHandler) Run(inv cli.Invocation, stdout, stderr io.Writer) error {
if err := validateConfigPaths(cfg); err != nil {
return err
}
cache, err := runtimeCacheForRepo(ctx, repo, inv.Global, inv.Global.Verbose, stderr)
if err != nil {
return err
}

var mirrors []source.SourceMirror
showHeaders := inv.Diff.LocalPath == ""
if inv.Diff.LocalPath != "" {
selection, err := resolveSourceSelection(repo, cfg, inv.Diff.LocalPath, true)
if err != nil {
return err
}
if inv.Diff.SyncPushOnly && !selection.Source.SyncPush {
return nil
}
for _, mirror := range selection.Mirrors {
if len(selection.Mirrors) > 1 {
if _, err := fmt.Fprintf(stdout, "=======================================================\nBraid: Diffing mirror %s\n=======================================================\n", mirror.LocalPath); err != nil {
return err
}
}
if err := h.diffOne(ctx, git, processGit, cache, selection.Source.WithMirror(mirror), inv.Diff, inv.Global.Verbose, progress, stdout, stderr); err != nil {
return err
mirrors = append(mirrors, selection.Source.WithMirror(mirror))
}
showHeaders = len(mirrors) > 1
} else {
for _, mirror := range cfg.MirrorsSorted() {
if !inv.Diff.SyncPushOnly || mirror.SyncPush {
mirrors = append(mirrors, mirror)
}
}
}
if len(mirrors) == 0 {
return nil
}

for _, m := range cfg.MirrorsSorted() {
localPath := m.LocalPath
if _, err := fmt.Fprintf(stdout, "=======================================================\nBraid: Diffing mirror %s\n=======================================================\n", localPath); err != nil {
return err
cache, err := runtimeCacheForRepo(ctx, repo, inv.Global, inv.Global.Verbose, stderr)
if err != nil {
return err
}
for _, mirror := range mirrors {
if showHeaders {
if _, err := fmt.Fprintf(stdout, "=======================================================\nBraid: Diffing mirror %s\n=======================================================\n", mirror.LocalPath); err != nil {
return err
}
}
if err := h.diffOne(ctx, git, processGit, cache, m, inv.Diff, inv.Global.Verbose, progress, stdout, stderr); err != nil {
if err := h.diffOne(ctx, git, processGit, cache, mirror, inv.Diff, inv.Global.Verbose, progress, stdout, stderr); err != nil {
return err
}
}
Expand Down
42 changes: 42 additions & 0 deletions internal/command/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,48 @@ func TestDiffCommandAllMirrors(t *testing.T) {
assertContains(t, out, "two changed")
}

func TestDiffCommandSyncPushOnlyFiltersSources(t *testing.T) {
upstreamEnabled := testutil.InitRepo(t)
testutil.WriteFile(t, upstreamEnabled, "README.md", "enabled base\n")
testutil.CommitAll(t, upstreamEnabled, "enabled")

upstreamDisabled := testutil.InitRepo(t)
testutil.WriteFile(t, upstreamDisabled, "README.md", "disabled base\n")
testutil.CommitAll(t, upstreamDisabled, "disabled")

repo := initDownstream(t)
runCommandOK(t, repo, []string{"add", upstreamEnabled, "--name", "enabled", "vendor/enabled-a", "vendor/enabled-b", "--sync-push"})
runCommandOK(t, repo, []string{"add", upstreamDisabled, "--name", "disabled", "vendor/disabled"})
testutil.WriteFile(t, repo, "vendor/enabled-a/README.md", "enabled a changed\n")
testutil.WriteFile(t, repo, "vendor/enabled-b/README.md", "enabled b changed\n")
testutil.WriteFile(t, repo, "vendor/disabled/README.md", "disabled changed\n")

out := runCommandOK(t, repo, []string{"diff", "--sync-push-only"})
assertContains(t, out, "Braid: Diffing mirror vendor/enabled-a")
assertContains(t, out, "Braid: Diffing mirror vendor/enabled-b")
assertContains(t, out, "enabled a changed")
assertContains(t, out, "enabled b changed")
assertNotContains(t, out, "vendor/disabled")
assertNotContains(t, out, "disabled changed")

sourceOut := runCommandOK(t, repo, []string{"diff", ":enabled", "--sync-push-only"})
assertContains(t, sourceOut, "Braid: Diffing mirror vendor/enabled-a")
assertContains(t, sourceOut, "Braid: Diffing mirror vendor/enabled-b")

disabledOut, disabledErr := runCommandOKWithOutput(t, repo, []string{"diff", "vendor/disabled", "--sync-push-only"})
if disabledOut != "" || disabledErr != "" {
t.Fatalf("disabled source output = (%q, %q), want empty", disabledOut, disabledErr)
}

disabledOnlyRepo := initDownstream(t)
runCommandOK(t, disabledOnlyRepo, []string{"add", upstreamDisabled, "vendor/disabled"})
testutil.WriteFile(t, disabledOnlyRepo, "vendor/disabled/README.md", "disabled changed\n")
disabledOut, disabledErr = runCommandOKWithOutput(t, disabledOnlyRepo, []string{"diff", "--sync-push-only"})
if disabledOut != "" || disabledErr != "" {
t.Fatalf("no eligible sources output = (%q, %q), want empty", disabledOut, disabledErr)
}
}

func TestDiffCommandMirrorVariants(t *testing.T) {
tests := []struct {
name string
Expand Down