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
12 changes: 6 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,13 @@ scripts/bootstrap.py`. README's "What setup.sh does" carries the exact commands.
## Adopting an existing repository

`adopt-project.py` converts a repository that already exists, IN PLACE: it keeps
its name, its identity and its full history and becomes the assembly root, and
the two legs are new repositories extracted with `git filter-repo` (a hard
dependency: `execute` refuses without it, printing the install command). Seven
steps, in order, and step 4 is the one you must not skip.
its name, which `--project` must be, its identity and its full history and
becomes the assembly root, and the two legs are new repositories extracted with
`git filter-repo` (a hard dependency: `execute` refuses without it, printing the
install command). Seven steps, in order, and step 4 is the one you must not skip.

```sh
./adopt-project.py plan --source <org/repo|path> --project <Project>
./adopt-project.py plan --source <org/repo|path> --project <its own name>
# 2. answer every `review_required: true` entry IN THE PLAN
./adopt-project.py check --plan adoption-plan.yaml
# 4. show the human the plan and the follow-ups; get an explicit yes
Expand Down Expand Up @@ -167,7 +167,7 @@ history, not an empty repository. `plan` and `check` both WARN; `check` still
passes, because a spec-only repository is a legitimate thing to adopt.

```sh
./adopt-project.py plan --source <org>/<Repo> --project <Project> \
./adopt-project.py plan --source <org>/<Repo> --project <Repo> \
--allow-empty-leg code
```

Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -727,14 +727,14 @@ it satisfies no leg form to be admitted into, and is still refused as any leg.

## Adopting an existing repository

A repository that already exists is converted **in place**, by
`adopt-project.py`, and this is a ruling (**Brett Heap, 2026-09-02**): it KEEPS
ITS NAME, ITS IDENTITY AND ITS FULL HISTORY and becomes the assembly root, while
`<Project>-spec` and `<Project>-code` are NEW repositories extracted with
history-preserving filters. The source is never deleted, never renamed and never
force-pushed; the only change to its history is ONE split commit, arriving on a
branch by pull request — the organisations this serves are pull-request only,
and a tool needing a bypass cannot be used where it is needed.
A repository that already exists is converted **in place**, by `adopt-project.py`,
and this is a ruling (**Brett Heap, 2026-09-02**): it KEEPS ITS NAME, which
`--project` must be, ITS IDENTITY AND ITS FULL HISTORY and becomes the assembly
root, while `<Project>-spec` and `<Project>-code` are NEW repositories extracted
with history-preserving filters. The source is never deleted, never renamed and
never force-pushed; the only change to its history is ONE split commit, arriving
on a branch by pull request — the organisations this serves are pull-request
only, and a tool needing a bypass cannot be used where it is needed.

