mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-09-01 20:40:11 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b85886fa9f | ||
|
|
d9da609d8c | ||
|
|
4bec297eb6 | ||
|
|
54fa31cd84 | ||
|
|
29b6630a65 | ||
|
|
0b82311632 | ||
|
|
06c4f9e1cc | ||
|
|
dd1130352b | ||
|
|
eeba6f268d | ||
|
|
1729aaebae | ||
|
|
978adb7ea5 | ||
|
|
0398c6718e | ||
|
|
3bb4871c44 |
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user