Skip to content

Instantly share code, notes, and snippets.

@malcolmgreaves
Created February 20, 2026 21:10
Show Gist options
  • Select an option

  • Save malcolmgreaves/c83e018cb21399da7fca85dbbfba74f8 to your computer and use it in GitHub Desktop.

Select an option

Save malcolmgreaves/c83e018cb21399da7fca85dbbfba74f8 to your computer and use it in GitHub Desktop.
Managed git worktrees: `git-wt` for creating managed worktreees and `git-wt-done` for cleaning them up.
#!/usr/bin/env bash
# Creates a new managed git worktree: `git-wt BRANCH_NAME`
# Puts it under `$GIT_WORKTREE_SOURCES` as `<REPO_NAME>/<BRANCH_NAME>`.
# Creates a new branch if necessary or switches to one if it exists.
# Switches to the workspace directory at the end.
git-wt() {
### Get branch name
local BRANCH="${1}"
if [[ -z "${BRANCH}" ]] || [[ "${BRANCH}" == "--help" ]] || [[ "${BRANCH}" == "-h" ]]; then
echo "ERROR: Must supply branch name as first argument!" >&2
return 1
fi
### Where all worktrees for all repositories are stored.
local GIT_WORKTREE_SOURCES="${GIT_WORKTREE_SOURCES:-${HOME}/.local/state/git/worktrees}"
if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
echo "ERROR: Not in a git repository" >&2
return 1
fi
local REPO_NAME GIT_DIR_PATH
GIT_DIR_PATH="$(git rev-parse --git-dir)"
if [[ "${GIT_DIR_PATH}" == *.git/worktrees/* ]]; then
# Inside a worktree: --git-dir is <repo>/.git/worktrees/<name>
# Use --git-common-dir to get <repo>/.git, then go up one level.
REPO_NAME="$(basename "$(dirname "$(git rev-parse --git-common-dir)")")"
else
# Inside the main repo
REPO_NAME="$(basename "$(git rev-parse --show-toplevel)")"
fi
local WORKTREE_DIR="${GIT_WORKTREE_SOURCES}/${REPO_NAME}/${BRANCH}"
### Create the branch if it doesn't exist, handling checked-out case
if [[ -n "$(git branch --list "${BRANCH}")" ]]; then
# branch exists — check if it's currently checked out
local CURRENT_BRANCH
CURRENT_BRANCH="$(git branch --show-current)"
if [[ "${CURRENT_BRANCH}" == "${BRANCH}" ]]; then
# branch is checked out — error if dirty, otherwise switch to main
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "ERROR: Branch '${BRANCH}' is checked out with unstaged/staged changes. Commit or stash first." >&2
return 1
fi
git checkout main || { echo "ERROR: Failed to switch to main" >&2; return 1; }
fi
else
# branch doesn't exist — create it
git branch "${BRANCH}" || { echo "ERROR: Failed to create branch '${BRANCH}'" >&2; return 1; }
fi
### Create the worktree if one doesn't already exist for this branch
if git worktree list --porcelain | grep -q "^branch refs/heads/${BRANCH}$"; then
echo "Worktree for '${BRANCH}' already exists, skipping creation."
else
git worktree add "${WORKTREE_DIR}" "${BRANCH}" || { echo "ERROR: Failed to create worktree" >&2; return 1; }
fi
### Switch to the worktree directory
cd "${WORKTREE_DIR}" || { echo "ERROR: Failed to cd to '${WORKTREE_DIR}'" >&2; return 1; }
echo "Switched to worktree '${BRANCH}' in '${REPO_NAME}' (\$GIT_WORKTREE_SOURCES)"
### iTerm2: change the name of the active tab
if [[ -n "${ITERM_SESSION_ID}" ]]; then
echo -ne "\033]1;Worktree ${REPO_NAME} ${BRANCH}\007"
fi
}
# Delete the worktree: `git-wt-done BRANCH_NAME` or `git-wt-done` if in a worktree.
# Removes the worktree, keeping the branch around.
# Moves back to the base directory.
git-wt-done() {
### Get branch name
local BRANCH="${1}"
if [[ "${BRANCH}" == "--help" ]] || [[ "${BRANCH}" == "-h" ]]; then
echo "Usage: git-wt-done [<branch>]" >&2
echo " If <branch> is omitted, auto-detects from the current worktree." >&2
return 1
fi
if ! git rev-parse HEAD > /dev/null 2>&1; then
echo "ERROR: Not in a git repository" >&2
return 1
fi
if [[ -z "${BRANCH}" ]]; then
# Auto-detect branch from current worktree
if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
BRANCH="$(git branch --show-current)"
else
echo "ERROR: Must supply branch name as first argument (or run from within a worktree)!" >&2
return 1
fi
fi
### Where all worktrees for all repositories are stored.
local GIT_WORKTREE_SOURCES="${GIT_WORKTREE_SOURCES:-${HOME}/.local/state/git/worktrees}"
### Resolve the real repo name (works from main checkout or a worktree)
local REPO_NAME GIT_DIR_PATH REPO_ROOT
GIT_DIR_PATH="$(cd $(git rev-parse --git-common-dir) && pwd -P)"
REPO_ROOT="${GIT_DIR_PATH%/*}"
REPO_NAME="${REPO_ROOT##*/}"
local WORKTREE_DIR="${GIT_WORKTREE_SOURCES}/${REPO_NAME}/${BRANCH}"
### Verify the worktree exists
if ! git worktree list --porcelain | grep -q "^branch refs/heads/${BRANCH}$"; then
echo "ERROR: No worktree found for branch '${BRANCH}'" >&2
return 1
fi
### If we're currently inside the worktree being removed, move out first
if [[ "$(pwd)" == "${WORKTREE_DIR}"* ]]; then
cd "${REPO_ROOT}" || { echo "ERROR: Failed to cd to '${REPO_ROOT}'" >&2; return 1; }
fi
### Remove the worktree (deletes the directory and cleans up git internal tracking)
git worktree remove "${WORKTREE_DIR}" || { echo "ERROR: Failed to remove worktree at '${WORKTREE_DIR}'" >&2; return 1; }
### cd to the main repo root
cd "${REPO_ROOT}" || { echo "ERROR: Failed to cd to '${REPO_ROOT}'" >&2; return 1; }
echo "Worktree '${BRANCH}' deleted. Moved back to ${REPO_NAME} ($(pwd))"
### iTerm2: change the name of the active tab
if [[ -n "${ITERM_SESSION_ID}" ]]; then
echo -ne "\033]1;${REPO_NAME}\007"
fi
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment