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
20 changes: 20 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: release

# Pushing an annotated tag like v8.2.0 publishes a GitHub release for it,
# with the tag's own message as the release notes. Write the notes into the
# tag (git tag -a --cleanup=verbatim -F notes.md vX.Y.Z) and push it.
on:
push:
tags: ["v*"]

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- run: gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --verify-tag --notes-from-tag --title "gra ${TAG#v}"
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
137 changes: 67 additions & 70 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ gra clone git@github.com:martinus/oans.git # into ~/gra/oans, with a worktree
gra cd warmhare # jump into that worktree, from anywhere
gra work OA-2345 # another worktree, on that branch
gra work warmhare # back to that worktree's tmux window
gra switch main # or reuse the one you are standing in
gra done # throw it away once the work is merged
gra ls # what do I have, and where is it?
```
Expand Down Expand Up @@ -64,7 +63,7 @@ your shell afterwards.

> [!NOTE]
> Needs **Git** and **Python 3.10+**. [`fzf`](https://github.com/junegunn/fzf)
> powers the worktree and branch pickers, [`gh`](https://cli.github.com/) lets
> powers the branch picker of `gra work`, [`gh`](https://cli.github.com/) lets
> `gra` spot that a clone is a fork, and `tmux` - when you work inside it -
> gets a window per worktree.

Expand All @@ -88,8 +87,7 @@ When there is no `~/.bashrc` (`gra shell` only speaks bash) nothing is written
and the lines to add are printed instead.

That one `eval` is what makes `gra cd` change directories, `gra done` leave
the removed directory, and `<TAB>` complete worktrees, repositories and
branches.
the removed directory, and `<TAB>` complete worktree names and branches.

</details>

Expand All @@ -112,15 +110,14 @@ git config --global gra.root ~/develop
| ------- | ------------ |
| [`gra clone <url>`](#gra-clone) | Clone into `~/gra/<repo>` as a bare checkout, with a worktree ready to use |
| [`gra work [target]`](#gra-work) | Create a worktree for a branch - or open a worktree's tmux window |
| [`gra switch [branch]`](#gra-switch) | Move the worktree you are standing in to another branch |
| [`gra done [name]`](#gra-done) | Remove a worktree once its work is in origin |
| [`gra cd [name]`](#gra-cd) | Jump to a worktree, by name or with `fzf` |
| [`gra cd <name>`](#gra-cd) | Jump to a worktree by name |
| [`gra ls [--fetch]`](#gra-ls) | One table of every repository and worktree |
| [`gra fetch`](#gra-fetch) | `git fetch --prune --all` in every repository, in parallel |
| [`gra each [--wt] <command>`](#gra-each) | Run a command once in every repository or worktree |
| [`gra install`](#gra-install) | Install or upgrade `gra` itself |

Everything runs from anywhere. The exceptions are the commands that act on
*where you are standing*: `gra switch`, and a bare `gra work` or `gra done`.
*where you are standing*: a bare `gra work` or `gra done`.

---

Expand Down Expand Up @@ -171,7 +168,7 @@ repository it was forked from:

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

A fork is a GitHub concept rather than a Git one, so `gra` asks `gh`, which
Expand Down Expand Up @@ -264,8 +261,7 @@ with the newest commits first - the branch you are here for is almost always
near the top. Branches already checked out in a worktree are not offered,
because Git allows a branch in only one worktree at a time. The first entry
starts the worktree detached at origin's default branch instead - instantly
usable for looking around, running builds, or letting a later `gra switch`
pick the real work.
usable for looking around or running builds.

If the branch you name is already checked out somewhere, `gra` names that
worktree instead of leaving you with Git's message:
Expand Down Expand Up @@ -302,7 +298,7 @@ ERROR: 'OA-7777' matches several branches; name one:
```

Nothing matching means the old behaviour: `gra` offers to create the branch
from origin's default branch. The same resolution applies to `gra switch`.
from origin's default branch.

</details>

Expand Down Expand Up @@ -353,33 +349,6 @@ repository claim a contested name first, and then only that repository shifts.

</details>

## `gra switch`

To reuse the worktree you are standing in for other work instead of creating
a new one:

```sh
gra switch feature/search # by name, or part of one
gra switch # pick with fzf
```

