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
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,10 @@ sequence, and relay their per-repository lines rather than summarising them.
clone with `--ff-only` and REFUSES BY NAME one that is dirty or on a feature
branch, skipping it and leaving it exactly as it is — and not running the
verb in it either. It also refuses a root whose LEG sits on a feature
branch, because that root's own `make bootstrap` would walk the leg back
onto its tracking branch; the superproject looks clean in that state, which
branch, or anywhere but the branch that root's own `make bootstrap` places
it on — its row's own `branch:` in `project.yaml`, else the root's
`tracking_branch` — because bootstrap would walk the leg onto that branch;
the superproject looks clean in that state, which
is why the refusal names the leg. Never `git reset`, `git stash` or
`git checkout -f` — and never `git checkout main` in a leg — to make the
next run succeed: that is the work the refusal exists to protect.
Expand Down
69 changes: 67 additions & 2 deletions park
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,68 @@ legs_of() {
esac
done <"$root/project.yaml"
}

# The branch a leg's OWN row declares for it: `leg_branch_of <root> <leg path>`
# prints the `branch:` of the `legs:` row whose `path:` is <leg path>, and
# NOTHING when that row declares none, or no row has the path. It is the
# value `scripts/bootstrap.py` puts first — `str(leg.get("branch") or branch)`
# — so a leg is placed on its own `branch:` and on the root's
# `tracking_branch:` only when its row has no `branch:` of its own; a caller
# falls back to `tracking_branch_of` on an empty answer, which is that `or`.
#
# Read the way `legs_of` reads the same rows — `role:` before `path:`, the
# assembly row and a row with no role skipped — except that `branch:` may sit
# on either side of `path:` in its row, so the answer comes the moment the
# row has shown both, and a row that does not open with `- role:` clears what
# the row before it left. A leg is found by its path SPELLED EITHER WAY: the
# caller may hold `.gitmodules`' `spec` (`status`) or the manifest's own text
# (`resume`), and `./spec` and `spec/` are one leg, so a leading `./` and a
# trailing `/` come off both sides before they are compared.
#
# WHAT IS NO BRANCH: an empty value, `null`, `Null`, `NULL` or `~`, once a
# trailing ` # comment` is cut off the value (a branch name holds no space, so
# ` #` cannot be inside one). The loader is not matched to the letter: it keeps
# a QUOTED `"null"` and `'~'` as branches and reads `false` and `0` as no
# branch, while this does the opposite for both. No real branch is named any
# of those, so nothing here chases the difference.
#
# shellcheck disable=SC2329 # `park` never asks where a leg belongs;
# `resume` and `status` do.
leg_branch_of() {
local root="$1" want="$2" line role="" path="" branch=""
[ -n "$want" ] || return 0
[ -f "$root/project.yaml" ] || return 0
want="${want#./}"
want="${want%/}"
while IFS= read -r line || [ -n "$line" ]; do
line="${line%$'\r'}"
case "$line" in
' - role:'*)
role="$(trim_unquote "${line#*:}")"
path=""
branch=""
;;
' - '*) role=""; path=""; branch="" ;;
' path:'*)
path="$(trim_unquote "${line#*:}")"
path="${path#./}"
path="${path%/}"
[ -n "$role" ] || path=""
[ "$role" != "assembly" ] || path=""
;;
' branch:'*)
branch="${line#*:}"
branch="$(trim_unquote "${branch%%[[:space:]]#*}")"
case "$branch" in null | Null | NULL | '~') branch="" ;; esac
;;
[![:space:]]*) role=""; path=""; branch="" ;;
esac
if [ -n "$path" ] && [ "$path" = "$want" ] && [ -n "$branch" ]; then
printf '%s\n' "$branch"
return 0
fi
done <"$root/project.yaml"
}
# --- END shared estate resolver ---------------------------------------------

