From 261e663e978f0b39b02010e3d6654d301dc619e5 Mon Sep 17 00:00:00 2001 From: rootiest Date: Fri, 19 Jun 2026 02:21:21 -0400 Subject: [PATCH] feat(auto-pull): opt-in background fast-forward for registered repos Add a C2-guarded PWD handler that background fast-forwards the fish-config repo (baseline) plus any repos in a machine-local registry. Safety is guaranteed by _auto_pull_sync: clean tree + has-upstream + --ff-only, else no-op. Never rebases, merges, or overwrites work. - conf.d/auto-pull.fish: --on-variable PWD handler, throttled to fire once per repo entry; spawns a --no-config child to run the worker - functions/_auto_pull_sync.fish: the ff-only worker (safe on any branch) - functions/auto-pull.fish: add/remove/list/status registry management, usable even when C2 is disabled - completions/auto-pull.fish: subcommand + registered-repo completions Registry lives at ~/.config/.user-dots/fish/auto-pull.list (machine-local, never committed). --- completions/auto-pull.fish | 33 ++++++++ conf.d/auto-pull.fish | 62 ++++++++++++++ functions/_auto_pull_sync.fish | 47 +++++++++++ functions/auto-pull.fish | 150 +++++++++++++++++++++++++++++++++ 4 files changed, 292 insertions(+) create mode 100644 completions/auto-pull.fish create mode 100644 conf.d/auto-pull.fish create mode 100644 functions/_auto_pull_sync.fish create mode 100644 functions/auto-pull.fish diff --git a/completions/auto-pull.fish b/completions/auto-pull.fish new file mode 100644 index 0000000..67803f7 --- /dev/null +++ b/completions/auto-pull.fish @@ -0,0 +1,33 @@ +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later + +# Completions for the `auto-pull` registry command. + +function __auto_pull_registered + set -l list "$XDG_CONFIG_HOME/.user-dots/fish/auto-pull.list" + test -r "$list"; or return + for l in (command cat "$list" 2>/dev/null) + test -n "$l"; or continue + printf '%s\t%s\n' (path basename "$l") "$l" + end +end + +set -l subcmds list add remove status + +# Subcommands (only as the first argument). +complete -c auto-pull -f -n "not __fish_seen_subcommand_from $subcmds" \ + -a list -d 'Show registered repos' +complete -c auto-pull -f -n "not __fish_seen_subcommand_from $subcmds" \ + -a add -d "Register a repo (default: current)" +complete -c auto-pull -f -n "not __fish_seen_subcommand_from $subcmds" \ + -a remove -d 'Unregister a repo' +complete -c auto-pull -f -n "not __fish_seen_subcommand_from $subcmds" \ + -a status -d 'Show enabled state and registry path' +complete -c auto-pull -f -n "not __fish_seen_subcommand_from $subcmds" \ + -s h -l help -d 'Show help' + +# `add` takes a directory path. +complete -c auto-pull -n "__fish_seen_subcommand_from add" -a '(__fish_complete_directories)' + +# `remove` completes registered repo basenames. +complete -c auto-pull -f -n "__fish_seen_subcommand_from remove" -a '(__auto_pull_registered)' diff --git a/conf.d/auto-pull.fish b/conf.d/auto-pull.fish new file mode 100644 index 0000000..4096ed2 --- /dev/null +++ b/conf.d/auto-pull.fish @@ -0,0 +1,62 @@ +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later + +# ╭──────────────────────────────────────────────────────────╮ +# │ Auto-Pull (C2 — Autoexec) │ +# ╰──────────────────────────────────────────────────────────╯ +# +# Background, fast-forward-only `git pull` for opted-in repositories. Fires +# when the working directory enters the fish-config repo (always covered as a +# baseline) or any repository registered via the `auto-pull` command. The +# actual sync is delegated to `_auto_pull_sync`, which only ever fast-forwards +# a clean repo — see that function for the full safety contract. +# +# Manage the registry with: auto-pull add / remove / list / status + +# C2 guard: when auto-execution is disabled, do not register the handler. +__fish_config_op_enabled __fish_config_op_autoexec; or exit + +# SYNOPSIS +# __auto_pull_on_pwd (event handler, --on-variable PWD) +# +# DESCRIPTION +# On every directory change, checks whether the new $PWD is inside a +# registered repository (or the fish-config baseline) and, if so, kicks a +# background fast-forward. A throttle global ensures it fires once per +# repository entry rather than on every sub-directory change. +function __auto_pull_on_pwd --on-variable PWD + set -q __fish_config_dir; or set -g __fish_config_dir $XDG_CONFIG_HOME/fish + + set -l list "$XDG_CONFIG_HOME/.user-dots/fish/auto-pull.list" + + # Candidate roots: the fish-config repo plus the user's registry. + set -l roots $__fish_config_dir + test -r "$list"; and set -a roots (command cat "$list" 2>/dev/null) + + # Find the registered root containing $PWD (exact match or a sub-directory). + set -l hit + for r in $roots + test -n "$r"; or continue + if test "$PWD" = "$r"; or string match -q -- "$r/*" "$PWD" + set hit "$r" + break + end + end + + # Outside every registered repo: clear the throttle so re-entry re-syncs. + if test -z "$hit" + set -e __auto_pull_last + return + end + + # Already synced for this repo entry — skip until we leave and return. + test "$hit" = "$__auto_pull_last"; and return + set -g __auto_pull_last "$hit" + + # Background fast-forward. A --no-config child sources just the worker so + # it stays fast and free of shell side-effects; real `git`, no shadows. + set -l worker "$__fish_config_dir/functions/_auto_pull_sync.fish" + test -r "$worker"; or return + fish --no-config -c 'source $argv[1]; _auto_pull_sync $argv[2]' -- "$worker" "$hit" & + disown 2>/dev/null +end diff --git a/functions/_auto_pull_sync.fish b/functions/_auto_pull_sync.fish new file mode 100644 index 0000000..d03be5e --- /dev/null +++ b/functions/_auto_pull_sync.fish @@ -0,0 +1,47 @@ +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later + +# SYNOPSIS +# _auto_pull_sync +# +# DESCRIPTION +# Best-effort, side-effect-safe fast-forward of a single git repository. +# Performs a fetch and a fast-forward-only merge of the current branch's +# upstream, but only when it is completely safe to do so: the work tree +# must be clean, the branch must have an upstream, and the merge must be a +# true fast-forward. Any failed precondition makes the function a no-op. +# It never rebases, never creates a merge commit, and never overwrites +# committed or uncommitted work — worst case it does nothing. +# +# Intended to be invoked in the background by the auto-pull PWD handler +# (conf.d/auto-pull.fish), but safe to call directly. +# +# ARGUMENTS +# dir Absolute path to the git repository to fast-forward +# +# RETURNS +# 0 Fast-forward applied (or already up to date) +# 1 A precondition failed; nothing was changed +# +# EXAMPLE +# _auto_pull_sync ~/.config/fish +function _auto_pull_sync --description 'Fast-forward a repo when clean and a ff is possible' + set -l d $argv[1] + test -n "$d"; or return 1 + + # Must be a git work tree. + command git -C "$d" rev-parse --is-inside-work-tree >/dev/null 2>&1; or return 1 + + # Work tree must be clean (no staged or unstaged changes). + command git -C "$d" diff --quiet 2>/dev/null + and command git -C "$d" diff --cached --quiet 2>/dev/null + or return 1 + + # Current branch must have an upstream to fast-forward against. + command git -C "$d" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1 + or return 1 + + # Fetch, then fast-forward only. --ff-only aborts (no-op) on any divergence. + command git -C "$d" fetch -q 2>/dev/null; or return 1 + command git -C "$d" merge -q --ff-only '@{u}' >/dev/null 2>&1 +end diff --git a/functions/auto-pull.fish b/functions/auto-pull.fish new file mode 100644 index 0000000..0378503 --- /dev/null +++ b/functions/auto-pull.fish @@ -0,0 +1,150 @@ +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later + +# SYNOPSIS +# auto-pull [list] +# auto-pull add [PATH] +# auto-pull remove +# auto-pull status +# +# DESCRIPTION +# Manages the auto-pull registry: the list of repositories that are +# background fast-forwarded when you enter them (see conf.d/auto-pull.fish +# and _auto_pull_sync). The fish-config repo is always covered as a baseline +# and does not need to be added. The registry is a plain text file, one +# absolute git-toplevel path per line, stored machine-locally in +# ~/.config/.user-dots/fish/auto-pull.list (never committed to the config). +# +# Registry management works regardless of the C2 auto-execution guard; only +# the background sync itself is gated by __fish_config_op_autoexec. +# +# ARGUMENTS +# list Show registered repos (default when no subcommand given) +# add [PATH] Register PATH's git root; defaults to the current repo +# remove Unregister by basename or exact path +# status Show whether auto-pull is enabled and the registry path +# -h, --help Show this help message +# +# RETURNS +# 0 Subcommand succeeded +# 1 Bad usage, target is not a git repo, or target not registered +# +# EXAMPLE +# cd ~/src/qmk_firmware; and auto-pull add +# auto-pull add ~/work/api +# auto-pull list +# auto-pull remove qmk_firmware +function auto-pull --description 'Manage the auto-pull repository registry' + set -l c_head (set_color --bold cyan) + set -l c_cmd (set_color --bold white) + set -l c_flag (set_color yellow) + set -l c_ok (set_color green) + set -l c_warn (set_color yellow) + set -l c_err (set_color red) + set -l c_dim (set_color brblack) + set -l c_reset (set_color normal) + + set -q __fish_config_dir; or set -l __fish_config_dir $XDG_CONFIG_HOME/fish + set -l list "$XDG_CONFIG_HOME/.user-dots/fish/auto-pull.list" + + set -l cmd $argv[1] + set -e argv[1] + + if test "$cmd" = -h; or test "$cmd" = --help; or contains -- -h $argv; or contains -- --help $argv + echo "$c_head""Usage:$c_reset $c_cmd""auto-pull$c_reset $c_flag""[list | add | remove | status]$c_reset $c_dim""[PATH|NAME]$c_reset" + echo + echo " Manage the registry of repos that are background fast-forwarded on entry." + echo + echo "$c_head""Subcommands:$c_reset" + echo " $c_flag""list$c_reset Show registered repos (default)" + echo " $c_flag""add$c_reset $c_dim""[PATH]$c_reset Register PATH's git root (default: current repo)" + echo " $c_flag""remove$c_reset $c_dim""$c_reset Unregister by basename or exact path" + echo " $c_flag""status$c_reset Show enabled/disabled state and registry path" + echo " $c_flag-h$c_reset, $c_flag--help$c_reset Show this help message" + return 0 + end + + switch "$cmd" + case '' list + echo "$c_head""Auto-pull registry:$c_reset" + echo " $c_dim$__fish_config_dir$c_reset $c_dim(fish-config, baseline)$c_reset" + if test -r "$list" + for l in (command cat "$list" 2>/dev/null) + test -n "$l"; or continue + echo " $l $c_dim("(path basename "$l")")$c_reset" + end + end + return 0 + + case add + set -l target $argv[1] + test -n "$target"; or set target $PWD + set -l root (command git -C "$target" rev-parse --show-toplevel 2>/dev/null) + if test -z "$root" + echo "$c_err""auto-pull: not a git repository: $target$c_reset" >&2 + return 1 + end + if test "$root" = "$__fish_config_dir" + echo "$c_warn""auto-pull: fish-config is always covered (baseline); nothing to add$c_reset" + return 0 + end + command mkdir -p (path dirname "$list") + test -f "$list"; or command touch "$list" + if contains -- "$root" (command cat "$list" 2>/dev/null) + echo "$c_dim""auto-pull: already registered: $root$c_reset" + return 0 + end + echo "$root" >>"$list" + echo "$c_ok""→ auto-pull added: $root$c_reset" + return 0 + + case remove rm + set -l target $argv[1] + if test -z "$target" + echo "$c_err""auto-pull: usage: auto-pull remove $c_reset" >&2 + return 1 + end + if not test -r "$list" + echo "$c_dim""auto-pull: registry is empty$c_reset" + return 1 + end + set -l kept + set -l removed + for l in (command cat "$list" 2>/dev/null) + test -n "$l"; or continue + if test "$l" = "$target"; or test (path basename "$l") = "$target" + set -a removed "$l" + else + set -a kept "$l" + end + end + if test -z "$removed" + echo "$c_err""auto-pull: not registered: $target$c_reset" >&2 + return 1 + end + if test -n "$kept" + printf '%s\n' $kept >"$list" + else + printf '' >"$list" + end + echo "$c_warn""→ auto-pull removed: $removed$c_reset" + return 0 + + case status + if __fish_config_op_enabled __fish_config_op_autoexec + echo "$c_ok""auto-pull: ENABLED$c_reset $c_dim(C2 auto-execution on)$c_reset" + else + echo "$c_warn""auto-pull: DISABLED$c_reset $c_dim(via __fish_config_op_autoexec)$c_reset" + end + set -l repos + test -r "$list"; and set repos (command cat "$list" 2>/dev/null | string match -rv '^\s*$') + echo " registered repos: "(count $repos)" $c_dim(+ fish-config baseline)$c_reset" + echo " list file: $c_dim$list$c_reset" + return 0 + + case '*' + echo "$c_err""auto-pull: unknown subcommand: $cmd$c_reset" >&2 + echo "$c_dim""try: auto-pull --help$c_reset" >&2 + return 1 + end +end