Skip to content

Shell Setup

Helper functions I keep in .zshrc/.bashrc across devices.

Git Worktree Helpers

These functions wrap git worktree to automatically name worktrees using the pattern {repo-name}-{branch-name}.

By default, worktrees are created at {repo-root}/.worktrees/{repo-name}-{branch-name}. Pass --sibling to place the worktree as a sibling of the repo directory instead.

gwtadd

Create a worktree for a new branch.

# Usage: gwtadd [--sibling] <branch-name>
gwtadd() {
    local use_sibling=false
    if [[ "$1" == "--sibling" ]]; then
        use_sibling=true
        shift
    fi

    local branch_name="$1"

    if [ -z "$branch_name" ]; then
        echo "Usage: gwtadd [--sibling] <branch-name>"
        return 1
    fi

    local remote_url=$(git config --get remote.origin.url)
    local repo_name=$(basename "$remote_url" .git)
    local repo_root=$(git rev-parse --show-toplevel)

    local safe_branch_name=$(echo "$branch_name" | sed 's/[\/:]/-/g')

    local worktree_path
    if $use_sibling; then
        local worktrees_parent=$(dirname "$repo_root")
        worktree_path="${worktrees_parent}/${repo_name}-${safe_branch_name}"
    else
        worktree_path="${repo_root}/.worktrees/${repo_name}-${safe_branch_name}"
        mkdir -p "${repo_root}/.worktrees"
    fi

    git worktree add "$worktree_path" -b "$branch_name"

    echo "Worktree created at: $worktree_path"
}

gwtexisting

Create a worktree for an existing branch (local or remote).

# Usage: gwtexisting [--sibling] <existing-branch-name>
gwtexisting() {
    local use_sibling=false
    if [[ "$1" == "--sibling" ]]; then
        use_sibling=true
        shift
    fi

    local branch_name="$1"

    if [ -z "$branch_name" ]; then
        echo "Usage: gwtexisting [--sibling] <existing-branch-name>"
        return 1
    fi

    local remote_url=$(git config --get remote.origin.url)
    local repo_name=$(basename "$remote_url" .git)
    local repo_root=$(git rev-parse --show-toplevel)

    local safe_branch_name=$(echo "$branch_name" | sed 's/[\/:]/-/g')

    local worktree_path
    if $use_sibling; then
        local worktrees_parent=$(dirname "$repo_root")
        worktree_path="${worktrees_parent}/${repo_name}-${safe_branch_name}"
    else
        worktree_path="${repo_root}/.worktrees/${repo_name}-${safe_branch_name}"
        mkdir -p "${repo_root}/.worktrees"
    fi

    # Ensure a local tracking branch exists for the remote branch
    if ! git show-ref --verify --quiet "refs/heads/$branch_name"; then
        if git show-ref --verify --quiet "refs/remotes/origin/$branch_name"; then
            git branch --track "$branch_name" "origin/$branch_name"
        else
            echo "error: branch '$branch_name' not found locally or on origin"
            return 1
        fi
    fi

    git worktree add "$worktree_path" "$branch_name"

    echo "Worktree created at: $worktree_path"
}

gwtremove

Remove a worktree with optional force (-f) and branch deletion (-D).

If the worktree has uncommitted changes and -f wasn't passed, prompts before continuing. If -D wasn't passed, prompts whether to delete the branch after removal.

