mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-11-14 16:27:13 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7c431b996 | ||
|
|
020767b9d3 | ||
|
|
0f83e19bbb | ||
|
|
221a31cd0a | ||
|
|
ccddf51633 | ||
|
|
72e3c8c277 | ||
|
|
4cef373d98 | ||
|
|
95d7c8c0df | ||
|
|
616396317c | ||
|
|
4978203b07 | ||
|
|
cc3f52cd6c | ||
|
|
e045aa19d8 | ||
|
|
c382d3807a | ||
|
|
9d01d216e0 | ||
|
|
52f01a18da | ||
|
|
3a3d1fc7cd | ||
|
|
2029997aef | ||
|
|
caa4cd6564 | ||
|
|
e15b3e0298 | ||
|
|
a4b44c5026 | ||
|
|
8f2c0f1483 | ||
|
|
7ed9700a95 | ||
|
|
d621aa8ee4 | ||
|
|
b16e5f49d6 | ||
|
|
6596cb6499 | ||
|
|
ecb91f465e |
No files matched your search
@@ -1,8 +1,54 @@
|
|||||||
|
# https://flake8.pycqa.org/en/latest/user/configuration.html
|
||||||
[flake8]
|
[flake8]
|
||||||
max-line-length = 80
|
max-line-length = 80
|
||||||
per-file-ignores =
|
per-file-ignores =
|
||||||
# E501: line too long
|
# E501: line too long
|
||||||
tests/test_git_superproject.py: E501
|
tests/test_git_superproject.py: E501,
|
||||||
|
# IMR241: When using the from syntax, only submodules are imported, not module elements.
|
||||||
|
# NB: Do not add any more here.
|
||||||
|
command.py: IMR241,
|
||||||
|
git_status.py: IMR241,
|
||||||
|
main.py: IMR241,
|
||||||
|
manifest_xml.py: IMR241,
|
||||||
|
subcmds/abandon.py: IMR241,
|
||||||
|
subcmds/branches.py: IMR241,
|
||||||
|
subcmds/checkout.py: IMR241,
|
||||||
|
subcmds/cherry_pick.py: IMR241,
|
||||||
|
subcmds/diffmanifests.py: IMR241,
|
||||||
|
subcmds/diff.py: IMR241,
|
||||||
|
subcmds/download.py: IMR241,
|
||||||
|
subcmds/forall.py: IMR241,
|
||||||
|
subcmds/gc.py: IMR241,
|
||||||
|
subcmds/grep.py: IMR241,
|
||||||
|
subcmds/help.py: IMR241,
|
||||||
|
subcmds/info.py: IMR241,
|
||||||
|
subcmds/init.py: IMR241,
|
||||||
|
subcmds/list.py: IMR241,
|
||||||
|
subcmds/manifest.py: IMR241,
|
||||||
|
subcmds/overview.py: IMR241,
|
||||||
|
subcmds/prune.py: IMR241,
|
||||||
|
subcmds/rebase.py: IMR241,
|
||||||
|
subcmds/selfupdate.py: IMR241,
|
||||||
|
subcmds/smartsync.py: IMR241,
|
||||||
|
subcmds/stage.py: IMR241,
|
||||||
|
subcmds/start.py: IMR241,
|
||||||
|
subcmds/status.py: IMR241,
|
||||||
|
subcmds/sync.py: IMR241,
|
||||||
|
subcmds/upload.py: IMR241,
|
||||||
|
subcmds/version.py: IMR241,
|
||||||
|
subcmds/wipe.py: IMR241,
|
||||||
|
tests/e2e/test_failures.py: IMR241,
|
||||||
|
tests/e2e/test_groups.py: IMR241,
|
||||||
|
tests/e2e/test_init_sync.py: IMR241,
|
||||||
|
tests/e2e/test_manifest_regressions.py: IMR241,
|
||||||
|
tests/e2e/test_partial_clone.py: IMR241,
|
||||||
|
tests/e2e/test_sync_modes.py: IMR241,
|
||||||
|
tests/e2e/test_workflows.py: IMR241,
|
||||||
|
tests/test_command.py: IMR241,
|
||||||
|
tests/test_error.py: IMR241,
|
||||||
|
tests/test_main.py: IMR241,
|
||||||
|
tests/test_subcmds.py: IMR241,
|
||||||
|
tests/test_subcmds_sync.py: IMR241,
|
||||||
extend-ignore =
|
extend-ignore =
|
||||||
# E203: Whitespace before ':'
|
# E203: Whitespace before ':'
|
||||||
# See https://github.com/PyCQA/pycodestyle/issues/373
|
# See https://github.com/PyCQA/pycodestyle/issues/373
|
||||||
@@ -15,3 +61,11 @@ exclude =
|
|||||||
.venv,
|
.venv,
|
||||||
venv,
|
venv,
|
||||||
.tox,
|
.tox,
|
||||||
|
# IMR241: When using the from syntax, only submodules are imported, not module elements.
|
||||||
|
# NB: Do not add more modules here without strong discussion. This is aligned
|
||||||
|
# with Google's Python style guide.
|
||||||
|
imr241_exclude =
|
||||||
|
collections.abc,
|
||||||
|
pathlib,
|
||||||
|
typing,
|
||||||
|
typing_extensions,
|
||||||
+2
-2
@@ -20,7 +20,7 @@ import re
|
|||||||
from typing import List, Optional, TYPE_CHECKING
|
from typing import List, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
import error
|
import error
|
||||||
from event_log import EventLog
|
import event_log
|
||||||
import progress
|
import progress
|
||||||
|
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ class Command:
|
|||||||
# provide event summary to callers. Only used by sync subcommand currently.
|
# provide event summary to callers. Only used by sync subcommand currently.
|
||||||
#
|
#
|
||||||
# NB: This is being replaced by git trace2 events. See git_trace2_event_log.
|
# NB: This is being replaced by git trace2 events. See git_trace2_event_log.
|
||||||
event_log = EventLog()
|
event_log = event_log.EventLog(enabled=False)
|
||||||
|
|
||||||
# Whether this command is a "common" one, i.e. whether the user would
|
# Whether this command is a "common" one, i.e. whether the user would
|
||||||
# commonly use it or it's a more uncommon command. This is used by the help
|
# commonly use it or it's a more uncommon command. This is used by the help
|
||||||
|
|||||||
+14
-9
@@ -42,13 +42,13 @@ postconditions to be met:
|
|||||||
|
|
||||||
1. When the manifest revision names a branch or tag, the mapped local tracking
|
1. When the manifest revision names a branch or tag, the mapped local tracking
|
||||||
ref (e.g. `refs/remotes/REPO_REMOTE/<branch>` for a branch, or
|
ref (e.g. `refs/remotes/REPO_REMOTE/<branch>` for a branch, or
|
||||||
`refs/tags/<tag>` for a tag) must resolve to a valid commit in the object
|
`refs/tags/<tag>` for a tag) must name the target commit, and must equal
|
||||||
store (`git rev-parse --verify <ref>^{commit}`), and must equal `REPO_TREV`
|
`REPO_TREV` if `REPO_TREV` was non-empty. A tag ref may name an annotated
|
||||||
if `REPO_TREV` was non-empty.
|
tag of the target commit.
|
||||||
2. `FETCH_HEAD` must resolve to the same target commit in the object store
|
2. `FETCH_HEAD` must name the same target commit.
|
||||||
(`git rev-parse --verify FETCH_HEAD^{commit}`).
|
3. The target commit, and the commit graph from it far enough to compute
|
||||||
3. The commit graph from the target commit must be reachable far enough to
|
merge bases with local branches, must be readable from the object store.
|
||||||
compute merge bases with local branches.
|
They may be materialized lazily, when first read.
|
||||||
|
|
||||||
### Invariants
|
### Invariants
|
||||||
|
|
||||||
@@ -66,5 +66,10 @@ postconditions to be met:
|
|||||||
|
|
||||||
* A non-zero exit status aborts the project's sync, and the command's stderr
|
* A non-zero exit status aborts the project's sync, and the command's stderr
|
||||||
is surfaced to the user.
|
is surfaced to the user.
|
||||||
* `repo` verifies the tracking ref and target reachability after exit 0. Any
|
* After exit 0, `repo` checks postconditions 1 and 2 by comparing the object
|
||||||
mismatch is treated as a failure.
|
IDs that the refs record (`git rev-parse --verify <ref>`), so it does not
|
||||||
|
read the object store during the network half of the sync. It peels the
|
||||||
|
IDs to commits only when they differ, as for an annotated tag. Any mismatch
|
||||||
|
is treated as a failure.
|
||||||
|
* A target commit missing from the object store fails the checkout that reads
|
||||||
|
it.
|
||||||
+69
-6
@@ -74,7 +74,8 @@ following DTD:
|
|||||||
<!ELEMENT project (annotation*,
|
<!ELEMENT project (annotation*,
|
||||||
project*,
|
project*,
|
||||||
copyfile*,
|
copyfile*,
|
||||||
linkfile*)>
|
linkfile*,
|
||||||
|
sparse-path*)>
|
||||||
<!ATTLIST project name CDATA #REQUIRED>
|
<!ATTLIST project name CDATA #REQUIRED>
|
||||||
<!ATTLIST project path CDATA #IMPLIED>
|
<!ATTLIST project path CDATA #IMPLIED>
|
||||||
<!ATTLIST project remote IDREF #IMPLIED>
|
<!ATTLIST project remote IDREF #IMPLIED>
|
||||||
@@ -102,9 +103,13 @@ following DTD:
|
|||||||
<!ATTLIST linkfile src CDATA #REQUIRED>
|
<!ATTLIST linkfile src CDATA #REQUIRED>
|
||||||
<!ATTLIST linkfile dest CDATA #REQUIRED>
|
<!ATTLIST linkfile dest CDATA #REQUIRED>
|
||||||
|
|
||||||
|
<!ELEMENT sparse-path EMPTY>
|
||||||
|
<!ATTLIST sparse-path path CDATA #REQUIRED>
|
||||||
|
|
||||||
<!ELEMENT extend-project (annotation*,
|
<!ELEMENT extend-project (annotation*,
|
||||||
copyfile*,
|
copyfile*,
|
||||||
linkfile*)>
|
linkfile*,
|
||||||
|
sparse-path*)>
|
||||||
<!ATTLIST extend-project name CDATA #REQUIRED>
|
<!ATTLIST extend-project name CDATA #REQUIRED>
|
||||||
<!ATTLIST extend-project path CDATA #IMPLIED>
|
<!ATTLIST extend-project path CDATA #IMPLIED>
|
||||||
<!ATTLIST extend-project dest-path CDATA #IMPLIED>
|
<!ATTLIST extend-project dest-path CDATA #IMPLIED>
|
||||||
@@ -424,10 +429,11 @@ attributes of an existing project without completely replacing the
|
|||||||
existing project definition. This makes the local manifest more robust
|
existing project definition. This makes the local manifest more robust
|
||||||
against changes to the original manifest.
|
against changes to the original manifest.
|
||||||
|
|
||||||
The `extend-project` element can also contain `annotation`, `copyfile`, and
|
The `extend-project` element can also contain `annotation`, `copyfile`,
|
||||||
`linkfile` child elements. These are added to the project's definition. A
|
`linkfile`, and `sparse-path` child elements. These are added to the project's
|
||||||
`copyfile` or `linkfile` with a `dest` that already exists in the project
|
definition. A `copyfile` or `linkfile` with a `dest` that already exists in the
|
||||||
will overwrite the original.
|
project will overwrite the original. There is no way to remove a `sparse-path`
|
||||||
|
that the original project definition declared.
|
||||||
|
|
||||||
Attribute `path`: If specified, limit the change to projects checked out
|
Attribute `path`: If specified, limit the change to projects checked out
|
||||||
at the specified path, rather than all projects with the given name.
|
at the specified path, rather than all projects with the given name.
|
||||||
@@ -471,6 +477,63 @@ attribute "keep" which accepts the case insensitive values "true"
|
|||||||
(default) or "false". This attribute determines whether or not the
|
(default) or "false". This attribute determines whether or not the
|
||||||
annotation will be kept when exported with the manifest subcommand.
|
annotation will be kept when exported with the manifest subcommand.
|
||||||
|
|
||||||
|
### Element sparse-path
|
||||||
|
|
||||||
|
Zero or more sparse-path elements may be specified as children of a
|
||||||
|
project element or an extend-project element. Each element specifies a
|
||||||
|
path to include in the working directory.
|
||||||
|
|
||||||
|
Requires Git 2.25.0 or later, which is when `git sparse-checkout` and cone mode
|
||||||
|
were introduced. Declaring a sparse-path with an older Git fails the sync for
|
||||||
|
that project.
|
||||||
|
|
||||||
|
Declaring at least one sparse-path is what enables Git sparse-checkout for
|
||||||
|
the project: the listed paths are checked out, and other directories are left
|
||||||
|
out of the working directory. Cone mode also checks out the files sitting
|
||||||
|
directly in the project's top-level directory and in each leading directory of
|
||||||
|
a sparse-path, so `src/backend` brings along the top-level files and the files
|
||||||
|
directly under `src/` too. A project with no sparse-path elements is checked
|
||||||
|
out in full.
|
||||||
|
The checkout uses [cone mode](https://git-scm.com/docs/git-sparse-checkout#_internalscone_mode_handling)
|
||||||
|
for better performance, which means only directory paths are supported;
|
||||||
|
individual files and globs are not. Cone mode does no pattern matching, so `*`,
|
||||||
|
`?`, `[`, `]`, and `\` are not glob syntax in a `path`. In fact, Git 2.36.0 and
|
||||||
|
later reject them with `specify directories rather than patterns`, which
|
||||||
|
surfaces as a sync failure for the project. Earlier versions starting with
|
||||||
|
2.25.0 have no such check and are treated as literals.
|
||||||
|
|
||||||
|
Sparse-checkout is configured only when the project's working directory is
|
||||||
|
first created. Changes to `sparse-path` in the manifest (added, changed, or
|
||||||
|
removed) are not applied to a project that is already checked out. To change
|
||||||
|
the paths in an existing checkout, run `git sparse-checkout` in the project
|
||||||
|
directly, or remove the project's working directory and sync again.
|
||||||
|
|
||||||
|
Because sparse-checkout only prunes the working directory, a project with at
|
||||||
|
least one sparse-path is fetched with `--filter=blob:none` so that blobs which
|
||||||
|
will never be checked out are not downloaded. Without the filter Git still
|
||||||
|
downloads every blob in the project and sparse-checkout merely declines to
|
||||||
|
write most of them to disk, so the filter is what turns sparse-checkout into a
|
||||||
|
saving in transfer size and server load. The filter from
|
||||||
|
`repo init --partial-clone --clone-filter` takes precedence, and projects listed
|
||||||
|
in `repo init --partial-clone-exclude` are fetched in full as usual. Like
|
||||||
|
sparse-checkout itself, the implied filter only applies when the project is
|
||||||
|
first fetched (or is already a partial clone), so adding a sparse-path to an
|
||||||
|
existing full checkout does not convert it to a partial clone. Mirrors are never
|
||||||
|
filtered.
|
||||||
|
|
||||||
|
Because that filter is only useful if the objects it excludes are not fetched
|
||||||
|
by some other route, a project that takes the implied `--filter=blob:none` also
|
||||||
|
skips the `/clone.bundle` download: the bundle is an unfiltered pack of every
|
||||||
|
object, so fetching it would undo the filter. Projects that set the filter
|
||||||
|
explicitly, or that are listed in `repo init --partial-clone-exclude`, keep
|
||||||
|
using the clone bundle.
|
||||||
|
|
||||||
|
Attribute `path`: A path relative to the project root to include in the
|
||||||
|
sparse-checkout. The path is validated like `<copyfile>` and `<linkfile>`
|
||||||
|
paths: it must stay inside the project, so absolute paths, `.` or `..`
|
||||||
|
components, `~`, and `.git` or `.repo*` components are all rejected. A trailing
|
||||||
|
`/` is allowed.
|
||||||
|
|
||||||
### Element copyfile
|
### Element copyfile
|
||||||
|
|
||||||
Zero or more copyfile elements may be specified as children of a
|
Zero or more copyfile elements may be specified as children of a
|
||||||
|
|||||||
@@ -91,8 +91,9 @@ Detecting collisions with untracked files or unstaged working-tree modifications
|
|||||||
After the command exits with status 0, `repo` expects the following
|
After the command exits with status 0, `repo` expects the following
|
||||||
postconditions to be met:
|
postconditions to be met:
|
||||||
|
|
||||||
1. `git diff-index --quiet --cached REPO_TREV^{tree}` exits 0 (the index
|
1. The index matches `REPO_TREV`'s tree (`repo` trusts the command's exit
|
||||||
matches the target tree).
|
status and does not run `git diff-index` against `REPO_TREV^{tree}`, which
|
||||||
|
would read every tree object from `.git/objects`).
|
||||||
2. `HEAD` still names what it did before the command, and its resolved commit
|
2. `HEAD` still names what it did before the command, and its resolved commit
|
||||||
object ID has not changed.
|
object ID has not changed.
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -89,8 +90,6 @@ least one of these before using this command.""", # noqa: E501
|
|||||||
|
|
||||||
if platform_utils.isWindows():
|
if platform_utils.isWindows():
|
||||||
# Split on spaces, respecting quoted strings
|
# Split on spaces, respecting quoted strings
|
||||||
import shlex
|
|
||||||
|
|
||||||
args = shlex.split(editor)
|
args = shlex.split(editor)
|
||||||
shell = False
|
shell = False
|
||||||
elif re.compile("^.*[$ \t'].*$").match(editor):
|
elif re.compile("^.*[$ \t'].*$").match(editor):
|
||||||
|
|||||||
+11
-3
@@ -47,10 +47,15 @@ class EventLog:
|
|||||||
Specific tasks may include additional informational properties.
|
Specific tasks may include additional informational properties.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, enabled: bool = True) -> None:
|
||||||
"""Initializes the event log."""
|
"""Initializes the event log.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enabled: Whether event logging is enabled.
|
||||||
|
"""
|
||||||
self._log = []
|
self._log = []
|
||||||
self._parent = None
|
self._parent = None
|
||||||
|
self.enabled = enabled
|
||||||
|
|
||||||
def Add(
|
def Add(
|
||||||
self,
|
self,
|
||||||
@@ -74,8 +79,11 @@ class EventLog:
|
|||||||
kind: The kind of the object for the unique identifier.
|
kind: The kind of the object for the unique identifier.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A dictionary of the event added to the log.
|
A dictionary of the event added to the log, or None if disabled.
|
||||||
"""
|
"""
|
||||||
|
if not self.enabled:
|
||||||
|
return None
|
||||||
|
|
||||||
event = {
|
event = {
|
||||||
"id": (kind, _NextEventId()),
|
"id": (kind, _NextEventId()),
|
||||||
"name": name,
|
"name": name,
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from urllib.parse import urlparse
|
import urllib.parse
|
||||||
from urllib.request import urlopen
|
import urllib.request
|
||||||
|
|
||||||
import error
|
import error
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ def fetch_file(url, verbose=False):
|
|||||||
Returns:
|
Returns:
|
||||||
The contents of the file as bytes.
|
The contents of the file as bytes.
|
||||||
"""
|
"""
|
||||||
scheme = urlparse(url).scheme
|
scheme = urllib.parse.urlparse(url).scheme
|
||||||
if scheme == "gs":
|
if scheme == "gs":
|
||||||
cmd = ["gcloud", "storage", "cat", url]
|
cmd = ["gcloud", "storage", "cat", url]
|
||||||
errors = []
|
errors = []
|
||||||
@@ -54,5 +54,5 @@ def fetch_file(url, verbose=False):
|
|||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
raise FetchFileError(aggregate_errors=errors)
|
raise FetchFileError(aggregate_errors=errors)
|
||||||
with urlopen(url) as f:
|
with urllib.request.urlopen(url) as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
+4
-4
@@ -21,11 +21,11 @@ import sys
|
|||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
import error
|
import error
|
||||||
from git_trace2_event_log_base import BaseEventLog
|
import git_trace2_event_log_base
|
||||||
import platform_utils
|
import platform_utils
|
||||||
import repo_logging
|
import repo_logging
|
||||||
import repo_trace
|
import repo_trace
|
||||||
from wrapper import Wrapper
|
import wrapper
|
||||||
|
|
||||||
|
|
||||||
GIT = "git"
|
GIT = "git"
|
||||||
@@ -46,7 +46,7 @@ logger = repo_logging.RepoLogger(__file__)
|
|||||||
class _GitCall:
|
class _GitCall:
|
||||||
@functools.lru_cache(maxsize=None) # noqa: B019
|
@functools.lru_cache(maxsize=None) # noqa: B019
|
||||||
def version_tuple(self):
|
def version_tuple(self):
|
||||||
ret = Wrapper().ParseGitVersion()
|
ret = wrapper.Wrapper().ParseGitVersion()
|
||||||
if ret is None:
|
if ret is None:
|
||||||
msg = "fatal: unable to detect git version"
|
msg = "fatal: unable to detect git version"
|
||||||
logger.error(msg)
|
logger.error(msg)
|
||||||
@@ -347,7 +347,7 @@ class GitCommand:
|
|||||||
command.extend(cmdv[1:])
|
command.extend(cmdv[1:])
|
||||||
|
|
||||||
event_log = (
|
event_log = (
|
||||||
BaseEventLog(env=env, add_init_count=True)
|
git_trace2_event_log_base.BaseEventLog(env=env, add_init_count=True)
|
||||||
if add_event_log
|
if add_event_log
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|||||||
+169
-32
@@ -15,6 +15,7 @@
|
|||||||
import contextlib
|
import contextlib
|
||||||
import datetime
|
import datetime
|
||||||
import errno
|
import errno
|
||||||
|
import hashlib
|
||||||
import http.client
|
import http.client
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -22,7 +23,8 @@ import re
|
|||||||
import ssl
|
import ssl
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from typing import Union
|
import tempfile
|
||||||
|
from typing import Optional, Tuple, Union
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
@@ -41,6 +43,9 @@ ID_RE = re.compile(r"^(?:[0-9a-f]{40}|[0-9a-f]{64})$")
|
|||||||
|
|
||||||
REVIEW_CACHE = {}
|
REVIEW_CACHE = {}
|
||||||
|
|
||||||
|
# Metadata key stored in .repo_config.json to detect same-tick config edits.
|
||||||
|
_JSON_META_KEY = "__repo_meta__"
|
||||||
|
|
||||||
|
|
||||||
def IsChange(rev):
|
def IsChange(rev):
|
||||||
return rev.startswith(git_refs.R_CHANGES)
|
return rev.startswith(git_refs.R_CHANGES)
|
||||||
@@ -161,6 +166,7 @@ class GitConfig:
|
|||||||
self.file = str(configfile)
|
self.file = str(configfile)
|
||||||
self.defaults = defaults
|
self.defaults = defaults
|
||||||
self._cache_dict = None
|
self._cache_dict = None
|
||||||
|
self._cache_fingerprint = None
|
||||||
self._section_dict = None
|
self._section_dict = None
|
||||||
self._remotes = {}
|
self._remotes = {}
|
||||||
self._branches = {}
|
self._branches = {}
|
||||||
@@ -175,6 +181,20 @@ class GitConfig:
|
|||||||
def ClearCache(self):
|
def ClearCache(self):
|
||||||
"""Clear the in-memory cache of config."""
|
"""Clear the in-memory cache of config."""
|
||||||
self._cache_dict = None
|
self._cache_dict = None
|
||||||
|
self._cache_fingerprint = None
|
||||||
|
self._section_dict = None
|
||||||
|
self._remotes = {}
|
||||||
|
self._branches = {}
|
||||||
|
|
||||||
|
def _RemoveJson(self) -> None:
|
||||||
|
with contextlib.suppress(OSError):
|
||||||
|
platform_utils.remove(self._json, missing_ok=True)
|
||||||
|
|
||||||
|
def Reload(self) -> None:
|
||||||
|
"""Clear cached state and reload configuration from disk."""
|
||||||
|
self.ClearCache()
|
||||||
|
self._RemoveJson()
|
||||||
|
_ = self._cache
|
||||||
|
|
||||||
def Has(self, name, include_defaults=True):
|
def Has(self, name, include_defaults=True):
|
||||||
"""Return true if this configuration file has the key."""
|
"""Return true if this configuration file has the key."""
|
||||||
@@ -292,6 +312,13 @@ class GitConfig:
|
|||||||
"""
|
"""
|
||||||
key = _key(name)
|
key = _key(name)
|
||||||
|
|
||||||
|
if (
|
||||||
|
self._cache_dict is not None
|
||||||
|
and self._ReadFileFingerprint() != self._cache_fingerprint
|
||||||
|
):
|
||||||
|
self.ClearCache()
|
||||||
|
self._RemoveJson()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
old = self._cache[key]
|
old = self._cache[key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -306,8 +333,9 @@ class GitConfig:
|
|||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
if old:
|
if old:
|
||||||
|
self._do("--unset-all", name, allow_missing=True)
|
||||||
del self._cache[key]
|
del self._cache[key]
|
||||||
self._do("--unset-all", name)
|
self._SyncCacheAfterSet(key)
|
||||||
|
|
||||||
elif isinstance(value, list):
|
elif isinstance(value, list):
|
||||||
if len(value) == 0:
|
if len(value) == 0:
|
||||||
@@ -317,14 +345,48 @@ class GitConfig:
|
|||||||
self.SetString(name, value[0])
|
self.SetString(name, value[0])
|
||||||
|
|
||||||
elif old != value:
|
elif old != value:
|
||||||
self._cache[key] = list(value)
|
|
||||||
self._do("--replace-all", name, value[0])
|
self._do("--replace-all", name, value[0])
|
||||||
for i in range(1, len(value)):
|
try:
|
||||||
self._do("--add", name, value[i])
|
for i in range(1, len(value)):
|
||||||
|
self._do("--add", name, value[i])
|
||||||
|
except BaseException:
|
||||||
|
self.ClearCache()
|
||||||
|
self._RemoveJson()
|
||||||
|
raise
|
||||||
|
self._cache[key] = list(value)
|
||||||
|
self._SyncCacheAfterSet(key)
|
||||||
|
|
||||||
elif len(old) != 1 or old[0] != value:
|
elif len(old) != 1 or old[0] != value:
|
||||||
self._cache[key] = [value]
|
|
||||||
self._do("--replace-all", name, value)
|
self._do("--replace-all", name, value)
|
||||||
|
self._cache[key] = [value]
|
||||||
|
self._SyncCacheAfterSet(key)
|
||||||
|
|
||||||
|
def _SyncCacheAfterSet(self, key: str) -> None:
|
||||||
|
"""Keep the in-memory section index and JSON cache coherent.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Canonicalized configuration key that was modified.
|
||||||
|
"""
|
||||||
|
self._section_dict = None
|
||||||
|
if key.startswith("remote."):
|
||||||
|
self._remotes = {}
|
||||||
|
self._branches = {}
|
||||||
|
elif key.startswith("branch."):
|
||||||
|
self._branches = {}
|
||||||
|
|
||||||
|
has_includes = self._cache_dict is not None and (
|
||||||
|
"include.path" in self._cache_dict
|
||||||
|
or any(k.startswith("includeif.") for k in self._cache_dict)
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
key == "include.path"
|
||||||
|
or key.startswith("includeif.")
|
||||||
|
or has_includes
|
||||||
|
):
|
||||||
|
self.ClearCache()
|
||||||
|
self._RemoveJson()
|
||||||
|
else:
|
||||||
|
self._SaveJson(self._cache)
|
||||||
|
|
||||||
def GetRemote(self, name):
|
def GetRemote(self, name):
|
||||||
"""Get the remote.$name.* configuration values as an object."""
|
"""Get the remote.$name.* configuration values as an object."""
|
||||||
@@ -417,27 +479,109 @@ class GitConfig:
|
|||||||
self._SaveJson(d)
|
self._SaveJson(d)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def _ReadFileFingerprint(self) -> Optional[Tuple[int, int, int, int, str]]:
|
||||||
|
"""Return (mtime_ns, ctime_ns, size, ino, sha1_hex) for self.file."""
|
||||||
|
try:
|
||||||
|
with open(self.file, "rb") as fd:
|
||||||
|
raw_cfg = fd.read()
|
||||||
|
st = os.fstat(fd.fileno())
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
return (
|
||||||
|
st.st_mtime_ns,
|
||||||
|
st.st_ctime_ns,
|
||||||
|
st.st_size,
|
||||||
|
st.st_ino,
|
||||||
|
hashlib.sha1(raw_cfg).hexdigest(),
|
||||||
|
)
|
||||||
|
|
||||||
def _ReadJson(self):
|
def _ReadJson(self):
|
||||||
try:
|
try:
|
||||||
if os.path.getmtime(self._json) <= os.path.getmtime(self.file):
|
cfg_stat = os.stat(self.file)
|
||||||
platform_utils.remove(self._json)
|
json_stat = os.stat(self._json)
|
||||||
return None
|
|
||||||
except OSError:
|
except OSError:
|
||||||
return None
|
return None
|
||||||
|
if json_stat.st_mtime_ns < cfg_stat.st_mtime_ns:
|
||||||
|
self._RemoveJson()
|
||||||
|
return None
|
||||||
try:
|
try:
|
||||||
with repo_trace.Trace(": parsing %s", self.file):
|
with repo_trace.Trace(": parsing %s", self.file):
|
||||||
with open(self._json) as fd:
|
with open(self._json, encoding="utf-8") as fd:
|
||||||
return json.load(fd)
|
data = json.load(fd)
|
||||||
except (OSError, ValueError):
|
except (OSError, ValueError):
|
||||||
platform_utils.remove(self._json, missing_ok=True)
|
self._RemoveJson()
|
||||||
|
return None
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
self._RemoveJson()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
meta = data.pop(_JSON_META_KEY, None)
|
||||||
|
if (
|
||||||
|
isinstance(meta, list)
|
||||||
|
and len(meta) == 2
|
||||||
|
and isinstance(meta[1], str)
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
json_stat.st_mtime_ns == cfg_stat.st_mtime_ns
|
||||||
|
or cfg_stat.st_size != meta[0]
|
||||||
|
):
|
||||||
|
if cfg_stat.st_size != meta[0]:
|
||||||
|
self._RemoveJson()
|
||||||
|
return None
|
||||||
|
fp = self._ReadFileFingerprint()
|
||||||
|
if fp is None:
|
||||||
|
return None
|
||||||
|
if fp[4] != meta[1]:
|
||||||
|
self._RemoveJson()
|
||||||
|
return None
|
||||||
|
self._cache_fingerprint = fp
|
||||||
|
else:
|
||||||
|
self._cache_fingerprint = (
|
||||||
|
cfg_stat.st_mtime_ns,
|
||||||
|
cfg_stat.st_ctime_ns,
|
||||||
|
cfg_stat.st_size,
|
||||||
|
cfg_stat.st_ino,
|
||||||
|
meta[1],
|
||||||
|
)
|
||||||
|
elif json_stat.st_mtime_ns <= cfg_stat.st_mtime_ns:
|
||||||
|
self._RemoveJson()
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
fp = self._ReadFileFingerprint()
|
||||||
|
if fp is None:
|
||||||
|
return None
|
||||||
|
self._cache_fingerprint = fp
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
def _SaveJson(self, cache):
|
def _SaveJson(self, cache):
|
||||||
|
fp = self._ReadFileFingerprint()
|
||||||
|
if fp is None:
|
||||||
|
self._cache_fingerprint = None
|
||||||
|
self._RemoveJson()
|
||||||
|
return
|
||||||
|
self._cache_fingerprint = fp
|
||||||
|
|
||||||
|
payload = dict(cache)
|
||||||
|
payload[_JSON_META_KEY] = [fp[2], fp[4]]
|
||||||
|
json_dir = os.path.dirname(self._json) or "."
|
||||||
|
tmp_path = None
|
||||||
try:
|
try:
|
||||||
with open(self._json, "w") as fd:
|
with tempfile.NamedTemporaryFile(
|
||||||
json.dump(cache, fd, indent=2)
|
"w",
|
||||||
|
dir=json_dir,
|
||||||
|
prefix=f".tmp{os.path.basename(self._json)}.",
|
||||||
|
delete=False,
|
||||||
|
encoding="utf-8",
|
||||||
|
) as fd:
|
||||||
|
tmp_path = fd.name
|
||||||
|
json.dump(payload, fd, indent=2)
|
||||||
|
os.replace(tmp_path, self._json)
|
||||||
except (OSError, TypeError):
|
except (OSError, TypeError):
|
||||||
platform_utils.remove(self._json, missing_ok=True)
|
if tmp_path is not None:
|
||||||
|
with contextlib.suppress(OSError):
|
||||||
|
platform_utils.remove(tmp_path, missing_ok=True)
|
||||||
|
self._RemoveJson()
|
||||||
|
|
||||||
def _ReadGit(self):
|
def _ReadGit(self):
|
||||||
"""
|
"""
|
||||||
@@ -448,6 +592,7 @@ class GitConfig:
|
|||||||
"""
|
"""
|
||||||
c = {}
|
c = {}
|
||||||
if not os.path.exists(self.file):
|
if not os.path.exists(self.file):
|
||||||
|
self._cache_fingerprint = None
|
||||||
return c
|
return c
|
||||||
|
|
||||||
d = self._do("--null", "--list")
|
d = self._do("--null", "--list")
|
||||||
@@ -463,9 +608,10 @@ class GitConfig:
|
|||||||
else:
|
else:
|
||||||
c[key] = [val]
|
c[key] = [val]
|
||||||
|
|
||||||
|
self._cache_fingerprint = self._ReadFileFingerprint()
|
||||||
return c
|
return c
|
||||||
|
|
||||||
def _do(self, *args):
|
def _do(self, *args, allow_missing: bool = False) -> str:
|
||||||
if self.file == self._SYSTEM_CONFIG:
|
if self.file == self._SYSTEM_CONFIG:
|
||||||
command = ["config", "--system", "--includes"]
|
command = ["config", "--system", "--includes"]
|
||||||
else:
|
else:
|
||||||
@@ -475,10 +621,10 @@ class GitConfig:
|
|||||||
p = git_command.GitCommand(
|
p = git_command.GitCommand(
|
||||||
None, command, capture_stdout=True, capture_stderr=True
|
None, command, capture_stdout=True, capture_stderr=True
|
||||||
)
|
)
|
||||||
if p.Wait() == 0:
|
rc = p.Wait()
|
||||||
|
if rc == 0 or (allow_missing and rc == 5):
|
||||||
return p.stdout
|
return p.stdout
|
||||||
else:
|
raise error.GitError(f"git {' '.join(command)}: {p.stderr}")
|
||||||
raise error.GitError(f"git {' '.join(command)}: {p.stderr}")
|
|
||||||
|
|
||||||
|
|
||||||
class RepoConfig(GitConfig):
|
class RepoConfig(GitConfig):
|
||||||
@@ -814,20 +960,11 @@ class Branch:
|
|||||||
|
|
||||||
def Save(self):
|
def Save(self):
|
||||||
"""Save this branch back into the configuration."""
|
"""Save this branch back into the configuration."""
|
||||||
if self._config.HasSection("branch", self.name):
|
if self.remote:
|
||||||
if self.remote:
|
self._Set("remote", self.remote.name)
|
||||||
self._Set("remote", self.remote.name)
|
|
||||||
else:
|
|
||||||
self._Set("remote", None)
|
|
||||||
self._Set("merge", self.merge)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
with open(self._config.file, "a") as fd:
|
self._Set("remote", None)
|
||||||
fd.write('[branch "%s"]\n' % self.name)
|
self._Set("merge", self.merge)
|
||||||
if self.remote:
|
|
||||||
fd.write("\tremote = %s\n" % self.remote.name)
|
|
||||||
if self.merge:
|
|
||||||
fd.write("\tmerge = %s\n" % self.merge)
|
|
||||||
|
|
||||||
def _Set(self, key, value):
|
def _Set(self, key, value):
|
||||||
key = f"branch.{self.name}.{key}"
|
key = f"branch.{self.name}.{key}"
|
||||||
|
|||||||
+123
-16
@@ -27,6 +27,8 @@ R_PUB = "refs/published/"
|
|||||||
R_WORKTREE = "refs/worktree/"
|
R_WORKTREE = "refs/worktree/"
|
||||||
R_WORKTREE_M = R_WORKTREE + "m/"
|
R_WORKTREE_M = R_WORKTREE + "m/"
|
||||||
R_M = "refs/remotes/m/"
|
R_M = "refs/remotes/m/"
|
||||||
|
# Maximum symbolic-ref indirection depth (matches Git SYMREF_MAXDEPTH).
|
||||||
|
_SYMREF_MAXDEPTH = 5
|
||||||
|
|
||||||
|
|
||||||
class GitRefs:
|
class GitRefs:
|
||||||
@@ -76,6 +78,101 @@ class GitRefs:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def UpdateSymref(self, name: str, target: str) -> None:
|
||||||
|
"""Update a symbolic reference in an already-loaded ref snapshot.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Full symbolic reference name (e.g. "refs/remotes/m/main").
|
||||||
|
target: Target reference name that the symref points to.
|
||||||
|
"""
|
||||||
|
if self._phyref is None:
|
||||||
|
return
|
||||||
|
if self._HasExternalRefChanges(name):
|
||||||
|
self._phyref = None
|
||||||
|
return
|
||||||
|
|
||||||
|
self._symref[name] = target
|
||||||
|
self._ResolveSymrefs()
|
||||||
|
|
||||||
|
parts = name.strip("/").split("/")
|
||||||
|
for idx in range(1, len(parts)):
|
||||||
|
anc = os.path.join(*parts[:idx])
|
||||||
|
self._TrackMtime(anc)
|
||||||
|
self._TrackMtime(os.path.join(*parts))
|
||||||
|
|
||||||
|
def _HasExternalRefChanges(self, updated_symref: str) -> bool:
|
||||||
|
"""Check whether refs outside |updated_symref| changed on disk.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
updated_symref: Symbolic ref path just updated on disk.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if reftable is in use or any unrelated ref path changed.
|
||||||
|
"""
|
||||||
|
if os.path.exists(os.path.join(self._gitdir, "reftable")):
|
||||||
|
return True
|
||||||
|
if "packed-refs" not in self._mtime and os.path.exists(
|
||||||
|
os.path.join(self._gitdir, "packed-refs")
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
parts = updated_symref.strip("/").split("/")
|
||||||
|
expected_child = {
|
||||||
|
"/".join(parts[:idx]): parts[idx] for idx in range(1, len(parts))
|
||||||
|
}
|
||||||
|
|
||||||
|
for tracked, mtime in self._mtime.items():
|
||||||
|
norm = tracked.replace(os.sep, "/")
|
||||||
|
if norm == updated_symref:
|
||||||
|
continue
|
||||||
|
full_path = os.path.join(self._gitdir, tracked)
|
||||||
|
try:
|
||||||
|
curr_mtime = os.path.getmtime(full_path)
|
||||||
|
except OSError:
|
||||||
|
return True
|
||||||
|
if norm in expected_child:
|
||||||
|
if curr_mtime != mtime:
|
||||||
|
try:
|
||||||
|
children = platform_utils.listdir(full_path)
|
||||||
|
except OSError:
|
||||||
|
return True
|
||||||
|
next_comp = expected_child[norm]
|
||||||
|
for child in children:
|
||||||
|
if child == next_comp:
|
||||||
|
continue
|
||||||
|
if os.path.join(tracked, child) not in self._mtime:
|
||||||
|
return True
|
||||||
|
elif curr_mtime != mtime:
|
||||||
|
return True
|
||||||
|
|
||||||
|
for anc, next_comp in expected_child.items():
|
||||||
|
anc_os = anc.replace("/", os.sep)
|
||||||
|
if anc_os not in self._mtime:
|
||||||
|
anc_full = os.path.join(self._gitdir, anc_os)
|
||||||
|
try:
|
||||||
|
if platform_utils.listdir(anc_full) != [next_comp]:
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _ResolveSymrefs(self) -> None:
|
||||||
|
"""Resolve all symbolic references into _phyref up to max depth."""
|
||||||
|
for name in self._symref:
|
||||||
|
self._phyref.pop(name, None)
|
||||||
|
|
||||||
|
scan = self._symref
|
||||||
|
attempts = 0
|
||||||
|
while scan and attempts < _SYMREF_MAXDEPTH:
|
||||||
|
scan_next = {}
|
||||||
|
for name, dest in scan.items():
|
||||||
|
if dest in self._phyref:
|
||||||
|
self._phyref[name] = self._phyref[dest]
|
||||||
|
else:
|
||||||
|
scan_next[name] = dest
|
||||||
|
scan = scan_next
|
||||||
|
attempts += 1
|
||||||
|
|
||||||
def _EnsureLoaded(self):
|
def _EnsureLoaded(self):
|
||||||
if self._phyref is None or self._NeedUpdate():
|
if self._phyref is None or self._NeedUpdate():
|
||||||
self._LoadAll()
|
self._LoadAll()
|
||||||
@@ -105,23 +202,17 @@ class GitRefs:
|
|||||||
# --include-root-refs does not report an unborn HEAD.
|
# --include-root-refs does not report an unborn HEAD.
|
||||||
self._ReadSymbolicRef(HEAD)
|
self._ReadSymbolicRef(HEAD)
|
||||||
|
|
||||||
scan = self._symref
|
self._ResolveSymrefs()
|
||||||
attempts = 0
|
self._RefreshMtimes()
|
||||||
while scan and attempts < 5:
|
|
||||||
scan_next = {}
|
|
||||||
for name, dest in scan.items():
|
|
||||||
if dest in self._phyref:
|
|
||||||
self._phyref[name] = self._phyref[dest]
|
|
||||||
else:
|
|
||||||
scan_next[name] = dest
|
|
||||||
scan = scan_next
|
|
||||||
attempts += 1
|
|
||||||
|
|
||||||
self._TrackMtime(HEAD)
|
def _RefreshMtimes(self) -> None:
|
||||||
self._TrackMtime("config")
|
"""Record current modification times for ref storage paths."""
|
||||||
self._TrackMtime("packed-refs")
|
self._mtime = {}
|
||||||
self._TrackTreeMtimes("refs")
|
self._TrackMtime(HEAD)
|
||||||
self._TrackTreeMtimes("reftable")
|
self._TrackMtime("config")
|
||||||
|
self._TrackMtime("packed-refs")
|
||||||
|
self._TrackTreeMtimes("refs")
|
||||||
|
self._TrackTreeMtimes("reftable")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _IsNullRef(ref_id: str) -> bool:
|
def _IsNullRef(ref_id: str) -> bool:
|
||||||
@@ -165,6 +256,22 @@ class GitRefs:
|
|||||||
|
|
||||||
def _ReadSymbolicRef(self, name: str) -> None:
|
def _ReadSymbolicRef(self, name: str) -> None:
|
||||||
"""Read a symbolic reference."""
|
"""Read a symbolic reference."""
|
||||||
|
if name == HEAD:
|
||||||
|
path = os.path.join(self._gitdir, HEAD)
|
||||||
|
try:
|
||||||
|
if not platform_utils.islink(path):
|
||||||
|
with open(
|
||||||
|
path, "r", encoding="utf-8", errors="replace"
|
||||||
|
) as fd:
|
||||||
|
line = fd.readline().strip()
|
||||||
|
if line.startswith("ref:"):
|
||||||
|
ref = line[4:].strip()
|
||||||
|
if ref and ref != R_HEADS + ".invalid":
|
||||||
|
self._symref[name] = ref
|
||||||
|
return
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
p = git_command.GitCommand(
|
p = git_command.GitCommand(
|
||||||
None,
|
None,
|
||||||
["symbolic-ref", "-q", name],
|
["symbolic-ref", "-q", name],
|
||||||
|
|||||||
+3
-3
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
"""Read a worktree's state from one machine-readable git status snapshot."""
|
"""Read a worktree's state from one machine-readable git status snapshot."""
|
||||||
|
|
||||||
from collections import OrderedDict
|
import collections
|
||||||
import os
|
import os
|
||||||
from typing import Iterator, List, Optional, TYPE_CHECKING
|
from typing import Iterator, List, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
@@ -45,8 +45,8 @@ class StatusSnapshot:
|
|||||||
"""A consistent view of worktree, index, and branch state."""
|
"""A consistent view of worktree, index, and branch state."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.index_changes = OrderedDict()
|
self.index_changes = collections.OrderedDict()
|
||||||
self.worktree_changes = OrderedDict()
|
self.worktree_changes = collections.OrderedDict()
|
||||||
self.untracked = []
|
self.untracked = []
|
||||||
self.branch_oid = None
|
self.branch_oid = None
|
||||||
self.branch_head = None
|
self.branch_head = None
|
||||||
|
|||||||
+5
-6
@@ -33,8 +33,7 @@ from typing import NamedTuple
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
import git_command
|
import git_command
|
||||||
from git_config import IsId
|
import git_config
|
||||||
from git_config import RepoConfig
|
|
||||||
import platform_utils
|
import platform_utils
|
||||||
|
|
||||||
|
|
||||||
@@ -324,7 +323,7 @@ class Superproject:
|
|||||||
# creating a local branch of the same name.
|
# creating a local branch of the same name.
|
||||||
refspec = (
|
refspec = (
|
||||||
self.revision
|
self.revision
|
||||||
if IsId(self.revision)
|
if git_config.IsId(self.revision)
|
||||||
else f"{self.revision}:{self.revision}"
|
else f"{self.revision}:{self.revision}"
|
||||||
)
|
)
|
||||||
cmd.append(refspec)
|
cmd.append(refspec)
|
||||||
@@ -429,7 +428,7 @@ class Superproject:
|
|||||||
|
|
||||||
if not self._Init():
|
if not self._Init():
|
||||||
return SyncResult(False, should_exit)
|
return SyncResult(False, should_exit)
|
||||||
if IsId(self.revision) and self.commit_id:
|
if git_config.IsId(self.revision) and self.commit_id:
|
||||||
return SyncResult(True, False)
|
return SyncResult(True, False)
|
||||||
if not self._Fetch():
|
if not self._Fetch():
|
||||||
return SyncResult(False, should_exit)
|
return SyncResult(False, should_exit)
|
||||||
@@ -568,7 +567,7 @@ class Superproject:
|
|||||||
@functools.lru_cache(maxsize=None)
|
@functools.lru_cache(maxsize=None)
|
||||||
def _UseSuperprojectFromConfiguration():
|
def _UseSuperprojectFromConfiguration():
|
||||||
"""Returns the user choice of whether to use superproject."""
|
"""Returns the user choice of whether to use superproject."""
|
||||||
user_cfg = RepoConfig.ForUser()
|
user_cfg = git_config.RepoConfig.ForUser()
|
||||||
time_now = int(time.time())
|
time_now = int(time.time())
|
||||||
|
|
||||||
user_value = user_cfg.GetBoolean("repo.superprojectChoice")
|
user_value = user_cfg.GetBoolean("repo.superprojectChoice")
|
||||||
@@ -602,7 +601,7 @@ def _UseSuperprojectFromConfiguration():
|
|||||||
return user_value
|
return user_value
|
||||||
|
|
||||||
# We don't have an unexpired choice, ask for one.
|
# We don't have an unexpired choice, ask for one.
|
||||||
system_cfg = RepoConfig.ForSystem()
|
system_cfg = git_config.RepoConfig.ForSystem()
|
||||||
system_value = system_cfg.GetBoolean("repo.superprojectChoice")
|
system_value = system_cfg.GetBoolean("repo.superprojectChoice")
|
||||||
if system_value:
|
if system_value:
|
||||||
# The system configuration is proposing that we should enable the
|
# The system configuration is proposing that we should enable the
|
||||||
|
|||||||
@@ -15,10 +15,10 @@
|
|||||||
"""Event logging in the git trace2 EVENT format."""
|
"""Event logging in the git trace2 EVENT format."""
|
||||||
|
|
||||||
import git_command
|
import git_command
|
||||||
from git_trace2_event_log_base import BaseEventLog
|
import git_trace2_event_log_base
|
||||||
|
|
||||||
|
|
||||||
class EventLog(BaseEventLog):
|
class EventLog(git_trace2_event_log_base.BaseEventLog):
|
||||||
"""Event log that records events that occurred during a repo invocation.
|
"""Event log that records events that occurred during a repo invocation.
|
||||||
|
|
||||||
Events are written to the log as a consecutive JSON entries, one per line.
|
Events are written to the log as a consecutive JSON entries, one per line.
|
||||||
|
|||||||
@@ -41,23 +41,21 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
kerberos = None
|
kerberos = None
|
||||||
|
|
||||||
from color import SetDefaultColoring
|
import color
|
||||||
from command import InteractiveCommand
|
from command import InteractiveCommand
|
||||||
from command import MirrorSafeCommand
|
from command import MirrorSafeCommand
|
||||||
from editor import Editor
|
import editor
|
||||||
import error
|
import error
|
||||||
import event_log
|
import event_log
|
||||||
import git_command
|
import git_command
|
||||||
from git_config import RepoConfig
|
import git_config
|
||||||
from git_trace2_event_log import EventLog
|
import git_trace2_event_log
|
||||||
from manifest_xml import RepoClient
|
import manifest_xml
|
||||||
from pager import RunPager
|
import pager
|
||||||
from pager import TerminatePager
|
|
||||||
import repo_trace
|
import repo_trace
|
||||||
from subcmds import all_commands
|
from subcmds import all_commands
|
||||||
from subcmds.version import Version
|
from subcmds.version import Version
|
||||||
from wrapper import Wrapper
|
import wrapper
|
||||||
from wrapper import WrapperPath
|
|
||||||
|
|
||||||
|
|
||||||
logger = repo_logging.RepoLogger(__file__)
|
logger = repo_logging.RepoLogger(__file__)
|
||||||
@@ -195,7 +193,7 @@ class _Repo:
|
|||||||
help_commands = "".join(f"\n {x}" for x in wrapped_commands)
|
help_commands = "".join(f"\n {x}" for x in wrapped_commands)
|
||||||
print(f"Available commands:{help_commands}")
|
print(f"Available commands:{help_commands}")
|
||||||
print("\nRun `repo help <command>` for command-specific details.")
|
print("\nRun `repo help <command>` for command-specific details.")
|
||||||
print("Bug reports:", Wrapper().BUG_URL)
|
print("Bug reports:", wrapper.Wrapper().BUG_URL)
|
||||||
else:
|
else:
|
||||||
cmd = self.commands["help"]()
|
cmd = self.commands["help"]()
|
||||||
if all_commands:
|
if all_commands:
|
||||||
@@ -230,9 +228,9 @@ class _Repo:
|
|||||||
return name, []
|
return name, []
|
||||||
|
|
||||||
key = f"alias.{name}"
|
key = f"alias.{name}"
|
||||||
alias = RepoConfig.ForRepository(self.repodir).GetString(key)
|
alias = git_config.RepoConfig.ForRepository(self.repodir).GetString(key)
|
||||||
if alias is None:
|
if alias is None:
|
||||||
alias = RepoConfig.ForUser().GetString(key)
|
alias = git_config.RepoConfig.ForUser().GetString(key)
|
||||||
if alias is None:
|
if alias is None:
|
||||||
return name, []
|
return name, []
|
||||||
|
|
||||||
@@ -263,18 +261,18 @@ class _Repo:
|
|||||||
self._PrintHelp(short=True)
|
self._PrintHelp(short=True)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
git_trace2_event_log = EventLog()
|
git_event_log = git_trace2_event_log.EventLog()
|
||||||
run = (
|
run = lambda: self._RunLong(name, gopts, argv, git_event_log) or 0
|
||||||
lambda: self._RunLong(name, gopts, argv, git_trace2_event_log) or 0
|
|
||||||
)
|
|
||||||
with repo_trace.Trace(
|
with repo_trace.Trace(
|
||||||
"starting new command: %s [sid=%s]",
|
"starting new command: %s [sid=%s]",
|
||||||
", ".join([name] + argv),
|
", ".join([name] + argv),
|
||||||
git_trace2_event_log.full_sid,
|
git_event_log.full_sid,
|
||||||
first_trace=True,
|
first_trace=True,
|
||||||
):
|
):
|
||||||
if gopts.trace_python:
|
if gopts.trace_python:
|
||||||
import trace
|
# This is a debug module that is not otherwise used, so don't
|
||||||
|
# import it all the time.
|
||||||
|
import trace # noqa: IMR200
|
||||||
|
|
||||||
tracer = trace.Trace(
|
tracer = trace.Trace(
|
||||||
count=False,
|
count=False,
|
||||||
@@ -288,7 +286,7 @@ class _Repo:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def _autocorrect_command_name(
|
def _autocorrect_command_name(
|
||||||
self, name: str, config: RepoConfig
|
self, name: str, config: git_config.RepoConfig
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""Autocorrect command name based on user's git config."""
|
"""Autocorrect command name based on user's git config."""
|
||||||
|
|
||||||
@@ -386,12 +384,12 @@ class _Repo:
|
|||||||
def _RunLong(self, name, gopts, argv, git_trace2_event_log):
|
def _RunLong(self, name, gopts, argv, git_trace2_event_log):
|
||||||
"""Execute the (longer running) requested subcommand."""
|
"""Execute the (longer running) requested subcommand."""
|
||||||
result = 0
|
result = 0
|
||||||
SetDefaultColoring(gopts.color)
|
color.SetDefaultColoring(gopts.color)
|
||||||
|
|
||||||
outer_client = RepoClient(self.repodir)
|
outer_client = manifest_xml.RepoClient(self.repodir)
|
||||||
repo_client = outer_client
|
repo_client = outer_client
|
||||||
if gopts.submanifest_path:
|
if gopts.submanifest_path:
|
||||||
repo_client = RepoClient(
|
repo_client = manifest_xml.RepoClient(
|
||||||
self.repodir,
|
self.repodir,
|
||||||
submanifest_path=gopts.submanifest_path,
|
submanifest_path=gopts.submanifest_path,
|
||||||
outer_client=outer_client,
|
outer_client=outer_client,
|
||||||
@@ -413,8 +411,9 @@ class _Repo:
|
|||||||
outer_manifest=outer_client.manifest,
|
outer_manifest=outer_client.manifest,
|
||||||
git_event_log=git_trace2_event_log,
|
git_event_log=git_trace2_event_log,
|
||||||
)
|
)
|
||||||
|
cmd.event_log.enabled = bool(gopts.event_log)
|
||||||
|
|
||||||
Editor.globalConfig = cmd.client.globalConfig
|
editor.Editor.globalConfig = cmd.client.globalConfig
|
||||||
|
|
||||||
if not isinstance(cmd, MirrorSafeCommand) and cmd.manifest.IsMirror:
|
if not isinstance(cmd, MirrorSafeCommand) and cmd.manifest.IsMirror:
|
||||||
logger.error("fatal: '%s' requires a working directory", name)
|
logger.error("fatal: '%s' requires a working directory", name)
|
||||||
@@ -442,7 +441,7 @@ class _Repo:
|
|||||||
if use_pager is None:
|
if use_pager is None:
|
||||||
use_pager = cmd.WantPager(copts)
|
use_pager = cmd.WantPager(copts)
|
||||||
if use_pager:
|
if use_pager:
|
||||||
RunPager(config)
|
pager.RunPager(config)
|
||||||
|
|
||||||
start = time.time()
|
start = time.time()
|
||||||
cmd_event = cmd.event_log.Add(name, event_log.TASK_COMMAND, start)
|
cmd_event = cmd.event_log.Add(name, event_log.TASK_COMMAND, start)
|
||||||
@@ -588,9 +587,10 @@ class _Repo:
|
|||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd.event_log.FinishEvent(
|
if cmd_event is not None:
|
||||||
cmd_event, finish, result is None or result == 0
|
cmd.event_log.FinishEvent(
|
||||||
)
|
cmd_event, finish, result is None or result == 0
|
||||||
|
)
|
||||||
git_trace2_event_log.DefParamRepoEvents(
|
git_trace2_event_log.DefParamRepoEvents(
|
||||||
cmd.manifest.manifestProject.config.DumpConfigDict()
|
cmd.manifest.manifestProject.config.DumpConfigDict()
|
||||||
)
|
)
|
||||||
@@ -627,7 +627,7 @@ def _CheckWrapperVersion(ver_str, repo_path):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Pull out the version of the repo launcher we know about to compare.
|
# Pull out the version of the repo launcher we know about to compare.
|
||||||
exp = Wrapper().VERSION
|
exp = wrapper.Wrapper().VERSION
|
||||||
ver = tuple(map(int, ver_str.split(".")))
|
ver = tuple(map(int, ver_str.split(".")))
|
||||||
|
|
||||||
exp_str = ".".join(map(str, exp))
|
exp_str = ".".join(map(str, exp))
|
||||||
@@ -645,7 +645,7 @@ repo: error:
|
|||||||
ver_str,
|
ver_str,
|
||||||
min_str,
|
min_str,
|
||||||
exp_str,
|
exp_str,
|
||||||
WrapperPath(),
|
wrapper.WrapperPath(),
|
||||||
repo_path,
|
repo_path,
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -660,7 +660,7 @@ repo: error:
|
|||||||
... You should upgrade soon:
|
... You should upgrade soon:
|
||||||
cp %s %s
|
cp %s %s
|
||||||
""",
|
""",
|
||||||
WrapperPath(),
|
wrapper.WrapperPath(),
|
||||||
repo_path,
|
repo_path,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -671,7 +671,7 @@ repo: error:
|
|||||||
!!! The launcher is not writable. Please talk to your sysadmin or distro
|
!!! The launcher is not writable. Please talk to your sysadmin or distro
|
||||||
!!! to get an update installed.
|
!!! to get an update installed.
|
||||||
""",
|
""",
|
||||||
WrapperPath(),
|
wrapper.WrapperPath(),
|
||||||
repo_path,
|
repo_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -955,7 +955,7 @@ def _Main(argv):
|
|||||||
print("fatal: %s" % e, file=sys.stderr)
|
print("fatal: %s" % e, file=sys.stderr)
|
||||||
result = 128
|
result = 128
|
||||||
|
|
||||||
TerminatePager()
|
pager.TerminatePager()
|
||||||
sys.exit(result)
|
sys.exit(result)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+68
-7
@@ -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" "August 2026" "repo manifest" "Repo Manual"
|
.TH REPO "1" "October 2026" "repo manifest" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo manifest - manual page for repo manifest
|
repo \- repo manifest - manual page for repo manifest
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -149,7 +149,8 @@ include*)>
|
|||||||
<!ELEMENT project (annotation*,
|
<!ELEMENT project (annotation*,
|
||||||
project*,
|
project*,
|
||||||
copyfile*,
|
copyfile*,
|
||||||
linkfile*)>
|
linkfile*,
|
||||||
|
sparse\-path*)>
|
||||||
.TP
|
.TP
|
||||||
<!ATTLIST project name
|
<!ATTLIST project name
|
||||||
CDATA #REQUIRED>
|
CDATA #REQUIRED>
|
||||||
@@ -201,10 +202,14 @@ CDATA #IMPLIED>
|
|||||||
<!ELEMENT linkfile EMPTY>
|
<!ELEMENT linkfile EMPTY>
|
||||||
<!ATTLIST linkfile src CDATA #REQUIRED>
|
<!ATTLIST linkfile src CDATA #REQUIRED>
|
||||||
<!ATTLIST linkfile dest CDATA #REQUIRED>
|
<!ATTLIST linkfile dest CDATA #REQUIRED>
|
||||||
|
.IP
|
||||||
|
<!ELEMENT sparse\-path EMPTY>
|
||||||
|
<!ATTLIST sparse\-path path CDATA #REQUIRED>
|
||||||
.TP
|
.TP
|
||||||
<!ELEMENT extend\-project (annotation*,
|
<!ELEMENT extend\-project (annotation*,
|
||||||
copyfile*,
|
copyfile*,
|
||||||
linkfile*)>
|
linkfile*,
|
||||||
|
sparse\-path*)>
|
||||||
.TP
|
.TP
|
||||||
<!ATTLIST extend\-project name
|
<!ATTLIST extend\-project name
|
||||||
CDATA #REQUIRED>
|
CDATA #REQUIRED>
|
||||||
@@ -514,10 +519,11 @@ of an existing project without completely replacing the existing project
|
|||||||
definition. This makes the local manifest more robust against changes to the
|
definition. This makes the local manifest more robust against changes to the
|
||||||
original manifest.
|
original manifest.
|
||||||
.PP
|
.PP
|
||||||
The `extend\-project` element can also contain `annotation`, `copyfile`, and
|
The `extend\-project` element can also contain `annotation`, `copyfile`,
|
||||||
`linkfile` child elements. These are added to the project's definition. A
|
`linkfile`, and `sparse\-path` child elements. These are added to the project's
|
||||||
`copyfile` or `linkfile` with a `dest` that already exists in the project will
|
definition. A `copyfile` or `linkfile` with a `dest` that already exists in the
|
||||||
overwrite the original.
|
project will overwrite the original. There is no way to remove a `sparse\-path`
|
||||||
|
that the original project definition declared.
|
||||||
.PP
|
.PP
|
||||||
Attribute `path`: If specified, limit the change to projects checked out at the
|
Attribute `path`: If specified, limit the change to projects checked out at the
|
||||||
specified path, rather than all projects with the given name.
|
specified path, rather than all projects with the given name.
|
||||||
@@ -561,6 +567,61 @@ insensitive values "true" (default) or "false". This attribute determines
|
|||||||
whether or not the annotation will be kept when exported with the manifest
|
whether or not the annotation will be kept when exported with the manifest
|
||||||
subcommand.
|
subcommand.
|
||||||
.PP
|
.PP
|
||||||
|
Element sparse\-path
|
||||||
|
.PP
|
||||||
|
Zero or more sparse\-path elements may be specified as children of a project
|
||||||
|
element or an extend\-project element. Each element specifies a path to include
|
||||||
|
in the working directory.
|
||||||
|
.PP
|
||||||
|
Requires Git 2.25.0 or later, which is when `git sparse\-checkout` and cone mode
|
||||||
|
were introduced. Declaring a sparse\-path with an older Git fails the sync for
|
||||||
|
that project.
|
||||||
|
.PP
|
||||||
|
Declaring at least one sparse\-path is what enables Git sparse\-checkout for the
|
||||||
|
project: the listed paths are checked out, and other directories are left out of
|
||||||
|
the working directory. Cone mode also checks out the files sitting directly in
|
||||||
|
the project's top\-level directory and in each leading directory of a
|
||||||
|
sparse\-path, so `src/backend` brings along the top\-level files and the files
|
||||||
|
directly under `src/` too. A project with no sparse\-path elements is checked out
|
||||||
|
in full. The checkout uses [cone
|
||||||
|
mode](https://git\-scm.com/docs/git\-sparse\-checkout#_internalscone_mode_handling)
|
||||||
|
for better performance, which means only directory paths are supported;
|
||||||
|
individual files and globs are not. Cone mode does no pattern matching, so `*`,
|
||||||
|
`?`, `[`, `]`, and `\e` are not glob syntax in a `path`. In fact, Git 2.36.0 and
|
||||||
|
later reject them with `specify directories rather than patterns`, which
|
||||||
|
surfaces as a sync failure for the project. Earlier versions starting with
|
||||||
|
2.25.0 have no such check and are treated as literals.
|
||||||
|
.PP
|
||||||
|
Sparse\-checkout is configured only when the project's working directory is first
|
||||||
|
created. Changes to `sparse\-path` in the manifest (added, changed, or removed)
|
||||||
|
are not applied to a project that is already checked out. To change the paths in
|
||||||
|
an existing checkout, run `git sparse\-checkout` in the project directly, or
|
||||||
|
remove the project's working directory and sync again.
|
||||||
|
.PP
|
||||||
|
Because sparse\-checkout only prunes the working directory, a project with at
|
||||||
|
least one sparse\-path is fetched with `\-\-filter=blob:none` so that blobs which
|
||||||
|
will never be checked out are not downloaded. Without the filter Git still
|
||||||
|
downloads every blob in the project and sparse\-checkout merely declines to write
|
||||||
|
most of them to disk, so the filter is what turns sparse\-checkout into a saving
|
||||||
|
in transfer size and server load. The filter from `repo init \fB\-\-partial\-clone\fR
|
||||||
|
\fB\-\-clone\-filter\fR` takes precedence, and projects listed in `repo init
|
||||||
|
\fB\-\-partial\-clone\-exclude\fR` are fetched in full as usual. Like sparse\-checkout
|
||||||
|
itself, the implied filter only applies when the project is first fetched (or is
|
||||||
|
already a partial clone), so adding a sparse\-path to an existing full checkout
|
||||||
|
does not convert it to a partial clone. Mirrors are never filtered.
|
||||||
|
.PP
|
||||||
|
Because that filter is only useful if the objects it excludes are not fetched by
|
||||||
|
some other route, a project that takes the implied `\-\-filter=blob:none` also
|
||||||
|
skips the `/clone.bundle` download: the bundle is an unfiltered pack of every
|
||||||
|
object, so fetching it would undo the filter. Projects that set the filter
|
||||||
|
explicitly, or that are listed in `repo init \fB\-\-partial\-clone\-exclude\fR`, keep
|
||||||
|
using the clone bundle.
|
||||||
|
.PP
|
||||||
|
Attribute `path`: A path relative to the project root to include in the
|
||||||
|
sparse\-checkout. The path is validated like `<copyfile>` and `<linkfile>` paths:
|
||||||
|
it must stay inside the project, so absolute paths, `.` or `..` components, `~`,
|
||||||
|
and `.git` or `.repo*` components are all rejected. A trailing `/` is allowed.
|
||||||
|
.PP
|
||||||
Element copyfile
|
Element copyfile
|
||||||
.PP
|
.PP
|
||||||
Zero or more copyfile elements may be specified as children of a project
|
Zero or more copyfile elements may be specified as children of a project
|
||||||
|
|||||||
+40
-7
@@ -22,16 +22,16 @@ import urllib.parse
|
|||||||
import xml.dom.minidom
|
import xml.dom.minidom
|
||||||
|
|
||||||
import error
|
import error
|
||||||
from git_config import GitConfig
|
import git_config
|
||||||
import git_refs
|
import git_refs
|
||||||
from git_superproject import Superproject
|
import git_superproject
|
||||||
import platform_utils
|
import platform_utils
|
||||||
from project import Annotation
|
from project import Annotation
|
||||||
from project import ManifestProject
|
from project import ManifestProject
|
||||||
from project import Project
|
from project import Project
|
||||||
from project import RemoteSpec
|
from project import RemoteSpec
|
||||||
from project import RepoProject
|
from project import RepoProject
|
||||||
from wrapper import Wrapper
|
import wrapper
|
||||||
|
|
||||||
|
|
||||||
MANIFEST_FILE_NAME = "manifest.xml"
|
MANIFEST_FILE_NAME = "manifest.xml"
|
||||||
@@ -414,7 +414,7 @@ class XmlManifest:
|
|||||||
default_groups: a string, the default manifest groups to use.
|
default_groups: a string, the default manifest groups to use.
|
||||||
"""
|
"""
|
||||||
# TODO(vapier): Move this out of this class.
|
# TODO(vapier): Move this out of this class.
|
||||||
self.globalConfig = GitConfig.ForUser()
|
self.globalConfig = git_config.GitConfig.ForUser()
|
||||||
|
|
||||||
self.repodir = os.path.abspath(repodir)
|
self.repodir = os.path.abspath(repodir)
|
||||||
self._CheckLocalPath(submanifest_path)
|
self._CheckLocalPath(submanifest_path)
|
||||||
@@ -754,6 +754,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
le.setAttribute("dest", lf.dest)
|
le.setAttribute("dest", lf.dest)
|
||||||
e.appendChild(le)
|
e.appendChild(le)
|
||||||
|
|
||||||
|
for sp in p.sparse_paths:
|
||||||
|
se = doc.createElement("sparse-path")
|
||||||
|
se.setAttribute("path", sp)
|
||||||
|
e.appendChild(se)
|
||||||
|
|
||||||
groups = p.groups - {"all", f"name:{p.name}", f"path:{p.relpath}"}
|
groups = p.groups - {"all", f"name:{p.name}", f"path:{p.relpath}"}
|
||||||
if groups:
|
if groups:
|
||||||
e.setAttribute("groups", ",".join(sorted(groups)))
|
e.setAttribute("groups", ",".join(sorted(groups)))
|
||||||
@@ -815,7 +820,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
e.setAttribute("revision", self._superproject.revision)
|
e.setAttribute("revision", self._superproject.revision)
|
||||||
root.appendChild(e)
|
root.appendChild(e)
|
||||||
|
|
||||||
if self._contactinfo.bugurl != Wrapper().BUG_URL:
|
if self._contactinfo.bugurl != wrapper.Wrapper().BUG_URL:
|
||||||
root.appendChild(doc.createTextNode(""))
|
root.appendChild(doc.createTextNode(""))
|
||||||
e = doc.createElement("contactinfo")
|
e = doc.createElement("contactinfo")
|
||||||
e.setAttribute("bugurl", self._contactinfo.bugurl)
|
e.setAttribute("bugurl", self._contactinfo.bugurl)
|
||||||
@@ -846,6 +851,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
"annotation",
|
"annotation",
|
||||||
"copyfile",
|
"copyfile",
|
||||||
"linkfile",
|
"linkfile",
|
||||||
|
"sparse-path",
|
||||||
}
|
}
|
||||||
|
|
||||||
doc = self.ToXml(**kwargs)
|
doc = self.ToXml(**kwargs)
|
||||||
@@ -1176,7 +1182,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
self._submanifests = {}
|
self._submanifests = {}
|
||||||
self._repo_hooks_project = None
|
self._repo_hooks_project = None
|
||||||
self._superproject = None
|
self._superproject = None
|
||||||
self._contactinfo = ContactInfo(Wrapper().BUG_URL)
|
self._contactinfo = ContactInfo(wrapper.Wrapper().BUG_URL)
|
||||||
self._notice = None
|
self._notice = None
|
||||||
self.branch = None
|
self.branch = None
|
||||||
self._manifest_server = None
|
self._manifest_server = None
|
||||||
@@ -1569,6 +1575,8 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
self._ParseLinkFile(p, n)
|
self._ParseLinkFile(p, n)
|
||||||
elif n.nodeName == "annotation":
|
elif n.nodeName == "annotation":
|
||||||
self._ParseAnnotation(p, n)
|
self._ParseAnnotation(p, n)
|
||||||
|
elif n.nodeName == "sparse-path":
|
||||||
|
self._ParseSparsePath(p, n)
|
||||||
|
|
||||||
if node.nodeName == "repo-hooks":
|
if node.nodeName == "repo-hooks":
|
||||||
# Only one project can be the hooks project
|
# Only one project can be the hooks project
|
||||||
@@ -1608,7 +1616,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
"no revision for superproject %s within %s"
|
"no revision for superproject %s within %s"
|
||||||
% (name, self.manifestFile)
|
% (name, self.manifestFile)
|
||||||
)
|
)
|
||||||
self._superproject = Superproject(
|
self._superproject = git_superproject.Superproject(
|
||||||
self,
|
self,
|
||||||
name=name,
|
name=name,
|
||||||
remote=remote.ToRemoteSpec(name),
|
remote=remote.ToRemoteSpec(name),
|
||||||
@@ -2033,6 +2041,8 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
self._ParseLinkFile(project, n)
|
self._ParseLinkFile(project, n)
|
||||||
elif n.nodeName == "annotation":
|
elif n.nodeName == "annotation":
|
||||||
self._ParseAnnotation(project, n)
|
self._ParseAnnotation(project, n)
|
||||||
|
elif n.nodeName == "sparse-path":
|
||||||
|
self._ParseSparsePath(project, n)
|
||||||
elif n.nodeName == "project":
|
elif n.nodeName == "project":
|
||||||
project.subprojects.append(
|
project.subprojects.append(
|
||||||
self._ParseProject(n, parent=project)
|
self._ParseProject(n, parent=project)
|
||||||
@@ -2307,6 +2317,29 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
)
|
)
|
||||||
element.AddAnnotation(name, value, keep)
|
element.AddAnnotation(name, value, keep)
|
||||||
|
|
||||||
|
def _ParseSparsePath(
|
||||||
|
self, project: Project, node: xml.dom.minidom.Element
|
||||||
|
) -> None:
|
||||||
|
"""Parses a <sparse-path> element and adds it to the project.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project: The Project object to add the sparse path to.
|
||||||
|
node: The XML node representing the <sparse-path> element.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ManifestInvalidPathError: |path| is not usable as a project
|
||||||
|
relative path.
|
||||||
|
"""
|
||||||
|
path = self._reqatt(node, "path")
|
||||||
|
# Sparse paths name directories inside the project checkout, so they
|
||||||
|
# get the same scrutiny as <copyfile> & <linkfile> sources.
|
||||||
|
msg = self._CheckLocalPath(path, dir_ok=True)
|
||||||
|
if msg:
|
||||||
|
raise error.ManifestInvalidPathError(
|
||||||
|
f'<sparse-path> invalid "path": {path}: {msg}'
|
||||||
|
)
|
||||||
|
project.sparse_paths.append(path)
|
||||||
|
|
||||||
def _get_remote(self, node):
|
def _get_remote(self, node):
|
||||||
name = node.getAttribute("remote")
|
name = node.getAttribute("remote")
|
||||||
if not name:
|
if not name:
|
||||||
|
|||||||
+4
-6
@@ -28,6 +28,10 @@ def isWindows():
|
|||||||
return platform.system() == "Windows"
|
return platform.system() == "Windows"
|
||||||
|
|
||||||
|
|
||||||
|
if isWindows():
|
||||||
|
import platform_utils_win32
|
||||||
|
|
||||||
|
|
||||||
def symlink(source, link_name):
|
def symlink(source, link_name):
|
||||||
"""Creates a symbolic link pointing to source named link_name.
|
"""Creates a symbolic link pointing to source named link_name.
|
||||||
|
|
||||||
@@ -35,8 +39,6 @@ def symlink(source, link_name):
|
|||||||
to know whether to create a "File" or a "Directory" symbolic link.
|
to know whether to create a "File" or a "Directory" symbolic link.
|
||||||
"""
|
"""
|
||||||
if isWindows():
|
if isWindows():
|
||||||
import platform_utils_win32
|
|
||||||
|
|
||||||
source = _validate_winpath(source)
|
source = _validate_winpath(source)
|
||||||
link_name = _validate_winpath(link_name)
|
link_name = _validate_winpath(link_name)
|
||||||
target = os.path.join(os.path.dirname(link_name), source)
|
target = os.path.join(os.path.dirname(link_name), source)
|
||||||
@@ -273,8 +275,6 @@ def islink(path):
|
|||||||
Availability: Windows, Unix.
|
Availability: Windows, Unix.
|
||||||
"""
|
"""
|
||||||
if isWindows():
|
if isWindows():
|
||||||
import platform_utils_win32
|
|
||||||
|
|
||||||
return platform_utils_win32.islink(_makelongpath(path))
|
return platform_utils_win32.islink(_makelongpath(path))
|
||||||
else:
|
else:
|
||||||
return os.path.islink(path)
|
return os.path.islink(path)
|
||||||
@@ -289,8 +289,6 @@ def readlink(path):
|
|||||||
Availability: Windows, Unix.
|
Availability: Windows, Unix.
|
||||||
"""
|
"""
|
||||||
if isWindows():
|
if isWindows():
|
||||||
import platform_utils_win32
|
|
||||||
|
|
||||||
return platform_utils_win32.readlink(_makelongpath(path))
|
return platform_utils_win32.readlink(_makelongpath(path))
|
||||||
else:
|
else:
|
||||||
return os.readlink(path)
|
return os.readlink(path)
|
||||||
+501
-186
File diff suppressed because it is too large.
Load diff
+2
-2
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from color import Coloring
|
import color
|
||||||
import error
|
import error
|
||||||
|
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ class _ConfigMock:
|
|||||||
return self.default_values.get(x, None)
|
return self.default_values.get(x, None)
|
||||||
|
|
||||||
|
|
||||||
class _LogColoring(Coloring):
|
class _LogColoring(color.Coloring):
|
||||||
"""Coloring outstream for logging."""
|
"""Coloring outstream for logging."""
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
|
|||||||
@@ -18,9 +18,10 @@
|
|||||||
# dependencies = [
|
# dependencies = [
|
||||||
# 'pytest==8.3.4',
|
# 'pytest==8.3.4',
|
||||||
# 'black==25.1.0',
|
# 'black==25.1.0',
|
||||||
# 'flake8==6.0.0',
|
# 'flake8==7.4.1',
|
||||||
# 'flake8-bugbear==24.12.12',
|
# 'flake8-bugbear==26.9.30',
|
||||||
# 'flake8-comprehensions==3.16.0',
|
# 'flake8-comprehensions==3.17.0',
|
||||||
|
# 'flake8-import-restrictions==2.1',
|
||||||
# 'isort==5.10.1'
|
# 'isort==5.10.1'
|
||||||
# ]
|
# ]
|
||||||
# ///
|
# ///
|
||||||
|
|||||||
Generated
+41
-64
@@ -1,101 +1,78 @@
|
|||||||
attrs==24.2.0 \
|
attrs==25.4.0 ; python_full_version < '3.12' \
|
||||||
--hash=sha256:37c4653e51383dc1249dba15635d59011ca11714a8e61db8cd6dc73011e4b603 \
|
--hash=sha256:3e2be3ccc931d6c0cda7d37122b992d6d4b905968a437da7843cff016e2c41e8 \
|
||||||
--hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2
|
--hash=sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373
|
||||||
# via flake8-bugbear
|
# via flake8-bugbear
|
||||||
black==25.1.0 \
|
black==25.1.0 ; python_full_version < '3.12' \
|
||||||
--hash=sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717 \
|
--hash=sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717 \
|
||||||
--hash=sha256:c9ed033e41998dd45d6f19bf9923e2444cb401185ccb26d1a2101a9049637c1f
|
--hash=sha256:c9ed033e41998dd45d6f19bf9923e2444cb401185ccb26d1a2101a9049637c1f
|
||||||
click==8.0.3 \
|
click==8.4.1 ; python_full_version < '3.12' \
|
||||||
--hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \
|
--hash=sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2
|
||||||
--hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \
|
|
||||||
--hash=sha256:b0a0492d07c8e7e4a88ee153ca6553d357a0746792d6868bfca79dc1a344bb1b
|
|
||||||
# via black
|
# via black
|
||||||
colorama==0.4.6 ; sys_platform == 'win32' \
|
colorama==0.4.6 ; python_full_version < '3.12' and sys_platform == 'win32' \
|
||||||
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 \
|
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 \
|
||||||
--hash=sha256:8a5a9494346d7ae2d8f599e4661738007d2df56dbbf4ccbbf7a6a23679a3a302
|
--hash=sha256:8a5a9494346d7ae2d8f599e4661738007d2df56dbbf4ccbbf7a6a23679a3a302
|
||||||
# via
|
# via
|
||||||
# click
|
# click
|
||||||
# pytest
|
# pytest
|
||||||
flake8==6.0.0 \
|
flake8==6.0.0 ; python_full_version < '3.12' \
|
||||||
--hash=sha256:9b5bf0ebd06aff1601e38dfc4e91f9b99ba3cdf446e74ee25a9cbbe01aef81ee \
|
--hash=sha256:9b5bf0ebd06aff1601e38dfc4e91f9b99ba3cdf446e74ee25a9cbbe01aef81ee \
|
||||||
--hash=sha256:f34b5cb8bf908ff49e4673d3dbc557b2381a14132b637d53e9d3a59816f4b6ba
|
--hash=sha256:f34b5cb8bf908ff49e4673d3dbc557b2381a14132b637d53e9d3a59816f4b6ba
|
||||||
# via
|
# via
|
||||||
# flake8-bugbear
|
# flake8-bugbear
|
||||||
# flake8-comprehensions
|
# flake8-comprehensions
|
||||||
flake8-bugbear==24.12.12 \
|
flake8-bugbear==24.12.12 ; python_full_version < '3.12' \
|
||||||
--hash=sha256:1b6967436f65ca22a42e5373aaa6f2d87966ade9aa38d4baf2a1be550767545e
|
--hash=sha256:1b6967436f65ca22a42e5373aaa6f2d87966ade9aa38d4baf2a1be550767545e
|
||||||
flake8-comprehensions==3.16.0 \
|
flake8-comprehensions==3.16.0 ; python_full_version < '3.12' \
|
||||||
--hash=sha256:7c1eadc9d22e765f39857798febe7766b4d9c519793c6c149e3e13bf99693f70
|
--hash=sha256:7c1eadc9d22e765f39857798febe7766b4d9c519793c6c149e3e13bf99693f70
|
||||||
iniconfig==1.1.1 \
|
flake8-import-restrictions==2.1 ; python_full_version < '3.12' \
|
||||||
--hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \
|
--hash=sha256:77522e76b82a6ccb25820b1e195825a695094ceded23a18c1b5ce026c23e1071
|
||||||
--hash=sha256:9d81514370059186276e9a1799e5a9752e4c060233c254d6954b53fad2ff8a5e
|
iniconfig==2.3.0 ; python_full_version < '3.12' \
|
||||||
|
--hash=sha256:e6ae3625a739878eeb4baf25f17a0b4e9d985421e45b897d1332ed7478f822d9 \
|
||||||
|
--hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12
|
||||||
# via pytest
|
# via pytest
|
||||||
isort==5.10.1 \
|
isort==5.10.1 ; python_full_version < '3.12' \
|
||||||
--hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
|
--hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
|
||||||
--hash=sha256:ec0da0d88470e89ade75bf1e2ea7b4349ec906f95c84aff08e4d41dea6864d25
|
--hash=sha256:ec0da0d88470e89ade75bf1e2ea7b4349ec906f95c84aff08e4d41dea6864d25
|
||||||
mccabe==0.7.0 \
|
mccabe==0.7.0 ; python_full_version < '3.12' \
|
||||||
--hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e \
|
--hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e \
|
||||||
--hash=sha256:b85abd0a321cdd357d0be34adbf49cf01453a857a23e94d44612ec6f159cb261
|
--hash=sha256:b85abd0a321cdd357d0be34adbf49cf01453a857a23e94d44612ec6f159cb261
|
||||||
# via flake8
|
# via flake8
|
||||||
mypy-extensions==0.4.3 \
|
mypy-extensions==1.1.0 ; python_full_version < '3.12' \
|
||||||
--hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \
|
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
|
||||||
--hash=sha256:38cbac35a502c4f13b9d9b1d06258fa51e01da4f5828b171b4d1200d00378e7c
|
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
|
||||||
|
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
|
||||||
|
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
|
||||||
|
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
|
||||||
|
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
|
||||||
|
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
|
||||||
|
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
|
||||||
|
--hash=sha256:4ec363d206d93f233330fa7f08eefe9a382dd1e326112a94244f95ec2f22f10e
|
||||||
# via black
|
# via black
|
||||||
packaging==23.0 \
|
packaging==26.2 ; python_full_version < '3.12' \
|
||||||
--hash=sha256:37cb5b4759d17ae6bad78ee3347f0ee1057452895fcd1bbff48fb12908ce00a1 \
|
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e
|
||||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
|
||||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
|
||||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
|
||||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
|
||||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
|
||||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
|
||||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
|
||||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2
|
|
||||||
# via
|
# via
|
||||||
# black
|
# black
|
||||||
# pytest
|
# pytest
|
||||||
pathspec==0.9.0 \
|
pathspec==1.0.4 ; python_full_version < '3.12' \
|
||||||
--hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \
|
--hash=sha256:eeaf9fc7cbc0a4492b666be276f38956baa67cd3faca745462db83d98f1cef87 \
|
||||||
--hash=sha256:aaf90f4ad38a16bd3ebe0fa64da7f6ee01819c881120db68cc6440941a9adc08
|
--hash=sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723
|
||||||
# via black
|
# via black
|
||||||
platformdirs==2.5.2 \
|
platformdirs==4.9.2 ; python_full_version < '3.12' \
|
||||||
--hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \
|
--hash=sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd \
|
||||||
--hash=sha256:e257a04b5d0528cb9836b766c8a16a85368e249cba7ae42888a403976ccd7d2d
|
--hash=sha256:ef245e78eb7a2ba57599bdb39c4ff323cfae3de356a47207b58bb29e7f479f46
|
||||||
# via black
|
# via black
|
||||||
pluggy==1.5.0 \
|
pluggy==1.6.0 ; python_full_version < '3.12' \
|
||||||
--hash=sha256:40da5bf96b15e359e280fd760a80c11d7a8ea20c3c296c3f3e09f8be67cec10b \
|
--hash=sha256:88349c39e440567519b15b27a781bcd3430de6bae17c44c37e758795be96dc5a \
|
||||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
|
||||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
|
||||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
|
||||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
|
||||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
|
||||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
|
||||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
|
||||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669
|
|
||||||
# via pytest
|
# via pytest
|
||||||
py==1.11.0 \
|
pycodestyle==2.10.0 ; python_full_version < '3.12' \
|
||||||
--hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 \
|
|
||||||
--hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378
|
|
||||||
pycodestyle==2.10.0 \
|
|
||||||
--hash=sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610 \
|
--hash=sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610 \
|
||||||
--hash=sha256:aacf8d28ecea10fad31b2d14943a42d85d007f4d2fee8f212fc05751c8460647
|
--hash=sha256:aacf8d28ecea10fad31b2d14943a42d85d007f4d2fee8f212fc05751c8460647
|
||||||
# via flake8
|
# via flake8
|
||||||
pyflakes==3.0.1 \
|
pyflakes==3.0.1 ; python_full_version < '3.12' \
|
||||||
--hash=sha256:a968b8baa43926124b692c7e76279bc5fa8c6ef9a2281e4a2a475be69f954bdf \
|
--hash=sha256:a968b8baa43926124b692c7e76279bc5fa8c6ef9a2281e4a2a475be69f954bdf \
|
||||||
--hash=sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf
|
--hash=sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf
|
||||||
# via flake8
|
# via flake8
|
||||||
pyparsing==3.0.7 \
|
pytest==8.3.4 ; python_full_version < '3.12' \
|
||||||
--hash=sha256:943c220ca64e93940b74dd070152ed9ef1402bc3b28fd1f899843bee34e83708 \
|
|
||||||
--hash=sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484
|
|
||||||
pytest==8.3.4 \
|
|
||||||
--hash=sha256:00cba7845d2dccfe5403025a688d60ceae228b1ddaa866d2eb86b2edb6923e73 \
|
--hash=sha256:00cba7845d2dccfe5403025a688d60ceae228b1ddaa866d2eb86b2edb6923e73 \
|
||||||
--hash=sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6
|
--hash=sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6
|
||||||
toml==0.10.1 \
|
|
||||||
--hash=sha256:4099063d9b50219bf09bfa693ab8dcd96ce585cee9fdadce5a48a08ee23b0646 \
|
|
||||||
--hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88
|
|
||||||
tomli==2.0.1 \
|
|
||||||
--hash=sha256:864f310e021d68415d6ba45b23e29808f99703b471d884267d6942eecdf8d6c5 \
|
|
||||||
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc
|
|
||||||
typing-extensions==4.3.0 \
|
|
||||||
--hash=sha256:09126dab54422810a541e078c37e62024e7260a022d4f67c075a6707d9553649 \
|
|
||||||
--hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02
|
|
||||||
+2
-2
@@ -20,7 +20,7 @@ from command import Command
|
|||||||
from command import DEFAULT_LOCAL_JOBS
|
from command import DEFAULT_LOCAL_JOBS
|
||||||
import error
|
import error
|
||||||
import git_command
|
import git_command
|
||||||
from progress import Progress
|
import progress
|
||||||
import repo_logging
|
import repo_logging
|
||||||
|
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ It is equivalent to "git branch -D <branchname>".
|
|||||||
functools.partial(self._ExecuteOne, opt.all, nb),
|
functools.partial(self._ExecuteOne, opt.all, nb),
|
||||||
range(len(all_projects)),
|
range(len(all_projects)),
|
||||||
callback=_ProcessResults,
|
callback=_ProcessResults,
|
||||||
output=Progress(
|
output=progress.Progress(
|
||||||
f"Abandon {nb}", len(all_projects), quiet=opt.quiet
|
f"Abandon {nb}", len(all_projects), quiet=opt.quiet
|
||||||
),
|
),
|
||||||
chunksize=1,
|
chunksize=1,
|
||||||
|
|||||||
+3
-3
@@ -15,14 +15,14 @@
|
|||||||
import itertools
|
import itertools
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from color import Coloring
|
import color
|
||||||
from command import Command
|
from command import Command
|
||||||
from command import DEFAULT_LOCAL_JOBS
|
from command import DEFAULT_LOCAL_JOBS
|
||||||
|
|
||||||
|
|
||||||
class BranchColoring(Coloring):
|
class BranchColoring(color.Coloring):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
Coloring.__init__(self, config, "branch")
|
super().__init__(config, "branch")
|
||||||
self.current = self.printer("current", fg="green")
|
self.current = self.printer("current", fg="green")
|
||||||
self.local = self.printer("local")
|
self.local = self.printer("local")
|
||||||
self.notinproject = self.printer("notinproject", fg="red")
|
self.notinproject = self.printer("notinproject", fg="red")
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
import error
|
import error
|
||||||
from progress import Progress
|
import progress
|
||||||
import repo_logging
|
import repo_logging
|
||||||
|
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ The command is equivalent to:
|
|||||||
functools.partial(self._ExecuteOne, nb),
|
functools.partial(self._ExecuteOne, nb),
|
||||||
range(len(all_projects)),
|
range(len(all_projects)),
|
||||||
callback=_ProcessResults,
|
callback=_ProcessResults,
|
||||||
output=Progress(
|
output=progress.Progress(
|
||||||
f"Checkout {nb}", len(all_projects), quiet=opt.quiet
|
f"Checkout {nb}", len(all_projects), quiet=opt.quiet
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,14 +12,14 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
from color import Coloring
|
import color
|
||||||
from command import PagedCommand
|
from command import PagedCommand
|
||||||
from manifest_xml import RepoClient
|
import manifest_xml
|
||||||
|
|
||||||
|
|
||||||
class _Coloring(Coloring):
|
class _Coloring(color.Coloring):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
Coloring.__init__(self, config, "status")
|
super().__init__(config, "status")
|
||||||
|
|
||||||
|
|
||||||
class Diffmanifests(PagedCommand):
|
class Diffmanifests(PagedCommand):
|
||||||
@@ -238,12 +238,12 @@ synced and their revisions won't be found.
|
|||||||
self.printRevision
|
self.printRevision
|
||||||
) = self.printText
|
) = self.printText
|
||||||
|
|
||||||
manifest1 = RepoClient(self.repodir)
|
manifest1 = manifest_xml.RepoClient(self.repodir)
|
||||||
manifest1.Override(args[0], load_local_manifests=False)
|
manifest1.Override(args[0], load_local_manifests=False)
|
||||||
if len(args) == 1:
|
if len(args) == 1:
|
||||||
manifest2 = self.manifest
|
manifest2 = self.manifest
|
||||||
else:
|
else:
|
||||||
manifest2 = RepoClient(self.repodir)
|
manifest2 = manifest_xml.RepoClient(self.repodir)
|
||||||
manifest2.Override(args[1], load_local_manifests=False)
|
manifest2.Override(args[1], load_local_manifests=False)
|
||||||
|
|
||||||
diff = manifest1.projectsDiff(manifest2)
|
diff = manifest1.projectsDiff(manifest2)
|
||||||
|
|||||||
+5
-5
@@ -21,7 +21,7 @@ import signal
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from color import Coloring
|
import color
|
||||||
from command import Command
|
from command import Command
|
||||||
from command import DEFAULT_LOCAL_JOBS
|
from command import DEFAULT_LOCAL_JOBS
|
||||||
from command import MirrorSafeCommand
|
from command import MirrorSafeCommand
|
||||||
@@ -37,9 +37,9 @@ _CAN_COLOR = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ForallColoring(Coloring):
|
class ForallColoring(color.Coloring):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
Coloring.__init__(self, config, "forall")
|
super().__init__(config, "forall")
|
||||||
self.project = self.printer("project", attr="bold")
|
self.project = self.printer("project", attr="bold")
|
||||||
|
|
||||||
|
|
||||||
@@ -234,9 +234,9 @@ without iterating through the remaining projects.
|
|||||||
cn = None
|
cn = None
|
||||||
if cn and cn in _CAN_COLOR:
|
if cn and cn in _CAN_COLOR:
|
||||||
|
|
||||||
class ColorCmd(Coloring):
|
class ColorCmd(color.Coloring):
|
||||||
def __init__(self, config, cmd):
|
def __init__(self, config, cmd):
|
||||||
Coloring.__init__(self, config, cmd)
|
super().__init__(config, cmd)
|
||||||
|
|
||||||
if ColorCmd(self.manifest.manifestProject.config, cn).is_on:
|
if ColorCmd(self.manifest.manifestProject.config, cn).is_on:
|
||||||
cmd.insert(cmd.index(cn) + 1, "--color")
|
cmd.insert(cmd.index(cn) + 1, "--color")
|
||||||
|
|||||||
+3
-3
@@ -18,7 +18,7 @@ from typing import List, Set
|
|||||||
from command import Command
|
from command import Command
|
||||||
import git_command
|
import git_command
|
||||||
import platform_utils
|
import platform_utils
|
||||||
from progress import Progress
|
import progress
|
||||||
from project import Project
|
from project import Project
|
||||||
|
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ are still in the manifests.
|
|||||||
if ask.lower() != "y":
|
if ask.lower() != "y":
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
pm = Progress(
|
pm = progress.Progress(
|
||||||
"Deleting",
|
"Deleting",
|
||||||
len(to_delete),
|
len(to_delete),
|
||||||
delay=False,
|
delay=False,
|
||||||
@@ -180,7 +180,7 @@ are still in the manifests.
|
|||||||
print(f"Would have repacked {len(repack_projects)} projects.")
|
print(f"Would have repacked {len(repack_projects)} projects.")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
pm = Progress(
|
pm = progress.Progress(
|
||||||
"Repacking (this will take a while)",
|
"Repacking (this will take a while)",
|
||||||
len(repack_projects),
|
len(repack_projects),
|
||||||
delay=False,
|
delay=False,
|
||||||
|
|||||||
+3
-3
@@ -16,7 +16,7 @@ import functools
|
|||||||
import sys
|
import sys
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
from color import Coloring
|
import color
|
||||||
from command import DEFAULT_LOCAL_JOBS
|
from command import DEFAULT_LOCAL_JOBS
|
||||||
from command import PagedCommand
|
from command import PagedCommand
|
||||||
import error
|
import error
|
||||||
@@ -27,9 +27,9 @@ import repo_logging
|
|||||||
logger = repo_logging.RepoLogger(__file__)
|
logger = repo_logging.RepoLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class GrepColoring(Coloring):
|
class GrepColoring(color.Coloring):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
Coloring.__init__(self, config, "grep")
|
super().__init__(config, "grep")
|
||||||
self.project = self.printer("project", attr="bold")
|
self.project = self.printer("project", attr="bold")
|
||||||
self.fail = self.printer("fail", fg="red")
|
self.fail = self.printer("fail", fg="red")
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -16,12 +16,12 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from color import Coloring
|
import color
|
||||||
from command import MirrorSafeCommand
|
from command import MirrorSafeCommand
|
||||||
from command import PagedCommand
|
from command import PagedCommand
|
||||||
import error
|
import error
|
||||||
from subcmds import all_commands
|
from subcmds import all_commands
|
||||||
from wrapper import Wrapper
|
import wrapper
|
||||||
|
|
||||||
|
|
||||||
class InvalidHelpCommand(error.RepoExitError):
|
class InvalidHelpCommand(error.RepoExitError):
|
||||||
@@ -65,7 +65,7 @@ Displays detailed usage information about a command.
|
|||||||
"See 'repo help <command>' for more information on a "
|
"See 'repo help <command>' for more information on a "
|
||||||
"specific command."
|
"specific command."
|
||||||
)
|
)
|
||||||
print("Bug reports:", Wrapper().BUG_URL)
|
print("Bug reports:", wrapper.Wrapper().BUG_URL)
|
||||||
|
|
||||||
def _PrintCommonCommands(self):
|
def _PrintCommonCommands(self):
|
||||||
print("usage: repo COMMAND [ARGS]")
|
print("usage: repo COMMAND [ARGS]")
|
||||||
@@ -84,12 +84,12 @@ Displays detailed usage information about a command.
|
|||||||
"command.\nSee 'repo help --all' for a complete list of recognized "
|
"command.\nSee 'repo help --all' for a complete list of recognized "
|
||||||
"commands."
|
"commands."
|
||||||
)
|
)
|
||||||
print("Bug reports:", Wrapper().BUG_URL)
|
print("Bug reports:", wrapper.Wrapper().BUG_URL)
|
||||||
|
|
||||||
def _PrintCommandHelp(self, cmd, header_prefix=""):
|
def _PrintCommandHelp(self, cmd, header_prefix=""):
|
||||||
class _Out(Coloring):
|
class _Out(color.Coloring):
|
||||||
def __init__(self, gc):
|
def __init__(self, gc):
|
||||||
Coloring.__init__(self, gc, "help")
|
super().__init__(gc, "help")
|
||||||
self.heading = self.printer("heading", attr="bold")
|
self.heading = self.printer("heading", attr="bold")
|
||||||
self._first = True
|
self._first = True
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -20,7 +20,7 @@ import optparse
|
|||||||
import sys
|
import sys
|
||||||
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
|
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
|
||||||
|
|
||||||
from color import Coloring
|
import color
|
||||||
from command import DEFAULT_LOCAL_JOBS
|
from command import DEFAULT_LOCAL_JOBS
|
||||||
from command import PagedCommand
|
from command import PagedCommand
|
||||||
import git_refs
|
import git_refs
|
||||||
@@ -46,9 +46,9 @@ class OutputFormat(enum.Enum):
|
|||||||
JSON = enum.auto()
|
JSON = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
class _Coloring(Coloring):
|
class _Coloring(color.Coloring):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
Coloring.__init__(self, config, "status")
|
super().__init__(config, "status")
|
||||||
|
|
||||||
|
|
||||||
class Info(PagedCommand):
|
class Info(PagedCommand):
|
||||||
|
|||||||
+8
-11
@@ -15,14 +15,13 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from color import Coloring
|
import color
|
||||||
from command import InteractiveCommand
|
from command import InteractiveCommand
|
||||||
from command import MirrorSafeCommand
|
from command import MirrorSafeCommand
|
||||||
import error
|
import error
|
||||||
import git_command
|
import git_command
|
||||||
import repo_logging
|
import repo_logging
|
||||||
from wrapper import Wrapper
|
import wrapper
|
||||||
from wrapper import WrapperDir
|
|
||||||
|
|
||||||
|
|
||||||
logger = repo_logging.RepoLogger(__file__)
|
logger = repo_logging.RepoLogger(__file__)
|
||||||
@@ -95,7 +94,7 @@ to update the working directory files.
|
|||||||
"""Disable due to re-use of Wrapper()."""
|
"""Disable due to re-use of Wrapper()."""
|
||||||
|
|
||||||
def _Options(self, p):
|
def _Options(self, p):
|
||||||
Wrapper().InitParser(p)
|
wrapper.Wrapper().InitParser(p)
|
||||||
m = p.add_option_group("Multi-manifest")
|
m = p.add_option_group("Multi-manifest")
|
||||||
m.add_option(
|
m.add_option(
|
||||||
"--outer-manifest",
|
"--outer-manifest",
|
||||||
@@ -244,9 +243,9 @@ to update the working directory files.
|
|||||||
if self._HasColorSet(gc):
|
if self._HasColorSet(gc):
|
||||||
return
|
return
|
||||||
|
|
||||||
class _Test(Coloring):
|
class _Test(color.Coloring):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
Coloring.__init__(self, gc, "test color display")
|
super().__init__(gc, "test color display")
|
||||||
self._on = True
|
self._on = True
|
||||||
|
|
||||||
out = _Test()
|
out = _Test()
|
||||||
@@ -344,9 +343,7 @@ to update the working directory files.
|
|||||||
self.OptionParser.error("too many arguments to init")
|
self.OptionParser.error("too many arguments to init")
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
wrapper = Wrapper()
|
reqs = wrapper.Wrapper().Requirements.from_dir(wrapper.WrapperDir())
|
||||||
|
|
||||||
reqs = wrapper.Requirements.from_dir(WrapperDir())
|
|
||||||
git_command.git_require(reqs.get_hard_ver("git"), fail=True)
|
git_command.git_require(reqs.get_hard_ver("git"), fail=True)
|
||||||
min_git_version_soft = reqs.get_soft_ver("git")
|
min_git_version_soft = reqs.get_soft_ver("git")
|
||||||
if not git_command.git_require(min_git_version_soft):
|
if not git_command.git_require(min_git_version_soft):
|
||||||
@@ -368,13 +365,13 @@ to update the working directory files.
|
|||||||
# Handle new --repo-rev requests.
|
# Handle new --repo-rev requests.
|
||||||
if opt.repo_rev:
|
if opt.repo_rev:
|
||||||
try:
|
try:
|
||||||
remote_ref, rev = wrapper.check_repo_rev(
|
remote_ref, rev = wrapper.Wrapper().check_repo_rev(
|
||||||
rp.worktree,
|
rp.worktree,
|
||||||
opt.repo_rev,
|
opt.repo_rev,
|
||||||
repo_verify=opt.repo_verify,
|
repo_verify=opt.repo_verify,
|
||||||
quiet=opt.quiet,
|
quiet=opt.quiet,
|
||||||
)
|
)
|
||||||
except wrapper.CloneFailure as e:
|
except wrapper.Wrapper().CloneFailure as e:
|
||||||
err_msg = "fatal: double check your --repo-rev setting."
|
err_msg = "fatal: double check your --repo-rev setting."
|
||||||
logger.error(err_msg)
|
logger.error(err_msg)
|
||||||
self.git_event_log.ErrorEvent(err_msg)
|
self.git_event_log.ErrorEvent(err_msg)
|
||||||
|
|||||||
+3
-3
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
import optparse
|
import optparse
|
||||||
|
|
||||||
from color import Coloring
|
import color
|
||||||
from command import PagedCommand
|
from command import PagedCommand
|
||||||
|
|
||||||
|
|
||||||
@@ -73,9 +73,9 @@ are displayed.
|
|||||||
if not all_branches:
|
if not all_branches:
|
||||||
return
|
return
|
||||||
|
|
||||||
class Report(Coloring):
|
class Report(color.Coloring):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
Coloring.__init__(self, config, "status")
|
super().__init__(config, "status")
|
||||||
self.project = self.printer("header", attr="bold")
|
self.project = self.printer("header", attr="bold")
|
||||||
self.text = self.printer("text")
|
self.text = self.printer("text")
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from color import Coloring
|
import color
|
||||||
from command import DEFAULT_LOCAL_JOBS
|
from command import DEFAULT_LOCAL_JOBS
|
||||||
from command import PagedCommand
|
from command import PagedCommand
|
||||||
|
|
||||||
@@ -56,9 +56,9 @@ class Prune(PagedCommand):
|
|||||||
if not all_branches:
|
if not all_branches:
|
||||||
return
|
return
|
||||||
|
|
||||||
class Report(Coloring):
|
class Report(color.Coloring):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
Coloring.__init__(self, config, "status")
|
super().__init__(config, "status")
|
||||||
self.project = self.printer("header", attr="bold")
|
self.project = self.printer("header", attr="bold")
|
||||||
|
|
||||||
out = Report(all_branches[0].project.config)
|
out = Report(all_branches[0].project.config)
|
||||||
|
|||||||
+3
-3
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from color import Coloring
|
import color
|
||||||
from command import Command
|
from command import Command
|
||||||
import error
|
import error
|
||||||
import git_command
|
import git_command
|
||||||
@@ -25,9 +25,9 @@ import repo_logging
|
|||||||
logger = repo_logging.RepoLogger(__file__)
|
logger = repo_logging.RepoLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class RebaseColoring(Coloring):
|
class RebaseColoring(color.Coloring):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
Coloring.__init__(self, config, "rebase")
|
super().__init__(config, "rebase")
|
||||||
self.project = self.printer("project", attr="bold")
|
self.project = self.printer("project", attr="bold")
|
||||||
self.fail = self.printer("fail", fg="red")
|
self.fail = self.printer("fail", fg="red")
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from color import Coloring
|
import color
|
||||||
from command import InteractiveCommand
|
from command import InteractiveCommand
|
||||||
import git_command
|
import git_command
|
||||||
import repo_logging
|
import repo_logging
|
||||||
@@ -23,9 +23,9 @@ import repo_logging
|
|||||||
logger = repo_logging.RepoLogger(__file__)
|
logger = repo_logging.RepoLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class _ProjectList(Coloring):
|
class _ProjectList(color.Coloring):
|
||||||
def __init__(self, gc):
|
def __init__(self, gc):
|
||||||
Coloring.__init__(self, gc, "interactive")
|
super().__init__(gc, "interactive")
|
||||||
self.prompt = self.printer("prompt", fg="blue", attr="bold")
|
self.prompt = self.printer("prompt", fg="blue", attr="bold")
|
||||||
self.header = self.printer("header", attr="bold")
|
self.header = self.printer("header", attr="bold")
|
||||||
self.help = self.printer("help", fg="red", attr="bold")
|
self.help = self.printer("help", fg="red", attr="bold")
|
||||||
|
|||||||
+4
-4
@@ -19,8 +19,8 @@ from command import Command
|
|||||||
from command import DEFAULT_LOCAL_JOBS
|
from command import DEFAULT_LOCAL_JOBS
|
||||||
import error
|
import error
|
||||||
import git_command
|
import git_command
|
||||||
from git_config import IsImmutable
|
import git_config
|
||||||
from progress import Progress
|
import progress
|
||||||
import repo_logging
|
import repo_logging
|
||||||
|
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ revision specified in the manifest.
|
|||||||
branch_merge = ""
|
branch_merge = ""
|
||||||
err = None
|
err = None
|
||||||
project = cls.get_parallel_context()["projects"][project_idx]
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
if IsImmutable(project.revisionExpr):
|
if git_config.IsImmutable(project.revisionExpr):
|
||||||
if project.dest_branch:
|
if project.dest_branch:
|
||||||
branch_merge = project.dest_branch
|
branch_merge = project.dest_branch
|
||||||
else:
|
else:
|
||||||
@@ -137,7 +137,7 @@ revision specified in the manifest.
|
|||||||
),
|
),
|
||||||
range(len(all_projects)),
|
range(len(all_projects)),
|
||||||
callback=_ProcessResults,
|
callback=_ProcessResults,
|
||||||
output=Progress(
|
output=progress.Progress(
|
||||||
f"Starting {nb}", len(all_projects), quiet=opt.quiet
|
f"Starting {nb}", len(all_projects), quiet=opt.quiet
|
||||||
),
|
),
|
||||||
chunksize=1,
|
chunksize=1,
|
||||||
|
|||||||
+3
-3
@@ -17,7 +17,7 @@ import glob
|
|||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from color import Coloring
|
import color
|
||||||
from command import DEFAULT_LOCAL_JOBS
|
from command import DEFAULT_LOCAL_JOBS
|
||||||
from command import PagedCommand
|
from command import PagedCommand
|
||||||
import platform_utils
|
import platform_utils
|
||||||
@@ -180,9 +180,9 @@ the following meanings:
|
|||||||
(head, _tail) = os.path.split(head)
|
(head, _tail) = os.path.split(head)
|
||||||
proj_dirs.add(".repo")
|
proj_dirs.add(".repo")
|
||||||
|
|
||||||
class StatusColoring(Coloring):
|
class StatusColoring(color.Coloring):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
Coloring.__init__(self, config, "status")
|
super().__init__(config, "status")
|
||||||
self.project = self.printer("header", attr="bold")
|
self.project = self.printer("header", attr="bold")
|
||||||
self.untracked = self.printer("untracked", fg="red")
|
self.untracked = self.printer("untracked", fg="red")
|
||||||
|
|
||||||
|
|||||||
+31
-32
@@ -60,16 +60,13 @@ from command import WORKER_BATCH_SIZE
|
|||||||
import error
|
import error
|
||||||
import event_log
|
import event_log
|
||||||
import git_command
|
import git_command
|
||||||
from git_config import GetUrlCookieFile
|
import git_config
|
||||||
from git_config import RepoConfig
|
|
||||||
import git_refs
|
import git_refs
|
||||||
import git_superproject
|
import git_superproject
|
||||||
from hooks import RepoHook
|
from hooks import RepoHook
|
||||||
from manifest_xml import XmlManifest
|
import manifest_xml
|
||||||
import platform_utils
|
import platform_utils
|
||||||
from progress import elapsed_str
|
import progress
|
||||||
from progress import jobs_str
|
|
||||||
from progress import Progress
|
|
||||||
from project import DeleteWorktreeError
|
from project import DeleteWorktreeError
|
||||||
from project import Project
|
from project import Project
|
||||||
from project import RemoteSpec
|
from project import RemoteSpec
|
||||||
@@ -77,7 +74,7 @@ from project import SyncBuffer
|
|||||||
import repo_logging
|
import repo_logging
|
||||||
import repo_trace
|
import repo_trace
|
||||||
import ssh
|
import ssh
|
||||||
from wrapper import Wrapper
|
import wrapper
|
||||||
|
|
||||||
|
|
||||||
_ONE_DAY_S = 24 * 60 * 60
|
_ONE_DAY_S = 24 * 60 * 60
|
||||||
@@ -888,7 +885,7 @@ later is required to fix a server side protocol bug.
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _GetSuperprojectRevision(
|
def _GetSuperprojectRevision(
|
||||||
self, opt: optparse.Values, manifest: XmlManifest
|
self, opt: optparse.Values, manifest: manifest_xml.XmlManifest
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""Returns the --superproject-revision to apply to manifest.
|
"""Returns the --superproject-revision to apply to manifest.
|
||||||
|
|
||||||
@@ -907,7 +904,7 @@ later is required to fix a server side protocol bug.
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _ConfigureSuperproject(
|
def _ConfigureSuperproject(
|
||||||
self, opt: optparse.Values, manifest: XmlManifest
|
self, opt: optparse.Values, manifest: manifest_xml.XmlManifest
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Configure superproject with options.
|
"""Configure superproject with options.
|
||||||
|
|
||||||
@@ -1143,8 +1140,8 @@ later is required to fix a server side protocol bug.
|
|||||||
return "..working.."
|
return "..working.."
|
||||||
|
|
||||||
elapsed = time.time() - earliest_time
|
elapsed = time.time() - earliest_time
|
||||||
jobs = jobs_str(len(items))
|
jobs = progress.jobs_str(len(items))
|
||||||
return f"{jobs} | {elapsed_str(elapsed)} {earliest_proj}"
|
return f"{jobs} | {progress.elapsed_str(elapsed)} {earliest_proj}"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def InitWorker(cls):
|
def InitWorker(cls):
|
||||||
@@ -1161,7 +1158,7 @@ later is required to fix a server side protocol bug.
|
|||||||
|
|
||||||
fetched = set()
|
fetched = set()
|
||||||
remote_fetched = set()
|
remote_fetched = set()
|
||||||
pm = Progress(
|
pm = progress.Progress(
|
||||||
"Fetching",
|
"Fetching",
|
||||||
len(projects),
|
len(projects),
|
||||||
delay=False,
|
delay=False,
|
||||||
@@ -1485,7 +1482,7 @@ later is required to fix a server side protocol bug.
|
|||||||
),
|
),
|
||||||
range(len(projects)),
|
range(len(projects)),
|
||||||
callback=_ProcessResults,
|
callback=_ProcessResults,
|
||||||
output=Progress(
|
output=progress.Progress(
|
||||||
"Checking out", len(all_projects), quiet=opt.quiet
|
"Checking out", len(all_projects), quiet=opt.quiet
|
||||||
),
|
),
|
||||||
# Use chunksize=1 to avoid the chance that some workers are
|
# Use chunksize=1 to avoid the chance that some workers are
|
||||||
@@ -1617,7 +1614,7 @@ later is required to fix a server side protocol bug.
|
|||||||
cls._SetPreciousObjectsState(project, opt)
|
cls._SetPreciousObjectsState(project, opt)
|
||||||
return
|
return
|
||||||
|
|
||||||
pm = Progress(
|
pm = progress.Progress(
|
||||||
"Garbage collecting", len(projects), delay=False, quiet=opt.quiet
|
"Garbage collecting", len(projects), delay=False, quiet=opt.quiet
|
||||||
)
|
)
|
||||||
pm.update(inc=0, msg="prescan")
|
pm.update(inc=0, msg="prescan")
|
||||||
@@ -1748,7 +1745,9 @@ later is required to fix a server side protocol bug.
|
|||||||
return project.name
|
return project.name
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _CheckForBloatedProjects(self, projects, opt):
|
def _CheckForBloatedProjects(
|
||||||
|
self, projects: List[Project], opt: optparse.Values
|
||||||
|
) -> None:
|
||||||
"""Check for shallow projects that are accumulating unoptimized data.
|
"""Check for shallow projects that are accumulating unoptimized data.
|
||||||
|
|
||||||
For projects with clone-depth="1" that are dirty (have local changes),
|
For projects with clone-depth="1" that are dirty (have local changes),
|
||||||
@@ -1766,12 +1765,14 @@ later is required to fix a server side protocol bug.
|
|||||||
if not git_command.git_require((2, 23, 0)):
|
if not git_command.git_require((2, 23, 0)):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Skip projects with no worktree on disk, e.g. ones only ever synced
|
# Skip projects fetched via repo.fetchcmd (which bypasses _RemoteFetch
|
||||||
# with --network-only.
|
# and manages object storage externally) or with no worktree on disk,
|
||||||
|
# e.g. ones only ever synced with --network-only.
|
||||||
projects = [
|
projects = [
|
||||||
p
|
p
|
||||||
for p in projects
|
for p in projects
|
||||||
if p.clone_depth
|
if p.clone_depth
|
||||||
|
and not p.UseFetchCmd
|
||||||
and not p.stateless_prune_needed
|
and not p.stateless_prune_needed
|
||||||
and p.worktree
|
and p.worktree
|
||||||
and p.Exists
|
and p.Exists
|
||||||
@@ -1780,7 +1781,7 @@ later is required to fix a server side protocol bug.
|
|||||||
if not projects:
|
if not projects:
|
||||||
return
|
return
|
||||||
|
|
||||||
pm = Progress(
|
pm = progress.Progress(
|
||||||
"Checking for bloat", len(projects), delay=False, quiet=opt.quiet
|
"Checking for bloat", len(projects), delay=False, quiet=opt.quiet
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2153,7 +2154,7 @@ later is required to fix a server side protocol bug.
|
|||||||
self,
|
self,
|
||||||
opt: optparse.Values,
|
opt: optparse.Values,
|
||||||
smart_sync_manifest_path: str,
|
smart_sync_manifest_path: str,
|
||||||
manifest: XmlManifest,
|
manifest: manifest_xml.XmlManifest,
|
||||||
best_effort: bool = False,
|
best_effort: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
if not manifest.manifest_server:
|
if not manifest.manifest_server:
|
||||||
@@ -2471,7 +2472,7 @@ later is required to fix a server side protocol bug.
|
|||||||
def _SyncToSuperprojectRev(
|
def _SyncToSuperprojectRev(
|
||||||
self,
|
self,
|
||||||
opt: optparse.Values,
|
opt: optparse.Values,
|
||||||
manifest: XmlManifest,
|
manifest: manifest_xml.XmlManifest,
|
||||||
mp: Project,
|
mp: Project,
|
||||||
manifest_name: Optional[str],
|
manifest_name: Optional[str],
|
||||||
errors: List[Exception],
|
errors: List[Exception],
|
||||||
@@ -2517,7 +2518,7 @@ later is required to fix a server side protocol bug.
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _ResolveSmartSyncOption(
|
def _ResolveSmartSyncOption(
|
||||||
self, opt: optparse.Values, manifest: XmlManifest
|
self, opt: optparse.Values, manifest: manifest_xml.XmlManifest
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Resolve opt.smart_sync from CLI flags, config, and manifest.
|
"""Resolve opt.smart_sync from CLI flags, config, and manifest.
|
||||||
|
|
||||||
@@ -2533,7 +2534,7 @@ later is required to fix a server side protocol bug.
|
|||||||
checkout_pref = getattr(manifest.manifestProject, "smart_sync", None)
|
checkout_pref = getattr(manifest.manifestProject, "smart_sync", None)
|
||||||
if not isinstance(checkout_pref, bool):
|
if not isinstance(checkout_pref, bool):
|
||||||
checkout_pref = None
|
checkout_pref = None
|
||||||
user_pref = RepoConfig.ForUser().GetBoolean("sync.smartsync")
|
user_pref = git_config.RepoConfig.ForUser().GetBoolean("sync.smartsync")
|
||||||
default_allowed = (
|
default_allowed = (
|
||||||
opt.smart_sync is None
|
opt.smart_sync is None
|
||||||
and not opt.smart_tag
|
and not opt.smart_tag
|
||||||
@@ -2770,7 +2771,7 @@ later is required to fix a server side protocol bug.
|
|||||||
self,
|
self,
|
||||||
opt: optparse.Values,
|
opt: optparse.Values,
|
||||||
args: List[str],
|
args: List[str],
|
||||||
manifest: XmlManifest,
|
manifest: manifest_xml.XmlManifest,
|
||||||
all_projects: List[Project],
|
all_projects: List[Project],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Fail when repo.reprojectcmd is used on a manifest nesting projects.
|
"""Fail when repo.reprojectcmd is used on a manifest nesting projects.
|
||||||
@@ -2807,7 +2808,7 @@ later is required to fix a server side protocol bug.
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
def _CreateSyncProgressThread(
|
def _CreateSyncProgressThread(
|
||||||
self, pm: Progress, stop_event: _threading.Event
|
self, pm: progress.Progress, stop_event: _threading.Event
|
||||||
) -> _threading.Thread:
|
) -> _threading.Thread:
|
||||||
"""Creates and returns a daemon thread to update a Progress object.
|
"""Creates and returns a daemon thread to update a Progress object.
|
||||||
|
|
||||||
@@ -3191,7 +3192,7 @@ later is required to fix a server side protocol bug.
|
|||||||
errors: List[Exception],
|
errors: List[Exception],
|
||||||
opt: optparse.Values,
|
opt: optparse.Values,
|
||||||
pool: Optional[multiprocessing.Pool],
|
pool: Optional[multiprocessing.Pool],
|
||||||
pm: Progress,
|
pm: progress.Progress,
|
||||||
results_sets: List[_InterleavedSyncResult],
|
results_sets: List[_InterleavedSyncResult],
|
||||||
):
|
):
|
||||||
"""Callback to process results from interleaved sync workers."""
|
"""Callback to process results from interleaved sync workers."""
|
||||||
@@ -3290,7 +3291,7 @@ later is required to fix a server side protocol bug.
|
|||||||
err_event = multiprocessing.Event()
|
err_event = multiprocessing.Event()
|
||||||
finished_relpaths = set()
|
finished_relpaths = set()
|
||||||
project_list = list(all_projects)
|
project_list = list(all_projects)
|
||||||
pm = Progress(
|
pm = progress.Progress(
|
||||||
"Syncing",
|
"Syncing",
|
||||||
len(project_list),
|
len(project_list),
|
||||||
delay=False,
|
delay=False,
|
||||||
@@ -3460,9 +3461,8 @@ def _PostRepoUpgrade(manifest, quiet=False):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
wrapper = Wrapper()
|
if wrapper.Wrapper().NeedSetupGnuPG():
|
||||||
if wrapper.NeedSetupGnuPG():
|
wrapper.Wrapper().SetupGnuPG(quiet)
|
||||||
wrapper.SetupGnuPG(quiet)
|
|
||||||
for project in manifest.projects:
|
for project in manifest.projects:
|
||||||
if project.Exists:
|
if project.Exists:
|
||||||
project.PostRepoUpgrade()
|
project.PostRepoUpgrade()
|
||||||
@@ -3471,12 +3471,11 @@ def _PostRepoUpgrade(manifest, quiet=False):
|
|||||||
def _PostRepoFetch(rp, repo_verify=True, verbose=False):
|
def _PostRepoFetch(rp, repo_verify=True, verbose=False):
|
||||||
if rp.HasChanges:
|
if rp.HasChanges:
|
||||||
logger.warning("info: A new version of repo is available")
|
logger.warning("info: A new version of repo is available")
|
||||||
wrapper = Wrapper()
|
|
||||||
try:
|
try:
|
||||||
rev = rp.bare_git.describe(rp.GetRevisionId())
|
rev = rp.bare_git.describe(rp.GetRevisionId())
|
||||||
except error.GitError:
|
except error.GitError:
|
||||||
rev = None
|
rev = None
|
||||||
_, new_rev = wrapper.check_repo_rev(
|
_, new_rev = wrapper.Wrapper().check_repo_rev(
|
||||||
rp.gitdir, rev, repo_verify=repo_verify
|
rp.gitdir, rev, repo_verify=repo_verify
|
||||||
)
|
)
|
||||||
# See if we're held back due to missing signed tag.
|
# See if we're held back due to missing signed tag.
|
||||||
@@ -3652,7 +3651,7 @@ class PersistentTransport(xmlrpc.client.Transport):
|
|||||||
self.proxy = proxy
|
self.proxy = proxy
|
||||||
|
|
||||||
def request(self, host, handler, request_body, verbose=False):
|
def request(self, host, handler, request_body, verbose=False):
|
||||||
with GetUrlCookieFile(self.orig_host, not verbose) as (
|
with git_config.GetUrlCookieFile(self.orig_host, not verbose) as (
|
||||||
cookiefile,
|
cookiefile,
|
||||||
cookie_proxy,
|
cookie_proxy,
|
||||||
):
|
):
|
||||||
|
|||||||
+2
-2
@@ -21,7 +21,7 @@ from typing import List
|
|||||||
|
|
||||||
from command import DEFAULT_LOCAL_JOBS
|
from command import DEFAULT_LOCAL_JOBS
|
||||||
from command import InteractiveCommand
|
from command import InteractiveCommand
|
||||||
from editor import Editor
|
import editor
|
||||||
import error
|
import error
|
||||||
import git_refs
|
import git_refs
|
||||||
import git_superproject
|
import git_superproject
|
||||||
@@ -494,7 +494,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
branches[project_path] = b
|
branches[project_path] = b
|
||||||
script.append("")
|
script.append("")
|
||||||
|
|
||||||
script = Editor.EditString("\n".join(script)).split("\n")
|
script = editor.Editor.EditString("\n".join(script)).split("\n")
|
||||||
|
|
||||||
project_re = re.compile(r"^#?\s*project\s*([^\s]+)/:$")
|
project_re = re.compile(r"^#?\s*project\s*([^\s]+)/:$")
|
||||||
branch_re = re.compile(r"^\s*branch\s*([^\s(]+)\s*\(.*")
|
branch_re = re.compile(r"^\s*branch\s*([^\s(]+)\s*\(.*")
|
||||||
|
|||||||
+2
-2
@@ -20,7 +20,7 @@ from command import Command
|
|||||||
from command import MirrorSafeCommand
|
from command import MirrorSafeCommand
|
||||||
import git_command
|
import git_command
|
||||||
import git_refs
|
import git_refs
|
||||||
from wrapper import Wrapper
|
import wrapper
|
||||||
|
|
||||||
|
|
||||||
class Version(Command, MirrorSafeCommand):
|
class Version(Command, MirrorSafeCommand):
|
||||||
@@ -81,4 +81,4 @@ class Version(Command, MirrorSafeCommand):
|
|||||||
print(f"OS {uname.system} {uname.release} ({uname.version})")
|
print(f"OS {uname.system} {uname.release} ({uname.version})")
|
||||||
processor = uname.processor if uname.processor else "unknown"
|
processor = uname.processor if uname.processor else "unknown"
|
||||||
print(f"CPU {uname.machine} ({processor})")
|
print(f"CPU {uname.machine} ({processor})")
|
||||||
print("Bug reports:", Wrapper().BUG_URL)
|
print("Bug reports:", wrapper.Wrapper().BUG_URL)
|
||||||
@@ -16,30 +16,30 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from editor import Editor
|
import editor
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def reset_editor() -> None:
|
def reset_editor() -> None:
|
||||||
"""Take care of resetting Editor state across tests."""
|
"""Take care of resetting Editor state across tests."""
|
||||||
Editor._editor = None
|
editor.Editor._editor = None
|
||||||
yield
|
yield
|
||||||
Editor._editor = None
|
editor.Editor._editor = None
|
||||||
|
|
||||||
|
|
||||||
def test_basic() -> None:
|
def test_basic() -> None:
|
||||||
"""Basic checking of _GetEditor."""
|
"""Basic checking of _GetEditor."""
|
||||||
Editor._editor = ":"
|
editor.Editor._editor = ":"
|
||||||
assert Editor._GetEditor() == ":"
|
assert editor.Editor._GetEditor() == ":"
|
||||||
|
|
||||||
|
|
||||||
def test_no_editor() -> None:
|
def test_no_editor() -> None:
|
||||||
"""Check behavior when no editor is available."""
|
"""Check behavior when no editor is available."""
|
||||||
Editor._editor = ":"
|
editor.Editor._editor = ":"
|
||||||
assert Editor.EditString("foo") == "foo"
|
assert editor.Editor.EditString("foo") == "foo"
|
||||||
|
|
||||||
|
|
||||||
def test_cat_editor() -> None:
|
def test_cat_editor() -> None:
|
||||||
"""Check behavior when editor is `cat`."""
|
"""Check behavior when editor is `cat`."""
|
||||||
Editor._editor = "cat"
|
editor.Editor._editor = "cat"
|
||||||
assert Editor.EditString("foo") == "foo"
|
assert editor.Editor.EditString("foo") == "foo"
|
||||||
@@ -14,12 +14,15 @@
|
|||||||
|
|
||||||
"""Unittests for the git_config.py module."""
|
"""Unittests for the git_config.py module."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import utils_for_test
|
import utils_for_test
|
||||||
|
|
||||||
|
import error
|
||||||
import git_config
|
import git_config
|
||||||
|
|
||||||
|
|
||||||
@@ -346,3 +349,214 @@ def test_get_boolean_git_spellings(
|
|||||||
config_file.write_text(f"[section]\n\tkey = {value}\n")
|
config_file.write_text(f"[section]\n\tkey = {value}\n")
|
||||||
config = git_config.GitConfig(config_file)
|
config = git_config.GitConfig(config_file)
|
||||||
assert config.GetBoolean("section.key") is expected
|
assert config.GetBoolean("section.key") is expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_string_updates_json_and_section_caches(
|
||||||
|
rw_config_file: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
"""Test SetString keeps .repo_config.json and _sections coherent."""
|
||||||
|
config = git_config.GitConfig(str(rw_config_file))
|
||||||
|
assert not config.HasSection("remote", "origin")
|
||||||
|
|
||||||
|
config.SetString("remote.origin.url", "https://example.com/repo")
|
||||||
|
assert config.HasSection("remote", "origin")
|
||||||
|
|
||||||
|
json_file = rw_config_file.parent / ".repo_config.json"
|
||||||
|
raw = json.loads(json_file.read_text(encoding="utf-8"))
|
||||||
|
assert git_config._JSON_META_KEY in raw
|
||||||
|
raw.pop(git_config._JSON_META_KEY)
|
||||||
|
assert raw == {"remote.origin.url": ["https://example.com/repo"]}
|
||||||
|
|
||||||
|
reader = git_config.GitConfig(str(rw_config_file))
|
||||||
|
|
||||||
|
def fail_read_git() -> None:
|
||||||
|
raise AssertionError("Expected _ReadJson cache hit without git config")
|
||||||
|
|
||||||
|
monkeypatch.setattr(reader, "_ReadGit", fail_read_git)
|
||||||
|
assert reader.GetString("remote.origin.url") == "https://example.com/repo"
|
||||||
|
assert git_config._JSON_META_KEY not in reader.DumpConfigDict()
|
||||||
|
|
||||||
|
|
||||||
|
def test_reload_refreshes_external_edits(rw_config_file: Path) -> None:
|
||||||
|
"""Test Reload() discards stale JSON cache and reloads external edits."""
|
||||||
|
config = git_config.GitConfig(str(rw_config_file))
|
||||||
|
config.SetString("abfs.initialized", "false")
|
||||||
|
assert config.GetBoolean("abfs.initialized") is False
|
||||||
|
|
||||||
|
rw_config_file.write_text(
|
||||||
|
"[abfs]\n\tinitialized = true\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
config.Reload()
|
||||||
|
|
||||||
|
assert config.GetBoolean("abfs.initialized") is True
|
||||||
|
json_file = rw_config_file.parent / ".repo_config.json"
|
||||||
|
raw = json.loads(json_file.read_text(encoding="utf-8"))
|
||||||
|
raw.pop(git_config._JSON_META_KEY, None)
|
||||||
|
assert raw == {"abfs.initialized": ["true"]}
|
||||||
|
|
||||||
|
|
||||||
|
def test_same_tick_external_edit_invalidates_json_cache(
|
||||||
|
rw_config_file: Path,
|
||||||
|
) -> None:
|
||||||
|
"""External edit with identical mtime_ns and size invalidates JSON."""
|
||||||
|
config = git_config.GitConfig(str(rw_config_file))
|
||||||
|
config.SetString("foo.bar", "val1")
|
||||||
|
|
||||||
|
json_file = rw_config_file.parent / ".repo_config.json"
|
||||||
|
json_stat = os.stat(json_file)
|
||||||
|
|
||||||
|
# Rewrite with identical byte length and force identical mtime_ns.
|
||||||
|
updated = rw_config_file.read_text(encoding="utf-8").replace("val1", "val2")
|
||||||
|
rw_config_file.write_text(updated, encoding="utf-8")
|
||||||
|
os.utime(rw_config_file, ns=(json_stat.st_atime_ns, json_stat.st_mtime_ns))
|
||||||
|
|
||||||
|
reader = git_config.GitConfig(str(rw_config_file))
|
||||||
|
assert reader.GetString("foo.bar") == "val2"
|
||||||
|
|
||||||
|
|
||||||
|
def test_clear_cache_and_set_string_refresh_remotes_and_branches(
|
||||||
|
rw_config_file: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
"""ClearCache, Reload, SetString, and Branch.Save keep caches coherent."""
|
||||||
|
config = git_config.GitConfig(str(rw_config_file))
|
||||||
|
remote = config.GetRemote("origin")
|
||||||
|
remote.url = "https://old.example.com/repo"
|
||||||
|
remote.ResetFetch()
|
||||||
|
remote.Save()
|
||||||
|
|
||||||
|
branch = config.GetBranch("main")
|
||||||
|
branch.remote = remote
|
||||||
|
branch.merge = "refs/heads/main"
|
||||||
|
branch.Save()
|
||||||
|
|
||||||
|
assert config.HasSection("branch", "main")
|
||||||
|
assert config.GetString("branch.main.remote") == "origin"
|
||||||
|
assert config.GetString("branch.main.merge") == "refs/heads/main"
|
||||||
|
|
||||||
|
# Subsequent SetString must not clobber branch.* keys in .repo_config.json.
|
||||||
|
config.SetString("custom.flag", "1")
|
||||||
|
reader = git_config.GitConfig(str(rw_config_file))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
reader,
|
||||||
|
"_ReadGit",
|
||||||
|
lambda: (_ for _ in ()).throw(AssertionError("expected JSON hit")),
|
||||||
|
)
|
||||||
|
assert reader.GetString("branch.main.remote") == "origin"
|
||||||
|
assert reader.GetString("branch.main.merge") == "refs/heads/main"
|
||||||
|
|
||||||
|
# Direct SetString on remote.* invalidates cached Remote and Branch objects.
|
||||||
|
config.SetString(
|
||||||
|
"remote.origin.fetch", "+refs/heads/dev:refs/remotes/origin/dev"
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
config.GetRemote("origin").ToLocal("dev") == "refs/remotes/origin/dev"
|
||||||
|
)
|
||||||
|
assert config.GetBranch("main").remote is config.GetRemote("origin")
|
||||||
|
|
||||||
|
# External edit + Reload() refreshes both GetRemote() and GetBranch().
|
||||||
|
rw_config_file.write_text(
|
||||||
|
'[remote "origin"]\n'
|
||||||
|
"\turl = https://new.example.com/repo\n"
|
||||||
|
"\tfetch = +refs/heads/*:refs/remotes/origin/*\n"
|
||||||
|
'[branch "main"]\n'
|
||||||
|
"\tremote = origin\n"
|
||||||
|
"\tmerge = refs/heads/next\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
config.Reload()
|
||||||
|
assert config.GetRemote("origin").url == "https://new.example.com/repo"
|
||||||
|
assert config.GetBranch("main").merge == "refs/heads/next"
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_string_with_include_invalidates_instead_of_corrupting(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""SetString with [include] avoids freezing or dropping included values."""
|
||||||
|
inc_file = tmp_path / "extra.gitconfig"
|
||||||
|
inc_file.write_text("[inc]\n\tkey = v1\n", encoding="utf-8")
|
||||||
|
|
||||||
|
cfg_file = tmp_path / "config"
|
||||||
|
config = git_config.GitConfig(str(cfg_file))
|
||||||
|
config.SetString("include.path", str(inc_file))
|
||||||
|
assert config.GetString("inc.key") == "v1"
|
||||||
|
|
||||||
|
config.SetString("local.only", "yes")
|
||||||
|
json_file = tmp_path / ".repo_config.json"
|
||||||
|
assert not json_file.exists()
|
||||||
|
|
||||||
|
inc_file.write_text("[inc]\n\tkey = v2\n", encoding="utf-8")
|
||||||
|
assert config.GetString("inc.key") == "v2"
|
||||||
|
assert config.GetString("local.only") == "yes"
|
||||||
|
|
||||||
|
# Unsetting an included-only key does not fail with exit code 5 or hide it.
|
||||||
|
config.SetString("inc.key", None)
|
||||||
|
assert config.GetString("inc.key") == "v2"
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_string_failure_preserves_cache(
|
||||||
|
rw_config_file: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
"""Failed _do() must not leave mutated values in _cache."""
|
||||||
|
config = git_config.GitConfig(str(rw_config_file))
|
||||||
|
config.SetString("foo.bar", "good")
|
||||||
|
|
||||||
|
def fail_do(*_args: Any, **_kwargs: Any) -> str:
|
||||||
|
raise error.GitError("lock contention")
|
||||||
|
|
||||||
|
monkeypatch.setattr(config, "_do", fail_do)
|
||||||
|
with pytest.raises(error.GitError):
|
||||||
|
config.SetString("foo.bar", "bad")
|
||||||
|
assert config.GetString("foo.bar") == "good"
|
||||||
|
|
||||||
|
with pytest.raises(error.GitError):
|
||||||
|
config.SetString("foo.bar", None)
|
||||||
|
assert config.GetString("foo.bar") == "good"
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_string_after_external_edit_preserves_external_keys(
|
||||||
|
rw_config_file: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
"""Warm SetString reloads external edits instead of overwriting JSON."""
|
||||||
|
config = git_config.GitConfig(str(rw_config_file))
|
||||||
|
config.SetString("init.key", "v0")
|
||||||
|
assert config._cache_dict is not None
|
||||||
|
|
||||||
|
# External edit adds other.key while config._cache_dict is warm in memory.
|
||||||
|
rw_config_file.write_text(
|
||||||
|
"[init]\n\tkey = v0\n[other]\n\tkey = ext1\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
config.SetString("user.name", "Alice")
|
||||||
|
|
||||||
|
assert config.GetString("other.key") == "ext1"
|
||||||
|
assert config.GetString("user.name") == "Alice"
|
||||||
|
|
||||||
|
reader = git_config.GitConfig(str(rw_config_file))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
reader,
|
||||||
|
"_ReadGit",
|
||||||
|
lambda: (_ for _ in ()).throw(AssertionError("expected JSON hit")),
|
||||||
|
)
|
||||||
|
assert reader.GetString("other.key") == "ext1"
|
||||||
|
assert reader.GetString("user.name") == "Alice"
|
||||||
|
|
||||||
|
# Same-mtime_ns and same-size external edit while warm is also detected.
|
||||||
|
prev_stat = os.stat(rw_config_file)
|
||||||
|
same_len = rw_config_file.read_text(encoding="utf-8").replace(
|
||||||
|
"ext1", "ext2"
|
||||||
|
)
|
||||||
|
rw_config_file.write_text(same_len, encoding="utf-8")
|
||||||
|
os.utime(rw_config_file, ns=(prev_stat.st_atime_ns, prev_stat.st_mtime_ns))
|
||||||
|
|
||||||
|
config.SetString("user.email", "alice@example.com")
|
||||||
|
assert config.GetString("other.key") == "ext2"
|
||||||
|
assert config.GetString("user.email") == "alice@example.com"
|
||||||
|
|
||||||
|
reader2 = git_config.GitConfig(str(rw_config_file))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
reader2,
|
||||||
|
"_ReadGit",
|
||||||
|
lambda: (_ for _ in ()).throw(AssertionError("expected JSON hit")),
|
||||||
|
)
|
||||||
|
assert reader2.GetString("other.key") == "ext2"
|
||||||
|
assert reader2.GetString("user.name") == "Alice"
|
||||||
|
assert reader2.GetString("user.email") == "alice@example.com"
|
||||||
@@ -244,3 +244,165 @@ def test_updates_when_storage_backend_toggles(tmp_path):
|
|||||||
_run(repo, "refs", "migrate", "--ref-format=files")
|
_run(repo, "refs", "migrate", "--ref-format=files")
|
||||||
_run(repo, "branch", "files-branch")
|
_run(repo, "branch", "files-branch")
|
||||||
assert refs.get("refs/heads/files-branch") == head
|
assert refs.get("refs/heads/files-branch") == head
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("reftable", [False, True])
|
||||||
|
def test_unborn_head_resolution(
|
||||||
|
tmp_path: Path, reftable: bool, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
"""Unborn HEAD reads files backend directly and uses git on reftable."""
|
||||||
|
if reftable and not utils_for_test.supports_reftable():
|
||||||
|
pytest.skip("reftable not supported")
|
||||||
|
|
||||||
|
repo = os.path.join(tmp_path, "unborn")
|
||||||
|
ref_format = "reftable" if reftable else "files"
|
||||||
|
utils_for_test.init_git_tree(repo, ref_format=ref_format)
|
||||||
|
_run(repo, "symbolic-ref", "HEAD", "refs/heads/unborn-main")
|
||||||
|
|
||||||
|
refs = git_refs.GitRefs(os.path.join(repo, ".git"))
|
||||||
|
seen_symbolic_ref = []
|
||||||
|
|
||||||
|
class SpyGitCommand(git_command.GitCommand):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
proj: Any,
|
||||||
|
cmdv: List[str],
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
if cmdv and cmdv[0] == "symbolic-ref":
|
||||||
|
seen_symbolic_ref.append(cmdv)
|
||||||
|
super().__init__(proj, cmdv, *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(git_command, "GitCommand", SpyGitCommand)
|
||||||
|
assert refs.symref("HEAD") == "refs/heads/unborn-main"
|
||||||
|
if not reftable:
|
||||||
|
assert seen_symbolic_ref == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("reftable", [False, True])
|
||||||
|
def test_update_symref_keeps_snapshot_warm(
|
||||||
|
tmp_path: Path, reftable: bool
|
||||||
|
) -> None:
|
||||||
|
"""UpdateSymref stays warm on files backend and reloads on reftable."""
|
||||||
|
if reftable and not utils_for_test.supports_reftable():
|
||||||
|
pytest.skip("reftable not supported")
|
||||||
|
|
||||||
|
repo = _init_repo(tmp_path, reftable=reftable)
|
||||||
|
branch = _run(repo, "symbolic-ref", "--short", "HEAD")
|
||||||
|
head = _run(repo, "rev-parse", "HEAD")
|
||||||
|
refs = git_refs.GitRefs(os.path.join(repo, ".git"))
|
||||||
|
assert refs.get(f"refs/heads/{branch}") == head
|
||||||
|
|
||||||
|
_run(repo, "symbolic-ref", "refs/remotes/m/main", f"refs/heads/{branch}")
|
||||||
|
refs.UpdateSymref("refs/remotes/m/main", f"refs/heads/{branch}")
|
||||||
|
|
||||||
|
if reftable:
|
||||||
|
assert not refs.is_loaded
|
||||||
|
assert refs.symref("refs/remotes/m/main") == f"refs/heads/{branch}"
|
||||||
|
assert refs.get("refs/remotes/m/main") == head
|
||||||
|
else:
|
||||||
|
with mock.patch.object(
|
||||||
|
refs,
|
||||||
|
"_LoadAll",
|
||||||
|
side_effect=AssertionError("expected warm GitRefs snapshot"),
|
||||||
|
):
|
||||||
|
assert refs.symref("refs/remotes/m/main") == f"refs/heads/{branch}"
|
||||||
|
assert refs.get("refs/remotes/m/main") == head
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_symref_invalidates_when_external_ref_changed(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""External ref writes before UpdateSymref invalidate instead of masking."""
|
||||||
|
repo = _init_repo(tmp_path, reftable=False)
|
||||||
|
branch = _run(repo, "symbolic-ref", "--short", "HEAD")
|
||||||
|
head = _run(repo, "rev-parse", "HEAD")
|
||||||
|
refs = git_refs.GitRefs(os.path.join(repo, ".git"))
|
||||||
|
assert refs.get(f"refs/heads/{branch}") == head
|
||||||
|
|
||||||
|
# External branch write happens before symbolic-ref + UpdateSymref.
|
||||||
|
_run(repo, "branch", "external-topic")
|
||||||
|
_run(repo, "symbolic-ref", "refs/remotes/m/main", f"refs/heads/{branch}")
|
||||||
|
refs.UpdateSymref("refs/remotes/m/main", f"refs/heads/{branch}")
|
||||||
|
|
||||||
|
assert not refs.is_loaded
|
||||||
|
assert refs.get("refs/heads/external-topic") == head
|
||||||
|
assert refs.get("refs/remotes/m/main") == head
|
||||||
|
|
||||||
|
# External sibling ref in the same directory is also detected.
|
||||||
|
_run(repo, "update-ref", "refs/remotes/m/sibling", head)
|
||||||
|
_run(
|
||||||
|
repo,
|
||||||
|
"symbolic-ref",
|
||||||
|
"refs/remotes/m/main",
|
||||||
|
"refs/heads/external-topic",
|
||||||
|
)
|
||||||
|
refs.UpdateSymref("refs/remotes/m/main", "refs/heads/external-topic")
|
||||||
|
|
||||||
|
assert not refs.is_loaded
|
||||||
|
assert refs.get("refs/remotes/m/sibling") == head
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_symref_self_or_cyclic_clears_phyref(tmp_path: Path) -> None:
|
||||||
|
"""Self or cyclic symrefs clear any prior physical SHA from _phyref."""
|
||||||
|
repo = _init_repo(tmp_path, reftable=False)
|
||||||
|
head = _run(repo, "rev-parse", "HEAD")
|
||||||
|
gitdir = os.path.join(repo, ".git")
|
||||||
|
_run(repo, "update-ref", "refs/remotes/m/main", head)
|
||||||
|
_run(repo, "update-ref", "refs/remotes/m/other", head)
|
||||||
|
|
||||||
|
refs = git_refs.GitRefs(gitdir)
|
||||||
|
assert refs.get("refs/remotes/m/main") == head
|
||||||
|
assert refs.get("refs/remotes/m/other") == head
|
||||||
|
|
||||||
|
_run(repo, "symbolic-ref", "refs/remotes/m/main", "refs/remotes/m/main")
|
||||||
|
refs.UpdateSymref("refs/remotes/m/main", "refs/remotes/m/main")
|
||||||
|
assert refs.get("refs/remotes/m/main") == ""
|
||||||
|
|
||||||
|
_run(repo, "symbolic-ref", "refs/remotes/m/main", "refs/remotes/m/other")
|
||||||
|
refs.UpdateSymref("refs/remotes/m/main", "refs/remotes/m/other")
|
||||||
|
assert refs.get("refs/remotes/m/main") == head
|
||||||
|
|
||||||
|
_run(repo, "symbolic-ref", "refs/remotes/m/other", "refs/remotes/m/main")
|
||||||
|
refs.UpdateSymref("refs/remotes/m/other", "refs/remotes/m/main")
|
||||||
|
assert refs.get("refs/remotes/m/main") == ""
|
||||||
|
assert refs.get("refs/remotes/m/other") == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_symref_propagates_chained_symrefs(tmp_path: Path) -> None:
|
||||||
|
"""Re-pointing an intermediate symref updates chained symrefs cleanly."""
|
||||||
|
repo = _init_repo(tmp_path, reftable=False)
|
||||||
|
branch = _run(repo, "symbolic-ref", "--short", "HEAD")
|
||||||
|
c1 = _run(repo, "rev-parse", "HEAD")
|
||||||
|
|
||||||
|
Path(os.path.join(repo, "a")).write_text("2", encoding="utf-8")
|
||||||
|
_run(repo, "add", "a")
|
||||||
|
_run(repo, "commit", "-q", "-m", "second")
|
||||||
|
c2 = _run(repo, "rev-parse", "HEAD")
|
||||||
|
_run(repo, "update-ref", "refs/heads/first", c1)
|
||||||
|
|
||||||
|
# Point HEAD at unborn refs/remotes/m/main before warming GitRefs.
|
||||||
|
_run(repo, "symbolic-ref", "HEAD", "refs/remotes/m/main")
|
||||||
|
refs = git_refs.GitRefs(os.path.join(repo, ".git"))
|
||||||
|
assert refs.symref("HEAD") == "refs/remotes/m/main"
|
||||||
|
assert refs.get("HEAD") == ""
|
||||||
|
|
||||||
|
_run(repo, "symbolic-ref", "refs/remotes/m/main", "refs/heads/first")
|
||||||
|
refs.UpdateSymref("refs/remotes/m/main", "refs/heads/first")
|
||||||
|
_run(repo, "symbolic-ref", "refs/remotes/m/alias", "refs/remotes/m/main")
|
||||||
|
refs.UpdateSymref("refs/remotes/m/alias", "refs/remotes/m/main")
|
||||||
|
assert refs.get("HEAD") == c1
|
||||||
|
assert refs.get("refs/remotes/m/alias") == c1
|
||||||
|
|
||||||
|
_run(repo, "symbolic-ref", "refs/remotes/m/main", f"refs/heads/{branch}")
|
||||||
|
refs.UpdateSymref("refs/remotes/m/main", f"refs/heads/{branch}")
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
refs,
|
||||||
|
"_LoadAll",
|
||||||
|
side_effect=AssertionError("expected warm GitRefs snapshot"),
|
||||||
|
):
|
||||||
|
assert refs.get("refs/remotes/m/main") == c2
|
||||||
|
assert refs.get("refs/remotes/m/alias") == c2
|
||||||
|
assert refs.get("HEAD") == c2
|
||||||
@@ -22,7 +22,7 @@ import unittest
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from test_manifest_xml import sort_attributes
|
import test_manifest_xml
|
||||||
|
|
||||||
import git_superproject
|
import git_superproject
|
||||||
import git_trace2_event_log
|
import git_trace2_event_log
|
||||||
@@ -255,7 +255,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
|||||||
with open(manifest_path) as fp:
|
with open(manifest_path) as fp:
|
||||||
manifest_xml_data = fp.read()
|
manifest_xml_data = fp.read()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sort_attributes(manifest_xml_data),
|
test_manifest_xml.sort_attributes(manifest_xml_data),
|
||||||
'<?xml version="1.0" ?><manifest>'
|
'<?xml version="1.0" ?><manifest>'
|
||||||
'<remote fetch="http://localhost" name="default-remote"/>'
|
'<remote fetch="http://localhost" name="default-remote"/>'
|
||||||
'<default remote="default-remote" revision="refs/heads/main"/>'
|
'<default remote="default-remote" revision="refs/heads/main"/>'
|
||||||
@@ -290,7 +290,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
|||||||
with open(update_result.manifest_path) as fp:
|
with open(update_result.manifest_path) as fp:
|
||||||
manifest_xml_data = fp.read()
|
manifest_xml_data = fp.read()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sort_attributes(manifest_xml_data),
|
test_manifest_xml.sort_attributes(manifest_xml_data),
|
||||||
'<?xml version="1.0" ?><manifest>'
|
'<?xml version="1.0" ?><manifest>'
|
||||||
'<remote fetch="http://localhost" name="default-remote"/>'
|
'<remote fetch="http://localhost" name="default-remote"/>'
|
||||||
'<default remote="default-remote" revision="refs/heads/main"/>'
|
'<default remote="default-remote" revision="refs/heads/main"/>'
|
||||||
@@ -317,7 +317,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
|||||||
self.maxDiff = None
|
self.maxDiff = None
|
||||||
self.assertIsNone(manifest.superproject)
|
self.assertIsNone(manifest.superproject)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sort_attributes(manifest.ToXml().toxml()),
|
test_manifest_xml.sort_attributes(manifest.ToXml().toxml()),
|
||||||
'<?xml version="1.0" ?><manifest>'
|
'<?xml version="1.0" ?><manifest>'
|
||||||
'<remote fetch="http://localhost" name="default-remote"/>'
|
'<remote fetch="http://localhost" name="default-remote"/>'
|
||||||
'<default remote="default-remote" revision="refs/heads/main"/>'
|
'<default remote="default-remote" revision="refs/heads/main"/>'
|
||||||
@@ -379,7 +379,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
|||||||
# Verify platform/vendor/x's project revision hasn't
|
# Verify platform/vendor/x's project revision hasn't
|
||||||
# changed.
|
# changed.
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sort_attributes(manifest_xml_data),
|
test_manifest_xml.sort_attributes(manifest_xml_data),
|
||||||
'<?xml version="1.0" ?><manifest>'
|
'<?xml version="1.0" ?><manifest>'
|
||||||
'<remote fetch="http://localhost" name="default-remote"/>'
|
'<remote fetch="http://localhost" name="default-remote"/>'
|
||||||
'<remote fetch="http://localhost2" name="goog"/>'
|
'<remote fetch="http://localhost2" name="goog"/>'
|
||||||
@@ -444,7 +444,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
|||||||
# Verify platform/vendor/x's project revision hasn't
|
# Verify platform/vendor/x's project revision hasn't
|
||||||
# changed.
|
# changed.
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
sort_attributes(manifest_xml_data),
|
test_manifest_xml.sort_attributes(manifest_xml_data),
|
||||||
'<?xml version="1.0" ?><manifest>'
|
'<?xml version="1.0" ?><manifest>'
|
||||||
'<remote fetch="http://localhost" name="default-remote"/>'
|
'<remote fetch="http://localhost" name="default-remote"/>'
|
||||||
'<default remote="default-remote" revision="refs/heads/main"/>'
|
'<default remote="default-remote" revision="refs/heads/main"/>'
|
||||||
|
|||||||
+2
-2
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
"""Unittests for the hooks.py module."""
|
"""Unittests for the hooks.py module."""
|
||||||
|
|
||||||
from io import StringIO
|
import io
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ def test_post_sync_argument_validation() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
old_stderr = sys.stderr
|
old_stderr = sys.stderr
|
||||||
sys.stderr = StringIO()
|
sys.stderr = io.StringIO()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Call with missing arg `sync_duration_seconds`
|
# Call with missing arg `sync_duration_seconds`
|
||||||
|
|||||||
@@ -14,10 +14,12 @@
|
|||||||
|
|
||||||
"""Tests for the main repo script and subcommand routing."""
|
"""Tests for the main repo script and subcommand routing."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import event_log
|
||||||
from main import _Repo
|
from main import _Repo
|
||||||
|
|
||||||
|
|
||||||
@@ -164,3 +166,52 @@ def test_autocorrect_prompt_interrupt(mock_input, repo, mock_config):
|
|||||||
res = repo._autocorrect_command_name("tart", mock_config)
|
res = repo._autocorrect_command_name("tart", mock_config)
|
||||||
|
|
||||||
assert res is None
|
assert res is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_log_disabled_skips_add_and_git_hash() -> None:
|
||||||
|
"""Test disabled EventLog returns None and skips GetCommitRevisionId."""
|
||||||
|
log = event_log.EventLog(enabled=False)
|
||||||
|
project = mock.MagicMock()
|
||||||
|
|
||||||
|
assert log.Add("sync", event_log.TASK_COMMAND, 1.0) is None
|
||||||
|
assert (
|
||||||
|
log.AddSync(project, event_log.TASK_SYNC_NETWORK, 1.0, 2.0, True)
|
||||||
|
is None
|
||||||
|
)
|
||||||
|
project.GetCommitRevisionId.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("manifest_xml.RepoClient")
|
||||||
|
def test_run_long_gates_event_log_on_flag(
|
||||||
|
mock_client_cls: mock.MagicMock, tmp_path: Path
|
||||||
|
) -> None:
|
||||||
|
"""Test _RunLong enables cmd.event_log only when --event-log is set."""
|
||||||
|
client = mock_client_cls.return_value
|
||||||
|
client.manifest.IsMirror = False
|
||||||
|
client.manifest.manifestProject.config.DumpConfigDict.return_value = {}
|
||||||
|
repo = _Repo("repodir")
|
||||||
|
|
||||||
|
for log_path, expected in (
|
||||||
|
(None, False),
|
||||||
|
(str(tmp_path / "events.log"), True),
|
||||||
|
):
|
||||||
|
cmd = mock.MagicMock()
|
||||||
|
cmd.event_log = event_log.EventLog(enabled=False)
|
||||||
|
cmd.manifest = client.manifest
|
||||||
|
cmd.MULTI_MANIFEST_SUPPORT = True
|
||||||
|
cmd.OptionParser.parse_args.return_value = (mock.MagicMock(), [])
|
||||||
|
cmd.ReadEnvironmentOptions.side_effect = lambda copts: copts
|
||||||
|
cmd.Execute.return_value = 0
|
||||||
|
repo.commands = {"sync": mock.Mock(return_value=cmd)}
|
||||||
|
gopts = mock.MagicMock(
|
||||||
|
color=None,
|
||||||
|
submanifest_path="",
|
||||||
|
pager=False,
|
||||||
|
time=False,
|
||||||
|
event_log=log_path,
|
||||||
|
git_trace2_event_log=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert repo._RunLong("sync", gopts, [], mock.MagicMock()) == 0
|
||||||
|
assert cmd.event_log.enabled is expected
|
||||||
|
assert bool(log_path and Path(log_path).exists()) is expected
|
||||||
@@ -933,6 +933,106 @@ class TestProjectElement:
|
|||||||
with pytest.raises(error.ManifestInvalidPathError):
|
with pytest.raises(error.ManifestInvalidPathError):
|
||||||
parse("ok", path)
|
parse("ok", path)
|
||||||
|
|
||||||
|
def test_sparse_checkout(self, repo_client: RepoClient) -> None:
|
||||||
|
"""Check sparse-checkout settings."""
|
||||||
|
manifest = repo_client.get_xml_manifest(
|
||||||
|
"""
|
||||||
|
<manifest>
|
||||||
|
<remote name="test-remote" fetch="http://localhost" />
|
||||||
|
<default remote="test-remote" revision="refs/heads/main" />
|
||||||
|
<project name="test-name" path="test-path">
|
||||||
|
<sparse-path path="src/main" />
|
||||||
|
<sparse-path path="docs" />
|
||||||
|
</project>
|
||||||
|
<project name="no-sparse" path="no-sparse-path" />
|
||||||
|
</manifest>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
assert len(manifest.projects) == 2
|
||||||
|
result = {p.name: p for p in manifest.projects}
|
||||||
|
|
||||||
|
assert result["test-name"].sparse_paths == ["src/main", "docs"]
|
||||||
|
assert result["no-sparse"].sparse_paths == []
|
||||||
|
|
||||||
|
def test_sparse_checkout_bad_paths(self, repo_client: RepoClient) -> None:
|
||||||
|
"""Check <sparse-path> rejects paths that escape the checkout."""
|
||||||
|
|
||||||
|
def parse(path: str) -> None:
|
||||||
|
path = repo_client.encode_xml_attr(path)
|
||||||
|
manifest = repo_client.get_xml_manifest(
|
||||||
|
f"""
|
||||||
|
<manifest>
|
||||||
|
<remote name="test-remote" fetch="http://localhost" />
|
||||||
|
<default remote="test-remote" revision="refs/heads/main" />
|
||||||
|
<project name="test-name" path="test-path">
|
||||||
|
<sparse-path path="{path}" />
|
||||||
|
</project>
|
||||||
|
</manifest>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
# Force the manifest to be parsed.
|
||||||
|
manifest.ToXml()
|
||||||
|
|
||||||
|
# Verify the parser is valid by default to avoid buggy tests below.
|
||||||
|
parse("src/main")
|
||||||
|
|
||||||
|
# Sparse paths name directories, so a trailing slash is fine.
|
||||||
|
parse("src/main/")
|
||||||
|
|
||||||
|
# A missing path is rejected by a different codepath.
|
||||||
|
with pytest.raises(error.ManifestParseError):
|
||||||
|
parse("")
|
||||||
|
|
||||||
|
for path in INVALID_FS_PATHS:
|
||||||
|
if not path or path.endswith("/") or path.endswith(os.path.sep):
|
||||||
|
continue
|
||||||
|
|
||||||
|
with pytest.raises(error.ManifestInvalidPathError):
|
||||||
|
parse(path)
|
||||||
|
|
||||||
|
def test_sparse_checkout_extend_project(
|
||||||
|
self, repo_client: RepoClient
|
||||||
|
) -> None:
|
||||||
|
"""Check sparse-path elements added via extend-project."""
|
||||||
|
manifest = repo_client.get_xml_manifest(
|
||||||
|
"""
|
||||||
|
<manifest>
|
||||||
|
<remote name="test-remote" fetch="http://localhost" />
|
||||||
|
<default remote="test-remote" revision="refs/heads/main" />
|
||||||
|
<project name="test-name" path="test-path">
|
||||||
|
<sparse-path path="src/main" />
|
||||||
|
</project>
|
||||||
|
<extend-project name="test-name">
|
||||||
|
<sparse-path path="docs" />
|
||||||
|
</extend-project>
|
||||||
|
</manifest>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
assert len(manifest.projects) == 1
|
||||||
|
assert manifest.projects[0].sparse_paths == ["src/main", "docs"]
|
||||||
|
|
||||||
|
def test_sparse_checkout_roundtrip(self, repo_client: RepoClient) -> None:
|
||||||
|
"""Check sparse-path elements survive a manifest round-trip."""
|
||||||
|
manifest = repo_client.get_xml_manifest(
|
||||||
|
"""
|
||||||
|
<manifest>
|
||||||
|
<remote name="test-remote" fetch="http://localhost" />
|
||||||
|
<default remote="test-remote" revision="refs/heads/main" />
|
||||||
|
<project name="test-name" path="test-path">
|
||||||
|
<sparse-path path="src/main" />
|
||||||
|
<sparse-path path="docs" />
|
||||||
|
</project>
|
||||||
|
</manifest>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
roundtrip = repo_client.get_xml_manifest(manifest.ToXml().toxml())
|
||||||
|
assert roundtrip.projects[0].sparse_paths == ["src/main", "docs"]
|
||||||
|
|
||||||
|
assert manifest.ToDict()["project"][0]["sparse-path"] == [
|
||||||
|
{"path": "src/main"},
|
||||||
|
{"path": "docs"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class TestSuperProjectElement:
|
class TestSuperProjectElement:
|
||||||
"""Tests for <superproject>."""
|
"""Tests for <superproject>."""
|
||||||
|
|||||||
+1332
-30
File diff suppressed because it is too large.
Load diff
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import runpy
|
import runpy
|
||||||
import sys
|
import sys
|
||||||
from types import ModuleType
|
import types
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import utils_for_test
|
import utils_for_test
|
||||||
@@ -31,7 +31,7 @@ def test_wrapper_does_not_run_main_for_multiprocessing_child(
|
|||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Do not rerun main when multiprocessing re-executes the wrapper."""
|
"""Do not rerun main when multiprocessing re-executes the wrapper."""
|
||||||
fake_update_manpages = ModuleType("update_manpages")
|
fake_update_manpages = types.ModuleType("update_manpages")
|
||||||
|
|
||||||
# Mock this because the real module requires newer Python versions than
|
# Mock this because the real module requires newer Python versions than
|
||||||
# our unittest framework does.
|
# our unittest framework does.
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from unittest import mock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from color import SetDefaultColoring
|
import color
|
||||||
import error
|
import error
|
||||||
import repo_logging
|
import repo_logging
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ def test_log_with_format_string(level: int) -> None:
|
|||||||
# Set color output to "always" for consistent test results.
|
# Set color output to "always" for consistent test results.
|
||||||
# This ensures the logger's behavior is uniform across different
|
# This ensures the logger's behavior is uniform across different
|
||||||
# environments and git configurations.
|
# environments and git configurations.
|
||||||
SetDefaultColoring("always")
|
color.SetDefaultColoring("always")
|
||||||
|
|
||||||
# Regex pattern to match optional ANSI color codes.
|
# Regex pattern to match optional ANSI color codes.
|
||||||
# \033 - Escape character
|
# \033 - Escape character
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import pytest
|
|||||||
|
|
||||||
from command import Command
|
from command import Command
|
||||||
import subcmds
|
import subcmds
|
||||||
|
from subcmds import sync
|
||||||
|
|
||||||
|
|
||||||
# NB: We don't test all subcommands as we want to avoid "change detection"
|
# NB: We don't test all subcommands as we want to avoid "change detection"
|
||||||
@@ -164,9 +165,7 @@ def test_common_validate_options(name: str, cls: Type[Command]) -> None:
|
|||||||
|
|
||||||
def test_attribute_error_repro() -> None:
|
def test_attribute_error_repro() -> None:
|
||||||
"""Confirm that accessing verbose before CommonValidateOptions fails."""
|
"""Confirm that accessing verbose before CommonValidateOptions fails."""
|
||||||
from subcmds.sync import Sync
|
cmd = sync.Sync()
|
||||||
|
|
||||||
cmd = Sync()
|
|
||||||
opts, args = cmd.OptionParser.parse_args([])
|
opts, args = cmd.OptionParser.parse_args([])
|
||||||
|
|
||||||
# This confirms that without the fix in main.py, an AttributeError
|
# This confirms that without the fix in main.py, an AttributeError
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import io
|
import io
|
||||||
from types import SimpleNamespace
|
import types
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import error
|
import error
|
||||||
@@ -66,7 +66,7 @@ def test_execute_delegates_autostash_to_rebase() -> None:
|
|||||||
branch.LocalMerge = "refs/remotes/origin/main"
|
branch.LocalMerge = "refs/remotes/origin/main"
|
||||||
project.GetBranch.return_value = branch
|
project.GetBranch.return_value = branch
|
||||||
cmd.GetProjects = mock.MagicMock(return_value=[project])
|
cmd.GetProjects = mock.MagicMock(return_value=[project])
|
||||||
opt = SimpleNamespace(
|
opt = types.SimpleNamespace(
|
||||||
interactive=False,
|
interactive=False,
|
||||||
fail_fast=False,
|
fail_fast=False,
|
||||||
whitespace=None,
|
whitespace=None,
|
||||||
|
|||||||
+41
-15
@@ -19,6 +19,7 @@ import optparse
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -33,7 +34,9 @@ import pytest
|
|||||||
import command
|
import command
|
||||||
import error
|
import error
|
||||||
import git_command
|
import git_command
|
||||||
|
import git_config
|
||||||
import manifest_xml
|
import manifest_xml
|
||||||
|
import progress
|
||||||
from project import SyncNetworkHalfResult
|
from project import SyncNetworkHalfResult
|
||||||
from subcmds import sync
|
from subcmds import sync
|
||||||
|
|
||||||
@@ -555,7 +558,9 @@ def test_cli_smart_sync_with_config(
|
|||||||
cmd = sync.Sync(manifest=manifest)
|
cmd = sync.Sync(manifest=manifest)
|
||||||
opts, args = cmd.OptionParser.parse_args(argv)
|
opts, args = cmd.OptionParser.parse_args(argv)
|
||||||
cmd.ValidateOptions(opts, args)
|
cmd.ValidateOptions(opts, args)
|
||||||
with mock.patch.object(sync.RepoConfig, "ForUser", return_value=user_cfg):
|
with mock.patch.object(
|
||||||
|
git_config.RepoConfig, "ForUser", return_value=user_cfg
|
||||||
|
):
|
||||||
implicit = cmd._ResolveSmartSyncOption(opts, manifest)
|
implicit = cmd._ResolveSmartSyncOption(opts, manifest)
|
||||||
assert opts.smart_sync == expected_smart_sync
|
assert opts.smart_sync == expected_smart_sync
|
||||||
assert implicit == expected_implicit
|
assert implicit == expected_implicit
|
||||||
@@ -603,7 +608,9 @@ def test_user_config_smart_sync(
|
|||||||
cmd = sync.Sync(manifest=manifest)
|
cmd = sync.Sync(manifest=manifest)
|
||||||
opts, args = cmd.OptionParser.parse_args(argv)
|
opts, args = cmd.OptionParser.parse_args(argv)
|
||||||
cmd.ValidateOptions(opts, args)
|
cmd.ValidateOptions(opts, args)
|
||||||
with mock.patch.object(sync.RepoConfig, "ForUser", return_value=user_cfg):
|
with mock.patch.object(
|
||||||
|
git_config.RepoConfig, "ForUser", return_value=user_cfg
|
||||||
|
):
|
||||||
implicit = cmd._ResolveSmartSyncOption(opts, manifest)
|
implicit = cmd._ResolveSmartSyncOption(opts, manifest)
|
||||||
user_cfg.GetBoolean.assert_called_once_with("sync.smartsync")
|
user_cfg.GetBoolean.assert_called_once_with("sync.smartsync")
|
||||||
assert opts.smart_sync == expected_smart_sync
|
assert opts.smart_sync == expected_smart_sync
|
||||||
@@ -622,7 +629,7 @@ def test_user_config_smart_sync_from_file(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Tests that sync.smartsync is read from ~/.repoconfig/config."""
|
"""Tests that sync.smartsync is read from ~/.repoconfig/config."""
|
||||||
# ForUser() caches its instance process-wide; make it pick up this HOME.
|
# ForUser() caches its instance process-wide; make it pick up this HOME.
|
||||||
monkeypatch.setattr(sync.RepoConfig, "_ForUser", None)
|
monkeypatch.setattr(git_config.RepoConfig, "_ForUser", None)
|
||||||
monkeypatch.delenv("REPO_CONFIG_DIR", raising=False)
|
monkeypatch.delenv("REPO_CONFIG_DIR", raising=False)
|
||||||
cfg_dir = tmp_home_dir / ".repoconfig"
|
cfg_dir = tmp_home_dir / ".repoconfig"
|
||||||
cfg_dir.mkdir()
|
cfg_dir.mkdir()
|
||||||
@@ -684,7 +691,9 @@ def test_cli_manifest_server_credentials(
|
|||||||
|
|
||||||
cmd = sync.Sync(manifest=manifest)
|
cmd = sync.Sync(manifest=manifest)
|
||||||
opts, args = cmd.OptionParser.parse_args(argv)
|
opts, args = cmd.OptionParser.parse_args(argv)
|
||||||
with mock.patch.object(sync.RepoConfig, "ForUser", return_value=user_cfg):
|
with mock.patch.object(
|
||||||
|
git_config.RepoConfig, "ForUser", return_value=user_cfg
|
||||||
|
):
|
||||||
if expected_exception:
|
if expected_exception:
|
||||||
with pytest.raises(expected_exception):
|
with pytest.raises(expected_exception):
|
||||||
cmd.ValidateOptions(opts, args)
|
cmd.ValidateOptions(opts, args)
|
||||||
@@ -979,6 +988,7 @@ class FakeProject:
|
|||||||
|
|
||||||
self.use_git_worktrees = False
|
self.use_git_worktrees = False
|
||||||
self.UseAlternates = False
|
self.UseAlternates = False
|
||||||
|
self.UseFetchCmd = False
|
||||||
self.UseReprojectCmd = False
|
self.UseReprojectCmd = False
|
||||||
self.manifest = mock.MagicMock()
|
self.manifest = mock.MagicMock()
|
||||||
self.manifest.GetProjectsWithName.return_value = [self]
|
self.manifest.GetProjectsWithName.return_value = [self]
|
||||||
@@ -1425,6 +1435,7 @@ class CheckForBloatedProjects(unittest.TestCase):
|
|||||||
self.project.name = "project"
|
self.project.name = "project"
|
||||||
self.project.Exists = True
|
self.project.Exists = True
|
||||||
self.project.worktree = self.tempdirobj.name
|
self.project.worktree = self.tempdirobj.name
|
||||||
|
self.project.UseFetchCmd = False
|
||||||
self.project.stateless_prune_needed = False
|
self.project.stateless_prune_needed = False
|
||||||
self.cmd.git_event_log = mock.MagicMock()
|
self.cmd.git_event_log = mock.MagicMock()
|
||||||
self.cmd._bloated_projects = []
|
self.cmd._bloated_projects = []
|
||||||
@@ -1492,7 +1503,7 @@ class CheckForBloatedProjects(unittest.TestCase):
|
|||||||
self.assertFalse(self.cmd.git_event_log.ErrorEvent.called)
|
self.assertFalse(self.cmd.git_event_log.ErrorEvent.called)
|
||||||
|
|
||||||
@mock.patch.object(git_command, "git_require", return_value=True)
|
@mock.patch.object(git_command, "git_require", return_value=True)
|
||||||
@mock.patch("subcmds.sync.Progress")
|
@mock.patch.object(progress, "Progress")
|
||||||
def test_network_only_skips_check(
|
def test_network_only_skips_check(
|
||||||
self, mock_progress: mock.Mock, mock_git_require: mock.Mock
|
self, mock_progress: mock.Mock, mock_git_require: mock.Mock
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -1506,7 +1517,7 @@ class CheckForBloatedProjects(unittest.TestCase):
|
|||||||
self.cmd.ExecuteInParallel.assert_not_called()
|
self.cmd.ExecuteInParallel.assert_not_called()
|
||||||
|
|
||||||
@mock.patch.object(git_command, "git_require", return_value=True)
|
@mock.patch.object(git_command, "git_require", return_value=True)
|
||||||
@mock.patch("subcmds.sync.Progress")
|
@mock.patch.object(progress, "Progress")
|
||||||
def test_projects_without_worktree_excluded(
|
def test_projects_without_worktree_excluded(
|
||||||
self, mock_progress: mock.Mock, mock_git_require: mock.Mock
|
self, mock_progress: mock.Mock, mock_git_require: mock.Mock
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -1528,7 +1539,7 @@ class CheckForBloatedProjects(unittest.TestCase):
|
|||||||
self.cmd.ExecuteInParallel.assert_not_called()
|
self.cmd.ExecuteInParallel.assert_not_called()
|
||||||
|
|
||||||
@mock.patch.object(git_command, "git_require")
|
@mock.patch.object(git_command, "git_require")
|
||||||
@mock.patch("subcmds.sync.Progress")
|
@mock.patch.object(progress, "Progress")
|
||||||
def test_bloated_project_found(self, mock_progress, mock_git_require):
|
def test_bloated_project_found(self, mock_progress, mock_git_require):
|
||||||
"""Test that it adds project to _bloated_projects."""
|
"""Test that it adds project to _bloated_projects."""
|
||||||
mock_git_require.return_value = True
|
mock_git_require.return_value = True
|
||||||
@@ -1551,7 +1562,7 @@ class CheckForBloatedProjects(unittest.TestCase):
|
|||||||
self.assertEqual(self.cmd._bloated_projects, ["project"])
|
self.assertEqual(self.cmd._bloated_projects, ["project"])
|
||||||
|
|
||||||
@mock.patch.object(git_command, "git_require")
|
@mock.patch.object(git_command, "git_require")
|
||||||
@mock.patch("subcmds.sync.Progress")
|
@mock.patch.object(progress, "Progress")
|
||||||
def test_stateless_prune_excluded(self, mock_progress, mock_git_require):
|
def test_stateless_prune_excluded(self, mock_progress, mock_git_require):
|
||||||
"""Test that projects pruned for stateless sync are excluded."""
|
"""Test that projects pruned for stateless sync are excluded."""
|
||||||
mock_git_require.return_value = True
|
mock_git_require.return_value = True
|
||||||
@@ -1565,6 +1576,22 @@ class CheckForBloatedProjects(unittest.TestCase):
|
|||||||
self.assertFalse(self.cmd.ExecuteInParallel.called)
|
self.assertFalse(self.cmd.ExecuteInParallel.called)
|
||||||
self.assertEqual(self.cmd._bloated_projects, [])
|
self.assertEqual(self.cmd._bloated_projects, [])
|
||||||
|
|
||||||
|
@mock.patch.object(git_command, "git_require", return_value=True)
|
||||||
|
@mock.patch.object(progress, "Progress")
|
||||||
|
def test_fetch_cmd_excluded(
|
||||||
|
self, mock_progress: mock.Mock, mock_git_require: mock.Mock
|
||||||
|
) -> None:
|
||||||
|
"""Projects using repo.fetchcmd are excluded from the bloat check."""
|
||||||
|
self.project.UseFetchCmd = True
|
||||||
|
self.cmd.ExecuteInParallel = mock.Mock()
|
||||||
|
|
||||||
|
with mock.patch.object(self.cmd, "ParallelContext"):
|
||||||
|
self.cmd._CheckForBloatedProjects([self.project], self.opt)
|
||||||
|
|
||||||
|
mock_progress.assert_not_called()
|
||||||
|
self.cmd.ExecuteInParallel.assert_not_called()
|
||||||
|
self.assertEqual(self.cmd._bloated_projects, [])
|
||||||
|
|
||||||
|
|
||||||
class GCProjectsTest(unittest.TestCase):
|
class GCProjectsTest(unittest.TestCase):
|
||||||
"""Tests for Sync._GCProjects."""
|
"""Tests for Sync._GCProjects."""
|
||||||
@@ -1583,7 +1610,7 @@ class GCProjectsTest(unittest.TestCase):
|
|||||||
self.project.bare_git._project = self.project
|
self.project.bare_git._project = self.project
|
||||||
self.cmd.git_event_log = mock.MagicMock()
|
self.cmd.git_event_log = mock.MagicMock()
|
||||||
|
|
||||||
@mock.patch("subcmds.sync.Progress")
|
@mock.patch.object(progress, "Progress")
|
||||||
def test_GCProjects_skip_gc(self, mock_progress):
|
def test_GCProjects_skip_gc(self, mock_progress):
|
||||||
"""Test that it skips GC if opt.auto_gc is False."""
|
"""Test that it skips GC if opt.auto_gc is False."""
|
||||||
self.opt.auto_gc = False
|
self.opt.auto_gc = False
|
||||||
@@ -1594,7 +1621,7 @@ class GCProjectsTest(unittest.TestCase):
|
|||||||
mock_set_state.assert_called_once_with(self.project, self.opt)
|
mock_set_state.assert_called_once_with(self.project, self.opt)
|
||||||
self.assertFalse(self.project.bare_git.gc.called)
|
self.assertFalse(self.project.bare_git.gc.called)
|
||||||
|
|
||||||
@mock.patch("subcmds.sync.Progress")
|
@mock.patch.object(progress, "Progress")
|
||||||
def test_GCProjects_sequential(self, mock_progress):
|
def test_GCProjects_sequential(self, mock_progress):
|
||||||
"""Test sequential GC (jobs < 2)."""
|
"""Test sequential GC (jobs < 2)."""
|
||||||
with mock.patch.object(sync.Sync, "_SetPreciousObjectsState"):
|
with mock.patch.object(sync.Sync, "_SetPreciousObjectsState"):
|
||||||
@@ -1606,7 +1633,7 @@ class GCProjectsTest(unittest.TestCase):
|
|||||||
for call in self.project.config.SetString.call_args_list:
|
for call in self.project.config.SetString.call_args_list:
|
||||||
self.assertNotEqual(call.args[0], "gc.autoDetach")
|
self.assertNotEqual(call.args[0], "gc.autoDetach")
|
||||||
|
|
||||||
@mock.patch("subcmds.sync.Progress")
|
@mock.patch.object(progress, "Progress")
|
||||||
def test_GCProjects_parallel(self, mock_progress):
|
def test_GCProjects_parallel(self, mock_progress):
|
||||||
"""Test parallel GC (jobs >= 2)."""
|
"""Test parallel GC (jobs >= 2)."""
|
||||||
self.opt.jobs = 2
|
self.opt.jobs = 2
|
||||||
@@ -1654,6 +1681,7 @@ class SyncCommand(unittest.TestCase):
|
|||||||
self.project = p = mock.MagicMock(
|
self.project = p = mock.MagicMock(
|
||||||
use_git_worktrees=False,
|
use_git_worktrees=False,
|
||||||
UseAlternates=False,
|
UseAlternates=False,
|
||||||
|
UseFetchCmd=False,
|
||||||
UseReprojectCmd=False,
|
UseReprojectCmd=False,
|
||||||
name="project",
|
name="project",
|
||||||
relpath="rel_path",
|
relpath="rel_path",
|
||||||
@@ -1861,7 +1889,7 @@ class SyncCommand(unittest.TestCase):
|
|||||||
user_cfg = mock.MagicMock()
|
user_cfg = mock.MagicMock()
|
||||||
user_cfg.GetBoolean.return_value = False
|
user_cfg.GetBoolean.return_value = False
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
sync.RepoConfig, "ForUser", return_value=user_cfg
|
git_config.RepoConfig, "ForUser", return_value=user_cfg
|
||||||
):
|
):
|
||||||
with mock.patch.object(self.cmd, "_SmartSyncSetup") as mock_setup:
|
with mock.patch.object(self.cmd, "_SmartSyncSetup") as mock_setup:
|
||||||
with mock.patch.object(self.cmd, "_UpdateRepoProject"):
|
with mock.patch.object(self.cmd, "_UpdateRepoProject"):
|
||||||
@@ -1884,7 +1912,7 @@ class SyncCommand(unittest.TestCase):
|
|||||||
user_cfg = mock.MagicMock()
|
user_cfg = mock.MagicMock()
|
||||||
user_cfg.GetBoolean.return_value = True
|
user_cfg.GetBoolean.return_value = True
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
sync.RepoConfig, "ForUser", return_value=user_cfg
|
git_config.RepoConfig, "ForUser", return_value=user_cfg
|
||||||
):
|
):
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
self.cmd,
|
self.cmd,
|
||||||
@@ -3012,8 +3040,6 @@ class TestSmartSyncSetupRemoteHelper(unittest.TestCase):
|
|||||||
self, mock_transport_class, mock_server_class, mock_popen, mock_which
|
self, mock_transport_class, mock_server_class, mock_popen, mock_which
|
||||||
):
|
):
|
||||||
"""Test _SmartSyncSetup when a helper is present and succeeds."""
|
"""Test _SmartSyncSetup when a helper is present and succeeds."""
|
||||||
import subprocess
|
|
||||||
|
|
||||||
self.manifest.manifest_server = (
|
self.manifest.manifest_server = (
|
||||||
"persistent-https://android-smartsync.corp.google.com/"
|
"persistent-https://android-smartsync.corp.google.com/"
|
||||||
"manifestserver"
|
"manifestserver"
|
||||||
|
|||||||
Reference in new issue
Block a user