Last active
March 13, 2026 16:41
-
-
Save podratz/d25270ada86e090ae9051b0af25cbd4a to your computer and use it in GitHub Desktop.
Shell functions for managing git bare repos and worktrees with fzf. Clone repos as bare, create/switch/remove worktrees by branch name, with fuzzy selection and interactive prompts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # git-worktree.zsh — bare-repo + worktree workflow for zsh | |
| # | |
| # Layout: | |
| # ~/Code/repo/<name>.git — bare repos | |
| # ~/Code/work/<name>-<branch> — worktrees | |
| # | |
| # Requirements: git, fzf | |
| # | |
| # Commands: | |
| # rp [URL] — jump to bare repo, clone if URL given, fuzzy-select if not in a worktree | |
| # repos — fuzzy-select a bare repo | |
| # mkwt [branch] — create worktree (fuzzy from branchless if no args, auto-detects existing branches) | |
| # wt [branch] — jump to worktree (fuzzy if no args, create if missing) | |
| # wts — fuzzy-select across all worktrees | |
| # lswt — list worktrees for current repo | |
| # lswts — list all worktrees | |
| # rmwt [branch] — remove worktree (fuzzy if no args, current if in one) | |
| # | |
| # Install: source this file from .zshrc | |
| # source /path/to/git-worktree.zsh | |
| # | |
| # Note: avoid `local path` in zsh functions — it shadows the special | |
| # path variable tied to $PATH. | |
| # --- internals --- | |
| # Convert branch name to a folder-safe name: feat/name -> feat-name | |
| _branch_to_path() { | |
| printf '%s' "${1//\//-}" | |
| } | |
| # Infer repo base name from the current git dir: | |
| # /path/to/project.git -> project | |
| # /path/to/project/.git -> project | |
| _repo_base() { | |
| local gitdir repo | |
| gitdir="$(cd -- "$(git rev-parse --git-common-dir 2>/dev/null)" 2>/dev/null && pwd)" || { | |
| echo "Not inside a git repository." >&2 | |
| return 1 | |
| } | |
| repo="$(basename "$gitdir")" | |
| repo="${repo%.git}" | |
| printf '%s\n' "$repo" | |
| } | |
| # --- repos --- | |
| # Jump to bare repo from worktree, clone if URL given, fuzzy-select otherwise | |
| rp() { | |
| if [ $# -gt 0 ]; then | |
| local url="$1" name | |
| name="$(basename "$url" .git).git" | |
| local target="$HOME/Code/repo/$name" | |
| if [ -d "$target" ]; then | |
| echo "Already exists: $target" >&2 | |
| cd "$target" | |
| return | |
| fi | |
| git clone --bare "$url" "$target" && cd "$target" | |
| return | |
| fi | |
| if [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = "true" ]; then | |
| local gitdir | |
| gitdir="$(cd -- "$(git rev-parse --git-common-dir 2>/dev/null)" 2>/dev/null && pwd)" | |
| cd "$gitdir" | |
| else | |
| printf "Already in bare repo. Switch to another? [y/N] " | |
| read -r reply | |
| if [[ "$reply" =~ ^[Yy]$ ]]; then | |
| repos | |
| fi | |
| fi | |
| } | |
| # Fuzzy-jump to a bare repo under ~/Code/repo or ~/Code/*/repo | |
| repos() { | |
| local dir | |
| dir="$( | |
| { | |
| find "$HOME/Code/repo" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | |
| find "$HOME/Code" -mindepth 2 -maxdepth 2 -path "*/repo/*" -type d 2>/dev/null | |
| } | sort -u | fzf | |
| )" || return 1 | |
| cd "$dir" | |
| } | |
| # --- worktrees --- | |
| # Create a worktree (new or existing branch) | |
| # Usage: mkwt (fuzzy select from branches without worktrees) | |
| # mkwt feat name (creates branch feat/name, or checks out if it exists) | |
| # Result: ~/Code/work/project-feat-name on branch feat/name | |
| mkwt() { | |
| local branch repo dashed target | |
| repo="$(_repo_base)" || return 1 | |
| if [ $# -eq 0 ]; then | |
| local existing | |
| existing="$(git worktree list --porcelain | awk '/^branch refs\/heads\//{print substr($0,19)}')" | |
| branch="$(git for-each-ref --format='%(refname:short)' refs/heads/ | grep -vxF "$existing" | fzf)" || return 1 | |
| else | |
| branch="$(IFS=/; echo "$*")" | |
| fi | |
| dashed="$(_branch_to_path "$branch")" | |
| target="$HOME/Code/work/${repo}-${dashed}" | |
| if [ -d "$target" ]; then | |
| cd "$target" | |
| return | |
| fi | |
| if git show-ref --verify --quiet "refs/heads/$branch"; then | |
| git worktree add "$target" "$branch" && cd "$target" | |
| else | |
| git worktree add "$target" -b "$branch" && cd "$target" | |
| fi | |
| } | |
| # Jump to a worktree for the current repo | |
| # Usage: wt (fuzzy select) | |
| # wt main (open main worktree) | |
| # wt feat name (open feat/name worktree, offer to create if missing) | |
| wt() { | |
| local repo branch dashed target dir | |
| repo="$(_repo_base)" || return 1 | |
| if [ $# -eq 0 ]; then | |
| dir="$(find "$HOME/Code/work" -mindepth 1 -maxdepth 1 -type d -name "${repo}-*" 2>/dev/null | sort | fzf)" || return 1 | |
| cd "$dir" | |
| return | |
| fi | |
| branch="$(IFS=/; echo "$*")" | |
| dashed="$(_branch_to_path "$branch")" | |
| target="$HOME/Code/work/${repo}-${dashed}" | |
| if [ -d "$target" ]; then | |
| cd "$target" | |
| return | |
| fi | |
| echo "Worktree not found: $target" | |
| printf "Create worktree for branch '%s'? [y/N] " "$branch" | |
| read -r reply | |
| if [[ "$reply" =~ ^[Yy]$ ]]; then | |
| mkwt "$@" | |
| fi | |
| } | |
| # Fuzzy-jump to any worktree across all repos | |
| wts() { | |
| local dir | |
| dir="$(find "$HOME/Code/work" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort | fzf)" || return 1 | |
| cd "$dir" | |
| } | |
| # List worktrees for the current repo | |
| lswt() { | |
| local repo | |
| repo="$(_repo_base)" || return 1 | |
| git worktree list | grep "${repo}-" | |
| } | |
| # List all worktrees (excluding bare repos) | |
| lswts() { | |
| find "$HOME/Code/work" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort | |
| } | |
| # Remove a worktree (fuzzy select, with confirmation) | |
| # Usage: rmwt (fuzzy select, or current worktree if in one) | |
| # rmwt feat name (remove feat/name worktree directly) | |
| rmwt() { | |
| local selected repo dashed | |
| if [ $# -gt 0 ]; then | |
| repo="$(_repo_base)" || return 1 | |
| dashed="$(_branch_to_path "$(IFS=/; echo "$*")")" | |
| selected="$HOME/Code/work/${repo}-${dashed}" | |
| if [ ! -d "$selected" ]; then | |
| echo "Worktree not found: $selected" >&2 | |
| return 1 | |
| fi | |
| elif [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" = "true" ]; then | |
| selected="$(git rev-parse --show-toplevel 2>/dev/null)" | |
| else | |
| local worktrees | |
| worktrees="$(git worktree list --porcelain | awk '/^worktree /{print substr($0,10)}' | grep -v '\.git$')" | |
| if [ -z "$worktrees" ]; then | |
| echo "No worktrees found (are you inside a git repo?)." >&2 | |
| return 1 | |
| fi | |
| selected="$(echo "$worktrees" | fzf)" || return 1 | |
| fi | |
| printf "Remove worktree '%s'? [y/N] " "$selected" | |
| read -r reply | |
| if [[ "$reply" =~ ^[Yy]$ ]]; then | |
| local gitdir branch | |
| gitdir="$(cd -- "$(git rev-parse --git-common-dir 2>/dev/null)" 2>/dev/null && pwd)" | |
| branch="$(git -C "$selected" rev-parse --abbrev-ref HEAD 2>/dev/null)" | |
| git worktree remove "$selected" || return 1 | |
| [[ "$PWD" == "$selected"* ]] && cd "$gitdir" | |
| if [ -n "$branch" ] && [ "$branch" != "HEAD" ]; then | |
| printf "Also delete branch '%s'? [y/N] " "$branch" | |
| read -r reply | |
| if [[ "$reply" =~ ^[Yy]$ ]]; then | |
| git branch -d "$branch" | |
| fi | |
| fi | |
| fi | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment