Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/fm-backend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ fm_backend_validate_task_endpoint() { # <meta-file> <task-id>
herdr)
[ "$binding" = "$id" ] || {
echo "REFUSED: legacy Herdr endpoint metadata for task $id lacks an exact task binding; preserving task state." >&2
echo "A finished legacy record can be bound through the guarded bin/fm-herdr-legacy-repair.sh $id (see its --help); nothing repairs a binding implicitly." >&2
return 1
}
recorded_session=$(fm_backend_meta_exact_value "$meta" herdr_session) || recorded_session=
Expand Down
357 changes: 357 additions & 0 deletions bin/fm-herdr-legacy-repair.sh

Large diffs are not rendered by default.

168 changes: 168 additions & 0 deletions bin/fm-landed-lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
#!/usr/bin/env bash
# bin/fm-landed-lib.sh - the single owner of the landed-work test's helper
# functions, extracted verbatim from bin/fm-teardown.sh so a second caller
# (bin/fm-herdr-legacy-repair.sh) cannot drift from teardown's semantics.
# bin/fm-teardown.sh remains the owner of the COMPLETE landed-work decision -
# when the test runs, what --force may skip, and every refusal message; this
# lib only holds the shared predicates. Every function takes explicit
# arguments and reads no caller globals.
#
# Landed means: the worktree's committed work is reachable from a
# remote-tracking branch (the caller checks that with `git log HEAD --not
# --remotes` before calling fm_landed_work_is_landed), OR a merged PR's head
# contains the current local work, OR the branch's content is already present
# in the up-to-date default branch (the squash-merge-then-delete-branch flow).
# Uncommitted changes are never landed and are the caller's check.
# Sourced only; not executable on its own.

# fm_landed_default_branch <project-dir>: the project's default branch name
# from origin/HEAD, falling back to a local main or master head.
fm_landed_default_branch() { # <project-dir>
local proj=$1 ref branch
ref=$(git -C "$proj" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || true)
if [ -n "$ref" ]; then
echo "${ref#origin/}"
return 0
fi
for branch in main master; do
if git -C "$proj" show-ref --verify --quiet "refs/heads/$branch"; then
echo "$branch"
return 0
fi
done
return 1
}

# Resolve the PR number for a worktree branch via gh-axi. Echoes the number on a
# single match and returns 0; returns non-zero on no match or any lookup failure,
# so the caller treats it as "no PR found" (fail-safe).
fm_landed_pr_number_from_branch() { # <worktree> <branch>
local wt=$1 branch=$2 out n
[ -n "$branch" ] && [ "$branch" != HEAD ] || return 1
out=$( cd "$wt" && gh-axi pr list --state all --head "$branch" --limit 1 2>/dev/null ) || return 1
n=$(printf '%s\n' "$out" | sed -n 's/^[[:space:]]*\([0-9][0-9]*\),.*/\1/p' | head -1)
[ -n "$n" ] || return 1
printf '%s' "$n"
}

fm_landed_pr_number_from_target() { # <pr-url-or-number>
local target=$1 n
case "$target" in
'' ) return 1 ;;
*"/pull/"*)
n=${target##*/pull/}
n=${n%%[!0-9]*}
;;
[0-9]*)
n=${target%%[!0-9]*}
;;
*) return 1 ;;
esac
[ -n "$n" ] || return 1
printf '%s' "$n"
}

fm_landed_ensure_commit_object() { # <worktree> <pr-target> <commit>
local wt=$1 target=$2 commit=$3 n
git -C "$wt" cat-file -e "$commit^{commit}" 2>/dev/null && return 0
n=$(fm_landed_pr_number_from_target "$target") || return 1
git -C "$wt" remote get-url origin >/dev/null 2>&1 || return 1
git -C "$wt" fetch --quiet origin "refs/pull/$n/head" >/dev/null 2>&1 || return 1
git -C "$wt" cat-file -e "$commit^{commit}" 2>/dev/null
}

fm_landed_patch_id_for_commit() { # <worktree> <commit>
local wt=$1 commit=$2
git -C "$wt" show --pretty=medium --no-ext-diff "$commit" 2>/dev/null \
| git patch-id --stable 2>/dev/null \
| awk 'NR == 1 { print $1 }'
}

fm_landed_unpushed_patches_are_in_pr_head() { # <worktree> <pr-head>
local wt=$1 pr_head=$2 current base pr_patch_ids commit patch_id unpushed
current=$(git -C "$wt" rev-parse --verify HEAD 2>/dev/null) || return 1
base=$(git -C "$wt" merge-base "$current" "$pr_head" 2>/dev/null) || return 1
pr_patch_ids=$(
git -C "$wt" log --format=%H "$base..$pr_head" -- 2>/dev/null \
| while IFS= read -r commit; do
fm_landed_patch_id_for_commit "$wt" "$commit"
done \
| sed '/^$/d' \
| sort -u
) || return 1
[ -n "$pr_patch_ids" ] || return 1
unpushed=$(git -C "$wt" log --format=%H HEAD --not --remotes -- 2>/dev/null) || return 1
[ -n "$unpushed" ] || return 1
while IFS= read -r commit; do
[ -n "$commit" ] || continue
patch_id=$(fm_landed_patch_id_for_commit "$wt" "$commit") || return 1
[ -n "$patch_id" ] || return 1
printf '%s\n' "$pr_patch_ids" | grep -qxF "$patch_id" || return 1
done <<EOF
$unpushed
EOF
}

# Is the worktree's PR merged for local work contained in that PR? Resolves the
# PR from the recorded pr= URL first, then from the branch name, and asks GitHub
# for both the PR state and head. Returns non-zero when the PR is not merged, the
# current work is not contained in the PR head, no PR is found, or any gh error
# occurs - the caller then falls back to the content check.
fm_landed_pr_is_merged() { # <worktree> <pr-url-or-empty> <branch>
local wt=$1 pr_url=$2 branch=$3 target view state head current
if [ -n "$pr_url" ]; then
target=$pr_url
else
target=$(fm_landed_pr_number_from_branch "$wt" "$branch") || return 1
fi
[ -n "$target" ] || return 1
view=$(cd "$wt" && gh pr view "$target" --json state,headRefOid -q '.state + "\t" + .headRefOid' 2>/dev/null) || return 1
state=${view%%$'\t'*}
head=${view#*$'\t'}
[ "$state" != "$view" ] || return 1
case "$state" in
MERGED|merged) ;;
*) return 1 ;;
esac
[ -n "$head" ] || return 1
fm_landed_ensure_commit_object "$wt" "$target" "$head" || return 1
current=$(git -C "$wt" rev-parse --verify HEAD 2>/dev/null) || return 1
git -C "$wt" merge-base --is-ancestor "$current" "$head" 2>/dev/null && return 0
fm_landed_unpushed_patches_are_in_pr_head "$wt" "$head"
}

# Is the branch's content already present in the up-to-date default branch? Fetches
# first, then 3-way merges the default branch with HEAD: when HEAD introduces nothing
# the default branch does not already contain (e.g. its change landed via squash) the
# merged tree equals the default branch's tree. This isolates branch-only changes, so
# unrelated commits the default branch gained past the merge-base do not count as
# "added". Returns non-zero when inconclusive (no default ref, or a merge conflict),
# so the caller refuses rather than guesses.
fm_landed_content_in_default() { # <worktree> <project-dir>
local wt=$1 proj=$2 name ref default_tree merged_tree
name=$(fm_landed_default_branch "$proj") || return 1
if git -C "$wt" remote get-url origin >/dev/null 2>&1; then
git -C "$wt" fetch --quiet origin "+refs/heads/$name:refs/remotes/origin/$name" >/dev/null 2>&1 || return 1
ref="refs/remotes/origin/$name"
elif git -C "$wt" rev-parse --quiet --verify "refs/heads/$name" >/dev/null 2>&1; then
ref="refs/heads/$name"
else
return 1
fi
default_tree=$(git -C "$wt" rev-parse --quiet --verify "$ref^{tree}" 2>/dev/null) || return 1
[ -n "$default_tree" ] || return 1
merged_tree=$(git -C "$wt" merge-tree --write-tree "$ref" HEAD 2>/dev/null) || return 1
merged_tree=$(printf '%s\n' "$merged_tree" | head -1)
[ "$merged_tree" = "$default_tree" ]
}

