diff --git a/.agents/shared b/.agents/shared index fefe0ed..017c8d3 160000 --- a/.agents/shared +++ b/.agents/shared @@ -1 +1 @@ -Subproject commit fefe0ed0bf47066b4b50eb01c2823a18dd2e9702 +Subproject commit 017c8d38ff02352de1c5a3a4e00f09a65b04d074 diff --git a/.claude/settings.json b/.claude/settings.json index 337c904..d386d14 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -11,9 +11,11 @@ "Bash(git switch:*)", "Bash(git checkout:*)", "Bash(git add:*)", + "Bash(git commit:*)", "Bash(git restore:*)", "Bash(git stash:*)", "Bash(git fetch:*)", + "Bash(git push:*)", "Bash(git rev-parse:*)", "Bash(git ls-files:*)", "Bash(git mv:*)", @@ -36,7 +38,6 @@ "Skill(pre-pr:*)" ], "deny": [ - "Bash(git push:*)", "Bash(git reset --hard:*)", "Bash(git clean -fdx:*)", "Bash(rm -rf /:*)", @@ -45,7 +46,15 @@ "Bash(gh release create:*)" ], "ask": [ - "Bash(git commit:*)", + "Bash(git push --force*)", + "Bash(git push * --force*)", + "Bash(git push -f*)", + "Bash(git push * -f*)", + "Bash(git push -uf*)", + "Bash(git push * -uf*)", + "Bash(git push * +*)", + "Bash(git push --mirror*)", + "Bash(git push * --mirror*)", "Bash(git rebase:*)", "Bash(git merge:*)", "Bash(git cherry-pick:*)" diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index 91fa52c..3f9b156 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -18,6 +18,8 @@ env: # SHA256 of the above tarball, pinned at download time. Update alongside # LYCHEE_VERSION_TAG whenever the binary is upgraded. LYCHEE_SHA256: "1f4e0ef7f6554a6ed33dd7ac144fb2e1bbed98598e7af973042fc5cd43951c9a" + # Pinned to the release the Spine sites were checked with. Newer releases + # add Sass deprecations, which nobody has checked against those sites yet. DART_SASS_VERSION: 1.99.0 # SHA256 of `dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz`, from the GitHub release. # Update alongside DART_SASS_VERSION. @@ -83,6 +85,13 @@ jobs: hugo-version: ${{ env.HUGO_VERSION }} extended: true + # The Spine Hugo theme compiles its SCSS with Dart Sass, because Hugo + # deprecated LibSass in v0.153.0 and will remove it. Hugo does not + # bundle Dart Sass: it runs the `sass` binary it finds on `PATH`, and + # a site on that theme fails to build without one. The release + # tarball is standalone, so no Node or Dart setup is needed. The + # checksum guards the download, as it does for Lychee below. Sites + # still on LibSass ignore the binary. - name: Install Dart Sass if: steps.docs.outputs.present == 'true' run: | @@ -94,6 +103,9 @@ jobs: echo "${RUNNER_TEMP}/dart-sass" >> "${GITHUB_PATH}" # Hugo finds Dart Sass on `PATH`. `hugo env` lists it when it does. + # `GITHUB_PATH` only takes effect from the next step on, so this check + # cannot live in the install step above. It fails here, rather than + # mid-build, if the install did not put `sass` where Hugo looks. - name: Check Dart Sass if: steps.docs.outputs.present == 'true' run: hugo env | grep 'github.com/sass/dart-sass' diff --git a/.lycheeignore b/.lycheeignore new file mode 100644 index 0000000..16e4602 --- /dev/null +++ b/.lycheeignore @@ -0,0 +1,17 @@ +# Lychee skips the URLs matching these patterns. It reads this file from the +# directory it runs in, the repository root. `lychee.toml` comes from `config` +# and `./config/pull` overwrites it, so links specific to this site go here. +# +# These links are broken in the docs of `SpineEventEngine/validation`, which +# `docs/hugo.toml` imports as a module, so they can only be fixed there. +# Remove a pattern once the module update that fixes its link lands here. +# See https://github.com/SpineEventEngine/documentation/issues/97. +^https://github\.com/SpineEventEngine/validation/blob/master/\.agents/running-builds\.md$ +^https://github\.com/SpineEventEngine/validation/blob/master/\.agents/documentation-guidelines\.md$ +^https://github\.com/SpineEventEngine/embed-code-go/blob/master/EMBEDDING\.md$ +^https://github\.com/SpineEventEngine/embed-code-go/tree/master/bin$ +^https://plugins\.gradle\.org/plugin/io\.spine\.validation$ +# Relative links that resolve outside the module's pages. +^http://localhost:[0-9]+/06-developers-guide/$ +^http://localhost:[0-9]+/developer/$ +^http://localhost:[0-9]+/typical_custom_option\.jpg$ diff --git a/AGENTS.md b/AGENTS.md index 8c5f619..dc6927f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,17 +20,29 @@ Shared skills, scripts, and guidelines come from the `.agents/shared` submodule (and some shallow clones / cloud checkouts) start with NO submodules checked out, so those symlinks dangle and no skills are found. Bootstrap such a tree with **`./init-submodules`** — a root script that materializes the missing -*config-managed* submodules at their pinned commits: `config` itself, plus every -submodule that declares a tracked `branch` in `.gitmodules` (`.agents/shared`, and -any shared submodule added later) — the same rule `./config/pull` uses to decide -what it floats. Submodules the consumer owns (a Hugo theme, a vendored library, -doc-example submodules, …) declare no tracked branch and are left untouched, so the -automatic `SessionStart` run never tries to clone — or fail on credentials for — a -submodule this project does not manage. It depends on no pre-existing `config` -submodule, so it works before `./config/pull` (which lives inside the `config` -submodule) can. Claude Code runs it automatically via a `SessionStart` hook; other -agents and humans run it by hand, then `./config/pull` to float the shared submodules -to their branch tips. +*config-managed* submodules at the tip of their branch (`master` for `config`, the +tracked `branch` for the rest): `config` itself, plus every submodule that declares +a tracked `branch` in `.gitmodules` (`.agents/shared`, and any shared submodule +added later) — the same rule `./config/pull` uses to decide what it floats. It +deliberately does not stop at the commit the superproject pins for such a +submodule: nothing keeps that pin current — `ignore = all` hides the submodule from +`git status`, and `./config/pull` floats the on-disk copy without committing the +moved gitlink — so materializing the pin as-is means months-old hook scripts, +skills, and guidelines. If the float fails — a branch the remote does not have, +say — a warning is printed and the submodule is left wherever the steps that +did succeed put it, at worst the pin, so a session still starts with a usable +tree; it is not retried on a later run, which is why that warning points at +`./config/pull`. With no network at all the clone itself fails and the +submodule is left uninitialized, as it was before. Submodules the consumer owns +(a Hugo theme, a vendored library, doc-example submodules, …) declare no tracked +branch and are left untouched, so the automatic `SessionStart` run never tries +to clone — or fail on credentials for — a submodule this project does not +manage. A submodule that is already checked out is left exactly as it is, which +keeps repeated runs cheap and offline-safe and never rewinds a tree someone +parked on a particular commit; run `./config/pull` to float those. It depends on +no pre-existing `config` submodule, so it works before `./config/pull` (which +lives inside the `config` submodule) can. Claude Code runs it automatically via +a `SessionStart` hook; other agents and humans run it by hand. ## Commit and history safety diff --git a/config b/config index 94a9e08..f5a8d40 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 94a9e08b70625d251dc3ecc0b7241d557a4b9586 +Subproject commit f5a8d4093cd9d54b39a13f4da61c2932cc12a909 diff --git a/docs/_preview/go.mod b/docs/_preview/go.mod index 6d9077c..4d64e36 100644 --- a/docs/_preview/go.mod +++ b/docs/_preview/go.mod @@ -3,7 +3,7 @@ module github.com/SpineEventEngine/documentation/docs/_preview go 1.27.3 require ( - github.com/SpineEventEngine/site-commons v0.0.0-20261030153256-b79100d8a139 // indirect + github.com/SpineEventEngine/site-commons v0.0.0-20261030172811-9216e48eb656 // indirect github.com/SpineEventEngine/validation/docs v0.0.0-20260902132507-6465029c83b9 // indirect github.com/gohugoio/hugo-mod-bootstrap-scss/v5 v5.20300.20800 // indirect github.com/gohugoio/hugo-mod-jslibs-dist/popperjs/v2 v2.21100.20000 // indirect diff --git a/docs/_preview/go.sum b/docs/_preview/go.sum index 3155fa9..84161e8 100644 --- a/docs/_preview/go.sum +++ b/docs/_preview/go.sum @@ -1,7 +1,5 @@ -github.com/SpineEventEngine/site-commons v0.0.0-20261001182820-d2b63aedd0f4 h1:PoGISjoFRXwKpWwG/oTaGuSG6qF3I3THV2p+/r7qgk0= -github.com/SpineEventEngine/site-commons v0.0.0-20261001182820-d2b63aedd0f4/go.mod h1:tkAl4StIREKmz9r5PiJtuDhvwMMkFXKWcaTyxhIikho= -github.com/SpineEventEngine/site-commons v0.0.0-20261030153256-b79100d8a139 h1:DOYFVaEw9v3dML+40FysPUaTczyqK5+LqWJZQkK4Hs0= -github.com/SpineEventEngine/site-commons v0.0.0-20261030153256-b79100d8a139/go.mod h1:TfFFABXgPop1uc4eBdfWZF0Es06TGmNHfReiOzkZqrk= +github.com/SpineEventEngine/site-commons v0.0.0-20261030172811-9216e48eb656 h1:9H15KSiUrQP3U/uhDfVgSXMwqS7ehUppeqtQzRxR6LE= +github.com/SpineEventEngine/site-commons v0.0.0-20261030172811-9216e48eb656/go.mod h1:TfFFABXgPop1uc4eBdfWZF0Es06TGmNHfReiOzkZqrk= github.com/SpineEventEngine/validation/docs v0.0.0-20260205202311-181ba8844107 h1:2DHRIwwLudP6l3Kh7Nd4KR4uXcsDtIAXE9adW68ivME= github.com/SpineEventEngine/validation/docs v0.0.0-20260205202311-181ba8844107/go.mod h1:STHyjXejVvPmfrxujfDvhofmjg55mMk+fwI3TVL0b4Y= github.com/SpineEventEngine/validation/docs v0.0.0-20260522175555-cf09b4c706ea h1:YEMSlr5KJXkZdK7epMSnN+6HSr2K41n8O3c8OhtF8pM= diff --git a/docs/content/docs/guides/validation.md b/docs/content/docs/guides/validation.md index 9c1a24c..5436144 100755 --- a/docs/content/docs/guides/validation.md +++ b/docs/content/docs/guides/validation.md @@ -91,7 +91,7 @@ represented by required fields of an entity state, a Command, an Event, etc. {{% note-block class="note" %}} Protobuf 2 used to have native support for required fields. However, from the serialization perspective, that proved to be -a [design mistake](https://stackoverflow.com/a/31814967/3183076). If a required field was missing, +a [design mistake](https://stackoverflow.com/questions/31801257/why-were-required-and-optional-removed-in-protocol-buffers-3/31814967#31814967). If a required field was missing, the message could not be serialized and sent over the wire. Also, it is often too easy to add a new required field, thereby breaking backwards-compatibility of the message type. In Protobuf 3 all the fields are optional. diff --git a/init-submodules b/init-submodules index 2ae143d..64bb085 100755 --- a/init-submodules +++ b/init-submodules @@ -17,10 +17,10 @@ # (distributed by `config`), so `git worktree add` always checks it out — it can # therefore bring `config` itself into existence. # -# It initializes ONLY submodules that are BOTH: +# It acts ONLY on submodules that are BOTH: # -# * not yet checked out — those `git submodule status` marks with a leading `-`, -# at the commit the branch pins; and +# * not yet checked out — those `git submodule status` marks with a leading +# `-`; and # # * config-managed — `config` itself (the bootstrap target `pull` lives inside, # which carries no tracked `branch` in a consumer's `.gitmodules`), plus every @@ -28,6 +28,33 @@ # the rule `./config/pull` uses to decide what it floats, so the two scripts # can never disagree about what is shared. # +# Such a submodule is initialized and then advanced to the tip of its tracked +# branch, the way `./config/pull` floats it — NOT left at the commit the +# superproject pins. That pin is not a version anyone maintains: `.gitmodules` +# declares `ignore = all` for `.agents/shared`, which hides the submodule from +# `git status` and `git diff`, and `./config/pull` floats the on-disk copy without +# ever committing the moved gitlink. The pin therefore still holds the value it +# had on the day the submodule was adopted; an audit of all 19 Spine consumer +# repositories on 2026-10-16 found every one of them months stale. Materializing +# it hands a fresh worktree, CI checkout, or cloud session hook scripts, skills, +# and guidelines that are months out of date. +# +# A submodule may declare `branch = .`, which gitmodules(5) defines as the branch +# the superproject is on rather than a branch name. It is resolved before it +# reaches a refspec, because `git fetch origin .` is a `fatal: invalid refspec`. +# Nothing in Spine declares it today — `.agents/shared` tracks `agents`' `master` +# whatever branch the consumer is on — but it is valid in any `.gitmodules`. +# +# Neither the clone nor the float can fail the session start. With no network at +# all the clone is what fails: the submodule is left uninitialized and the +# symlinks keep dangling until a run that has a network, exactly as before this +# script floated anything. A float that fails after a successful clone — a branch +# the remote does not have, a fetch that dies midway, a local branch that is not +# a fast-forward — warns and leaves the submodule wherever the steps that did +# succeed put it: at worst the pin, a usable, if older, tree. The submodule is +# checked out by then, so later runs no longer see a `-` line for it and never +# retry the float — which is why the warning points at `./config/pull`. +# # Consumer-owned submodules (a Hugo theme, a vendored library, documentation # examples, ...) declare no tracked branch and are deliberately left untouched. # Because a `SessionStart` hook runs this script automatically on every session, @@ -35,39 +62,126 @@ # a submodule this project does not manage, on every single start. They are # skipped (noted on stderr). # -# Submodules already present are left exactly as they are, so a tree that floated -# `config` / `.agents/shared` to a branch tip via `./config/pull` is never -# silently rewound to the pin. That makes the script idempotent and safe to run on -# every session start. +# Submodules already present are left exactly as they are, neither rewound to the +# pin nor advanced. That keeps the script idempotent, keeps the runs that find +# nothing missing free of network access, and never disturbs a tree whose +# submodules someone deliberately parked on a particular commit. Run +# `./config/pull` to float those. # -# It does NOT float submodules to their branch tips — run `./config/pull` -# afterwards for that. Unlike `pull`, it depends on no pre-existing `config` -# submodule, so it can bootstrap a bare worktree where `./config/pull` does not -# yet exist. +# Unlike `pull`, this script depends on no pre-existing `config` submodule, so it +# can bootstrap a bare worktree where `./config/pull` does not yet exist. # ################################################################################ set -u -root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0 +# Ignore an inherited `CDPATH`: it would redirect a relative `cd`, such as into +# the `.git` that `git rev-parse --git-common-dir` prints in a main checkout, +# and make `cd` print the directory it enters. +unset CDPATH + +# Decode the JSON string escapes that a path can hold: `\\` (each backslash of a +# Windows path), `\"`, and `\/`. Fail on any other escape, such as `\n`. +json_unescape() { + local rest=$1 out="" next + while :; do + case $rest in + *\\*) ;; + *) printf '%s' "$out$rest"; return 0 ;; + esac + out=$out${rest%%\\*} + rest=${rest#*\\} + next=${rest%"${rest#?}"} + case $next in + \\ | \" | /) out=$out$next ;; + *) return 1 ;; + esac + rest=${rest#?} + done +} + +# Act on the work tree the session is in. Claude Code runs this script as a +# `SessionStart` hook and passes the hook input JSON on stdin. The `cwd` field +# of that JSON is the session's working directory. In a worktree session, that +# directory is inside the worktree, while this process and `$CLAUDE_PROJECT_DIR` +# stay in the main checkout. Read stdin only when Claude Code started the script +# (it exports `CLAUDE_PROJECT_DIR` to hooks) and stdin is not a terminal, so that +# a manual run never waits for input. +# +# Parse the input with `jq` when it is installed. The bootstrap cannot require +# `jq`, so without it a pattern match reads the first `cwd` member, and +# `json_unescape` decodes it; a value it cannot decode falls back as described +# below. Unlike `jq`, the pattern does not validate the rest of the input, which +# Claude Code writes. Either way, the path must belong to the project, as checked +# below. +session_dir="" +if [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ ! -t 0 ]; then + if command -v jq >/dev/null 2>&1; then + session_dir=$(jq -r '.cwd // empty' 2>/dev/null) || session_dir="" + else + input=$(cat) + cwd_pattern='"cwd"[[:space:]]*:[[:space:]]*"(([^"\\]|\\.)*)"' + if [[ $input =~ $cwd_pattern ]]; then + session_dir=$(json_unescape "${BASH_REMATCH[1]}") || session_dir="" + fi + fi +fi + +# The physical path of the Git directory that a repository's main checkout and +# linked worktrees share; empty outside a work tree. Each `git` result is checked +# before use, because `cd ""` succeeds. +common_git_dir() { + (cd "$1" && top=$(git rev-parse --show-toplevel) && cd "$top" \ + && common=$(git rev-parse --git-common-dir) && cd "$common" && pwd -P) 2>/dev/null +} + +# From the session directory, climb out of any submodule (`config`, +# `.agents/shared`) to the first work tree of the project's own repository — its +# main checkout or a linked worktree. Anything else (no `cwd`, one that does not +# resolve, an unrelated repository) falls back to the current directory, as +# before: this script changes repository configuration, so it must not act on a +# repository the project does not own. +root="" +if [ -n "$session_dir" ]; then + project=$(common_git_dir "$CLAUDE_PROJECT_DIR") + tree=$(git -C "$session_dir" rev-parse --show-toplevel 2>/dev/null) + while [ -n "$tree" ] && [ -n "$project" ]; do + if [ "$(common_git_dir "$tree")" = "$project" ]; then + root=$tree + break + fi + tree=$(git -C "$tree" rev-parse --show-superproject-working-tree 2>/dev/null) + done +fi +[ -n "$root" ] || root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0 cd "$root" || exit 0 # Nothing to do in a repo without submodules. [ -f .gitmodules ] || exit 0 -# The set of config-managed submodule paths: `config` itself (handled specially — -# it carries no tracked branch, exactly as in `./config/pull`), plus every -# submodule declaring a tracked `branch` in `.gitmodules`. Mirrors `pull`'s rule. -config_managed_paths() { - printf '%s\n' 'config' +# The config-managed submodules, as `pathbranch` lines: every submodule that +# declares a tracked `branch` in `.gitmodules`, plus `config` itself — which +# carries no tracked branch in a consumer's `.gitmodules`, and which +# `./config/pull` floats to `master` in its step 1. Mirrors `pull`'s rule. The +# declared entries come first, so a `.gitmodules` that does give `config` a +# tracked branch wins over that default. +config_managed() { git config -f .gitmodules --get-regexp '^submodule\..*\.branch$' 2>/dev/null \ - | while read -r key _branch; do + | while read -r key branch; do name=${key#submodule.}; name=${name%.branch} - git config -f .gitmodules --get "submodule.$name.path" 2>/dev/null + path=$(git config -f .gitmodules --get "submodule.$name.path" 2>/dev/null) || continue + printf '%s\t%s\n' "$path" "$branch" done + printf '%s\t%s\n' 'config' 'master' } -managed=$(config_managed_paths | sort -u) +managed=$(config_managed) + +# The branch the submodule at `$1` tracks; empty when that path is not +# config-managed at all. The first match wins, as noted above. +tracked_branch() { + printf '%s\n' "$managed" | awk -F'\t' -v path="$1" '$1 == path { print $2; exit }' +} # `git submodule status` prefixes each uninitialized submodule with `-`; an # initialized one starts with a space (at the pinned commit) or `+` (ahead of it). @@ -75,13 +189,97 @@ managed=$(config_managed_paths | sort -u) # that path is config-managed. git submodule status 2>/dev/null | awk '$1 ~ /^-/ { print $2 }' | while read -r path; do [ -n "$path" ] || continue - if printf '%s\n' "$managed" | grep -qxF -- "$path"; then - echo "init-submodules: initializing '$path'" - git submodule update --init --recursive -- "$path" \ - || echo "init-submodules: WARNING — could not initialize '$path' (offline?)." >&2 - else + branch=$(tracked_branch "$path") + if [ -z "$branch" ]; then echo "init-submodules: skipping consumer-owned '$path' (not config-managed)." >&2 + continue + fi + echo "init-submodules: initializing '$path'" + git submodule update --init --recursive -- "$path" &2 + continue + } + # `.gitmodules` may declare `branch = .`, which gitmodules(5) defines as the + # branch the superproject itself is on. That is a sentinel, not a ref: passed + # through, it dies with `fatal: invalid refspec '.'`, and because the clone + # has already succeeded by then the submodule would keep the pin and never be + # retried. Resolve it to the current branch name. A detached superproject has + # no name to inherit, so the float is skipped and the pin kept — the one case + # where `./config/pull` is the only way forward, once on a branch. + if [ "$branch" = "." ]; then + # `git branch --show-current` prints the name exactly, and prints nothing + # when `HEAD` is detached. `git rev-parse --abbrev-ref HEAD` cannot stand + # in: it shortens to a *non-ambiguous* name, so a superproject carrying + # both a branch and a tag called `foo` yields `heads/foo`; the fetch + # then succeeds but the checkout fails, leaving this freshly + # initialized submodule stale and never retried. `./config/pull` + # resolves it the same way, for the same reason. + branch=$(git branch --show-current 2>/dev/null) + if [ -z "$branch" ]; then + echo "init-submodules: WARNING — '$path' tracks the superproject's" \ + "branch ('.'), which is detached here; left at the pin. Run" \ + "'./config/pull' from a branch to float it." >&2 + continue + fi fi + # What that just checked out is the superproject's pin, which nobody + # maintains (see the header). Advance the fresh checkout to the tip of its + # tracked branch, leaving it ON the branch rather than in a detached `HEAD` — + # the same three steps `./config/pull` takes in its step 2, both scripts + # naming the fetch destination and the checkout start point, for the two + # reasons below. A failure leaves the submodule wherever the steps that did + # succeed put it, so the warning promises no particular commit, as `pull`'s + # does not. + # + # The fetch names its destination. A bare `git fetch origin ` relies + # on the refspec configured for the remote to update + # `refs/remotes/origin/` opportunistically as well, which a full + # clone's does. But a submodule declaring `shallow = true` in `.gitmodules` + # is cloned single-branch (a shallow clone implies `--single-branch`), as + # `+refs/heads/:refs/remotes/origin/`, so any other tracked + # branch lands in `FETCH_HEAD` alone, `refs/remotes/origin/` never + # appears, and the checkout below fails. Spelling the refspec out makes the + # float work whatever refspec the clone carries, and the leading `+` matches + # what a default clone configures. + # + # The checkout asks whether the branch exists locally rather than letting Git + # work it out, because both answers are otherwise wrong in a reachable case. + # A fresh clone has a local branch only for the default branch, so `-b` alone + # would fail with `already exists` on the common path. And plain + # `git checkout ` is not a reliable substitute: with no local branch + # of that name it guesses one from a remote-tracking ref, considering only + # refs the remote's fetch refspec covers — so in the single-branch clone + # above it fails even though the explicit fetch did create + # `refs/remotes/origin/` — and if the submodule carries a *tag* of + # that name it quietly resolves the tag and detaches `HEAD` instead, which + # the `--ff-only` below then fast-forwards to the right commit on no branch + # at all. Naming the start point avoids both. + # + # `merge --ff-only` is kept after either path: a local branch that has + # diverged from `origin/` is reported rather than overwritten. + # Nothing here force-moves a ref except the remote-tracking ref the fetch + # owns, which a default clone force-updates anyway. + # + # It merges `refs/remotes/origin/`, not the `origin/` + # shorthand, for the reason the checkout above spells a ref out too. + # gitrevisions(7) resolves a bare name through `refs/tags/` BEFORE + # `refs/remotes/`, so a tag literally named `origin/` wins the + # shorthand — and then this merge would fast-forward the branch the + # checkout just placed exactly onto whatever that tag points at, reporting + # success. Every ref in the chain is therefore fully qualified. + echo "init-submodules: floating '$path' -> origin/$branch" + ( cd "$path" \ + && git fetch --quiet origin \ + "+refs/heads/$branch:refs/remotes/origin/$branch" \ + && if git show-ref --verify --quiet "refs/heads/$branch"; then \ + git checkout --quiet "$branch"; \ + else \ + git checkout --quiet -b "$branch" \ + "refs/remotes/origin/$branch"; \ + fi \ + && git merge --quiet --ff-only "refs/remotes/origin/$branch" ) &2 done # Route Git hooks to the shared hooks directory so the secret-scan `pre-commit`