Compare commits

...
13 Commits
Author SHA1 Message Date
Gavin Mak b85886fa9f project: skip manifest default fallback for MetaProjects
Avoid loading manifest.xml during repo init when syncing a manifest
commit SHA. Override _GetUpstreamFallback and _SharingProjectHasShallow
in MetaProject to prevent premature manifest parsing.

Bug: 544041102
Change-Id: I7aa54a7c1282e5bfe811d59e977b44163a37653c
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/617081
Commit-Queue: Gavin Mak <gavinmak@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
Reviewed-by: Brian Gan <brgan@google.com>
2026-08-10 13:37:04 -07:00
Gavin Mak d9da609d8c project: preserve -c optimization when revision is a SHA-1
Avoid disabling --current-branch when syncing a SHA-1 revision without
an explicit project upstream (e.g., smart tags). Resolve a fallback
upstream from dest-branch or manifest defaults so -c only fetches the
target branch.

Bug: 541240657
Change-Id: Ib44b6a732131210e1ec3a3136747d1a19bc5aa18
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/614762
Reviewed-by: Brian Gan <brgan@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
2026-08-04 15:00:19 -07:00
Gavin Mak 4bec297eb6 command: Respect smart sync override declaratively by default
Introduce a `RESPECT_SMART_SYNC_OVERRIDE` class attribute to the base
`Command` class, defaulting to `True`. This allows subcommands to
automatically respect the smart sync override manifest if it exists.

The override is applied in `CommonValidateOptions` before any
subcommand-specific validation or execution occurs. The `sync` and
`init` commands explicitly opt out.

This ensures all workspace-aware subcommands consistently align with the
active smart sync override manifest. It also fixes a bug in
multi-manifest setups where running a command from a submanifest would
not apply the override to the outer manifest, causing inconsistency when
resolving projects across all manifests.

Bug: 279204331
Change-Id: I9426e90a13a77ce6bd94b4a82efda4d485cbe116
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/585081
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
2026-08-04 11:38:03 -07:00
Brian Gan 54fa31cd84 project: derive HEAD fallback from git's own default branch
When the default branch cannot be determined dynamically, repo fell back
to a hardcoded "refs/heads/master". This can point at a branch that does
not exist on the server, since projects increasingly default to "main".
Rather than swap one hardcoded name for another, ask git itself what it
would use.

Bug: 483758905
Change-Id: Ic66712acea98f8e548a2d6d8211865ee5c416b4d
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/612845
Tested-by: Brian Gan <brgan@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Brian Gan <brgan@google.com>
2026-07-30 17:00:00 -07:00
Brian Gan 29b6630a65 project: make GetHead file-read fallback reftable-aware
When both `git symbolic-ref` and `git rev-parse` fail to resolve HEAD,
GetHead falls back to reading .git/HEAD directly. With the "reftable" ref
backend, .git/HEAD is only a stub pointing at the "refs/heads/.invalid"
placeholder while the real HEAD lives in the reftable stack. Reading the
stub returned that bogus placeholder value.

Detect the placeholder and treat HEAD as unresolvable (raising
NoManifestException) instead of returning the invalid ref.

Bug: 483758905
Change-Id: Ia0a825c0d1874686b98c5ddcdea6dc26c0d46784
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/612882
Tested-by: Brian Gan <brgan@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Brian Gan <brgan@google.com>
2026-07-30 10:01:58 -07:00
Andrew Chant 0b82311632 sync: allow syncing groups with repo sync -g group
Similar to how repo init -g can restrict repo syncs to
a subset of the manifest globally, allow "repo sync -g" to only sync a
subset of projects from the manifest when running that specific
sync command.

Change-Id: I4929aad109de05c73a7db42bb27fd8d47eea32fc
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/609481
Reviewed-by: Mike Frysinger <vapier@google.com>
Commit-Queue: Andrew Chant <achant@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Tested-by: Andrew Chant <achant@google.com>
2026-07-21 13:42:38 -07:00
Andrew Chant 06c4f9e1cc git_superproject: don't filter rewritten manifest
When creating the superproject manifest, take all groups
from the original manifest, including notdefault.

Using the ManifestGroupsStr will inadvertantly filter out
notdefault projects from the superproject manifest.

Change-Id: If3daf41e9a6572348de9182fe8679b90d3cec833
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/610021
Commit-Queue: Andrew Chant <achant@google.com>
Tested-by: Andrew Chant <achant@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
2026-07-21 13:42:30 -07:00
Josef Malmström dd1130352b sync: Deprecate fetch-submodules flag names
The names for flags --fetch-submodules / --no-fetch-submodules are
misleading, since they impact the full sync operation (fetch and
checkout), not just the fetching.

Introduce new flags --recurse-submodules / --no-recurse-submodules
and treat the old ones as deprecated aliases.

Change-Id: I78339a3e0496a855c222c1869b27b578507886a7
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/608881
Commit-Queue: Josef Malmstrom <Josef.Malmstrom@arm.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Josef Malmstrom <Josef.Malmstrom@arm.com>
2026-07-21 00:19:21 -07:00
Rahul Yadav eeba6f268d hooks: pass yes flag when available
Pass the -y flag as a keyword argument "yes" to the hook main function.
This allows upload hooks (such as auto-fixers) to automatically
apply fixes when the -y flag is passed, rather than prompting the user.

Bug: 498893733
Change-Id: I12096029aa3af471ba9175314d749deb0ebd1007
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/605261
Commit-Queue: Rahul Yadav <yadavrah@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Rahul Yadav <yadavrah@google.com>
2026-07-20 07:56:03 -07:00
Ajay Gupta 1729aaebae project: Skip superproject upstream check for MetaProjects
The superproject-gated upstream check in _CheckForImmutableRevision
only applies to user projects listed in the manifest. MetaProjects
(ManifestProject and RepoProject) never participate in a superproject
relationship, so evaluating git_superproject.UseSuperproject(...,
self.manifest) for them serves no purpose and, worse, calls the
manifest.superproject property which forces a manifest load.

During repo init, ManifestProject._ConfigureDependencies calls
self.Sync_NetworkHalf before manifest.xml has been linked into
.repo/. That reaches _CheckForImmutableRevision, which triggered the
manifest load and failed with:

  ManifestParseError: .../.repo/manifest.xml: [Errno 2] No such file
  or directory

breaking fresh repo init with SHA-based --manifest-branch combined
with --manifest-upstream-branch.

Factor the "should we consult the superproject for upstream?"
decision into a small overridable hook, _UseSuperprojectForUpstream.
Project's default delegates to git_superproject.UseSuperproject;
MetaProject overrides it to return False, localizing the
MetaProject-specific behavior to MetaProject.

Test verifies that calling _CheckForImmutableRevision on a
ManifestProject whose manifest.xml is not yet on disk returns False
without raising and does not create the file.

Change-Id: I22059109243d914036c06c6fe0081a5aba05da89
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/574201
Reviewed-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Ajay Gupta <ajagup@qti.qualcomm.com>
Tested-by: Ajay Gupta <ajagup@qti.qualcomm.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Nasser Grainawi <nasser.grainawi@oss.qualcomm.com>
2026-07-16 11:51:00 -07:00
Josef Malmström 978adb7ea5 sync: Add CLI flag for globally disabling submodule fetch
A global setting for disabling fetching of submodules is useful
since this can currently otherwise only be done by modifying
the manifest, or by explicitly providing projects on command line.

