diff --git a/tests/e2e/README.md b/tests/e2e/README.md
index 075699a61..201a05a3b 100644
--- a/tests/e2e/README.md
+++ b/tests/e2e/README.md
@@ -23,3 +23,10 @@ They run as part of `./run_tests` by default, after the unit tests.
The suite is skipped automatically when `git http-backend` isn't
available, when git is older than 2.28 (need default branch support and
superproject support), or when not run from a git checkout.
+
+## Layout
+
+* `harness.py`: the git server, server-side repos & the repo client.
+* `test_init_sync.py`: core `repo init` & `repo sync` options.
+* `test_workflows.py`: multi-step workflows.
+* `test_manifest_regressions.py`: manifests that broke users in the past.
diff --git a/tests/e2e/harness.py b/tests/e2e/harness.py
index 0cd1d319a..29a162a57 100644
--- a/tests/e2e/harness.py
+++ b/tests/e2e/harness.py
@@ -49,6 +49,7 @@ from typing import (
Mapping,
NamedTuple,
Optional,
+ Set,
Tuple,
Union,
)
@@ -934,6 +935,24 @@ class RepoClient:
"""Return the commit checked out in `path`."""
return self.git(path, "rev-parse", "HEAD")
+ def remote_branches(self, path: str) -> Set[str]:
+ """Return the names of the `origin` remote-tracking branches."""
+ out = self.git(
+ path,
+ "for-each-ref",
+ "--format=%(refname:lstrip=3)",
+ "refs/remotes/origin/",
+ )
+ return set(out.split())
+
+ def checkouts(self) -> Set[str]:
+ """Return the names of the top-level dirs, except `.repo`."""
+ return {
+ x.name
+ for x in self.top.iterdir()
+ if x.is_dir() and x.name != ".repo"
+ }
+
def has_object(self, path: str, oid: str) -> bool:
"""Whether the checkout at `path` has object `oid` locally."""
# Unlike `cat-file -e`, this never lazily fetches missing objects in
diff --git a/tests/e2e/test_init_sync.py b/tests/e2e/test_init_sync.py
new file mode 100644
index 000000000..5fb1db100
--- /dev/null
+++ b/tests/e2e/test_init_sync.py
@@ -0,0 +1,227 @@
+# Copyright (C) 2026 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""E2E tests for the core `repo init` & `repo sync` options."""
+
+from typing import List, Set
+
+from harness import GIT_DEFAULTS_PARAMS
+from harness import GitHost
+from harness import manifest_xml
+from harness import RepoClient
+import pytest
+
+
+@pytest.mark.parametrize("git_defaults", GIT_DEFAULTS_PARAMS)
+def test_init_sync_basic(host: GitHost, client: RepoClient) -> None:
+ """The basic `repo init && repo sync` flow."""
+ foo = host.create_repo("platform/foo", {"foo.txt": "foo"})
+ host.create_repo("platform/bar", {"bar.txt": "bar"})
+ host.create_manifest(
+ manifest_xml(
+ '',
+ '',
+ )
+ )
+
+ client.init(host.manifest_url)
+ client.sync()
+
+ assert (client.top / "foo" / "foo.txt").read_text() == "foo"
+ assert (client.top / "sub" / "bar" / "bar.txt").read_text() == "bar"
+ assert client.head("foo") == foo.rev_parse("main")
+ assert client.list_projects() == {
+ "foo": "platform/foo",
+ "sub/bar": "platform/bar",
+ }
+ # The code under test is what got installed & is running.
+ assert "v99.0-e2e" in client.run("version").stdout
+
+
+@pytest.mark.parametrize(
+ "init_args,sync_args,manifest_refs,project_refs",
+ [
+ # `init -c` is the default for the manifest project.
+ ([], [], {"main"}, {"main", "other"}),
+ (["--no-current-branch"], [], {"main", "other"}, {"main", "other"}),
+ ([], ["-c"], {"main"}, {"main"}),
+ ],
+)
+def test_current_branch(
+ host: GitHost,
+ client: RepoClient,
+ init_args: List[str],
+ sync_args: List[str],
+ manifest_refs: Set[str],
+ project_refs: Set[str],
+) -> None:
+ """`init` limits the manifest fetch; `sync -c` limits projects."""
+ foo = host.create_repo("platform/foo", {"foo.txt": "main"})
+ foo.commit({"foo.txt": "other"}, branch="other")
+ manifest = host.create_manifest(
+ manifest_xml('')
+ )
+ manifest.commit({"README": "other"}, branch="other")
+
+ client.init(host.manifest_url, *init_args)
+ assert client.remote_branches(".repo/manifests") == manifest_refs
+
+ client.sync(*sync_args)
+ assert client.remote_branches("platform/foo") == project_refs
+ assert (client.top / "platform/foo/foo.txt").read_text() == "main"
+
+
+@pytest.mark.parametrize(
+ "sync_args,expect_bundle", [([], True), (["--no-clone-bundle"], False)]
+)
+def test_clone_bundle(
+ host: GitHost,
+ client: RepoClient,
+ sync_args: List[str],
+ expect_bundle: bool,
+) -> None:
+ """repo bootstraps new projects from clone.bundle when available."""
+ foo = host.create_repo("platform/foo", {"foo.txt": "foo"})
+ # A commit that only the bundle has, to prove the bundle was applied.
+ bundle_only = foo.commit({"x": "bundle"}, branch="bundle-only")
+ foo.write_bundle()
+ foo.git("update-ref", "-d", "refs/heads/bundle-only")
+ host.create_manifest(manifest_xml(''))
+
+ client.init(host.manifest_url)
+ client.sync(*sync_args)
+
+ bundle_gets = [
+ r
+ for r in host.requests()
+ if r.path.endswith("/platform/foo/clone.bundle")
+ ]
+ assert [r.status for r in bundle_gets] == ([200] if expect_bundle else [])
+ assert client.has_object("platform/foo", bundle_only) == expect_bundle
+ assert client.head("platform/foo") == foo.rev_parse("main")
+
+
+@pytest.mark.parametrize(
+ "init_args,project_attrs",
+ [
+ (["--depth=1"], ""),
+ ([], 'clone-depth="1"'),
+ ],
+)
+def test_shallow(
+ host: GitHost,
+ client: RepoClient,
+ init_args: List[str],
+ project_attrs: str,
+) -> None:
+ """Shallow clones via `--depth` and manifest `clone-depth`."""
+ foo = host.create_repo("platform/foo", {"foo.txt": "1"})
+ foo.commit({"foo.txt": "2"})
+ foo.commit({"foo.txt": "3"})
+ host.create_manifest(
+ manifest_xml(f'')
+ )
+
+ client.init(host.manifest_url, *init_args)
+ client.sync()
+
+ path = "platform/foo"
+ assert client.is_shallow(path)
+ assert client.git(path, "rev-list", "--count", "HEAD") == "1"
+ assert (client.top / path / "foo.txt").read_text() == "3"
+
+
+@pytest.mark.parametrize(
+ "init_args,expected",
+ [
+ ([], {"a", "b"}),
+ (["-g", "all"], {"a", "b", "c", "d"}),
+ (["-g", "g2"], {"c"}),
+ (["-g", "default,-g1"], {"a"}),
+ (["-g", "default,g2"], {"a", "b", "c"}),
+ (["-g", "name:platform/d"], {"d"}),
+ ],
+)
+def test_groups(
+ host: GitHost, client: RepoClient, init_args: List[str], expected: Set[str]
+) -> None:
+ """Manifest groups select which projects get synced."""
+ for name in "abcd":
+ host.create_repo(f"platform/{name}", {"f": name})
+ host.create_manifest(
+ manifest_xml(
+ '',
+ '',
+ '',
+ '',
+ )
+ )
+
+ client.init(host.manifest_url, *init_args)
+ client.sync()
+
+ assert set(client.list_projects()) == expected
+ assert client.checkouts() == expected
+
+
+def test_local_manifests(host: GitHost, client: RepoClient) -> None:
+ """.repo/local_manifests/ can add, remove & extend projects."""
+ foo = host.create_repo("platform/foo", {"f": "main"})
+ stable = foo.commit({"f": "stable"}, branch="stable")
+ host.create_repo("platform/bar", {"f": "bar"})
+ host.create_repo("vendor/extra", {"f": "extra"})
+ host.create_manifest(
+ manifest_xml(
+ '',
+ '',
+ )
+ )
+ client.init(host.manifest_url)
+ local = client.repodir / "local_manifests"
+ local.mkdir()
+ (local / "local.xml").write_text(
+ """
+
+
+
+
+
+"""
+ )
+
+ client.sync()
+
+ assert client.list_projects() == {
+ "extra": "vendor/extra",
+ "foo": "platform/foo",
+ }
+ assert client.head("foo") == stable
+ assert not (client.top / "bar").exists()
+
+
+def test_manifest_name(host: GitHost, client: RepoClient) -> None:
+ """`-m` selects a manifest file other than default.xml."""
+ host.create_repo("platform/foo", {"f": "foo"})
+ host.create_repo("platform/bar", {"f": "bar"})
+ host.create_manifest(
+ {
+ "default.xml": manifest_xml(''),
+ "other.xml": manifest_xml(''),
+ }
+ )
+
+ client.init(host.manifest_url, "-m", "other.xml")
+ client.sync()
+
+ assert client.list_projects() == {"platform/bar": "platform/bar"}
diff --git a/tests/e2e/test_manifest_regressions.py b/tests/e2e/test_manifest_regressions.py
new file mode 100644
index 000000000..dfc3ff506
--- /dev/null
+++ b/tests/e2e/test_manifest_regressions.py
@@ -0,0 +1,387 @@
+# Copyright (C) 2026 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""E2E regression tests for manifests that broke users in the past.
+
+Each test names the commit and/or bug that fixed the original breakage.
+"""
+
+import os
+from typing import List
+
+from harness import GitHost
+from harness import Gitlink
+from harness import manifest_xml
+from harness import RepoClient
+import pytest
+
+
+def test_empty_manifest(host: GitHost, client: RepoClient) -> None:
+ """A manifest without projects syncs (616e314, b/377546300)."""
+ host.create_manifest(manifest_xml())
+
+ client.init(host.manifest_url)
+ client.sync()
+
+ assert client.list_projects() == {}
+
+
+def test_local_extend_project_with_superproject(
+ host: GitHost, client: RepoClient
+) -> None:
+ """Local `extend-project` works with superproject overrides.
+
+ Regression test for 3c0e67b (b/470374343).
+ """
+ foo = host.create_repo("platform/foo", {"f": "v1"})
+ pinned = foo.rev_parse("main")
+ host.create_repo("superproject", {"foo": Gitlink(pinned)})
+ foo.commit({"f": "v2"})
+ host.create_manifest(
+ manifest_xml(
+ '',
+ '',
+ )
+ )
+ client.init(host.manifest_url, "--use-superproject")
+ local = client.repodir / "local_manifests"
+ local.mkdir()
+ (local / "local.xml").write_text(
+ "\n"
+ ' \n'
+ "\n"
+ )
+
+ client.sync("--use-superproject")
+
+ assert client.head("foo") == pinned
+
+
+# 3af9e2f fixed both the regular & the --optimized-fetch skip-fetch checks.
+@pytest.mark.parametrize("sync_args", [[], ["--optimized-fetch"]])
+def test_same_project_shallow_pinned_paths(
+ host: GitHost, client: RepoClient, sync_args: List[str]
+) -> None:
+ """One project at two paths pinned to SHAs with sync-c, one shallow.
+
+ The non-shallow path shares the shallow path's (depth-limited) object
+ store, so its fetch must not be skipped as already up to date, or its
+ history is left truncated.
+
+ Regression test for 3af9e2f (b/505072873).
+ """
+ foo = host.create_repo("platform/foo", {"f": "1"})
+ host.create_manifest(manifest_xml())
+
+ def pin(rev: str) -> None:
+ host.update_manifest(
+ manifest_xml(
+ f'',
+ f'',
+ default='remote="origin" revision="main" sync-c="true"',
+ )
+ )
+
+ pin(foo.commit({"f": "2"}))
+ client.init(host.manifest_url)
+ client.sync(*sync_args)
+
+ # Skip a commit so the shallow fetch alone would leave a gap in the shared
+ # history.
+ foo.commit({"f": "3"})
+ latest = foo.commit({"f": "4"})
+ pin(latest)
+ client.sync(*sync_args)
+
+ assert client.is_shallow("a")
+ for path in ("a", "b"):
+ assert client.head(path) == latest
+ # Fails to traverse the shared, truncated history if `path`'s fetch
+ # was skipped.
+ client.git(path, "rev-list", "HEAD")
+
+
+def test_shallow_pinned_with_upstream(
+ host: GitHost, client: RepoClient
+) -> None:
+ """clone-depth + upstream + SHA revisions stay shallow.
+
+ The bug only triggered when the upstream branch was ahead of the pinned
+ SHA, so each pin leaves `main` one commit ahead.
+
+ Regression test for d9cc0a1 (b/427093249).
+ """
+ foo = host.create_repo("platform/foo", {"f": "1"})
+ host.create_manifest(manifest_xml())
+
+ def pin(rev: str) -> None:
+ host.update_manifest(
+ manifest_xml(
+ f''
+ )
+ )
+
+ path = "platform/foo"
+ pinned = foo.commit({"f": "2"})
+ foo.commit({"f": "2 ahead"})
+ pin(pinned)
+ client.init(host.manifest_url)
+ client.sync()
+ assert client.head(path) == pinned
+ assert client.git(path, "rev-list", "--count", "HEAD") == "1"
+
+ pinned = foo.commit({"f": "3"})
+ foo.commit({"f": "3 ahead"})
+ pin(pinned)
+ client.sync()
+
+ assert client.head(path) == pinned
+ assert client.git(path, "rev-list", "--count", "HEAD") == "1"
+
+
+def test_pinned_revision_refreshes_upstream(
+ host: GitHost, client: RepoClient
+) -> None:
+ """Pinned SHAs already present locally still refresh the upstream ref.
+
+ Regression test for d27d682: when the pinned commit was already in the
+ local object store (e.g. prefetched), `sync -c` skipped the fetch as an
+ immutable revision, so the stale tracking ref of the upstream branch was
+ never updated.
+ """
+ foo = host.create_repo("platform/foo", {"f": "1"})
+ host.create_manifest(manifest_xml())
+
+ def pin(rev: str) -> None:
+ host.update_manifest(
+ manifest_xml(
+ f'',
+ # The skip only happens when fetching the current branch only.
+ default='remote="origin" revision="main" sync-c="true"',
+ )
+ )
+
+ initial = foo.rev_parse("main")
+ pin(initial)
+ client.init(host.manifest_url)
+ client.sync()
+
+ path = "platform/foo"
+ second = foo.commit({"f": "2"})
+ latest = foo.commit({"f": "3"})
+ # Emulate a background prefetch that doesn't touch the tracking refs.
+ client.git(
+ path, "fetch", "-q", "--refmap=", "origin", "+main:refs/prefetch/main"
+ )
+ assert client.has_object(path, second)
+ assert client.git(path, "rev-parse", "refs/remotes/origin/main") == initial
+ pin(second)
+ client.sync()
+
+ assert client.head(path) == second
+ assert client.git(path, "rev-parse", "refs/remotes/origin/main") == latest
+
+
+def test_nested_projects(host: GitHost, client: RepoClient) -> None:
+ """Nested project paths are checked out and removed in the right order.
+
+ Regression test for 4679022 (b/325119758).
+ """
+ foo = host.create_repo("platform/foo", {"f": "foo", "bar/f": "old"})
+ host.create_repo("platform/bar", {"f": "foo/bar"})
+ host.create_repo("platform/foo-bar", {"f": "foo-bar"})
+ # aaa shares foo/bar's project, so foo/bar is grouped with it & checked out
+ # early unless the checkout order puts it after its parent foo.
+ host.create_manifest(
+ manifest_xml(
+ '',
+ '',
+ '',
+ )
+ )
+ client.init(host.manifest_url)
+ client.sync()
+
+ # Move foo's bar/f into the new nested project foo/bar.
+ foo.commit({"bar/f": None})
+ host.update_manifest(
+ manifest_xml(
+ '',
+ '',
+ '',
+ '',
+ )
+ )
+ client.sync("-j1")
+
+ for path in ("foo", "foo/bar", "foo-bar"):
+ assert (client.top / path / "f").read_text() == path
+ assert client.git("foo/bar", "status", "--porcelain") == ""
+
+ host.update_manifest(
+ manifest_xml(
+ '',
+ '',
+ )
+ )
+ client.sync()
+
+ assert set(client.list_projects()) == {"foo", "foo-bar"}
+ assert not (client.top / "foo/bar").exists()
+ assert (client.top / "foo/f").read_text() == "foo"
+
+
+def test_copyfile_linkfile(host: GitHost, client: RepoClient) -> None:
+ """copyfile and linkfile are created, updated and cleaned up.
+
+ linkfile symlinks are relative (0184dcc) so trees can be moved.
+ """
+ foo = host.create_repo(
+ "platform/foo", {"copy.txt": "v1", "dir/link.txt": "link"}
+ )
+ host.create_manifest(
+ manifest_xml(
+ ''
+ ''
+ ''
+ ""
+ )
+ )
+ client.init(host.manifest_url)
+ client.sync()
+
+ link = client.top / "links/link.txt"
+ # Python <3.8 on Windows rejects Path objects here.
+ target = os.readlink(str(link))
+ assert target == os.path.join("..", "foo", "dir", "link.txt")
+ assert link.read_text() == "link"
+ assert (client.top / "copied.txt").read_text() == "v1"
+
+ foo.commit({"copy.txt": "v2"})
+ host.update_manifest(
+ manifest_xml(
+ ''
+ ''
+ ""
+ )
+ )
+ client.sync()
+
+ assert (client.top / "copied.txt").read_text() == "v2"
+ assert not os.path.lexists(link)
+
+
+@pytest.mark.parametrize("sync_args", [[], ["--no-interleaved"]])
+def test_submodules(
+ host: GitHost, client: RepoClient, sync_args: List[str]
+) -> None:
+ """sync-s checks out & updates submodules, incl. for repeated projects.
+
+ Regression tests for 4623264 (b/444366154, `git submodule init` in
+ interleaved sync), 1b4e7a0 (the same repo checked out at multiple paths),
+ and 3a6e25a/3f087a8 (b/550074864, submodules follow their parent's new
+ revision in the same interleaved & phased sync).
+ """
+ sub = host.create_repo("platform/sub", {"s": "1"})
+ gitmodules = '[submodule "sub"]\n\tpath = sub\n\turl = ../sub\n'
+ foo = host.create_repo(
+ "platform/foo",
+ {".gitmodules": gitmodules, "sub": Gitlink(sub.rev_parse("main"))},
+ )
+ paths = ("a", "b")
+ # Track a branch, as gitlinks are only stale (b/550074864) when the
+ # parent's revision resolves before it's fetched (unlike new pinned SHAs).
+ # With sync-c, phased syncs skip fetching the stale (present) revision.
+ host.create_manifest(
+ manifest_xml(
+ *(
+ f''
+ for p in paths
+ )
+ )
+ )
+
+ client.init(host.manifest_url)
+ client.sync(*sync_args)
+ for path in paths:
+ assert (client.top / path / "sub/s").read_text() == "1"
+ assert client.git(path, "config", "--get", "submodule.sub.url")
+
+ latest = foo.commit({"sub": Gitlink(sub.commit({"s": "2"}))})
+ client.sync(*sync_args)
+
+ for path in paths:
+ assert client.head(path) == latest
+ assert (client.top / path / "sub/s").read_text() == "2"
+
+
+# The default layout reads HEAD from the ref cache instead of GetHead().
+@pytest.mark.parametrize("init_args", [[], ["--worktree"]])
+def test_prune_detached_head(
+ host: GitHost, client: RepoClient, init_args: List[str]
+) -> None:
+ """`repo prune` handles projects with a detached HEAD.
+
+ Regression test for 7f7d70e (b/434077990).
+ """
+ host.create_repo("platform/foo", {"f": "foo"})
+ host.create_manifest(manifest_xml(''))
+ client.init(host.manifest_url, *init_args)
+ client.sync()
+ path = "platform/foo"
+ client.run("start", "topic", "--all")
+ client.git(path, "checkout", "-q", "--detach")
+
+ client.run("prune")
+
+ assert not client.git(path, "branch", "--list", "topic")
+
+
+def test_submanifest_groups(host: GitHost, client: RepoClient) -> None:
+ """Submanifests apply their own default groups.
+
+ Regression test for cc88be3 (b/382086294).
+ """
+ for name in ("parent", "child", "child-extra"):
+ host.create_repo(f"platform/{name}", {"f": name})
+ host.create_repo(
+ "platform/child-manifest",
+ {
+ "default.xml": manifest_xml(
+ '',
+ '',
+ )
+ },
+ )
+ host.create_manifest(
+ manifest_xml(
+ '',
+ '',
+ )
+ )
+
+ client.init(host.manifest_url)
+ client.sync()
+
+ assert client.list_projects() == {
+ "parent": "platform/parent",
+ "sub/child": "platform/child",
+ }
+ assert (client.top / "sub/child/f").read_text() == "child"
+ assert not (client.top / "sub/extra").exists()
diff --git a/tests/e2e/test_workflows.py b/tests/e2e/test_workflows.py
new file mode 100644
index 000000000..d91d5b310
--- /dev/null
+++ b/tests/e2e/test_workflows.py
@@ -0,0 +1,261 @@
+# Copyright (C) 2026 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""E2E tests for multi-step workflows, many from past point releases."""
+
+import os
+import re
+import time
+from typing import List
+
+from harness import GIT_DEFAULTS_PARAMS
+from harness import GitHost
+from harness import manifest_xml
+from harness import RepoClient
+from harness import run_git
+from harness import ServerRepo
+import pytest
+
+
+@pytest.mark.parametrize("git_defaults", GIT_DEFAULTS_PARAMS)
+@pytest.mark.parametrize(
+ "project_attrs",
+ ["", 'clone-depth="1"', 'sync-strategy="stateless"'],
+)
+def test_two_phase_sync(
+ host: GitHost, client: RepoClient, project_attrs: str
+) -> None:
+ """`repo sync -n` then `repo sync -l` before worktrees exist.
+
+ Regression tests for b/565047698 (v2.68.1, incl. e1e215a for stateless
+ syncs).
+ """
+ foo = host.create_repo("platform/foo", {"f": "1"})
+ host.create_manifest(
+ manifest_xml(
+ f''
+ )
+ )
+ client.init(host.manifest_url)
+
+ # The b/565047698 regression only logs a warning & still exits 0, so also
+ # require a clean stderr.
+ assert client.sync("-n").stderr == ""
+ assert not (client.top / "foo").exists()
+ # A second network-only sync (after upstream moved) with no worktree yet.
+ latest = foo.commit({"f": "2"})
+ assert client.sync("-n").stderr == ""
+ assert not (client.top / "foo").exists()
+
+ client.sync("-l")
+ assert client.head("foo") == latest
+ assert (client.top / "foo/f").read_text() == "2"
+
+ # And the checkout keeps working with regular syncs afterwards.
+ latest = foo.commit({"f": "3"})
+ client.sync()
+ assert client.head("foo") == latest
+
+
+@pytest.mark.parametrize(
+ "extra_init_args", [[], ["--manifest-upstream-branch", "main"]]
+)
+def test_init_manifest_commit(
+ host: GitHost, client: RepoClient, extra_init_args: List[str]
+) -> None:
+ """`repo init -b ` pins the manifest to a raw commit.
+
+ Regression test for b/544041102 (v2.66.1).
+ """
+ host.create_repo("platform/foo", {"f": "foo"})
+ host.create_repo("platform/bar", {"f": "bar"})
+ manifest = host.create_manifest(
+ manifest_xml('')
+ )
+ pinned = manifest.rev_parse("main")
+ host.update_manifest(
+ manifest_xml(
+ '',
+ '',
+ )
+ )
+
+ client.init(host.manifest_url, "-b", pinned, *extra_init_args)
+ client.sync()
+
+ assert client.list_projects() == {"platform/foo": "platform/foo"}
+ assert client.git(".repo/manifests", "rev-parse", "HEAD") == pinned
+
+
+@pytest.mark.parametrize("git_defaults", GIT_DEFAULTS_PARAMS)
+def test_mirror(host: GitHost, client: RepoClient) -> None:
+ """`repo init --mirror && repo sync` creates bare mirrors (v2.57.1)."""
+ foo = host.create_repo("platform/foo", {"f": "1"})
+ foo.commit({"f": "other"}, branch="other")
+ host.create_manifest(manifest_xml(''))
+
+ client.init(host.manifest_url, "--mirror")
+ client.sync()
+
+ mirror = client.top / "platform/foo.git"
+
+ def mirror_rev(ref: str) -> str:
+ return run_git(f"--git-dir={mirror}", "rev-parse", ref, env=client.env)
+
+ assert not (client.top / "platform/foo").exists()
+ assert mirror_rev("refs/heads/main") == foo.rev_parse("main")
+ assert mirror_rev("refs/heads/other") == foo.rev_parse("other")
+
+ latest = foo.commit({"f": "2"})
+ client.sync()
+ assert mirror_rev("refs/heads/main") == latest
+
+
+def test_incremental_sync(host: GitHost, client: RepoClient) -> None:
+ """Syncs pick up new commits, new projects & removed projects."""
+ foo = host.create_repo("platform/foo", {"f": "1"})
+ host.create_repo("platform/bar", {"f": "bar"})
+ host.create_repo("platform/baz", {"f": "baz"})
+ host.create_manifest(
+ manifest_xml(
+ '',
+ '',
+ )
+ )
+ client.init(host.manifest_url)
+ client.sync()
+
+ latest = foo.commit({"f": "2", "new": "new"})
+ host.update_manifest(
+ manifest_xml(
+ '',
+ '',
+ )
+ )
+ client.sync()
+
+ assert client.head("foo") == latest
+ assert (client.top / "foo/new").read_text() == "new"
+ assert (client.top / "baz/f").read_text() == "baz"
+ assert not (client.top / "bar").exists()
+ assert client.list_projects() == {
+ "baz": "platform/baz",
+ "foo": "platform/foo",
+ }
+
+
+def test_rebase_local_work(host: GitHost, client: RepoClient) -> None:
+ """Local commits on a `repo start` branch are rebased by sync."""
+ foo = host.create_repo("platform/foo", {"f": "1"})
+ host.create_manifest(manifest_xml(''))
+ client.init(host.manifest_url)
+ client.sync()
+
+ path = "platform/foo"
+ client.run("start", "topic", "--all")
+ (client.top / path / "local").write_text("local")
+ client.git(path, "add", "local")
+ client.git(path, "commit", "-q", "-m", "local change")
+ latest = foo.commit({"f": "2"})
+
+ client.sync()
+
+ assert client.git(path, "symbolic-ref", "--short", "HEAD") == "topic"
+ assert client.git(path, "rev-parse", "HEAD~1") == latest
+ assert (client.top / path / "local").read_text() == "local"
+
+
+def test_switch_manifest_branch(host: GitHost, client: RepoClient) -> None:
+ """Re-running `repo init -b` on an existing checkout switches branches."""
+ foo = host.create_repo("platform/foo", {"f": "main"})
+ release = foo.commit({"f": "release"}, branch="release")
+ host.create_manifest(
+ manifest_xml('')
+ )
+ host.update_manifest(
+ manifest_xml(
+ '',
+ default='remote="origin" revision="release"',
+ ),
+ branch="release",
+ )
+ client.init(host.manifest_url)
+ client.sync()
+ assert client.head("foo") == foo.rev_parse("main")
+
+ client.init(host.manifest_url, "-b", "release")
+ client.sync()
+
+ assert client.head("foo") == release
+
+
+@pytest.mark.parametrize(
+ "command",
+ [
+ ["selfupdate", "--no-repo-verify"],
+ ["sync", "--no-repo-verify"],
+ ],
+)
+def test_selfupdate(
+ host: GitHost,
+ client: RepoClient,
+ repo_source: ServerRepo,
+ command: List[str],
+) -> None:
+ """repo upgrades itself when its source moves forward."""
+ source = host.fork(repo_source, "git-repo")
+ client.repo_url = source.url
+ host.create_repo("platform/foo", {"f": "foo"})
+ host.create_manifest(manifest_xml(''))
+ # Start from an existing synced checkout before publishing the repo update.
+ client.init(host.manifest_url)
+ client.sync()
+
+ latest = source.commit({"E2E_SELFUPDATE_MARKER": "new"})
+ # sync only checks for updates once a day.
+ fetch_head = client.repodir / "repo/.git/FETCH_HEAD"
+ old = time.time() - 2 * 24 * 60 * 60
+ os.utime(fetch_head, (old, old))
+
+ result = client.run(*command)
+
+ assert "A new version of repo is available" in result.stderr
+ assert client.git(".repo/repo", "rev-parse", "HEAD") == latest
+ assert (client.repodir / "repo/E2E_SELFUPDATE_MARKER").exists()
+ # The restarted (upgraded) repo keeps working.
+ assert "v99.0-e2e-1-g" in client.run("version").stdout
+
+
+def test_commands_after_sync(host: GitHost, client: RepoClient) -> None:
+ """Common read-only commands work on a freshly synced checkout."""
+ host.create_repo("platform/foo", {"f": "foo"})
+ host.create_manifest(manifest_xml(''))
+ client.init(host.manifest_url)
+ client.sync()
+
+ for command, expected in (
+ (["status"], r"nothing to commit"),
+ (["info"], r"Manifest branch: main"),
+ (["branches"], r"\(no branches\)"),
+ # No local changes, so no output.
+ (["diff"], r"\A\Z"),
+ (["forall", "-c", "git", "log", "-1"], r"platform/foo: initial"),
+ # Projects are pinned to commit ids.
+ (["manifest", "-r"], r'revision="[0-9a-f]{40}"'),
+ ):
+ output = client.run(*command).output
+ assert re.search(expected, output), (
+ f"`repo {' '.join(command)}` output {output!r} "
+ f"did not match {expected!r}"
+ )