https://github.com/fverse/berrypick
CLI to cherry-pick a commit, a PR, or whatever last changed a file:line onto your target branch.
https://github.com/fverse/berrypick
Last synced: 7 days ago
JSON representation
CLI to cherry-pick a commit, a PR, or whatever last changed a file:line onto your target branch.
- Host: GitHub
- URL: https://github.com/fverse/berrypick
- Owner: fverse
- Created: 2026-06-16T14:20:33.000Z (19 days ago)
- Default Branch: main
- Last Pushed: 2026-06-26T02:44:42.000Z (10 days ago)
- Last Synced: 2026-06-26T03:12:09.102Z (10 days ago)
- Language: Go
- Homepage:
- Size: 5.85 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# berrypick
**When the same change has to land on several long-lived branches, `berrypick`
moves the work over and tracks where it still needs to go.** Teams that maintain
more than one branch in parallel constantly re-apply the same commits by hand and
lose track of which branches got which change. `berrypick` does the move for you
and keeps a **shared, committed record** of what's been ported and what's still
pending: queue the picks a branch needs, see an at-a-glance matrix of done versus
todo across every branch, and have each pick record itself automatically — so
nothing slips through and the whole team works from the same picture.
[Jump to tracking →](#tracking-cherry-picks-across-branches)
It does the cherry-picking. Point it at a **commit hash**, a
**`:`** (the commit that last changed that line), or a **GitHub Pull
Request**, and it cherry-picks the work onto a fresh branch off your target —
recording the result as it goes.
## Install
### macOS / Linux
```sh
curl -fsSL https://raw.githubusercontent.com/fverse/berrypick/main/install.sh | sh
```
This downloads the **latest** release for your OS and architecture and installs
it to `/usr/local/bin`.
### Windows (PowerShell)
```powershell
irm https://raw.githubusercontent.com/fverse/berrypick/main/install.ps1 | iex
```
### With Go
```sh
go install github.com/fverse/berrypick@latest
```
### Manual download
Grab a prebuilt archive for your platform from the
[latest release](https://github.com/fverse/berrypick/releases/latest) — builds
are provided for **macOS**, **Linux**, and **Windows** (`amd64` and `arm64`).
## Usage
```
berrypick [branch-name] [flags]
```
The first argument can be:
- a **commit hash** (full or abbreviated) — cherry-picks that one commit;
every commit in the PR, in chronological order;
- a **`:`** reference — uses `git blame` to find the commit that last
changed that line and cherry-picks it (see [Blame mode](#blame-mode)).
- a **GitHub PR URL** (`https://github.com///pull/`) — cherry-picks
By default the new branch is named `cherry-pick/`. Pass
an optional third argument to name the branch yourself instead.
### Examples
```sh
# Cherry-pick a single commit onto a new branch off release/1.2
berrypick a1b2c3d4e5f6 release/1.2
# Cherry-pick the commit that last changed line 42 of a file, onto main
berrypick src/utils/util.js:42 main
# Cherry-pick every commit in PR #123 onto a new branch off main, then push
berrypick https://github.com/owner/repo/pull/123 main --push
# Cherry-pick a commit onto a new branch off release/1.2 with a custom branch name
berrypick a1b2c3d4e5f6 release/1.2 hotfix/login-bug
```
### Flags
| Flag | Description |
| -------------------- | --------------------------------------------------------------------------------------- |
| `--push` | Push the new branch to `origin` after a successful cherry-pick. |
| `--delete-local` | Delete the local cherry-pick branch after a successful push. Requires `--push`. |
| `--force` | Recreate the working branch if it already exists. |
| `-m`, `--mainline N` | Parent number (1-based) to follow when cherry-picking a **merge commit** (e.g. `-m 1`). |
| `--rev ` | For a `:` source, blame this revision instead of the working tree. |
| `-h`, `--help` | Show help. |
### Merge commits
A merge commit has more than one parent, so git needs to know which side to
treat as the mainline. `berrypick` detects this and stops with guidance; re-run
with `-m 1` (usually the target-branch parent) to cherry-pick it:
```sh
berrypick -m 1 9f1e5d1b2622d3538a0589ac2d76758b551a1340 dev
```
### Blame mode
Pass a `:` reference to cherry-pick the commit that last modified
that line, as reported by `git blame`:
```sh
berrypick internal/git/git.go:42 main
# Blame a specific revision instead of the working tree
berrypick internal/git/git.go:42 main --rev origin/main
```
> **This cherry-picks the _entire_ commit that last touched the line, not just
> the line.** If that commit changed 10 files, all 10 come along.
The line is blamed in your working tree by default; use `--rev ` to blame a
specific commit or branch. It fails clearly when the line is not committed yet,
the line number is out of range, or the file is missing or untracked.
## Tracking cherry-picks across branches
For teams that back-port the same fixes onto several branches, `berrypick`
can track what still needs picking and what's already done — in a **shared,
committed** log so everyone sees the same picture.
```sh
berrypick init # scaffold .berrypick/ (config.toml + log.jsonl)
```
This creates a `.berrypick/` directory at the repo root:
```
.berrypick/
config.toml # hand-edited, committed: your branch topology
log.jsonl # append-only event log, committed
```
**Commit both files.** The log is append-only (one line per event) precisely so
two teammates recording picks the same day produce two lines, not a merge
conflict.
### `config.toml`
Simple form — one source, many targets:
```toml
[branches]
source = "main"
targets = ["release/2.0", "release/1.0"]
```
Advanced form — chained back-ports. If `[[flows]]` is present it wins over
`[branches]`:
```toml
[[flows]]
from = "main"
to = ["release/2.0"]
[[flows]]
from = "release/2.0"
to = ["release/1.0"]
```
Optional forge block (autodetected from the remote when omitted):
```toml
[forge]
kind = "github" # github | gitlab | ...
host = "github.com"
```
A configured branch that doesn't exist yet only warns — you can write the config
ahead of creating the branches.
### `berrypick todo`
Track which picks still need to happen:
```sh
# Queue a change onto every configured target (or pass --to to pick branches)
berrypick todo add a1b2c3d4
berrypick todo add https://github.com/owner/repo/pull/123 --to release/2.0
berrypick todo add internal/git/git.go:42
berrypick todo list # grouped by target branch
berrypick todo list --branch release/2.0
berrypick todo list --json # for scripting
berrypick todo remove a1b2c3d4 # across all targets where it's queued
berrypick todo remove a1b2c3d4 --to release/1.0
```
Each `(item, target)` pair is its own todo, so partial back-ports are trackable.
Subject and author are resolved when you add, so the list reads well without
re-fetching. Adding an already-queued or already-done pair is skipped, not
double-added.
### `berrypick status`
Fold the log into a matrix — rows are tracked changes, columns are target
branches:
```sh
berrypick status
```
```
Cherry-pick status (source: main)
# ID SUBJECT release/2.0 release/1.0
─────────────────────────────────────────────────────────────────
1 a1b2c3d4 Fix null deref ✓ done ⧗ todo
2 e5f6a7b8 Patch CVE-2026-1234 ✓ done ✓ done
```
| Cell | Meaning |
| -------- | ------------------------------------------- |
| `✓ done` | latest event for `(id, branch)` is **done** |
| `⧗ todo` | latest event is **queued** |
| `· -` | not tracked for that branch |
The `#` column gives each row a number you can pick by (see below).
```sh
berrypick status --branch release/2.0 # one column
berrypick status --json # full matrix as JSON
berrypick status --reconcile # see below
```
`--reconcile` scans each target branch's history for `cherry picked from commit`
(`-x`) annotations and surfaces picks made **outside** berrypick, offering to
backfill `done` events for them. It's behind the flag so plain `status` stays
fast.
### Cherry-picking straight from a todo
Once a change is queued, you can pick it without retyping the SHA and target. The
ordinary `berrypick` command gains two tracking shortcuts:
```sh
berrypick :2 # pick row #2 from `berrypick status`
berrypick a1b2c3d4 # pick a queued change onto the branch it's queued for
```
- A **`:N`** argument refers to row `N` in `berrypick status` (the leading colon
keeps it distinct from a commit hash). Row numbers are positional — they shift
as todos are added or completed, so they mean "the list I'm looking at now,"
not a stable id.
- The **target branch is optional**: when the change is queued for exactly one
branch, that branch is used; when it's queued for several, you're asked which
(or pass the branch explicitly, e.g. `berrypick :2 release/1.0`).
The pick runs the normal cherry-pick flow, so it records the `done` event
automatically — the todo flips from `⧗ todo` to `✓ done`.
### `berrypick compact`
```sh
berrypick compact
```
Rewrites `log.jsonl` keeping only the latest event per `(id, target)` key, to
bound growth. Derived state is unchanged. Because this rewrites the **shared**
committed log, coordinate it like any shared-file rewrite (compact, commit, have
teammates re-pull).
## GitHub access
For PR sources, the tool talks to the GitHub REST API and discovers credentials
automatically, with **no `gh` binary required at runtime**:
- A token from the `GH_TOKEN` / `GITHUB_TOKEN` environment variable, **or** the
token stored by a previous `gh auth login` — whichever is found first.
- Enterprise hosts (`https:///api/v3`) are handled automatically.
- **Public repositories work with no credentials at all** (subject to GitHub's
low anonymous rate limit). A token is only required for **private** repos.
```sh
export GITHUB_TOKEN=ghp_xxx # for private repos, if you haven't run `gh auth login`
```
## Build
From a clone:
```sh
go build -o berrypick . # or: make build
make install # builds and installs into $(go env GOPATH)/bin
```