mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-09-21 22:30:29 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d27d6829a8 | ||
|
|
0ea57e2eed | ||
|
|
ba8ddf396c | ||
|
|
d88ce8d952 | ||
|
|
5e8d2a6e3a | ||
|
|
e59c9cde99 | ||
|
|
83428a9b26 | ||
|
|
948abc85bc | ||
|
|
c63a2f92fa | ||
|
|
0039e39000 | ||
|
|
5f378458d2 | ||
|
|
d27034bf62 | ||
|
|
6541729a18 | ||
|
|
b85e76a86a | ||
|
|
e5bbb5c9e6 | ||
|
|
4fe87617ff | ||
|
|
d7299422ae | ||
|
|
3f087a8dd9 | ||
|
|
3a6e25af75 | ||
|
|
41c2597509 | ||
|
|
e6ad708009 | ||
|
|
09914bcab7 | ||
|
|
3f1775607f | ||
|
|
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
|
DEFAULT = None
|
||||||
|
|
||||||
|
|
||||||
|
class _CheckConsoleSentinel:
|
||||||
|
"""Sentinel for checking console coloring."""
|
||||||
|
|
||||||
|
|
||||||
# Placholder value that indicates we need to check if the user is in an
|
# 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.
|
# 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
|
# https://git-scm.com/docs/git-config#Documentation/git-config.txt-colorui
|
||||||
_CONFIG_TO_COLOR_SETTING = {
|
_CONFIG_TO_COLOR_SETTING = {
|
||||||
|
|||||||
+26
-4
@@ -17,6 +17,7 @@ import multiprocessing
|
|||||||
import optparse
|
import optparse
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from error import InvalidProjectGroupsError
|
from error import InvalidProjectGroupsError
|
||||||
from error import NoSuchProjectError
|
from error import NoSuchProjectError
|
||||||
@@ -25,6 +26,10 @@ from event_log import EventLog
|
|||||||
import progress
|
import progress
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from project import Project
|
||||||
|
|
||||||
|
|
||||||
# Are we generating man-pages?
|
# Are we generating man-pages?
|
||||||
GENERATE_MANPAGES = os.environ.get("_REPO_GENERATE_MANPAGES_") == " indeed! "
|
GENERATE_MANPAGES = os.environ.get("_REPO_GENERATE_MANPAGES_") == " indeed! "
|
||||||
|
|
||||||
@@ -61,6 +66,10 @@ class Command:
|
|||||||
# command to show short-vs-full summaries.
|
# command to show short-vs-full summaries.
|
||||||
COMMON = False
|
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,
|
# Whether this command supports running in parallel. If greater than 0,
|
||||||
# it is the number of parallel jobs to default to.
|
# it is the number of parallel jobs to default to.
|
||||||
PARALLEL_JOBS = None
|
PARALLEL_JOBS = None
|
||||||
@@ -242,6 +251,12 @@ class Command:
|
|||||||
# from the user's perspective.
|
# from the user's perspective.
|
||||||
opt.outer_manifest = True
|
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):
|
def ValidateOptions(self, opt, args):
|
||||||
"""Validate the user options & arguments before executing.
|
"""Validate the user options & arguments before executing.
|
||||||
|
|
||||||
@@ -375,7 +390,7 @@ class Command:
|
|||||||
manifest=None,
|
manifest=None,
|
||||||
groups="",
|
groups="",
|
||||||
missing_ok=False,
|
missing_ok=False,
|
||||||
submodules_ok=False,
|
submodules_ok=None,
|
||||||
all_manifests=False,
|
all_manifests=False,
|
||||||
):
|
):
|
||||||
"""A list of projects that match the arguments.
|
"""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.
|
manifest: an XmlManifest, the manifest to use, or None for default.
|
||||||
groups: a string, the manifest groups in use.
|
groups: a string, the manifest groups in use.
|
||||||
missing_ok: a boolean, whether to allow missing projects.
|
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
|
all_manifests: a boolean, if True then all manifests and
|
||||||
submanifests are used. If False, then only the local
|
submanifests are used. If False, then only the local
|
||||||
(sub)manifest is used.
|
(sub)manifest is used.
|
||||||
@@ -403,6 +420,11 @@ class Command:
|
|||||||
all_projects_list = manifest.projects
|
all_projects_list = manifest.projects
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
|
def should_include_submodules(project: "Project") -> bool:
|
||||||
|
if submodules_ok is None:
|
||||||
|
return project.sync_s
|
||||||
|
return submodules_ok
|
||||||
|
|
||||||
if not groups:
|
if not groups:
|
||||||
groups = manifest.GetManifestGroupsStr()
|
groups = manifest.GetManifestGroupsStr()
|
||||||
groups = [x for x in re.split(r"[,\s]+", groups) if x]
|
groups = [x for x in re.split(r"[,\s]+", groups) if x]
|
||||||
@@ -410,7 +432,7 @@ class Command:
|
|||||||
if not args:
|
if not args:
|
||||||
derived_projects = {}
|
derived_projects = {}
|
||||||
for project in all_projects_list:
|
for project in all_projects_list:
|
||||||
if submodules_ok or project.sync_s:
|
if should_include_submodules(project):
|
||||||
derived_projects.update(
|
derived_projects.update(
|
||||||
(p.RelPath(local=False), p)
|
(p.RelPath(local=False), p)
|
||||||
for p in project.GetDerivedSubprojects()
|
for p in project.GetDerivedSubprojects()
|
||||||
@@ -452,7 +474,7 @@ class Command:
|
|||||||
if (
|
if (
|
||||||
project
|
project
|
||||||
and not project.Derived
|
and not project.Derived
|
||||||
and (submodules_ok or project.sync_s)
|
and should_include_submodules(project)
|
||||||
):
|
):
|
||||||
search_again = False
|
search_again = False
|
||||||
for subproject in project.GetDerivedSubprojects():
|
for subproject in project.GetDerivedSubprojects():
|
||||||
|
|||||||
+5
-1
@@ -322,7 +322,10 @@ _repo() {
|
|||||||
'--no-clone-bundle[Do not use clone bundle]' \
|
'--no-clone-bundle[Do not use clone bundle]' \
|
||||||
'(-u --manifest-server-username)'{-u,--manifest-server-username=}'[Username for manifest server]:username:' \
|
'(-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:' \
|
'(-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]' \
|
'--use-superproject[Use superproject]' \
|
||||||
'--no-use-superproject[Do not use superproject]' \
|
'--no-use-superproject[Do not use superproject]' \
|
||||||
'--tags[Sync tags]' \
|
'--tags[Sync tags]' \
|
||||||
@@ -397,6 +400,7 @@ _repo() {
|
|||||||
'--no-verify[Do not verify]' \
|
'--no-verify[Do not verify]' \
|
||||||
'--verify[Verify]' \
|
'--verify[Verify]' \
|
||||||
'--ignore-hooks[Ignore hooks]' \
|
'--ignore-hooks[Ignore hooks]' \
|
||||||
|
'--fix[Automatically fix]' \
|
||||||
'*: :->project'
|
'*: :->project'
|
||||||
;;
|
;;
|
||||||
version)
|
version)
|
||||||
|
|||||||
+18
-1
@@ -83,6 +83,20 @@ 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
|
the user. Although user interaction is discouraged in the common case, it can
|
||||||
be useful when deploying automatic fixes.
|
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 for safe non-modifying operations.
|
||||||
|
|
||||||
|
### Automated Fixes
|
||||||
|
|
||||||
|
If the repo command that triggered the hook supports a "fix" option (e.g.,
|
||||||
|
`repo upload --fix`), this option is propagated to the hook's `main` function
|
||||||
|
as `fix` parameter (defaulting to `False`). Hooks can use this to automatically
|
||||||
|
apply fixes without prompting the user.
|
||||||
|
|
||||||
### Shebang Handling
|
### Shebang Handling
|
||||||
|
|
||||||
*** note
|
*** note
|
||||||
@@ -119,7 +133,7 @@ This hook runs when people run `repo upload`.
|
|||||||
The `pre-upload.py` file should be defined like:
|
The `pre-upload.py` file should be defined like:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def main(project_list, worktree_list=None, **kwargs):
|
def main(project_list, worktree_list=None, fix=False, yes=False, **kwargs):
|
||||||
"""Main function invoked directly by repo.
|
"""Main function invoked directly by repo.
|
||||||
|
|
||||||
We must use the name "main" as that is what repo requires.
|
We must use the name "main" as that is what repo requires.
|
||||||
@@ -130,6 +144,9 @@ def main(project_list, worktree_list=None, **kwargs):
|
|||||||
project_list, so that each entry in project_list matches with a
|
project_list, so that each entry in project_list matches with a
|
||||||
directory in worktree_list. If None, we will attempt to calculate
|
directory in worktree_list. If None, we will attempt to calculate
|
||||||
the directories automatically.
|
the directories automatically.
|
||||||
|
fix: Whether to automatically apply fixes without prompting.
|
||||||
|
yes: Whether to answer yes to all safe prompts (see
|
||||||
|
[Safe Prompts](#safe-prompts)).
|
||||||
kwargs: Leave this here for forward-compatibility.
|
kwargs: Leave this here for forward-compatibility.
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -70,6 +70,19 @@ class _GitCall:
|
|||||||
git = _GitCall()
|
git = _GitCall()
|
||||||
|
|
||||||
|
|
||||||
|
def IsValidBranchName(name: str) -> bool:
|
||||||
|
"""Return whether |name| is valid where Git expects a branch name."""
|
||||||
|
p = GitCommand(
|
||||||
|
None,
|
||||||
|
["check-ref-format", "--branch", name],
|
||||||
|
capture_stdout=True,
|
||||||
|
capture_stderr=True,
|
||||||
|
add_event_log=False,
|
||||||
|
log_as_error=False,
|
||||||
|
)
|
||||||
|
return p.Wait() == 0
|
||||||
|
|
||||||
|
|
||||||
def RepoSourceVersion():
|
def RepoSourceVersion():
|
||||||
"""Return the version of the repo.git tree."""
|
"""Return the version of the repo.git tree."""
|
||||||
ver = getattr(RepoSourceVersion, "version", None)
|
ver = getattr(RepoSourceVersion, "version", None)
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ from repo_trace import Trace
|
|||||||
# that is saved in the config.
|
# that is saved in the config.
|
||||||
SYNC_STATE_PREFIX = "repo.syncstate."
|
SYNC_STATE_PREFIX = "repo.syncstate."
|
||||||
|
|
||||||
ID_RE = re.compile(r"^[0-9a-f]{40,64}$")
|
ID_RE = re.compile(r"^(?:[0-9a-f]{40}|[0-9a-f]{64})$")
|
||||||
|
|
||||||
REVIEW_CACHE = {}
|
REVIEW_CACHE = {}
|
||||||
|
|
||||||
|
|||||||
+37
-6
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from git_command import git_require
|
||||||
from git_command import GitCommand
|
from git_command import GitCommand
|
||||||
import platform_utils
|
import platform_utils
|
||||||
from repo_trace import Trace
|
from repo_trace import Trace
|
||||||
@@ -41,6 +42,17 @@ class GitRefs:
|
|||||||
self._EnsureLoaded()
|
self._EnsureLoaded()
|
||||||
return self._phyref
|
return self._phyref
|
||||||
|
|
||||||
|
@property
|
||||||
|
def head(self) -> str:
|
||||||
|
"""Return HEAD's symbolic target or detached object ID."""
|
||||||
|
self._EnsureLoaded()
|
||||||
|
return self._symref.get(HEAD) or self._phyref.get(HEAD, "")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_loaded(self) -> bool:
|
||||||
|
"""Whether a ref snapshot has already been loaded."""
|
||||||
|
return self._phyref is not None
|
||||||
|
|
||||||
def get(self, name):
|
def get(self, name):
|
||||||
try:
|
try:
|
||||||
return self.all[name]
|
return self.all[name]
|
||||||
@@ -87,8 +99,12 @@ class GitRefs:
|
|||||||
self._symref = {}
|
self._symref = {}
|
||||||
self._mtime = {}
|
self._mtime = {}
|
||||||
|
|
||||||
self._ReadRefs()
|
root_refs_loaded = self._ReadRefs()
|
||||||
self._ReadSymbolicRef(HEAD)
|
if not root_refs_loaded or (
|
||||||
|
HEAD not in self._phyref and HEAD not in self._symref
|
||||||
|
):
|
||||||
|
# --include-root-refs does not report an unborn HEAD.
|
||||||
|
self._ReadSymbolicRef(HEAD)
|
||||||
|
|
||||||
scan = self._symref
|
scan = self._symref
|
||||||
attempts = 0
|
attempts = 0
|
||||||
@@ -113,18 +129,32 @@ class GitRefs:
|
|||||||
"""Check if a ref_id is a null object ID."""
|
"""Check if a ref_id is a null object ID."""
|
||||||
return ref_id and all(ch == "0" for ch in ref_id)
|
return ref_id and all(ch == "0" for ch in ref_id)
|
||||||
|
|
||||||
def _ReadRefs(self) -> None:
|
def _ReadRefs(self) -> bool:
|
||||||
"""Read all references using git for-each-ref."""
|
"""Read all references using git for-each-ref.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Whether root refs, including HEAD when it exists, were loaded.
|
||||||
|
"""
|
||||||
|
include_root_refs = git_require((2, 45, 0))
|
||||||
|
cmd = [
|
||||||
|
"for-each-ref",
|
||||||
|
"--format=%(objectname)%00%(refname)%00%(symref)",
|
||||||
|
]
|
||||||
|
if include_root_refs:
|
||||||
|
cmd.insert(1, "--include-root-refs")
|
||||||
|
# Avoid caching volatile root refs such as ORIG_HEAD. HEAD and
|
||||||
|
# refs/* are the only namespaces GitRefs exposes to callers.
|
||||||
|
cmd.extend([HEAD, "refs"])
|
||||||
p = GitCommand(
|
p = GitCommand(
|
||||||
None,
|
None,
|
||||||
["for-each-ref", "--format=%(objectname)%00%(refname)%00%(symref)"],
|
cmd,
|
||||||
capture_stdout=True,
|
capture_stdout=True,
|
||||||
capture_stderr=True,
|
capture_stderr=True,
|
||||||
bare=True,
|
bare=True,
|
||||||
gitdir=self._gitdir,
|
gitdir=self._gitdir,
|
||||||
)
|
)
|
||||||
if p.Wait() != 0:
|
if p.Wait() != 0:
|
||||||
return
|
return False
|
||||||
|
|
||||||
for line in p.stdout.splitlines():
|
for line in p.stdout.splitlines():
|
||||||
ref_id, name, symref = line.split("\0")
|
ref_id, name, symref = line.split("\0")
|
||||||
@@ -132,6 +162,7 @@ class GitRefs:
|
|||||||
self._symref[name] = symref
|
self._symref[name] = symref
|
||||||
elif ref_id and not self._IsNullRef(ref_id):
|
elif ref_id and not self._IsNullRef(ref_id):
|
||||||
self._phyref[name] = ref_id
|
self._phyref[name] = ref_id
|
||||||
|
return include_root_refs
|
||||||
|
|
||||||
def _ReadSymbolicRef(self, name: str) -> None:
|
def _ReadSymbolicRef(self, name: str) -> None:
|
||||||
"""Read a symbolic reference."""
|
"""Read a symbolic reference."""
|
||||||
|
|||||||
+21
-4
@@ -36,7 +36,6 @@ from git_command import git_require
|
|||||||
from git_command import GitCommand
|
from git_command import GitCommand
|
||||||
from git_config import IsId
|
from git_config import IsId
|
||||||
from git_config import RepoConfig
|
from git_config import RepoConfig
|
||||||
from git_refs import GitRefs
|
|
||||||
import platform_utils
|
import platform_utils
|
||||||
|
|
||||||
|
|
||||||
@@ -189,7 +188,7 @@ class Superproject:
|
|||||||
if netloc:
|
if netloc:
|
||||||
parts = netloc.split("-review", 1)
|
parts = netloc.split("-review", 1)
|
||||||
host = parts[0]
|
host = parts[0]
|
||||||
rev = GitRefs(self._work_git).get("HEAD")
|
rev = self._GetRef("HEAD")
|
||||||
return f"{host}/{self.name}@{rev}"
|
return f"{host}/{self.name}@{rev}"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -314,7 +313,10 @@ class Superproject:
|
|||||||
# We use --negotiation-tip to speed up the fetch. Superproject branches
|
# We use --negotiation-tip to speed up the fetch. Superproject branches
|
||||||
# do not share commits. So this lets git know it only needs to send
|
# do not share commits. So this lets git know it only needs to send
|
||||||
# commits reachable from the specified local refs.
|
# commits reachable from the specified local refs.
|
||||||
rev_commit = GitRefs(self._work_git).get(f"refs/heads/{self.revision}")
|
negotiation_ref = self.revision
|
||||||
|
if negotiation_ref and not negotiation_ref.startswith("refs/"):
|
||||||
|
negotiation_ref = f"refs/heads/{negotiation_ref}"
|
||||||
|
rev_commit = self._GetRef(negotiation_ref) if negotiation_ref else ""
|
||||||
if rev_commit:
|
if rev_commit:
|
||||||
cmd.extend(["--negotiation-tip", rev_commit])
|
cmd.extend(["--negotiation-tip", rev_commit])
|
||||||
|
|
||||||
@@ -347,6 +349,21 @@ class Superproject:
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _GetRef(self, ref: str) -> str:
|
||||||
|
"""Resolve one local ref without loading the entire ref namespace."""
|
||||||
|
p = GitCommand(
|
||||||
|
None,
|
||||||
|
["rev-parse", "--verify", "--quiet", ref],
|
||||||
|
gitdir=self._work_git,
|
||||||
|
bare=True,
|
||||||
|
capture_stdout=True,
|
||||||
|
capture_stderr=True,
|
||||||
|
log_as_error=False,
|
||||||
|
)
|
||||||
|
if p.Wait() == 0:
|
||||||
|
return p.stdout.strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
def _LsTree(self):
|
def _LsTree(self):
|
||||||
"""Gets the commit ids for all projects.
|
"""Gets the commit ids for all projects.
|
||||||
|
|
||||||
@@ -473,7 +490,7 @@ class Superproject:
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
manifest_str = self._manifest.ToXml(
|
manifest_str = self._manifest.ToXml(
|
||||||
filter_groups=self._manifest.GetManifestGroupsStr(),
|
filter_groups="all",
|
||||||
omit_local=True,
|
omit_local=True,
|
||||||
).toxml()
|
).toxml()
|
||||||
manifest_path = self._manifest_path
|
manifest_path = self._manifest_path
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import optparse
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
@@ -68,6 +69,8 @@ class RepoHook:
|
|||||||
allow_all_hooks=False,
|
allow_all_hooks=False,
|
||||||
ignore_hooks=False,
|
ignore_hooks=False,
|
||||||
abort_if_user_denies=False,
|
abort_if_user_denies=False,
|
||||||
|
yes=False,
|
||||||
|
fix=False,
|
||||||
):
|
):
|
||||||
"""RepoHook constructor.
|
"""RepoHook constructor.
|
||||||
|
|
||||||
@@ -89,6 +92,8 @@ class RepoHook:
|
|||||||
ignore_hooks: If True, then 'Do not abort action if hooks fail'.
|
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
|
abort_if_user_denies: If True, we'll abort running the hook if the
|
||||||
user doesn't allow us to run the hook.
|
user doesn't allow us to run the hook.
|
||||||
|
yes: If True, then 'Yes' is assumed for any prompts.
|
||||||
|
fix: If True, then 'Fix' is assumed for any fixup prompts.
|
||||||
"""
|
"""
|
||||||
self._hook_type = hook_type
|
self._hook_type = hook_type
|
||||||
self._hooks_project = hooks_project
|
self._hooks_project = hooks_project
|
||||||
@@ -99,6 +104,8 @@ class RepoHook:
|
|||||||
self._allow_all_hooks = allow_all_hooks
|
self._allow_all_hooks = allow_all_hooks
|
||||||
self._ignore_hooks = ignore_hooks
|
self._ignore_hooks = ignore_hooks
|
||||||
self._abort_if_user_denies = abort_if_user_denies
|
self._abort_if_user_denies = abort_if_user_denies
|
||||||
|
self._yes = yes
|
||||||
|
self._fix = fix
|
||||||
|
|
||||||
# Store the full path to the script for convenience.
|
# Store the full path to the script for convenience.
|
||||||
self._script_fullpath = None
|
self._script_fullpath = None
|
||||||
@@ -374,8 +381,12 @@ class RepoHook:
|
|||||||
# def main(project_list, **kwargs):
|
# def main(project_list, **kwargs):
|
||||||
#
|
#
|
||||||
# This allows us to later expand the API without breaking old hooks.
|
# This allows us to later expand the API without breaking old hooks.
|
||||||
kwargs = kwargs.copy()
|
kwargs = {
|
||||||
kwargs["hook_should_take_kwargs"] = True
|
**kwargs,
|
||||||
|
"hook_should_take_kwargs": True,
|
||||||
|
"fix": self._fix,
|
||||||
|
"yes": self._yes,
|
||||||
|
}
|
||||||
|
|
||||||
# See what version of python the hook has been written against.
|
# See what version of python the hook has been written against.
|
||||||
data = open(self._script_fullpath).read()
|
data = open(self._script_fullpath).read()
|
||||||
@@ -497,12 +508,18 @@ class RepoHook:
|
|||||||
"origin"
|
"origin"
|
||||||
).url,
|
).url,
|
||||||
"bug_url": manifest.contactinfo.bugurl,
|
"bug_url": manifest.contactinfo.bugurl,
|
||||||
|
"yes": getattr(opt, "yes", False),
|
||||||
|
"fix": getattr(opt, "fix", False),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return cls(*args, **kwargs)
|
return cls(*args, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def AddOptionGroup(parser, name):
|
def AddOptionGroup(
|
||||||
|
parser: optparse.OptionParser,
|
||||||
|
name: str,
|
||||||
|
allow_fix: bool = False,
|
||||||
|
) -> None:
|
||||||
"""Help options relating to the various hooks."""
|
"""Help options relating to the various hooks."""
|
||||||
|
|
||||||
# Note that verify and no-verify are NOT opposites of each other, which
|
# Note that verify and no-verify are NOT opposites of each other, which
|
||||||
@@ -526,3 +543,10 @@ class RepoHook:
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Do not abort if %s hooks fail." % name,
|
help="Do not abort if %s hooks fail." % name,
|
||||||
)
|
)
|
||||||
|
if allow_fix:
|
||||||
|
group.add_option(
|
||||||
|
"--fix",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Automatically apply %s fixes without prompting." % name,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" 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
|
.SH NAME
|
||||||
repo \- repo smartsync - manual page for repo smartsync
|
repo \- repo smartsync - manual page for repo smartsync
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -68,6 +68,9 @@ fetch all branches from server
|
|||||||
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
|
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
|
||||||
temporary manifest to use for this sync
|
temporary manifest to use for this sync
|
||||||
.TP
|
.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
|
\fB\-\-clone\-bundle\fR
|
||||||
enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
|
enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
|
||||||
.TP
|
.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
|
\fB\-p\fR MANIFEST_SERVER_PASSWORD, \fB\-\-manifest\-server\-password\fR=\fI\,MANIFEST_SERVER_PASSWORD\/\fR
|
||||||
password to authenticate with the manifest server
|
password to authenticate with the manifest server
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-fetch\-submodules\fR
|
\fB\-\-recurse\-submodules\fR
|
||||||
fetch submodules from server
|
sync submodules from server
|
||||||
|
.TP
|
||||||
|
\fB\-\-no\-recurse\-submodules\fR
|
||||||
|
don't sync submodules from server
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-use\-superproject\fR
|
\fB\-\-use\-superproject\fR
|
||||||
use the manifest superproject to sync projects; implies \fB\-c\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.
|
.\" 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
|
.SH NAME
|
||||||
repo \- repo sync - manual page for repo sync
|
repo \- repo sync - manual page for repo sync
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -68,6 +68,9 @@ fetch all branches from server
|
|||||||
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
|
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
|
||||||
temporary manifest to use for this sync
|
temporary manifest to use for this sync
|
||||||
.TP
|
.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
|
\fB\-\-clone\-bundle\fR
|
||||||
enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
|
enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
|
||||||
.TP
|
.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
|
\fB\-p\fR MANIFEST_SERVER_PASSWORD, \fB\-\-manifest\-server\-password\fR=\fI\,MANIFEST_SERVER_PASSWORD\/\fR
|
||||||
password to authenticate with the manifest server
|
password to authenticate with the manifest server
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-fetch\-submodules\fR
|
\fB\-\-recurse\-submodules\fR
|
||||||
fetch submodules from server
|
sync submodules from server
|
||||||
|
.TP
|
||||||
|
\fB\-\-no\-recurse\-submodules\fR
|
||||||
|
don't sync submodules from server
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-use\-superproject\fR
|
\fB\-\-use\-superproject\fR
|
||||||
use the manifest superproject to sync projects; implies \fB\-c\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
|
delivery network. This may be necessary if there are problems with the local
|
||||||
Python HTTP client or proxy configuration, but the Git binary works.
|
Python HTTP client or proxy configuration, but the Git binary works.
|
||||||
.PP
|
.PP
|
||||||
The \fB\-\-fetch\-submodules\fR option enables fetching Git submodules of a project from
|
The \fB\-\-recurse\-submodules\fR option enables syncing Git submodules of all projects
|
||||||
server.
|
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
|
.PP
|
||||||
The \fB\-c\fR/\-\-current\-branch option can be used to only fetch objects that are on the
|
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.
|
branch specified by a project's revision.
|
||||||
|
|||||||
+4
-1
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "June 2026" "repo upload" "Repo Manual"
|
.TH REPO "1" "August 2026" "repo upload" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo upload - manual page for repo upload
|
repo \- repo upload - manual page for repo upload
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -112,6 +112,9 @@ Run the pre\-upload hook without prompting.
|
|||||||
.TP
|
.TP
|
||||||
\fB\-\-ignore\-hooks\fR
|
\fB\-\-ignore\-hooks\fR
|
||||||
Do not abort if pre\-upload hooks fail.
|
Do not abort if pre\-upload hooks fail.
|
||||||
|
.TP
|
||||||
|
\fB\-\-fix\fR
|
||||||
|
Automatically apply pre\-upload fixes without prompting.
|
||||||
.PP
|
.PP
|
||||||
Run `repo help upload` to view the detailed manual.
|
Run `repo help upload` to view the detailed manual.
|
||||||
.SH DETAILS
|
.SH DETAILS
|
||||||
|
|||||||
+2
-2
@@ -692,9 +692,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
e.setAttribute("remote", remoteName)
|
e.setAttribute("remote", remoteName)
|
||||||
if peg_rev:
|
if peg_rev:
|
||||||
if self.IsMirror:
|
if self.IsMirror:
|
||||||
value = p.bare_git.rev_parse(p.revisionExpr + "^0")
|
value = p.bare_git.ResolveCommit(p.revisionExpr)
|
||||||
else:
|
else:
|
||||||
value = p.work_git.rev_parse(HEAD + "^0")
|
value = p.work_git.ResolveCommit(HEAD)
|
||||||
e.setAttribute("revision", value)
|
e.setAttribute("revision", value)
|
||||||
if peg_rev_upstream:
|
if peg_rev_upstream:
|
||||||
if p.upstream:
|
if p.upstream:
|
||||||
|
|||||||
+388
-99
@@ -15,6 +15,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import errno
|
import errno
|
||||||
import filecmp
|
import filecmp
|
||||||
|
import functools
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
@@ -194,6 +195,10 @@ class ReviewableBranch:
|
|||||||
def name(self):
|
def name(self):
|
||||||
return self.branch.name
|
return self.branch.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current(self) -> bool:
|
||||||
|
return getattr(self.branch, "current", False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def commits(self):
|
def commits(self):
|
||||||
if self._commit_cache is None:
|
if self._commit_cache is None:
|
||||||
@@ -238,6 +243,12 @@ class ReviewableBranch:
|
|||||||
"--pretty=format:%cd", "-n", "1", R_HEADS + self.name, "--"
|
"--pretty=format:%cd", "-n", "1", R_HEADS + self.name, "--"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def modified_files(self) -> List[str]:
|
||||||
|
return self.project.bare_git.diff(
|
||||||
|
"--name-only", f"{self.base}...{R_HEADS}{self.name}"
|
||||||
|
).splitlines()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_exists(self):
|
def base_exists(self):
|
||||||
"""Whether the branch we're tracking exists.
|
"""Whether the branch we're tracking exists.
|
||||||
@@ -270,7 +281,8 @@ class ReviewableBranch:
|
|||||||
validate_certs=True,
|
validate_certs=True,
|
||||||
push_options=None,
|
push_options=None,
|
||||||
patchset_description=None,
|
patchset_description=None,
|
||||||
):
|
git_event_log: Optional[EventLog] = None,
|
||||||
|
) -> None:
|
||||||
self.project.UploadForReview(
|
self.project.UploadForReview(
|
||||||
branch=self.name,
|
branch=self.name,
|
||||||
people=people,
|
people=people,
|
||||||
@@ -286,6 +298,7 @@ class ReviewableBranch:
|
|||||||
validate_certs=validate_certs,
|
validate_certs=validate_certs,
|
||||||
push_options=push_options,
|
push_options=push_options,
|
||||||
patchset_description=patchset_description,
|
patchset_description=patchset_description,
|
||||||
|
git_event_log=git_event_log,
|
||||||
)
|
)
|
||||||
|
|
||||||
def GetPublishedRefs(self):
|
def GetPublishedRefs(self):
|
||||||
@@ -567,6 +580,7 @@ class Project:
|
|||||||
parent=None,
|
parent=None,
|
||||||
use_git_worktrees=False,
|
use_git_worktrees=False,
|
||||||
is_derived=False,
|
is_derived=False,
|
||||||
|
gitlink_path: Optional[str] = None,
|
||||||
dest_branch=None,
|
dest_branch=None,
|
||||||
optimized_fetch=False,
|
optimized_fetch=False,
|
||||||
retry_fetches=0,
|
retry_fetches=0,
|
||||||
@@ -596,6 +610,8 @@ class Project:
|
|||||||
use_git_worktrees: Whether to use `git worktree` for this project.
|
use_git_worktrees: Whether to use `git worktree` for this project.
|
||||||
is_derived: False if the project was explicitly defined in the
|
is_derived: False if the project was explicitly defined in the
|
||||||
manifest; True if the project is a discovered submodule.
|
manifest; True if the project is a discovered submodule.
|
||||||
|
gitlink_path: For a discovered submodule, its path inside the
|
||||||
|
parent project.
|
||||||
dest_branch: The branch to which to push changes for review by
|
dest_branch: The branch to which to push changes for review by
|
||||||
default.
|
default.
|
||||||
optimized_fetch: If True, when a project is set to a sha1 revision,
|
optimized_fetch: If True, when a project is set to a sha1 revision,
|
||||||
@@ -623,6 +639,7 @@ class Project:
|
|||||||
# See the XmlManifest init code for more info.
|
# See the XmlManifest init code for more info.
|
||||||
self.use_git_worktrees = use_git_worktrees
|
self.use_git_worktrees = use_git_worktrees
|
||||||
self.is_derived = is_derived
|
self.is_derived = is_derived
|
||||||
|
self.gitlink_path = gitlink_path
|
||||||
self.optimized_fetch = optimized_fetch
|
self.optimized_fetch = optimized_fetch
|
||||||
self.retry_fetches = max(0, retry_fetches)
|
self.retry_fetches = max(0, retry_fetches)
|
||||||
self.subprojects = []
|
self.subprojects = []
|
||||||
@@ -757,15 +774,28 @@ class Project:
|
|||||||
work_git is otheriwse inaccessible (e.g. an incomplete sync).
|
work_git is otheriwse inaccessible (e.g. an incomplete sync).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
b = self.work_git.GetHead()
|
b = self._GetHead()
|
||||||
except NoManifestException:
|
except NoManifestException:
|
||||||
# If the local checkout is in a bad state, don't barf. Let the
|
# If the local checkout is in a bad state, don't barf. Let the
|
||||||
# callers process this like the head is unreadable.
|
# callers process this like the head is unreadable.
|
||||||
return None
|
return None
|
||||||
if b.startswith(R_HEADS):
|
if b and b.startswith(R_HEADS):
|
||||||
return b[len(R_HEADS) :]
|
return b[len(R_HEADS) :]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _GetHead(self) -> Optional[str]:
|
||||||
|
"""Return worktree HEAD, reusing a compatible loaded ref snapshot."""
|
||||||
|
if not self.work_git:
|
||||||
|
return None
|
||||||
|
# Git worktrees keep the checkout's HEAD in the worktree admin dir,
|
||||||
|
# while bare_ref reads the shared repository. Its HEAD is not the
|
||||||
|
# checked-out worktree's HEAD and must not be reused here.
|
||||||
|
if not self.use_git_worktrees and self.bare_ref.is_loaded:
|
||||||
|
head = self.bare_ref.head
|
||||||
|
if head:
|
||||||
|
return head
|
||||||
|
return self.work_git.GetHead()
|
||||||
|
|
||||||
def IsRebaseInProgress(self):
|
def IsRebaseInProgress(self):
|
||||||
"""Returns true if a rebase or "am" is in progress"""
|
"""Returns true if a rebase or "am" is in progress"""
|
||||||
# "rebase-apply" is used for "git rebase".
|
# "rebase-apply" is used for "git rebase".
|
||||||
@@ -864,8 +894,8 @@ class Project:
|
|||||||
|
|
||||||
def GetBranches(self):
|
def GetBranches(self):
|
||||||
"""Get all existing local branches."""
|
"""Get all existing local branches."""
|
||||||
current = self.CurrentBranch
|
|
||||||
all_refs = self._allrefs
|
all_refs = self._allrefs
|
||||||
|
current = self.CurrentBranch
|
||||||
heads = {}
|
heads = {}
|
||||||
|
|
||||||
for name, ref_id in all_refs.items():
|
for name, ref_id in all_refs.items():
|
||||||
@@ -1141,24 +1171,37 @@ class Project:
|
|||||||
|
|
||||||
def GetUploadableBranches(self, selected_branch=None):
|
def GetUploadableBranches(self, selected_branch=None):
|
||||||
"""List any branches which can be uploaded for review."""
|
"""List any branches which can be uploaded for review."""
|
||||||
heads = {}
|
if selected_branch:
|
||||||
pubed = {}
|
branch = self.GetBranch(selected_branch)
|
||||||
|
if not branch.LocalMerge:
|
||||||
|
return []
|
||||||
|
head_id = self.bare_ref.get(R_HEADS + selected_branch)
|
||||||
|
if not head_id:
|
||||||
|
return []
|
||||||
|
pub_id = self.bare_ref.get(R_PUB + selected_branch)
|
||||||
|
if pub_id and pub_id == head_id:
|
||||||
|
return []
|
||||||
|
rb = self.GetUploadableBranch(selected_branch)
|
||||||
|
if rb:
|
||||||
|
rb.branch.current = selected_branch == self.CurrentBranch
|
||||||
|
return [rb]
|
||||||
|
return []
|
||||||
|
|
||||||
for name, ref_id in self._allrefs.items():
|
# Optimization: Skip scanning _allrefs (which spawns git processes)
|
||||||
if name.startswith(R_HEADS):
|
# if no local branches with upstream tracking exist in .git/config.
|
||||||
heads[name[len(R_HEADS) :]] = ref_id
|
if not any(self.config.GetSubSections("branch")):
|
||||||
elif name.startswith(R_PUB):
|
return []
|
||||||
pubed[name[len(R_PUB) :]] = ref_id
|
|
||||||
|
branches = self.GetBranches()
|
||||||
|
|
||||||
ready = []
|
ready = []
|
||||||
for branch, ref_id in heads.items():
|
for branch, branch_config in branches.items():
|
||||||
if branch in pubed and pubed[branch] == ref_id:
|
if branch_config.published == branch_config.revision:
|
||||||
continue
|
|
||||||
if selected_branch and branch != selected_branch:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
rb = self.GetUploadableBranch(branch)
|
rb = self.GetUploadableBranch(branch)
|
||||||
if rb:
|
if rb:
|
||||||
|
rb.branch.current = branch_config.current
|
||||||
ready.append(rb)
|
ready.append(rb)
|
||||||
return ready
|
return ready
|
||||||
|
|
||||||
@@ -1188,7 +1231,8 @@ class Project:
|
|||||||
validate_certs=True,
|
validate_certs=True,
|
||||||
push_options=None,
|
push_options=None,
|
||||||
patchset_description=None,
|
patchset_description=None,
|
||||||
):
|
git_event_log: Optional[EventLog] = None,
|
||||||
|
) -> None:
|
||||||
"""Uploads the named branch for code review."""
|
"""Uploads the named branch for code review."""
|
||||||
if branch is None:
|
if branch is None:
|
||||||
branch = self.CurrentBranch
|
branch = self.CurrentBranch
|
||||||
@@ -1277,14 +1321,47 @@ class Project:
|
|||||||
ref_spec = ref_spec + "%" + ",".join(opts)
|
ref_spec = ref_spec + "%" + ",".join(opts)
|
||||||
cmd.append(ref_spec)
|
cmd.append(ref_spec)
|
||||||
|
|
||||||
GitCommand(self, cmd, bare=True, verify_command=True).Wait()
|
push_cmd = GitCommand(
|
||||||
|
self,
|
||||||
|
cmd,
|
||||||
|
bare=True,
|
||||||
|
verify_command=True,
|
||||||
|
)
|
||||||
|
push_cmd.Wait()
|
||||||
|
|
||||||
|
cls_urls = self._FindGerritUrls(push_cmd.stderr)
|
||||||
|
|
||||||
|
try:
|
||||||
|
rb = ReviewableBranch(self, branch, branch.LocalMerge)
|
||||||
|
modified_files_list = rb.modified_files
|
||||||
|
if git_event_log:
|
||||||
|
git_event_log.LogDataConfigEvents(
|
||||||
|
{
|
||||||
|
"cls": ",".join(cls_urls),
|
||||||
|
"remote": branch.remote.name,
|
||||||
|
"branch": branch.name,
|
||||||
|
"files": ",".join(modified_files_list),
|
||||||
|
},
|
||||||
|
"repo.uploadstate",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Tracing failed: %s", str(e))
|
||||||
if not dryrun:
|
if not dryrun:
|
||||||
msg = f"posted to {branch.remote.review} for {dest_branch}"
|
msg = f"posted to {branch.remote.review} for {dest_branch}"
|
||||||
self.bare_git.UpdateRef(
|
self.bare_git.UpdateRef(
|
||||||
R_PUB + branch.name, R_HEADS + branch.name, message=msg
|
R_PUB + branch.name, R_HEADS + branch.name, message=msg
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _FindGerritUrls(stderr: Optional[str]) -> List[str]:
|
||||||
|
"""Extracts Gerrit review URLs from git push output."""
|
||||||
|
if not stderr:
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
match.group(1)
|
||||||
|
for match in re.finditer(r"(https?://[^/]+/c/.+?/\+/\d+)", stderr)
|
||||||
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _encode_patchset_description(original):
|
def _encode_patchset_description(original):
|
||||||
"""Applies percent-encoding for strings sent as patchset description.
|
"""Applies percent-encoding for strings sent as patchset description.
|
||||||
@@ -1546,11 +1623,7 @@ class Project:
|
|||||||
|
|
||||||
# If the project has been manually unshallowed (e.g. via
|
# If the project has been manually unshallowed (e.g. via
|
||||||
# `git fetch --unshallow`), don't re-shallow it during sync.
|
# `git fetch --unshallow`), don't re-shallow it during sync.
|
||||||
if (
|
if depth and not is_new and not self._HasShallow():
|
||||||
depth
|
|
||||||
and not is_new
|
|
||||||
and not os.path.exists(os.path.join(self.gitdir, "shallow"))
|
|
||||||
):
|
|
||||||
depth = None
|
depth = None
|
||||||
|
|
||||||
if depth and clone_filter_for_depth:
|
if depth and clone_filter_for_depth:
|
||||||
@@ -1586,17 +1659,15 @@ class Project:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# See if we can skip the standard network fetch entirely.
|
# See if we can skip the standard network fetch entirely.
|
||||||
has_shallow = os.path.exists(os.path.join(self.gitdir, "shallow"))
|
has_shallow = self._HasShallow()
|
||||||
skip_fetch = (
|
skip_fetch = (
|
||||||
optimized_fetch
|
optimized_fetch
|
||||||
and IsId(self.revisionExpr)
|
and IsId(self.revisionExpr)
|
||||||
and self._CheckForImmutableRevision(
|
and self._CheckForImmutableRevision(
|
||||||
use_superproject=use_superproject
|
use_superproject=use_superproject,
|
||||||
)
|
depth=depth,
|
||||||
and (
|
|
||||||
has_shallow
|
|
||||||
or (not depth and not self._SharingProjectHasShallow())
|
|
||||||
)
|
)
|
||||||
|
and (has_shallow or not self._IsShallow(depth))
|
||||||
)
|
)
|
||||||
|
|
||||||
if not skip_fetch:
|
if not skip_fetch:
|
||||||
@@ -1678,7 +1749,9 @@ class Project:
|
|||||||
for linkfile in self.linkfiles:
|
for linkfile in self.linkfiles:
|
||||||
linkfile._Link()
|
linkfile._Link()
|
||||||
|
|
||||||
def GetCommitRevisionId(self):
|
def GetCommitRevisionId(
|
||||||
|
self, all_refs: Optional[Dict[str, str]] = None
|
||||||
|
) -> str:
|
||||||
"""Get revisionId of a commit.
|
"""Get revisionId of a commit.
|
||||||
|
|
||||||
Use this method instead of GetRevisionId to get the id of the commit
|
Use this method instead of GetRevisionId to get the id of the commit
|
||||||
@@ -1688,10 +1761,12 @@ class Project:
|
|||||||
if self.revisionId:
|
if self.revisionId:
|
||||||
return self.revisionId
|
return self.revisionId
|
||||||
if not self.revisionExpr.startswith(R_TAGS):
|
if not self.revisionExpr.startswith(R_TAGS):
|
||||||
return self.GetRevisionId(self._allrefs)
|
if all_refs is None:
|
||||||
|
all_refs = self._allrefs
|
||||||
|
return self.GetRevisionId(all_refs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.bare_git.rev_list(self.revisionExpr, "-1")[0]
|
return self.bare_git.ResolveCommit(self.revisionExpr)
|
||||||
except GitError:
|
except GitError:
|
||||||
raise ManifestInvalidRevisionError(
|
raise ManifestInvalidRevisionError(
|
||||||
f"revision {self.revisionExpr} in {self.name} not found"
|
f"revision {self.revisionExpr} in {self.name} not found"
|
||||||
@@ -1703,6 +1778,10 @@ class Project:
|
|||||||
Returns None if worktree is not checked out or HEAD cannot be resolved.
|
Returns None if worktree is not checked out or HEAD cannot be resolved.
|
||||||
"""
|
"""
|
||||||
if self.work_git:
|
if self.work_git:
|
||||||
|
if not self.use_git_worktrees and self.bare_ref.is_loaded:
|
||||||
|
head = self.bare_ref.get(HEAD)
|
||||||
|
if head:
|
||||||
|
return head
|
||||||
try:
|
try:
|
||||||
return self.work_git.rev_parse("HEAD")
|
return self.work_git.rev_parse("HEAD")
|
||||||
except GitError:
|
except GitError:
|
||||||
@@ -1720,7 +1799,7 @@ class Project:
|
|||||||
return all_refs[rev]
|
return all_refs[rev]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.bare_git.rev_parse("--verify", "%s^0" % rev)
|
return self.bare_git.ResolveCommit(rev)
|
||||||
except GitError:
|
except GitError:
|
||||||
raise ManifestInvalidRevisionError(
|
raise ManifestInvalidRevisionError(
|
||||||
f"revision {self.revisionExpr} in {self.name} not found"
|
f"revision {self.revisionExpr} in {self.name} not found"
|
||||||
@@ -1816,8 +1895,8 @@ class Project:
|
|||||||
if p.Wait() != 0:
|
if p.Wait() != 0:
|
||||||
logger.warning("warn: %s: stateless gc failed", self.name)
|
logger.warning("warn: %s: stateless gc failed", self.name)
|
||||||
|
|
||||||
head = self.work_git.GetHead()
|
head = self._GetHead()
|
||||||
if head.startswith(R_HEADS):
|
if head and head.startswith(R_HEADS):
|
||||||
branch = head[len(R_HEADS) :]
|
branch = head[len(R_HEADS) :]
|
||||||
try:
|
try:
|
||||||
head = all_refs[head]
|
head = all_refs[head]
|
||||||
@@ -2348,7 +2427,7 @@ class Project:
|
|||||||
# Doesn't exist
|
# Doesn't exist
|
||||||
return None
|
return None
|
||||||
|
|
||||||
head = self.work_git.GetHead()
|
head = self._GetHead()
|
||||||
if head == rev:
|
if head == rev:
|
||||||
# We can't destroy the branch while we are sitting
|
# We can't destroy the branch while we are sitting
|
||||||
# on it. Switch to a detached HEAD.
|
# on it. Switch to a detached HEAD.
|
||||||
@@ -2370,9 +2449,9 @@ class Project:
|
|||||||
|
|
||||||
def PruneHeads(self):
|
def PruneHeads(self):
|
||||||
"""Prune any topic branches already merged into upstream."""
|
"""Prune any topic branches already merged into upstream."""
|
||||||
cb = self.CurrentBranch
|
|
||||||
kill = []
|
kill = []
|
||||||
left = self._allrefs
|
left = self._allrefs
|
||||||
|
cb = self.CurrentBranch
|
||||||
for name in left.keys():
|
for name in left.keys():
|
||||||
if name.startswith(R_HEADS):
|
if name.startswith(R_HEADS):
|
||||||
name = name[len(R_HEADS) :]
|
name = name[len(R_HEADS) :]
|
||||||
@@ -2384,17 +2463,25 @@ class Project:
|
|||||||
if not kill and not cb:
|
if not kill and not cb:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
rev = self.GetRevisionId(left)
|
rev = self.GetCommitRevisionId(left)
|
||||||
|
head = left.get(R_HEADS + cb) if cb is not None else None
|
||||||
if (
|
if (
|
||||||
cb is not None
|
cb is not None
|
||||||
and not self._revlist(HEAD + "..." + rev)
|
and head == rev
|
||||||
and not self.IsDirty(consider_untracked=False)
|
and not self.IsDirty(consider_untracked=False)
|
||||||
):
|
):
|
||||||
self.work_git.DetachHead(HEAD)
|
self.work_git.DetachHead(HEAD)
|
||||||
kill.append(cb)
|
kill.append(cb)
|
||||||
|
|
||||||
if kill:
|
if kill:
|
||||||
old = self.bare_git.GetHead()
|
if not self.use_git_worktrees:
|
||||||
|
old = (
|
||||||
|
head
|
||||||
|
if cb in kill
|
||||||
|
else (self.bare_ref.head or self.bare_git.GetHead())
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
old = self.bare_git.GetHead()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.bare_git.DetachHead(rev)
|
self.bare_git.DetachHead(rev)
|
||||||
@@ -2409,7 +2496,11 @@ class Project:
|
|||||||
if IsId(old):
|
if IsId(old):
|
||||||
self.bare_git.DetachHead(old)
|
self.bare_git.DetachHead(old)
|
||||||
else:
|
else:
|
||||||
self.bare_git.SetHead(old)
|
branch = (
|
||||||
|
old[len(R_HEADS) :] if old.startswith(R_HEADS) else old
|
||||||
|
)
|
||||||
|
if branch not in kill:
|
||||||
|
self.bare_git.SetHead(old)
|
||||||
left = self._allrefs
|
left = self._allrefs
|
||||||
|
|
||||||
for branch in kill:
|
for branch in kill:
|
||||||
@@ -2425,6 +2516,7 @@ class Project:
|
|||||||
for branch in kill:
|
for branch in kill:
|
||||||
if R_HEADS + branch in left:
|
if R_HEADS + branch in left:
|
||||||
branch = self.GetBranch(branch)
|
branch = self.GetBranch(branch)
|
||||||
|
branch.current = branch.name == cb
|
||||||
base = branch.LocalMerge
|
base = branch.LocalMerge
|
||||||
if not base:
|
if not base:
|
||||||
base = rev
|
base = rev
|
||||||
@@ -2572,6 +2664,37 @@ class Project:
|
|||||||
return []
|
return []
|
||||||
return get_submodules(self.gitdir, rev)
|
return get_submodules(self.gitdir, rev)
|
||||||
|
|
||||||
|
def GetSubmoduleRevisions(self) -> Optional[Dict[str, str]]:
|
||||||
|
"""Read the gitlinks of our submodules at our current revision.
|
||||||
|
|
||||||
|
Discovered submodules are derived from the revision their parent was
|
||||||
|
at when the manifest was loaded, which is before it gets fetched.
|
||||||
|
Once it is up-to-date, its gitlinks have to be read again, otherwise
|
||||||
|
the submodules would be synced to the revisions of the previous sync.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The revision of every submodule, keyed by its path inside this
|
||||||
|
project, or None if our revision is not available locally. A
|
||||||
|
revision that is merely set does not have to be fetched yet, and
|
||||||
|
without its objects a removed submodule cannot be told apart from
|
||||||
|
one that was never fetched.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
rev = self.GetRevisionId()
|
||||||
|
self.bare_git.rev_list(
|
||||||
|
"-1",
|
||||||
|
"--missing=allow-any",
|
||||||
|
f"{rev}^0",
|
||||||
|
"--",
|
||||||
|
log_as_error=False,
|
||||||
|
)
|
||||||
|
except (GitError, ManifestInvalidRevisionError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: sha for sha, path, _url, _shallow in self._GetSubmodules()
|
||||||
|
}
|
||||||
|
|
||||||
def GetDerivedSubprojects(self):
|
def GetDerivedSubprojects(self):
|
||||||
result = []
|
result = []
|
||||||
if not self.Exists:
|
if not self.Exists:
|
||||||
@@ -2619,6 +2742,7 @@ class Project:
|
|||||||
parent=self,
|
parent=self,
|
||||||
clone_depth=clone_depth,
|
clone_depth=clone_depth,
|
||||||
is_derived=True,
|
is_derived=True,
|
||||||
|
gitlink_path=path,
|
||||||
)
|
)
|
||||||
result.append(subproject)
|
result.append(subproject)
|
||||||
result.extend(subproject.GetDerivedSubprojects())
|
result.extend(subproject.GetDerivedSubprojects())
|
||||||
@@ -2667,18 +2791,22 @@ class Project:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _CheckForImmutableRevision(
|
def _CheckForImmutableRevision(
|
||||||
self, use_superproject: Optional[bool] = None
|
self,
|
||||||
|
use_superproject: Optional[bool] = None,
|
||||||
|
depth: Optional[int] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
try:
|
try:
|
||||||
# if revision (sha or tag) is not present then following function
|
# if revision (sha or tag) is not present then following function
|
||||||
# throws an error.
|
# throws an error.
|
||||||
revs = [f"{self.revisionExpr}^0"]
|
revs = [f"{self.revisionExpr}^0"]
|
||||||
upstream_rev = None
|
upstream_rev = None
|
||||||
|
verify_upstream = self._ShouldVerifyUpstream(
|
||||||
|
use_superproject=use_superproject,
|
||||||
|
depth=depth,
|
||||||
|
)
|
||||||
|
|
||||||
# Only check upstream when using superproject.
|
# Ensure the local upstream tracking ref also exists in the ODB.
|
||||||
if self.upstream and git_superproject.UseSuperproject(
|
if verify_upstream:
|
||||||
use_superproject, self.manifest
|
|
||||||
):
|
|
||||||
upstream_rev = self.GetRemote().ToLocal(self.upstream)
|
upstream_rev = self.GetRemote().ToLocal(self.upstream)
|
||||||
revs.append(upstream_rev)
|
revs.append(upstream_rev)
|
||||||
|
|
||||||
@@ -2690,11 +2818,8 @@ class Project:
|
|||||||
log_as_error=False,
|
log_as_error=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only verify upstream relationship for superproject scenarios
|
# Verify revision is an ancestor of the upstream tracking ref.
|
||||||
# without affecting plain usage.
|
if verify_upstream:
|
||||||
if self.upstream and git_superproject.UseSuperproject(
|
|
||||||
use_superproject, self.manifest
|
|
||||||
):
|
|
||||||
self.bare_git.merge_base(
|
self.bare_git.merge_base(
|
||||||
"--is-ancestor",
|
"--is-ancestor",
|
||||||
self.revisionExpr,
|
self.revisionExpr,
|
||||||
@@ -2706,6 +2831,31 @@ class Project:
|
|||||||
# There is no such persistent revision. We have to fetch it.
|
# There is no such persistent revision. We have to fetch it.
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _HasShallow(self) -> bool:
|
||||||
|
"""Check if this project has a shallow file in its gitdir."""
|
||||||
|
return bool(
|
||||||
|
self.gitdir and os.path.exists(os.path.join(self.gitdir, "shallow"))
|
||||||
|
)
|
||||||
|
|
||||||
|
def _IsShallow(self, depth: Optional[int] = None) -> bool:
|
||||||
|
"""Check if the project is shallow or sharing shallow objects."""
|
||||||
|
return bool(
|
||||||
|
self._HasShallow() or self._SharingProjectHasShallow() or depth
|
||||||
|
)
|
||||||
|
|
||||||
|
def _ShouldVerifyUpstream(
|
||||||
|
self,
|
||||||
|
use_superproject: Optional[bool] = None,
|
||||||
|
depth: Optional[int] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""Whether to verify upstream ancestry during immutable revision
|
||||||
|
check."""
|
||||||
|
if not (IsId(self.revisionExpr) and self.upstream):
|
||||||
|
return False
|
||||||
|
if self._UseSuperprojectForUpstream(use_superproject):
|
||||||
|
return True
|
||||||
|
return not self._IsShallow(depth)
|
||||||
|
|
||||||
def _SharingProjectHasShallow(self) -> bool:
|
def _SharingProjectHasShallow(self) -> bool:
|
||||||
"""Check if another project sharing this objdir has a "shallow" file.
|
"""Check if another project sharing this objdir has a "shallow" file.
|
||||||
|
|
||||||
@@ -2719,10 +2869,16 @@ class Project:
|
|||||||
)
|
)
|
||||||
for proj in other_projects:
|
for proj in other_projects:
|
||||||
if proj.objdir == self.objdir and proj.gitdir != self.gitdir:
|
if proj.objdir == self.objdir and proj.gitdir != self.gitdir:
|
||||||
if os.path.exists(os.path.join(proj.gitdir, "shallow")):
|
if proj._HasShallow():
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _UseSuperprojectForUpstream(
|
||||||
|
self, use_superproject: Optional[bool] = None
|
||||||
|
) -> bool:
|
||||||
|
"""Whether to check upstream for superprojects."""
|
||||||
|
return git_superproject.UseSuperproject(use_superproject, self.manifest)
|
||||||
|
|
||||||
def _FetchArchive(self, tarpath, cwd=None):
|
def _FetchArchive(self, tarpath, cwd=None):
|
||||||
cmd = ["archive", "-v", "-o", tarpath]
|
cmd = ["archive", "-v", "-o", tarpath]
|
||||||
cmd.append("--remote=%s" % self.remote.url)
|
cmd.append("--remote=%s" % self.remote.url)
|
||||||
@@ -2847,6 +3003,20 @@ class Project:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _GetUpstreamFallback(self) -> Optional[str]:
|
||||||
|
"""Resolve a fallback upstream branch when revisionExpr is a SHA-1.
|
||||||
|
|
||||||
|
Returns manifest default upstream or revision if it names a branch,
|
||||||
|
or None to fall back to fetching all heads.
|
||||||
|
"""
|
||||||
|
default = self.manifest.default
|
||||||
|
if not default:
|
||||||
|
return None
|
||||||
|
for cand in (default.upstreamExpr, default.revisionExpr):
|
||||||
|
if cand and not IsId(cand) and not cand.startswith(R_TAGS):
|
||||||
|
return cand
|
||||||
|
return None
|
||||||
|
|
||||||
def _RemoteFetch(
|
def _RemoteFetch(
|
||||||
self,
|
self,
|
||||||
name=None,
|
name=None,
|
||||||
@@ -2880,31 +3050,9 @@ class Project:
|
|||||||
current_branch_only = True
|
current_branch_only = True
|
||||||
|
|
||||||
is_sha1 = IsId(self.revisionExpr)
|
is_sha1 = IsId(self.revisionExpr)
|
||||||
|
upstream = self.upstream
|
||||||
|
|
||||||
if current_branch_only:
|
if current_branch_only:
|
||||||
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):
|
|
||||||
# This is a tag and its commit id should never change.
|
|
||||||
tag_name = self.upstream[len(R_TAGS) :]
|
|
||||||
|
|
||||||
if is_sha1 or tag_name is not None:
|
|
||||||
has_shallow = os.path.exists(
|
|
||||||
os.path.join(self.gitdir, "shallow")
|
|
||||||
)
|
|
||||||
if self._CheckForImmutableRevision(
|
|
||||||
use_superproject=use_superproject
|
|
||||||
) and (
|
|
||||||
has_shallow
|
|
||||||
or (not depth and not self._SharingProjectHasShallow())
|
|
||||||
):
|
|
||||||
if verbose:
|
|
||||||
print(
|
|
||||||
"Skipped fetching project %s (already have "
|
|
||||||
"persistent ref)" % self.name
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
if is_sha1 and not depth:
|
if is_sha1 and not depth:
|
||||||
# When syncing a specific commit and --depth is not set:
|
# When syncing a specific commit and --depth is not set:
|
||||||
# * if upstream is explicitly specified and is not a sha1, fetch
|
# * if upstream is explicitly specified and is not a sha1, fetch
|
||||||
@@ -2913,11 +3061,34 @@ class Project:
|
|||||||
# sync will fail.
|
# sync will fail.
|
||||||
# * otherwise, fetch all branches to make sure we end up with
|
# * otherwise, fetch all branches to make sure we end up with
|
||||||
# the specific commit.
|
# the specific commit.
|
||||||
if self.upstream:
|
if not upstream:
|
||||||
current_branch_only = not IsId(self.upstream)
|
upstream = self._GetUpstreamFallback()
|
||||||
|
|
||||||
|
if upstream:
|
||||||
|
current_branch_only = not IsId(upstream)
|
||||||
else:
|
else:
|
||||||
current_branch_only = False
|
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 upstream and upstream.startswith(R_TAGS):
|
||||||
|
# This is a tag and its commit id should never change.
|
||||||
|
tag_name = upstream[len(R_TAGS) :]
|
||||||
|
|
||||||
|
if is_sha1 or tag_name is not None:
|
||||||
|
has_shallow = self._HasShallow()
|
||||||
|
if self._CheckForImmutableRevision(
|
||||||
|
use_superproject=use_superproject,
|
||||||
|
depth=depth,
|
||||||
|
) and (has_shallow or not self._IsShallow(depth)):
|
||||||
|
if verbose:
|
||||||
|
print(
|
||||||
|
"Skipped fetching project %s (already have "
|
||||||
|
"persistent ref)" % self.name
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
name = self.remote.name
|
name = self.remote.name
|
||||||
|
|
||||||
@@ -2976,7 +3147,7 @@ class Project:
|
|||||||
# have shallow objects or not. Tell git to unshallow all fetched
|
# have shallow objects or not. Tell git to unshallow all fetched
|
||||||
# refs. Don't do this with projects that don't have shallow
|
# refs. Don't do this with projects that don't have shallow
|
||||||
# objects, since it is less efficient.
|
# objects, since it is less efficient.
|
||||||
if os.path.exists(os.path.join(self.gitdir, "shallow")):
|
if self._HasShallow():
|
||||||
cmd.append("--depth=2147483647")
|
cmd.append("--depth=2147483647")
|
||||||
|
|
||||||
# Use clone-depth="1" as a heuristic for repositories containing
|
# Use clone-depth="1" as a heuristic for repositories containing
|
||||||
@@ -3028,11 +3199,11 @@ class Project:
|
|||||||
# Shallow checkout of a specific commit, fetch from that commit and
|
# Shallow checkout of a specific commit, fetch from that commit and
|
||||||
# not the heads only as the commit might be deeper in the history.
|
# not the heads only as the commit might be deeper in the history.
|
||||||
spec.append(branch)
|
spec.append(branch)
|
||||||
if self.upstream:
|
if upstream:
|
||||||
spec.append(self.upstream)
|
spec.append(upstream)
|
||||||
else:
|
else:
|
||||||
if is_sha1:
|
if is_sha1:
|
||||||
branch = self.upstream
|
branch = upstream
|
||||||
if branch is not None and branch.strip():
|
if branch is not None and branch.strip():
|
||||||
if not branch.startswith("refs/"):
|
if not branch.startswith("refs/"):
|
||||||
branch = R_HEADS + branch
|
branch = R_HEADS + branch
|
||||||
@@ -3219,7 +3390,8 @@ class Project:
|
|||||||
# got what we wanted, else trigger a second run of all
|
# got what we wanted, else trigger a second run of all
|
||||||
# refs.
|
# refs.
|
||||||
if not self._CheckForImmutableRevision(
|
if not self._CheckForImmutableRevision(
|
||||||
use_superproject=use_superproject
|
use_superproject=use_superproject,
|
||||||
|
depth=depth,
|
||||||
):
|
):
|
||||||
# Sync the current branch only with depth set to None.
|
# Sync the current branch only with depth set to None.
|
||||||
# We always pass depth=None down to avoid infinite recursion.
|
# We always pass depth=None down to avoid infinite recursion.
|
||||||
@@ -4357,8 +4529,52 @@ class Project:
|
|||||||
|
|
||||||
return dotgit if subpath is None else os.path.join(dotgit, subpath)
|
return dotgit if subpath is None else os.path.join(dotgit, subpath)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _ParseHead(line: str) -> Optional[str]:
|
||||||
|
"""Parse the content of a .git/HEAD file.
|
||||||
|
|
||||||
|
Handles both symbolic refs (e.g. 'ref: refs/heads/...') and raw
|
||||||
|
commit IDs (40-hex SHA-1 or 64-hex SHA-256).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The ref name (e.g. 'refs/heads/main') or lowercase commit hash
|
||||||
|
if valid, or None if empty or invalid.
|
||||||
|
"""
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("ref:"):
|
||||||
|
ref = line[4:].strip()
|
||||||
|
# Ensure the ref is not empty, pure whitespace, or the
|
||||||
|
# "refs/heads/.invalid" placeholder used for unborn branches,
|
||||||
|
# empty repositories, or when the reftables backend is used
|
||||||
|
# (which will be the default in Git 3.0).
|
||||||
|
if not ref or ref == R_HEADS + ".invalid":
|
||||||
|
return None
|
||||||
|
return ref
|
||||||
|
else:
|
||||||
|
# Normalize commit IDs to canonical lowercase hexadecimal,
|
||||||
|
# matching the output format of `git rev-parse`.
|
||||||
|
line_lower = line.lower()
|
||||||
|
if IsId(line_lower):
|
||||||
|
return line_lower
|
||||||
|
return None
|
||||||
|
|
||||||
def GetHead(self):
|
def GetHead(self):
|
||||||
"""Return the ref that HEAD points to."""
|
"""Return the ref that HEAD points to."""
|
||||||
|
path = None
|
||||||
|
try:
|
||||||
|
# Catch AssertionError raised by GetDotgitPath when worktree
|
||||||
|
# .git pointer file is malformed (e.g. missing 'gitdir:').
|
||||||
|
path = self.GetDotgitPath(subpath=HEAD)
|
||||||
|
if not platform_utils.islink(path):
|
||||||
|
with open(
|
||||||
|
path, "r", encoding="utf-8", errors="replace"
|
||||||
|
) as fd:
|
||||||
|
ref = self._ParseHead(fd.readline())
|
||||||
|
if ref:
|
||||||
|
return ref
|
||||||
|
except (OSError, AssertionError):
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.symbolic_ref("-q", HEAD, log_as_error=False)
|
return self.symbolic_ref("-q", HEAD, log_as_error=False)
|
||||||
except GitError:
|
except GitError:
|
||||||
@@ -4378,19 +4594,37 @@ class Project:
|
|||||||
|
|
||||||
# Fallback to direct file reading for compatibility with broken
|
# Fallback to direct file reading for compatibility with broken
|
||||||
# repos, e.g. if HEAD points to an unborn branch.
|
# repos, e.g. if HEAD points to an unborn branch.
|
||||||
path = self.GetDotgitPath(subpath=HEAD)
|
if not path:
|
||||||
|
raise NoManifestException(
|
||||||
|
self._project.RelPath(local=False), str(e)
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
with open(path) as fd:
|
with open(
|
||||||
line = fd.readline()
|
path, "r", encoding="utf-8", errors="replace"
|
||||||
|
) as fd:
|
||||||
|
ref = self._ParseHead(fd.readline())
|
||||||
except OSError:
|
except OSError:
|
||||||
raise NoManifestException(path, str(e))
|
raise NoManifestException(
|
||||||
try:
|
self._project.RelPath(local=False), str(e)
|
||||||
line = line.decode()
|
)
|
||||||
except AttributeError:
|
if not ref:
|
||||||
pass
|
raise NoManifestException(
|
||||||
if line.startswith("ref: "):
|
self._project.RelPath(local=False), str(e)
|
||||||
return line[5:-1]
|
)
|
||||||
return line[:-1]
|
return ref
|
||||||
|
|
||||||
|
def ResolveCommit(self, revision: str) -> str:
|
||||||
|
"""Resolve |revision| to a commit without option ambiguity."""
|
||||||
|
cmdv = ["--verify", "--quiet"]
|
||||||
|
if git_require((2, 30, 0)):
|
||||||
|
cmdv.append("--end-of-options")
|
||||||
|
elif revision.startswith("-"):
|
||||||
|
raise GitError(
|
||||||
|
f"invalid revision: {revision}",
|
||||||
|
project=self._project.name,
|
||||||
|
)
|
||||||
|
cmdv.append(f"{revision}^{{commit}}")
|
||||||
|
return self.rev_parse(*cmdv, log_as_error=False)
|
||||||
|
|
||||||
def SetHead(self, ref, message=None):
|
def SetHead(self, ref, message=None):
|
||||||
cmdv = []
|
cmdv = []
|
||||||
@@ -4650,6 +4884,28 @@ class SyncBuffer:
|
|||||||
self._pending_failures = []
|
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 = ""
|
||||||
|
if git_require((2, 35, 0)):
|
||||||
|
branch = _git(["var", "GIT_DEFAULT_BRANCH"])
|
||||||
|
if not branch:
|
||||||
|
branch = _git(["config", "--get", "init.defaultBranch"])
|
||||||
|
return f"refs/heads/{branch or 'master'}"
|
||||||
|
|
||||||
|
|
||||||
class MetaProject(Project):
|
class MetaProject(Project):
|
||||||
"""A special project housed under .repo."""
|
"""A special project housed under .repo."""
|
||||||
|
|
||||||
@@ -4663,7 +4919,7 @@ class MetaProject(Project):
|
|||||||
worktree=worktree,
|
worktree=worktree,
|
||||||
remote=RemoteSpec("origin"),
|
remote=RemoteSpec("origin"),
|
||||||
relpath=".repo/%s" % name,
|
relpath=".repo/%s" % name,
|
||||||
revisionExpr="refs/heads/master",
|
revisionExpr=_DefaultBranchFallback(),
|
||||||
revisionId=None,
|
revisionId=None,
|
||||||
groups=None,
|
groups=None,
|
||||||
)
|
)
|
||||||
@@ -4677,6 +4933,39 @@ class MetaProject(Project):
|
|||||||
self.revisionExpr = base
|
self.revisionExpr = base
|
||||||
self.revisionId = None
|
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
|
||||||
|
|
||||||
|
def _ShouldVerifyUpstream(
|
||||||
|
self,
|
||||||
|
use_superproject: Optional[bool] = None,
|
||||||
|
depth: Optional[int] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""MetaProjects (manifest repo and repo itself) do not verify upstream
|
||||||
|
ancestry.
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def HasChanges(self):
|
def HasChanges(self):
|
||||||
"""Has the remote received new commits not yet checked out?"""
|
"""Has the remote received new commits not yet checked out?"""
|
||||||
@@ -4685,8 +4974,8 @@ class MetaProject(Project):
|
|||||||
|
|
||||||
all_refs = self.bare_ref.all
|
all_refs = self.bare_ref.all
|
||||||
revid = self.GetRevisionId(all_refs)
|
revid = self.GetRevisionId(all_refs)
|
||||||
head = self.work_git.GetHead()
|
head = self._GetHead()
|
||||||
if head.startswith(R_HEADS):
|
if head and head.startswith(R_HEADS):
|
||||||
try:
|
try:
|
||||||
head = all_refs[head]
|
head = all_refs[head]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -4694,7 +4983,7 @@ class MetaProject(Project):
|
|||||||
|
|
||||||
if revid == head:
|
if revid == head:
|
||||||
return False
|
return False
|
||||||
elif self._revlist(not_rev(HEAD), revid):
|
elif self._revlist("-1", not_rev(HEAD), revid):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -5112,9 +5401,9 @@ class ManifestProject(MetaProject):
|
|||||||
if is_new:
|
if is_new:
|
||||||
default_branch = self.ResolveRemoteHead()
|
default_branch = self.ResolveRemoteHead()
|
||||||
if default_branch is None:
|
if default_branch is None:
|
||||||
# If the remote doesn't have HEAD configured, default to
|
# If the remote doesn't have HEAD configured, fall back
|
||||||
# master.
|
# to whatever git uses as its default branch.
|
||||||
default_branch = "refs/heads/master"
|
default_branch = _DefaultBranchFallback()
|
||||||
self.revisionExpr = default_branch
|
self.revisionExpr = default_branch
|
||||||
else:
|
else:
|
||||||
self.PreSync()
|
self.PreSync()
|
||||||
|
|||||||
+2
-5
@@ -20,7 +20,7 @@ from command import Command
|
|||||||
from command import DEFAULT_LOCAL_JOBS
|
from command import DEFAULT_LOCAL_JOBS
|
||||||
from error import RepoError
|
from error import RepoError
|
||||||
from error import RepoExitError
|
from error import RepoExitError
|
||||||
from git_command import git
|
from git_command import IsValidBranchName
|
||||||
from progress import Progress
|
from progress import Progress
|
||||||
from repo_logging import RepoLogger
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
@@ -58,9 +58,7 @@ It is equivalent to "git branch -D <branchname>".
|
|||||||
|
|
||||||
if not opt.all:
|
if not opt.all:
|
||||||
branches = args[0].split()
|
branches = args[0].split()
|
||||||
invalid_branches = [
|
invalid_branches = [x for x in branches if not IsValidBranchName(x)]
|
||||||
x for x in branches if not git.check_ref_format(f"heads/{x}")
|
|
||||||
]
|
|
||||||
|
|
||||||
if invalid_branches:
|
if invalid_branches:
|
||||||
self.OptionParser.error(
|
self.OptionParser.error(
|
||||||
@@ -94,7 +92,6 @@ It is equivalent to "git branch -D <branchname>".
|
|||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
nb = args[0].split()
|
nb = args[0].split()
|
||||||
self.TryOverrideManifestWithSmartSync()
|
|
||||||
err = collections.defaultdict(list)
|
err = collections.defaultdict(list)
|
||||||
success = collections.defaultdict(list)
|
success = collections.defaultdict(list)
|
||||||
aggregate_errors = []
|
aggregate_errors = []
|
||||||
|
|||||||
+45
-30
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
from command import Command
|
from command import Command
|
||||||
from error import GitError
|
from error import GitError
|
||||||
@@ -43,36 +44,8 @@ change id will be added.
|
|||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
reference = args[0]
|
reference = args[0]
|
||||||
|
sha1, commit = self._ResolveReference(reference)
|
||||||
p = GitCommand(
|
old_msg = self._StripHeader(commit)
|
||||||
None,
|
|
||||||
["rev-parse", "--verify", reference],
|
|
||||||
capture_stdout=True,
|
|
||||||
capture_stderr=True,
|
|
||||||
verify_command=True,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
p.Wait()
|
|
||||||
except GitError:
|
|
||||||
logger.error(p.stderr)
|
|
||||||
raise
|
|
||||||
|
|
||||||
sha1 = p.stdout.strip()
|
|
||||||
|
|
||||||
p = GitCommand(
|
|
||||||
None,
|
|
||||||
["cat-file", "commit", sha1],
|
|
||||||
capture_stdout=True,
|
|
||||||
verify_command=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
p.Wait()
|
|
||||||
except GitError:
|
|
||||||
logger.error("error: Failed to retrieve old commit message")
|
|
||||||
raise
|
|
||||||
|
|
||||||
old_msg = self._StripHeader(p.stdout)
|
|
||||||
|
|
||||||
p = GitCommand(
|
p = GitCommand(
|
||||||
None,
|
None,
|
||||||
@@ -117,6 +90,48 @@ change id will be added.
|
|||||||
logger.error("error: Failed to update commit message")
|
logger.error("error: Failed to update commit message")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def _ResolveReference(self, reference: str) -> Tuple[str, str]:
|
||||||
|
"""Resolve a commit and read it through one cat-file batch request."""
|
||||||
|
expression = f"{reference}^{{commit}}"
|
||||||
|
p = GitCommand(
|
||||||
|
None,
|
||||||
|
["cat-file", "--batch"],
|
||||||
|
input=expression + "\n",
|
||||||
|
capture_stdout=True,
|
||||||
|
capture_stderr=True,
|
||||||
|
verify_command=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
p.Wait()
|
||||||
|
header, separator, output = p.stdout.partition("\n")
|
||||||
|
if not separator:
|
||||||
|
raise ValueError("missing cat-file header")
|
||||||
|
if header.endswith(" missing") or header.endswith(" ambiguous"):
|
||||||
|
raise GitError(f"commit {reference} not found")
|
||||||
|
|
||||||
|
parts = header.split(" ", 2)
|
||||||
|
if len(parts) != 3 or parts[1] != "commit":
|
||||||
|
raise ValueError(
|
||||||
|
f"unexpected object type {parts[1]!r}"
|
||||||
|
if len(parts) >= 2
|
||||||
|
else "invalid header"
|
||||||
|
)
|
||||||
|
sha1, _object_type, _size = parts
|
||||||
|
|
||||||
|
if not output.endswith("\n"):
|
||||||
|
raise ValueError("truncated cat-file object")
|
||||||
|
|
||||||
|
commit = output[:-1]
|
||||||
|
except (GitError, ValueError) as e:
|
||||||
|
logger.error(
|
||||||
|
"error: Failed to resolve or read commit %s", reference
|
||||||
|
)
|
||||||
|
if isinstance(e, GitError):
|
||||||
|
raise
|
||||||
|
raise GitError(str(e)) from e
|
||||||
|
|
||||||
|
return sha1, commit
|
||||||
|
|
||||||
def _IsChangeId(self, line):
|
def _IsChangeId(self, line):
|
||||||
return CHANGE_ID_RE.match(line)
|
return CHANGE_ID_RE.match(line)
|
||||||
|
|
||||||
|
|||||||
@@ -243,8 +243,6 @@ without iterating through the remaining projects.
|
|||||||
|
|
||||||
mirror = self.manifest.IsMirror
|
mirror = self.manifest.IsMirror
|
||||||
|
|
||||||
self.TryOverrideManifestWithSmartSync()
|
|
||||||
|
|
||||||
if opt.regex:
|
if opt.regex:
|
||||||
projects = self.FindProjects(args, all_manifests=all_trees)
|
projects = self.FindProjects(args, all_manifests=all_trees)
|
||||||
elif opt.inverse_regex:
|
elif opt.inverse_regex:
|
||||||
|
|||||||
@@ -147,8 +147,6 @@ class Info(PagedCommand):
|
|||||||
if not opt.this_manifest_only:
|
if not opt.this_manifest_only:
|
||||||
self.manifest = self.manifest.outer_client
|
self.manifest = self.manifest.outer_client
|
||||||
|
|
||||||
self.TryOverrideManifestWithSmartSync()
|
|
||||||
|
|
||||||
output_format = OutputFormat[opt.format.upper()]
|
output_format = OutputFormat[opt.format.upper()]
|
||||||
if output_format == OutputFormat.JSON:
|
if output_format == OutputFormat.JSON:
|
||||||
self._ExecuteJson(opt, args)
|
self._ExecuteJson(opt, args)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ _REPO_ALLOW_SHALLOW = os.environ.get("REPO_ALLOW_SHALLOW")
|
|||||||
|
|
||||||
class Init(InteractiveCommand, MirrorSafeCommand):
|
class Init(InteractiveCommand, MirrorSafeCommand):
|
||||||
COMMON = True
|
COMMON = True
|
||||||
|
RESPECT_SMART_SYNC_OVERRIDE = False
|
||||||
MULTI_MANIFEST_SUPPORT = True
|
MULTI_MANIFEST_SUPPORT = True
|
||||||
helpSummary = "Initialize a repo client checkout in the current directory"
|
helpSummary = "Initialize a repo client checkout in the current directory"
|
||||||
helpUsage = """
|
helpUsage = """
|
||||||
|
|||||||
+9
-4
@@ -59,10 +59,15 @@ are displayed.
|
|||||||
for project in self.GetProjects(
|
for project in self.GetProjects(
|
||||||
args, all_manifests=not opt.this_manifest_only
|
args, all_manifests=not opt.this_manifest_only
|
||||||
):
|
):
|
||||||
br = [project.GetUploadableBranch(x) for x in project.GetBranches()]
|
local_branches = project.GetBranches()
|
||||||
br = [x for x in br if x]
|
br = []
|
||||||
|
for name, branch in local_branches.items():
|
||||||
|
uploadable = project.GetUploadableBranch(name)
|
||||||
|
if uploadable:
|
||||||
|
uploadable.branch.current = branch.current
|
||||||
|
br.append(uploadable)
|
||||||
if opt.current_branch:
|
if opt.current_branch:
|
||||||
br = [x for x in br if x.name == project.CurrentBranch]
|
br = [x for x in br if x.current]
|
||||||
all_branches.extend(br)
|
all_branches.extend(br)
|
||||||
|
|
||||||
if not all_branches:
|
if not all_branches:
|
||||||
@@ -97,7 +102,7 @@ are displayed.
|
|||||||
print(
|
print(
|
||||||
"%s %-33s (%2d commit%s, %s)"
|
"%s %-33s (%2d commit%s, %s)"
|
||||||
% (
|
% (
|
||||||
branch.name == project.CurrentBranch and "*" or " ",
|
branch.current and "*" or " ",
|
||||||
branch.name,
|
branch.name,
|
||||||
len(commits),
|
len(commits),
|
||||||
len(commits) != 1 and "s" or " ",
|
len(commits) != 1 and "s" or " ",
|
||||||
|
|||||||
+1
-1
@@ -80,7 +80,7 @@ class Prune(PagedCommand):
|
|||||||
print(
|
print(
|
||||||
"%s %-33s "
|
"%s %-33s "
|
||||||
% (
|
% (
|
||||||
branch.name == project.CurrentBranch and "*" or " ",
|
branch.current and "*" or " ",
|
||||||
branch.name,
|
branch.name,
|
||||||
),
|
),
|
||||||
end="",
|
end="",
|
||||||
|
|||||||
+16
-20
@@ -16,7 +16,9 @@ import sys
|
|||||||
|
|
||||||
from color import Coloring
|
from color import Coloring
|
||||||
from command import Command
|
from command import Command
|
||||||
|
from error import GitError
|
||||||
from git_command import GitCommand
|
from git_command import GitCommand
|
||||||
|
from project import Project
|
||||||
from repo_logging import RepoLogger
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
|
|
||||||
@@ -30,6 +32,17 @@ class RebaseColoring(Coloring):
|
|||||||
self.fail = self.printer("fail", fg="red")
|
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):
|
class Rebase(Command):
|
||||||
COMMON = True
|
COMMON = True
|
||||||
helpSummary = "Rebase local branches on upstream branch"
|
helpSummary = "Rebase local branches on upstream branch"
|
||||||
@@ -126,6 +139,8 @@ branch but need to incorporate new upstream changes "underneath" them.
|
|||||||
common_args.append("--autosquash")
|
common_args.append("--autosquash")
|
||||||
if opt.interactive:
|
if opt.interactive:
|
||||||
common_args.append("-i")
|
common_args.append("-i")
|
||||||
|
if opt.auto_stash:
|
||||||
|
common_args.append("--autostash")
|
||||||
|
|
||||||
config = self.manifest.manifestProject.config
|
config = self.manifest.manifestProject.config
|
||||||
out = RebaseColoring(config)
|
out = RebaseColoring(config)
|
||||||
@@ -162,7 +177,7 @@ branch but need to incorporate new upstream changes "underneath" them.
|
|||||||
args = common_args[:]
|
args = common_args[:]
|
||||||
if opt.onto_manifest:
|
if opt.onto_manifest:
|
||||||
args.append("--onto")
|
args.append("--onto")
|
||||||
args.append(project.revisionExpr)
|
args.append(_ResolveOntoManifest(project))
|
||||||
|
|
||||||
args.append(upbranch.LocalMerge)
|
args.append(upbranch.LocalMerge)
|
||||||
|
|
||||||
@@ -175,29 +190,10 @@ branch but need to incorporate new upstream changes "underneath" them.
|
|||||||
out.nl()
|
out.nl()
|
||||||
out.flush()
|
out.flush()
|
||||||
|
|
||||||
needs_stash = False
|
|
||||||
if opt.auto_stash:
|
|
||||||
stash_args = ["update-index", "--refresh", "-q"]
|
|
||||||
|
|
||||||
if GitCommand(project, stash_args).Wait() != 0:
|
|
||||||
needs_stash = True
|
|
||||||
# Dirty index, requires stash...
|
|
||||||
stash_args = ["stash"]
|
|
||||||
|
|
||||||
if GitCommand(project, stash_args).Wait() != 0:
|
|
||||||
ret += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if GitCommand(project, args).Wait() != 0:
|
if GitCommand(project, args).Wait() != 0:
|
||||||
ret += 1
|
ret += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if needs_stash:
|
|
||||||
stash_args.append("pop")
|
|
||||||
stash_args.append("--quiet")
|
|
||||||
if GitCommand(project, stash_args).Wait() != 0:
|
|
||||||
ret += 1
|
|
||||||
|
|
||||||
if ret:
|
if ret:
|
||||||
msg_fmt = "%d projects had errors"
|
msg_fmt = "%d projects had errors"
|
||||||
self.git_event_log.ErrorEvent(msg_fmt % (ret), msg_fmt)
|
self.git_event_log.ErrorEvent(msg_fmt % (ret), msg_fmt)
|
||||||
|
|||||||
+2
-3
@@ -18,7 +18,7 @@ from typing import NamedTuple
|
|||||||
from command import Command
|
from command import Command
|
||||||
from command import DEFAULT_LOCAL_JOBS
|
from command import DEFAULT_LOCAL_JOBS
|
||||||
from error import RepoExitError
|
from error import RepoExitError
|
||||||
from git_command import git
|
from git_command import IsValidBranchName
|
||||||
from git_config import IsImmutable
|
from git_config import IsImmutable
|
||||||
from progress import Progress
|
from progress import Progress
|
||||||
from repo_logging import RepoLogger
|
from repo_logging import RepoLogger
|
||||||
@@ -75,7 +75,7 @@ revision specified in the manifest.
|
|||||||
self.Usage()
|
self.Usage()
|
||||||
|
|
||||||
nb = args[0]
|
nb = args[0]
|
||||||
if not git.check_ref_format("heads/%s" % nb):
|
if not IsValidBranchName(nb):
|
||||||
self.OptionParser.error("'%s' is not a valid name" % nb)
|
self.OptionParser.error("'%s' is not a valid name" % nb)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -104,7 +104,6 @@ revision specified in the manifest.
|
|||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
nb = args[0]
|
nb = args[0]
|
||||||
self.TryOverrideManifestWithSmartSync()
|
|
||||||
err_projects = []
|
err_projects = []
|
||||||
err = []
|
err = []
|
||||||
projects = []
|
projects = []
|
||||||
|
|||||||
+190
-19
@@ -28,7 +28,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
from typing import List, NamedTuple, Optional, Set, Tuple, Union
|
from typing import Dict, List, NamedTuple, Optional, Set, Tuple, Union
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@@ -156,6 +156,83 @@ def _SafeCheckoutOrder(checkouts: List[Project]) -> List[List[Project]]:
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def _ParentFirstBatches(projects: List[Project]) -> List[List[Project]]:
|
||||||
|
"""Group |projects| so that a parent is fetched before its submodules.
|
||||||
|
|
||||||
|
A discovered submodule can only be fetched at the right revision once the
|
||||||
|
project holding its gitlink has been fetched, so it is held back to a later
|
||||||
|
batch than its parent. Projects that are not discovered submodules all end
|
||||||
|
up in the first batch, which keeps manifests without submodules on a single
|
||||||
|
batch.
|
||||||
|
"""
|
||||||
|
batches = collections.defaultdict(list)
|
||||||
|
for project in projects:
|
||||||
|
depth = 0
|
||||||
|
ancestor = project
|
||||||
|
while ancestor.Derived and ancestor.parent:
|
||||||
|
depth += 1
|
||||||
|
ancestor = ancestor.parent
|
||||||
|
batches[depth].append(project)
|
||||||
|
return [batches[depth] for depth in sorted(batches)]
|
||||||
|
|
||||||
|
|
||||||
|
def _RefreshDerivedRevisions(
|
||||||
|
projects: List[Project],
|
||||||
|
submodule_revisions: Optional[Dict[Project, Dict[str, str]]] = None,
|
||||||
|
) -> List[Project]:
|
||||||
|
"""Re-resolve the gitlinks of the discovered submodules in |projects|.
|
||||||
|
|
||||||
|
The revision of a discovered submodule is read from its parent when the
|
||||||
|
manifest is loaded, so it is stale as soon as the parent gets fetched. It
|
||||||
|
has to be resolved again once the parent is up-to-date, and before the
|
||||||
|
submodule itself is fetched and checked out.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
projects: The projects whose discovered submodules to resolve.
|
||||||
|
submodule_revisions: Gitlinks already read, keyed by the project
|
||||||
|
holding them. Passing the same dict for project sets that follow
|
||||||
|
the same fetches, e.g. the levels of one checkout order, keeps a
|
||||||
|
project from being read more than once.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The submodules that their parent no longer holds a gitlink for.
|
||||||
|
"""
|
||||||
|
if submodule_revisions is None:
|
||||||
|
submodule_revisions = {}
|
||||||
|
|
||||||
|
subprojects_by_parent = collections.defaultdict(list)
|
||||||
|
for project in projects:
|
||||||
|
if project.Derived and project.parent:
|
||||||
|
subprojects_by_parent[project.parent].append(project)
|
||||||
|
|
||||||
|
removed = []
|
||||||
|
for parent, subprojects in subprojects_by_parent.items():
|
||||||
|
revisions = submodule_revisions.get(parent)
|
||||||
|
if revisions is None:
|
||||||
|
revisions = parent.GetSubmoduleRevisions()
|
||||||
|
if revisions is None:
|
||||||
|
# Leave the submodules of a parent we cannot read alone.
|
||||||
|
continue
|
||||||
|
submodule_revisions[parent] = revisions
|
||||||
|
for subproject in subprojects:
|
||||||
|
rev = revisions.get(subproject.gitlink_path)
|
||||||
|
if rev:
|
||||||
|
subproject.SetRevision(rev, revisionId=rev)
|
||||||
|
else:
|
||||||
|
removed.append(subproject)
|
||||||
|
return removed
|
||||||
|
|
||||||
|
|
||||||
|
def _WithoutProjects(
|
||||||
|
projects: List[Project], unwanted: List[Project]
|
||||||
|
) -> List[Project]:
|
||||||
|
"""Return |projects| without the projects in |unwanted|."""
|
||||||
|
if not unwanted:
|
||||||
|
return projects
|
||||||
|
dropped = set(unwanted)
|
||||||
|
return [p for p in projects if p not in dropped]
|
||||||
|
|
||||||
|
|
||||||
def _chunksize(projects: int, jobs: int) -> int:
|
def _chunksize(projects: int, jobs: int) -> int:
|
||||||
"""Calculate chunk size for the given number of projects and jobs."""
|
"""Calculate chunk size for the given number of projects and jobs."""
|
||||||
return min(max(1, projects // jobs), WORKER_BATCH_SIZE)
|
return min(max(1, projects // jobs), WORKER_BATCH_SIZE)
|
||||||
@@ -224,7 +301,8 @@ class _SyncResult(NamedTuple):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
project_index (int): The index of the project in the shared list.
|
project_index (int): The index of the project in the shared list.
|
||||||
relpath (str): The project's relative path from the repo client top.
|
relpath (str): The project's path relative to the tree being synced.
|
||||||
|
Unlike Project.relpath, it is unique across submanifests.
|
||||||
remote_fetched (bool): True if the remote was actually queried.
|
remote_fetched (bool): True if the remote was actually queried.
|
||||||
fetch_success (bool): True if the fetch operation was successful.
|
fetch_success (bool): True if the fetch operation was successful.
|
||||||
fetch_errors (List[Exception]): The Exceptions from a failed fetch.
|
fetch_errors (List[Exception]): The Exceptions from a failed fetch.
|
||||||
@@ -312,6 +390,7 @@ class TeeStringIO(io.StringIO):
|
|||||||
|
|
||||||
class Sync(Command, MirrorSafeCommand):
|
class Sync(Command, MirrorSafeCommand):
|
||||||
COMMON = True
|
COMMON = True
|
||||||
|
RESPECT_SMART_SYNC_OVERRIDE = False
|
||||||
MULTI_MANIFEST_SUPPORT = True
|
MULTI_MANIFEST_SUPPORT = True
|
||||||
helpSummary = "Update working tree to the latest revision"
|
helpSummary = "Update working tree to the latest revision"
|
||||||
helpUsage = """
|
helpUsage = """
|
||||||
@@ -378,8 +457,12 @@ resumeable bundle file on a content delivery network. This
|
|||||||
may be necessary if there are problems with the local Python
|
may be necessary if there are problems with the local Python
|
||||||
HTTP client or proxy configuration, but the Git binary works.
|
HTTP client or proxy configuration, but the Git binary works.
|
||||||
|
|
||||||
The --fetch-submodules option enables fetching Git submodules
|
The --recurse-submodules option enables syncing Git submodules of all projects
|
||||||
of a project from server.
|
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
|
The -c/--current-branch option can be used to only fetch objects that
|
||||||
are on the branch specified by a project's revision.
|
are on the branch specified by a project's revision.
|
||||||
@@ -427,6 +510,15 @@ later is required to fix a server side protocol bug.
|
|||||||
|
|
||||||
_JOBS_WARN_THRESHOLD = 100
|
_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):
|
def _Options(self, p, show_smart=True):
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--jobs-network",
|
"--jobs-network",
|
||||||
@@ -545,6 +637,13 @@ later is required to fix a server side protocol bug.
|
|||||||
help="temporary manifest to use for this sync",
|
help="temporary manifest to use for this sync",
|
||||||
metavar="NAME.xml",
|
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(
|
p.add_option(
|
||||||
"--clone-bundle",
|
"--clone-bundle",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@@ -569,9 +668,29 @@ later is required to fix a server side protocol bug.
|
|||||||
help="password to authenticate with the manifest server",
|
help="password to authenticate with the manifest server",
|
||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--fetch-submodules",
|
"--recurse-submodules",
|
||||||
action="store_true",
|
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(
|
p.add_option(
|
||||||
"--use-superproject",
|
"--use-superproject",
|
||||||
@@ -741,8 +860,9 @@ later is required to fix a server side protocol bug.
|
|||||||
|
|
||||||
all_projects = self.GetProjects(
|
all_projects = self.GetProjects(
|
||||||
args,
|
args,
|
||||||
|
groups=opt.groups,
|
||||||
missing_ok=True,
|
missing_ok=True,
|
||||||
submodules_ok=opt.fetch_submodules,
|
submodules_ok=opt.recurse_submodules,
|
||||||
manifest=manifest,
|
manifest=manifest,
|
||||||
all_manifests=not opt.this_manifest_only,
|
all_manifests=not opt.this_manifest_only,
|
||||||
)
|
)
|
||||||
@@ -1025,6 +1145,40 @@ later is required to fix a server side protocol bug.
|
|||||||
|
|
||||||
return _FetchResult(ret, fetched)
|
return _FetchResult(ret, fetched)
|
||||||
|
|
||||||
|
def _FetchParentFirst(
|
||||||
|
self,
|
||||||
|
projects: List[Project],
|
||||||
|
opt: optparse.Values,
|
||||||
|
err_event: _threading.Event,
|
||||||
|
ssh_proxy: ssh.ProxyManager,
|
||||||
|
errors: List[Exception],
|
||||||
|
) -> _FetchResult:
|
||||||
|
"""Fetch |projects|, holding submodules back until their parent is done.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
projects: Projects to fetch.
|
||||||
|
opt: Program options returned from optparse. See _Options().
|
||||||
|
err_event: Whether an error was hit while processing.
|
||||||
|
ssh_proxy: SSH manager for clients & masters.
|
||||||
|
errors: A list to accumulate errors.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
_FetchResult for all the batches combined.
|
||||||
|
"""
|
||||||
|
success = True
|
||||||
|
fetched = set()
|
||||||
|
for batch in _ParentFirstBatches(projects):
|
||||||
|
batch = _WithoutProjects(batch, _RefreshDerivedRevisions(batch))
|
||||||
|
if not batch:
|
||||||
|
continue
|
||||||
|
batch.sort(key=self._fetch_times.Get, reverse=True)
|
||||||
|
result = self._Fetch(batch, opt, err_event, ssh_proxy, errors)
|
||||||
|
success = success and result.success
|
||||||
|
fetched.update(result.projects)
|
||||||
|
if not success and opt.fail_fast:
|
||||||
|
break
|
||||||
|
return _FetchResult(success, fetched)
|
||||||
|
|
||||||
def _FetchMain(
|
def _FetchMain(
|
||||||
self, opt, args, all_projects, err_event, ssh_proxy, manifest, errors
|
self, opt, args, all_projects, err_event, ssh_proxy, manifest, errors
|
||||||
):
|
):
|
||||||
@@ -1041,12 +1195,10 @@ later is required to fix a server side protocol bug.
|
|||||||
Returns:
|
Returns:
|
||||||
List of all projects that should be checked out.
|
List of all projects that should be checked out.
|
||||||
"""
|
"""
|
||||||
to_fetch = []
|
|
||||||
to_fetch.extend(all_projects)
|
|
||||||
to_fetch.sort(key=self._fetch_times.Get, reverse=True)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self._Fetch(to_fetch, opt, err_event, ssh_proxy, errors)
|
result = self._FetchParentFirst(
|
||||||
|
all_projects, opt, err_event, ssh_proxy, errors
|
||||||
|
)
|
||||||
success = result.success
|
success = result.success
|
||||||
fetched = result.projects
|
fetched = result.projects
|
||||||
if not success:
|
if not success:
|
||||||
@@ -1070,8 +1222,9 @@ later is required to fix a server side protocol bug.
|
|||||||
self._ReloadManifest(None, manifest)
|
self._ReloadManifest(None, manifest)
|
||||||
all_projects = self.GetProjects(
|
all_projects = self.GetProjects(
|
||||||
args,
|
args,
|
||||||
|
groups=opt.groups,
|
||||||
missing_ok=True,
|
missing_ok=True,
|
||||||
submodules_ok=opt.fetch_submodules,
|
submodules_ok=opt.recurse_submodules,
|
||||||
manifest=manifest,
|
manifest=manifest,
|
||||||
all_manifests=not opt.this_manifest_only,
|
all_manifests=not opt.this_manifest_only,
|
||||||
)
|
)
|
||||||
@@ -1088,7 +1241,9 @@ later is required to fix a server side protocol bug.
|
|||||||
if previously_missing_set == missing_set:
|
if previously_missing_set == missing_set:
|
||||||
break
|
break
|
||||||
previously_missing_set = missing_set
|
previously_missing_set = missing_set
|
||||||
result = self._Fetch(missing, opt, err_event, ssh_proxy, errors)
|
result = self._FetchParentFirst(
|
||||||
|
missing, opt, err_event, ssh_proxy, errors
|
||||||
|
)
|
||||||
success = result.success
|
success = result.success
|
||||||
new_fetched = result.projects
|
new_fetched = result.projects
|
||||||
if not success:
|
if not success:
|
||||||
@@ -2317,8 +2472,9 @@ later is required to fix a server side protocol bug.
|
|||||||
|
|
||||||
all_projects = self.GetProjects(
|
all_projects = self.GetProjects(
|
||||||
args,
|
args,
|
||||||
|
groups=opt.groups,
|
||||||
missing_ok=True,
|
missing_ok=True,
|
||||||
submodules_ok=opt.fetch_submodules,
|
submodules_ok=opt.recurse_submodules,
|
||||||
manifest=manifest,
|
manifest=manifest,
|
||||||
all_manifests=not opt.this_manifest_only,
|
all_manifests=not opt.this_manifest_only,
|
||||||
)
|
)
|
||||||
@@ -2714,7 +2870,7 @@ later is required to fix a server side protocol bug.
|
|||||||
|
|
||||||
return _SyncResult(
|
return _SyncResult(
|
||||||
project_index=project_index,
|
project_index=project_index,
|
||||||
relpath=project.relpath,
|
relpath=project.RelPath(local=opt.this_manifest_only),
|
||||||
fetch_success=fetch_success,
|
fetch_success=fetch_success,
|
||||||
remote_fetched=remote_fetched,
|
remote_fetched=remote_fetched,
|
||||||
checkout_success=checkout_success,
|
checkout_success=checkout_success,
|
||||||
@@ -2862,6 +3018,10 @@ later is required to fix a server side protocol bug.
|
|||||||
self._interleaved_err_checkout = False
|
self._interleaved_err_checkout = False
|
||||||
self._interleaved_err_checkout_results = []
|
self._interleaved_err_checkout_results = []
|
||||||
|
|
||||||
|
# Project.relpath is relative to its own (sub)manifest, so it does not
|
||||||
|
# tell apart projects of different manifests being synced together.
|
||||||
|
_RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
|
||||||
|
|
||||||
err_event = multiprocessing.Event()
|
err_event = multiprocessing.Event()
|
||||||
finished_relpaths = set()
|
finished_relpaths = set()
|
||||||
project_list = list(all_projects)
|
project_list = list(all_projects)
|
||||||
@@ -2898,13 +3058,13 @@ later is required to fix a server side protocol bug.
|
|||||||
projects_to_sync = [
|
projects_to_sync = [
|
||||||
p
|
p
|
||||||
for p in project_list
|
for p in project_list
|
||||||
if p.relpath not in finished_relpaths
|
if _RelPath(p) not in finished_relpaths
|
||||||
]
|
]
|
||||||
if not projects_to_sync:
|
if not projects_to_sync:
|
||||||
break
|
break
|
||||||
|
|
||||||
pending_relpaths = {
|
pending_relpaths = {
|
||||||
p.relpath for p in projects_to_sync
|
_RelPath(p) for p in projects_to_sync
|
||||||
}
|
}
|
||||||
if previously_pending_relpaths == pending_relpaths:
|
if previously_pending_relpaths == pending_relpaths:
|
||||||
stalled_projects_str = "\n".join(
|
stalled_projects_str = "\n".join(
|
||||||
@@ -2933,12 +3093,22 @@ later is required to fix a server side protocol bug.
|
|||||||
# projects in one level can be processed in
|
# projects in one level can be processed in
|
||||||
# parallel, but we must wait for a level to complete
|
# parallel, but we must wait for a level to complete
|
||||||
# before starting the next.
|
# before starting the next.
|
||||||
|
submodule_revisions = {}
|
||||||
for level_projects in _SafeCheckoutOrder(
|
for level_projects in _SafeCheckoutOrder(
|
||||||
projects_to_sync
|
projects_to_sync
|
||||||
):
|
):
|
||||||
if not level_projects:
|
if not level_projects:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
level_projects = _WithoutProjects(
|
||||||
|
level_projects,
|
||||||
|
_RefreshDerivedRevisions(
|
||||||
|
level_projects, submodule_revisions
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if not level_projects:
|
||||||
|
continue
|
||||||
|
|
||||||
objdir_project_map = collections.defaultdict(
|
objdir_project_map = collections.defaultdict(
|
||||||
list
|
list
|
||||||
)
|
)
|
||||||
@@ -2980,8 +3150,9 @@ later is required to fix a server side protocol bug.
|
|||||||
self._ReloadManifest(None, manifest)
|
self._ReloadManifest(None, manifest)
|
||||||
project_list = self.GetProjects(
|
project_list = self.GetProjects(
|
||||||
args,
|
args,
|
||||||
|
groups=opt.groups,
|
||||||
missing_ok=True,
|
missing_ok=True,
|
||||||
submodules_ok=opt.fetch_submodules,
|
submodules_ok=opt.recurse_submodules,
|
||||||
manifest=manifest,
|
manifest=manifest,
|
||||||
all_manifests=not opt.this_manifest_only,
|
all_manifests=not opt.this_manifest_only,
|
||||||
)
|
)
|
||||||
|
|||||||
+17
-22
@@ -25,7 +25,6 @@ from editor import Editor
|
|||||||
from error import GitError
|
from error import GitError
|
||||||
from error import SilentRepoExitError
|
from error import SilentRepoExitError
|
||||||
from error import UploadError
|
from error import UploadError
|
||||||
from git_command import GitCommand
|
|
||||||
from git_refs import R_HEADS
|
from git_refs import R_HEADS
|
||||||
import git_superproject
|
import git_superproject
|
||||||
from hooks import RepoHook
|
from hooks import RepoHook
|
||||||
@@ -379,7 +378,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
default=True,
|
default=True,
|
||||||
help="disable verifying ssl certs (unsafe)",
|
help="disable verifying ssl certs (unsafe)",
|
||||||
)
|
)
|
||||||
RepoHook.AddOptionGroup(p, "pre-upload")
|
RepoHook.AddOptionGroup(p, "pre-upload", allow_fix=True)
|
||||||
|
|
||||||
def _SingleBranch(self, opt, branch, people):
|
def _SingleBranch(self, opt, branch, people):
|
||||||
project = branch.project
|
project = branch.project
|
||||||
@@ -649,6 +648,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
validate_certs=opt.validate_certs,
|
validate_certs=opt.validate_certs,
|
||||||
push_options=push_options,
|
push_options=push_options,
|
||||||
patchset_description=opt.patchset_description,
|
patchset_description=opt.patchset_description,
|
||||||
|
git_event_log=self.git_event_log,
|
||||||
)
|
)
|
||||||
|
|
||||||
branch.uploaded = True
|
branch.uploaded = True
|
||||||
@@ -704,36 +704,31 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
raise UploadExitError(aggregate_errors=aggregate_errors)
|
raise UploadExitError(aggregate_errors=aggregate_errors)
|
||||||
|
|
||||||
def _GetMergeBranch(self, project, local_branch=None):
|
def _GetMergeBranch(self, project, local_branch=None):
|
||||||
|
"""Get the merge branch name for a local branch.
|
||||||
|
|
||||||
|
Resolves the merge branch in-memory via project configuration to
|
||||||
|
avoid git subprocess overhead during upload.
|
||||||
|
"""
|
||||||
if local_branch is None:
|
if local_branch is None:
|
||||||
p = GitCommand(
|
local_branch = project.CurrentBranch
|
||||||
project,
|
if local_branch:
|
||||||
["rev-parse", "--abbrev-ref", "HEAD"],
|
branch = project.GetBranch(local_branch)
|
||||||
capture_stdout=True,
|
if branch.merge:
|
||||||
capture_stderr=True,
|
return branch.merge
|
||||||
)
|
return ""
|
||||||
p.Wait()
|
|
||||||
local_branch = p.stdout.strip()
|
|
||||||
p = GitCommand(
|
|
||||||
project,
|
|
||||||
["config", "--get", "branch.%s.merge" % local_branch],
|
|
||||||
capture_stdout=True,
|
|
||||||
capture_stderr=True,
|
|
||||||
)
|
|
||||||
p.Wait()
|
|
||||||
merge_branch = p.stdout.strip()
|
|
||||||
return merge_branch
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _GatherOne(cls, opt, project_idx):
|
def _GatherOne(cls, opt, project_idx):
|
||||||
"""Figure out the upload status for |project|."""
|
"""Figure out the upload status for |project|."""
|
||||||
project = cls.get_parallel_context()["projects"][project_idx]
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
|
cbr = None
|
||||||
if opt.current_branch:
|
if opt.current_branch:
|
||||||
cbr = project.CurrentBranch
|
cbr = project.CurrentBranch
|
||||||
up_branch = project.GetUploadableBranch(cbr)
|
up_branch = project.GetUploadableBranch(cbr)
|
||||||
avail = [up_branch] if up_branch else None
|
avail = [up_branch] if up_branch else None
|
||||||
else:
|
else:
|
||||||
avail = project.GetUploadableBranches(opt.branch)
|
avail = project.GetUploadableBranches(opt.branch)
|
||||||
return (project_idx, avail)
|
return (project_idx, avail, cbr)
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
projects = self.GetProjects(
|
projects = self.GetProjects(
|
||||||
@@ -743,7 +738,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
def _ProcessResults(_pool, _out, results):
|
def _ProcessResults(_pool, _out, results):
|
||||||
pending = []
|
pending = []
|
||||||
for result in results:
|
for result in results:
|
||||||
project_idx, avail = result
|
project_idx, avail, current_branch = result
|
||||||
project = projects[project_idx]
|
project = projects[project_idx]
|
||||||
if avail is None:
|
if avail is None:
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -751,7 +746,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
"You might be able to fix the branch by running:\n"
|
"You might be able to fix the branch by running:\n"
|
||||||
" git branch --set-upstream-to m/%s",
|
" git branch --set-upstream-to m/%s",
|
||||||
project.RelPath(local=opt.this_manifest_only),
|
project.RelPath(local=opt.this_manifest_only),
|
||||||
project.CurrentBranch,
|
current_branch,
|
||||||
project.manifest.branch,
|
project.manifest.branch,
|
||||||
)
|
)
|
||||||
elif avail:
|
elif avail:
|
||||||
|
|||||||
+20
-2
@@ -14,10 +14,12 @@
|
|||||||
|
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Any, Tuple
|
||||||
|
|
||||||
from command import Command
|
from command import Command
|
||||||
from command import MirrorSafeCommand
|
from command import MirrorSafeCommand
|
||||||
from git_command import git
|
from git_command import git
|
||||||
|
from git_command import git_require
|
||||||
from git_command import RepoSourceVersion
|
from git_command import RepoSourceVersion
|
||||||
from git_command import user_agent
|
from git_command import user_agent
|
||||||
from git_refs import HEAD
|
from git_refs import HEAD
|
||||||
@@ -34,6 +36,22 @@ class Version(Command, MirrorSafeCommand):
|
|||||||
%prog
|
%prog
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _RepoVersion(project: Any) -> Tuple[str, str]:
|
||||||
|
"""Return repo's describe string and commit date."""
|
||||||
|
if git_require((2, 32, 0)):
|
||||||
|
output = project.bare_git.log(
|
||||||
|
"-1", "--format=%(describe)%n%cD", HEAD
|
||||||
|
)
|
||||||
|
description, commit_date = output.rstrip("\n").split("\n", 1)
|
||||||
|
if description:
|
||||||
|
return description, commit_date
|
||||||
|
|
||||||
|
return (
|
||||||
|
project.bare_git.describe(HEAD),
|
||||||
|
project.bare_git.log("-1", "--format=%cD", HEAD),
|
||||||
|
)
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
rp = self.manifest.repoProject
|
rp = self.manifest.repoProject
|
||||||
rem = rp.GetRemote()
|
rem = rp.GetRemote()
|
||||||
@@ -41,11 +59,11 @@ class Version(Command, MirrorSafeCommand):
|
|||||||
|
|
||||||
# These might not be the same. Report them both.
|
# These might not be the same. Report them both.
|
||||||
src_ver = RepoSourceVersion()
|
src_ver = RepoSourceVersion()
|
||||||
rp_ver = rp.bare_git.describe(HEAD)
|
rp_ver, commit_date = self._RepoVersion(rp)
|
||||||
print(f"repo version {rp_ver}")
|
print(f"repo version {rp_ver}")
|
||||||
print(f" (from {rem.url})")
|
print(f" (from {rem.url})")
|
||||||
print(f" (tracking {branch.merge})")
|
print(f" (tracking {branch.merge})")
|
||||||
print(f" ({rp.bare_git.log('-1', '--format=%cD', HEAD)})")
|
print(f" ({commit_date})")
|
||||||
|
|
||||||
if self.wrapper_path is not None:
|
if self.wrapper_path is not None:
|
||||||
print(f"repo launcher version {self.wrapper_version}")
|
print(f"repo launcher version {self.wrapper_version}")
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
"""Unittests for the command.py module."""
|
"""Unittests for the command.py module."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from command import Command
|
from command import Command
|
||||||
|
|
||||||
|
|
||||||
@@ -86,3 +88,32 @@ def test_get_projects_keeps_derived_subprojects_for_repeated_repo():
|
|||||||
projects = cmd.GetProjects([])
|
projects = cmd.GetProjects([])
|
||||||
|
|
||||||
assert set(projects) == {project_a, project_b, submodule_a, submodule_b}
|
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
|
||||||
|
|||||||
@@ -231,6 +231,24 @@ class GitCommandStreamLogsTest(unittest.TestCase):
|
|||||||
class GitCallUnitTest(unittest.TestCase):
|
class GitCallUnitTest(unittest.TestCase):
|
||||||
"""Tests the _GitCall class (via git_command.git)."""
|
"""Tests the _GitCall class (via git_command.git)."""
|
||||||
|
|
||||||
|
def test_valid_branch_name_uses_branch_mode(self) -> None:
|
||||||
|
"""Branch validation applies Git's branch-specific restrictions."""
|
||||||
|
command = mock.MagicMock()
|
||||||
|
command.Wait.return_value = 1
|
||||||
|
with mock.patch.object(
|
||||||
|
git_command, "GitCommand", return_value=command
|
||||||
|
) as check:
|
||||||
|
self.assertFalse(git_command.IsValidBranchName("-topic"))
|
||||||
|
|
||||||
|
check.assert_called_once_with(
|
||||||
|
None,
|
||||||
|
["check-ref-format", "--branch", "-topic"],
|
||||||
|
capture_stdout=True,
|
||||||
|
capture_stderr=True,
|
||||||
|
add_event_log=False,
|
||||||
|
log_as_error=False,
|
||||||
|
)
|
||||||
|
|
||||||
def test_version_tuple(self):
|
def test_version_tuple(self):
|
||||||
"""Check git.version_tuple() handling."""
|
"""Check git.version_tuple() handling."""
|
||||||
ver = git_command.git.version_tuple()
|
ver = git_command.git.version_tuple()
|
||||||
|
|||||||
@@ -256,8 +256,8 @@ def test_remote_save_with_push_url_without_projectname(
|
|||||||
("0" * 64, True),
|
("0" * 64, True),
|
||||||
("f" * 64, True),
|
("f" * 64, True),
|
||||||
("a" * 39, False),
|
("a" * 39, False),
|
||||||
("a" * 41, True),
|
("a" * 41, False),
|
||||||
("a" * 63, True),
|
("a" * 63, False),
|
||||||
("a" * 65, False),
|
("a" * 65, False),
|
||||||
("g" * 40, False),
|
("g" * 40, False),
|
||||||
("g" * 64, False),
|
("g" * 64, False),
|
||||||
|
|||||||
+148
-2
@@ -17,6 +17,8 @@
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from typing import Any, List
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import utils_for_test
|
import utils_for_test
|
||||||
@@ -24,7 +26,7 @@ import utils_for_test
|
|||||||
import git_refs
|
import git_refs
|
||||||
|
|
||||||
|
|
||||||
def _run(repo, *args):
|
def _run(repo: str, *args: str) -> str:
|
||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
["git", "-C", repo, *args],
|
["git", "-C", repo, *args],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
@@ -34,7 +36,7 @@ def _run(repo, *args):
|
|||||||
).stdout.strip()
|
).stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
def _init_repo(tmp_path, reftable=False):
|
def _init_repo(tmp_path: Path, reftable: bool = False) -> str:
|
||||||
repo = os.path.join(tmp_path, "repo")
|
repo = os.path.join(tmp_path, "repo")
|
||||||
ref_format = "reftable" if reftable else "files"
|
ref_format = "reftable" if reftable else "files"
|
||||||
utils_for_test.init_git_tree(repo, ref_format=ref_format)
|
utils_for_test.init_git_tree(repo, ref_format=ref_format)
|
||||||
@@ -57,10 +59,154 @@ def test_reads_refs(tmp_path, reftable):
|
|||||||
branch = _run(repo, "symbolic-ref", "--short", "HEAD")
|
branch = _run(repo, "symbolic-ref", "--short", "HEAD")
|
||||||
head = _run(repo, "rev-parse", "HEAD")
|
head = _run(repo, "rev-parse", "HEAD")
|
||||||
assert refs.symref("HEAD") == f"refs/heads/{branch}"
|
assert refs.symref("HEAD") == f"refs/heads/{branch}"
|
||||||
|
assert refs.head == f"refs/heads/{branch}"
|
||||||
assert refs.get("HEAD") == head
|
assert refs.get("HEAD") == head
|
||||||
assert refs.get(f"refs/heads/{branch}") == head
|
assert refs.get(f"refs/heads/{branch}") == head
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("reftable", [False, True])
|
||||||
|
def test_reads_detached_head(tmp_path: Path, reftable: bool) -> None:
|
||||||
|
if reftable and not utils_for_test.supports_reftable():
|
||||||
|
pytest.skip("reftable not supported")
|
||||||
|
|
||||||
|
repo = _init_repo(tmp_path, reftable=reftable)
|
||||||
|
head = _run(repo, "rev-parse", "HEAD")
|
||||||
|
_run(repo, "checkout", "--detach", head)
|
||||||
|
refs = git_refs.GitRefs(os.path.join(repo, ".git"))
|
||||||
|
|
||||||
|
assert refs.symref("HEAD") == ""
|
||||||
|
assert refs.head == head
|
||||||
|
assert refs.get("HEAD") == head
|
||||||
|
|
||||||
|
|
||||||
|
def test_reads_head_with_root_refs_in_one_command(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Git 2.45 and newer include HEAD in the ref snapshot."""
|
||||||
|
head = "1" * 40
|
||||||
|
commands = []
|
||||||
|
|
||||||
|
class FakeGitCommand:
|
||||||
|
def __init__(
|
||||||
|
self, _project: Any, cmdv: List[str], **_kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
commands.append(cmdv)
|
||||||
|
self.stdout = (
|
||||||
|
f"{head}\0HEAD\0refs/heads/main\n"
|
||||||
|
f"{head}\0refs/heads/main\0\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
def Wait(self) -> int:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
monkeypatch.setattr(git_refs, "GitCommand", FakeGitCommand)
|
||||||
|
monkeypatch.setattr(git_refs, "git_require", lambda _version: True)
|
||||||
|
refs = git_refs.GitRefs("/nonexistent")
|
||||||
|
with mock.patch.object(refs, "_ReadSymbolicRef") as read_head:
|
||||||
|
assert refs.get("HEAD") == head
|
||||||
|
|
||||||
|
assert commands == [
|
||||||
|
[
|
||||||
|
"for-each-ref",
|
||||||
|
"--include-root-refs",
|
||||||
|
"--format=%(objectname)%00%(refname)%00%(symref)",
|
||||||
|
"HEAD",
|
||||||
|
"refs",
|
||||||
|
]
|
||||||
|
]
|
||||||
|
read_head.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_root_ref_snapshot_falls_back_for_unborn_head(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""An unborn HEAD still uses symbolic-ref after the ref snapshot."""
|
||||||
|
|
||||||
|
class FakeGitCommand:
|
||||||
|
def __init__(
|
||||||
|
self, _project: Any, _cmdv: List[str], **_kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
self.stdout = ""
|
||||||
|
|
||||||
|
def Wait(self) -> int:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
monkeypatch.setattr(git_refs, "GitCommand", FakeGitCommand)
|
||||||
|
monkeypatch.setattr(git_refs, "git_require", lambda _version: True)
|
||||||
|
refs = git_refs.GitRefs("/nonexistent")
|
||||||
|
|
||||||
|
def read_head(name: str) -> None:
|
||||||
|
assert name == "HEAD"
|
||||||
|
refs._symref[name] = "refs/heads/main"
|
||||||
|
|
||||||
|
monkeypatch.setattr(refs, "_ReadSymbolicRef", read_head)
|
||||||
|
|
||||||
|
assert refs.symref("HEAD") == "refs/heads/main"
|
||||||
|
|
||||||
|
|
||||||
|
def test_old_git_keeps_separate_head_fallback(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Git before 2.45 uses the original for-each-ref and HEAD calls."""
|
||||||
|
head = "1" * 40
|
||||||
|
commands = []
|
||||||
|
|
||||||
|
class FakeGitCommand:
|
||||||
|
def __init__(
|
||||||
|
self, _project: Any, cmdv: List[str], **_kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
commands.append(cmdv)
|
||||||
|
self.stdout = f"{head}\0refs/heads/main\0\n"
|
||||||
|
|
||||||
|
def Wait(self) -> int:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
monkeypatch.setattr(git_refs, "GitCommand", FakeGitCommand)
|
||||||
|
monkeypatch.setattr(git_refs, "git_require", lambda _version: False)
|
||||||
|
refs = git_refs.GitRefs("/nonexistent")
|
||||||
|
|
||||||
|
def read_head(name: str) -> None:
|
||||||
|
assert name == "HEAD"
|
||||||
|
refs._symref[name] = "refs/heads/main"
|
||||||
|
|
||||||
|
monkeypatch.setattr(refs, "_ReadSymbolicRef", read_head)
|
||||||
|
|
||||||
|
assert refs.get("HEAD") == head
|
||||||
|
assert commands == [
|
||||||
|
[
|
||||||
|
"for-each-ref",
|
||||||
|
"--format=%(objectname)%00%(refname)%00%(symref)",
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_for_each_ref_failure_falls_back_to_symbolic_ref(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""When for-each-ref fails, HEAD is still resolved via symbolic-ref."""
|
||||||
|
|
||||||
|
class FakeGitCommand:
|
||||||
|
def __init__(
|
||||||
|
self, _project: Any, _cmdv: List[str], **_kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
self.stdout = ""
|
||||||
|
|
||||||
|
def Wait(self) -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
monkeypatch.setattr(git_refs, "GitCommand", FakeGitCommand)
|
||||||
|
monkeypatch.setattr(git_refs, "git_require", lambda _version: True)
|
||||||
|
refs = git_refs.GitRefs("/nonexistent")
|
||||||
|
|
||||||
|
def read_head(name: str) -> None:
|
||||||
|
assert name == "HEAD"
|
||||||
|
refs._symref[name] = "refs/heads/main"
|
||||||
|
|
||||||
|
monkeypatch.setattr(refs, "_ReadSymbolicRef", read_head)
|
||||||
|
|
||||||
|
assert refs.symref("HEAD") == "refs/heads/main"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("reftable", [False, True])
|
@pytest.mark.parametrize("reftable", [False, True])
|
||||||
def test_updates_when_refs_change(tmp_path, reftable):
|
def test_updates_when_refs_change(tmp_path, reftable):
|
||||||
if reftable and not utils_for_test.supports_reftable():
|
if reftable and not utils_for_test.supports_reftable():
|
||||||
|
|||||||
@@ -542,14 +542,15 @@ class SuperprojectTestCase(unittest.TestCase):
|
|||||||
with mock.patch(
|
with mock.patch(
|
||||||
"git_superproject.GitCommand", autospec=True
|
"git_superproject.GitCommand", autospec=True
|
||||||
) as mock_git_command:
|
) as mock_git_command:
|
||||||
with mock.patch(
|
with mock.patch.object(
|
||||||
"git_superproject.GitRefs.get", autospec=True
|
self._superproject, "_GetRef"
|
||||||
) as mock_git_refs:
|
) as get_ref:
|
||||||
instance = mock_git_command.return_value
|
instance = mock_git_command.return_value
|
||||||
instance.Wait.return_value = 0
|
instance.Wait.return_value = 0
|
||||||
mock_git_refs.side_effect = ["", "1234"]
|
get_ref.side_effect = ["", "1234"]
|
||||||
|
|
||||||
self.assertTrue(self._superproject._Fetch())
|
self.assertTrue(self._superproject._Fetch())
|
||||||
|
get_ref.assert_called_with("refs/heads/main")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
# TODO: Once we require Python 3.8+,
|
# TODO: Once we require Python 3.8+,
|
||||||
# use 'mock_git_command.call_args.args'.
|
# use 'mock_git_command.call_args.args'.
|
||||||
@@ -572,6 +573,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
# If branch for revision exists, set as --negotiation-tip.
|
# If branch for revision exists, set as --negotiation-tip.
|
||||||
self.assertTrue(self._superproject._Fetch())
|
self.assertTrue(self._superproject._Fetch())
|
||||||
|
get_ref.assert_called_with("refs/heads/main")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
# TODO: Once we require Python 3.8+,
|
# TODO: Once we require Python 3.8+,
|
||||||
# use 'mock_git_command.call_args.args'.
|
# use 'mock_git_command.call_args.args'.
|
||||||
@@ -593,3 +595,21 @@ class SuperprojectTestCase(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_GetRef_resolves_only_the_requested_ref(self) -> None:
|
||||||
|
command = mock.MagicMock(stdout="1234\n")
|
||||||
|
command.Wait.return_value = 0
|
||||||
|
with mock.patch(
|
||||||
|
"git_superproject.GitCommand", return_value=command
|
||||||
|
) as git_command:
|
||||||
|
self.assertEqual("1234", self._superproject._GetRef("HEAD"))
|
||||||
|
|
||||||
|
git_command.assert_called_once_with(
|
||||||
|
None,
|
||||||
|
["rev-parse", "--verify", "--quiet", "HEAD"],
|
||||||
|
gitdir=self._superproject._work_git,
|
||||||
|
bare=True,
|
||||||
|
capture_stdout=True,
|
||||||
|
capture_stderr=True,
|
||||||
|
log_as_error=False,
|
||||||
|
)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"""Unittests for the hooks.py module."""
|
"""Unittests for the hooks.py module."""
|
||||||
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -105,3 +106,98 @@ def test_post_sync_argument_validation() -> None:
|
|||||||
|
|
||||||
finally:
|
finally:
|
||||||
sys.stderr = old_stderr
|
sys.stderr = old_stderr
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("yes_val", (True, False))
|
||||||
|
def test_repo_upload_yes_arg(tmp_path: Path, yes_val: bool) -> None:
|
||||||
|
"""Test that yes is passed in kwargs during hook execution."""
|
||||||
|
|
||||||
|
class FakeProject:
|
||||||
|
def __init__(self, worktree: str) -> None:
|
||||||
|
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]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("fix_val", (True, False))
|
||||||
|
def test_repo_upload_fix_arg(tmp_path: Path, fix_val: bool) -> None:
|
||||||
|
"""Test that fix is passed in kwargs during hook execution."""
|
||||||
|
|
||||||
|
class FakeProject:
|
||||||
|
def __init__(self, worktree: str) -> None:
|
||||||
|
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("fix"))
|
||||||
|
"""
|
||||||
|
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,
|
||||||
|
fix=fix_val,
|
||||||
|
)
|
||||||
|
|
||||||
|
project_list = []
|
||||||
|
res = hook.Run(project_list=project_list, worktree_list=[])
|
||||||
|
|
||||||
|
assert res is True
|
||||||
|
assert project_list == [fix_val]
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_subcmd_without_fix_option() -> None:
|
||||||
|
"""Test that FromSubcmd works when opt does not have fix attribute."""
|
||||||
|
|
||||||
|
class Remote:
|
||||||
|
url = "https://gerrit"
|
||||||
|
|
||||||
|
class FakeManifest:
|
||||||
|
repo_hooks_project = None
|
||||||
|
topdir = "/fake/topdir"
|
||||||
|
|
||||||
|
class manifestProject:
|
||||||
|
@staticmethod
|
||||||
|
def GetRemote(name: str) -> "Remote":
|
||||||
|
return Remote()
|
||||||
|
|
||||||
|
class contactinfo:
|
||||||
|
bugurl = "https://bugs"
|
||||||
|
|
||||||
|
class FakeOpt:
|
||||||
|
bypass_hooks = False
|
||||||
|
allow_all_hooks = False
|
||||||
|
ignore_hooks = False
|
||||||
|
|
||||||
|
hook = hooks.RepoHook.FromSubcmd(FakeManifest(), FakeOpt(), "post-sync")
|
||||||
|
assert hook._fix is False
|
||||||
|
|||||||
+1488
-5
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
|||||||
|
# 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 subcmds/cherry_pick.py."""
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from error import GitError
|
||||||
|
from git_command import GitCommand
|
||||||
|
from subcmds import cherry_pick
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_reference_uses_one_typed_batch_request(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
oid = "1" * 40
|
||||||
|
commit = "tree " + "2" * 40 + "\n\nSubject 🚀\n\nBody\n"
|
||||||
|
output = oid + " commit " + str(len(commit.encode("utf-8"))) + "\n"
|
||||||
|
output += commit + "\n"
|
||||||
|
command = mock.create_autospec(GitCommand, instance=True)
|
||||||
|
command.stdout = output
|
||||||
|
command.stderr = ""
|
||||||
|
command.Wait.return_value = 0
|
||||||
|
run_git = mock.create_autospec(GitCommand, return_value=command)
|
||||||
|
monkeypatch.setattr(cherry_pick, "GitCommand", run_git)
|
||||||
|
|
||||||
|
resolved, contents = cherry_pick.CherryPick()._ResolveReference("topic")
|
||||||
|
|
||||||
|
assert resolved == oid
|
||||||
|
assert contents == commit
|
||||||
|
run_git.assert_called_once_with(
|
||||||
|
None,
|
||||||
|
["cat-file", "--batch"],
|
||||||
|
input="topic^{commit}\n",
|
||||||
|
capture_stdout=True,
|
||||||
|
capture_stderr=True,
|
||||||
|
verify_command=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_reference_rejects_missing_object(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
command = mock.create_autospec(GitCommand, instance=True)
|
||||||
|
command.stdout = "topic^{commit} missing\n"
|
||||||
|
command.stderr = ""
|
||||||
|
command.Wait.return_value = 0
|
||||||
|
monkeypatch.setattr(
|
||||||
|
cherry_pick,
|
||||||
|
"GitCommand",
|
||||||
|
mock.create_autospec(GitCommand, return_value=command),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(GitError, match="commit topic not found"):
|
||||||
|
cherry_pick.CherryPick()._ResolveReference("topic")
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_reference_rejects_ambiguous_object(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
command = mock.create_autospec(GitCommand, instance=True)
|
||||||
|
command.stdout = "topic^{commit} ambiguous\n"
|
||||||
|
command.stderr = ""
|
||||||
|
command.Wait.return_value = 0
|
||||||
|
monkeypatch.setattr(
|
||||||
|
cherry_pick,
|
||||||
|
"GitCommand",
|
||||||
|
mock.create_autospec(GitCommand, return_value=command),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(GitError, match="commit topic not found"):
|
||||||
|
cherry_pick.CherryPick()._ResolveReference("topic")
|
||||||
@@ -24,7 +24,7 @@ class GcCommand(unittest.TestCase):
|
|||||||
"""Tests for gc command."""
|
"""Tests for gc command."""
|
||||||
|
|
||||||
def setUp(self):
|
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, self.args = self.cmd.OptionParser.parse_args([])
|
||||||
self.opt.this_manifest_only = False
|
self.opt.this_manifest_only = False
|
||||||
self.opt.repack = False
|
self.opt.repack = False
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# 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."""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import io
|
||||||
|
from types import SimpleNamespace
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_delegates_autostash_to_rebase() -> None:
|
||||||
|
"""--auto-stash is one rebase process, including staged-only changes."""
|
||||||
|
cmd = rebase.Rebase()
|
||||||
|
cmd.manifest = mock.MagicMock()
|
||||||
|
cmd.git_event_log = mock.MagicMock()
|
||||||
|
project = mock.MagicMock()
|
||||||
|
project.CurrentBranch = "topic"
|
||||||
|
project.RelPath.return_value = "project"
|
||||||
|
branch = mock.MagicMock()
|
||||||
|
branch.LocalMerge = "refs/remotes/origin/main"
|
||||||
|
project.GetBranch.return_value = branch
|
||||||
|
cmd.GetProjects = mock.MagicMock(return_value=[project])
|
||||||
|
opt = SimpleNamespace(
|
||||||
|
interactive=False,
|
||||||
|
fail_fast=False,
|
||||||
|
whitespace=None,
|
||||||
|
quiet=False,
|
||||||
|
force_rebase=False,
|
||||||
|
ff=True,
|
||||||
|
autosquash=False,
|
||||||
|
auto_stash=True,
|
||||||
|
onto_manifest=False,
|
||||||
|
this_manifest_only=False,
|
||||||
|
)
|
||||||
|
git_command = mock.MagicMock()
|
||||||
|
git_command.Wait.return_value = 0
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
rebase, "GitCommand", return_value=git_command
|
||||||
|
) as run_git, contextlib.redirect_stdout(io.StringIO()):
|
||||||
|
assert cmd.Execute(opt, []) == 0
|
||||||
|
|
||||||
|
run_git.assert_called_once_with(
|
||||||
|
project,
|
||||||
|
["rebase", "--autostash", "refs/remotes/origin/main"],
|
||||||
|
)
|
||||||
+568
-4
@@ -14,10 +14,13 @@
|
|||||||
"""Unittests for the subcmds/sync.py module."""
|
"""Unittests for the subcmds/sync.py module."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import optparse
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
from typing import Dict, List, Optional
|
||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
@@ -26,10 +29,51 @@ import pytest
|
|||||||
import command
|
import command
|
||||||
from error import GitError
|
from error import GitError
|
||||||
from error import RepoExitError
|
from error import RepoExitError
|
||||||
|
import manifest_xml
|
||||||
from project import SyncNetworkHalfResult
|
from project import SyncNetworkHalfResult
|
||||||
from subcmds import sync
|
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(
|
@pytest.mark.parametrize(
|
||||||
"use_superproject, cli_args, result",
|
"use_superproject, cli_args, result",
|
||||||
[
|
[
|
||||||
@@ -56,6 +100,120 @@ def test_get_current_branch_only(use_superproject, cli_args, result):
|
|||||||
assert cmd._GetCurrentBranchOnly(opts, cmd.manifest) == 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.
|
# Used to patch os.cpu_count() for reliable results.
|
||||||
OS_CPU_COUNT = 24
|
OS_CPU_COUNT = 24
|
||||||
|
|
||||||
@@ -335,12 +493,26 @@ class LocalSyncState(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class FakeProject:
|
class FakeProject:
|
||||||
def __init__(self, relpath, name=None, objdir=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
relpath: str,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
objdir: Optional[str] = None,
|
||||||
|
parent: Optional["FakeProject"] = None,
|
||||||
|
is_derived: bool = False,
|
||||||
|
revisionId: Optional[str] = None,
|
||||||
|
gitlink_path: Optional[str] = None,
|
||||||
|
path_prefix: str = "",
|
||||||
|
) -> None:
|
||||||
self.relpath = relpath
|
self.relpath = relpath
|
||||||
|
self.path_prefix = path_prefix
|
||||||
self.name = name or relpath
|
self.name = name or relpath
|
||||||
self.objdir = objdir or relpath
|
self.objdir = objdir or relpath
|
||||||
self.worktree = relpath
|
self.worktree = relpath
|
||||||
self.parent = None
|
self.parent = parent
|
||||||
|
self.is_derived = is_derived
|
||||||
|
self.revisionId = revisionId
|
||||||
|
self.gitlink_path = gitlink_path
|
||||||
|
|
||||||
self.use_git_worktrees = False
|
self.use_git_worktrees = False
|
||||||
self.UseAlternates = False
|
self.UseAlternates = False
|
||||||
@@ -349,8 +521,20 @@ class FakeProject:
|
|||||||
self.config = mock.MagicMock()
|
self.config = mock.MagicMock()
|
||||||
self.EnableRepositoryExtension = mock.MagicMock()
|
self.EnableRepositoryExtension = mock.MagicMock()
|
||||||
|
|
||||||
def RelPath(self, local=None):
|
@property
|
||||||
return self.relpath
|
def Derived(self) -> bool:
|
||||||
|
return self.is_derived
|
||||||
|
|
||||||
|
def SetRevision(
|
||||||
|
self, revisionExpr: str, revisionId: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
self.revisionExpr = revisionExpr
|
||||||
|
self.revisionId = revisionId or revisionExpr
|
||||||
|
|
||||||
|
def RelPath(self, local: bool = True) -> str:
|
||||||
|
if local:
|
||||||
|
return self.relpath
|
||||||
|
return os.path.join(self.path_prefix, self.relpath)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"project: {self.relpath}"
|
return f"project: {self.relpath}"
|
||||||
@@ -458,6 +642,159 @@ class SafeCheckoutOrder(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ParentFirstBatches(unittest.TestCase):
|
||||||
|
def test_no_submodules(self) -> None:
|
||||||
|
p_a = FakeProject("a")
|
||||||
|
p_a_b = FakeProject("a/b")
|
||||||
|
out = sync._ParentFirstBatches([p_a, p_a_b])
|
||||||
|
self.assertEqual(out, [[p_a, p_a_b]])
|
||||||
|
|
||||||
|
def test_submodules_follow_their_parent(self) -> None:
|
||||||
|
p_a = FakeProject("a")
|
||||||
|
p_a_b = FakeProject("a/b", parent=p_a, is_derived=True)
|
||||||
|
p_a_b_c = FakeProject("a/b/c", parent=p_a_b, is_derived=True)
|
||||||
|
out = sync._ParentFirstBatches([p_a_b_c, p_a, p_a_b])
|
||||||
|
self.assertEqual(out, [[p_a], [p_a_b], [p_a_b_c]])
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshDerivedRevisions(unittest.TestCase):
|
||||||
|
def _parent_with_submodules(self, **gitlinks: str) -> FakeProject:
|
||||||
|
p_a = FakeProject("a")
|
||||||
|
p_a.GetSubmoduleRevisions = mock.Mock(return_value=gitlinks)
|
||||||
|
return p_a
|
||||||
|
|
||||||
|
def _submodule(self, parent: FakeProject, path: str) -> FakeProject:
|
||||||
|
return FakeProject(
|
||||||
|
f"a/{path}", parent=parent, is_derived=True, gitlink_path=path
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_reads_each_parent_once(self) -> None:
|
||||||
|
p_a = self._parent_with_submodules(b="beef1234", c="cafe1234")
|
||||||
|
p_a_b = self._submodule(p_a, "b")
|
||||||
|
p_a_c = self._submodule(p_a, "c")
|
||||||
|
|
||||||
|
sync._RefreshDerivedRevisions([p_a, p_a_b, p_a_c])
|
||||||
|
|
||||||
|
p_a.GetSubmoduleRevisions.assert_called_once_with()
|
||||||
|
self.assertEqual(p_a_b.revisionId, "beef1234")
|
||||||
|
self.assertEqual(p_a_c.revisionId, "cafe1234")
|
||||||
|
|
||||||
|
def test_reuses_gitlinks_read_for_an_earlier_level(self) -> None:
|
||||||
|
p_a = self._parent_with_submodules(b="beef1234", c="cafe1234")
|
||||||
|
p_a_b = self._submodule(p_a, "b")
|
||||||
|
p_a_c = self._submodule(p_a, "c")
|
||||||
|
submodule_revisions = {}
|
||||||
|
|
||||||
|
sync._RefreshDerivedRevisions([p_a_b], submodule_revisions)
|
||||||
|
sync._RefreshDerivedRevisions([p_a_c], submodule_revisions)
|
||||||
|
|
||||||
|
p_a.GetSubmoduleRevisions.assert_called_once_with()
|
||||||
|
self.assertEqual(p_a_c.revisionId, "cafe1234")
|
||||||
|
|
||||||
|
def test_tells_apart_projects_with_the_same_path(self) -> None:
|
||||||
|
# Paths are relative to their own (sub)manifest, so two projects can
|
||||||
|
# share one.
|
||||||
|
first = self._parent_with_submodules(b="beef1234")
|
||||||
|
second = self._parent_with_submodules(b="cafe1234")
|
||||||
|
first_sub = self._submodule(first, "b")
|
||||||
|
second_sub = self._submodule(second, "b")
|
||||||
|
submodule_revisions = {}
|
||||||
|
|
||||||
|
sync._RefreshDerivedRevisions([first_sub], submodule_revisions)
|
||||||
|
sync._RefreshDerivedRevisions([second_sub], submodule_revisions)
|
||||||
|
|
||||||
|
self.assertEqual(first_sub.revisionId, "beef1234")
|
||||||
|
self.assertEqual(second_sub.revisionId, "cafe1234")
|
||||||
|
|
||||||
|
def test_ignores_projects_from_the_manifest(self) -> None:
|
||||||
|
p_a = self._parent_with_submodules()
|
||||||
|
|
||||||
|
sync._RefreshDerivedRevisions([p_a])
|
||||||
|
|
||||||
|
p_a.GetSubmoduleRevisions.assert_not_called()
|
||||||
|
|
||||||
|
def test_reports_submodules_removed_from_their_parent(self) -> None:
|
||||||
|
p_a = self._parent_with_submodules(b="beef1234")
|
||||||
|
p_a_c = self._submodule(p_a, "c")
|
||||||
|
|
||||||
|
removed = sync._RefreshDerivedRevisions([p_a, p_a_c])
|
||||||
|
|
||||||
|
self.assertEqual(removed, [p_a_c])
|
||||||
|
|
||||||
|
def test_keeps_submodules_of_an_unreadable_parent(self) -> None:
|
||||||
|
p_a = FakeProject("a")
|
||||||
|
p_a.GetSubmoduleRevisions = mock.Mock(return_value=None)
|
||||||
|
p_a_b = FakeProject(
|
||||||
|
"a/b",
|
||||||
|
parent=p_a,
|
||||||
|
is_derived=True,
|
||||||
|
revisionId="stale",
|
||||||
|
gitlink_path="b",
|
||||||
|
)
|
||||||
|
|
||||||
|
removed = sync._RefreshDerivedRevisions([p_a, p_a_b])
|
||||||
|
|
||||||
|
self.assertEqual(removed, [])
|
||||||
|
self.assertEqual(p_a_b.revisionId, "stale")
|
||||||
|
|
||||||
|
|
||||||
|
class FetchParentFirst(unittest.TestCase):
|
||||||
|
def test_submodules_are_fetched_after_their_parent(self) -> None:
|
||||||
|
cmd = sync.Sync()
|
||||||
|
cmd._fetch_times = mock.Mock()
|
||||||
|
cmd._fetch_times.Get = mock.Mock(return_value=0)
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
p_a = FakeProject("a")
|
||||||
|
|
||||||
|
def fake_read() -> Dict[str, str]:
|
||||||
|
calls.append(("read gitlinks of", p_a.relpath))
|
||||||
|
return {"b": "beef1234"}
|
||||||
|
|
||||||
|
p_a.GetSubmoduleRevisions = mock.Mock(side_effect=fake_read)
|
||||||
|
p_a_b = FakeProject(
|
||||||
|
"a/b", parent=p_a, is_derived=True, gitlink_path="b"
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_fetch(
|
||||||
|
projects: List[FakeProject], *_args: object
|
||||||
|
) -> sync._FetchResult:
|
||||||
|
calls.append(("fetch", [p.relpath for p in projects]))
|
||||||
|
return sync._FetchResult(True, {p.objdir for p in projects})
|
||||||
|
|
||||||
|
opt = mock.Mock(fail_fast=False)
|
||||||
|
with mock.patch.object(cmd, "_Fetch", side_effect=fake_fetch):
|
||||||
|
result = cmd._FetchParentFirst([p_a_b, p_a], opt, None, None, [])
|
||||||
|
|
||||||
|
self.assertTrue(result.success)
|
||||||
|
self.assertEqual(result.projects, {"a", "a/b"})
|
||||||
|
self.assertEqual(
|
||||||
|
calls,
|
||||||
|
[
|
||||||
|
("fetch", ["a"]),
|
||||||
|
("read gitlinks of", "a"),
|
||||||
|
("fetch", ["a/b"]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WithoutProjects(unittest.TestCase):
|
||||||
|
def test_drops_the_unwanted_projects(self) -> None:
|
||||||
|
p_a = FakeProject("a")
|
||||||
|
p_a_b = FakeProject("a/b")
|
||||||
|
self.assertEqual(sync._WithoutProjects([p_a, p_a_b], [p_a_b]), [p_a])
|
||||||
|
self.assertEqual(sync._WithoutProjects([p_a, p_a_b], []), [p_a, p_a_b])
|
||||||
|
|
||||||
|
def test_keeps_projects_with_the_same_path(self) -> None:
|
||||||
|
# Paths are relative to their own (sub)manifest, so two projects can
|
||||||
|
# share one.
|
||||||
|
first = FakeProject("a/b")
|
||||||
|
second = FakeProject("a/b")
|
||||||
|
self.assertEqual(
|
||||||
|
sync._WithoutProjects([first, second], [second]), [first]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Chunksize(unittest.TestCase):
|
class Chunksize(unittest.TestCase):
|
||||||
"""Tests for _chunksize."""
|
"""Tests for _chunksize."""
|
||||||
|
|
||||||
@@ -792,6 +1129,19 @@ class SyncCommand(unittest.TestCase):
|
|||||||
self.assertIn(self.sync_local_half_error, e.aggregate_errors)
|
self.assertIn(self.sync_local_half_error, e.aggregate_errors)
|
||||||
self.assertIn(self.sync_network_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):
|
class SyncUpdateRepoProject(unittest.TestCase):
|
||||||
"""Tests for Sync._UpdateRepoProject."""
|
"""Tests for Sync._UpdateRepoProject."""
|
||||||
@@ -989,6 +1339,203 @@ class InterleavedSyncTest(unittest.TestCase):
|
|||||||
|
|
||||||
execute_mock.assert_called_once()
|
execute_mock.assert_called_once()
|
||||||
|
|
||||||
|
def test_interleaved_refreshes_submodule_revision(self) -> None:
|
||||||
|
"""Test submodules are synced at the revision of the fetched parent."""
|
||||||
|
opt, args = self.cmd.OptionParser.parse_args(["--interleaved", "-j4"])
|
||||||
|
opt.quiet = True
|
||||||
|
|
||||||
|
submodule = FakeProject(
|
||||||
|
"projA/sub",
|
||||||
|
name="projA_sub",
|
||||||
|
objdir="objA_sub",
|
||||||
|
parent=self.projA,
|
||||||
|
is_derived=True,
|
||||||
|
revisionId="stale",
|
||||||
|
gitlink_path="sub",
|
||||||
|
)
|
||||||
|
all_projects = [self.projA, submodule]
|
||||||
|
mock.patch.object(
|
||||||
|
self.cmd, "GetProjects", return_value=all_projects
|
||||||
|
).start()
|
||||||
|
|
||||||
|
self.projA.GetSubmoduleRevisions = mock.Mock(
|
||||||
|
return_value={"sub": "fetched"}
|
||||||
|
)
|
||||||
|
|
||||||
|
synced = []
|
||||||
|
|
||||||
|
def execute_side_effect(
|
||||||
|
jobs: int,
|
||||||
|
target: object,
|
||||||
|
work_items: List[List[int]],
|
||||||
|
**kwargs: object,
|
||||||
|
) -> bool:
|
||||||
|
synced_relpaths_set = kwargs["callback"].args[0]
|
||||||
|
projects_in_pass = self.cmd.get_parallel_context()["projects"]
|
||||||
|
for item in work_items:
|
||||||
|
for project_idx in item:
|
||||||
|
project = projects_in_pass[project_idx]
|
||||||
|
synced.append((project.relpath, project.revisionId))
|
||||||
|
synced_relpaths_set.add(project.relpath)
|
||||||
|
return True
|
||||||
|
|
||||||
|
mock.patch.object(
|
||||||
|
self.cmd, "ExecuteInParallel", side_effect=execute_side_effect
|
||||||
|
).start()
|
||||||
|
|
||||||
|
self.cmd._SyncInterleaved(
|
||||||
|
opt,
|
||||||
|
args,
|
||||||
|
[],
|
||||||
|
self.manifest,
|
||||||
|
self.manifest.manifestProject,
|
||||||
|
all_projects,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn(("projA/sub", "fetched"), synced)
|
||||||
|
|
||||||
|
def test_interleaved_skips_removed_submodule(self) -> None:
|
||||||
|
"""Test submodules dropped by their parent are not checked out."""
|
||||||
|
opt, args = self.cmd.OptionParser.parse_args(["--interleaved", "-j4"])
|
||||||
|
opt.quiet = True
|
||||||
|
|
||||||
|
submodule = FakeProject(
|
||||||
|
"projA/sub",
|
||||||
|
name="projA_sub",
|
||||||
|
objdir="objA_sub",
|
||||||
|
parent=self.projA,
|
||||||
|
is_derived=True,
|
||||||
|
revisionId="stale",
|
||||||
|
gitlink_path="sub",
|
||||||
|
)
|
||||||
|
# The parent no longer holds a gitlink for the submodule.
|
||||||
|
self.projA.GetSubmoduleRevisions = mock.Mock(return_value={})
|
||||||
|
# The reloaded manifest no longer derives the removed submodule.
|
||||||
|
mock.patch.object(
|
||||||
|
self.cmd, "GetProjects", return_value=[self.projA]
|
||||||
|
).start()
|
||||||
|
|
||||||
|
synced = []
|
||||||
|
|
||||||
|
def execute_side_effect(
|
||||||
|
jobs: int,
|
||||||
|
target: object,
|
||||||
|
work_items: List[List[int]],
|
||||||
|
**kwargs: object,
|
||||||
|
) -> bool:
|
||||||
|
synced_relpaths_set = kwargs["callback"].args[0]
|
||||||
|
projects_in_pass = self.cmd.get_parallel_context()["projects"]
|
||||||
|
for item in work_items:
|
||||||
|
for project_idx in item:
|
||||||
|
project = projects_in_pass[project_idx]
|
||||||
|
synced.append(project.relpath)
|
||||||
|
synced_relpaths_set.add(project.relpath)
|
||||||
|
return True
|
||||||
|
|
||||||
|
mock.patch.object(
|
||||||
|
self.cmd, "ExecuteInParallel", side_effect=execute_side_effect
|
||||||
|
).start()
|
||||||
|
|
||||||
|
self.cmd._SyncInterleaved(
|
||||||
|
opt,
|
||||||
|
args,
|
||||||
|
[],
|
||||||
|
self.manifest,
|
||||||
|
self.manifest.manifestProject,
|
||||||
|
[self.projA, submodule],
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(synced, ["projA"])
|
||||||
|
|
||||||
|
def _make_syncable(self, project: FakeProject) -> FakeProject:
|
||||||
|
project.Sync_NetworkHalf = mock.Mock(
|
||||||
|
return_value=SyncNetworkHalfResult(error=None, remote_fetched=True)
|
||||||
|
)
|
||||||
|
project.Sync_LocalHalf = mock.Mock()
|
||||||
|
return project
|
||||||
|
|
||||||
|
def _run_interleaved(
|
||||||
|
self,
|
||||||
|
opt: optparse.Values,
|
||||||
|
initial_projects: List[FakeProject],
|
||||||
|
reloaded_projects: List[FakeProject],
|
||||||
|
) -> None:
|
||||||
|
"""Run _SyncInterleaved with the real workers and callback.
|
||||||
|
|
||||||
|
|initial_projects| make up the first pass, |reloaded_projects| every
|
||||||
|
later one, the way reloading the manifest between passes does.
|
||||||
|
"""
|
||||||
|
mock.patch.object(
|
||||||
|
self.cmd, "GetProjects", return_value=reloaded_projects
|
||||||
|
).start()
|
||||||
|
mock.patch.object(self.cmd, "event_log").start()
|
||||||
|
|
||||||
|
def execute_side_effect(
|
||||||
|
jobs: int,
|
||||||
|
target: object,
|
||||||
|
work_items: List[List[int]],
|
||||||
|
**kwargs: object,
|
||||||
|
) -> bool:
|
||||||
|
results = [target(item) for item in work_items]
|
||||||
|
return kwargs["callback"](None, kwargs["output"], results)
|
||||||
|
|
||||||
|
mock.patch.object(
|
||||||
|
self.cmd, "ExecuteInParallel", side_effect=execute_side_effect
|
||||||
|
).start()
|
||||||
|
|
||||||
|
with mock.patch("subcmds.sync.SyncBuffer") as mock_sync_buffer:
|
||||||
|
mock_sync_buffer.return_value.Finish.return_value = True
|
||||||
|
mock_sync_buffer.return_value.errors = []
|
||||||
|
self.cmd._SyncInterleaved(
|
||||||
|
opt,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
self.manifest,
|
||||||
|
self.manifest.manifestProject,
|
||||||
|
initial_projects,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_interleaved_syncs_same_path_projects_of_every_manifest(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
"""Test a project is not skipped because another shares its path."""
|
||||||
|
opt = self._get_opts(["--interleaved", "-j4"])
|
||||||
|
outer = self._make_syncable(
|
||||||
|
FakeProject("foo", name="outer", objdir="a")
|
||||||
|
)
|
||||||
|
sub = self._make_syncable(
|
||||||
|
FakeProject("foo", name="sub", objdir="b", path_prefix="sub")
|
||||||
|
)
|
||||||
|
|
||||||
|
# |sub| is only discovered once the manifest is reloaded after the
|
||||||
|
# first pass has synced |outer|.
|
||||||
|
self._run_interleaved(opt, [outer], [outer, sub])
|
||||||
|
|
||||||
|
outer.Sync_LocalHalf.assert_called_once()
|
||||||
|
sub.Sync_LocalHalf.assert_called_once()
|
||||||
|
|
||||||
|
def test_interleaved_reports_failures_by_a_unique_path(self) -> None:
|
||||||
|
"""Test failing projects are listed by a path that is theirs alone."""
|
||||||
|
opt = self._get_opts(["--interleaved", "-j4"])
|
||||||
|
outer = self._make_syncable(
|
||||||
|
FakeProject("foo", name="outer", objdir="a")
|
||||||
|
)
|
||||||
|
sub = self._make_syncable(
|
||||||
|
FakeProject("foo", name="sub", objdir="b", path_prefix="sub")
|
||||||
|
)
|
||||||
|
sub.Sync_LocalHalf.side_effect = GitError("checkout failed")
|
||||||
|
self.cmd.git_event_log = mock.MagicMock()
|
||||||
|
|
||||||
|
with self.assertRaises(sync.SyncError):
|
||||||
|
self._run_interleaved(opt, [outer, sub], [outer, sub])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.cmd._interleaved_err_checkout_results, ["sub/foo"]
|
||||||
|
)
|
||||||
|
|
||||||
def test_interleaved_shared_objdir_serial(self):
|
def test_interleaved_shared_objdir_serial(self):
|
||||||
"""Test that projects with shared objdir are processed serially."""
|
"""Test that projects with shared objdir are processed serially."""
|
||||||
opt, args = self.cmd.OptionParser.parse_args(["--interleaved", "-j4"])
|
opt, args = self.cmd.OptionParser.parse_args(["--interleaved", "-j4"])
|
||||||
@@ -1083,6 +1630,23 @@ class InterleavedSyncTest(unittest.TestCase):
|
|||||||
project.Sync_NetworkHalf.assert_called_once()
|
project.Sync_NetworkHalf.assert_called_once()
|
||||||
project.Sync_LocalHalf.assert_called_once()
|
project.Sync_LocalHalf.assert_called_once()
|
||||||
|
|
||||||
|
def test_worker_reports_a_path_unique_across_manifests(self) -> None:
|
||||||
|
"""Test _SyncResult.relpath tells apart same-path projects."""
|
||||||
|
project = FakeProject("foo", objdir="objA", path_prefix="sub")
|
||||||
|
self._make_syncable(project)
|
||||||
|
self.mock_context["projects"] = [project]
|
||||||
|
|
||||||
|
for this_manifest_only, expected in ((False, "sub/foo"), (True, "foo")):
|
||||||
|
with self.subTest(this_manifest_only=this_manifest_only):
|
||||||
|
opt = self._get_opts()
|
||||||
|
opt.this_manifest_only = this_manifest_only
|
||||||
|
with mock.patch("subcmds.sync.SyncBuffer") as mock_sync_buffer:
|
||||||
|
mock_sync_buffer.return_value.Finish.return_value = True
|
||||||
|
mock_sync_buffer.return_value.errors = []
|
||||||
|
result_obj = self.cmd._SyncProjectList(opt, [0])
|
||||||
|
|
||||||
|
self.assertEqual(result_obj.results[0].relpath, expected)
|
||||||
|
|
||||||
def test_worker_fetch_fails(self):
|
def test_worker_fetch_fails(self):
|
||||||
"""Test _SyncProjectList with a failed fetch."""
|
"""Test _SyncProjectList with a failed fetch."""
|
||||||
opt = self._get_opts()
|
opt = self._get_opts()
|
||||||
|
|||||||
@@ -63,3 +63,57 @@ def test_UploadAndReport_UnhandledError(cmd: upload.Upload) -> None:
|
|||||||
with mock.patch.object(cmd, "_UploadBranch", side_effect=UnexpectedError):
|
with mock.patch.object(cmd, "_UploadBranch", side_effect=UnexpectedError):
|
||||||
with pytest.raises(UnexpectedError):
|
with pytest.raises(UnexpectedError):
|
||||||
cmd._UploadAndReport(opt, [mock.MagicMock()], _STUB_PEOPLE)
|
cmd._UploadAndReport(opt, [mock.MagicMock()], _STUB_PEOPLE)
|
||||||
|
|
||||||
|
|
||||||
|
def test_GetMergeBranch_explicit_branch(cmd: upload.Upload) -> None:
|
||||||
|
"""Verify _GetMergeBranch reads branch.merge for explicit local_branch."""
|
||||||
|
mock_project = mock.MagicMock()
|
||||||
|
mock_branch = mock.MagicMock()
|
||||||
|
mock_branch.merge = "refs/heads/main"
|
||||||
|
mock_project.GetBranch.return_value = mock_branch
|
||||||
|
|
||||||
|
res = cmd._GetMergeBranch(mock_project, local_branch="feature")
|
||||||
|
assert res == "refs/heads/main"
|
||||||
|
mock_project.GetBranch.assert_called_once_with("feature")
|
||||||
|
|
||||||
|
|
||||||
|
def test_GetMergeBranch_current_branch(cmd: upload.Upload) -> None:
|
||||||
|
"""Verify _GetMergeBranch falls back to project.CurrentBranch."""
|
||||||
|
mock_project = mock.MagicMock()
|
||||||
|
mock_project.CurrentBranch = "auto-cbr"
|
||||||
|
mock_branch = mock.MagicMock()
|
||||||
|
mock_branch.merge = "refs/heads/upstream-main"
|
||||||
|
mock_project.GetBranch.return_value = mock_branch
|
||||||
|
|
||||||
|
res = cmd._GetMergeBranch(mock_project, local_branch=None)
|
||||||
|
assert res == "refs/heads/upstream-main"
|
||||||
|
mock_project.GetBranch.assert_called_once_with("auto-cbr")
|
||||||
|
|
||||||
|
|
||||||
|
def test_GetMergeBranch_none_when_no_branch(cmd: upload.Upload) -> None:
|
||||||
|
"""Verify _GetMergeBranch returns empty string when detached HEAD."""
|
||||||
|
mock_project = mock.MagicMock()
|
||||||
|
mock_project.CurrentBranch = None
|
||||||
|
|
||||||
|
res = cmd._GetMergeBranch(mock_project, local_branch=None)
|
||||||
|
assert res == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_GatherOne_returns_resolved_current_branch(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Upload error reporting reuses the branch gathered by the worker."""
|
||||||
|
project = mock.MagicMock()
|
||||||
|
project.CurrentBranch = "topic"
|
||||||
|
branch = mock.sentinel.branch
|
||||||
|
project.GetUploadableBranch.return_value = branch
|
||||||
|
monkeypatch.setattr(
|
||||||
|
upload.Upload,
|
||||||
|
"get_parallel_context",
|
||||||
|
lambda: {"projects": [project]},
|
||||||
|
)
|
||||||
|
opt = mock.MagicMock(current_branch=True)
|
||||||
|
|
||||||
|
assert upload.Upload._GatherOne(opt, 0) == (0, [branch], "topic")
|
||||||
|
|
||||||
|
project.GetUploadableBranch.assert_called_once_with("topic")
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# 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 subcmds/version.py."""
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from subcmds import version
|
||||||
|
|
||||||
|
|
||||||
|
def test_repo_version_uses_one_pretty_format_call(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
project = mock.MagicMock()
|
||||||
|
project.bare_git.log.return_value = "v2.0-1-g12345678\nTue, 25 Aug\n"
|
||||||
|
monkeypatch.setattr(version, "git_require", lambda _version: True)
|
||||||
|
|
||||||
|
result = version.Version._RepoVersion(project)
|
||||||
|
|
||||||
|
assert result == ("v2.0-1-g12345678", "Tue, 25 Aug")
|
||||||
|
project.bare_git.log.assert_called_once_with(
|
||||||
|
"-1", "--format=%(describe)%n%cD", "HEAD"
|
||||||
|
)
|
||||||
|
project.bare_git.describe.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_repo_version_keeps_old_git_fallback(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
project = mock.MagicMock()
|
||||||
|
project.bare_git.describe.return_value = "v2.0"
|
||||||
|
project.bare_git.log.return_value = "Tue, 25 Aug"
|
||||||
|
monkeypatch.setattr(version, "git_require", lambda _version: False)
|
||||||
|
|
||||||
|
result = version.Version._RepoVersion(project)
|
||||||
|
|
||||||
|
assert result == ("v2.0", "Tue, 25 Aug")
|
||||||
|
project.bare_git.describe.assert_called_once_with("HEAD")
|
||||||
|
project.bare_git.log.assert_called_once_with("-1", "--format=%cD", "HEAD")
|
||||||
Reference in New Issue
Block a user