Add this setting as --no-fetch-submodules to mirror the existing
--fetch-submodules.

Change-Id: Ic727c54f11a594aa52315751284b87138cf246bb
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/607641
Commit-Queue: Josef Malmstrom <Josef.Malmstrom@arm.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Tested-by: Josef Malmstrom <Josef.Malmstrom@arm.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
2026-07-16 01:19:51 -07:00
Brian Gan 0398c6718e color: Replace anonymous sentinel with named class
Replace the bare `object()` sentinel used for `_CHECK_CONSOLE` with an
instance of a dedicated `_CheckConsoleSentinel` class. This gives the
sentinel a meaningful repr and type, making it easier to identify in
debugging output and type checks compared to an opaque `<object>`.

Change-Id: I916521d47ba13207e29a2412e00f3576aff8afff
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/605021
Commit-Queue: Brian Gan <brgan@google.com>
Tested-by: Brian Gan <brgan@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
2026-07-07 14:55:56 -07:00
Gavin Mak 3bb4871c44 rebase: Resolve revisionExpr to tracking branch for --onto-manifest
When running `repo rebase -m` (`--onto-manifest`), the command uses the
raw `revisionExpr` from the manifest (e.g. `main`) directly as the
`--onto` target. This can fail or behave incorrectly if it should
reference the local tracking branch (e.g. `refs/remotes/<remote>/main`).

Resolve `project.revisionExpr` to its local tracking branch using
`project.GetRemote().ToLocal()`. Fall back to using the raw
`revisionExpr` value if the resolution fails (raising a `GitError`).

