From ca8f9efe05edda0e649e914ae2ad02fa0324bded Mon Sep 17 00:00:00 2001 From: h7x4 Date: Sun, 18 Jan 2026 03:00:44 +0900 Subject: [PATCH] home/git/switch-interactive: add colors, list tags --- home/programs/git/default.nix | 1 + .../git/scripts/git-switch-interactive.sh | 136 ++++++++++++++++-- 2 files changed, 129 insertions(+), 8 deletions(-) diff --git a/home/programs/git/default.nix b/home/programs/git/default.nix index 2c885c1..437ed1e 100644 --- a/home/programs/git/default.nix +++ b/home/programs/git/default.nix @@ -350,6 +350,7 @@ lib.mkIf cfg.enable { text = lib.fileContents ./scripts/git-switch-interactive.sh; excludeShellChecks = [ "SC2001" # (style): See if you can use ${variable//search/replace} instead. (sed invocation) + "SC2155" ]; }) (pkgs.writeShellApplication { diff --git a/home/programs/git/scripts/git-switch-interactive.sh b/home/programs/git/scripts/git-switch-interactive.sh index 3fb9204..a85941d 100644 --- a/home/programs/git/scripts/git-switch-interactive.sh +++ b/home/programs/git/scripts/git-switch-interactive.sh @@ -1,16 +1,136 @@ set -euo pipefail -if [ -n "${1:-}" ]; then +if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ] || [ $# -ge 2 ]; then + declare -r ARGV0=$(basename "${0:-git-switch-interactive.sh}") + printf 'Usage: %s [BRANCH]\n' "$ARGV0" >&2 + cat <<'EOF' >&2 +Interactively choose a local branch, remote branch, or tag to switch to. + +Options: + -h, --help Show this help and exit + BRANCH If a single branch/tag/ref is provided, the script will + run git switch BRANCH and exit. +EOF + exit 0 +fi + +if [ $# -eq 1 ]; then git switch "$1" exit 0 fi -BRANCHES=$(cat <(git branch) <(git branch --remotes) | grep --invert-match '^\*\|HEAD ->' | sed 's|^\s*||') -CHOSEN_BRANCH=$(sk --reverse --info=inline --preview 'git show --color {}' <<<"$BRANCHES") +declare -r BRANCH_COLOR=$'\033[32m' +declare -r REMOTE_COLOR=$'\033[33m' +declare -r ORIGIN_COLOR=$'\033[31m' +declare -r TAG_COLOR=$'\033[35m' +declare -r RESET_COLOR=$'\033[0m' -CLEAN_BRANCH_NAME=$(sed 's|^\s*||' <<<"$CHOSEN_BRANCH") -for REMOTE in $(git remote); do - CLEAN_BRANCH_NAME=$(sed "s|^${REMOTE}/||" <<<"$CLEAN_BRANCH_NAME") -done +if [ -n "${NO_COLOR:-}" ]; then + declare -r BRANCH_COLOR='' + declare -r REMOTE_COLOR='' + declare -r ORIGIN_COLOR='' + declare -r TAG_COLOR='' + declare -r RESET_COLOR='' +fi -git switch "${CLEAN_BRANCH_NAME}" +declare -r STRIP_ANSI_SED='s/\x1B\[[0-9;]*[a-zA-Z]//g' +declare -r TRIM_SPACE_SED='s/^[[:space:]]+//; s/[[:space:]]+$//' +declare -r STRIP_SURROUNDING_QUOTES_SED="s/^[[:space:]\"'[]+//; s/[[:space:]\"'\\]]+\$//; s/^\"\\[+//; s/^\\[+//; s/\\]+$//" + +sanitize_ref_from_git() { + sed -E \ + -e "$STRIP_ANSI_SED" \ + -e "$TRIM_SPACE_SED" \ + -e "$STRIP_SURROUNDING_QUOTES_SED" <<<"$1" +} + +declare -a VISIBLE_ITEMS=() + +while IFS= read -r ref; do + [ -z "$ref" ] && continue + VISIBLE_ITEMS+=("${BRANCH_COLOR}(branch)${RESET_COLOR} $(sanitize_ref_from_git "$ref")") +done < <(git for-each-ref --sort=-committerdate --format='%(refname:short)' refs/heads 2>/dev/null) + +while IFS= read -r ref; do + [ -z "$ref" ] && continue + case "$ref" in + */*) + remote_part="$(sanitize_ref_from_git "${ref%%/*}")" + branch_part="$(sanitize_ref_from_git "${ref#*/}")" + if [ -z "$branch_part" ] || [ "$branch_part" = "HEAD" ]; then + continue + fi + VISIBLE_ITEMS+=("${REMOTE_COLOR}(remote)${RESET_COLOR} ${ORIGIN_COLOR}${remote_part}${RESET_COLOR}/${branch_part}") + ;; + *) + continue + ;; + esac +done < <(git for-each-ref --format='%(refname:short)' refs/remotes 2>/dev/null) + +while IFS= read -r ref; do + [ -z "$ref" ] && continue + VISIBLE_ITEMS+=("${TAG_COLOR}(tag)${RESET_COLOR} $(sanitize_ref_from_git "$ref")") +done < <(git for-each-ref --format='%(refname:short)' refs/tags 2>/dev/null) + +if [ ${#VISIBLE_ITEMS[@]} -eq 0 ]; then + echo "No branches or tags found." >&2 + exit 1 +fi + +declare -r SKIM_PREVIEW_COMMAND_RAW=$(cat <<'EOF' +raw=$(printf "%s" {} | sed -r 's/\x1B\[[0-9;]*[A-Za-z]//g') +ref=$(printf "%s" "$raw" | sed -n -E 's/^[[:space:]]*\([^)]*\)[[:space:]]*(.*)$/\1/p') +git show --color "$ref" 2>/dev/null || git log -n 50 --color "$ref" +EOF +) + +declare -r SKIM_PREVIEW_COMMAND_=${SKIM_PREVIEW_COMMAND_RAW//$'\n'/'; '} +declare -r SKIM_PREVIEW_COMMAND=${SKIM_PREVIEW_COMMAND_%'; '} + +CHOSEN=$(printf '%s\n' "${VISIBLE_ITEMS[@]}" | sk --ansi --reverse --info=inline --preview "$SKIM_PREVIEW_COMMAND") + +[ -z "${CHOSEN:-}" ] && exit 0 + +declare -r CLEAN=$(sed -E \ + -e "$STRIP_ANSI_SED" \ + -e "$TRIM_SPACE_SED" \ + <<<"$CHOSEN" +) + +declare -r PARSED=$(sed -n -E "s/^[[:space:]]*\(([^)]*)\)[[:space:]]*(.*)$/\1|\2/p" <<<"$CLEAN") || true +if [ -z "$PARSED" ]; then + echo "Failed to parse selection: $CLEAN" >&2 + exit 2 +fi + +declare -r TYPE=${PARSED%%|*} +declare -r RAW_REF=${PARSED#*|} +declare -r REF=$(sanitize_ref_from_git "$RAW_REF") + +case "$TYPE" in + tag) + git switch --detach "$REF" + ;; + remote) + SWITCH_NAME="$REF" + for R in $(git remote 2>/dev/null); do + if [ "${REF#"${R}"/}" != "$REF" ]; then + CAND=${REF#"${R}"/} + if git show-ref --verify --quiet "refs/heads/$CAND"; then + SWITCH_NAME="$CAND" + break + fi + fi + done + git switch "$SWITCH_NAME" + ;; + branch) + git switch "$REF" + ;; + *) + git switch "$REF" + ;; +esac + +exit 0