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
29 changes: 22 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ in your next `git commit` unless you unstage them first.
Global flags must appear before the command name:

```bash
braid [--verbose|-v | --quiet] [--no-cache | --cache-dir <path>] <command> [options]
braid [--verbose|-v | --quiet] [--no-cache | --global-cache-dir <path>] <command> [options]
```

`--verbose` prints Git command tracing. `--quiet` suppresses progress and other
Expand Down Expand Up @@ -551,10 +551,25 @@ braid setup
braid setup vendor/rails --force
```

The local cache is enabled by default. Without overrides, Braid stores it under
the OS user cache directory with a `braid` child directory. Use
`BRAID_LOCAL_CACHE_DIR` or `--cache-dir` to choose a location, and use
`BRAID_USE_LOCAL_CACHE=false` or `--no-cache` to disable it.
The local cache is enabled by default. Without overrides, Braid stores
repository-local per-mirror bare caches under `.git/braid/cache`. These caches
are implementation state and can be rebuilt by `braid add`, `braid setup`,
`braid status`, `braid pull`, `braid diff`, `braid push`, or `braid sync` while
the upstream still serves the recorded revisions from `.braids.json`.

Repository-local caches are shallow for common branch, tag, and full-SHA
revision workflows. Fetching from a shallow cache can make the downstream Git
repository report as shallow because Git records the shallow mirror commits in
`.git/shallow`; those commits are Braid mirror objects, not the downstream
branch history. If an upstream has removed a recorded revision and the
repository-local cache was deleted, Braid fails instead of guessing a base.

Use `BRAID_GLOBAL_CACHE_DIR` or `--global-cache-dir` to choose a shared full-cache
location, and use `BRAID_USE_LOCAL_CACHE=false` or `--no-cache` to disable
caching.

The old `BRAID_LOCAL_CACHE_DIR` environment variable and `--cache-dir` flag have
been replaced by `BRAID_GLOBAL_CACHE_DIR` and `--global-cache-dir`.

Mirror paths stored in `.braids.json` always use repo-root-relative `/`
separators, and ordinary Braid output uses those same repo-root-relative paths.
Expand All @@ -565,8 +580,8 @@ inside the Git working tree, and stored config paths remain relative.

Commands without a `local_path`, such as `braid status`, `braid diff`,
`braid setup`, `braid pull`, and `braid sync`, operate on the repository-wide
mirror set from any subdirectory. Relative `--cache-dir` values and
`BRAID_LOCAL_CACHE_DIR` values remain relative to the process directory. Git
mirror set from any subdirectory. Relative `--global-cache-dir` values and
`BRAID_GLOBAL_CACHE_DIR` values remain relative to the process directory. Git
diff arguments after `braid diff ... --` are passed through as raw `git diff`
arguments from the process directory; Braid only anchors its own internal mirror
pathspecs.
Expand Down
5 changes: 5 additions & 0 deletions bazelw
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env sh

set -eu

exec bazelisk "$@"
30 changes: 20 additions & 10 deletions docs/migration-from-ruby-braid.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ Migration impact:
| Help form | `braid help`, `braid add help`, `braid add --help`; the old README also advertised `braid help add`, but the `v1.1.10` gem does not provide command-specific help through that form | `braid help`, `braid add help`, `braid add --help` | Prefer `braid <command> help` or `braid <command> --help`. |
| Verbose flag | Per-command `--verbose`/`-v` | Global `--verbose`/`-v` before the command | Use `braid -v pull ...`, not `braid pull -v ...`. |
| Quiet flag | No global quiet flag | Global `--quiet` before the command; incompatible with `--verbose` | Use `braid --quiet <command> ...` in automation that wants data, warnings, errors, and recovery output without progress or informational chatter. |
| Cache flags | Environment variables only | Global `--no-cache` or `--cache-dir <path>` before the command, plus environment variables | Put cache flags before the command name. |
| Cache flags | Environment variables only | Global `--no-cache` or `--global-cache-dir <path>` before the command, plus environment variables | Put cache flags before the command name. |
| Bash completion | No documented generated completion command | `braid completion bash` prints a Bash completion script | Load it from shell startup or install it in the Bash completion directory to complete global options, commands, command options, and configured mirror paths. |
| `update --head` | Accepted as an option, then errors with a deprecation message | Unknown flag for `pull` and its aliases | Remove it; use `--branch`, `--tag`, or `--revision` with an explicit mirror path. |
| `update` without path | Updates all configured mirrors through Ruby's all-update flow | `pull` without a path updates branch/tag mirrors in lexicographic path order, skips revision-locked mirrors, and reports skipped paths; `update` and `up` behave the same way | Locked mirrors are no longer touched by all-update. |
Expand Down Expand Up @@ -222,21 +222,30 @@ Migration impact:
Both tools enable a local mirror cache by default, but the cache layout changed.

Ruby Braid defaults to `~/.braid/cache` and derives cache child paths by
sanitizing the upstream URL. Current Go Braid defaults to the operating system
user cache directory with a `braid` child directory and derives cache child paths
from a SHA-256 hash of the upstream URL.
sanitizing the upstream URL. Current Go Braid defaults to repository-local
per-mirror bare caches under `.git/braid/cache` and derives cache child paths
from a SHA-256-derived key covering the upstream URL, local path, upstream path,
and tracking mode.

Current controls:

- `BRAID_USE_LOCAL_CACHE=false` or `--no-cache` disables the cache.
- `BRAID_LOCAL_CACHE_DIR=<path>` or `--cache-dir <path>` selects the cache
directory.
- `--no-cache` and `--cache-dir` cannot be used together.
- `BRAID_GLOBAL_CACHE_DIR=<path>` or `--global-cache-dir <path>` selects a shared
full-cache directory.
- `--no-cache` and `--global-cache-dir` cannot be used together.
- `BRAID_LOCAL_CACHE_DIR` and `--cache-dir` have been replaced by
`BRAID_GLOBAL_CACHE_DIR` and `--global-cache-dir`.

Migration impact:

- Do not depend on the old `~/.braid/cache` layout or old cache path names.
- Braid-managed remotes created by `setup` may point at different cache paths.
- Braid-managed remotes created by `setup` may point at repository-local cache
paths under `.git/braid/cache`.
- Repository-local caches are disposable. If one is deleted, Braid can rebuild it
only while upstream still serves the recorded revisions from `.braids.json`.
Shallow repository-local caches can also make Git report the downstream
repository as shallow because Braid mirror commits are recorded in
`.git/shallow`.
- Tag mirrors work when the Go cache is disabled. Ruby Braid could not retrieve
tag revisions with the cache disabled.

Expand Down Expand Up @@ -313,8 +322,9 @@ Known output differences include:
result with expected mirror state.
6. Review `.braids.json` for unknown fields, unsafe paths, and remote-name
collisions.
7. Decide whether the new default cache location is acceptable; set
`BRAID_LOCAL_CACHE_DIR` or use `--cache-dir` if you need a controlled path.
7. Decide whether repository-local caches are acceptable; set
`BRAID_GLOBAL_CACHE_DIR` or use `--global-cache-dir` if you need a controlled
shared full-cache path.
8. For push workflows, make sure local mirror edits are committed downstream
before `braid push` or `braid sync`.
9. Prefer `braid sync` for the push-then-pull workflow once the team has
Expand Down
2 changes: 1 addition & 1 deletion integration/completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func TestExecutableBashCompletion(t *testing.T) {
globalFlags := completeExecutable(t, env, root, braid, "--")
assertCompletionCandidate(t, globalFlags, "--verbose")
assertCompletionCandidate(t, globalFlags, "--quiet")
assertCompletionCandidate(t, globalFlags, "--cache-dir")
assertCompletionCandidate(t, globalFlags, "--global-cache-dir")

noRepoMirrors := runBraid(t, env, root, braid, "__complete", "bash", "--", "status", "")
assertResult(t, noRepoMirrors, 0, "", "")
Expand Down
4 changes: 3 additions & 1 deletion integration/lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestExecutablePrimaryLifecycle(t *testing.T) {

localPath := "vendor/lib with spaces"
remote := remoteName("main", localPath)
cacheRemoteURL := cachePath(env.braidCacheDir(), upstream)
cacheRemoteURL := repositoryCachePath(t, downstream, localPath, configMirror{URL: upstream, Branch: "main", Path: "lib dir"})

add := runBraid(t, env, downstream, braid, "add", upstream, localPath, "--path", "lib dir")
assertExit(t, add, 0)
Expand All @@ -52,6 +52,7 @@ func TestExecutablePrimaryLifecycle(t *testing.T) {
assertExit(t, setup, 0)
assertEmpty(t, "setup stdout", setup.stdout)
assertProgress(t, setup.stderr,
"Braid: updated cache for mirror "+localPath,
"Braid: setting up mirror remote "+localPath,
"Braid: set up mirror remote "+localPath,
)
Expand All @@ -66,6 +67,7 @@ func TestExecutablePrimaryLifecycle(t *testing.T) {
assertExit(t, setupForce, 0)
assertEmpty(t, "setup force stdout", setupForce.stdout)
assertProgress(t, setupForce.stderr,
"Braid: updated cache for mirror "+localPath,
"Braid: setting up mirror remote "+localPath,
"Braid: set up mirror remote "+localPath,
)
Expand Down
2 changes: 1 addition & 1 deletion integration/no_commit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func TestExecutableNoCommitAddUpdateRemoveWorkflow(t *testing.T) {
assertContains(t, remove.stdout, "Braid: staged removal of mirror 'vendor/basic'")
assertPathMissing(t, downstream, "vendor/basic")
assertConfigRaw(t, downstream, map[string]configMirror{})
assertRemoteURL(t, env, downstream, remote, cachePath(env.braidCacheDir(), upstream))
assertRemoteURL(t, env, downstream, remote, repositoryCachePath(t, downstream, "vendor/basic", configMirror{URL: upstream, Branch: "main"}))
assertCachedNames(t, env, downstream, ".braids.json", "remove-staged.txt", "vendor/basic/README.md")
if got := gitOutput(t, env, downstream, "rev-parse", "HEAD"); got != removeHead {
t.Fatalf("HEAD = %s, want unchanged %s", got, removeHead)
Expand Down
19 changes: 9 additions & 10 deletions integration/setup_failure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,24 @@ func TestExecutableSetupCacheModes(t *testing.T) {

remote := remoteName("main", "vendor/repo")

defaultEnv := env.without("BRAID_LOCAL_CACHE_DIR")
setupDefault := runBraid(t, defaultEnv, downstream, braid, "--quiet", "setup", "vendor/repo")
setupDefault := runBraid(t, env, downstream, braid, "--quiet", "setup", "vendor/repo")
assertResult(t, setupDefault, 0, "", "")
defaultCacheURL := cachePath(defaultEnv.defaultBraidCacheDir(), upstream)
assertRemoteURL(t, defaultEnv, downstream, remote, defaultCacheURL)
statusDefault := runBraid(t, defaultEnv, downstream, braid, "--quiet", "status", "vendor/repo")
defaultCacheURL := repositoryCachePath(t, downstream, "vendor/repo", configMirror{URL: upstream, Branch: "main"})
assertRemoteURL(t, env, downstream, remote, defaultCacheURL)
statusDefault := runBraid(t, env, downstream, braid, "--quiet", "status", "vendor/repo")
assertExit(t, statusDefault, 0)
assertEmpty(t, "default cache status stderr", statusDefault.stderr)
assertContains(t, statusDefault.stdout, "(Removed Locally)")
assertPathExists(t, filepath.Join(defaultCacheURL, "HEAD"))
assertNoRemote(t, defaultEnv, downstream, remote)
assertNoRemote(t, env, downstream, remote)

envDisabled := defaultEnv.with("BRAID_USE_LOCAL_CACHE", "false")
envDisabled := env.with("BRAID_USE_LOCAL_CACHE", "false")
setupDisabled := runBraid(t, envDisabled, downstream, braid, "--quiet", "setup", "vendor/repo")
assertResult(t, setupDisabled, 0, "", "")
assertRemoteURL(t, envDisabled, downstream, remote, upstream)

gitOK(t, envDisabled, downstream, "remote", "rm", remote)
setupCacheDir := runBraid(t, envDisabled, downstream, braid, "--quiet", "--cache-dir", "explicit-cache", "setup", "vendor/repo")
setupCacheDir := runBraid(t, envDisabled, downstream, braid, "--quiet", "--global-cache-dir", "explicit-cache", "setup", "vendor/repo")
assertResult(t, setupCacheDir, 0, "", "")
assertRemoteURL(t, envDisabled, downstream, remote, cachePath(filepath.Join(processWorkingDir(t, downstream), "explicit-cache"), upstream))

Expand All @@ -53,10 +52,10 @@ func TestExecutableSetupCacheModes(t *testing.T) {
assertResult(t, setupNoCache, 0, "", "")
assertRemoteURL(t, env, downstream, remote, upstream)

invalid := runBraid(t, env, downstream, braid, "--no-cache", "--cache-dir", "cache", "setup")
invalid := runBraid(t, env, downstream, braid, "--no-cache", "--global-cache-dir", "cache", "setup")
assertExit(t, invalid, 2)
assertEmpty(t, "invalid cache stdout", invalid.stdout)
assertContains(t, invalid.stderr, "--no-cache and --cache-dir cannot be used together")
assertContains(t, invalid.stderr, "--no-cache and --global-cache-dir cannot be used together")
}

func TestExecutableFailurePaths(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion integration/subdirectory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func TestExecutableSubdirectoryLifecycle(t *testing.T) {

setup := runBraid(t, env, workDir, braid, "--quiet", "setup", "vendor/basic")
assertResult(t, setup, 0, "", "")
assertRemoteURL(t, env, downstream, remote, cachePath(env.braidCacheDir(), upstream))
assertRemoteURL(t, env, downstream, remote, repositoryCachePath(t, downstream, localPath, configMirror{URL: upstream, Branch: "main"}))

status := runBraid(t, env, workDir, braid, "--quiet", "status", "vendor/basic")
assertExit(t, status, 0)
Expand Down
85 changes: 39 additions & 46 deletions integration/support.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func expectedBraidVersion() string {

type processEnv struct {
values map[string]string
root string
}

func newProcessEnv(t *testing.T, root string) processEnv {
Expand All @@ -51,28 +52,28 @@ func newProcessEnv(t *testing.T, root string) processEnv {
}
}
globalConfig := filepath.Join(root, "xdg-config", "gitconfig")
if err := os.WriteFile(globalConfig, nil, 0o644); err != nil {
const globalConfigContents = "[gc]\n\tauto = 0\n[maintenance]\n\tauto = false\n"
if err := os.WriteFile(globalConfig, []byte(globalConfigContents), 0o644); err != nil {
t.Fatalf("write global gitconfig: %v", err)
}

values := map[string]string{
"APPDATA": filepath.Join(root, "appdata"),
"BRAID_LOCAL_CACHE_DIR": filepath.Join(root, "braid-cache"),
"GIT_AUTHOR_DATE": "2001-02-03T04:05:06Z",
"GIT_COMMITTER_DATE": "2001-02-03T04:05:06Z",
"GIT_CONFIG_GLOBAL": globalConfig,
"GIT_CONFIG_NOSYSTEM": "1",
"GIT_TERMINAL_PROMPT": "0",
"HOME": filepath.Join(root, "home"),
"LANG": "C",
"LC_ALL": "C",
"LOCALAPPDATA": filepath.Join(root, "local-appdata"),
"TEMP": filepath.Join(root, "tmp"),
"TMP": filepath.Join(root, "tmp"),
"TMPDIR": filepath.Join(root, "tmp"),
"USERPROFILE": filepath.Join(root, "userprofile"),
"XDG_CACHE_HOME": filepath.Join(root, "xdg-cache"),
"XDG_CONFIG_HOME": filepath.Join(root, "xdg-config"),
"APPDATA": filepath.Join(root, "appdata"),
"GIT_AUTHOR_DATE": "2001-02-03T04:05:06Z",
"GIT_COMMITTER_DATE": "2001-02-03T04:05:06Z",
"GIT_CONFIG_GLOBAL": globalConfig,
"GIT_CONFIG_NOSYSTEM": "1",
"GIT_TERMINAL_PROMPT": "0",
"HOME": filepath.Join(root, "home"),
"LANG": "C",
"LC_ALL": "C",
"LOCALAPPDATA": filepath.Join(root, "local-appdata"),
"TEMP": filepath.Join(root, "tmp"),
"TMP": filepath.Join(root, "tmp"),
"TMPDIR": filepath.Join(root, "tmp"),
"USERPROFILE": filepath.Join(root, "userprofile"),
"XDG_CACHE_HOME": filepath.Join(root, "xdg-cache"),
"XDG_CONFIG_HOME": filepath.Join(root, "xdg-config"),
}
if runtime.GOOS == "windows" {
if value, ok := os.LookupEnv("Path"); ok {
Expand All @@ -88,7 +89,7 @@ func newProcessEnv(t *testing.T, root string) processEnv {
} else if value, ok := os.LookupEnv("PATH"); ok {
values["PATH"] = value
}
return processEnv{values: values}
return processEnv{values: values, root: root}
}

func (e processEnv) list() []string {
Expand All @@ -110,33 +111,7 @@ func (e processEnv) with(key, value string) processEnv {
values[existingKey] = existingValue
}
values[key] = value
return processEnv{values: values}
}

func (e processEnv) without(keys ...string) processEnv {
values := make(map[string]string, len(e.values))
for existingKey, existingValue := range e.values {
values[existingKey] = existingValue
}
for _, key := range keys {
delete(values, key)
}
return processEnv{values: values}
}

func (e processEnv) braidCacheDir() string {
return e.values["BRAID_LOCAL_CACHE_DIR"]
}

func (e processEnv) defaultBraidCacheDir() string {
switch runtime.GOOS {
case "darwin":
return filepath.Join(e.values["HOME"], "Library", "Caches", "braid")
case "windows":
return filepath.Join(e.values["LOCALAPPDATA"], "braid")
default:
return filepath.Join(e.values["XDG_CACHE_HOME"], "braid")
}
return processEnv{values: values, root: e.root}
}

type commandResult struct {
Expand Down Expand Up @@ -346,6 +321,24 @@ func cachePath(cacheDir, url string) string {
return filepath.Join(cacheDir, hex.EncodeToString(sum[:]))
}

func repositoryCachePath(t *testing.T, repo, localPath string, m configMirror) string {
t.Helper()
cacheDir := filepath.Join(processWorkingDir(t, repo), ".git", "braid", "cache")
return filepath.Join(cacheDir, mirrorCacheID(localPath, m)+".git")
}

func mirrorCacheID(localPath string, m configMirror) string {
tracking := "revision"
if m.Branch != "" {
tracking = m.Branch
} else if m.Tag != "" {
tracking = m.Tag
}
parts := []string{m.URL, localPath, m.Path, tracking, m.Branch, m.Tag}
sum := sha256.Sum256([]byte(strings.Join(parts, "\x00")))
return hex.EncodeToString(sum[:16])
}

func remoteName(tracking, localPath string) string {
var b strings.Builder
for _, r := range tracking + "_braid_" + localPath {
Expand Down
Loading