# The legs AS MOUNTED, out of `.gitmodules` — the same file `make bootstrap`
Expand Down Expand Up @@ -533,8 +595,11 @@ report_repos() {
branch="$(tracking_branch_of "$ESTATE_DIR/$member")"
printf 'root\t%s\t%s\n' "$branch" "$ESTATE_DIR/$member"
while IFS= read -r leg; do
# A LEG'S branch is the ROOT's `tracking_branch:` — that is
# the one value `scripts/bootstrap.py` places every leg on.
# A LEG'S branch here is the ROOT's `tracking_branch:`, which
# is NOT what `scripts/bootstrap.py` places a leg on when its
# row declares its own `branch:` (`leg_branch_of`): such a
# leg's unpushed commits are counted against the wrong
# branch (#242).
printf 'leg\t%s\t%s\n' "$branch" "$ESTATE_DIR/$member/$leg"
done < <(submodule_paths "$ESTATE_DIR/$member")
done < <(family_members "$ESTATE_ROOT/family.yaml")
Expand Down
118 changes: 102 additions & 16 deletions resume
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ With no <Name> the estate around the current directory is refreshed.
WHAT RESUME NEVER DOES: it resets nothing and it moves nobody's HEAD. A clone
that is dirty, or on a branch that is not its tracking branch, is REFUSED BY
NAME and skipped - it is neither fast-forwarded, nor bootstrapped, nor given
the verb - and so is a root one of whose LEGS is off its tracking branch,
because that root's own `make bootstrap` would move the leg. The rest of the
the verb - and so is a root one of whose LEGS is off the branch that root's own
`make bootstrap` places it on (its row's `branch:`, else the root's
`tracking_branch`), because bootstrap would move the leg. The rest of the
estate still comes back.
USAGE
}
Expand Down Expand Up @@ -469,6 +470,68 @@ legs_of() {
esac
done <"$root/project.yaml"
}

# The branch a leg's OWN row declares for it: `leg_branch_of <root> <leg path>`
# prints the `branch:` of the `legs:` row whose `path:` is <leg path>, and
# NOTHING when that row declares none, or no row has the path. It is the
# value `scripts/bootstrap.py` puts first — `str(leg.get("branch") or branch)`
# — so a leg is placed on its own `branch:` and on the root's
# `tracking_branch:` only when its row has no `branch:` of its own; a caller
# falls back to `tracking_branch_of` on an empty answer, which is that `or`.
#
# Read the way `legs_of` reads the same rows — `role:` before `path:`, the
# assembly row and a row with no role skipped — except that `branch:` may sit
# on either side of `path:` in its row, so the answer comes the moment the
# row has shown both, and a row that does not open with `- role:` clears what
# the row before it left. A leg is found by its path SPELLED EITHER WAY: the
# caller may hold `.gitmodules`' `spec` (`status`) or the manifest's own text
# (`resume`), and `./spec` and `spec/` are one leg, so a leading `./` and a
# trailing `/` come off both sides before they are compared.
#
# WHAT IS NO BRANCH: an empty value, `null`, `Null`, `NULL` or `~`, once a
# trailing ` # comment` is cut off the value (a branch name holds no space, so
# ` #` cannot be inside one). The loader is not matched to the letter: it keeps
# a QUOTED `"null"` and `'~'` as branches and reads `false` and `0` as no
# branch, while this does the opposite for both. No real branch is named any
# of those, so nothing here chases the difference.
#
# shellcheck disable=SC2329 # `park` never asks where a leg belongs;
# `resume` and `status` do.
leg_branch_of() {
local root="$1" want="$2" line role="" path="" branch=""
[ -n "$want" ] || return 0
[ -f "$root/project.yaml" ] || return 0
want="${want#./}"
want="${want%/}"
while IFS= read -r line || [ -n "$line" ]; do
line="${line%$'\r'}"
case "$line" in
' - role:'*)
role="$(trim_unquote "${line#*:}")"
path=""
branch=""
;;
' - '*) role=""; path=""; branch="" ;;
' path:'*)
path="$(trim_unquote "${line#*:}")"
path="${path#./}"
path="${path%/}"
[ -n "$role" ] || path=""
[ "$role" != "assembly" ] || path=""
;;
' branch:'*)
branch="${line#*:}"
branch="$(trim_unquote "${branch%%[[:space:]]#*}")"
case "$branch" in null | Null | NULL | '~') branch="" ;; esac
;;
[![:space:]]*) role=""; path=""; branch="" ;;
esac
if [ -n "$path" ] && [ "$path" = "$want" ] && [ -n "$branch" ]; then
printf '%s\n' "$branch"
return 0
fi
done <"$root/project.yaml"
}
# --- END shared estate resolver ---------------------------------------------

