tests: e2e: add tests for init, sync & common workflows

Cover the core `repo init` & `repo sync` options (current branch only,
clone bundles, shallow clones, groups, local manifests & manifest
names), multi-step workflows (two-phase syncs, mirrors, incremental
syncs, local work, switching manifest branches, selfupdate, ...), and
manifests that broke users in the past.  Some tests also run under Git 3
defaults.

Add `RepoClient` helpers to list remote-tracking branches & checkouts,
and document the test layout in the README.

Bug: 566270944
Change-Id: Iac333216e0f27e489e64f4eb853189f860a91f20
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/648544
Tested-by: Brian Gan <brgan@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Brian Gan <brgan@google.com>
This commit is contained in:
Brian Gan authored and gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com committed 2026-10-26 12:53:32 -07:00
1 parent 9aac80dafc
commit dc1811ba23
5 files changed
+901

No files matched your search

+7
View File
@@ -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.
+19
View File
@@ -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
+227
View File
@@ -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(
'<project name="platform/foo" path="foo" />',
'<project name="platform/bar" path="sub/bar" />',
)
)
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('<project name="platform/foo" />')
)
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('<project name="platform/foo" />'))
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'<project name="platform/foo" {project_attrs} />')
)
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(
'<project name="platform/a" path="a" />',
'<project name="platform/b" path="b" groups="g1" />',
'<project name="platform/c" path="c" groups="g2,notdefault" />',
'<project name="platform/d" path="d" groups="notdefault" />',
)
)
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(
'<project name="platform/foo" path="foo" />',
'<project name="platform/bar" path="bar" />',
)
)
client.init(host.manifest_url)
local = client.repodir / "local_manifests"
local.mkdir()
(local / "local.xml").write_text(
"""<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<remove-project name="platform/bar" />
<extend-project name="platform/foo" revision="stable" />
<project name="vendor/extra" path="extra" />
</manifest>
"""
)
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('<project name="platform/foo" />'),
"other.xml": manifest_xml('<project name="platform/bar" />'),
}
)
client.init(host.manifest_url, "-m", "other.xml")
client.sync()
assert client.list_projects() == {"platform/bar": "platform/bar"}
+387
View File
@@ -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(
'<superproject name="superproject" />',
'<project name="platform/foo" path="foo" />',
)
)
client.init(host.manifest_url, "--use-superproject")
local = client.repodir / "local_manifests"
local.mkdir()
(local / "local.xml").write_text(
"<manifest>\n"
' <extend-project name="platform/foo" groups="extra" />\n'
"</manifest>\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'<project name="platform/foo" path="a" revision="{rev}" '
'clone-depth="1" />',
f'<project name="platform/foo" path="b" revision="{rev}" />',
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'<project name="platform/foo" revision="{rev}" '
'upstream="main" clone-depth="1" />'
)
)
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'<project name="platform/foo" revision="{rev}" '
'upstream="main" />',
# 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(
'<project name="platform/bar" path="aaa" />',
'<project name="platform/foo" path="foo" />',
'<project name="platform/foo-bar" path="foo-bar" />',
)
)
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(
'<project name="platform/bar" path="aaa" />',
'<project name="platform/foo" path="foo" />',
'<project name="platform/bar" path="foo/bar" />',
'<project name="platform/foo-bar" path="foo-bar" />',
)
)
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(
'<project name="platform/foo" path="foo" />',
'<project name="platform/foo-bar" path="foo-bar" />',
)
)
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(
'<project name="platform/foo" path="foo">'
'<copyfile src="copy.txt" dest="copied.txt" />'
'<linkfile src="dir/link.txt" dest="links/link.txt" />'
"</project>"
)
)
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(
'<project name="platform/foo" path="foo">'
'<copyfile src="copy.txt" dest="copied.txt" />'
"</project>"
)
)
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'<project name="platform/foo" path="{p}" sync-s="true" '
'sync-c="true" />'
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('<project name="platform/foo" />'))
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(
'<project name="platform/child" path="child" groups="g1" />',
'<project name="platform/child-extra" path="extra" />',
)
},
)
host.create_manifest(
manifest_xml(
'<project name="platform/parent" path="parent" />',
'<submanifest name="sub" project="platform/child-manifest" '
'revision="main" path="sub" default-groups="g1" />',
)
)
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()
+261
View File
@@ -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'<project name="platform/foo" path="foo" {project_attrs} />'
)
)
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 <sha>` 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('<project name="platform/foo" />')
)
pinned = manifest.rev_parse("main")
host.update_manifest(
manifest_xml(
'<project name="platform/foo" />',
'<project name="platform/bar" />',
)
)
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('<project name="platform/foo" />'))
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(
'<project name="platform/foo" path="foo" />',
'<project name="platform/bar" path="bar" />',
)
)
client.init(host.manifest_url)
client.sync()
latest = foo.commit({"f": "2", "new": "new"})
host.update_manifest(
manifest_xml(
'<project name="platform/foo" path="foo" />',
'<project name="platform/baz" path="baz" />',
)
)
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('<project name="platform/foo" />'))
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('<project name="platform/foo" path="foo" />')
)
host.update_manifest(
manifest_xml(
'<project name="platform/foo" path="foo" />',
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('<project name="platform/foo" />'))
# 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('<project name="platform/foo" />'))
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}"
)