# Has the worktree's committed work actually LANDED, though its commits are not
# reachable from any remote-tracking branch? True when a merged PR proves the
# current local work is contained in the PR head, OR the content is already in the
# default branch (fallback, which also covers the no-PR and gh-error paths). False
# only for genuinely unlanded work.
fm_landed_work_is_landed() { # <worktree> <project-dir> <pr-url-or-empty> <branch>
local wt=$1 proj=$2 pr_url=$3 branch=$4
fm_landed_pr_is_merged "$wt" "$pr_url" "$branch" && return 0
fm_landed_content_in_default "$wt" "$proj"
}
149 changes: 10 additions & 139 deletions bin/fm-teardown.sh
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ SUB_HOME_PARENT_MARKER=".fm-secondmate-parent"
. "$SCRIPT_DIR/fm-gate-refuse-lib.sh"
# shellcheck source=bin/fm-pr-lib.sh
. "$SCRIPT_DIR/fm-pr-lib.sh"
# shellcheck source=bin/fm-landed-lib.sh
. "$SCRIPT_DIR/fm-landed-lib.sh"
# shellcheck source=bin/fm-public-followup-lib.sh
. "$SCRIPT_DIR/fm-public-followup-lib.sh"
# shellcheck source=bin/fm-secondmate-registry-lib.sh
Expand Down Expand Up @@ -595,20 +597,11 @@ elif [ "$FORCE" != "--force" ] && fm_pf_relay_active "$FM_HOME"; then
PUBLIC_FOLLOWUP_RELAY_ACTIVE=1
fi

# Landed-work predicates live in bin/fm-landed-lib.sh (shared with the guarded
# legacy Herdr repair path); these wrappers keep teardown's historical names
# bound to this task's worktree, project, and recorded PR.
default_branch() {
local ref branch
ref=$(git -C "$PROJ" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || true)
if [ -n "$ref" ]; then
echo "${ref#origin/}"
return 0
fi
for branch in main master; do
if git -C "$PROJ" show-ref --verify --quiet "refs/heads/$branch"; then
echo "$branch"
return 0
fi
done
return 1
fm_landed_default_branch "$PROJ"
}

meta_value() {
Expand Down Expand Up @@ -757,138 +750,16 @@ remove_pr_poll_artifacts() {
fi
}

# Resolve the PR number for a worktree branch via gh-axi. Echoes the number on a
# single match and returns 0; returns non-zero on no match or any lookup failure,
# so the caller treats it as "no PR found" (fail-safe).
pr_number_from_branch() {
local branch=$1 out n
[ -n "$branch" ] && [ "$branch" != HEAD ] || return 1
out=$( cd "$WT" && gh-axi pr list --state all --head "$branch" --limit 1 2>/dev/null ) || return 1
n=$(printf '%s\n' "$out" | sed -n 's/^[[:space:]]*\([0-9][0-9]*\),.*/\1/p' | head -1)
[ -n "$n" ] || return 1
printf '%s' "$n"
}

pr_number_from_target() {
local target=$1 n
case "$target" in
'' ) return 1 ;;
*"/pull/"*)
n=${target##*/pull/}
n=${n%%[!0-9]*}
;;
[0-9]*)
n=${target%%[!0-9]*}
;;
*) return 1 ;;
esac
[ -n "$n" ] || return 1
printf '%s' "$n"
}

ensure_commit_object() {
local target=$1 commit=$2 n
git -C "$WT" cat-file -e "$commit^{commit}" 2>/dev/null && return 0
n=$(pr_number_from_target "$target") || return 1
git -C "$WT" remote get-url origin >/dev/null 2>&1 || return 1
git -C "$WT" fetch --quiet origin "refs/pull/$n/head" >/dev/null 2>&1 || return 1
git -C "$WT" cat-file -e "$commit^{commit}" 2>/dev/null
}

patch_id_for_commit() {
local commit=$1
git -C "$WT" show --pretty=medium --no-ext-diff "$commit" 2>/dev/null \
| git patch-id --stable 2>/dev/null \
| awk 'NR == 1 { print $1 }'
}

unpushed_patches_are_in_pr_head() {
local pr_head=$1 current base pr_patch_ids commit patch_id unpushed
current=$(git -C "$WT" rev-parse --verify HEAD 2>/dev/null) || return 1
base=$(git -C "$WT" merge-base "$current" "$pr_head" 2>/dev/null) || return 1
pr_patch_ids=$(
git -C "$WT" log --format=%H "$base..$pr_head" -- 2>/dev/null \
| while IFS= read -r commit; do
patch_id_for_commit "$commit"
done \
| sed '/^$/d' \
| sort -u
) || return 1
[ -n "$pr_patch_ids" ] || return 1
unpushed=$(git -C "$WT" log --format=%H HEAD --not --remotes -- 2>/dev/null) || return 1
[ -n "$unpushed" ] || return 1
while IFS= read -r commit; do
[ -n "$commit" ] || continue
patch_id=$(patch_id_for_commit "$commit") || return 1
[ -n "$patch_id" ] || return 1
printf '%s\n' "$pr_patch_ids" | grep -qxF "$patch_id" || return 1
done <<EOF
$unpushed
EOF
}