Branch resolution is the same as [`gra work`](#gra-work)'s: existing local
branches are switched to, `origin/<branch>` gets a local tracking branch,
missing branches can be created from origin's default branch and pushed, and
part of a name is enough when it matches one branch. Without a branch, an
`fzf` picker offers all branches, newest commits first - minus those already
checked out in a worktree, which a switch could never reach.

Afterwards the worktree's [tmux window](#tmux-windows) is opened or switched
to. The window is named after the worktree, not the branch, so a switch keeps
the window it already has.

> [!IMPORTANT]
> `gra switch` refuses when the worktree has uncommitted changes - commit or
> stash first. Switching to a branch checked out elsewhere is refused too,
> naming the worktree that holds it. Run anywhere but inside a worktree it
> fails: there is no checkout there whose branch it could change.

## `gra done`

Run it inside a worktree when the work in it is finished, or name one from
Expand Down Expand Up @@ -436,12 +405,14 @@ scripts, where an unanswered question counts as no.
## `gra cd`

```sh
gra cd # pick with fzf
gra cd snowwolf # jump straight to the worktree named snowwolf
gra cd snowwolf # jump to the worktree named snowwolf
```

The command prints the selected path; the shell integration is what turns that
into an actual `cd`. `gra install` adds it for you, or add it yourself:
Because a name identifies one worktree on the whole machine, that is all `gra`
needs - and `<TAB>` completes the names, so `gra ls` followed by `gra cd` and
Tab is the whole navigation. The command prints the worktree's path; the shell
integration is what turns that into an actual `cd`. `gra install` adds it for
you, or add it yourself:

```sh
eval "$(gra shell bash)"
Expand Down Expand Up @@ -474,34 +445,59 @@ meaning, `BRANCH` is the primary information - the name is just an address.
push, `↓1` is one to pull, blank is in sync or has no upstream, and `-` means
the question does not apply (detached, or no upstream). It reads local refs
only - `gra ls` never goes to the network - so it is as fresh as your last
fetch. `--fetch` is [`gra fetch`](#gra-fetch) followed by `gra ls`.
fetch.

`--fetch` first runs `git fetch --prune --all` in every repository, several at
a time. 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 it is safe to run at any time.

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

```text
Fetching 12 repositories
oans: fatal: could not read from remote repository.
fetched 11 of 12 repositories
```

## `gra fetch`
## `gra each`

Runs `git fetch --prune --all` in every repository under the gra root, several
at a time:
Runs a command of your own once in every repository under the gra root:

```sh
gra fetch
gra each git fetch --all --tags
gra each git gc
gra each du -sh .
```

Everything after `each` - apart from its own `--wt` - is the command; `gra`
passes it through untouched, flags and all. It runs in each repository's
`.bare` directory, so Git commands act on the repository itself, not on one
worktree. The repositories run one at a time, each announced by name, with
the command's output shown as it runs:

```text
Fetching 12 repositories
fetched 12 repositories
gra: git fetch --all --tags
Fetching origin
oans: git fetch --all --tags
Fetching origin
Fetching upstream
```

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 it is safe to run at any time, and it is
what makes the `SYNC` column of `gra ls` current.
With `--wt` the command runs in every worktree instead, one run per
worktree, labelled `<repo>/<worktree>`:

A repository that cannot be reached is named with the reason, and the others
are still fetched:
```sh
gra each --wt git merge --ff-only # fast-forward every worktree
gra each --wt git status --short
```

A repository where the command fails does not stop the others. The failed
ones are listed at the end, and `gra` exits with an error:

```text
Fetching 12 repositories
oans: fatal: could not read from remote repository.
fetched 11 of 12 repositories
ERROR: failed in: oans
```

## `gra install`
Expand Down Expand Up @@ -552,7 +548,6 @@ Inside tmux, a worktree gets one window, named `<repo>/<worktree>`:
| ------- | -------------------------- |
| `gra clone`, `gra work <branch>` | opened for the new worktree |
| `gra work <worktree>`, bare `gra work` inside one | opened, or switched to when already open |
| `gra switch` | opened or switched to, keeping its name |
| `gra done` | closed |

The name is the whole mechanism. There is no config file, nothing to install,
Expand All @@ -561,8 +556,8 @@ acts on the answer. Two consequences follow.

**A window is never duplicated.** A worktree outlives its window, and every
command that leaves you in a worktree looks the name up before opening
anything - so `gra switch`, a second `gra work`, and reopening a window you
closed yesterday all land in the same place.
anything - so a second `gra work`, and reopening a window you closed
yesterday, land in the same place.

**The window need not be in this session.** `gra` looks at every session, so
`gra done snowwolf` closes the window even when it lives in a session you are
Expand Down Expand Up @@ -636,12 +631,13 @@ The `eval` line installs Bash completion too, so there is nothing else to set
up:

```text
gra <TAB> install clone fetch ls work switch done cd shell
gra <TAB> install clone ls each work done cd shell
gra cd <TAB> warmhare goldfish snowwolf worktree names
gra done <TAB> warmhare goldfish snowwolf --force
gra work <TAB> worktree names, plus branches inside a repository
gra switch <TAB> branches, inside a repository
gra each <TAB> commands on your PATH
gra done -<TAB> --force
gra each -<TAB> --wt
```

What it offers follows the same rule the commands do, so the completion and
Expand All @@ -659,13 +655,14 @@ every time.

<br>

The layout is designed so that a coding agent can manage branches itself
inside one worktree. A useful convention for a repository's `CLAUDE.md`:
The layout is designed so that a coding agent can manage branches itself. A
useful convention for a repository's `CLAUDE.md`:

* to work on another branch in this worktree: commit or stash, then
`gra switch <branch>`,
* `gra switch` also creates missing branches (from origin's default branch,
pushed and tracking) after asking for confirmation.
* to work on another branch: `gra work <branch>` gives it a worktree of its
own, and creates a missing branch (from origin's default branch, pushed and
tracking) after asking for confirmation,
* to reuse the worktree you are in: commit or stash, then plain
`git switch <branch>`.

For parallel agents, give each its own worktree with `gra work` - one branch
can only be checked out in one worktree at a time.
Expand Down
Loading
Loading