marcgit (mg)
marcgit is a git worktree manager. Each project is laid out as a trunk/
directory (the main worktree) plus sibling folders next to it, one per
branch / PR:
myproject/
├── trunk/ # main worktree — the anchor, never rename it
├── feat-login/ # mg work feat/login
└── fix-typo/ # mg pr 42 (PR head branch fix/typo)
A branch name's slashes become dashes in the folder name: feat/login →
feat-login. No nested subfolders are created.
CRITICAL: call the binary directly, not mg
mg is a shell function users install into their interactive shell
(eval "$(marcgit init bash)"). It will not exist in the non-interactive
shell you run Bash commands in. Always invoke the real binary marcgit, and
handle the printed path yourself.
How output works:
- Navigating commands (
work, pr, new, clone, trunk) print the
target directory to stdout (the mg wrapper would cd there). Capture
stdout to learn where the worktree is.
- Everything else (status/info,
🌱/🧹 messages) goes to stderr.
So instead of relying on cd, capture the path and act on it explicitly:
dir="$(marcgit work feat/login)" # stdout = new/existing worktree path
git -C "$dir" status # operate without changing your own cwd
If marcgit is not on PATH, tell the user to install it
(cargo install --git https://github.com/marc2332/git); do not silently fall
back to raw git worktree unless they ask.
Command reference
| Command |
Alias |
Effect |
stdout |
marcgit new <project> |
n |
Create <project>/trunk and git init -b main it. |
trunk path |
marcgit clone <url> <project> |
c |
Clone <url> into <project>/trunk. |
trunk path |
marcgit work <branch> |
w |
Create (or jump to) a sibling worktree for <branch>; creates the branch off trunk's HEAD if it doesn't exist. Idempotent. |
worktree path |
marcgit pr <number> |
p |
Use gh to resolve PR #N's head branch, fetch it (pull/N/head), add a worktree named after that branch. Idempotent. Needs gh. |
worktree path |
marcgit trunk |
t |
Resolve the project's trunk path. |
trunk path |
marcgit list |
l |
git worktree list for the current project. |
listing |
marcgit remove <branch> |
r |
Force-remove the worktree and delete its branch, then point back at trunk. |
trunk path |
marcgit prune <days> [-n|--dry-run] |
— |
Remove sibling worktrees whose last commit is older than <days> days. Prompts [y/N] on stdin and skips dirty/stashed worktrees. -n previews only. |
— |
marcgit init <bash|zsh|fish|nushell> |
— |
Print the shell wrapper code. |
wrapper |
marcgit config init |
— |
Write a default marcgit.toml at the project root. |
config path |
marcgit config path |
— |
Print the marcgit.toml reachable from cwd. |
config path |
Notes:
- Every command except
new, clone, and init must run inside a project
(any worktree of it). marcgit finds the project by locating the worktree
literally named trunk. If it errors with "no trunk worktree found", you're
not in a marcgit project — use marcgit new/clone first, or cd into one.
work and pr are idempotent: if the folder already exists they just return
its path.
work and pr accept -s / --still, which moves the worktree path to stderr so
the user's mg wrapper doesn't cd. Don't pass it yourself — you need the
path on stdout. Mention it when the user wants a worktree set up without
leaving their current directory.
prune blocks on a y/N confirmation prompt. When running it
non-interactively, always do a --dry-run first, show the user what would
be pruned, and only then run the real prune (piping yes only with explicit
user approval). Never auto-confirm destructive prunes.
config init: init-submodules = true (default) makes work/pr run
git submodule update --init --recursive when a .gitmodules exists.
Workflow: "create a worktree for this feature and copy my changes into it"
This is the headline use case. The user is working in one worktree (often
trunk), has in-progress edits, and wants a clean feature worktree that
contains those same edits. Literally copy the file edits across.
- Identify the source worktree — your current location, or the path the
user names. Capture its absolute path:
src="$(git rev-parse --show-toplevel)"
- Pick the branch name from what the user said (e.g. "login feature" →
feat/login). Confirm the name if it's ambiguous.
- Create the worktree and capture its path:
dst="$(marcgit work feat/login)"
- Copy the edits from
src into dst. Cover both tracked and untracked:# tracked, staged + unstaged modifications → as a patch
git -C "$src" diff HEAD > /tmp/mg-feature.patch
if [ -s /tmp/mg-feature.patch ]; then
git -C "$dst" apply /tmp/mg-feature.patch
fi
# untracked files → copy verbatim, preserving relative paths
git -C "$src" ls-files --others --exclude-standard -z |
while IFS= read -r -d '' f; do
mkdir -p "$dst/$(dirname "$f")"
cp -p "$src/$f" "$dst/$f"
done
If git apply fails (e.g. the worktree branched from a different base), fall
back to a 3-way apply: git -C "$dst" apply --3way /tmp/mg-feature.patch, and
if that still conflicts, tell the user which files conflicted instead of
guessing.
- Verify the copy landed and report the destination:
git -C "$dst" status --short
Tell the user the new worktree path ($dst) and what was copied.
Decide tracked-vs-everything by intent: git diff HEAD carries every change
relative to the last commit (staged + unstaged). If the user only wants specific
files, scope the patch with pathspecs: git -C "$src" diff HEAD -- path/a path/b.
Do NOT remove the edits from the source unless the user asks to move (not
copy) them. If they want a move, after verifying the copy you can
git -C "$src" checkout -- . / clean untracked — but confirm first, it's
destructive.
When the user instead describes a feature in prose (no existing edits), just
create the worktree (step 3), cd/operate inside $dst, and implement the work
there fresh.
Other common requests
- "work on PR 42" →
dst="$(marcgit pr 42)", then operate in $dst.
Requires gh authenticated for the repo.
- "go back to trunk" →
marcgit trunk (path on stdout).
- "what worktrees exist" →
marcgit list.
- "delete the login worktree" →
marcgit remove feat/login (force-removes
worktree + branch; confirm with the user since it deletes the branch).
- "clean up stale worktrees older than 30 days" →
marcgit prune 30 -n
first, show the list, then marcgit prune 30 with user sign-off.
- "start a new project" →
marcgit new <name> or
marcgit clone <url> <name>.
Gotchas
- The shell wrapper only
cds when stdout is a single existing directory;
multi-line output (like list) is printed, not cd'd. You read stdout
directly anyway, so this only matters when you advise the user.
- Folder names are sanitized (
/ → -); when you need the folder for a branch,
derive it the same way, or just trust the path marcgit prints.
prune measures age by each worktree's last commit (git log -1) and refuses
worktrees with uncommitted changes, untracked files, or stashes on their
branch — so "nothing to prune" can mean "all candidates were dirty."
1---2name: marcgit3description: Use when the user wants to manage git worktrees with marcgit (a.k.a. `mg`) — creating a worktree for a feature/branch/PR, switching to trunk, listing or pruning worktrees, or moving in-progress edits into a fresh worktree. Triggers on phrases like "marcgit", "mg work", "create a worktree", "make a worktree for this feature", "spin up a branch worktree", "checkout PR N as a worktree", "prune old worktrees".4---56# marcgit (`mg`)78`marcgit` is a git worktree manager. Each project is laid out as a `trunk/`9directory (the main worktree) plus **sibling** folders next to it, one per10branch / PR:1112```13myproject/14├── trunk/ # main worktree — the anchor, never rename it15├── feat-login/ # mg work feat/login16└── fix-typo/ # mg pr 42 (PR head branch fix/typo)17```1819A branch name's slashes become dashes in the folder name: `feat/login` →20`feat-login`. No nested subfolders are created.2122## CRITICAL: call the binary directly, not `mg`2324`mg` is a **shell function** users install into their interactive shell25(`eval "$(marcgit init bash)"`). It will **not** exist in the non-interactive26shell you run Bash commands in. **Always invoke the real binary `marcgit`**, and27handle the printed path yourself.2829How output works:30- **Navigating** commands (`work`, `pr`, `new`, `clone`, `trunk`) print the31 **target directory to stdout** (the `mg` wrapper would `cd` there). Capture32 stdout to learn where the worktree is.33- Everything else (status/info, `🌱`/`🧹` messages) goes to **stderr**.3435So instead of relying on `cd`, capture the path and act on it explicitly:3637```bash38dir="$(marcgit work feat/login)" # stdout = new/existing worktree path39git -C "$dir" status # operate without changing your own cwd40```4142If `marcgit` is not on PATH, tell the user to install it43(`cargo install --git https://github.com/marc2332/git`); do not silently fall44back to raw `git worktree` unless they ask.4546## Command reference4748| Command | Alias | Effect | stdout |49| --- | --- | --- | --- |50| `marcgit new <project>` | `n` | Create `<project>/trunk` and `git init -b main` it. | trunk path |51| `marcgit clone <url> <project>` | `c` | Clone `<url>` into `<project>/trunk`. | trunk path |52| `marcgit work <branch>` | `w` | Create (or jump to) a sibling worktree for `<branch>`; creates the branch off trunk's HEAD if it doesn't exist. Idempotent. | worktree path |53| `marcgit pr <number>` | `p` | Use `gh` to resolve PR #N's head branch, fetch it (`pull/N/head`), add a worktree named after that branch. Idempotent. Needs `gh`. | worktree path |54| `marcgit trunk` | `t` | Resolve the project's `trunk` path. | trunk path |55| `marcgit list` | `l` | `git worktree list` for the current project. | listing |56| `marcgit remove <branch>` | `r` | Force-remove the worktree and delete its branch, then point back at trunk. | trunk path |57| `marcgit prune <days> [-n\|--dry-run]` | — | Remove sibling worktrees whose last commit is older than `<days>` days. **Prompts `[y/N]` on stdin** and skips dirty/stashed worktrees. `-n` previews only. | — |58| `marcgit init <bash\|zsh\|fish\|nushell>` | — | Print the shell wrapper code. | wrapper |59| `marcgit config init` | — | Write a default `marcgit.toml` at the project root. | config path |60| `marcgit config path` | — | Print the `marcgit.toml` reachable from cwd. | config path |6162Notes:63- Every command except `new`, `clone`, and `init` must run **inside a project**64 (any worktree of it). marcgit finds the project by locating the worktree65 literally named `trunk`. If it errors with "no `trunk` worktree found", you're66 not in a marcgit project — use `marcgit new`/`clone` first, or `cd` into one.67- `work` and `pr` are idempotent: if the folder already exists they just return68 its path.69- `work` and `pr` accept `-s` / `--still`, which moves the worktree path to stderr so70 the user's `mg` wrapper doesn't `cd`. Don't pass it yourself — you need the71 path on stdout. Mention it when the user wants a worktree set up without72 leaving their current directory.73- `prune` blocks on a `y/N` confirmation prompt. When running it74 non-interactively, **always do a `--dry-run` first**, show the user what would75 be pruned, and only then run the real prune (piping `yes` only with explicit76 user approval). Never auto-confirm destructive prunes.77- `config init`: `init-submodules = true` (default) makes `work`/`pr` run78 `git submodule update --init --recursive` when a `.gitmodules` exists.7980## Workflow: "create a worktree for this feature and copy my changes into it"8182This is the headline use case. The user is working in one worktree (often83`trunk`), has in-progress edits, and wants a clean feature worktree that84contains those same edits. **Literally copy the file edits across.**85861. **Identify the source worktree** — your current location, or the path the87 user names. Capture its absolute path:88 ```bash89 src="$(git rev-parse --show-toplevel)"90 ```912. **Pick the branch name** from what the user said (e.g. "login feature" →92 `feat/login`). Confirm the name if it's ambiguous.933. **Create the worktree** and capture its path:94 ```bash95 dst="$(marcgit work feat/login)"96 ```974. **Copy the edits** from `src` into `dst`. Cover both tracked and untracked:98 ```bash99 # tracked, staged + unstaged modifications → as a patch100 git -C "$src" diff HEAD > /tmp/mg-feature.patch101 if [ -s /tmp/mg-feature.patch ]; then102 git -C "$dst" apply /tmp/mg-feature.patch103 fi104 # untracked files → copy verbatim, preserving relative paths105 git -C "$src" ls-files --others --exclude-standard -z |106 while IFS= read -r -d '' f; do107 mkdir -p "$dst/$(dirname "$f")"108 cp -p "$src/$f" "$dst/$f"109 done110 ```111 If `git apply` fails (e.g. the worktree branched from a different base), fall112 back to a 3-way apply: `git -C "$dst" apply --3way /tmp/mg-feature.patch`, and113 if that still conflicts, tell the user which files conflicted instead of114 guessing.1155. **Verify** the copy landed and report the destination:116 ```bash117 git -C "$dst" status --short118 ```119 Tell the user the new worktree path (`$dst`) and what was copied.120121**Decide tracked-vs-everything by intent:** `git diff HEAD` carries every change122relative to the last commit (staged + unstaged). If the user only wants specific123files, scope the patch with pathspecs: `git -C "$src" diff HEAD -- path/a path/b`.124125**Do NOT remove the edits from the source** unless the user asks to *move* (not126copy) them. If they want a move, after verifying the copy you can127`git -C "$src" checkout -- .` / clean untracked — but confirm first, it's128destructive.129130When the user instead describes a feature in prose (no existing edits), just131create the worktree (step 3), `cd`/operate inside `$dst`, and implement the work132there fresh.133134## Other common requests135136- **"work on PR 42"** → `dst="$(marcgit pr 42)"`, then operate in `$dst`.137 Requires `gh` authenticated for the repo.138- **"go back to trunk"** → `marcgit trunk` (path on stdout).139- **"what worktrees exist"** → `marcgit list`.140- **"delete the login worktree"** → `marcgit remove feat/login` (force-removes141 worktree + branch; confirm with the user since it deletes the branch).142- **"clean up stale worktrees older than 30 days"** → `marcgit prune 30 -n`143 first, show the list, then `marcgit prune 30` with user sign-off.144- **"start a new project"** → `marcgit new <name>` or145 `marcgit clone <url> <name>`.146147## Gotchas148149- The shell wrapper only `cd`s when stdout is a single existing directory;150 multi-line output (like `list`) is printed, not `cd`'d. You read stdout151 directly anyway, so this only matters when you advise the user.152- Folder names are sanitized (`/` → `-`); when you need the folder for a branch,153 derive it the same way, or just trust the path marcgit prints.154- `prune` measures age by each worktree's last commit (`git log -1`) and refuses155 worktrees with uncommitted changes, untracked files, or stashes on their156 branch — so "nothing to prune" can mean "all candidates were dirty."