diff options
| author | Craig Jennings <c@cjennings.net> | 2026-04-20 09:05:39 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-04-20 09:05:39 -0500 |
| commit | f3526632257686692687b1b5a596eb399e42b119 (patch) | |
| tree | fd8f220fb3e54bee53c51364a5c9505354c9b67d | |
| parent | 93c137a953e7a5fe69d781ebdb49fcc85ae67b0c (diff) | |
| download | archsetup-f3526632257686692687b1b5a596eb399e42b119.tar.gz archsetup-f3526632257686692687b1b5a596eb399e42b119.zip | |
aix: synchronous fetch + git-status annotations + auto-pull if clean
Adds multi-machine sync awareness to the session launcher.
Before fzf:
- Fetches all candidate repos in parallel (capped at 6 concurrent)
- Annotates each entry with git status: (↑N ↓N dirty), (no upstream)
- Clean repos show no annotation
When launching a window:
- If working tree is clean, has upstream, is behind (not ahead): pull
- Otherwise: open window as-is, user handles any sync manually
Design choices:
- Fetches are synchronous — accurate status at the cost of a brief wait
on session start. Parallel execution keeps it fast (~1-2s for 22 repos).
- Pull is --ff-only — never merges, never creates merge commits in aix
- Diverged repos (ahead AND behind) trigger no auto-action; user decides
- No stash/pop dance — unreliable in multi-project batches; prefer
explicit awareness via annotation
Primary use case: moving between laptop and desktop. Wrap-it-up's
always-push ensures remote is current at session end; this ensures
local is current at next session start.
| -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 |
