Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package git-repo for openSUSE:Factory checked in at 2026-08-06 16:26:46 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/git-repo (Old) and /work/SRC/openSUSE:Factory/.git-repo.new.16738 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "git-repo" Thu Aug 6 16:26:46 2026 rev:19 rq:1369939 version:2.66 Changes: -------- --- /work/SRC/openSUSE:Factory/git-repo/git-repo.changes 2026-07-14 13:57:07.073779680 +0200 +++ /work/SRC/openSUSE:Factory/.git-repo.new.16738/git-repo.changes 2026-08-06 16:28:45.546149234 +0200 @@ -1,0 +2,17 @@ +Wed Aug 05 16:32:25 UTC 2026 - BenoƮt Monin <[email protected]> + +- Update to version 2.66: + * project: preserve -c optimization when revision is a SHA-1 + * command: Respect smart sync override declaratively by default + * project: derive HEAD fallback from git's own default branch + * project: make GetHead file-read fallback reftable-aware + * sync: allow syncing groups with repo sync -g group + * git_superproject: don't filter rewritten manifest + * sync: Deprecate fetch-submodules flag names + * hooks: pass yes flag when available + * project: Skip superproject upstream check for MetaProjects + * sync: Add CLI flag for globally disabling submodule fetch + * color: Replace anonymous sentinel with named class + * rebase: Resolve revisionExpr to tracking branch for --onto-manifest + +------------------------------------------------------------------- Old: ---- git-repo-2.65.tar.xz New: ---- git-repo-2.66.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ git-repo.spec ++++++ --- /var/tmp/diff_new_pack.3PaNvU/_old 2026-08-06 16:28:46.566184837 +0200 +++ /var/tmp/diff_new_pack.3PaNvU/_new 2026-08-06 16:28:46.570184977 +0200 @@ -17,7 +17,7 @@ Name: git-repo -Version: 2.65 +Version: 2.66 Release: 0 Summary: The Multiple Git Repository Tool License: Apache-2.0 ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.3PaNvU/_old 2026-08-06 16:28:46.614186513 +0200 +++ /var/tmp/diff_new_pack.3PaNvU/_new 2026-08-06 16:28:46.618186653 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://gerrit.googlesource.com/git-repo</param> - <param name="changesrevision">35bbf701d04de5c6a71937279bc3d16f6ce36808</param></service></servicedata> + <param name="changesrevision">d9da609d8c120bb882a43196a4a6b7f183418304</param></service></servicedata> (No newline at EOF) ++++++ git-repo-2.65.tar.xz -> git-repo-2.66.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/color.py new/git-repo-2.66/color.py --- old/git-repo-2.65/color.py 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/color.py 2026-08-05 00:00:19.000000000 +0200 @@ -84,9 +84,14 @@ 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 = { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/command.py new/git-repo-2.66/command.py --- old/git-repo-2.65/command.py 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/command.py 2026-08-05 00:00:19.000000000 +0200 @@ -17,6 +17,7 @@ import optparse import os import re +from typing import TYPE_CHECKING from error import InvalidProjectGroupsError from error import NoSuchProjectError @@ -25,6 +26,10 @@ 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 @@ # 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 @@ # 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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(): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/completion.zsh new/git-repo-2.66/completion.zsh --- old/git-repo-2.65/completion.zsh 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/completion.zsh 2026-08-05 00:00:19.000000000 +0200 @@ -322,7 +322,10 @@ '--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]' \ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/docs/repo-hooks.md new/git-repo-2.66/docs/repo-hooks.md --- old/git-repo-2.65/docs/repo-hooks.md 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/docs/repo-hooks.md 2026-08-05 00:00:19.000000000 +0200 @@ -83,6 +83,13 @@ 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 @@ 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 @@ 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. """ ``` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/git_superproject.py new/git-repo-2.66/git_superproject.py --- old/git-repo-2.65/git_superproject.py 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/git_superproject.py 2026-08-05 00:00:19.000000000 +0200 @@ -473,7 +473,7 @@ ) return None manifest_str = self._manifest.ToXml( - filter_groups=self._manifest.GetManifestGroupsStr(), + filter_groups="all", omit_local=True, ).toxml() manifest_path = self._manifest_path diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/hooks.py new/git-repo-2.66/hooks.py --- old/git-repo-2.65/hooks.py 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/hooks.py 2026-08-05 00:00:19.000000000 +0200 @@ -68,6 +68,7 @@ allow_all_hooks=False, ignore_hooks=False, abort_if_user_denies=False, + yes=False, ): """RepoHook constructor. @@ -89,6 +90,7 @@ 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 @@ 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 @@ # 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 @@ "origin" ).url, "bug_url": manifest.contactinfo.bugurl, + "yes": getattr(opt, "yes", False), } ) return cls(*args, **kwargs) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/man/repo-smartsync.1 new/git-repo-2.66/man/repo-smartsync.1 --- old/git-repo-2.65/man/repo-smartsync.1 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/man/repo-smartsync.1 2026-08-05 00:00:19.000000000 +0200 @@ -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 @@ \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 @@ \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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/man/repo-sync.1 new/git-repo-2.66/man/repo-sync.1 --- old/git-repo-2.65/man/repo-sync.1 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/man/repo-sync.1 2026-08-05 00:00:19.000000000 +0200 @@ -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 @@ \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 @@ \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 @@ 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. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/project.py new/git-repo-2.66/project.py --- old/git-repo-2.65/project.py 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/project.py 2026-08-05 00:00:19.000000000 +0200 @@ -15,6 +15,7 @@ import datetime import errno import filecmp +import functools import glob import os import platform @@ -2674,11 +2675,12 @@ # 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 @@ # 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 @@ 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,18 @@ return True + def _GetUpstreamFallback(self) -> Optional[str]: + """Resolve a fallback upstream ref when revisionExpr is a SHA-1.""" + for cand in ( + self.dest_branch, + self.manifest.default.upstreamExpr, + self.manifest.default.destBranchExpr, + self.manifest.default.revisionExpr, + ): + if cand and not IsId(cand): + return cand + return None + def _RemoteFetch( self, name=None, @@ -2880,14 +2902,31 @@ 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 +2944,6 @@ "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 +3055,11 @@ # 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 +4416,12 @@ 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 +4681,26 @@ self._pending_failures = [] [email protected]_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 +4714,7 @@ worktree=worktree, remote=RemoteSpec("origin"), relpath=".repo/%s" % name, - revisionExpr="refs/heads/master", + revisionExpr=_DefaultBranchFallback(), revisionId=None, groups=None, ) @@ -4677,6 +4728,15 @@ 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 + @property def HasChanges(self): """Has the remote received new commits not yet checked out?""" @@ -5112,9 +5172,9 @@ 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() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/subcmds/abandon.py new/git-repo-2.66/subcmds/abandon.py --- old/git-repo-2.65/subcmds/abandon.py 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/subcmds/abandon.py 2026-08-05 00:00:19.000000000 +0200 @@ -94,7 +94,6 @@ def Execute(self, opt, args): nb = args[0].split() - self.TryOverrideManifestWithSmartSync() err = collections.defaultdict(list) success = collections.defaultdict(list) aggregate_errors = [] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/subcmds/forall.py new/git-repo-2.66/subcmds/forall.py --- old/git-repo-2.65/subcmds/forall.py 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/subcmds/forall.py 2026-08-05 00:00:19.000000000 +0200 @@ -243,8 +243,6 @@ mirror = self.manifest.IsMirror - self.TryOverrideManifestWithSmartSync() - if opt.regex: projects = self.FindProjects(args, all_manifests=all_trees) elif opt.inverse_regex: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/subcmds/info.py new/git-repo-2.66/subcmds/info.py --- old/git-repo-2.65/subcmds/info.py 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/subcmds/info.py 2026-08-05 00:00:19.000000000 +0200 @@ -147,8 +147,6 @@ 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) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/subcmds/init.py new/git-repo-2.66/subcmds/init.py --- old/git-repo-2.65/subcmds/init.py 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/subcmds/init.py 2026-08-05 00:00:19.000000000 +0200 @@ -33,6 +33,7 @@ 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 = """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/subcmds/rebase.py new/git-repo-2.66/subcmds/rebase.py --- old/git-repo-2.65/subcmds/rebase.py 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/subcmds/rebase.py 2026-08-05 00:00:19.000000000 +0200 @@ -16,7 +16,9 @@ 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 @@ 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 @@ args = common_args[:] if opt.onto_manifest: args.append("--onto") - args.append(project.revisionExpr) + args.append(_ResolveOntoManifest(project)) args.append(upbranch.LocalMerge) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/subcmds/start.py new/git-repo-2.66/subcmds/start.py --- old/git-repo-2.65/subcmds/start.py 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/subcmds/start.py 2026-08-05 00:00:19.000000000 +0200 @@ -104,7 +104,6 @@ def Execute(self, opt, args): nb = args[0] - self.TryOverrideManifestWithSmartSync() err_projects = [] err = [] projects = [] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/subcmds/sync.py new/git-repo-2.66/subcmds/sync.py --- old/git-repo-2.65/subcmds/sync.py 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/subcmds/sync.py 2026-08-05 00:00:19.000000000 +0200 @@ -312,6 +312,7 @@ 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 @@ 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 @@ _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", @@ -546,6 +560,13 @@ 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", help="enable use of /clone.bundle on HTTP/HTTPS", @@ -569,9 +590,29 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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, ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/tests/test_command.py new/git-repo-2.66/tests/test_command.py --- old/git-repo-2.65/tests/test_command.py 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/tests/test_command.py 2026-08-05 00:00:19.000000000 +0200 @@ -14,6 +14,8 @@ """Unittests for the command.py module.""" +import pytest + from command import Command @@ -86,3 +88,32 @@ projects = cmd.GetProjects([]) assert set(projects) == {project_a, project_b, submodule_a, submodule_b} + + [email protected]( + "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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/tests/test_hooks.py new/git-repo-2.66/tests/test_hooks.py --- old/git-repo-2.65/tests/test_hooks.py 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/tests/test_hooks.py 2026-08-05 00:00:19.000000000 +0200 @@ -105,3 +105,37 @@ finally: sys.stderr = old_stderr + + [email protected]("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] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/tests/test_project.py new/git-repo-2.66/tests/test_project.py --- old/git-repo-2.65/tests/test_project.py 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/tests/test_project.py 2026-08-05 00:00:19.000000000 +0200 @@ -20,10 +20,11 @@ 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,31 @@ 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_sync_use_local_gitdirs_worktree_conflict(self): """Test that --use-local-gitdirs conflicts with --worktree.""" with utils_for_test.TempGitTree() as tempdir: @@ -789,6 +815,60 @@ self.assertFalse(os.path.exists(proj.gitdir)) +_VAR_CMD: List[str] = ["var", "GIT_DEFAULT_BRANCH"] +_CONFIG_CMD: List[str] = ["config", "--get", "init.defaultBranch"] + + [email protected]( + "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 +1045,20 @@ 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 +1285,124 @@ 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 + ) + class GetEnvVarsTests(unittest.TestCase): """Tests for GetEnvVars project environment variable generation.""" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/tests/test_subcmds_gc.py new/git-repo-2.66/tests/test_subcmds_gc.py --- old/git-repo-2.65/tests/test_subcmds_gc.py 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/tests/test_subcmds_gc.py 2026-08-05 00:00:19.000000000 +0200 @@ -24,7 +24,7 @@ """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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/tests/test_subcmds_rebase.py new/git-repo-2.66/tests/test_subcmds_rebase.py --- old/git-repo-2.65/tests/test_subcmds_rebase.py 1970-01-01 01:00:00.000000000 +0100 +++ new/git-repo-2.66/tests/test_subcmds_rebase.py 2026-08-05 00:00:19.000000000 +0200 @@ -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") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/git-repo-2.65/tests/test_subcmds_sync.py new/git-repo-2.66/tests/test_subcmds_sync.py --- old/git-repo-2.65/tests/test_subcmds_sync.py 2026-07-02 00:59:45.000000000 +0200 +++ new/git-repo-2.66/tests/test_subcmds_sync.py 2026-08-05 00:00:19.000000000 +0200 @@ -15,6 +15,7 @@ import json import os +from pathlib import Path import shutil import tempfile import time @@ -26,11 +27,52 @@ 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 + + [email protected]( + "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() + + [email protected]( "use_superproject, cli_args, result", [ (True, ["--current-branch"], True), @@ -56,6 +98,120 @@ assert cmd._GetCurrentBranchOnly(opts, cmd.manifest) == result [email protected]( + "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)) + + [email protected]( + "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 @@ 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.""" ++++++ git-repo.obsinfo ++++++ --- /var/tmp/diff_new_pack.3PaNvU/_old 2026-08-06 16:28:46.830194053 +0200 +++ /var/tmp/diff_new_pack.3PaNvU/_new 2026-08-06 16:28:46.834194192 +0200 @@ -1,5 +1,5 @@ name: git-repo -version: 2.65 -mtime: 1782946785 -commit: 35bbf701d04de5c6a71937279bc3d16f6ce36808 +version: 2.66 +mtime: 1785880819 +commit: d9da609d8c120bb882a43196a4a6b7f183418304
