aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-04-20 09:05:39 -0500
committerCraig Jennings <c@cjennings.net>2026-04-20 09:05:39 -0500
commitf3526632257686692687b1b5a596eb399e42b119 (patch)
treefd8f220fb3e54bee53c51364a5c9505354c9b67d
parent93c137a953e7a5fe69d781ebdb49fcc85ae67b0c (diff)
downloadarchsetup-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-xdotfiles/common/.local/bin/aix101
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