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
122 changes: 28 additions & 94 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ repository it was forked from:

```text
added remote 'upstream' -> git@github.com:upstream/oans.git
run 'git fetch upstream' to get its branches
run 'gra fetch' for its branches
```

A fork is a GitHub concept rather than a Git one, so `gra` asks `gh`, which
Expand Down Expand Up @@ -164,9 +164,8 @@ The bare checkout is set up to behave like a normal clone:
remote-tracking refs, and never mirrors them into local branches,
* `origin/HEAD` points at origin's default branch,
* reflogs are enabled (bare repositories disable them by default),
* `.claude/worktrees/` and `.grakeep` are added to the shared local Git
exclude file so tool-managed paths and keep markers do not show up as
untracked files in any worktree.
* `.claude/worktrees/` is added to the shared local Git exclude file so
tool-managed paths do not show up as untracked files in any worktree.

## work - create a new worktree, or set one up again

Expand Down Expand Up @@ -280,18 +279,14 @@ another repository is skipped the same way, and `gra done` frees a name for the
next `gra work` to reclaim. Machines only disagree when one of them let another
repository claim a contested name first, and then only that repository shifts.

Worktrees created before `gra` 4.0 carry a single four-letter word instead;
[`gra migrate`](#migrate---rename-worktrees-to-the-current-naming-scheme)
renames them.

### Hooks

`gra` runs two scripts from the repository container, next to `.bare`:

| Hook | When | Working directory |
| --------- | --------------------------------------------- | ----------------- |
| `work.sh` | whenever `gra` leaves you in a worktree | that worktree |
| `done.sh` | before `gra done` or `gra clean` removes one | the container |
| `done.sh` | before `gra done` removes one | the container |

`work.sh` runs once per worktree only in the simplest case. It also runs after
`gra switch` moves a worktree to another branch, and again whenever `gra work`
Expand Down Expand Up @@ -437,111 +432,50 @@ information; the name is just an address.
push, `↓1` is one to pull, and blank is in sync or has no upstream. It reads
local refs only - `gra ls` never goes to the network - so it is as fresh as
your last fetch. A `-` means the question does not apply: a detached worktree,
or a branch with no upstream. To refresh it, fetch (`gra clean` does, for every
repository) and run `gra ls` again.

## hooks - write missing hooks
or a branch with no upstream. Pass `--fetch` to refresh every repository
first, which is `gra fetch` followed by `gra ls`:

```sh
gra hooks
gra ls --fetch
```

Writes `work.sh` and `done.sh` into every repository under the gra root that
is missing them, and leaves existing ones alone - so it cannot overwrite your
edits and is safe to re-run.
## fetch - fetch every repository

## clean - report or remove clean merged worktrees

Run `gra clean` from anywhere to classify every worktree under the configured
gra root. It prints one table like `gra ls`, with `VERDICT` and `REASON`
columns added:
Run `gra fetch` from anywhere to run `git fetch --prune --all` in every
repository under the gra root, several at a time:

```sh
gra clean
gra fetch
```

Example output:

```text
Root: /home/me/gra
Repositories: 2 Worktrees: 2

REPOSITORY WORKTREE BRANCH STATUS VERDICT REASON
gra snowwolf feature ● dirty keep uncommitted changes
oans warmhare old-fix ✓ clean remove merged into origin/main

Dry run. Re-run with --yes to remove 1 worktree(s).
Fetching 12 repositories
fetched 12 repositories
```

Verdicts mean:
Every remote, not just `origin`: a fork's `upstream` is exactly the remote
that goes stale. Only remote-tracking refs move - no worktree, branch, or
uncommitted change is touched - so this is safe to run at any time, and what
makes the `SYNC` column of `gra ls` current.

* `keep` - the worktree has uncommitted changes or commits that are not merged
into origin's default branch. A clean worktree that would otherwise be
removable is also kept when it contains a `.grakeep` file.
* `remove` - the worktree is clean and its `HEAD` is already merged into
origin's default branch, or its commits are patch-equivalent to changes
already there after a squash or cherry-pick merge.
* `prune` - Git still knows about the worktree, but the directory no longer
exists on disk.

By default, `gra clean` is a dry run. Use `--yes` to remove worktrees marked
`remove` and prune missing entries:

```sh
gra clean --yes
```

Before classifying, `gra clean` runs `git fetch --prune origin` in each
repository. Use `--no-fetch` to skip that step.

`gra clean --yes` deletes the local branch of each removed worktree - the
merged or patch-equivalent verification already happened as part of the
verdict. It runs each repository's `done.sh` before removing a worktree, like
`gra done` does. It never removes dirty worktrees.

## migrate - rename worktrees to the current naming scheme

Worktree names used to be a single four-letter word. `gra migrate` renames
every worktree still named that way, giving each the name the current `gra`
would have given it:

```sh
gra migrate
```
A repository that cannot be reached is named with the reason, and the others
are still fetched:

```text
Root: /home/me/gra
Repositories: 2 Worktrees to rename: 3

REPOSITORY WORKTREE NEW NAME
gra wolf snowwolf
rose goldfish
oans hare warmhare

Dry run. Re-run with --yes to rename 3 worktree(s).
Fetching 12 repositories
oans: fatal: could not read from remote repository.
fetched 11 of 12 repositories
```

Like `gra clean`, it is a dry run until you pass `--yes`:
## hooks - write missing hooks

```sh
gra migrate --yes
gra hooks
```

Only the directory moves, with `git worktree move`, so Git follows it:
branches, commits, and uncommitted changes are all untouched. Names are handed
out exactly as `gra work` hands them out - the repository's own preference
order, skipping every name taken anywhere - so a migrated worktree gets the
same name it would have got had it been created today, and no two worktrees
can land on the same name.

Worktrees already on the current scheme are left alone, so re-running it does
nothing. That also makes it safe on a machine where only some repositories
were touched.

Two things it deliberately does not do. A tmux window opened for the old name
keeps that name; `gra work` inside the worktree re-runs `work.sh` and gets you
a window with the new one. And if your shell is standing in a worktree that
moved, `gra migrate` tells you where it went rather than moving you.
Writes `work.sh` and `done.sh` into every repository under the gra root that
is missing them, and leaves existing ones alone - so it cannot overwrite your
edits and is safe to re-run.

## cd - jump to a worktree

Expand All @@ -567,7 +501,7 @@ eval "$(gra shell bash)"
The same line installs Bash completion, so there is nothing else to set up:

```text
gra <TAB> install clone ls work switch done cd shell hooks clean migrate
gra <TAB> install clone fetch ls work switch done cd shell hooks
gra cd <TAB> warmhare goldfish snowwolf worktree names
gra done <TAB> warmhare goldfish snowwolf --force
gra work <TAB> branches, inside a repository
Expand Down
Loading
Loading