diff --git a/functions/_agents_repo_sync.fish b/functions/_agents_repo_sync.fish new file mode 100644 index 0000000..513d888 --- /dev/null +++ b/functions/_agents_repo_sync.fish @@ -0,0 +1,55 @@ +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later + +# SYNOPSIS +# _agents_repo_sync +# +# DESCRIPTION +# Pulls (when an upstream is configured), stages everything, and commits +# with . Shared by agents-init and agents-vault. +# +# A failed rebase is aborted and nothing is committed. Committing blindly +# after a failed pull would stage conflict markers and record them under a +# routine-looking message, so the failure is surfaced instead: the repo is +# left clean at local HEAD for the user to resolve by hand. +# +# Commits are made with commit.gpgsign=false so a pinentry prompt can +# never block a shell or an agent launch. +# +# ARGUMENTS +# dir Absolute path to the git repository +# message Commit subject used when there is something to commit +# +# EXIT STATUS +# 0 Committed, or nothing needed committing +# 1 is not a git repository +# 2 Rebase conflict; aborted, nothing committed +# +# RETURNS +# A single "→ Committed () " line on stdout when it +# commits; nothing when there was nothing to do. +# +# EXAMPLE +# _agents_repo_sync /path/to/AGENTS "chore: sync AGENTS repository" +function _agents_repo_sync --argument-names dir msg + test -n "$dir" -a -n "$msg"; or return 1 + test -d "$dir/.git"; or return 1 + + if git -C "$dir" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1 + if not git -C "$dir" pull --rebase --autostash -q >/dev/null 2>/dev/null + git -C "$dir" rebase --abort >/dev/null 2>/dev/null + echo "_agents_repo_sync: rebase conflict in $dir; aborted, left at local HEAD" >&2 + return 2 + end + end + + git -C "$dir" add -A 2>/dev/null + set -l status_out (git -C "$dir" status --porcelain 2>/dev/null) + test -n "$status_out"; or return 0 + + if git -C "$dir" -c commit.gpgsign=false commit -q -m "$msg" 2>/dev/null + set -l sha (git -C "$dir" rev-parse --short HEAD 2>/dev/null) + set -l subject (git -C "$dir" log -1 --pretty=%s 2>/dev/null) + echo "→ Committed ($sha) $subject" + end +end diff --git a/tests/test-agents-vault.fish b/tests/test-agents-vault.fish index c59589b..0c50b02 100644 --- a/tests/test-agents-vault.fish +++ b/tests/test-agents-vault.fish @@ -136,6 +136,53 @@ ln -s $p/one $p/link _agents_repo_ensure_symlink $p/link $p/two >/dev/null check "repins a wrong link" (path resolve $p/two) (path resolve $p/link) +# ─────────────────────────── sync policy ─────────────────────────────── +echo "" +echo "== _agents_repo_sync ==" + +set -l s (new_repo) +echo hello >$s/a.md +_agents_repo_sync $s "chore: test" >/dev/null +check "commits new content" 1 (git -C $s rev-list --count HEAD) + +# Nothing to do: no second commit, no output. +set -l out (_agents_repo_sync $s "chore: test") +check "idempotent, no new commit" 1 (git -C $s rev-list --count HEAD) +check "idempotent, silent" "" "$out" + +# Conflict: two clones diverge with conflicting commits on the same line. +# (Merely leaving "ours" uncommitted in the worktree isn't enough to force +# a rebase conflict -- with nothing local to replay, --autostash's rebase +# step fast-forwards cleanly and only the stash *pop* would conflict, +# leaving "theirs" committed on HEAD with "ours" stranded in the stash. +# Committing "ours" locally first means the rebase itself must replay a +# real commit over "theirs" on the same line, which is where the intended +# conflict-and-abort path actually lives.) +set -l origin (mktemp -d); set -ga TMPDIRS $origin +git -C $origin init -q --bare +git -C $s remote add origin $origin +git -C $s push -q -u origin HEAD:refs/heads/main 2>/dev/null + +set -l clone (mktemp -d); set -ga TMPDIRS $clone +git clone -q $origin $clone +git -C $clone config user.email t@t +git -C $clone config user.name t +git -C $clone config commit.gpgsign false +git -C $clone config core.hooksPath /dev/null +echo theirs >$clone/a.md +git -C $clone commit -qam theirs +git -C $clone push -q origin HEAD:main + +echo ours >$s/a.md +git -C $s commit -qam ours +set -l before_count (git -C $s rev-list --count HEAD) +set -l rc (_agents_repo_sync $s "chore: test" 2>/dev/null; echo $status) +check "conflict returns 2" 2 "$rc" +check "conflict leaves no rebase in progress" false (test -d $s/.git/rebase-merge -o -d $s/.git/rebase-apply; and echo true; or echo false) +check "conflict commits nothing" $before_count (git -C $s rev-list --count HEAD) +check "conflict content survives" ours (cat $s/a.md) +check "conflict left no markers" false (grep -q '<<<<<<<' $s/a.md; and echo true; or echo false) + cleanup echo "" echo (math $TESTS_RUN - $TESTS_FAILED)"/$TESTS_RUN passed"