Wednesday, March 6, 2024

personal_code/homedir/.local/bin/worktree at daab9eb1da9f777df57c742e5629247a94b54947 · llimllib/personal_code · GitHub

#!/usr/bin/env bash
# LICENSE: unlicense. This is free and unencumbered software released into the public domain.
# see unlicense.org for full license
#
# source this command if you want to have it change directories for you

RED="\033[0;31m"
GREEN="\033[0;32m"
YELLOW="\033[0;33m"
CLEAR="\033[0m"
VERBOSE=

function usage {
    cat <<EOF
worktree [-v] <branch name>

create a git worktree with <branch name>. Will create a worktree if one isn't
found that matches the given name.

Will copy over any .env, .envrc, or .tool-versions files to the new worktree
EOF
    kill -INT $$
}

function die {
    printf '%b%s%b\n' "$RED" "$1" "$CLEAR"
    # exit the script, but if it was sourced, don't kill the shell
    kill -INT $$
}

function warn {
    printf '%b%s%b\n' "$YELLOW" "$1" "$CLEAR"
}

# If at all possible, use copy-on-write to copy files. This is especially
# important to allow us to copy node_modules directories efficiently
#
# On mac or bsd: try to use -c
# see:
# https://spin.atomicobject.com/2021/02/23/git-worktrees-untracked-files/
#
# On gnu: use --reflink
#
# Use /bin/cp directly to avoid any of the user's aliases - this script is
# often eval'ed
#
# I tried to figure out how to actually determine the filesystem support for
# copy-on-write, but did not find any good references, so I'm falling back on
# "try and see if it fails"
function cp_cow {
    if ! /bin/cp -Rc "$1" "$2"; then
        if ! /bin/cp -R --reflink "$1" "$2"; then
            if ! /bin/cp -R "$1" "$2"; then
                warn "Unable to copy file $1 to $2 - folder may not exist"
            fi
        fi
    fi
}


# Create a worktree from a given branchname, in exactly the way I like it.
function _worktree {
    if [ -z "$1" ]; then
        usage
    fi

    if [ -n "$VERBOSE" ]; then
        set -x
    fi
    branchname="$1"

    # Replace slashes with underscores. If there's no slash, dirname will equal
    # branchname. So "alu/something-other" becomes "alu_something-other", but
    # "quick-fix" stays unchanged
    # https://www.tldp.org/LDP/abs/html/parameter-substitution.html
    dirname=${branchname//\//_}

    # pull the most recent version of the remote
    if ! git pull; then
        warn "Unable to run git pull, there may not be an upstream"
    fi

    # if the branch name already exists, we want to check it out. Otherwise,
    # create a new branch. I'm sure there's probably a way to do that in one
    # command, but I'm done fiddling with git at this point
    #
    # As far as I can tell, we have to check locally and remotely separately if
    # we want to be accurate. See https://stackoverflow.com/a/75040377 for the
    # reasoning here. Also this has some caveats, but probably works well
    # enough :shrug:
    #
    # if the branch exists locally:
    if git for-each-ref --format='%(refname:lstrip=2)' refs/heads | grep -E "^$branchname$" > /dev/null 2>&1; then
        if ! git worktree add "../$dirname" "$branchname"; then
            die "failed to create git worktree $branchname"
        fi
    # if the branch exists on a remote:
    elif git for-each-ref --format='%(refname:lstrip=3)' refs/remotes/origin | grep -E "^$branchname$" > /dev/null 2>&1; then
        if ! git worktree add "../$dirname" "$branchname"; then
            die "failed to create git worktree $branchname"
        fi
    else
        # otherwise, create a new branch
        if ! git worktree add -b "$branchname" "../$dirname"; then
            die "failed to create git worktree $branchname"
        fi
    fi

    # Find untracked files that we want to copy to the new worktree

    # packages in node_modules packages can have sub-node-modules packages, and
    # we don't want to copy them; only copy the root node_modules directory
    if [ -d "node_modules" ]; then
      cp_cow node_modules ../"$dirname"/node_modules
    fi

    # this will fail for any files with \n in their names. don't do that.
    IFS=$'\n'

    # (XXX: should I add some mechanism for users to spcify this list? perhaps
    # ~/.config/worktree/untracked or something?)
    #
    # this is the best of a bunch of bad options for reading the files into an
    # array. We're often executing in bash or zsh, so we're going to let them
    # use their file splitting rules, with an explicit IFS. We can't use find's
    # exec because we want to use cp_cow to copy files copy-on-write when
    # possible.
    #
    # Skip any of these files if they're found within node_modules.
    #
    # Putting the `-not -path` argument first is a great deal faster than the
    # other way around
    #
    # shellcheck disable=SC2207
    files_to_copy=( $(find -E . -not -path '*node_modules*' -and \
        -iregex '.*\/\.(envrc|env|env.local|tool-versions|mise.toml)' ) )
    for f in "${files_to_copy[@]}"; do
      cp_cow "$f" ../"$dirname"/"$f"
    done

    # return the shell to normal splitting mode
    unset IFS

    # if there was an envrc file, tell direnv that it's ok to run it
    if [ -f "../$dirname/.envrc" ]; then
        direnv allow "../$dirname"
    fi

    # now change to the new tree and enable the root envrc if present
    cd "../$dirname" || return
    printf "%bcreated worktree %s%b\n" "$GREEN" "../$dirname" "$CLEAR"
}

while true; do
    case $1 in
        help | -h | --help)
            usage
            ;;
        -v | --verbose)
            VERBOSE=true
            shift
            ;;
        *)
            break
            ;;
    esac
done

_worktree "$@"

<https://github.com/llimllib/personal_code/blob/daab9eb1/homedir/.local/bin/worktree#L35-L58>
sincerely,