```sh
./adopt-project.py plan --source MedxSoft/MedxEHR --project MedxEHR
Expand Down
260 changes: 255 additions & 5 deletions adopt-project.py

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions docs/handbook.html
Original file line number Diff line number Diff line change
Expand Up @@ -1168,7 +1168,7 @@ <h2>The three legs, and naming</h2>
<p class="eyebrow">Reference</p>
<h2>Adopting an existing repository</h2>
<p>A repository that already exists is converted <strong>in place</strong>. It KEEPS ITS NAME,
ITS IDENTITY AND ITS FULL HISTORY and becomes the assembly root, while
which <code>--project</code> must be, ITS IDENTITY AND ITS FULL HISTORY and becomes the assembly root, while
<code>&lt;Project&gt;-spec</code> and <code>&lt;Project&gt;-code</code> are NEW repositories
extracted with history-preserving filters. The source is never deleted, never renamed and
never force-pushed; the only change to its history is ONE split commit, arriving on a branch
Expand Down Expand Up @@ -1878,9 +1878,10 @@ <h2>For AI assistants</h2>
own seed, and the one ruleset exit that re-run is, added (#462); <code>family.py</code>
<code>add</code>, <code>bump</code> and <code>remove</code> committing on a branch made first,
never the tracking branch, and printing the push of it, added (#487); the installed doctor
keeping its clone of the standard, which its NEXT lines run from, added (#500) &mdash; a page
cannot name the commit that contains it, and the blob below is the fact a test checks.
README.md blob 8527950a3e47a4b6d87dababe6886155432d5c87. Every command, flag, exit code
keeping its clone of the standard, which its NEXT lines run from, added (#500); an
adoption&rsquo;s <code>--project</code> as the source&rsquo;s own name, added (#499) &mdash; a
page cannot name the commit that contains it, and the blob below is the fact a test checks.
README.md blob 6bf94d583bb15fc6196c1e0c5232281edc723885. Every command, flag, exit code
and file name on this page is quoted from that README and from AGENTS.md.</p>
</footer>
</main>
Expand Down
17 changes: 10 additions & 7 deletions tests/test_adopt_git_reads.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,14 @@ def by_git_config(base: Path, source: Path, extra: dict,
str(base / WORK), *flags, extra=extra)


def without_org(base: Path, source: Path) -> subprocess.CompletedProcess:
def without_org(base: Path, source: Path,
*flags: str) -> subprocess.CompletedProcess:
"""`plan` as `planned` runs it, but with no --org, so the org is what
the source's `origin` names."""
the source's `origin` names, and with `flags` after it."""
return adopted("plan", "--source", str(source), "--project", PROJECT,
"--elected-by", "Test Human", "--elected-on", "2026-09-02",
"--out", str(base / PLAN_FILE), "--work-dir",
str(base / WORK))
str(base / WORK), *flags)


def command_scope(key: str, value: bytes) -> dict:
Expand Down Expand Up @@ -268,9 +269,11 @@ def test_plan_reads_an_origin_that_is_not_utf8_as_none(tmp_path, url,
"""The issue's last row, with --org: on the base `plan` ended in a
`UnicodeDecodeError` traceback from `_remote_repository`, exit 1, and
wrote no plan. Now it plans, and records no repository. The ASCII URL
is the control: its `org/repo` is read, so the None is the byte's."""
is the control: its `org/repo` is read, so the None is the byte's, and
the plan is written in that name, which `plan` refuses any other for
(#499)."""
source = with_origin(make_source_repo(tmp_path / "Thing"), url)
done = planned(tmp_path, source)
done = planned(tmp_path, source, "--org", "cafe", "--project", "repo")
assert_no_traceback({"plan": done})
assert done.returncode == 0, said(done)
assert load_yaml(tmp_path / PLAN_FILE)["source"]["repository"] \
Expand Down Expand Up @@ -319,9 +322,9 @@ def test_plan_refuses_an_unsafe_org_that_is_passed(tmp_path, org):

def test_plan_without_org_takes_the_org_an_ascii_origin_names(tmp_path):
"""Control: an origin that names `cafe/repo` is the org, as on the
base."""
base, for a plan in the source's own name (#499)."""
source = with_origin(make_source_repo(tmp_path / "Thing"), ASCII_URL)
done = without_org(tmp_path, source)
done = without_org(tmp_path, source, "--project", "repo")
assert_no_traceback({"plan": done})
assert done.returncode == 0, said(done)
assert load_yaml(tmp_path / PLAN_FILE)["org"] == "cafe"
Expand Down
10 changes: 6 additions & 4 deletions tests/test_adopt_late_failure.py
Original file line number Diff line number Diff line change
Expand Up @@ -755,10 +755,12 @@ def test_an_org_repo_run_names_its_clone_when_a_leg_repository_fails(
refused at that clone (`source-unclonable`), but the notice read only
the legs and the split off the disk, so it named no work directory. It
names the clone. The run is not refused for its own clone: that is no
leg and not the split. `gh repo clone` is a local clone here."""
leg and not the split. `gh repo clone` is a local clone here. The plan
is in the source's own name, as `execute` refuses any other for an
`org/repo` source (#499)."""
no_filter_repo_probe(adopter, monkeypatch)
origin = make_source_repo(tmp_path / SOURCE, tree=SMALL_TREE, edits=())
plan = planned(tmp_path, origin)
plan = planned(tmp_path, origin, SOURCE)
real_run = adopter.run

def gh_clones_the_origin(args, *rest, **keywords):
Expand Down Expand Up @@ -1173,9 +1175,9 @@ def execute_args(tmp_path: Path, plan: Path) -> Namespace:
yes=True, work_dir=tmp_path / WORK)


def planned(tmp_path: Path, source: Path) -> Path:
def planned(tmp_path: Path, source: Path, project: str = ASSEMBLY) -> Path:
plan = tmp_path / "adoption-plan.yaml"
written = write_plan(source, plan, project=ASSEMBLY)
written = write_plan(source, plan, project=project)
assert written.returncode == 0, written.stderr + written.stdout
return plan

Expand Down
Loading
Loading