# Usage: gwtremove [--sibling] [-f] [-D] <worktree-name>
gwtremove() {
    local use_sibling=false
    if [[ "$1" == "--sibling" ]]; then
        use_sibling=true
        shift
    fi

    local force=false
    local delete_branch=false

    while getopts "fD" opt; do
        case $opt in
            f) force=true ;;
            D) delete_branch=true ;;
            *) echo "Usage: gwtremove [--sibling] [-f] [-D] <worktree-name>"; return 1 ;;
        esac
    done
    shift $((OPTIND - 1))
    OPTIND=1

    local worktree_name="$1"

    if [ -z "$worktree_name" ]; then
        echo "Usage: gwtremove [--sibling] [-f] [-D] <worktree-name>"
        return 1
    fi

    local repo_root=$(git rev-parse --show-toplevel)
    local remote_url=$(git config --get remote.origin.url)
    local repo_name=$(basename "$remote_url" .git)

    # Resolve worktree path
    local worktree_path
    if [[ "$worktree_name" = /* ]]; then
        worktree_path="$worktree_name"
    elif $use_sibling; then
        local worktrees_parent=$(dirname "$repo_root")
        worktree_path="${worktrees_parent}/${worktree_name}"
    else
        if [[ "$worktree_name" = ${repo_name}-* ]]; then
            worktree_path="${repo_root}/.worktrees/${worktree_name}"
        else
            worktree_path="${repo_root}/.worktrees/${repo_name}-${worktree_name}"
        fi
    fi

    if [ ! -d "$worktree_path" ]; then
        echo "Worktree not found: $worktree_path"
        return 1
    fi

    # Resolve the branch name from the worktree
    local branch_name
    branch_name=$(git worktree list --porcelain \
        | awk -v p="$worktree_path" '
            $1 == "worktree" && $2 == p { found=1; next }
            found && $1 == "branch" {
                gsub(/^refs\/heads\//, "", $2);
                print $2;
                exit
            }
        ')

    # Check if worktree has uncommitted changes
    local needs_force=false
    if ! $force; then
        local dirty
        dirty=$(git -C "$worktree_path" status --porcelain 2>/dev/null)
        if [ -n "$dirty" ]; then
            needs_force=true
        fi
    fi

    # Collect all user decisions upfront
    if $needs_force; then
        echo "Worktree has uncommitted changes:"
        git -C "$worktree_path" status --short
        echo -n "Force remove? (y/N): "
        read -r force_reply
        if [[ ! "$force_reply" =~ ^[Yy]$ ]]; then
            return 1
        fi
        force=true
    fi

    if [ -n "$branch_name" ] && ! $delete_branch; then
        echo -n "Delete branch '$branch_name'? (y/N): "
        read -r branch_reply
        if [[ "$branch_reply" =~ ^[Yy]$ ]]; then
            delete_branch=true
        fi
    fi

    # Execute: remove worktree
    if $force; then
        git worktree remove --force "$worktree_path" || return 1
    else
        git worktree remove "$worktree_path" || return 1
    fi

    echo "Worktree removed: $worktree_path"

    # Execute: delete branch
    if [ -n "$branch_name" ] && $delete_branch; then
        git branch -D "$branch_name"
        echo "Branch deleted: $branch_name"
    fi
}

Git Tag Helpers

Semantic versioning helpers that pair well with the worktree workflow.

latesttag

Print the latest semantic version tag in the repo.

latesttag() {
    git tag --sort=-v:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+' | head -n 1
}

bumptag

Calculate the next tag without creating it. Useful for scripting or previewing.

# Usage: bumptag <major|minor|patch>
bumptag() {
    local bump_type="$1"

    if [[ -z "$bump_type" ]]; then
        echo "Usage: bumptag <major|minor|patch>"
        return 1
    fi

    if [[ "$bump_type" != "major" && "$bump_type" != "minor" && "$bump_type" != "patch" ]]; then
        echo "Error: bump type must be 'major', 'minor', or 'patch'"
        return 1
    fi

    local latest_tag=$(latesttag)

    if [[ -z "$latest_tag" ]]; then
        echo "No semantic version tags found. Starting with v0.0.1"
        echo "v0.0.1"
        return 0
    fi

    # Remove 'v' prefix if present
    local version="${latest_tag#v}"

    # Split version into major.minor.patch
    local major=$(echo "$version" | cut -d. -f1)
    local minor=$(echo "$version" | cut -d. -f2)
    local patch=$(echo "$version" | cut -d. -f3)

    case "$bump_type" in
        major)
            major=$((major + 1))
            minor=0
            patch=0
            ;;
        minor)
            minor=$((minor + 1))
            patch=0
            ;;
        patch)
            patch=$((patch + 1))
            ;;
    esac

    local new_tag="v${major}.${minor}.${patch}"
    echo "$new_tag"
}

createtag

Create an annotated git tag at the next semantic version.

# Usage: createtag <major|minor|patch>
createtag() {
    local bump_type="$1"

    if [[ -z "$bump_type" ]]; then
        echo "Usage: createtag <major|minor|patch>"
        return 1
    fi

    local new_tag=$(bumptag "$bump_type")

    if [[ -z "$new_tag" ]]; then
        echo "Error: Failed to generate new tag"
        return 1
    fi

    git tag -a "$new_tag" -m "$new_tag"
    echo "Created annotated tag: $new_tag"
}