Bug: 532028666
Change-Id: I4c1bca1374a5842688be227f6aa2afffcdad5397
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/604941
Reviewed-by: Brian Gan <brgan@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
2026-07-07 13:40:52 -07:00
22 changed files with 833 additions and 66 deletions
+6 -1
View File
@@ -84,9 +84,14 @@ def _Color(fg=None, bg=None, attr=None):
DEFAULT = None
class _CheckConsoleSentinel:
"""Sentinel for checking console coloring."""
# Placholder value that indicates we need to check if the user is in an
# interactive terminal session to determine if we turn on color or not.
_CHECK_CONSOLE = object()
_CHECK_CONSOLE = _CheckConsoleSentinel()
# https://git-scm.com/docs/git-config#Documentation/git-config.txt-colorui
_CONFIG_TO_COLOR_SETTING = {
+26 -4
View File
@@ -17,6 +17,7 @@ import multiprocessing
import optparse
import os
import re
from typing import TYPE_CHECKING
from error import InvalidProjectGroupsError
from error import NoSuchProjectError
@@ -25,6 +26,10 @@ from event_log import EventLog
import progress
if TYPE_CHECKING:
from project import Project
# Are we generating man-pages?
GENERATE_MANPAGES = os.environ.get("_REPO_GENERATE_MANPAGES_") == " indeed! "
@@ -61,6 +66,10 @@ class Command:
# command to show short-vs-full summaries.
COMMON = False
# Whether this command should respect the smart sync override manifest if
# it exists.
RESPECT_SMART_SYNC_OVERRIDE = True
# Whether this command supports running in parallel. If greater than 0,
# it is the number of parallel jobs to default to.
PARALLEL_JOBS = None
@@ -242,6 +251,12 @@ class Command:
# from the user's perspective.
opt.outer_manifest = True
if self.RESPECT_SMART_SYNC_OVERRIDE:
if self.manifest:
self.TryOverrideManifestWithSmartSync(self.manifest)
if self.outer_manifest and self.outer_manifest != self.manifest:
self.TryOverrideManifestWithSmartSync(self.outer_manifest)
def ValidateOptions(self, opt, args):
"""Validate the user options & arguments before executing.
@@ -375,7 +390,7 @@ class Command:
manifest=None,
groups="",
missing_ok=False,
submodules_ok=False,
submodules_ok=None,
all_manifests=False,
):
"""A list of projects that match the arguments.
@@ -385,7 +400,9 @@ class Command:
manifest: an XmlManifest, the manifest to use, or None for default.
groups: a string, the manifest groups in use.
missing_ok: a boolean, whether to allow missing projects.
submodules_ok: a boolean, whether to allow submodules.
submodules_ok: whether to allow submodules. True allows them for
all projects, False disallows them for all projects, and None
defers to each project's sync-s setting.
all_manifests: a boolean, if True then all manifests and
submanifests are used. If False, then only the local
(sub)manifest is used.
@@ -403,6 +420,11 @@ class Command:
all_projects_list = manifest.projects
result = []
def should_include_submodules(project: "Project") -> bool:
if submodules_ok is None:
return project.sync_s
return submodules_ok
if not groups:
groups = manifest.GetManifestGroupsStr()
groups = [x for x in re.split(r"[,\s]+", groups) if x]
@@ -410,7 +432,7 @@ class Command:
if not args:
derived_projects = {}
for project in all_projects_list:
if submodules_ok or project.sync_s:
if should_include_submodules(project):
derived_projects.update(
(p.RelPath(local=False), p)
for p in project.GetDerivedSubprojects()
@@ -452,7 +474,7 @@ class Command:
if (
project
and not project.Derived
and (submodules_ok or project.sync_s)
and should_include_submodules(project)
):
search_again = False
for subproject in project.GetDerivedSubprojects():
+4 -1
View File
@@ -322,7 +322,10 @@ _repo() {
'--no-clone-bundle[Do not use clone bundle]' \
'(-u --manifest-server-username)'{-u,--manifest-server-username=}'[Username for manifest server]:username:' \
'(-p --manifest-server-password)'{-p,--manifest-server-password=}'[Password for manifest server]:password:' \
'--fetch-submodules[Fetch submodules]' \
'--recurse-submodules[Sync submodules]' \
'--no-recurse-submodules[Do not sync submodules]' \
'--fetch-submodules[Deprecated alias for --recurse-submodules]' \
'--no-fetch-submodules[Deprecated alias for --no-recurse-submodules]' \
'--use-superproject[Use superproject]' \
'--no-use-superproject[Do not use superproject]' \
'--tags[Sync tags]' \
+10 -1
View File
@@ -83,6 +83,13 @@ then check it directly. Hooks should not normally modify the active git repo
the user. Although user interaction is discouraged in the common case, it can
be useful when deploying automatic fixes.
### Safe Prompts
If the repo command that triggered the hook supports a "yes" option (e.g.,
`repo upload --yes`), this option is propagated to the hook's `main` function
as `yes` parameter (defaulting to `False`). Hooks can use this to bypass
interactive confirmation prompts when they can automatically fix issues.
### Shebang Handling
*** note
@@ -119,7 +126,7 @@ This hook runs when people run `repo upload`.
The `pre-upload.py` file should be defined like:
```py
def main(project_list, worktree_list=None, **kwargs):
def main(project_list, worktree_list=None, yes=False, **kwargs):
"""Main function invoked directly by repo.
We must use the name "main" as that is what repo requires.
@@ -130,6 +137,8 @@ def main(project_list, worktree_list=None, **kwargs):
project_list, so that each entry in project_list matches with a
directory in worktree_list. If None, we will attempt to calculate
the directories automatically.
yes: Whether to answer yes to all safe prompts (see
[Safe Prompts](#safe-prompts)).
kwargs: Leave this here for forward-compatibility.
"""
```
+1 -1
View File
@@ -473,7 +473,7 @@ class Superproject:
)
return None
manifest_str = self._manifest.ToXml(
filter_groups=self._manifest.GetManifestGroupsStr(),
filter_groups="all",
omit_local=True,
).toxml()
manifest_path = self._manifest_path
+9 -2
View File
@@ -68,6 +68,7 @@ class RepoHook:
allow_all_hooks=False,
ignore_hooks=False,
abort_if_user_denies=False,
yes=False,
):
"""RepoHook constructor.
@@ -89,6 +90,7 @@ class RepoHook:
ignore_hooks: If True, then 'Do not abort action if hooks fail'.
abort_if_user_denies: If True, we'll abort running the hook if the
user doesn't allow us to run the hook.
yes: If True, then 'Yes' is assumed for any prompts.
"""
self._hook_type = hook_type
self._hooks_project = hooks_project
@@ -99,6 +101,7 @@ class RepoHook:
self._allow_all_hooks = allow_all_hooks
self._ignore_hooks = ignore_hooks
self._abort_if_user_denies = abort_if_user_denies
self._yes = yes
# Store the full path to the script for convenience.
self._script_fullpath = None
@@ -374,8 +377,11 @@ class RepoHook:
# def main(project_list, **kwargs):
#
# This allows us to later expand the API without breaking old hooks.
kwargs = kwargs.copy()
kwargs["hook_should_take_kwargs"] = True
kwargs = {
**kwargs,
"hook_should_take_kwargs": True,
"yes": self._yes,
}
# See what version of python the hook has been written against.
data = open(self._script_fullpath).read()
@@ -497,6 +503,7 @@ class RepoHook:
"origin"
).url,
"bug_url": manifest.contactinfo.bugurl,
"yes": getattr(opt, "yes", False),
}
)
return cls(*args, **kwargs)
+9 -3
View File
@@ -1,5 +1,5 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
.TH REPO "1" "June 2026" "repo smartsync" "Repo Manual"
.TH REPO "1" "July 2026" "repo smartsync" "Repo Manual"
.SH NAME
repo \- repo smartsync - manual page for repo smartsync
.SH SYNOPSIS
@@ -68,6 +68,9 @@ fetch all branches from server
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
temporary manifest to use for this sync
.TP
\fB\-g\fR GROUP, \fB\-\-groups\fR=\fI\,GROUP\/\fR
sync projects matching the specific groups. Not persistent unlike when used on init
.TP
\fB\-\-clone\-bundle\fR
enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
.TP
@@ -80,8 +83,11 @@ username to authenticate with the manifest server
\fB\-p\fR MANIFEST_SERVER_PASSWORD, \fB\-\-manifest\-server\-password\fR=\fI\,MANIFEST_SERVER_PASSWORD\/\fR
password to authenticate with the manifest server
.TP
\fB\-\-fetch\-submodules\fR
fetch submodules from server
\fB\-\-recurse\-submodules\fR
sync submodules from server
.TP
\fB\-\-no\-recurse\-submodules\fR
don't sync submodules from server
.TP
\fB\-\-use\-superproject\fR
use the manifest superproject to sync projects; implies \fB\-c\fR
+15 -5
View File
@@ -1,5 +1,5 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
.TH REPO "1" "June 2026" "repo sync" "Repo Manual"
.TH REPO "1" "July 2026" "repo sync" "Repo Manual"
.SH NAME
repo \- repo sync - manual page for repo sync
.SH SYNOPSIS
@@ -68,6 +68,9 @@ fetch all branches from server
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
temporary manifest to use for this sync
.TP
\fB\-g\fR GROUP, \fB\-\-groups\fR=\fI\,GROUP\/\fR
sync projects matching the specific groups. Not persistent unlike when used on init
.TP
\fB\-\-clone\-bundle\fR
enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
.TP
@@ -80,8 +83,11 @@ username to authenticate with the manifest server
\fB\-p\fR MANIFEST_SERVER_PASSWORD, \fB\-\-manifest\-server\-password\fR=\fI\,MANIFEST_SERVER_PASSWORD\/\fR
password to authenticate with the manifest server
.TP
\fB\-\-fetch\-submodules\fR
fetch submodules from server
\fB\-\-recurse\-submodules\fR
sync submodules from server
.TP
\fB\-\-no\-recurse\-submodules\fR
don't sync submodules from server
.TP
\fB\-\-use\-superproject\fR
use the manifest superproject to sync projects; implies \fB\-c\fR
@@ -212,8 +218,12 @@ bootstrap a new Git repository from a resumeable bundle file on a content
delivery network. This may be necessary if there are problems with the local
Python HTTP client or proxy configuration, but the Git binary works.
.PP
The \fB\-\-fetch\-submodules\fR option enables fetching Git submodules of a project from
server.
The \fB\-\-recurse\-submodules\fR option enables syncing Git submodules of all projects
from the server. The \fB\-\-no\-recurse\-submodules\fR option disables syncing Git
submodules, even when a project has sync\-s="true" in the manifest.
.PP
The \fB\-\-fetch\-submodules\fR and \fB\-\-no\-fetch\-submodules\fR options are deprecated aliases
for \fB\-\-recurse\-submodules\fR and \fB\-\-no\-recurse\-submodules\fR, respectively.
.PP
The \fB\-c\fR/\-\-current\-branch option can be used to only fetch objects that are on the
branch specified by a project's revision.
+108 -29
View File
@@ -15,6 +15,7 @@
import datetime
import errno
import filecmp
import functools
import glob
import os
import platform
@@ -2674,11 +2675,12 @@ class Project:
# throws an error.
revs = [f"{self.revisionExpr}^0"]
upstream_rev = None
use_superproject_for_upstream = self.upstream and (
self._UseSuperprojectForUpstream(use_superproject)
)
# Only check upstream when using superproject.
if self.upstream and git_superproject.UseSuperproject(
use_superproject, self.manifest
):
if use_superproject_for_upstream:
upstream_rev = self.GetRemote().ToLocal(self.upstream)
revs.append(upstream_rev)
@@ -2692,9 +2694,7 @@ class Project:
# Only verify upstream relationship for superproject scenarios
# without affecting plain usage.
if self.upstream and git_superproject.UseSuperproject(
use_superproject, self.manifest
):
if use_superproject_for_upstream:
self.bare_git.merge_base(
"--is-ancestor",
self.revisionExpr,
@@ -2723,6 +2723,16 @@ class Project:
return True
return False
def _UseSuperprojectForUpstream(
self, use_superproject: Optional[bool] = None
) -> bool:
"""Whether to include upstream in the immutability check.
The upstream ancestry check is only meaningful for projects
that participate in a superproject relationship.
"""
return git_superproject.UseSuperproject(use_superproject, self.manifest)
def _FetchArchive(self, tarpath, cwd=None):
cmd = ["archive", "-v", "-o", tarpath]
cmd.append("--remote=%s" % self.remote.url)
@@ -2847,6 +2857,23 @@ class Project:
return True
def _GetUpstreamFallback(self) -> Optional[str]:
"""Resolve a fallback upstream ref when revisionExpr is a SHA-1."""
default = self.manifest.default
candidates = [self.dest_branch]
if default:
candidates.extend(
(
default.upstreamExpr,
default.destBranchExpr,
default.revisionExpr,
)
)
for cand in candidates:
if cand and not IsId(cand):
return cand
return None
def _RemoteFetch(
self,
name=None,
@@ -2880,14 +2907,31 @@ class Project:
current_branch_only = True
is_sha1 = IsId(self.revisionExpr)
upstream = self.upstream
if current_branch_only:
if is_sha1 and not depth:
# When syncing a specific commit and --depth is not set:
# * if upstream is explicitly specified and is not a sha1, fetch
# only upstream as users expect only upstream to be fetch.
# Note: The commit might not be in upstream in which case the
# sync will fail.
# * otherwise, fetch all branches to make sure we end up with
# the specific commit.
if not upstream:
upstream = self._GetUpstreamFallback()
if upstream:
current_branch_only = not IsId(upstream)
else:
current_branch_only = False
if self.revisionExpr.startswith(R_TAGS):
# This is a tag and its commit id should never change.
tag_name = self.revisionExpr[len(R_TAGS) :]
elif self.upstream and self.upstream.startswith(R_TAGS):
elif upstream and upstream.startswith(R_TAGS):
# This is a tag and its commit id should never change.
tag_name = self.upstream[len(R_TAGS) :]
tag_name = upstream[len(R_TAGS) :]
if is_sha1 or tag_name is not None:
has_shallow = os.path.exists(
@@ -2905,18 +2949,6 @@ class Project:
"persistent ref)" % self.name
)
return True
if is_sha1 and not depth:
# When syncing a specific commit and --depth is not set:
# * if upstream is explicitly specified and is not a sha1, fetch
# only upstream as users expect only upstream to be fetch.
# Note: The commit might not be in upstream in which case the
# sync will fail.
# * otherwise, fetch all branches to make sure we end up with
# the specific commit.
if self.upstream:
current_branch_only = not IsId(self.upstream)
else:
current_branch_only = False
if not name:
name = self.remote.name
@@ -3028,11 +3060,11 @@ class Project:
# Shallow checkout of a specific commit, fetch from that commit and
# not the heads only as the commit might be deeper in the history.
spec.append(branch)
if self.upstream:
spec.append(self.upstream)
if upstream:
spec.append(upstream)
else:
if is_sha1:
branch = self.upstream
branch = upstream
if branch is not None and branch.strip():
if not branch.startswith("refs/"):
branch = R_HEADS + branch
@@ -4389,8 +4421,12 @@ class Project:
except AttributeError:
pass
if line.startswith("ref: "):
return line[5:-1]
return line[:-1]
ref = line[5:-1]
else:
ref = line[:-1]
if ref == R_HEADS + ".invalid":
raise NoManifestException(path, str(e))
return ref
def SetHead(self, ref, message=None):
cmdv = []
@@ -4650,6 +4686,26 @@ class SyncBuffer:
self._pending_failures = []
@functools.lru_cache(maxsize=None)
def _DefaultBranchFallback() -> str:
"""Return the ref to use when remote default branch can't be resolved."""
def _git(args: List[str]) -> str:
p = GitCommand(
None,
args,
capture_stdout=True,
capture_stderr=True,
log_as_error=False,
)
return p.stdout.strip() if p.Wait() == 0 else ""
branch = _git(["var", "GIT_DEFAULT_BRANCH"]) or _git(
["config", "--get", "init.defaultBranch"]
)
return f"refs/heads/{branch or 'master'}"
class MetaProject(Project):
"""A special project housed under .repo."""
@@ -4663,7 +4719,7 @@ class MetaProject(Project):
worktree=worktree,
remote=RemoteSpec("origin"),
relpath=".repo/%s" % name,
revisionExpr="refs/heads/master",
revisionExpr=_DefaultBranchFallback(),
revisionId=None,
groups=None,
)
@@ -4677,6 +4733,29 @@ class MetaProject(Project):
self.revisionExpr = base
self.revisionId = None
def _UseSuperprojectForUpstream(
self, use_superproject: Optional[bool] = None
) -> bool:
# MetaProjects (the manifest repo and repo itself) never
# participate in a superproject relationship. Returning False
# here also avoids loading the manifest during `repo init`,
# before manifest.xml has been linked into .repo/.
return False
def _GetUpstreamFallback(self) -> Optional[str]:
# MetaProjects (the manifest repo and repo itself) do not have
# defaults in a manifest. Returning None here also avoids
# loading the manifest during `repo init`, before manifest.xml
# has been linked into .repo/.
return None
def _SharingProjectHasShallow(self) -> bool:
# MetaProjects (the manifest repo and repo itself) are never
# shared with other projects in the manifest. Returning False
# here also avoids loading the manifest during `repo init`,
# before manifest.xml has been linked into .repo/.
return False
@property
def HasChanges(self):
"""Has the remote received new commits not yet checked out?"""
@@ -5112,9 +5191,9 @@ class ManifestProject(MetaProject):
if is_new:
default_branch = self.ResolveRemoteHead()
if default_branch is None:
# If the remote doesn't have HEAD configured, default to
# master.
default_branch = "refs/heads/master"
# If the remote doesn't have HEAD configured, fall back
# to whatever git uses as its default branch.
default_branch = _DefaultBranchFallback()
self.revisionExpr = default_branch
else:
self.PreSync()
-1
View File
@@ -94,7 +94,6 @@ It is equivalent to "git branch -D <branchname>".
def Execute(self, opt, args):
nb = args[0].split()
self.TryOverrideManifestWithSmartSync()
err = collections.defaultdict(list)
success = collections.defaultdict(list)
aggregate_errors = []
-2
View File
@@ -243,8 +243,6 @@ without iterating through the remaining projects.
mirror = self.manifest.IsMirror
self.TryOverrideManifestWithSmartSync()
if opt.regex:
projects = self.FindProjects(args, all_manifests=all_trees)
elif opt.inverse_regex:
-2
View File
@@ -147,8 +147,6 @@ class Info(PagedCommand):
if not opt.this_manifest_only:
self.manifest = self.manifest.outer_client
self.TryOverrideManifestWithSmartSync()
output_format = OutputFormat[opt.format.upper()]
if output_format == OutputFormat.JSON:
self._ExecuteJson(opt, args)
+1
View File
@@ -33,6 +33,7 @@ _REPO_ALLOW_SHALLOW = os.environ.get("REPO_ALLOW_SHALLOW")
class Init(InteractiveCommand, MirrorSafeCommand):
COMMON = True
RESPECT_SMART_SYNC_OVERRIDE = False
MULTI_MANIFEST_SUPPORT = True
helpSummary = "Initialize a repo client checkout in the current directory"
helpUsage = """
+14 -1
View File
@@ -16,7 +16,9 @@ import sys
from color import Coloring
from command import Command
from error import GitError
from git_command import GitCommand
from project import Project
from repo_logging import RepoLogger
@@ -30,6 +32,17 @@ class RebaseColoring(Coloring):
self.fail = self.printer("fail", fg="red")
def _ResolveOntoManifest(project: Project) -> str:
"""Resolve project's revisionExpr to a local tracking branch.
Falls back to the raw revisionExpr if ToLocal fails or raises GitError.
"""
try:
return project.GetRemote().ToLocal(project.revisionExpr)
except GitError:
return project.revisionExpr
class Rebase(Command):
COMMON = True
helpSummary = "Rebase local branches on upstream branch"
@@ -162,7 +175,7 @@ branch but need to incorporate new upstream changes "underneath" them.
args = common_args[:]
if opt.onto_manifest:
args.append("--onto")
args.append(project.revisionExpr)
args.append(_ResolveOntoManifest(project))
args.append(upbranch.LocalMerge)
-1
View File
@@ -104,7 +104,6 @@ revision specified in the manifest.
def Execute(self, opt, args):
nb = args[0]
self.TryOverrideManifestWithSmartSync()
err_projects = []
err = []
projects = []
+53 -8
View File
@@ -312,6 +312,7 @@ class TeeStringIO(io.StringIO):
class Sync(Command, MirrorSafeCommand):
COMMON = True
RESPECT_SMART_SYNC_OVERRIDE = False
MULTI_MANIFEST_SUPPORT = True
helpSummary = "Update working tree to the latest revision"
helpUsage = """
@@ -378,8 +379,12 @@ resumeable bundle file on a content delivery network. This
may be necessary if there are problems with the local Python
HTTP client or proxy configuration, but the Git binary works.
The --fetch-submodules option enables fetching Git submodules
of a project from server.
The --recurse-submodules option enables syncing Git submodules of all projects
from the server. The --no-recurse-submodules option disables syncing Git
submodules, even when a project has sync-s="true" in the manifest.
The --fetch-submodules and --no-fetch-submodules options are deprecated aliases
for --recurse-submodules and --no-recurse-submodules, respectively.
The -c/--current-branch option can be used to only fetch objects that
are on the branch specified by a project's revision.
@@ -427,6 +432,15 @@ later is required to fix a server side protocol bug.
_JOBS_WARN_THRESHOLD = 100
@staticmethod
def _deprecated_submodules_option(option, opt_str, _value, parser):
enabled = opt_str == "--fetch-submodules"
replacement = (
"--recurse-submodules" if enabled else "--no-recurse-submodules"
)
logger.warning("%s is deprecated; use %s instead", opt_str, replacement)
setattr(parser.values, option.dest, enabled)
def _Options(self, p, show_smart=True):
p.add_option(
"--jobs-network",
@@ -545,6 +559,13 @@ later is required to fix a server side protocol bug.
help="temporary manifest to use for this sync",
metavar="NAME.xml",
)
p.add_option(
"-g",
"--groups",
help="sync projects matching the specific groups. Not persistent "
"unlike when used on init",
metavar="GROUP",
)
p.add_option(
"--clone-bundle",
action="store_true",
@@ -569,9 +590,29 @@ later is required to fix a server side protocol bug.
help="password to authenticate with the manifest server",
)
p.add_option(
"--fetch-submodules",
"--recurse-submodules",
action="store_true",
help="fetch submodules from server",
help="sync submodules from server",
)
p.add_option(
"--no-recurse-submodules",
dest="recurse_submodules",
action="store_false",
help="don't sync submodules from server",
)
p.add_option(
"--fetch-submodules",
dest="recurse_submodules",
action="callback",
callback=self._deprecated_submodules_option,
help=optparse.SUPPRESS_HELP,
)
p.add_option(
"--no-fetch-submodules",
dest="recurse_submodules",
action="callback",
callback=self._deprecated_submodules_option,
help=optparse.SUPPRESS_HELP,
)
p.add_option(
"--use-superproject",
@@ -741,8 +782,9 @@ later is required to fix a server side protocol bug.
all_projects = self.GetProjects(
args,
groups=opt.groups,
missing_ok=True,
submodules_ok=opt.fetch_submodules,
submodules_ok=opt.recurse_submodules,
manifest=manifest,
all_manifests=not opt.this_manifest_only,
)
@@ -1070,8 +1112,9 @@ later is required to fix a server side protocol bug.
self._ReloadManifest(None, manifest)
all_projects = self.GetProjects(
args,
groups=opt.groups,
missing_ok=True,
submodules_ok=opt.fetch_submodules,
submodules_ok=opt.recurse_submodules,
manifest=manifest,
all_manifests=not opt.this_manifest_only,
)
@@ -2317,8 +2360,9 @@ later is required to fix a server side protocol bug.
all_projects = self.GetProjects(
args,
groups=opt.groups,
missing_ok=True,
submodules_ok=opt.fetch_submodules,
submodules_ok=opt.recurse_submodules,
manifest=manifest,
all_manifests=not opt.this_manifest_only,
)
@@ -2980,8 +3024,9 @@ later is required to fix a server side protocol bug.
self._ReloadManifest(None, manifest)
project_list = self.GetProjects(
args,
groups=opt.groups,
missing_ok=True,
submodules_ok=opt.fetch_submodules,
submodules_ok=opt.recurse_submodules,
manifest=manifest,
all_manifests=not opt.this_manifest_only,
)
+31
View File
@@ -14,6 +14,8 @@
"""Unittests for the command.py module."""
import pytest
from command import Command
@@ -86,3 +88,32 @@ def test_get_projects_keeps_derived_subprojects_for_repeated_repo():
projects = cmd.GetProjects([])
assert set(projects) == {project_a, project_b, submodule_a, submodule_b}
@pytest.mark.parametrize(
"submodules_ok, sync_s, includes_submodule",
[
(None, False, False),
(None, True, True),
(True, False, True),
(True, True, True),
(False, False, False),
(False, True, False),
],
)
def test_get_projects_submodule_override(
submodules_ok, sync_s, includes_submodule
):
"""The CLI override takes precedence over a project's sync-s setting."""
submodule = FakeProject("submodule", "project/submodule")
project = FakeProject(
"project",
"project",
derived_subprojects=[submodule],
sync_s=sync_s,
)
cmd = Command(manifest=FakeManifest([project]))
projects = cmd.GetProjects([], submodules_ok=submodules_ok)
assert (submodule in projects) is includes_submodule
+34
View File
@@ -105,3 +105,37 @@ def test_post_sync_argument_validation() -> None:
finally:
sys.stderr = old_stderr
@pytest.mark.parametrize("yes_val", (True, False))
def test_repo_upload_yes_arg(tmp_path, yes_val: bool) -> None:
"""Test that yes is passed in kwargs during hook execution."""
class FakeProject:
def __init__(self, worktree):
self.worktree = worktree
self.enabled_repo_hooks = ["pre-upload"]
self.config = None
hook_file = tmp_path / "pre-upload.py"
hook_content = """
def main(project_list, **kwargs):
project_list.append(kwargs.get("yes"))
"""
hook_file.write_text(hook_content)
hook = hooks.RepoHook(
hook_type="pre-upload",
hooks_project=FakeProject(str(tmp_path)),
repo_topdir=str(tmp_path),
manifest_url="https://gerrit",
allow_all_hooks=True,
yes=yes_val,
)
project_list = []
res = hook.Run(project_list=project_list, worktree_list=[])
assert res is True
assert project_list == [yes_val]
+292 -3
View File
@@ -20,10 +20,11 @@ from pathlib import Path
import shutil
import subprocess
import tempfile
from typing import Optional
from typing import Dict, List, Optional, Tuple
import unittest
from unittest import mock
import pytest
import utils_for_test
import error
@@ -716,6 +717,60 @@ class ManifestPropertiesFetchedCorrectly(unittest.TestCase):
fakeproj.config.SetString("manifest.platform", "auto")
self.assertEqual(fakeproj.manifest_platform, "auto")
def test_check_immutable_revision_metaproject_skips_manifest_load(self):
"""MetaProjects must not parse manifest.xml during immutable check.
During `repo init` the manifestProject's own Sync_NetworkHalf runs
before manifest.xml has been linked into .repo/, so
_CheckForImmutableRevision must not touch it.
"""
with utils_for_test.TempGitTree() as tempdir:
fakeproj = self.setUpManifest(tempdir)
manifest_path = os.path.join(
tempdir, ".repo", manifest_xml.MANIFEST_FILE_NAME
)
self.assertFalse(os.path.exists(manifest_path))
fakeproj.revisionExpr = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
fakeproj.upstream = "refs/heads/main"
# Must return False without raising ManifestParseError, and
# must leave the absent manifest.xml untouched.
self.assertFalse(
fakeproj._CheckForImmutableRevision(use_superproject=None)
)
self.assertFalse(os.path.exists(manifest_path))
def test_get_upstream_fallback_metaproject_skips_manifest_load(
self,
) -> None:
"""MetaProjects must not parse manifest.xml during upstream fallback."""
with utils_for_test.TempGitTree() as tempdir:
fakeproj = self.setUpManifest(tempdir)
manifest_path = os.path.join(
tempdir, ".repo", manifest_xml.MANIFEST_FILE_NAME
)
self.assertFalse(os.path.exists(manifest_path))
self.assertIsNone(fakeproj._GetUpstreamFallback())
self.assertFalse(os.path.exists(manifest_path))
def test_sharing_project_has_shallow_metaproject_skips_manifest_load(
self,
) -> None:
"""MetaProjects must not parse manifest.xml during sharing shallow
check."""
with utils_for_test.TempGitTree() as tempdir:
fakeproj = self.setUpManifest(tempdir)
manifest_path = os.path.join(
tempdir, ".repo", manifest_xml.MANIFEST_FILE_NAME
)
self.assertFalse(os.path.exists(manifest_path))
self.assertFalse(fakeproj._SharingProjectHasShallow())
self.assertFalse(os.path.exists(manifest_path))
def test_sync_use_local_gitdirs_worktree_conflict(self):
"""Test that --use-local-gitdirs conflicts with --worktree."""
with utils_for_test.TempGitTree() as tempdir:
@@ -789,6 +844,60 @@ class ManifestPropertiesFetchedCorrectly(unittest.TestCase):
self.assertFalse(os.path.exists(proj.gitdir))
_VAR_CMD: List[str] = ["var", "GIT_DEFAULT_BRANCH"]
_CONFIG_CMD: List[str] = ["config", "--get", "init.defaultBranch"]
@pytest.mark.parametrize(
"responses, expected_ref, expected_calls",
(
# git >= 2.35 answers `git var GIT_DEFAULT_BRANCH`.
({"var": (0, "jellybean\n")}, "refs/heads/jellybean", [_VAR_CMD]),
# Older git: `git var` fails, so read init.defaultBranch instead.
(
{"var": (1, ""), "config": (0, "custom\n")},
"refs/heads/custom",
[_VAR_CMD, _CONFIG_CMD],
),
# Nothing configured anywhere: git's historical built-in default.
(
{"var": (1, ""), "config": (1, "")},
"refs/heads/master",
[_VAR_CMD, _CONFIG_CMD],
),
),
ids=("git_var", "old_git_reads_config", "unconfigured_defaults_to_master"),
)
def test_default_branch_fallback(
responses: Dict[str, Tuple[int, str]],
expected_ref: str,
expected_calls: List[List[str]],
) -> None:
"""_DefaultBranchFallback resolves the default branch via git."""
seen: List[List[str]] = []
class FakeGitCommand:
# Emulate git by returning the canned response for the subcommand.
def __init__(
self, project_: Optional[project.Project], cmdv: List[str], **kwargs
) -> None:
self.returncode, self.stdout = responses[cmdv[0]]
seen.append(cmdv)
def Wait(self) -> int:
return self.returncode
# The result is memoized, so clear it before (to bypass any cached real
# value) and after (so the mocked value doesn't leak to other tests).
project._DefaultBranchFallback.cache_clear()
try:
with mock.patch.object(project, "GitCommand", FakeGitCommand):
assert project._DefaultBranchFallback() == expected_ref
assert seen == expected_calls
finally:
project._DefaultBranchFallback.cache_clear()
def _create_mock_project(
tempdir,
use_local_gitdirs=False,
@@ -965,13 +1074,20 @@ class StatelessSyncTests(unittest.TestCase):
class SyncOptimizationTests(unittest.TestCase):
"""Tests for sync optimization logic involving shallow clones."""
def _get_project(self, tempdir, depth=None):
def _get_project(
self,
tempdir: str,
depth: Optional[int] = None,
revisionExpr: Optional[str] = None,
) -> project.Project:
if revisionExpr is None:
revisionExpr = "0123456789abcdef0123456789abcdef01234567"
proj = _create_mock_project(
tempdir,
depth=depth,
gitdir=os.path.join(tempdir, "gitdir"),
objdir=os.path.join(tempdir, "objdir"),
revisionExpr="0123456789abcdef0123456789abcdef01234567",
revisionExpr=revisionExpr,
)
proj._CheckForImmutableRevision = mock.MagicMock(return_value=True)
proj.DeleteWorktree = mock.MagicMock()
@@ -1198,6 +1314,179 @@ class SyncOptimizationTests(unittest.TestCase):
self.assertTrue(res)
mock_git_cmd.assert_not_called()
def test_remote_fetch_sha1_upstream_fallback(self) -> None:
"""Test _RemoteFetch resolves upstream fallback for SHA-1 revisions."""
sha = "4f8a3c0000000000000000000000000000000000"
with utils_for_test.TempGitTree() as tempdir:
proj = self._get_project(tempdir, revisionExpr=sha)
proj._CheckForImmutableRevision.side_effect = [False, True]
proj.upstream = None
proj.dest_branch = "my-dest-branch"
mock_remote = mock.MagicMock()
mock_remote.name = "origin"
def _to_local(r: str) -> str:
if r.startswith("refs/heads/"):
return "refs/remotes/origin/" + r[11:]
return r
mock_remote.ToLocal.side_effect = _to_local
mock_remote.PreConnectFetch.return_value = True
proj.GetRemote = mock.MagicMock(return_value=mock_remote)
with mock.patch("project.GitCommand") as mock_git_cmd:
mock_cmd_instance = mock.MagicMock()
mock_cmd_instance.Wait.return_value = 0
mock_git_cmd.return_value = mock_cmd_instance
res = proj._RemoteFetch(current_branch_only=True)
self.assertTrue(res)
mock_git_cmd.assert_called_once()
cmd_args = mock_git_cmd.call_args[0][1]
self.assertIn(
"+refs/heads/my-dest-branch:"
"refs/remotes/origin/my-dest-branch",
cmd_args,
)
self.assertNotIn(
"+refs/heads/*:refs/remotes/origin/*", cmd_args
)
def test_remote_fetch_sha1_manifest_default_fallback(self) -> None:
"""Test _RemoteFetch upstream fallback from manifest defaults."""
sha = "4f8a3c0000000000000000000000000000000000"
with utils_for_test.TempGitTree() as tempdir:
proj = self._get_project(tempdir, revisionExpr=sha)
proj._CheckForImmutableRevision.side_effect = [False, True]
proj.upstream = None
proj.dest_branch = None
proj.manifest.default.upstreamExpr = "manifest-upstream"
mock_remote = mock.MagicMock()
mock_remote.name = "origin"
def _to_local(r: str) -> str:
if r.startswith("refs/heads/"):
return "refs/remotes/origin/" + r[11:]
return r
mock_remote.ToLocal.side_effect = _to_local
mock_remote.PreConnectFetch.return_value = True
proj.GetRemote = mock.MagicMock(return_value=mock_remote)
with mock.patch("project.GitCommand") as mock_git_cmd:
mock_cmd_instance = mock.MagicMock()
mock_cmd_instance.Wait.return_value = 0
mock_git_cmd.return_value = mock_cmd_instance
res = proj._RemoteFetch(current_branch_only=True)
self.assertTrue(res)
mock_git_cmd.assert_called_once()
cmd_args = mock_git_cmd.call_args[0][1]
self.assertIn(
"+refs/heads/manifest-upstream:"
"refs/remotes/origin/manifest-upstream",
cmd_args,
)
self.assertNotIn(
"+refs/heads/*:refs/remotes/origin/*", cmd_args
)
def test_remote_fetch_sha1_tag_fallback(self) -> None:
"""Test _RemoteFetch resolves upstream fallback to tag correctly."""
sha = "4f8a3c0000000000000000000000000000000000"
with utils_for_test.TempGitTree() as tempdir:
proj = self._get_project(tempdir, revisionExpr=sha)
proj._CheckForImmutableRevision.side_effect = [False, True]
proj.upstream = None
proj.dest_branch = "refs/tags/v1.0"
mock_remote = mock.MagicMock()
mock_remote.name = "origin"
def _to_local(r: str) -> str:
if r.startswith("refs/tags/"):
return "refs/tags/" + r[10:]
return r
mock_remote.ToLocal.side_effect = _to_local
mock_remote.PreConnectFetch.return_value = True
proj.GetRemote = mock.MagicMock(return_value=mock_remote)
with mock.patch("project.GitCommand") as mock_git_cmd:
mock_cmd_instance = mock.MagicMock()
mock_cmd_instance.Wait.return_value = 0
mock_git_cmd.return_value = mock_cmd_instance
res = proj._RemoteFetch(current_branch_only=True)
self.assertTrue(res)
mock_git_cmd.assert_called_once()
cmd_args = mock_git_cmd.call_args[0][1]
self.assertIn("tag", cmd_args)
self.assertIn("v1.0", cmd_args)
self.assertNotIn(
"+refs/heads/*:refs/remotes/origin/*", cmd_args
)
def test_remote_fetch_sha1_metaproject_without_manifest_xml(self) -> None:
"""Test MetaProject _RemoteFetch with SHA-1 fetches all branches."""
sha = "4f8a3c0000000000000000000000000000000000"
with utils_for_test.TempGitTree() as tempdir:
repodir = os.path.join(tempdir, ".repo")
manifest_dir = os.path.join(repodir, "manifests")
manifest_file = os.path.join(
repodir, manifest_xml.MANIFEST_FILE_NAME
)
os.mkdir(repodir)
os.mkdir(manifest_dir)
manifest = manifest_xml.XmlManifest(repodir, manifest_file)
proj = project.ManifestProject(
manifest,
"test/manifest",
os.path.join(tempdir, ".git"),
tempdir,
)
proj.revisionExpr = sha
proj.upstream = None
proj._CheckForImmutableRevision = mock.MagicMock(return_value=False)
mock_remote = mock.MagicMock()
mock_remote.name = "origin"
def _to_local(r: str) -> str:
if r.startswith("refs/heads/"):
return "refs/remotes/origin/" + r[11:]
return r
mock_remote.ToLocal.side_effect = _to_local
mock_remote.PreConnectFetch.return_value = True
proj.GetRemote = mock.MagicMock(return_value=mock_remote)
with mock.patch("project.GitCommand") as mock_git_cmd:
mock_cmd_instance = mock.MagicMock()
mock_cmd_instance.Wait.return_value = 0
mock_git_cmd.return_value = mock_cmd_instance
res = proj._RemoteFetch(current_branch_only=True)
self.assertTrue(res)
mock_git_cmd.assert_called_once()
cmd_args = mock_git_cmd.call_args[0][1]
self.assertIn("+refs/heads/*:refs/remotes/origin/*", cmd_args)
def test_remote_fetch_sha1_none_manifest_default(self) -> None:
"""Test _GetUpstreamFallback when manifest.default is None."""
sha = "4f8a3c0000000000000000000000000000000000"
with utils_for_test.TempGitTree() as tempdir:
proj = self._get_project(tempdir, revisionExpr=sha)
proj.dest_branch = None
proj.manifest.default = None
self.assertIsNone(proj._GetUpstreamFallback())
class GetEnvVarsTests(unittest.TestCase):
"""Tests for GetEnvVars project environment variable generation."""
+1 -1
View File
@@ -24,7 +24,7 @@ class GcCommand(unittest.TestCase):
"""Tests for gc command."""
def setUp(self):
self.cmd = gc.Gc()
self.cmd = gc.Gc(manifest=mock.MagicMock())
self.opt, self.args = self.cmd.OptionParser.parse_args([])
self.opt.this_manifest_only = False
self.opt.repack = False
+50
View File
@@ -0,0 +1,50 @@
# 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.
"""Unittests for the subcmds/rebase.py module."""
from unittest import mock
from error import GitError
from subcmds import rebase
def test_resolve_onto_manifest_success() -> None:
"""Test _ResolveOntoManifest when ToLocal succeeds."""
project = mock.MagicMock()
project.revisionExpr = "main"
remote = mock.MagicMock()
remote.ToLocal.return_value = "refs/remotes/goog/main"
project.GetRemote.return_value = remote
res = rebase._ResolveOntoManifest(project)
assert res == "refs/remotes/goog/main"
project.GetRemote.assert_called_once()
remote.ToLocal.assert_called_once_with("main")
def test_resolve_onto_manifest_fallback() -> None:
"""Test _ResolveOntoManifest when ToLocal raises GitError."""
project = mock.MagicMock()
project.revisionExpr = "main"
remote = mock.MagicMock()
remote.ToLocal.side_effect = GitError("Failed to resolve")
project.GetRemote.return_value = remote
res = rebase._ResolveOntoManifest(project)
assert res == "main"
project.GetRemote.assert_called_once()
remote.ToLocal.assert_called_once_with("main")
+169
View File
@@ -15,6 +15,7 @@
import json
import os
from pathlib import Path
import shutil
import tempfile
import time
@@ -26,10 +27,51 @@ import pytest
import command
from error import GitError
from error import RepoExitError
import manifest_xml
from project import SyncNetworkHalfResult
from subcmds import sync
@pytest.mark.parametrize(
"cli_args, expected",
[
([], None),
(["--recurse-submodules"], True),
(["--no-recurse-submodules"], False),
(["--fetch-submodules"], True),
(["--no-fetch-submodules"], False),
],
)
def test_recurse_submodules_option(cli_args, expected):
"""The submodule flags preserve an unset manifest-driven state."""
cmd = sync.Sync()
opts, _ = cmd.OptionParser.parse_args(cli_args)
assert opts.recurse_submodules is expected
@pytest.mark.parametrize(
"old_flag, new_flag",
[
("--fetch-submodules", "--recurse-submodules"),
("--no-fetch-submodules", "--no-recurse-submodules"),
],
)
def test_recurse_submodules_option_deprecation(old_flag, new_flag):
"""The old submodule flags warn and direct users to their replacements."""
cmd = sync.Sync()
with mock.patch.object(sync.logger, "warning") as warning:
cmd.OptionParser.parse_args([old_flag])
warning.assert_called_once_with(
"%s is deprecated; use %s instead", old_flag, new_flag
)
assert old_flag not in cmd.OptionParser.format_help()
@pytest.mark.parametrize(
"use_superproject, cli_args, result",
[
@@ -56,6 +98,120 @@ def test_get_current_branch_only(use_superproject, cli_args, result):
assert cmd._GetCurrentBranchOnly(opts, cmd.manifest) == result
@pytest.mark.parametrize(
"cli_args, expected_groups",
[
([], None),
(["-g", "groupA"], "groupA"),
(["--groups=groupB,groupC"], "groupB,groupC"),
],
)
def test_groups_option_parsing(cli_args, expected_groups):
"""Test --groups / -g option parsing."""
cmd = sync.Sync()
opts, _ = cmd.OptionParser.parse_args(cli_args)
assert opts.groups == expected_groups
def _create_manifest_with_groups(topdir: Path) -> manifest_xml.XmlManifest:
"""Create a test XmlManifest with projects assigned to various groups."""
repodir = topdir / ".repo"
manifest_dir = repodir / "manifests"
manifest_file = repodir / manifest_xml.MANIFEST_FILE_NAME
repodir.mkdir(exist_ok=True)
manifest_dir.mkdir(exist_ok=True)
gitdir = repodir / "manifests.git"
gitdir.mkdir(exist_ok=True)
(gitdir / "config").write_text(
"""[remote "origin"]
url = https://localhost:0/manifest
""",
encoding="utf-8",
)
manifest_file.write_text(
"""
<manifest>
<remote name="origin" fetch="http://localhost" />
<default remote="origin" revision="refs/heads/main" />
<project name="proj_g1" path="path_g1" groups="group1" />
<project name="proj_g2" path="path_g2" groups="group2" />
<project name="proj_g1_g2" path="path_g1_g2"
groups="group1,group2" />
<project name="proj_default" path="path_default" />
<project name="proj_notdefault" path="path_notdefault"
groups="notdefault" />
</manifest>
""",
encoding="utf-8",
)
for p in [
"proj_g1",
"proj_g2",
"proj_g1_g2",
"proj_default",
"proj_notdefault",
]:
(repodir / "projects" / f"{p}.git").mkdir(parents=True, exist_ok=True)
return manifest_xml.XmlManifest(str(repodir), str(manifest_file))
@pytest.mark.parametrize(
"cli_args, expected_projects",
[
(["-g", "group1"], ["proj_g1", "proj_g1_g2"]),
(["-g", "group2"], ["proj_g2", "proj_g1_g2"]),
(["-g", "group1,group2"], ["proj_g1", "proj_g1_g2", "proj_g2"]),
(["-g", "default,-group1"], ["proj_default", "proj_g2"]),
([], ["proj_default", "proj_g1", "proj_g1_g2", "proj_g2"]),
],
)
def test_sync_groups_manifest_filtering(
tmp_path: Path, cli_args, expected_projects
):
"""Test that repo sync -g selects only matching projects."""
manifest = _create_manifest_with_groups(tmp_path)
cmd = sync.Sync()
cmd.manifest = manifest
opts, args = cmd.OptionParser.parse_args(cli_args)
projects = cmd.GetProjects(args, groups=opts.groups, missing_ok=True)
project_names = sorted([p.name for p in projects])
assert project_names == sorted(expected_projects)
def test_sync_update_projects_revision_id_respects_groups(tmp_path: Path):
"""Test that _UpdateProjectsRevisionId filters projects using opt.groups."""
manifest = _create_manifest_with_groups(tmp_path)
cmd = sync.Sync()
cmd.manifest = manifest
superproject = mock.MagicMock()
superproject.UpdateProjectsRevisionId.return_value = mock.MagicMock(
manifest_path=None
)
manifest._superproject = superproject
opts, args = cmd.OptionParser.parse_args(["-g", "group1"])
opts.verbose = False
opts.fetch_submodules = False
opts.this_manifest_only = True
opts.local_only = False
with mock.patch.object(
cmd, "GetProjects", wraps=cmd.GetProjects
) as spy_get_projects:
with mock.patch.object(cmd, "ManifestList", return_value=[manifest]):
cmd._UpdateProjectsRevisionId(opts, args, {}, manifest)
spy_get_projects.assert_called_once()
_, kwargs = spy_get_projects.call_args
assert kwargs.get("groups") == "group1"
# Used to patch os.cpu_count() for reliable results.
OS_CPU_COUNT = 24
@@ -792,6 +948,19 @@ class SyncCommand(unittest.TestCase):
self.assertIn(self.sync_local_half_error, e.aggregate_errors)
self.assertIn(self.sync_network_half_error, e.aggregate_errors)
def test_groups_passed_to_get_projects(self):
"""Ensure Execute passes opt.groups to GetProjects."""
self.opt.groups = "my_group"
self.opt.mp_update = False
with mock.patch.object(self.cmd, "_UpdateRepoProject"):
with mock.patch.object(self.cmd, "_ValidateOptionsWithManifest"):
with mock.patch.object(self.cmd, "_SyncInterleaved"):
with mock.patch.object(self.cmd, "_RunPostSyncHook"):
self.cmd.Execute(self.opt, [])
self.cmd.GetProjects.assert_called()
_, kwargs = self.cmd.GetProjects.call_args
self.assertEqual(kwargs.get("groups"), "my_group")
class SyncUpdateRepoProject(unittest.TestCase):
"""Tests for Sync._UpdateRepoProject."""