diff options
| -rwxr-xr-x | dotfiles/common/.local/bin/aix | 101 |
1 files changed, 98 insertions, 3 deletions
diff --git a/dotfiles/common/.local/bin/aix b/dotfiles/common/.local/bin/aix index 6f94cfb..f16af2e 100755 --- a/dotfiles/common/.local/bin/aix +++ b/dotfiles/common/.local/bin/aix @@ -33,11 +33,12 @@ create_window() { echo "$wid" } -# Read fzf selections into the 'selected' array +# Read fzf selections into the 'selected' array — strip any " (annotation)" +# suffix so downstream code sees the raw tilde path. read_selections() { selected=() while IFS= read -r line; do - selected+=("$line") + selected+=("${line%% (*}") done <<<"$1" } @@ -64,6 +65,87 @@ build_candidates() { fi } +# Fetch all candidate repos in parallel (capped concurrency). +# Blocking call — synchronous relative to the rest of the script. +fetch_candidates() { + local max=6 running=0 dir + for c in "${candidates[@]}"; do + dir="${c/#\~/$HOME}" + [ -d "$dir/.git" ] || continue + git -C "$dir" fetch --quiet 2>/dev/null & + ((running++)) + if ((running >= max)); then + wait + running=0 + fi + done + wait +} + +# Produce a git-status indicator string for a directory, e.g. +# "(↑1 ↓3 dirty)" or "(no upstream)". Empty string if clean. +git_status_indicator() { + local dir="$1" upstream ahead behind parts=() + [ -d "$dir/.git" ] || return + + upstream=$(git -C "$dir" rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null) + if [ -n "$upstream" ]; then + ahead=$(git -C "$dir" rev-list --count "$upstream..HEAD" 2>/dev/null || echo 0) + behind=$(git -C "$dir" rev-list --count "HEAD..$upstream" 2>/dev/null || echo 0) + [ "${ahead:-0}" -gt 0 ] 2>/dev/null && parts+=("↑$ahead") + [ "${behind:-0}" -gt 0 ] 2>/dev/null && parts+=("↓$behind") + else + parts+=("no upstream") + fi + + if ! git -C "$dir" diff --quiet 2>/dev/null \ + || ! git -C "$dir" diff --cached --quiet 2>/dev/null \ + || [ -n "$(git -C "$dir" ls-files --others --exclude-standard 2>/dev/null)" ]; then + parts+=("dirty") + fi + + if [ ${#parts[@]} -gt 0 ]; then + local IFS=' ' + printf ' (%s)' "${parts[*]}" + fi +} + +# Annotate the 'candidates' array with git-status indicators +annotate_candidates() { + local dir status annotated=() + for c in "${candidates[@]}"; do + dir="${c/#\~/$HOME}" + status=$(git_status_indicator "$dir") + annotated+=("${c}${status}") + done + candidates=("${annotated[@]}") +} + +# Auto-pull a directory if its working tree is clean, upstream is set, +# the branch is behind (not ahead, not diverged). No-op otherwise. +auto_pull_if_clean() { + local dir="$1" upstream ahead behind + [ -d "$dir/.git" ] || return + + # Dirty? Skip. + if ! git -C "$dir" diff --quiet 2>/dev/null \ + || ! git -C "$dir" diff --cached --quiet 2>/dev/null \ + || [ -n "$(git -C "$dir" ls-files --others --exclude-standard 2>/dev/null)" ]; then + return + fi + + upstream=$(git -C "$dir" rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null) + [ -z "$upstream" ] && return + + ahead=$(git -C "$dir" rev-list --count "$upstream..HEAD" 2>/dev/null || echo 0) + [ "${ahead:-0}" -gt 0 ] 2>/dev/null && return + + behind=$(git -C "$dir" rev-list --count "HEAD..$upstream" 2>/dev/null || echo 0) + [ "${behind:-0}" -eq 0 ] 2>/dev/null && return + + git -C "$dir" pull --ff-only --quiet 2>/dev/null +} + # Sort windows: non-project windows at base-index, projects alphabetically after sort_windows() { local windows others projects base_idx project_names @@ -147,7 +229,13 @@ if tmux has-session -t "$SESSION" 2>/dev/null; then if [ ${#filtered[@]} -eq 0 ]; then echo "All projects already have windows open." else - selections=$(printf '%s\n' "${filtered[@]}" | fzf --multi --height=70% --reverse) + # Annotate filtered list with git-status indicators (fetch first) + candidates=("${filtered[@]}") + echo "Fetching remotes..." >&2 + fetch_candidates + annotate_candidates + + selections=$(printf '%s\n' "${candidates[@]}" | fzf --multi --height=70% --reverse) if [ -n "$selections" ]; then read_selections "$selections" @@ -156,6 +244,7 @@ if tmux has-session -t "$SESSION" 2>/dev/null; then for entry in "${selected[@]}"; do dir="${entry/#\~/$HOME}" name="$(basename "$dir")" + auto_pull_if_clean "$dir" wid=$(create_window "$dir" "$name") [ -z "$first_wid" ] && first_wid="$wid" done @@ -171,6 +260,10 @@ fi # New session: select projects and create session build_candidates +echo "Fetching remotes..." >&2 +fetch_candidates +annotate_candidates + selections=$(printf '%s\n' "${candidates[@]}" | fzf --multi --height=70% --reverse) [ -z "$selections" ] && exit 0 @@ -180,6 +273,7 @@ read_selections "$selections" first="${selected[0]}" dir="${first/#\~/$HOME}" name="$(basename "$dir")" +auto_pull_if_clean "$dir" first_wid=$(tmux new-session -d -s "$SESSION" -n "$name" -c "$dir" -P -F '#{window_id}') tmux send-keys -t "$first_wid" "$AI_CMD \"$AI_INSTRUCTIONS\"" Enter @@ -187,6 +281,7 @@ tmux send-keys -t "$first_wid" "$AI_CMD \"$AI_INSTRUCTIONS\"" Enter for entry in "${selected[@]:1}"; do dir="${entry/#\~/$HOME}" name="$(basename "$dir")" + auto_pull_if_clean "$dir" create_window "$dir" "$name" > /dev/null done |
