Identities live in ~/.git-identities (alias|Name|email). Interactive menu to select, add, or remove identities; direct mode via alias argument; --global/-g scope flag (defaults to local inside a repo, global fallback outside); --list/--current helpers. Full --dryrun support and a 27-assertion test suite driven through dryrun. GIT_IDENTITIES_FILE env override for testability.
270 lines
7.9 KiB
Bash
Executable File
270 lines
7.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -uo pipefail
|
|
|
|
# git-identity — switch git author identity from a saved menu.
|
|
# Spec: specs/git-identity.spec.md
|
|
|
|
IDENTITIES_FILE="${GIT_IDENTITIES_FILE:-$HOME/.git-identities}"
|
|
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[0;33m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m'
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: git-identity [OPTIONS] [ALIAS]
|
|
|
|
Switch git user.name/user.email from identities saved in ~/.git-identities.
|
|
Run with no arguments for an interactive menu (select, add, or remove).
|
|
|
|
Arguments:
|
|
ALIAS Apply this saved identity directly
|
|
|
|
Options:
|
|
-g, --global Apply to global git config (default: local when in a repo)
|
|
-l, --list List saved identities
|
|
-c, --current Show the identity in effect here
|
|
-n, --dryrun Preview actions without changing anything
|
|
-h, --help Show this help
|
|
|
|
File format (~/.git-identities): alias|Name|email, one per line, # comments.
|
|
EOF
|
|
}
|
|
|
|
dryrun=false
|
|
scope_global=false
|
|
action=""
|
|
alias_arg=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-h|--help) usage; exit 0 ;;
|
|
-n|--dryrun) dryrun=true; shift ;;
|
|
-g|--global) scope_global=true; shift ;;
|
|
-l|--list) action="list"; shift ;;
|
|
-c|--current) action="current"; shift ;;
|
|
-*) echo "Error: Unknown option: $1" >&2; exit 1 ;;
|
|
*)
|
|
if [[ -n "$alias_arg" ]]; then
|
|
echo "Error: Too many arguments" >&2; exit 1
|
|
fi
|
|
alias_arg="$1"; shift ;;
|
|
esac
|
|
done
|
|
|
|
in_repo() { git rev-parse --is-inside-work-tree >/dev/null 2>&1; }
|
|
|
|
scope_desc() {
|
|
if $scope_global; then
|
|
echo "--global"
|
|
else
|
|
echo "--local in $(git rev-parse --show-toplevel)"
|
|
fi
|
|
}
|
|
|
|
# Load identities into parallel arrays.
|
|
aliases=() names=() emails=()
|
|
load_identities() {
|
|
aliases=() names=() emails=()
|
|
[[ -f "$IDENTITIES_FILE" ]] || return 0
|
|
while IFS='|' read -r a n e; do
|
|
[[ -z "$a" || "$a" == \#* ]] && continue
|
|
[[ -z "$n" || -z "$e" ]] && continue
|
|
aliases+=("$a"); names+=("$n"); emails+=("$e")
|
|
done < "$IDENTITIES_FILE"
|
|
}
|
|
|
|
show_current() {
|
|
local scope="global" name email
|
|
if in_repo && ! $scope_global; then scope="local"; fi
|
|
name=$(git config user.name 2>/dev/null || true)
|
|
email=$(git config user.email 2>/dev/null || true)
|
|
echo -e "Current identity (${scope}): ${CYAN}${name:-<unset>} <${email:-unset}>${NC}"
|
|
}
|
|
|
|
apply_identity() {
|
|
local name="$1" email="$2"
|
|
if ! $scope_global && ! in_repo; then
|
|
echo -e "${YELLOW}Note: not inside a git repository — applying globally${NC}"
|
|
scope_global=true
|
|
fi
|
|
local desc; desc=$(scope_desc)
|
|
if $dryrun; then
|
|
echo "[dryrun] Would set user.name '$name' and user.email '$email' ($desc)"
|
|
return 0
|
|
fi
|
|
local flag="--local"
|
|
$scope_global && flag="--global"
|
|
git config "$flag" user.name "$name" && git config "$flag" user.email "$email" || {
|
|
echo "Error: git config failed" >&2; exit 1
|
|
}
|
|
echo -e "${GREEN}Set user.name '$name' and user.email '$email' ($desc)${NC}"
|
|
}
|
|
|
|
apply_alias() {
|
|
local wanted="$1" i
|
|
for i in "${!aliases[@]}"; do
|
|
if [[ "${aliases[$i]}" == "$wanted" ]]; then
|
|
apply_identity "${names[$i]}" "${emails[$i]}"
|
|
return 0
|
|
fi
|
|
done
|
|
echo "Error: No identity '$wanted' in $IDENTITIES_FILE" >&2
|
|
if [[ ${#aliases[@]} -gt 0 ]]; then
|
|
echo "Available: ${aliases[*]}" >&2
|
|
fi
|
|
exit 1
|
|
}
|
|
|
|
list_identities() {
|
|
local i
|
|
for i in "${!aliases[@]}"; do
|
|
printf '%-14s %s <%s>\n' "${aliases[$i]}" "${names[$i]}" "${emails[$i]}"
|
|
done
|
|
}
|
|
|
|
ensure_file() {
|
|
[[ -f "$IDENTITIES_FILE" ]] && return 0
|
|
local gname gemail
|
|
gname=$(git config --global user.name 2>/dev/null || true)
|
|
gemail=$(git config --global user.email 2>/dev/null || true)
|
|
echo "No identities file at $IDENTITIES_FILE."
|
|
local seed=""
|
|
if [[ -n "$gname" && -n "$gemail" ]]; then
|
|
seed="default|$gname|$gemail"
|
|
echo "Create it seeded with your global identity ($gname <$gemail>)? [Y/n]"
|
|
else
|
|
echo "Create an empty one? [Y/n]"
|
|
fi
|
|
read -r reply
|
|
[[ "$reply" =~ ^[Nn] ]] && exit 0
|
|
if $dryrun; then
|
|
echo "[dryrun] Would create $IDENTITIES_FILE${seed:+ seeded with '$seed'}"
|
|
return 0
|
|
fi
|
|
{ echo "# alias|Name|email"; [[ -n "$seed" ]] && echo "$seed"; } > "$IDENTITIES_FILE"
|
|
echo -e "${GREEN}Created $IDENTITIES_FILE${NC}"
|
|
}
|
|
|
|
add_identity() {
|
|
local a n e
|
|
while true; do
|
|
read -rp "Alias: " a
|
|
[[ -z "$a" ]] && { echo "Cancelled."; return 0; }
|
|
if [[ "$a" == *"|"* ]]; then echo "Alias must not contain '|'"; continue; fi
|
|
local i dup=false
|
|
for i in "${!aliases[@]}"; do
|
|
[[ "${aliases[$i]}" == "$a" ]] && dup=true
|
|
done
|
|
$dup && { echo "Alias '$a' already exists"; continue; }
|
|
break
|
|
done
|
|
read -rp "Name: " n
|
|
[[ -z "$n" ]] && { echo "Cancelled."; return 0; }
|
|
while true; do
|
|
read -rp "Email: " e
|
|
[[ -z "$e" ]] && { echo "Cancelled."; return 0; }
|
|
[[ "$e" == *@* ]] && break
|
|
echo "Email must contain '@'"
|
|
done
|
|
if $dryrun; then
|
|
echo "[dryrun] Would append '$a|$n|$e' to $IDENTITIES_FILE"
|
|
else
|
|
echo "$a|$n|$e" >> "$IDENTITIES_FILE"
|
|
echo -e "${GREEN}Added '$a'${NC}"
|
|
fi
|
|
read -rp "Apply it now? [y/N] " reply
|
|
if [[ "$reply" =~ ^[Yy] ]]; then
|
|
apply_identity "$n" "$e"
|
|
fi
|
|
}
|
|
|
|
remove_identity() {
|
|
if [[ ${#aliases[@]} -eq 0 ]]; then
|
|
echo "No identities to remove."
|
|
return 0
|
|
fi
|
|
local i
|
|
for i in "${!aliases[@]}"; do
|
|
printf ' %d) %-14s %s <%s>\n' "$((i+1))" "${aliases[$i]}" "${names[$i]}" "${emails[$i]}"
|
|
done
|
|
read -rp "Remove which? [1-${#aliases[@]}/q]: " reply
|
|
[[ -z "$reply" || "$reply" == "q" ]] && { echo "Cancelled."; return 0; }
|
|
if ! [[ "$reply" =~ ^[0-9]+$ ]] || (( reply < 1 || reply > ${#aliases[@]} )); then
|
|
echo "Error: Invalid selection" >&2; return 1
|
|
fi
|
|
local victim="${aliases[$((reply-1))]}"
|
|
if $dryrun; then
|
|
echo "[dryrun] Would remove identity '$victim' from $IDENTITIES_FILE"
|
|
return 0
|
|
fi
|
|
local tmp; tmp=$(mktemp)
|
|
awk -F'|' -v a="$victim" '$1 != a' "$IDENTITIES_FILE" > "$tmp" && mv "$tmp" "$IDENTITIES_FILE"
|
|
echo -e "${GREEN}Removed '$victim'${NC}"
|
|
}
|
|
|
|
something_else() {
|
|
echo " a) Add identity"
|
|
echo " r) Remove identity"
|
|
echo " q) Cancel"
|
|
read -rp "Select [a/r/q]: " reply
|
|
case "$reply" in
|
|
a) add_identity ;;
|
|
r) remove_identity ;;
|
|
*) echo "Cancelled." ;;
|
|
esac
|
|
}
|
|
|
|
interactive_menu() {
|
|
ensure_file
|
|
load_identities
|
|
show_current
|
|
echo ""
|
|
local i
|
|
for i in "${!aliases[@]}"; do
|
|
printf ' %d) %-14s %s <%s>\n' "$((i+1))" "${aliases[$i]}" "${names[$i]}" "${emails[$i]}"
|
|
done
|
|
echo " s) Something else..."
|
|
echo ""
|
|
local prompt="Select [s/q]: "
|
|
[[ ${#aliases[@]} -gt 0 ]] && prompt="Select [1-${#aliases[@]}/s/q]: "
|
|
read -rp "$prompt" reply
|
|
if [[ -z "$reply" || "$reply" == "q" ]]; then
|
|
echo "Cancelled."
|
|
exit 0
|
|
elif [[ "$reply" == "s" ]]; then
|
|
something_else
|
|
elif [[ "$reply" =~ ^[0-9]+$ ]] && (( reply >= 1 && reply <= ${#aliases[@]} )); then
|
|
apply_identity "${names[$((reply-1))]}" "${emails[$((reply-1))]}"
|
|
else
|
|
echo "Error: Invalid selection" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
case "$action" in
|
|
current)
|
|
show_current
|
|
exit 0 ;;
|
|
list)
|
|
if [[ ! -f "$IDENTITIES_FILE" ]]; then
|
|
echo "Error: No identities file at $IDENTITIES_FILE (run git-identity to create one)" >&2
|
|
exit 1
|
|
fi
|
|
load_identities
|
|
list_identities
|
|
exit 0 ;;
|
|
esac
|
|
|
|
if [[ -n "$alias_arg" ]]; then
|
|
if [[ ! -f "$IDENTITIES_FILE" ]]; then
|
|
echo "Error: No identities file at $IDENTITIES_FILE (run git-identity to create one)" >&2
|
|
exit 1
|
|
fi
|
|
load_identities
|
|
apply_alias "$alias_arg"
|
|
else
|
|
interactive_menu
|
|
fi
|