#!/usr/bin/env bash
#
# git-worktree-init — set up a bare-repo worktree layout from a clone URL.
#
#   git-worktree-init <repo-url> [directory]
#
# Creates:
#
#   <directory>/
#   ├── .bare/        the repository: objects, refs, config, hooks
#   ├── .git          a file containing "gitdir: ./.bare"
#   └── <branch>/     a worktree for the remote's default branch
#
# Why not `git clone --bare`: a bare clone copies the remote's heads into
# local refs/heads, so the branches exist with no upstream and `git pull`
# fails inside the worktree. `git init --bare` plus an explicit remote never
# creates them, so the first `git worktree add` tracks origin properly.
#
# More detail: https://vzav.eu/posts/tools/git-worktree/

set -euo pipefail

readonly PROGRAM=${0##*/}

die()  { printf '%s: %s\n' "$PROGRAM" "$*" >&2; exit 1; }
info() { printf '\033[2m==>\033[0m %s\n' "$*"; }
ok()   { printf '\033[32m==>\033[0m %s\n' "$*"; }

usage() {
    cat <<EOF
$PROGRAM — set up a bare-repo worktree layout from a clone URL.

Usage:
  $PROGRAM <repo-url> [directory]

Arguments:
  repo-url    Any URL git can clone: git@host:user/repo.git,
              https://host/user/repo.git, ssh://git@host/user/repo
  directory   Where to create it. Defaults to the repository name
              taken from the URL.

Examples:
  $PROGRAM git@github.com:torvalds/linux.git
  $PROGRAM https://github.com/cli/cli.git ~/code/gh-cli

Afterwards:
  cd <directory>
  git worktree add -b feature/x feature-x origin/main   # new branch
  git worktree add review origin/some-branch            # existing branch
  git worktree list
EOF
}

# Derive a directory name from a clone URL. Handles scp-style (git@h:u/r.git),
# https, ssh:// and trailing slashes.
repo_name_from_url() {
    local url=$1
    url=${url%/}          # drop a trailing slash
    url=${url##*/}        # basename, covers https:// and ssh://
    url=${url##*:}        # scp-style with no path, e.g. git@host:repo.git
    url=${url%.git}       # drop the .git suffix
    printf '%s' "$url"
}

main() {
    case ${1-} in
        -h|--help|'') usage; [ -n "${1-}" ] && exit 0 || exit 2 ;;
    esac

    command -v git >/dev/null 2>&1 || die "git is not installed"

    local url=$1
    local dir=${2-}
    [ -n "$dir" ] || dir=$(repo_name_from_url "$url")
    [ -n "$dir" ] || die "could not work out a directory name from '$url' — pass one explicitly"

    # If we create the directory and then fail, don't leave a half-built one.
    local created=0 abs_dir=""
    cleanup() {
        local code=$?
        if [ "$code" -ne 0 ] && [ "$created" -eq 1 ] && [ -n "$abs_dir" ]; then
            info "failed — removing '$abs_dir'"
            cd / && rm -rf -- "$abs_dir"
        fi
        exit "$code"
    }
    trap cleanup EXIT

    # Refuse to touch anything that already has content in it.
    if [ -e "$dir" ]; then
        [ -d "$dir" ] || die "'$dir' exists and is not a directory"
        if [ -n "$(ls -A -- "$dir" 2>/dev/null)" ]; then
            die "'$dir' already exists and is not empty"
        fi
    fi

    if [ ! -d "$dir" ]; then
        mkdir -p -- "$dir"
        created=1
    fi

    # Resolve to an absolute path before entering it: the cleanup below runs
    # from inside this directory, where the relative name no longer resolves.
    local abs_dir
    abs_dir=$(cd -- "$dir" && pwd)
    cd -- "$abs_dir"

    info "creating bare repository in .bare"
    git init --quiet --bare .bare

    # A .git *file* with a gitdir: pointer, so git commands work from the root
    # of the layout and not only from inside a worktree.
    printf 'gitdir: ./.bare\n' > .git

    info "configuring origin"
    git remote add origin "$url"
    # A bare repo has no refspec mapping the remote's branches into
    # refs/remotes/origin/*, so without this you cannot see what exists.
    git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'
    # Off by default. Without it, `git worktree add <name>` for a branch that
    # only exists on the remote creates an orphan branch with no history.
    git config worktree.guessRemote true

    info "fetching"
    git fetch --quiet origin || die "could not fetch from '$url'"

    # Work out the remote's default branch rather than assuming main.
    local default=""
    git remote set-head origin --auto >/dev/null 2>&1 || true
    if default=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null); then
        default=${default#origin/}
    fi
    if [ -z "$default" ]; then
        for candidate in main master trunk; do
            if git show-ref --quiet --verify "refs/remotes/origin/$candidate"; then
                default=$candidate
                break
            fi
        done
    fi
    [ -n "$default" ] || die "fetched, but could not determine the default branch — add a worktree by hand"

    info "adding worktree for '$default'"
    # Branch names containing a slash would be read as a directory path and the
    # branch would be named after the basename, so be explicit about both.
    git worktree add --quiet --track -b "$default" "${default//\//-}" "origin/$default"

    trap - EXIT
    ok "ready — $PWD"
    printf '\n'
    git worktree list
    printf '\n  cd %s/%s\n\n' "$dir" "${default//\//-}"
}

main "$@"
