Skip to content
Open
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,21 @@ Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Linked worktrees are reported inside their main repo's row, wherever they are checked out: a
worktree holding changes joins the branches in `UNPUSHED` as `branch [n files]`. Inside a repo,
a worktree can be selected, diffed, committed, and its branch pushed.

### Changed

- GitHub actions bumped off the Node 20 runtime.

### Fixed

- A nested repo, such as a worktree under `.claude/worktrees`, no longer shows as an untracked
`+0 -0` file of the repo around it, and `add --all` no longer stages it as an embedded gitlink.

## [0.1.0] 2026-09-09

First version.
Expand Down
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ CI runs `cargo test` alone on ubuntu and macos: no clippy or fmt gate, no `rustf
`main.rs` scan + table, `interactive.rs` browser + git writes, `term.rs` raw mode, keys, redraw.

`main()`: `collect_repos` walks sequentially and stops at the first `.git`, then `rayon` runs
`scan_repo` per repo (3 git calls each: `diff --numstat HEAD`, `ls-files --others`, `for-each-ref`).
A fourth call multiplies across every repo.
`scan_repo` per repo (3 git calls each: `diff --numstat HEAD`, `ls-files --others`, `for-each-ref`,
plus the first two per linked worktree, listed from `.git/worktrees` without a git call). A fourth
call multiplies across every repo. A linked worktree the walk reaches is mapped to its main repo.

Two git paths, on purpose. Reads go through `git()` (`main.rs:248`), which captures stdout and
returns `None` on non-zero exit. Writes go through `run_git()` (`interactive.rs:700`), which prints
Expand Down
34 changes: 25 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ z/s move space select a select all d open enter act q quit
| Key | In the repo list | Inside a repo |
| --- | --- | --- |
| `z` / `s`, or up / down | Move the cursor | Move the cursor |
| `space` | Select the repo | Select a file or a branch |
| `space` | Select the repo | Select a file, a worktree or a branch |
| `a` | Select or clear every repo | |
| `d`, or right | Open the repo | Show the diff of a file, the whole diff on `all changes`, or the unpushed commits of a branch |
| `d`, or right | Open the repo | Show the diff of a file, the whole diff on `all changes` or a worktree, or the unpushed commits of a branch |
| `enter` | Act on the selection | Act on this repo |
| `q`, or left | Quit | Back to the list |
| `ctrl-c` | Quit | Quit |
Expand Down Expand Up @@ -142,13 +142,17 @@ orgC/repoE
action: [p] push branches [c] commit [b] commit on new branch [q] cancel >
```

* `p` pushes the selected branches, or every unpushed branch of the selected repos. It then asks
for optional flags: `l` for `--force-with-lease`, `n` for `--no-verify`, `ln` for both.
* `c` commits, `b` creates a branch first. Each repo asks for its own message, shows
`git status --short`, and offers to push afterwards.
* `p` pushes the selected branches and the ones the selected worktrees have checked out, or every
unpushed branch of the selected repos. It then asks for optional flags: `l` for
`--force-with-lease`, `n` for `--no-verify`, `ln` for both. A worktree holds no commits of its
own: its branch is shared with the repo, so it is pushed like any other.
* `c` commits, `b` creates a branch first. Each checkout, the repo or one of its worktrees, asks
for its own message, shows `git status --short`, and offers to push afterwards. With nothing
selected the commit goes through every checkout showing changes; selecting files or worktrees
narrows it to those.
* Selected files are staged on their own (`git add -- file`), which is how you commit part of a
repo. With nothing selected the commit stages everything (`git add --all`), and the preview
says which of the two applies.
repo. With nothing selected the commit stages everything (`git add --all`) except nested repos,
which would otherwise land as embedded gitlinks, and the preview says which of the two applies.

Every git command is printed before it runs, and git keeps the real terminal, so credential
prompts, hooks and progress output behave normally. A repo that becomes clean after an action
Expand All @@ -162,6 +166,16 @@ ignore, kept only if the file was modified inside the window. A deleted file has
own, so its nearest surviving parent directory is used instead: removing an entry updates the
mtime of the directory that held it.

**Worktrees**: a repo keeps a single row. Its linked worktrees are read from `.git/worktrees`, so
one checked out under `.claude/worktrees`, next to the repo or outside DIR is found too, and a
worktree the walk reaches on its own is folded into its main repo. `CHANGES` counts the main
checkout only. A worktree holding uncommitted changes joins the branches in `UNPUSHED`, named
after the branch it has checked out (its directory when HEAD is detached) with its file count in
brackets: `OW-701-skill-owasp-mobile [8 files]`, or `feat ahead 2 (new) [3 files]` when that
branch also has unpushed commits. Worktrees come first, then the other branches. A nested repo is
not a change of the repo around it, so its directory is left out of that repo's untracked
files.

**Unpushed**: a local branch whose last commit falls inside the window and which holds commits
its remote does not. With an upstream, the count comes from `%(upstream:track)`. Without one, from
`git rev-list --count <branch> --not --remotes`, so a branch that was never pushed but exists on
Expand All @@ -178,7 +192,9 @@ differently named remote branch goes where it should. A branch with no upstream
files come from `git ls-files --others --exclude-standard`.
* Untracked files over 1 MiB count as a changed file but contribute no line count.
* Binary files count as a changed file with no lines, as git reports no numbers for them.
* Submodules are not followed: the walk stops at the first `.git` it finds.
* Submodules and nested clones are not followed: the walk stops at the first `.git` it finds.
Linked worktrees are the exception.
* A worktree is committed as a whole (`git add --all`), files inside it cannot be selected.
* Below roughly 50 columns the repo column gets short enough to be ambiguous. Nothing wraps or
garbles, but a wider terminal is easier to read.
* No stash inspection, no hunk staging, no amend.
Expand Down
Loading
Loading