# Is the worktree's PR merged for local work contained in that PR? Resolves the
# PR from the recorded pr= URL first, then from the branch name, and asks GitHub
# for both the PR state and head. Returns non-zero when the PR is not merged, the
# current work is not contained in the PR head, no PR is found, or any gh error
# occurs - the caller then falls back to the content check.
pr_is_merged() {
local branch=$1 target view state head current
if [ -n "$PR_URL" ]; then
target=$PR_URL
else
target=$(pr_number_from_branch "$branch") || return 1
fi
[ -n "$target" ] || return 1
view=$(cd "$WT" && gh pr view "$target" --json state,headRefOid -q '.state + "\t" + .headRefOid' 2>/dev/null) || return 1
state=${view%%$'\t'*}
head=${view#*$'\t'}
[ "$state" != "$view" ] || return 1
case "$state" in
MERGED|merged) ;;
*) return 1 ;;
esac
[ -n "$head" ] || return 1
ensure_commit_object "$target" "$head" || return 1
current=$(git -C "$WT" rev-parse --verify HEAD 2>/dev/null) || return 1
git -C "$WT" merge-base --is-ancestor "$current" "$head" 2>/dev/null && return 0
unpushed_patches_are_in_pr_head "$head"
}

# Is the branch's content already present in the up-to-date default branch? Fetches
# first, then 3-way merges the default branch with HEAD: when HEAD introduces nothing
# the default branch does not already contain (e.g. its change landed via squash) the
# merged tree equals the default branch's tree. This isolates branch-only changes, so
# unrelated commits the default branch gained past the merge-base do not count as
# "added". Returns non-zero when inconclusive (no default ref, or a merge conflict),
# so the caller refuses rather than guesses.
content_in_default() {
local name ref default_tree merged_tree
name=$(default_branch) || return 1
if git -C "$WT" remote get-url origin >/dev/null 2>&1; then
git -C "$WT" fetch --quiet origin "+refs/heads/$name:refs/remotes/origin/$name" >/dev/null 2>&1 || return 1
ref="refs/remotes/origin/$name"
elif git -C "$WT" rev-parse --quiet --verify "refs/heads/$name" >/dev/null 2>&1; then
ref="refs/heads/$name"
else
return 1
fi
default_tree=$(git -C "$WT" rev-parse --quiet --verify "$ref^{tree}" 2>/dev/null) || return 1
[ -n "$default_tree" ] || return 1
merged_tree=$(git -C "$WT" merge-tree --write-tree "$ref" HEAD 2>/dev/null) || return 1
merged_tree=$(printf '%s\n' "$merged_tree" | head -1)
[ "$merged_tree" = "$default_tree" ]
}

# The landed-work predicates (PR-merged proof, content-in-default fallback, and
# their helpers) are owned by bin/fm-landed-lib.sh; these wrappers bind them to
# this task's worktree, project, and recorded PR.
# Has the worktree's committed work actually LANDED, though its commits are not
# reachable from any remote-tracking branch? True when a merged PR proves the
# current local work is contained in the PR head, OR the content is already in the
# default branch (fallback, which also covers the no-PR and gh-error paths). False
# only for genuinely unlanded work.
work_is_landed() {
local branch=$1
pr_is_merged "$branch" && return 0
content_in_default
fm_landed_work_is_landed "$WT" "$PROJ" "$PR_URL" "$1"
}

backlog_refresh_reminder() {
Expand Down
3 changes: 2 additions & 1 deletion bin/fm-test-isolation-proof.sh
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ exclusion_reason() {
fm-backend-autodetect-smoke.test.sh|fm-backend-herdr-eventwait-smoke.test.sh|\
fm-backend-herdr-presentation-e2e.test.sh|fm-backend-herdr-prune-safety-e2e.test.sh|\
fm-backend-herdr-respawn-idem-e2e.test.sh|fm-backend-herdr-smoke.test.sh|\
fm-backend-herdr-workspace-per-home-e2e.test.sh|fm-herdr-session-cleanup-e2e.test.sh)
fm-backend-herdr-workspace-per-home-e2e.test.sh|fm-herdr-session-cleanup-e2e.test.sh|\
fm-herdr-legacy-repair-e2e.test.sh)
printf '%s\n' 'real Herdr-gated; Herdr lane is a later phase'
;;
fm-backend-cmux.test.sh|fm-backend-cmux-smoke.test.sh)
Expand Down
Loading
Loading