diff --git a/home/netty-worktree.nix b/home/netty-worktree.nix index 2b97894..e7890e9 100644 --- a/home/netty-worktree.nix +++ b/home/netty-worktree.nix @@ -9,6 +9,26 @@ in { home.packages = builtins.attrValues customScripts.nettyPackages; programs.zsh.initContent = lib.mkAfter '' + wt() { + if [[ "''${1:-}" == remove ]]; then + local current_worktree_root common_git_dir main_repo_root + + current_worktree_root=$(git rev-parse --show-toplevel 2>/dev/null) || { + command wt "$@" + return + } + + common_git_dir=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || return + main_repo_root=$(cd "''${common_git_dir}/.." && pwd -P) || return + + command wt "$@" || return + cd -- "$main_repo_root" || return + return + fi + + command wt "$@" + } + wtc() { if [[ $# -ne 1 ]]; then printf 'usage: wtc \n' >&2 @@ -16,7 +36,7 @@ in { fi local worktree_path - worktree_path=$(command wt-create "$1") || return + worktree_path=$(wt create "$1") || return cd -- "$worktree_path" || return } ''; diff --git a/scripts/default.nix b/scripts/default.nix index a1fcb55..5cd7a12 100644 --- a/scripts/default.nix +++ b/scripts/default.nix @@ -112,16 +112,15 @@ }; nettyPackages = { - wt-create = mkScript { - name = "wt-create"; - file = ./wt-create.sh; + wt = mkScript { + name = "wt"; + file = ./wt.sh; runtimeInputs = with pkgs; [coreutils git gnused]; }; - wt-path = mkScript { - name = "wt-path"; - file = ./wt-path.sh; - runtimeInputs = with pkgs; [coreutils git gnused]; + wt-create = mkScript { + name = "wt-create"; + file = ./wt-create.sh; }; }; in { diff --git a/scripts/wt-create.sh b/scripts/wt-create.sh index 278a91f..8e6c7d5 100644 --- a/scripts/wt-create.sh +++ b/scripts/wt-create.sh @@ -2,28 +2,4 @@ set -euo pipefail -if [[ $# -ne 1 ]]; then - printf 'usage: wt-create \n' >&2 - exit 1 -fi - -branch_name=$1 -repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || { - printf 'wt-create: not inside a git repository\n' >&2 - exit 1 -} - -target_path=$(wt-path "$branch_name") - -if [[ -e "$target_path" ]]; then - printf 'wt-create: path already exists: %s\n' "$target_path" >&2 - exit 1 -fi - -if git -C "$repo_root" show-ref --verify --quiet "refs/heads/$branch_name"; then - git -C "$repo_root" worktree add -- "$target_path" "$branch_name" 1>&2 -else - git -C "$repo_root" worktree add -b "$branch_name" -- "$target_path" HEAD 1>&2 -fi - -printf '%s\n' "$target_path" +exec wt create "$@" diff --git a/scripts/wt-path.sh b/scripts/wt-path.sh deleted file mode 100644 index 9d8abb9..0000000 --- a/scripts/wt-path.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -if [[ $# -ne 1 ]]; then - printf 'usage: wt-path \n' >&2 - exit 1 -fi - -common_git_dir=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || { - printf 'wt-path: not inside a git repository\n' >&2 - exit 1 -} - -repo_root=$(cd "${common_git_dir}/.." && pwd -P) || { - printf 'wt-path: failed to resolve repository root\n' >&2 - exit 1 -} - -worktree_name=$1 -clean_name=$(printf '%s' "$worktree_name" | sed -E 's#[^[:alnum:]._-]+#-#g; s#-+#-#g; s#(^[.-]+|[.-]+$)##g') - -if [[ -z "$clean_name" ]]; then - printf 'wt-path: %s does not produce a usable path name\n' "$worktree_name" >&2 - exit 1 -fi - -repo_parent=$(dirname "$repo_root") -repo_name=$(basename "$repo_root") - -printf '%s/%s-%s\n' "$repo_parent" "$repo_name" "$clean_name" diff --git a/scripts/wt.sh b/scripts/wt.sh new file mode 100644 index 0000000..59dc436 --- /dev/null +++ b/scripts/wt.sh @@ -0,0 +1,190 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat >&2 <<'EOF' +usage: + wt create + wt remove + wt prune +EOF + exit 1 +} + +current_worktree_root= +main_repo_root= + +resolve_repo_context() { + local common_git_dir + + current_worktree_root=$(git rev-parse --show-toplevel 2>/dev/null) || { + printf 'wt: not inside a git repository\n' >&2 + exit 1 + } + + common_git_dir=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || { + printf 'wt: failed to resolve common git directory\n' >&2 + exit 1 + } + + main_repo_root=$(cd "${common_git_dir}/.." && pwd -P) || { + printf 'wt: failed to resolve repository root\n' >&2 + exit 1 + } +} + +sanitize_name() { + local clean_name + + clean_name=$(printf '%s' "$1" | sed -E 's#[^[:alnum:]._-]+#-#g; s#-+#-#g; s#(^[.-]+|[.-]+$)##g') + + if [[ -z "$clean_name" ]]; then + printf 'wt: %s does not produce a usable path name\n' "$1" >&2 + exit 1 + fi + + printf '%s\n' "$clean_name" +} + +target_path_for() { + local clean_name repo_name repo_parent + + clean_name=$(sanitize_name "$1") + repo_parent=$(dirname "$main_repo_root") + repo_name=$(basename "$main_repo_root") + + printf '%s/%s-%s\n' "$repo_parent" "$repo_name" "$clean_name" +} + +resolve_main_ref() { + if git -C "$main_repo_root" show-ref --verify --quiet refs/heads/main; then + printf 'main\n' + return + fi + + if git -C "$main_repo_root" symbolic-ref -q --short refs/remotes/origin/HEAD >/dev/null; then + git -C "$main_repo_root" symbolic-ref -q --short refs/remotes/origin/HEAD | sed 's#^origin/##' + return + fi + + if git -C "$main_repo_root" symbolic-ref -q --short HEAD >/dev/null; then + git -C "$main_repo_root" symbolic-ref -q --short HEAD + return + fi + + printf 'wt prune: could not resolve the primary branch\n' >&2 + exit 1 +} + +worktree_is_clean() { + [[ -z "$(git -C "$1" status --porcelain --untracked-files=normal 2>/dev/null)" ]] +} + +create_worktree() { + local branch_name target_path + + [[ $# -eq 1 ]] || usage + + branch_name=$1 + target_path=$(target_path_for "$branch_name") + + if [[ -e "$target_path" ]]; then + printf 'wt create: path already exists: %s\n' "$target_path" >&2 + exit 1 + fi + + if git -C "$current_worktree_root" show-ref --verify --quiet "refs/heads/$branch_name"; then + git -C "$current_worktree_root" worktree add -- "$target_path" "$branch_name" 1>&2 + else + git -C "$current_worktree_root" worktree add -b "$branch_name" -- "$target_path" HEAD 1>&2 + fi + + printf '%s\n' "$target_path" +} + +remove_current_worktree() { + [[ $# -eq 0 ]] || usage + + if [[ "$current_worktree_root" == "$main_repo_root" ]]; then + printf 'wt remove: not inside a linked worktree\n' >&2 + exit 1 + fi + + git -C "$main_repo_root" worktree remove "$current_worktree_root" 1>&2 + printf '%s\n' "$current_worktree_root" +} + +prune_worktree() { + local path=$1 + local main_commit=$2 + local current_commit + + if [[ "$path" == "$main_repo_root" || "$path" == "$current_worktree_root" ]]; then + return 1 + fi + + if ! worktree_is_clean "$path"; then + return 1 + fi + + current_commit=$(git -C "$path" rev-parse HEAD 2>/dev/null) || return 1 + + if [[ "$current_commit" != "$main_commit" ]]; then + return 1 + fi + + git -C "$main_repo_root" worktree remove "$path" 1>&2 + printf '%s\n' "$path" + return 0 +} + +prune_worktrees() { + local line main_commit main_ref path removed_any=0 + + [[ $# -eq 0 ]] || usage + + main_ref=$(resolve_main_ref) + main_commit=$(git -C "$main_repo_root" rev-parse "$main_ref") + path= + + while IFS= read -r line; do + case "$line" in + worktree\ *) + path=${line#worktree } + ;; + "") + if [[ -n "$path" ]] && prune_worktree "$path" "$main_commit"; then + removed_any=1 + fi + path= + ;; + esac + done < <(git -C "$main_repo_root" worktree list --porcelain && printf '\n') + + git -C "$main_repo_root" worktree prune 1>&2 + + if [[ $removed_any -eq 0 ]]; then + printf 'wt prune: no removable worktrees found\n' >&2 + fi +} + +resolve_repo_context + +case "${1:-}" in + create) + shift + create_worktree "$@" + ;; + remove) + shift + remove_current_worktree "$@" + ;; + prune) + shift + prune_worktrees "$@" + ;; + *) + usage + ;; +esac