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
32 changes: 32 additions & 0 deletions Utilities/Maintenance/RemoteModuleIngest/INGESTION_STRATEGY.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,38 @@ whitelisted file.
**Topology requirement (mandatory).** Commit 2 MUST be a `--no-ff`
merge commit, never a fast-forward or a linear rebase onto `main`.

**All upstream-side merges must also survive.** If the source remote
module had `Merge pull request #N from …` commits in its history,
those merge commits MUST appear in the ingested history with both
parents reachable. Operator check before push:

```bash
[ "$(git rev-list --count --merges upstream/main..HEAD)" -ge \
$((1 + UPSTREAM_INTERNAL_MERGES)) ] \
|| { echo "FAIL: merge topology lost"; exit 1; }
```

**Linearization is forbidden.** Plain `git rebase`, plain
`git cherry-pick` of each upstream commit, and the deprecated
`normalize-ingest-commits.py` (which uses linear cherry-pick replay
and explicitly skips merges) all produce a linearized branch.
Reviewers have flagged linearization repeatedly (PRs #6135, #6137,
#6159, #6161); it is a process violation, not an acceptable
shortcut. When per-commit re-formatting is needed, use a
merge-preserving rewriter:

| Use | When |
|-----|------|
| `rewrite-history-merge-preserving.py` (this directory) | Standard ingest per-commit re-formatting (text-blob trailing-whitespace + end-of-file uniform pass) |
| `git rebase --rebase-merges -i` with `edit` on a single commit | One historical commit needs in-place edit |
| `git filter-repo --commit-callback` | Many commits need uniform path/mode rewrites (e.g. exec-bit drift) |
| `git filter-repo --blob-callback` | Per-file content rewrites across history |

**`normalize-ingest-commits.py` is DEPRECATED** as of 2026-04-28. It
will refuse to run unless invoked with `--i-understand-this-linearizes`.
Use `rewrite-history-merge-preserving.py` for per-commit
re-formatting on ingest branches.

### The root-commit ghostflow error is REQUIRED, not a failure

Every Mode A or Mode B ingest, when correctly built, MUST produce
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,34 @@ def replay(base: str, *, dry_run: bool, run_pre_commit: bool) -> int:
return 0


DEPRECATION_NOTICE = """
ERROR: normalize-ingest-commits.py is DEPRECATED as of 2026-04-28.

This script linearizes the ingest history (it skips merge commits
and replays only the non-merge parents via cherry-pick). The
resulting branch loses the upstream merge topology, which is a
process violation per
Utilities/Maintenance/RemoteModuleIngest/INGESTION_STRATEGY.md
("Topology requirement (mandatory).").

Linearization was applied to PRs #6135, #6137, #6159, and #6161
before the user repeatedly flagged it as unacceptable.

Use the merge-preserving replacement instead:
rewrite-history-merge-preserving.py

That replacement walks every commit (including merges) via
git filter-repo --blob-callback, applies trailing-whitespace +
end-of-file fixes uniformly to text blobs, and verifies the
rewritten tip matches a Phase-1 reference tree (upstream main with
current ITK pre-commit hooks applied once).

To override this guard for a specific case where you accept the
linearization (you almost certainly should not), pass
--i-understand-this-linearizes.
"""


def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
Expand All @@ -283,8 +311,21 @@ def main(argv: list[str] | None = None) -> int:
action="store_true",
help="Skip the per-commit pre-commit auto-fix pass (subject-only normalization)",
)
p.add_argument(
"--i-understand-this-linearizes",
action="store_true",
help="Acknowledge the linearization caveat (see deprecation notice). "
"Required to override the deprecation guard.",
)
args = p.parse_args(argv)

# Hard refuse-to-run guard (deprecation as of 2026-04-28).
# See INGESTION_STRATEGY.md "Linearization is forbidden" subsection
# and rewrite-history-merge-preserving.py for the replacement.
if not args.i_understand_this_linearizes:
sys.stderr.write(DEPRECATION_NOTICE)
return 3

# Sanity: clean working tree
if (
run(["git", "diff", "--quiet"]).returncode != 0
Expand Down
Loading
Loading