# --- the user config and the workspace repository ---------------------------
Expand Down Expand Up @@ -678,25 +741,48 @@ pull_ff_only() {

# THE LEGS, BEFORE A ROOT IS BOOTSTRAPPED (F3 of the review on #83). A root's
# own `make bootstrap` — `templates/assembly-root/scripts/bootstrap.py`,
# `checkout_tracking_branch` — runs `git checkout <tracking>` in a leg whose
# local tracking-branch tip equals the pin, and `checkout -b <tracking> <pin>`
# in a leg that has no local one. Either MOVES A LEG'S HEAD. A leg sitting on
# a clean feature branch at the pin makes the superproject clean, so the dirty
# and off-branch guards above see nothing and say nothing — and `resume` used
# to walk that leg off `001-my-feature` onto `main` and exit 0.
# `checkout_tracking_branch` — MOVES A LEG'S HEAD onto the branch it places the
# leg on: `git checkout <branch>` in a leg whose local <branch> tip equals the
# pin, `checkout -b <branch> <pin>` in a leg that has no local one and, since
# openRepoShape v1.1.0, a fast-forward of a <branch> BEHIND the pin to the pin
# and a checkout of it — from that branch, from a HEAD detached at the pin, or
# from ANOTHER branch (opensoft/openRepoShape#475, and opensoft/openRepoShape#603
# → opensoft/openRepoShape#615) — and a move of a <branch> AHEAD of the pin,
# with no commit of its own, back to the pin from a HEAD detached at it. A leg
# sitting on a clean feature branch at the pin makes the superproject clean,
# so the dirty and off-branch guards above see nothing and say nothing — and
# `resume` used to walk that leg off `001-my-feature` onto `main` and exit 0.
#
# So the legs are asked first, and a root with one off its tracking branch is
# REFUSED BY NAME and neither pulled nor bootstrapped. DETACHED AT THE PIN is
# not a refusal: that is the state a fresh clone arrives in and precisely what
# bootstrap exists to fix.
# WHICH <branch> IS THE LEG'S OWN (#235). `bootstrap.py` places a leg on
# `str(leg.get("branch") or branch)`: the `branch:` its row in the root's
# `project.yaml` declares for that leg when it declares one, and the root's
# `tracking_branch` ONLY when the row has none (`leg_branch_of`, then
# `tracking_branch_of`, above). Every leg held to the ROOT's branch lets a leg
# declaring `branch: develop` be moved onto `develop` while `resume` exits 0,
# and refuses a leg that is already on `develop`.
#
# So the legs are asked first, and a root with one off the branch bootstrap
# would put it on is REFUSED BY NAME and neither pulled nor bootstrapped,
# whichever of the two that branch comes from, and the refusal says which.
# DETACHED AT THE PIN is not a refusal: that is the state a fresh clone arrives
# in and precisely what bootstrap exists to fix.
root_legs_ok() {
local label="$1" root="$2" branch leg head pin at
local label="$1" root="$2" branch leg want kind from head pin at
branch="$(tracking_branch_of "$root")"
while IFS= read -r leg; do
[ -n "$leg" ] || continue
[ -e "$root/$leg/.git" ] || continue
want="$(leg_branch_of "$root" "$leg")"
if [ -n "$want" ]; then
kind="its own branch"
from="the \`branch:\` of its row in project.yaml"
else
want="$branch"
kind="its tracking branch"
from="the root's \`tracking_branch\`"
fi
head="$(git -C "$root/$leg" rev-parse --abbrev-ref HEAD 2>/dev/null || printf '?')"
[ "$head" != "$branch" ] || continue
[ "$head" != "$want" ] || continue
if [ "$head" = "HEAD" ]; then
pin="$(git -C "$root" rev-parse "HEAD:$leg" 2>/dev/null || true)"
at="$(git -C "$root/$leg" rev-parse HEAD 2>/dev/null || true)"
Expand All @@ -706,10 +792,10 @@ root_legs_ok() {
head="detached at ${at:-?}, and the pin is ${pin:-?}"
fi
REFUSED=$((REFUSED + 1))
say " REFUSED $label: leg $leg is on '$head', not its tracking branch '$branch';"
say " REFUSED $label: leg $leg is on '$head', not $kind '$want' ($from);"
say " \`make bootstrap\` would move it - nothing was touched."
say " look: git -C $root/$leg status --short --branch"
say " then: finish or park that leg's work, put it back on $branch, and re-run."
say " then: finish or park that leg's work, put it back on $want, and re-run."
return 1
done < <(legs_of "$root")
return 0
Expand Down
Loading
Loading