mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-07-16 21:57:00 +00:00
Compare commits
203 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d54384a5e | |||
| 1b4e7a04be | |||
| a94c9e2b52 | |||
| 51021fb209 | |||
| d453273f06 | |||
| 0129ac1c46 | |||
| bf8c59f4a0 | |||
| 003f0407fc | |||
| 1db50e49fb | |||
| c97a306fe7 | |||
| 5534f164d6 | |||
| 67e52a120b | |||
| d5037230e9 | |||
| 7cc99b24c1 | |||
| 12ad396f67 | |||
| a6bc1b7cf0 | |||
| a98e422113 | |||
| 5d8585012f | |||
| e8deeabf2a | |||
| 11428ae984 | |||
| 27d2232eb3 | |||
| e3eadd3728 | |||
| 12cfc6036a | |||
| 134eeb024b | |||
| 03fb18109f | |||
| 5af71ce907 | |||
| baa281d99e | |||
| 7e9079b7cf | |||
| 61bd6b3ccb | |||
| 32e7327ca6 | |||
| 4f707ff91e | |||
| e2671c184e | |||
| b43a20b859 | |||
| 8869a30283 | |||
| 3b0eebeccf | |||
| 00991bfb42 | |||
| e8338b54bd | |||
| 951666fb23 | |||
| 854b330967 | |||
| 654690e1b8 | |||
| ac2be4c089 | |||
| 3d819e8e3e | |||
| 573983948a | |||
| 3f3c681a02 | |||
| 242e97d9dd | |||
| ade45de770 | |||
| 0251fb33c4 | |||
| 0176586544 | |||
| 582804a59e | |||
| afc3d55d39 | |||
| f24bc7aed5 | |||
| 83b8ebdbbe | |||
| a0abfd7339 | |||
| 403fedfeb5 | |||
| f14c577fce | |||
| 67881c0c3b | |||
| 551087cd98 | |||
| 8da56a0cc5 | |||
| 0f01cd24e9 | |||
| 1ee98667cc | |||
| 6f9622fe1c | |||
| 5cb0251248 | |||
| a214fd31bd | |||
| 62cd0de6cf | |||
| b60512a75a | |||
| 5d88972390 | |||
| 3c0e67bbc5 | |||
| 3b7b20ac1d | |||
| e71a8c6dd8 | |||
| c687b5df9e | |||
| 1dd9c57a28 | |||
| 4525c2e0ad | |||
| 45dcd738b7 | |||
| 1dad86dc00 | |||
| 622a5bf9c2 | |||
| 871e4c7ed1 | |||
| 5b0b5513d6 | |||
| b5991d7128 | |||
| 7f87c54043 | |||
| 50c6226075 | |||
| 1e4b2887a7 | |||
| 31b4b19387 | |||
| 2b6de52a36 | |||
| 91ec998598 | |||
| 08964a1658 | |||
| 3073a90046 | |||
| 75773b8b9d | |||
| 412367bfaf | |||
| 47c24b5c40 | |||
| be33106ffc | |||
| 5998c0b506 | |||
| 877ef91be2 | |||
| 4ab2284a94 | |||
| 1afe96a7e9 | |||
| 2719a8e203 | |||
| e4872ac8ba | |||
| 4623264809 | |||
| 67383bdba9 | |||
| d30414bb53 | |||
| 80d1a5ad3e | |||
| c615c964fb | |||
| 5ed12ec81d | |||
| 58a59fdfbc | |||
| 38d2fe11b9 | |||
| 854fe440f2 | |||
| d534a5537f | |||
| a64149a7a7 | |||
| 3e6acf2778 | |||
| a6e1a59ac1 | |||
| 380bf9546e | |||
| d9cc0a1526 | |||
| 8c3585f367 | |||
| 239fad7146 | |||
| d3eec0acdd | |||
| 7f7d70efe4 | |||
| 720bd1e96b | |||
| 25858c8b16 | |||
| 52bab0ba27 | |||
| 2e6d0881d9 | |||
| 74edacd8e5 | |||
| 5d95ba8d85 | |||
| 82d500eb7a | |||
| 21269c3eed | |||
| 99b5a17f2c | |||
| df3c4017f9 | |||
| f7a3f99dc9 | |||
| 6b8e9fc8db | |||
| 7b6ffed4ae | |||
| b4b323a8bd | |||
| f91f4462e6 | |||
| 85352825ff | |||
| b262d0e461 | |||
| 044e52e236 | |||
| 0cb88a8d79 | |||
| 08815ad3eb | |||
| 3c8bae27ec | |||
| 06338abe79 | |||
| 8d37f61471 | |||
| 1acbc14c34 | |||
| c448ba9cc7 | |||
| 21cbcc54e9 | |||
| 0f200bb3a1 | |||
| c8da28c3ed | |||
| c061593a12 | |||
| a94457d1ce | |||
| 97dc5c1bd9 | |||
| 0214730c9a | |||
| daebd6cbc2 | |||
| 3667de1d0f | |||
| 85ee1738e6 | |||
| f070331a4c | |||
| 9ecb80ba26 | |||
| dc8185f2a9 | |||
| 59b81c84de | |||
| 507d463600 | |||
| cd391e77d0 | |||
| 8310436be0 | |||
| d5087392ed | |||
| 91f428058d | |||
| 243df2042e | |||
| 4b94e773ef | |||
| fc901b92bb | |||
| 8d5f032611 | |||
| 99eca45eb2 | |||
| 66685f07ec | |||
| cf9a2a2a76 | |||
| 5ae8292fea | |||
| dfdf577e98 | |||
| 747ec83f58 | |||
| 1711bc23c0 | |||
| db111d3924 | |||
| 3405446a4e | |||
| 41a27eb854 | |||
| d93fe60e89 | |||
| 61224d01fa | |||
| 13d6588bf6 | |||
| 9500aca754 | |||
| e8a7b9d596 | |||
| cf411b3f03 | |||
| 1feecbd91e | |||
| 616e314902 | |||
| fafd1ec23e | |||
| b1613d741e | |||
| ab2d321104 | |||
| aada468916 | |||
| 1d5098617e | |||
| e219c78fe5 | |||
| f9f4df62e0 | |||
| ebdf0409d2 | |||
| 303bd963d5 | |||
| ae384f8623 | |||
| 70a4e643e6 | |||
| 8da4861b38 | |||
| 39ffd9977e | |||
| 584863fb5e | |||
| 454fdaf119 | |||
| f7f9dd4deb | |||
| 70ee4dd313 | |||
| cfe3095e50 | |||
| 621de7ed12 | |||
| d7ebdf56be | |||
| fabab4e245 | |||
| b577444a90 |
@@ -12,5 +12,6 @@ extend-ignore =
|
||||
# E731: do not assign a lambda expression, use a def
|
||||
E731,
|
||||
exclude =
|
||||
.venv,
|
||||
venv,
|
||||
.tox,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# GitHub actions workflow.
|
||||
# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
|
||||
# https://black.readthedocs.io/en/stable/integrations/github_actions.html
|
||||
|
||||
name: Format
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: psf/black@stable
|
||||
@@ -18,5 +18,5 @@ jobs:
|
||||
Thanks for your contribution!
|
||||
Unfortunately, we don't use GitHub pull requests to manage code
|
||||
contributions to this repository.
|
||||
Instead, please see [README.md](../blob/HEAD/SUBMITTING_PATCHES.md)
|
||||
Instead, please see [README.md](../blob/HEAD/CONTRIBUTING.md)
|
||||
which provides full instructions on how to get involved.
|
||||
|
||||
@@ -27,6 +27,6 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install tox tox-gh-actions
|
||||
- name: Test with tox
|
||||
run: tox
|
||||
python -m pip install pytest
|
||||
- name: Run tests
|
||||
run: python -m pytest
|
||||
|
||||
@@ -6,6 +6,7 @@ __pycache__
|
||||
/dist
|
||||
.repopickle_*
|
||||
/repoc
|
||||
/.cipd_bin
|
||||
/.tox
|
||||
/.venv
|
||||
|
||||
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
# Copyright 2023 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Config file for the isort python module.
|
||||
# This is used to enforce import sorting standards.
|
||||
#
|
||||
# https://pycqa.github.io/isort/docs/configuration/options.html
|
||||
|
||||
[settings]
|
||||
# Be compatible with `black` since it also matches what we want.
|
||||
profile = black
|
||||
|
||||
line_length = 80
|
||||
length_sort = false
|
||||
force_single_line = true
|
||||
lines_after_imports = 2
|
||||
from_first = false
|
||||
case_sensitive = false
|
||||
force_sort_within_sections = true
|
||||
order_by_type = false
|
||||
|
||||
# Ignore generated files.
|
||||
extend_skip_glob = *_pb2.py
|
||||
|
||||
# Allow importing multiple classes on a single line from these modules.
|
||||
# https://google.github.io/styleguide/pyguide#s2.2-imports
|
||||
single_line_exclusions =
|
||||
abc,
|
||||
collections.abc,
|
||||
typing,
|
||||
@@ -5,6 +5,5 @@
|
||||
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
|
||||
<path>/git-repo</path>
|
||||
</pydev_pathproperty>
|
||||
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
|
||||
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
|
||||
</pydev_project>
|
||||
|
||||
@@ -24,7 +24,7 @@ However there are some differences, so please review and familiarize
|
||||
yourself with the following relevant bits.
|
||||
|
||||
|
||||
## Make separate commits for logically separate changes.
|
||||
## Make separate commits for logically separate changes
|
||||
|
||||
Unless your patch is really trivial, you should not be sending out a patch that
|
||||
was generated between your working tree and your commit head.
|
||||
@@ -43,17 +43,12 @@ probably need to split up your commit to finer grained pieces.
|
||||
|
||||
Lint any changes by running:
|
||||
```sh
|
||||
$ tox -e lint -- file.py
|
||||
$ flake8
|
||||
```
|
||||
|
||||
And format with:
|
||||
```sh
|
||||
$ tox -e format -- file.py
|
||||
```
|
||||
|
||||
Or format everything:
|
||||
```sh
|
||||
$ tox -e format
|
||||
$ black file.py
|
||||
```
|
||||
|
||||
Repo uses [black](https://black.readthedocs.io/) with line length of 80 as its
|
||||
@@ -73,15 +68,11 @@ the entire project in the included `.flake8` file.
|
||||
[PEP 8]: https://www.python.org/dev/peps/pep-0008/
|
||||
[flake8 documentation]: https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html#in-line-ignoring-errors
|
||||
|
||||
|
||||
## Running tests
|
||||
|
||||
We use [pytest](https://pytest.org/) and [tox](https://tox.readthedocs.io/) for
|
||||
running tests. You should make sure to install those first.
|
||||
|
||||
To run the full suite against all supported Python versions, simply execute:
|
||||
```sh
|
||||
$ tox -p auto
|
||||
```
|
||||
We use [pytest](https://pytest.org/) for running tests. You should make sure to
|
||||
install that first.
|
||||
|
||||
We have [`./run_tests`](./run_tests) which is a simple wrapper around `pytest`:
|
||||
```sh
|
||||
@@ -127,7 +118,7 @@ your patch. It is virtually impossible to remove a patch once it
|
||||
has been applied and pushed out.
|
||||
|
||||
|
||||
## Sending your patches.
|
||||
## Sending your patches
|
||||
|
||||
Do not email your patches to anyone.
|
||||
|
||||
@@ -143,7 +134,7 @@ they have right to redistribute your work under the Apache License:
|
||||
|
||||
Ensure you have obtained an HTTP password to authenticate:
|
||||
|
||||
https://gerrit-review.googlesource.com/new-password
|
||||
https://www.googlesource.com/new-password
|
||||
|
||||
Ensure that you have the local commit hook installed to automatically
|
||||
add a ChangeId to your commits:
|
||||
@@ -14,7 +14,7 @@ that you can put anywhere in your path.
|
||||
* Docs: <https://source.android.com/source/using-repo.html>
|
||||
* [repo Manifest Format](./docs/manifest-format.md)
|
||||
* [repo Hooks](./docs/repo-hooks.md)
|
||||
* [Submitting patches](./SUBMITTING_PATCHES.md)
|
||||
* [Contributing](./CONTRIBUTING.md)
|
||||
* Running Repo in [Microsoft Windows](./docs/windows.md)
|
||||
* GitHub mirror: <https://github.com/GerritCodeReview/git-repo>
|
||||
* Postsubmit tests: <https://github.com/GerritCodeReview/git-repo/actions>
|
||||
@@ -49,6 +49,43 @@ $ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.bin/repo
|
||||
$ chmod a+rx ~/.bin/repo
|
||||
```
|
||||
|
||||
## Shell Completion
|
||||
|
||||
Repo includes completion scripts for Bash and Zsh.
|
||||
|
||||
### Bash
|
||||
|
||||
To enable completion in Bash, source `completion.bash` in your `~/.bashrc`:
|
||||
|
||||
```sh
|
||||
source /path/to/git-repo/completion.bash
|
||||
```
|
||||
|
||||
### Zsh
|
||||
|
||||
To enable completion in Zsh, you can either:
|
||||
|
||||
1. Copy or symlink `completion.zsh` to a file named `_repo` in a directory in your `$fpath`:
|
||||
```sh
|
||||
mkdir -p ~/.zsh/completion
|
||||
# You can copy the file:
|
||||
cp /path/to/git-repo/completion.zsh ~/.zsh/completion/_repo
|
||||
# Or symlink it:
|
||||
ln -s /path/to/git-repo/completion.zsh ~/.zsh/completion/_repo
|
||||
```
|
||||
Then add that directory to your `fpath` in `~/.zshrc` before `compinit`:
|
||||
```zsh
|
||||
fpath=(~/.zsh/completion $fpath)
|
||||
autoload -Uz compinit
|
||||
compinit
|
||||
```
|
||||
|
||||
2. Or source the file directly and call `compdef` in your `~/.zshrc`:
|
||||
```zsh
|
||||
source /path/to/git-repo/completion.zsh
|
||||
compdef _repo repo
|
||||
```
|
||||
|
||||
|
||||
[new-bug]: https://issues.gerritcodereview.com/issues/new?component=1370071
|
||||
[issue tracker]: https://issues.gerritcodereview.com/issues?q=is:open%20componentid:1370071
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# Copyright (C) 2026 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# This file contains version pins of a few tools used by run_tests.
|
||||
|
||||
# Pin resolved versions in the repo, to reduce trust in the CIPD backend.
|
||||
#
|
||||
# Most of these tools are generated via builders at
|
||||
# https://ci.chromium.org/p/infra/g/infra/console
|
||||
#
|
||||
# For these, the git revision is the one of
|
||||
# https://chromium.googlesource.com/infra/infra.git.
|
||||
#
|
||||
# To regenerate them (after modifying this file):
|
||||
# cipd ensure-file-resolve -ensure-file cipd_manifest.txt
|
||||
$ResolvedVersions cipd_manifest.versions
|
||||
|
||||
# Supported platforms. Feel free to add more as long as the tools work.
|
||||
$VerifiedPlatform linux-amd64 linux-arm64 mac-amd64 mac-arm64
|
||||
|
||||
infra/3pp/tools/help2man/${platform} version:3@1.49.3
|
||||
@@ -0,0 +1,18 @@
|
||||
# This file is auto-generated by 'cipd ensure-file-resolve'.
|
||||
# Do not modify manually. All changes will be overwritten.
|
||||
|
||||
infra/3pp/tools/help2man/linux-amd64
|
||||
version:3@1.49.3
|
||||
82ySLC4--mvjnmuwj_j_2odHLj5zFaNFpWvfCyMunY0C
|
||||
|
||||
infra/3pp/tools/help2man/linux-arm64
|
||||
version:3@1.49.3
|
||||
szfWfjI4RyCT6gIB48CeUOGG6_Un4wfbIb0Vvs5M9MUC
|
||||
|
||||
infra/3pp/tools/help2man/mac-amd64
|
||||
version:3@1.49.3
|
||||
t7iyQx5TnGbuJfj6zMTvjPdBl-Cb_w5papUh80NI2S8C
|
||||
|
||||
infra/3pp/tools/help2man/mac-arm64
|
||||
version:3@1.49.3
|
||||
t5eD-eLKRpnSifZdbi072U8bnPfGye1pIMmQ07nxJ7kC
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
import pager
|
||||
|
||||
@@ -84,7 +85,7 @@ def _Color(fg=None, bg=None, attr=None):
|
||||
DEFAULT = None
|
||||
|
||||
|
||||
def SetDefaultColoring(state):
|
||||
def SetDefaultColoring(state: Optional[str]) -> None:
|
||||
"""Set coloring behavior to |state|.
|
||||
|
||||
This is useful for overriding config options via the command line.
|
||||
@@ -97,9 +98,9 @@ def SetDefaultColoring(state):
|
||||
state = state.lower()
|
||||
if state in ("auto",):
|
||||
DEFAULT = state
|
||||
elif state in ("always", "yes", "true", True):
|
||||
elif state in ("always", "yes", "true"):
|
||||
DEFAULT = "always"
|
||||
elif state in ("never", "no", "false", False):
|
||||
elif state in ("never", "no", "false"):
|
||||
DEFAULT = "never"
|
||||
|
||||
|
||||
|
||||
+74
-10
@@ -12,6 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import contextlib
|
||||
import multiprocessing
|
||||
import optparse
|
||||
import os
|
||||
@@ -70,6 +71,14 @@ class Command:
|
||||
# migrated subcommands can set it to False.
|
||||
MULTI_MANIFEST_SUPPORT = True
|
||||
|
||||
# Shared data across parallel execution workers.
|
||||
_parallel_context = None
|
||||
|
||||
@classmethod
|
||||
def get_parallel_context(cls):
|
||||
assert cls._parallel_context is not None
|
||||
return cls._parallel_context
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
repodir=None,
|
||||
@@ -92,6 +101,11 @@ class Command:
|
||||
def WantPager(self, _opt):
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_multiprocessing_active() -> bool:
|
||||
"""Whether the current process is a worker in a pool."""
|
||||
return multiprocessing.current_process().name != "MainProcess"
|
||||
|
||||
def ReadEnvironmentOptions(self, opts):
|
||||
"""Set options from environment variables."""
|
||||
|
||||
@@ -242,9 +256,39 @@ class Command:
|
||||
"""Perform the action, after option parsing is complete."""
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
@classmethod
|
||||
@contextlib.contextmanager
|
||||
def ParallelContext(cls):
|
||||
"""Obtains the context, which is shared to ExecuteInParallel workers.
|
||||
|
||||
Callers can store data in the context dict before invocation of
|
||||
ExecuteInParallel. The dict will then be shared to child workers of
|
||||
ExecuteInParallel.
|
||||
"""
|
||||
assert cls._parallel_context is None
|
||||
cls._parallel_context = {}
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
cls._parallel_context = None
|
||||
|
||||
@classmethod
|
||||
def _InitParallelWorker(cls, context, initializer):
|
||||
cls._parallel_context = context
|
||||
if initializer:
|
||||
initializer()
|
||||
|
||||
@classmethod
|
||||
def ExecuteInParallel(
|
||||
jobs, func, inputs, callback, output=None, ordered=False
|
||||
cls,
|
||||
jobs,
|
||||
func,
|
||||
inputs,
|
||||
callback,
|
||||
output=None,
|
||||
ordered=False,
|
||||
chunksize=WORKER_BATCH_SIZE,
|
||||
initializer=None,
|
||||
):
|
||||
"""Helper for managing parallel execution boiler plate.
|
||||
|
||||
@@ -269,6 +313,9 @@ class Command:
|
||||
output: An output manager. May be progress.Progess or
|
||||
color.Coloring.
|
||||
ordered: Whether the jobs should be processed in order.
|
||||
chunksize: The number of jobs processed in batch by parallel
|
||||
workers.
|
||||
initializer: Worker initializer.
|
||||
|
||||
Returns:
|
||||
The |callback| function's results are returned.
|
||||
@@ -278,12 +325,16 @@ class Command:
|
||||
if len(inputs) == 1 or jobs == 1:
|
||||
return callback(None, output, (func(x) for x in inputs))
|
||||
else:
|
||||
with multiprocessing.Pool(jobs) as pool:
|
||||
with multiprocessing.Pool(
|
||||
jobs,
|
||||
initializer=cls._InitParallelWorker,
|
||||
initargs=(cls._parallel_context, initializer),
|
||||
) as pool:
|
||||
submit = pool.imap if ordered else pool.imap_unordered
|
||||
return callback(
|
||||
pool,
|
||||
output,
|
||||
submit(func, inputs, chunksize=WORKER_BATCH_SIZE),
|
||||
submit(func, inputs, chunksize=chunksize),
|
||||
)
|
||||
finally:
|
||||
if isinstance(output, progress.Progress):
|
||||
@@ -353,7 +404,7 @@ class Command:
|
||||
result = []
|
||||
|
||||
if not groups:
|
||||
groups = manifest.GetGroupsStr()
|
||||
groups = manifest.GetManifestGroupsStr()
|
||||
groups = [x for x in re.split(r"[,\s]+", groups) if x]
|
||||
|
||||
if not args:
|
||||
@@ -361,7 +412,8 @@ class Command:
|
||||
for project in all_projects_list:
|
||||
if submodules_ok or project.sync_s:
|
||||
derived_projects.update(
|
||||
(p.name, p) for p in project.GetDerivedSubprojects()
|
||||
(p.RelPath(local=False), p)
|
||||
for p in project.GetDerivedSubprojects()
|
||||
)
|
||||
all_projects_list.extend(derived_projects.values())
|
||||
for project in all_projects_list:
|
||||
@@ -465,6 +517,22 @@ class Command:
|
||||
)
|
||||
return result
|
||||
|
||||
def GetSmartSyncOverridePath(self, manifest=None) -> str:
|
||||
"""Return the path where smart sync writes its override manifest."""
|
||||
if manifest is None:
|
||||
manifest = self.manifest
|
||||
return os.path.join(
|
||||
manifest.manifestProject.worktree, "smart_sync_override.xml"
|
||||
)
|
||||
|
||||
def TryOverrideManifestWithSmartSync(self, manifest=None) -> None:
|
||||
"""Override manifest with smart_sync_override.xml if it exists."""
|
||||
if manifest is None:
|
||||
manifest = self.manifest
|
||||
smart_sync_manifest_path = self.GetSmartSyncOverridePath(manifest)
|
||||
if os.path.isfile(smart_sync_manifest_path):
|
||||
manifest.Override(smart_sync_manifest_path)
|
||||
|
||||
def ManifestList(self, opt):
|
||||
"""Yields all of the manifests to traverse.
|
||||
|
||||
@@ -501,7 +569,3 @@ class MirrorSafeCommand:
|
||||
"""Command permits itself to run within a mirror, and does not require a
|
||||
working directory.
|
||||
"""
|
||||
|
||||
|
||||
class GitcClientCommand:
|
||||
"""Command that requires the local client to be a GITC client."""
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Copyright 2021 The Android Open Source Project
|
||||
# Copyright (C) 2021 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
+449
@@ -0,0 +1,449 @@
|
||||
#compdef _repo repo
|
||||
|
||||
# Copyright (C) 2026 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
_repo() {
|
||||
local context state line
|
||||
typeset -A opt_args
|
||||
|
||||
local -a subcommands
|
||||
subcommands=(
|
||||
'abandon:Abandon a development branch'
|
||||
'branches:View current topic branches'
|
||||
'checkout:Checkout a branch'
|
||||
'cherry-pick:Cherry-pick a change'
|
||||
'diff:Show changes between commit and working tree'
|
||||
'diffmanifests:Show differences between two manifests'
|
||||
'download:Download and find a change'
|
||||
'forall:Run a shell command in each project'
|
||||
'gc:Garbage collect'
|
||||
'grep:Print lines matching a pattern'
|
||||
'help:Display help about a command'
|
||||
'info:Get info about the manifest'
|
||||
'init:Initialize a repo client'
|
||||
'list:List projects and their names'
|
||||
'manifest:Manifest management'
|
||||
'overview:Display overview of topic branches'
|
||||
'prune:Prune topic branches'
|
||||
'rebase:Rebase local branches'
|
||||
'selfupdate:Update repo'
|
||||
'smartsync:Update working tree to latest known good revision'
|
||||
'stage:Stage file(s) for commit'
|
||||
'start:Start a new branch for development'
|
||||
'status:Show the working tree status'
|
||||
'sync:Update working tree'
|
||||
'upload:Upload changes to the review server'
|
||||
'version:Display version'
|
||||
'wipe:Wipe uncommitted changes'
|
||||
)
|
||||
|
||||
local -a global_opts
|
||||
global_opts=(
|
||||
'(-h --help)'{-h,--help}'[Show help message]'
|
||||
'--help-all[Show help message for all commands]'
|
||||
'(-p --paginate)'{-p,--paginate}'[Pipe all output into less]'
|
||||
'--no-pager[Do not pipe output into a pager]'
|
||||
'--color=[Control color output]:color:(always never auto)'
|
||||
'--trace[Trace git command execution]'
|
||||
'--trace-to-stderr[Trace git command execution to stderr]'
|
||||
'--trace-python[Trace python execution]'
|
||||
'--time[Time execute of command]'
|
||||
'--version[Show version]'
|
||||
'--show-toplevel[Show top level of workspace]'
|
||||
'--event-log=[File to log events to]:file:_files'
|
||||
'--git-trace2-event-log=[File to log git trace2 events to]:file:_files'
|
||||
'--submanifest-path=[Path to submanifest]:path:_files'
|
||||
)
|
||||
|
||||
_arguments -C \
|
||||
${global_opts} \
|
||||
'1: :->cmds' \
|
||||
'*:: :->subcmds'
|
||||
|
||||
case ${state} in
|
||||
cmds)
|
||||
_describe -t commands 'repo command' subcommands
|
||||
;;
|
||||
subcmds)
|
||||
local cmd=${words[1]}
|
||||
curcontext="${curcontext%:*}-${cmd}:"
|
||||
|
||||
local -a common_opts
|
||||
common_opts=(
|
||||
'(-h --help)'{-h,--help}'[Show help message]'
|
||||
'(-v --verbose)'{-v,--verbose}'[Show verbose messages]'
|
||||
'(-q --quiet)'{-q,--quiet}'[Show only errors]'
|
||||
'(-j --jobs)'{-j,--jobs=}'[Number of jobs to run in parallel]:jobs:'
|
||||
'--outer-manifest[Use outer manifest]'
|
||||
'--no-outer-manifest[Do not use outer manifest]'
|
||||
'--this-manifest-only[Only use this manifest]'
|
||||
'--no-this-manifest-only[Do not only use this manifest]'
|
||||
'--all-manifests[Use all manifests]'
|
||||
)
|
||||
|
||||
case ${cmd} in
|
||||
abandon)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'--all[Abandon all branches]' \
|
||||
'1: :->branch' \
|
||||
'*: :->project'
|
||||
;;
|
||||
branches)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'*: :->project'
|
||||
;;
|
||||
checkout)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'1: :->branch' \
|
||||
'*: :->project'
|
||||
;;
|
||||
cherry-pick)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'1: :_message "sha1"'
|
||||
;;
|
||||
diff)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'(-u --absolute)'{-u,--absolute}'[Show absolute paths]' \
|
||||
'*: :->project'
|
||||
;;
|
||||
diffmanifests)
|
||||
_arguments \
|
||||
'--raw[Show raw diff]' \
|
||||
'--no-color[Do not show color]' \
|
||||
'--pretty-format=[Pretty format]:format:' \
|
||||
'1: :_files -g "*.xml"' \
|
||||
'2: :_files -g "*.xml"'
|
||||
;;
|
||||
download)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'(-b --branch)'{-b,--branch=}'[One or more branches to check]:branch:' \
|
||||
'(-c --cherry-pick)'{-c,--cherry-pick}'[Cherry-pick the change]' \
|
||||
'(-x --record-origin)'{-x,--record-origin}'[Record origin of cherry-pick]' \
|
||||
'(-r --revert)'{-r,--revert}'[Revert the change]' \
|
||||
'(-f --ff-only)'{-f,--ff-only}'[Force fast-forward]' \
|
||||
'*: :_message "change[/patchset]"'
|
||||
;;
|
||||
forall)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'(-r --regex)'{-r,--regex}'[Execute command only on projects matching regex]' \
|
||||
'(-i --inverse-regex)'{-i,--inverse-regex}'[Execute command only on projects not matching regex]' \
|
||||
'(-g --groups)'{-g,--groups=}'[Execute command only on projects matching groups]:groups:' \
|
||||
'(-c --command)'{-c,--command}'[Command to execute]' \
|
||||
'(-e --abort-on-errors)'{-e,--abort-on-errors}'[Abort on errors]' \
|
||||
'--ignore-missing[Ignore missing projects]' \
|
||||
'--interactive[Run interactively]' \
|
||||
'-p[Show project headers]' \
|
||||
'*: :->project'
|
||||
;;
|
||||
gc)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'(-n --dry-run)'{-n,--dry-run}'[Dry run]' \
|
||||
'(-y --yes)'{-y,--yes}'[Answer yes to all prompts]' \
|
||||
'--repack[Repack objects]'
|
||||
;;
|
||||
grep)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'--cached[Search cached files]' \
|
||||
'(-r --revision)'{-r,--revision=}'[Search in revision]:revision:' \
|
||||
'-e[Pattern]' \
|
||||
'(-i --ignore-case)'{-i,--ignore-case}'[Ignore case]' \
|
||||
'(-a --text)'{-a,--text}'[Treat all files as text]' \
|
||||
'-I[Do not match binary files]' \
|
||||
'(-w --word-regexp)'{-w,--word-regexp}'[Match word boundaries]' \
|
||||
'(-v --invert-match)'{-v,--invert-match}'[Invert match]' \
|
||||
'(-G --basic-regexp)'{-G,--basic-regexp}'[Basic regexp]' \
|
||||
'(-E --extended-regexp)'{-E,--extended-regexp}'[Extended regexp]' \
|
||||
'(-F --fixed-strings)'{-F,--fixed-strings}'[Fixed strings]' \
|
||||
'--all-match[All match]' \
|
||||
'--and[And]' \
|
||||
'--or[Or]' \
|
||||
'--not[Not]' \
|
||||
'-([Open paren]' \
|
||||
'-)[Close paren]' \
|
||||
'-n[Line number]' \
|
||||
'-C[Context]:lines:' \
|
||||
'-B[Before context]:lines:' \
|
||||
'-A[After context]:lines:' \
|
||||
'-l[Name only]' \
|
||||
'--name-only[Name only]' \
|
||||
'--files-with-matches[Files with matches]' \
|
||||
'-L[Files without match]' \
|
||||
'--files-without-match[Files without match]' \
|
||||
'1: :->pattern' \
|
||||
'*: :->project'
|
||||
;;
|
||||
help)
|
||||
_arguments \
|
||||
'(-a --all)'{-a,--all}'[Show all commands]' \
|
||||
'--help-all[Show help message for all commands]' \
|
||||
'1: :->help_cmds'
|
||||
;;
|
||||
info)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'(-d --diff)'{-d,--diff}'[Show diff]' \
|
||||
'(-o --overview)'{-o,--overview}'[Show overview]' \
|
||||
'(-c --current-branch)'{-c,--current-branch}'[Show current branch]' \
|
||||
'--no-current-branch[Do not show current branch]' \
|
||||
'(-l --local-only)'{-l,--local-only}'[Local only]' \
|
||||
'*: :->project'
|
||||
;;
|
||||
init)
|
||||
_arguments \
|
||||
'(-u --manifest-url)'{-u,--manifest-url=}'[Manifest URL]:url:' \
|
||||
'(-b --manifest-branch)'{-b,--manifest-branch=}'[Manifest branch]:branch:' \
|
||||
'--manifest-upstream-branch=[Manifest upstream branch]:branch:' \
|
||||
'(-m --manifest-name)'{-m,--manifest-name=}'[Manifest name]:file:_files -g "*.xml"' \
|
||||
'(-g --groups)'{-g,--groups=}'[Restrict manifest projects to groups]:groups:' \
|
||||
'(-p --platform)'{-p,--platform=}'[Restrict manifest projects to platform]:platform:' \
|
||||
'--submodules[Sync submodules]' \
|
||||
'--standalone-manifest[Standalone manifest]' \
|
||||
'--manifest-depth=[Manifest depth]:depth:' \
|
||||
'(-c --current-branch)'{-c,--current-branch}'[Sync current branch only]' \
|
||||
'--no-current-branch[Do not sync current branch only]' \
|
||||
'--tags[Sync tags]' \
|
||||
'--no-tags[Do not sync tags]' \
|
||||
'--mirror[Mirror]' \
|
||||
'--archive[Archive]' \
|
||||
'--worktree[Worktree]' \
|
||||
'--reference=[Reference repository]:repository:_files -/' \
|
||||
'--dissociate[Dissociate from reference]' \
|
||||
'--depth=[Depth]:depth:' \
|
||||
'--partial-clone[Partial clone]' \
|
||||
'--no-partial-clone[Do not partial clone]' \
|
||||
'--partial-clone-exclude=[Exclude from partial clone]:projects:' \
|
||||
'--clone-filter=[Clone filter]:filter:' \
|
||||
'--use-superproject[Use superproject]' \
|
||||
'--no-use-superproject[Do not use superproject]' \
|
||||
'--clone-bundle[Use clone bundle]' \
|
||||
'--no-clone-bundle[Do not use clone bundle]' \
|
||||
'--git-lfs[Use git lfs]' \
|
||||
'--no-git-lfs[Do not use git lfs]' \
|
||||
'--repo-url=[Repo URL]:url:' \
|
||||
'--repo-rev=[Repo revision]:revision:' \
|
||||
'--no-repo-verify[Do not verify repo]' \
|
||||
'--config-name[Use config name]'
|
||||
;;
|
||||
list)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'(-r --regex)'{-r,--regex}'[Filter by regex]' \
|
||||
'(-g --groups)'{-g,--groups=}'[Filter by groups]:groups:' \
|
||||
'(-a --all)'{-a,--all}'[Show all projects]' \
|
||||
'(-n --name-only)'{-n,--name-only}'[Show only names]' \
|
||||
'(-p --path-only)'{-p,--path-only}'[Show only paths]' \
|
||||
'(-f --fullpath)'{-f,--fullpath}'[Show full paths]' \
|
||||
'--relative-to=[Show paths relative to]:path:_files -/' \
|
||||
'*: :->project'
|
||||
;;
|
||||
manifest)
|
||||
_arguments \
|
||||
'(-r --revision-as-HEAD)'{-r,--revision-as-HEAD}'[Save revisions as current HEAD]' \
|
||||
'(-m --manifest-name)'{-m,--manifest-name=}'[Manifest name]:file:_files -g "*.xml"' \
|
||||
'--suppress-upstream-revision[Suppress upstream revision]' \
|
||||
'--suppress-dest-branch[Suppress dest branch]' \
|
||||
'--format=[Output format]:format:(xml json)' \
|
||||
'--pretty[Pretty print]' \
|
||||
'--no-local-manifests[Ignore local manifests]' \
|
||||
'(-o --output-file)'{-o,--output-file=}'[Output file]:file:_files'
|
||||
;;
|
||||
overview)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'(-c --current-branch)'{-c,--current-branch}'[Show current branch]' \
|
||||
'--no-current-branch[Do not show current branch]' \
|
||||
'*: :->project'
|
||||
;;
|
||||
prune)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'*: :->project'
|
||||
;;
|
||||
rebase)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'--fail-fast[Fail fast]' \
|
||||
'(-f --force-rebase)'{-f,--force-rebase}'[Force rebase]' \
|
||||
'--no-ff[No fast forward]' \
|
||||
'--autosquash[Autosquash]' \
|
||||
'--whitespace=[Whitespace option]:option:' \
|
||||
'--auto-stash[Auto stash]' \
|
||||
'(-m --onto-manifest)'{-m,--onto-manifest}'[Rebase onto manifest]' \
|
||||
'(-i --interactive)'{-i,--interactive}'[Interactive rebase]' \
|
||||
'*: :->project'
|
||||
;;
|
||||
selfupdate)
|
||||
_arguments \
|
||||
'--no-repo-verify[Do not verify repo]'
|
||||
;;
|
||||
smartsync | sync)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'--jobs-network=[Number of network jobs]:jobs:' \
|
||||
'--jobs-checkout=[Number of checkout jobs]:jobs:' \
|
||||
'(-f --force-broken)'{-f,--force-broken}'[Continue sync even if a project fails]' \
|
||||
'--fail-fast[Fail fast]' \
|
||||
'--force-sync[Overwrite existing git directories]' \
|
||||
'--force-checkout[Force checkout]' \
|
||||
'--force-remove-dirty[Force remove dirty]' \
|
||||
'--rebase[Rebase local branches]' \
|
||||
'(-l --local-only)'{-l,--local-only}'[Only use local files]' \
|
||||
'--no-manifest-update[Do not update manifest]' \
|
||||
'--nmu[Do not update manifest]' \
|
||||
'--interleaved[Interleave output]' \
|
||||
'--no-interleaved[Do not interleave output]' \
|
||||
'(-n --network-only)'{-n,--network-only}'[Only fetch]' \
|
||||
'(-d --detach)'{-d,--detach}'[Detach HEAD]' \
|
||||
'(-c --current-branch)'{-c,--current-branch}'[Fetch only current branch]' \
|
||||
'--no-current-branch[Do not fetch only current branch]' \
|
||||
'(-m --manifest-name)'{-m,--manifest-name=}'[Manifest name]:file:_files -g "*.xml"' \
|
||||
'--clone-bundle[Use clone bundle]' \
|
||||
'--no-clone-bundle[Do not use clone bundle]' \
|
||||
'(-u --manifest-server-username)'{-u,--manifest-server-username=}'[Username for manifest server]:username:' \
|
||||
'(-p --manifest-server-password)'{-p,--manifest-server-password=}'[Password for manifest server]:password:' \
|
||||
'--fetch-submodules[Fetch submodules]' \
|
||||
'--use-superproject[Use superproject]' \
|
||||
'--no-use-superproject[Do not use superproject]' \
|
||||
'--tags[Sync tags]' \
|
||||
'--no-tags[Do not sync tags]' \
|
||||
'--optimized-fetch[Optimized fetch]' \
|
||||
'--retry-fetches=[Retry fetches]:count:' \
|
||||
'--prune[Prune]' \
|
||||
'--no-prune[Do not prune]' \
|
||||
'--auto-gc[Run auto gc]' \
|
||||
'--no-auto-gc[Do not run auto gc]' \
|
||||
'(-s --smart-sync)'{-s,--smart-sync}'[Smart sync]' \
|
||||
'(-t --smart-tag)'{-t,--smart-tag=}'[Smart tag]:tag:' \
|
||||
'--no-repo-verify[Do not verify repo]' \
|
||||
'--no-verify[Do not verify]' \
|
||||
'--verify[Verify]' \
|
||||
'--ignore-hooks[Ignore hooks]' \
|
||||
'*: :->project'
|
||||
;;
|
||||
stage)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'(-i --interactive)'{-i,--interactive}'[Interactive staging]' \
|
||||
'*: :->project'
|
||||
;;
|
||||
start)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'--all[Start branch in all projects]' \
|
||||
'(-r --rev --revision)'{-r,--rev=,--revision=}'[Revision]:revision:' \
|
||||
'--head[Head]' \
|
||||
'--HEAD[Head]' \
|
||||
'1: :->newbranch' \
|
||||
'*: :->project'
|
||||
;;
|
||||
status)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'(-o --orphans)'{-o,--orphans}'[Show orphans]' \
|
||||
'*: :->project'
|
||||
;;
|
||||
upload)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'(-t --topic-branch)'{-t,--topic-branch}'[Topic branch]' \
|
||||
'--topic=[Topic]:topic:' \
|
||||
'--hashtag=[Hashtag]:hashtag:' \
|
||||
'--ht=[Hashtag]:hashtag:' \
|
||||
'--hashtag-branch[Hashtag branch]' \
|
||||
'--htb[Hashtag branch]' \
|
||||
'(-l --label)'{-l,--label=}'[Label]:label:' \
|
||||
'--pd=[Patchset description]:description:' \
|
||||
'--patchset-description=[Patchset description]:description:' \
|
||||
'--re=[Reviewers]:reviewers:' \
|
||||
'--reviewers=[Reviewers]:reviewers:' \
|
||||
'--cc=[CC]:cc:' \
|
||||
'--br=[Branch]:branch:' \
|
||||
'--branch=[Branch]:branch:' \
|
||||
'(-c --current-branch)'{-c,--current-branch}'[Upload current branch]' \
|
||||
'--no-current-branch[Do not upload current branch]' \
|
||||
'--ne[No emails]' \
|
||||
'--no-emails[No emails]' \
|
||||
'(-p --private)'{-p,--private}'[Private]' \
|
||||
'(-w --wip)'{-w,--wip}'[Work in progress]' \
|
||||
'(-r --ready)'{-r,--ready}'[Ready]' \
|
||||
'(-o --push-option)'{-o,--push-option=}'[Push option]:option:' \
|
||||
'(-D --destination --dest)'{-D,--destination=,--dest=}'[Destination]:destination:' \
|
||||
'(-n --dry-run)'{-n,--dry-run}'[Dry run]' \
|
||||
'(-y --yes)'{-y,--yes}'[Answer yes to all prompts]' \
|
||||
'--ignore-untracked-files[Ignore untracked files]' \
|
||||
'--no-ignore-untracked-files[Do not ignore untracked files]' \
|
||||
'--no-cert-checks[Do not check certificates]' \
|
||||
'--no-verify[Do not verify]' \
|
||||
'--verify[Verify]' \
|
||||
'--ignore-hooks[Ignore hooks]' \
|
||||
'*: :->project'
|
||||
;;
|
||||
version)
|
||||
_arguments ${common_opts}
|
||||
;;
|
||||
wipe)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'(-f --force)'{-f,--force}'[Force wipe]' \
|
||||
'--force-uncommitted[Force uncommitted]' \
|
||||
'--force-shared[Force shared]' \
|
||||
'*: :->project'
|
||||
;;
|
||||
esac
|
||||
|
||||
# Handle states for positional arguments.
|
||||
case ${state} in
|
||||
branch)
|
||||
[[ ${PREFIX} != -* ]] && _repo_branches
|
||||
;;
|
||||
project)
|
||||
[[ ${PREFIX} != -* ]] && _repo_projects
|
||||
;;
|
||||
pattern)
|
||||
_message 'pattern'
|
||||
;;
|
||||
newbranch)
|
||||
_message 'new branch name'
|
||||
;;
|
||||
help_cmds)
|
||||
_describe -t commands 'repo command' subcommands
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_repo_branches() {
|
||||
local -a branches
|
||||
branches=(${(f)"$(_call_program branches repo branches 2>/dev/null | sed -E 's/^.*[[:space:]]([^[:space:]]+)[[:space:]]+\|.*$/\1/' | awk '{print $1}')"})
|
||||
_describe -t branches 'branch' branches
|
||||
}
|
||||
|
||||
_repo_projects() {
|
||||
local -a projects
|
||||
projects=(${(f)"$(_call_program projects repo list -n 2>/dev/null)"})
|
||||
_describe -t projects 'project' projects
|
||||
}
|
||||
|
||||
_repo "$@"
|
||||
+2
-1
@@ -1 +1,2 @@
|
||||
black<24
|
||||
# NB: Keep in sync with run_tests.vpython3.
|
||||
black<26
|
||||
|
||||
@@ -141,7 +141,7 @@ Instead, you should use standard Git workflows like [git worktree] or
|
||||
(e.g. a local mirror & a public review server) while avoiding duplicating
|
||||
the content. However, this can run into problems if different remotes use
|
||||
the same path on their respective servers. Best to avoid that.
|
||||
* `subprojects/`: Like `projects/`, but for git submodules.
|
||||
* `modules/`: Like `projects/`, but for git submodules.
|
||||
* `subproject-objects/`: Like `project-objects/`, but for git submodules.
|
||||
* `worktrees/`: Bare checkouts of every project synced by the manifest. The
|
||||
filesystem layout matches the `<project name=...` setting in the manifest
|
||||
|
||||
+99
-60
@@ -51,6 +51,7 @@ following DTD:
|
||||
<!ATTLIST default dest-branch CDATA #IMPLIED>
|
||||
<!ATTLIST default upstream CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-j CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-j-max CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-c CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-s CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-tags CDATA #IMPLIED>
|
||||
@@ -59,7 +60,7 @@ following DTD:
|
||||
<!ATTLIST manifest-server url CDATA #REQUIRED>
|
||||
|
||||
<!ELEMENT submanifest EMPTY>
|
||||
<!ATTLIST submanifest name ID #REQUIRED>
|
||||
<!ATTLIST submanifest name ID #REQUIRED>
|
||||
<!ATTLIST submanifest remote IDREF #IMPLIED>
|
||||
<!ATTLIST submanifest project CDATA #IMPLIED>
|
||||
<!ATTLIST submanifest manifest-name CDATA #IMPLIED>
|
||||
@@ -72,18 +73,19 @@ following DTD:
|
||||
project*,
|
||||
copyfile*,
|
||||
linkfile*)>
|
||||
<!ATTLIST project name CDATA #REQUIRED>
|
||||
<!ATTLIST project path CDATA #IMPLIED>
|
||||
<!ATTLIST project remote IDREF #IMPLIED>
|
||||
<!ATTLIST project revision CDATA #IMPLIED>
|
||||
<!ATTLIST project dest-branch CDATA #IMPLIED>
|
||||
<!ATTLIST project groups CDATA #IMPLIED>
|
||||
<!ATTLIST project sync-c CDATA #IMPLIED>
|
||||
<!ATTLIST project sync-s CDATA #IMPLIED>
|
||||
<!ATTLIST project sync-tags CDATA #IMPLIED>
|
||||
<!ATTLIST project upstream CDATA #IMPLIED>
|
||||
<!ATTLIST project clone-depth CDATA #IMPLIED>
|
||||
<!ATTLIST project force-path CDATA #IMPLIED>
|
||||
<!ATTLIST project name CDATA #REQUIRED>
|
||||
<!ATTLIST project path CDATA #IMPLIED>
|
||||
<!ATTLIST project remote IDREF #IMPLIED>
|
||||
<!ATTLIST project revision CDATA #IMPLIED>
|
||||
<!ATTLIST project dest-branch CDATA #IMPLIED>
|
||||
<!ATTLIST project groups CDATA #IMPLIED>
|
||||
<!ATTLIST project sync-c CDATA #IMPLIED>
|
||||
<!ATTLIST project sync-s CDATA #IMPLIED>
|
||||
<!ATTLIST project sync-tags CDATA #IMPLIED>
|
||||
<!ATTLIST project upstream CDATA #IMPLIED>
|
||||
<!ATTLIST project clone-depth CDATA #IMPLIED>
|
||||
<!ATTLIST project force-path CDATA #IMPLIED>
|
||||
<!ATTLIST project sync-strategy CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT annotation EMPTY>
|
||||
<!ATTLIST annotation name CDATA #REQUIRED>
|
||||
@@ -95,26 +97,30 @@ following DTD:
|
||||
<!ATTLIST copyfile dest CDATA #REQUIRED>
|
||||
|
||||
<!ELEMENT linkfile EMPTY>
|
||||
<!ATTLIST linkfile src CDATA #REQUIRED>
|
||||
<!ATTLIST linkfile src CDATA #REQUIRED>
|
||||
<!ATTLIST linkfile dest CDATA #REQUIRED>
|
||||
|
||||
<!ELEMENT extend-project EMPTY>
|
||||
<!ATTLIST extend-project name CDATA #REQUIRED>
|
||||
<!ATTLIST extend-project path CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project dest-path CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project groups CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project revision CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project remote CDATA #IMPLIED>
|
||||
<!ELEMENT extend-project (annotation*,
|
||||
copyfile*,
|
||||
linkfile*)>
|
||||
<!ATTLIST extend-project name CDATA #REQUIRED>
|
||||
<!ATTLIST extend-project path CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project dest-path CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project groups CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project revision CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project remote CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project dest-branch CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project upstream CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project upstream CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project base-rev CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT remove-project EMPTY>
|
||||
<!ATTLIST remove-project name CDATA #IMPLIED>
|
||||
<!ATTLIST remove-project path CDATA #IMPLIED>
|
||||
<!ATTLIST remove-project optional CDATA #IMPLIED>
|
||||
<!ATTLIST remove-project base-rev CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT repo-hooks EMPTY>
|
||||
<!ATTLIST repo-hooks in-project CDATA #REQUIRED>
|
||||
<!ATTLIST repo-hooks in-project CDATA #REQUIRED>
|
||||
<!ATTLIST repo-hooks enabled-list CDATA #REQUIRED>
|
||||
|
||||
<!ELEMENT superproject EMPTY>
|
||||
@@ -123,7 +129,7 @@ following DTD:
|
||||
<!ATTLIST superproject revision CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT contactinfo EMPTY>
|
||||
<!ATTLIST contactinfo bugurl CDATA #REQUIRED>
|
||||
<!ATTLIST contactinfo bugurl CDATA #REQUIRED>
|
||||
|
||||
<!ELEMENT include EMPTY>
|
||||
<!ATTLIST include name CDATA #REQUIRED>
|
||||
@@ -209,7 +215,9 @@ can be found. Used when syncing a revision locked manifest in
|
||||
-c mode to avoid having to sync the entire ref space. Project elements
|
||||
not setting their own `upstream` will inherit this value.
|
||||
|
||||
Attribute `sync-j`: Number of parallel jobs to use when synching.
|
||||
Attribute `sync-j`: Number of parallel jobs to use when syncing.
|
||||
|
||||
Attribute `sync-j-max`: Maximum number of parallel jobs to use when syncing.
|
||||
|
||||
Attribute `sync-c`: Set to true to only sync the given Git
|
||||
branch (specified in the `revision` attribute) rather than the
|
||||
@@ -229,26 +237,7 @@ At most one manifest-server may be specified. The url attribute
|
||||
is used to specify the URL of a manifest server, which is an
|
||||
XML RPC service.
|
||||
|
||||
The manifest server should implement the following RPC methods:
|
||||
|
||||
GetApprovedManifest(branch, target)
|
||||
|
||||
Return a manifest in which each project is pegged to a known good revision
|
||||
for the current branch and target. This is used by repo sync when the
|
||||
--smart-sync option is given.
|
||||
|
||||
The target to use is defined by environment variables TARGET_PRODUCT
|
||||
and TARGET_BUILD_VARIANT. These variables are used to create a string
|
||||
of the form $TARGET_PRODUCT-$TARGET_BUILD_VARIANT, e.g. passion-userdebug.
|
||||
If one of those variables or both are not present, the program will call
|
||||
GetApprovedManifest without the target parameter and the manifest server
|
||||
should choose a reasonable default target.
|
||||
|
||||
GetManifest(tag)
|
||||
|
||||
Return a manifest in which each project is pegged to the revision at
|
||||
the specified tag. This is used by repo sync when the --smart-tag option
|
||||
is given.
|
||||
See the [smart sync documentation](./smart-sync.md) for more details.
|
||||
|
||||
|
||||
### Element submanifest
|
||||
@@ -302,7 +291,7 @@ should be placed. If not supplied, `revision` is used.
|
||||
|
||||
`path` may not be an absolute path or use "." or ".." path components.
|
||||
|
||||
Attribute `groups`: List of additional groups to which all projects
|
||||
Attribute `groups`: Set of additional groups to which all projects
|
||||
in the included submanifest belong. This appends and recurses, meaning
|
||||
all projects in submanifests carry all parent submanifest groups.
|
||||
Same syntax as the corresponding element of `project`.
|
||||
@@ -370,7 +359,7 @@ When using `repo upload`, changes will be submitted for code
|
||||
review on this branch. If unspecified both here and in the
|
||||
default element, `revision` is used instead.
|
||||
|
||||
Attribute `groups`: List of groups to which this project belongs,
|
||||
Attribute `groups`: Set of groups to which this project belongs,
|
||||
whitespace or comma separated. All projects belong to the group
|
||||
"all", and each project automatically belongs to a group of
|
||||
its name:`name` and path:`path`. E.g. for
|
||||
@@ -401,6 +390,22 @@ rather than the `name` attribute. This attribute only applies to the
|
||||
local mirrors syncing, it will be ignored when syncing the projects in a
|
||||
client working directory.
|
||||
|
||||
Attribute `sync-strategy`: Set the sync strategy used when fetching this
|
||||
project. Currently the only supported value is `stateless`. When set to
|
||||
`stateless`, repo will run a reflog expiration and aggressive garbage collection
|
||||
at the end of the sync process. This is useful for projects that contain
|
||||
large binary files and use `clone-depth="1"`, where garbage can accumulate
|
||||
as binaries are added, deleted, or modified across successive syncs.
|
||||
|
||||
During a stateless sync, repo checks the following before cleaning up:
|
||||
1. The project does not share an object directory with other projects.
|
||||
2. The working tree is clean (no uncommitted changes, no untracked files).
|
||||
3. There are no unpushed local commits.
|
||||
4. There is no Git stash.
|
||||
|
||||
If any of these conditions are not met, repo falls back to a standard
|
||||
sync without garbage collection.
|
||||
|
||||
### Element extend-project
|
||||
|
||||
Modify the attributes of the named project.
|
||||
@@ -410,6 +415,11 @@ attributes of an existing project without completely replacing the
|
||||
existing project definition. This makes the local manifest more robust
|
||||
against changes to the original manifest.
|
||||
|
||||
The `extend-project` element can also contain `annotation`, `copyfile`, and
|
||||
`linkfile` child elements. These are added to the project's definition. A
|
||||
`copyfile` or `linkfile` with a `dest` that already exists in the project
|
||||
will overwrite the original.
|
||||
|
||||
Attribute `path`: If specified, limit the change to projects checked out
|
||||
at the specified path, rather than all projects with the given name.
|
||||
|
||||
@@ -418,7 +428,7 @@ of the repo client where the Git working directory for this project
|
||||
should be placed. This is used to move a project in the checkout by
|
||||
overriding the existing `path` setting.
|
||||
|
||||
Attribute `groups`: List of additional groups to which this project
|
||||
Attribute `groups`: Set of additional groups to which this project
|
||||
belongs. Same syntax as the corresponding element of `project`.
|
||||
|
||||
Attribute `revision`: If specified, overrides the revision of the original
|
||||
@@ -433,22 +443,31 @@ project. Same syntax as the corresponding element of `project`.
|
||||
Attribute `upstream`: If specified, overrides the upstream of the original
|
||||
project. Same syntax as the corresponding element of `project`.
|
||||
|
||||
Attribute `base-rev`: If specified, adds a check against the revision
|
||||
to be extended. Manifest parse will fail and give a list of mismatch extends
|
||||
if the revisions being extended have changed since base-rev was set.
|
||||
Intended for use with layered manifests using hash revisions to prevent
|
||||
patch branches hiding newer upstream revisions. Also compares named refs
|
||||
like branches or tags but is misleading if branches are used as base-rev.
|
||||
Same syntax as the corresponding element of `project`.
|
||||
|
||||
### Element annotation
|
||||
|
||||
Zero or more annotation elements may be specified as children of a
|
||||
project or remote element. Each element describes a name-value pair.
|
||||
For projects, this name-value pair will be exported into each project's
|
||||
environment during a 'forall' command, prefixed with `REPO__`. In addition,
|
||||
there is an optional attribute "keep" which accepts the case insensitive values
|
||||
"true" (default) or "false". This attribute determines whether or not the
|
||||
project element, an extend-project element, or a remote element. Each
|
||||
element describes a name-value pair. For projects, this name-value pair
|
||||
will be exported into each project's environment during a 'forall'
|
||||
command, prefixed with `REPO__`. In addition, there is an optional
|
||||
attribute "keep" which accepts the case insensitive values "true"
|
||||
(default) or "false". This attribute determines whether or not the
|
||||
annotation will be kept when exported with the manifest subcommand.
|
||||
|
||||
### Element copyfile
|
||||
|
||||
Zero or more copyfile elements may be specified as children of a
|
||||
project element. Each element describes a src-dest pair of files;
|
||||
the "src" file will be copied to the "dest" place during `repo sync`
|
||||
command.
|
||||
project element, or an extend-project element. Each element describes a
|
||||
src-dest pair of files; the "src" file will be copied to the "dest"
|
||||
place during `repo sync` command.
|
||||
|
||||
"src" is project relative, "dest" is relative to the top of the tree.
|
||||
Copying from paths outside of the project or to paths outside of the repo
|
||||
@@ -459,10 +478,14 @@ Intermediate paths must not be symlinks either.
|
||||
|
||||
Parent directories of "dest" will be automatically created if missing.
|
||||
|
||||
The files are copied in the order they are specified in the manifests.
|
||||
If multiple elements specify the same source and destination, they will
|
||||
only be applied as one, based on the first occurence. Files are copied
|
||||
before any links specified via linkfile elements are created.
|
||||
|
||||
### Element linkfile
|
||||
|
||||
It's just like copyfile and runs at the same time as copyfile but
|
||||
instead of copying it creates a symlink.
|
||||
It's just like copyfile, but instead of copying it creates a symlink.
|
||||
|
||||
The symlink is created at "dest" (relative to the top of the tree) and
|
||||
points to the path specified by "src" which is a path in the project.
|
||||
@@ -472,6 +495,11 @@ Parent directories of "dest" will be automatically created if missing.
|
||||
The symlink target may be a file or directory, but it may not point outside
|
||||
of the repo client.
|
||||
|
||||
The links are created in the order they are specified in the manifests.
|
||||
If multiple elements specify the same source and destination, they will
|
||||
only be applied as one, based on the first occurence. Links are created
|
||||
after any files specified via copyfile elements are copied.
|
||||
|
||||
### Element remove-project
|
||||
|
||||
Deletes a project from the internal manifest table, possibly
|
||||
@@ -496,6 +524,14 @@ name. Logic otherwise behaves like both are specified.
|
||||
Attribute `optional`: Set to true to ignore remove-project elements with no
|
||||
matching `project` element.
|
||||
|
||||
Attribute `base-rev`: If specified, adds a check against the revision
|
||||
to be removed. Manifest parse will fail and give a list of mismatch removes
|
||||
if the revisions being removed have changed since base-rev was set.
|
||||
Intended for use with layered manifests using hash revisions to prevent
|
||||
patch branches hiding newer upstream revisions. Also compares named refs
|
||||
like branches or tags but is misleading if branches are used as base-rev.
|
||||
Same syntax as the corresponding element of `project`.
|
||||
|
||||
### Element repo-hooks
|
||||
|
||||
NB: See the [practical documentation](./repo-hooks.md) for using repo hooks.
|
||||
@@ -561,13 +597,16 @@ the manifest repository's root.
|
||||
"name" may not be an absolute path or use "." or ".." path components.
|
||||
These restrictions are not enforced for [Local Manifests].
|
||||
|
||||
Attribute `groups`: List of additional groups to which all projects
|
||||
Attribute `groups`: Set of additional groups to which all projects
|
||||
in the included manifest belong. This appends and recurses, meaning
|
||||
all projects in included manifests carry all parent include groups.
|
||||
This also applies to all extend-project elements in the included manifests.
|
||||
Same syntax as the corresponding element of `project`.
|
||||
|
||||
Attribute `revision`: Name of a Git branch (e.g. `main` or `refs/heads/main`)
|
||||
default to which all projects in the included manifest belong.
|
||||
default to which all projects in the included manifest belong. This recurses,
|
||||
meaning it will apply to all projects in all manifests included as a result of
|
||||
this element.
|
||||
|
||||
## Local Manifests {#local-manifests}
|
||||
|
||||
|
||||
@@ -96,6 +96,9 @@ If that tag is valid, then repo will warn and use that commit instead.
|
||||
|
||||
If that tag cannot be verified, it gives up and forces the user to resolve.
|
||||
|
||||
If env variable `REPO_SKIP_SELF_UPDATE` is defined, this will
|
||||
bypass the self update algorithm.
|
||||
|
||||
### Force an update
|
||||
|
||||
The `repo selfupdate` command can be used to force an immediate update.
|
||||
|
||||
@@ -133,3 +133,44 @@ def main(project_list, worktree_list=None, **kwargs):
|
||||
kwargs: Leave this here for forward-compatibility.
|
||||
"""
|
||||
```
|
||||
|
||||
### post-sync
|
||||
|
||||
This hook runs when `repo sync` completes without errors.
|
||||
|
||||
Note: This includes cases where no actual checkout may occur. The hook will still run.
|
||||
For example:
|
||||
- `repo sync -n` performs network fetches only and skips the checkout phase.
|
||||
- `repo sync <project>` only updates the specified project(s).
|
||||
- Partial failures may still result in a successful exit.
|
||||
|
||||
This hook is useful for post-processing tasks such as setting up git hooks,
|
||||
bootstrapping configuration files, or running project initialization logic.
|
||||
|
||||
The hook is defined using the existing `<repo-hooks>` manifest block and is
|
||||
optional. If the hook script fails or is missing, `repo sync` will still
|
||||
complete successfully, and the error will be printed as a warning.
|
||||
|
||||
Example:
|
||||
|
||||
```xml
|
||||
<project name="myorg/dev-tools" path="tools" revision="main" />
|
||||
<repo-hooks in-project="myorg/dev-tools" enabled-list="post-sync">
|
||||
<hook name="post-sync" />
|
||||
</repo-hooks>
|
||||
```
|
||||
|
||||
The `post-sync.py` file should be defined like:
|
||||
|
||||
```py
|
||||
def main(repo_topdir=None, sync_duration_seconds=None, **kwargs):
|
||||
"""Main function invoked directly by repo.
|
||||
|
||||
We must use the name "main" as that is what repo requires.
|
||||
|
||||
Args:
|
||||
repo_topdir: The absolute path to the top-level directory of the repo workspace.
|
||||
sync_duration_seconds: The duration of the sync operation in seconds.
|
||||
kwargs: Leave this here for forward-compatibility.
|
||||
"""
|
||||
```
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# repo Smart Syncing
|
||||
|
||||
Repo normally fetches & syncs manifests from the same URL specified during
|
||||
`repo init`, and that often fetches the latest revisions of all projects in
|
||||
the manifest. This flow works well for tracking and developing with the
|
||||
latest code, but often it's desirable to sync to other points. For example,
|
||||
to get a local build matching a specific release or build to reproduce bugs
|
||||
reported by other people.
|
||||
|
||||
Repo's sync subcommand has support for fetching manifests from a server over
|
||||
an XML-RPC connection. The local configuration and network API are defined by
|
||||
repo, but individual projects have to host their own server for the client to
|
||||
communicate with.
|
||||
|
||||
This process is called "smart syncing" -- instead of blindly fetching the latest
|
||||
revision of all projects and getting an unknown state to develop against, the
|
||||
client passes a request to the server and is given a matching manifest that
|
||||
typically specifies specific commits for every project to fetch a known source
|
||||
state.
|
||||
|
||||
[TOC]
|
||||
|
||||
## Manifest Configuration
|
||||
|
||||
The manifest specifies the server to communicate with via the
|
||||
the [`<manifest-server>` element](manifest-format.md#Element-manifest_server)
|
||||
element. This is how the client knows what service to talk to.
|
||||
|
||||
```xml
|
||||
<manifest-server url="https://example.com/your/manifest/server/url" />
|
||||
```
|
||||
|
||||
If the URL starts with `persistent-`, then the
|
||||
[`git-remote-persistent-https` helper](https://github.com/git/git/blob/HEAD/contrib/persistent-https/README)
|
||||
is used to communicate with the server.
|
||||
|
||||
## Credentials
|
||||
|
||||
Credentials may be specified directly in typical `username:password`
|
||||
[URI syntax](https://en.wikipedia.org/wiki/URI#Syntax) in the
|
||||
`<manifest-server>` element directly in the manifest.
|
||||
|
||||
If they are not specified, `repo sync` has `--manifest-server-username=USERNAME`
|
||||
and `--manifest-server-password=PASSWORD` options.
|
||||
|
||||
If those are not used, then repo will look up the host in your
|
||||
[`~/.netrc`](https://docs.python.org/3/library/netrc.html) database.
|
||||
|
||||
When making the connection, cookies matching the host are automatically loaded
|
||||
from the cookiejar specified in
|
||||
[Git's `http.cookiefile` setting](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpcookieFile).
|
||||
|
||||
## Manifest Server
|
||||
|
||||
Unfortunately, there are no public reference implementations. Google has an
|
||||
internal one for Android, but it is written using Google's internal systems,
|
||||
so wouldn't be that helpful as a reference.
|
||||
|
||||
That said, the XML-RPC API is pretty simple, so any standard XML-RPC server
|
||||
example would do. Google's internal server uses Python's
|
||||
[xmlrpc.server.SimpleXMLRPCDispatcher](https://docs.python.org/3/library/xmlrpc.server.html).
|
||||
|
||||
## Network API
|
||||
|
||||
The manifest server should implement the following RPC methods.
|
||||
|
||||
### GetApprovedManifest
|
||||
|
||||
> `GetApprovedManifest(branch: str, target: Optional[str]) -> str`
|
||||
|
||||
The meaning of `branch` and `target` is not strictly defined. The server may
|
||||
interpret them however it wants. The recommended interpretation is that the
|
||||
`branch` matches the manifest branch, and `target` is an identifier for your
|
||||
project that matches something users would build.
|
||||
|
||||
See the client section below for how repo typically generates these values.
|
||||
|
||||
The server will return a manifest or an error. If it's an error, repo will
|
||||
show the output directly to the user to provide a limited feedback channel.
|
||||
|
||||
If the user's request is ambiguous and could match multiple manifests, the
|
||||
server has to decide whether to pick one automatically (and silently such that
|
||||
the user won't know there were multiple matches), or return an error and force
|
||||
the user to be more specific.
|
||||
|
||||
### GetManifest
|
||||
|
||||
> `GetManifest(tag: str) -> str`
|
||||
|
||||
The meaning of `tag` is not strictly defined. Projects are encouraged to use
|
||||
a system where the tag matches a unique source state.
|
||||
|
||||
See the client section below for how repo typically generates these values.
|
||||
|
||||
The server will return a manifest or an error. If it's an error, repo will
|
||||
show the output directly to the user to provide a limited feedback channel.
|
||||
|
||||
If the user's request is ambiguous and could match multiple manifests, the
|
||||
server has to decide whether to pick one automatically (and silently such that
|
||||
the user won't know there were multiple matches), or return an error and force
|
||||
the user to be more specific.
|
||||
|
||||
## Client Options
|
||||
|
||||
Once repo has successfully downloaded the manifest from the server, it saves a
|
||||
copy into `.repo/manifests/smart_sync_override.xml` so users can examine it.
|
||||
The next time `repo sync` is run, this file is automatically replaced or removed
|
||||
based on the current set of options.
|
||||
|
||||
### --smart-sync
|
||||
|
||||
Repo will call `GetApprovedManifest(branch[, target])`.
|
||||
|
||||
The `branch` is determined by the current manifest branch as specified by
|
||||
`--manifest-branch=BRANCH` when running `repo init`.
|
||||
|
||||
The `target` is defined by environment variables in the order below. If none
|
||||
of them match, then `target` is omitted. These variables were decided as they
|
||||
match the settings Android build environments automatically setup.
|
||||
|
||||
1. `${SYNC_TARGET}`: If defined, the value is used directly.
|
||||
2. `${TARGET_PRODUCT}-${TARGET_RELEASE}-${TARGET_BUILD_VARIANT}`: If these
|
||||
variables are all defined, then they are merged with `-` and used.
|
||||
3. `${TARGET_PRODUCT}-${TARGET_BUILD_VARIANT}`: If these variables are all
|
||||
defined, then they are merged with `-` and used.
|
||||
|
||||
### --smart-tag=TAG
|
||||
|
||||
Repo will call `GetManifest(TAG)`.
|
||||
+6
-3
@@ -50,8 +50,11 @@ Git worktrees (see the previous section for more info).
|
||||
Repo will use symlinks heavily internally.
|
||||
On *NIX platforms, this isn't an issue, but Windows makes it a bit difficult.
|
||||
|
||||
There are some documents out there for how to do this, but usually the easiest
|
||||
answer is to run your shell as an Administrator and invoke repo/git in that.
|
||||
The easiest method to allow users to create symlinks is by enabling
|
||||
[Windows Developer Mode](https://learn.microsoft.com/en-us/windows/advanced-settings/developer-mode).
|
||||
|
||||
The next easiest answer is to run your shell as an Administrator and invoke
|
||||
repo/git in that.
|
||||
|
||||
This isn't a great solution, but Windows doesn't make this easy, so here we are.
|
||||
|
||||
@@ -142,7 +145,7 @@ You will have to specify this flag every time you upload.
|
||||
|
||||
[mintty]: https://github.com/mintty/mintty/issues/56
|
||||
|
||||
### repohooks always fail with an close_fds error.
|
||||
### repohooks always fail with an close_fds error
|
||||
|
||||
When using the [reference repohooks project][repohooks] included in AOSP,
|
||||
you might see errors like this when running `repo upload`:
|
||||
|
||||
@@ -107,8 +107,8 @@ class GitError(RepoError):
|
||||
return self.message
|
||||
|
||||
|
||||
class GitcUnsupportedError(RepoExitError):
|
||||
"""Gitc no longer supported."""
|
||||
class GitAuthError(RepoExitError):
|
||||
"""Cannot talk to remote due to auth issue."""
|
||||
|
||||
|
||||
class UploadError(RepoError):
|
||||
|
||||
+10
-2
@@ -168,8 +168,10 @@ class EventLog:
|
||||
f.write("\n")
|
||||
|
||||
|
||||
# An integer id that is unique across this invocation of the program.
|
||||
_EVENT_ID = multiprocessing.Value("i", 1)
|
||||
# An integer id that is unique across this invocation of the program, to be set
|
||||
# by the first Add event. We can't set it here since it results in leaked
|
||||
# resources (see: https://issues.gerritcodereview.com/353656374).
|
||||
_EVENT_ID = None
|
||||
|
||||
|
||||
def _NextEventId():
|
||||
@@ -178,6 +180,12 @@ def _NextEventId():
|
||||
Returns:
|
||||
A unique, to this invocation of the program, integer id.
|
||||
"""
|
||||
global _EVENT_ID
|
||||
if _EVENT_ID is None:
|
||||
# There is a small chance of race condition - two parallel processes
|
||||
# setting up _EVENT_ID. However, we expect TASK_COMMAND to happen before
|
||||
# mp kicks in.
|
||||
_EVENT_ID = multiprocessing.Value("i", 1)
|
||||
with _EVENT_ID.get_lock():
|
||||
val = _EVENT_ID.value
|
||||
_EVENT_ID.value += 1
|
||||
|
||||
@@ -34,7 +34,7 @@ def fetch_file(url, verbose=False):
|
||||
"""
|
||||
scheme = urlparse(url).scheme
|
||||
if scheme == "gs":
|
||||
cmd = ["gsutil", "cat", url]
|
||||
cmd = ["gcloud", "storage", "cat", url]
|
||||
errors = []
|
||||
try:
|
||||
result = subprocess.run(
|
||||
@@ -42,7 +42,7 @@ def fetch_file(url, verbose=False):
|
||||
)
|
||||
if result.stderr and verbose:
|
||||
print(
|
||||
'warning: non-fatal error running "gsutil": %s'
|
||||
'warning: non-fatal error running "gcloud storage": %s'
|
||||
% result.stderr,
|
||||
file=sys.stderr,
|
||||
)
|
||||
@@ -50,7 +50,8 @@ def fetch_file(url, verbose=False):
|
||||
except subprocess.CalledProcessError as e:
|
||||
errors.append(e)
|
||||
print(
|
||||
'fatal: error running "gsutil": %s' % e.stderr, file=sys.stderr
|
||||
'fatal: error running "gcloud storage": %s' % e.stderr,
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise FetchFileError(aggregate_errors=errors)
|
||||
with urlopen(url) as f:
|
||||
|
||||
+19
-17
@@ -22,7 +22,6 @@ from typing import Any, Optional
|
||||
|
||||
from error import GitError
|
||||
from error import RepoExitError
|
||||
from git_refs import HEAD
|
||||
from git_trace2_event_log_base import BaseEventLog
|
||||
import platform_utils
|
||||
from repo_logging import RepoLogger
|
||||
@@ -48,7 +47,7 @@ logger = RepoLogger(__file__)
|
||||
|
||||
|
||||
class _GitCall:
|
||||
@functools.lru_cache(maxsize=None)
|
||||
@functools.lru_cache(maxsize=None) # noqa: B019
|
||||
def version_tuple(self):
|
||||
ret = Wrapper().ParseGitVersion()
|
||||
if ret is None:
|
||||
@@ -83,7 +82,7 @@ def RepoSourceVersion():
|
||||
proj = os.path.dirname(os.path.abspath(__file__))
|
||||
env[GIT_DIR] = os.path.join(proj, ".git")
|
||||
result = subprocess.run(
|
||||
[GIT, "describe", HEAD],
|
||||
[GIT, "describe", "HEAD"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
encoding="utf-8",
|
||||
@@ -96,7 +95,7 @@ def RepoSourceVersion():
|
||||
ver = ver[1:]
|
||||
else:
|
||||
ver = "unknown"
|
||||
setattr(RepoSourceVersion, "version", ver)
|
||||
RepoSourceVersion.version = ver
|
||||
|
||||
return ver
|
||||
|
||||
@@ -238,9 +237,9 @@ def _build_env(
|
||||
s = p + " " + s
|
||||
env["GIT_CONFIG_PARAMETERS"] = s
|
||||
if "GIT_ALLOW_PROTOCOL" not in env:
|
||||
env[
|
||||
"GIT_ALLOW_PROTOCOL"
|
||||
] = "file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc"
|
||||
env["GIT_ALLOW_PROTOCOL"] = (
|
||||
"file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc"
|
||||
)
|
||||
env["GIT_HTTP_USER_AGENT"] = user_agent.git
|
||||
|
||||
if objdir:
|
||||
@@ -313,12 +312,15 @@ class GitCommand:
|
||||
cwd = None
|
||||
command_name = cmdv[0]
|
||||
command.append(command_name)
|
||||
# Need to use the --progress flag for fetch/clone so output will be
|
||||
# displayed as by default git only does progress output if stderr is a
|
||||
# TTY.
|
||||
if sys.stderr.isatty() and command_name in ("fetch", "clone"):
|
||||
if "--progress" not in cmdv and "--quiet" not in cmdv:
|
||||
command.append("--progress")
|
||||
|
||||
if command_name in ("fetch", "clone"):
|
||||
env["GIT_TERMINAL_PROMPT"] = "0"
|
||||
# Need to use the --progress flag for fetch/clone so output will be
|
||||
# displayed as by default git only does progress output if stderr is
|
||||
# a TTY.
|
||||
if sys.stderr.isatty():
|
||||
if "--progress" not in cmdv and "--quiet" not in cmdv:
|
||||
command.append("--progress")
|
||||
command.extend(cmdv[1:])
|
||||
|
||||
event_log = (
|
||||
@@ -347,9 +349,9 @@ class GitCommand:
|
||||
"Project": e.project,
|
||||
"CommandName": command_name,
|
||||
"Message": str(e),
|
||||
"ReturnCode": str(e.git_rc)
|
||||
if e.git_rc is not None
|
||||
else None,
|
||||
"ReturnCode": (
|
||||
str(e.git_rc) if e.git_rc is not None else None
|
||||
),
|
||||
"IsError": log_as_error,
|
||||
}
|
||||
)
|
||||
@@ -609,7 +611,7 @@ class GitCommandError(GitError):
|
||||
self.git_stderr = git_stderr
|
||||
|
||||
@property
|
||||
@functools.lru_cache(maxsize=None)
|
||||
@functools.lru_cache(maxsize=None) # noqa: B019
|
||||
def suggestion(self):
|
||||
"""Returns helpful next steps for the given stderr."""
|
||||
if not self.git_stderr:
|
||||
|
||||
+27
-4
@@ -42,7 +42,7 @@ SYNC_STATE_PREFIX = "repo.syncstate."
|
||||
|
||||
ID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
|
||||
REVIEW_CACHE = dict()
|
||||
REVIEW_CACHE = {}
|
||||
|
||||
|
||||
def IsChange(rev):
|
||||
@@ -90,6 +90,20 @@ class GitConfig:
|
||||
|
||||
@staticmethod
|
||||
def _getUserConfig():
|
||||
"""Get the user-specific config file.
|
||||
|
||||
Prefers the XDG config location if available, with fallback to
|
||||
~/.gitconfig
|
||||
|
||||
This matches git behavior:
|
||||
https://git-scm.com/docs/git-config#FILES
|
||||
"""
|
||||
xdg_config_home = os.getenv(
|
||||
"XDG_CONFIG_HOME", os.path.expanduser("~/.config")
|
||||
)
|
||||
xdg_config_file = os.path.join(xdg_config_home, "git", "config")
|
||||
if os.path.exists(xdg_config_file):
|
||||
return xdg_config_file
|
||||
return os.path.expanduser("~/.gitconfig")
|
||||
|
||||
@classmethod
|
||||
@@ -97,7 +111,7 @@ class GitConfig:
|
||||
return cls(configfile=os.path.join(gitdir, "config"), defaults=defaults)
|
||||
|
||||
def __init__(self, configfile, defaults=None, jsonFile=None):
|
||||
self.file = configfile
|
||||
self.file = str(configfile)
|
||||
self.defaults = defaults
|
||||
self._cache_dict = None
|
||||
self._section_dict = None
|
||||
@@ -208,6 +222,12 @@ class GitConfig:
|
||||
value = "true" if value else "false"
|
||||
self.SetString(name, value)
|
||||
|
||||
def SetInt(self, name: str, value: int) -> None:
|
||||
"""Set an integer value for a key."""
|
||||
if value is not None:
|
||||
value = str(value)
|
||||
self.SetString(name, value)
|
||||
|
||||
def GetString(self, name: str, all_keys: bool = False) -> Union[str, None]:
|
||||
"""Get the first value for a key, or None if it is not defined.
|
||||
|
||||
@@ -418,7 +438,7 @@ class GitConfig:
|
||||
if p.Wait() == 0:
|
||||
return p.stdout
|
||||
else:
|
||||
raise GitError(f"git config {str(args)}: {p.stderr}")
|
||||
raise GitError(f"git {' '.join(command)}: {p.stderr}")
|
||||
|
||||
|
||||
class RepoConfig(GitConfig):
|
||||
@@ -704,7 +724,10 @@ class Remote:
|
||||
def Save(self):
|
||||
"""Save this remote to the configuration."""
|
||||
self._Set("url", self.url)
|
||||
if self.pushUrl is not None:
|
||||
# projectname is initialized for projects listed in the manifest, but
|
||||
# not for others (e.g. the manifest project). This class is used for
|
||||
# all of them.
|
||||
if self.pushUrl is not None and self.projectname is not None:
|
||||
self._Set("pushurl", self.pushUrl + "/" + self.projectname)
|
||||
else:
|
||||
self._Set("pushurl", self.pushUrl)
|
||||
|
||||
+88
-57
@@ -14,6 +14,7 @@
|
||||
|
||||
import os
|
||||
|
||||
from git_command import GitCommand
|
||||
import platform_utils
|
||||
from repo_trace import Trace
|
||||
|
||||
@@ -86,9 +87,8 @@ class GitRefs:
|
||||
self._symref = {}
|
||||
self._mtime = {}
|
||||
|
||||
self._ReadPackedRefs()
|
||||
self._ReadLoose("refs/")
|
||||
self._ReadLoose1(os.path.join(self._gitdir, HEAD), HEAD)
|
||||
self._ReadRefs()
|
||||
self._ReadSymbolicRef(HEAD)
|
||||
|
||||
scan = self._symref
|
||||
attempts = 0
|
||||
@@ -102,64 +102,95 @@ class GitRefs:
|
||||
scan = scan_next
|
||||
attempts += 1
|
||||
|
||||
def _ReadPackedRefs(self):
|
||||
path = os.path.join(self._gitdir, "packed-refs")
|
||||
self._TrackMtime(HEAD)
|
||||
self._TrackMtime("config")
|
||||
self._TrackMtime("packed-refs")
|
||||
self._TrackTreeMtimes("refs")
|
||||
self._TrackTreeMtimes("reftable")
|
||||
|
||||
@staticmethod
|
||||
def _IsNullRef(ref_id: str) -> bool:
|
||||
"""Check if a ref_id is a null object ID."""
|
||||
return ref_id and all(ch == "0" for ch in ref_id)
|
||||
|
||||
def _ReadRefs(self) -> None:
|
||||
"""Read all references using git for-each-ref."""
|
||||
p = GitCommand(
|
||||
None,
|
||||
["for-each-ref", "--format=%(objectname)%00%(refname)%00%(symref)"],
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
bare=True,
|
||||
gitdir=self._gitdir,
|
||||
)
|
||||
if p.Wait() != 0:
|
||||
return
|
||||
|
||||
for line in p.stdout.splitlines():
|
||||
ref_id, name, symref = line.split("\0")
|
||||
if symref:
|
||||
self._symref[name] = symref
|
||||
elif ref_id and not self._IsNullRef(ref_id):
|
||||
self._phyref[name] = ref_id
|
||||
|
||||
def _ReadSymbolicRef(self, name: str) -> None:
|
||||
"""Read a symbolic reference."""
|
||||
p = GitCommand(
|
||||
None,
|
||||
["symbolic-ref", "-q", name],
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
bare=True,
|
||||
gitdir=self._gitdir,
|
||||
)
|
||||
if p.Wait() == 0:
|
||||
ref = p.stdout.strip()
|
||||
if ref:
|
||||
self._symref[name] = ref
|
||||
return
|
||||
|
||||
p = GitCommand(
|
||||
None,
|
||||
["rev-parse", "--verify", "-q", name],
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
bare=True,
|
||||
gitdir=self._gitdir,
|
||||
)
|
||||
if p.Wait() == 0:
|
||||
ref_id = p.stdout.strip()
|
||||
if ref_id:
|
||||
self._phyref[name] = ref_id
|
||||
|
||||
def _TrackMtime(self, name: str) -> None:
|
||||
"""Track the modification time of a specific gitdir path."""
|
||||
path = os.path.join(self._gitdir, name)
|
||||
try:
|
||||
fd = open(path)
|
||||
mtime = os.path.getmtime(path)
|
||||
self._mtime[name] = os.path.getmtime(path)
|
||||
except OSError:
|
||||
return
|
||||
|
||||
def _TrackTreeMtimes(self, root: str) -> None:
|
||||
"""Recursively track modification times for a directory tree."""
|
||||
root_path = os.path.join(self._gitdir, root)
|
||||
try:
|
||||
for line in fd:
|
||||
line = str(line)
|
||||
if line[0] == "#":
|
||||
continue
|
||||
if line[0] == "^":
|
||||
continue
|
||||
|
||||
line = line[:-1]
|
||||
p = line.split(" ")
|
||||
ref_id = p[0]
|
||||
name = p[1]
|
||||
|
||||
self._phyref[name] = ref_id
|
||||
finally:
|
||||
fd.close()
|
||||
self._mtime["packed-refs"] = mtime
|
||||
|
||||
def _ReadLoose(self, prefix):
|
||||
base = os.path.join(self._gitdir, prefix)
|
||||
for name in platform_utils.listdir(base):
|
||||
p = os.path.join(base, name)
|
||||
# We don't implement the full ref validation algorithm, just the
|
||||
# simple rules that would show up in local filesystems.
|
||||
# https://git-scm.com/docs/git-check-ref-format
|
||||
if name.startswith(".") or name.endswith(".lock"):
|
||||
pass
|
||||
elif platform_utils.isdir(p):
|
||||
self._mtime[prefix] = os.path.getmtime(base)
|
||||
self._ReadLoose(prefix + name + "/")
|
||||
else:
|
||||
self._ReadLoose1(p, prefix + name)
|
||||
|
||||
def _ReadLoose1(self, path, name):
|
||||
try:
|
||||
with open(path) as fd:
|
||||
mtime = os.path.getmtime(path)
|
||||
ref_id = fd.readline()
|
||||
except (OSError, UnicodeError):
|
||||
if not platform_utils.isdir(root_path):
|
||||
return
|
||||
except OSError:
|
||||
return
|
||||
|
||||
try:
|
||||
ref_id = ref_id.decode()
|
||||
except AttributeError:
|
||||
pass
|
||||
if not ref_id:
|
||||
return
|
||||
ref_id = ref_id[:-1]
|
||||
to_scan = [root]
|
||||
while to_scan:
|
||||
name = to_scan.pop()
|
||||
self._TrackMtime(name)
|
||||
path = os.path.join(self._gitdir, name)
|
||||
if not platform_utils.isdir(path):
|
||||
continue
|
||||
|
||||
if ref_id.startswith("ref: "):
|
||||
self._symref[name] = ref_id[5:]
|
||||
else:
|
||||
self._phyref[name] = ref_id
|
||||
self._mtime[name] = mtime
|
||||
for child in platform_utils.listdir(path):
|
||||
child_name = os.path.join(name, child)
|
||||
child_path = os.path.join(self._gitdir, child_name)
|
||||
if platform_utils.isdir(child_path):
|
||||
to_scan.append(child_name)
|
||||
else:
|
||||
self._TrackMtime(child_name)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Copyright (C) 2009 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
+124
-37
@@ -23,16 +23,21 @@ Examples:
|
||||
"""
|
||||
|
||||
import functools
|
||||
import glob
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from typing import NamedTuple
|
||||
import urllib.parse
|
||||
|
||||
from git_command import git_require
|
||||
from git_command import GitCommand
|
||||
from git_config import IsId
|
||||
from git_config import RepoConfig
|
||||
from git_refs import GitRefs
|
||||
import platform_utils
|
||||
|
||||
|
||||
_SUPERPROJECT_GIT_NAME = "superproject.git"
|
||||
@@ -96,7 +101,7 @@ class Superproject:
|
||||
self._manifest = manifest
|
||||
self.name = name
|
||||
self.remote = remote
|
||||
self.revision = self._branch = revision
|
||||
self.revision = revision
|
||||
self._repodir = manifest.repodir
|
||||
self._superproject_dir = superproject_dir
|
||||
self._superproject_path = manifest.SubmanifestInfoDir(
|
||||
@@ -128,6 +133,34 @@ class Superproject:
|
||||
"""Set the _print_messages attribute."""
|
||||
self._print_messages = value
|
||||
|
||||
def SetRevisionId(self, revision_id: str) -> None:
|
||||
"""Set the revisionId of the superproject to sync to."""
|
||||
self.revision = revision_id
|
||||
|
||||
@property
|
||||
def commit_id(self):
|
||||
"""Returns the commit ID of the superproject checkout."""
|
||||
cmd = ["rev-parse", self.revision]
|
||||
p = GitCommand(
|
||||
None, # project
|
||||
cmd,
|
||||
gitdir=self._work_git,
|
||||
bare=True,
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
)
|
||||
retval = p.Wait()
|
||||
if retval != 0:
|
||||
self._LogWarning(
|
||||
"git rev-parse call failed, command: git {}, "
|
||||
"return code: {}, stderr: {}",
|
||||
cmd,
|
||||
retval,
|
||||
p.stderr,
|
||||
)
|
||||
return None
|
||||
return p.stdout
|
||||
|
||||
@property
|
||||
def project_commit_ids(self):
|
||||
"""Returns a dictionary of projects and their commit ids."""
|
||||
@@ -140,17 +173,39 @@ class Superproject:
|
||||
self._manifest_path if os.path.exists(self._manifest_path) else None
|
||||
)
|
||||
|
||||
@property
|
||||
def repo_id(self):
|
||||
"""Returns the repo ID for the superproject.
|
||||
|
||||
For example, if the superproject points to:
|
||||
https://android-review.googlesource.com/platform/superproject/
|
||||
Then the repo_id would be:
|
||||
android/platform/superproject
|
||||
"""
|
||||
review_url = self.remote.review
|
||||
if review_url:
|
||||
parsed_url = urllib.parse.urlparse(review_url)
|
||||
netloc = parsed_url.netloc
|
||||
if netloc:
|
||||
parts = netloc.split("-review", 1)
|
||||
host = parts[0]
|
||||
rev = GitRefs(self._work_git).get("HEAD")
|
||||
return f"{host}/{self.name}@{rev}"
|
||||
return None
|
||||
|
||||
def _LogMessage(self, fmt, *inputs):
|
||||
"""Logs message to stderr and _git_event_log."""
|
||||
message = f"{self._LogMessagePrefix()} {fmt.format(*inputs)}"
|
||||
if self._print_messages:
|
||||
print(message, file=sys.stderr)
|
||||
self._git_event_log.ErrorEvent(message, fmt)
|
||||
if self._git_event_log:
|
||||
self._git_event_log.ErrorEvent(message, fmt)
|
||||
|
||||
def _LogMessagePrefix(self):
|
||||
"""Returns the prefix string to be logged in each log message"""
|
||||
return (
|
||||
f"repo superproject branch: {self._branch} url: {self._remote_url}"
|
||||
f"repo superproject revision: {self.revision} "
|
||||
f"url: {self._remote_url}"
|
||||
)
|
||||
|
||||
def _LogError(self, fmt, *inputs):
|
||||
@@ -169,30 +224,63 @@ class Superproject:
|
||||
"""
|
||||
if not os.path.exists(self._superproject_path):
|
||||
os.mkdir(self._superproject_path)
|
||||
if not self._quiet and not os.path.exists(self._work_git):
|
||||
|
||||
if os.path.exists(self._work_git):
|
||||
return True
|
||||
|
||||
if not self._quiet:
|
||||
print(
|
||||
"%s: Performing initial setup for superproject; this might "
|
||||
"take several minutes." % self._work_git
|
||||
)
|
||||
cmd = ["init", "--bare", self._work_git_name]
|
||||
p = GitCommand(
|
||||
None,
|
||||
cmd,
|
||||
cwd=self._superproject_path,
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
|
||||
tmp_gitdir_prefix = ".tmp-superproject-initgitdir-"
|
||||
tmp_gitdir = tempfile.mkdtemp(
|
||||
prefix=tmp_gitdir_prefix,
|
||||
dir=self._superproject_path,
|
||||
)
|
||||
retval = p.Wait()
|
||||
if retval:
|
||||
self._LogWarning(
|
||||
"git init call failed, command: git {}, "
|
||||
"return code: {}, stderr: {}",
|
||||
tmp_git_name = os.path.basename(tmp_gitdir)
|
||||
|
||||
try:
|
||||
cmd = ["init", "--bare", tmp_git_name]
|
||||
p = GitCommand(
|
||||
None,
|
||||
cmd,
|
||||
retval,
|
||||
p.stderr,
|
||||
cwd=self._superproject_path,
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
retval = p.Wait()
|
||||
if retval:
|
||||
self._LogWarning(
|
||||
"git init call failed, command: git {}, "
|
||||
"return code: {}, stderr: {}",
|
||||
cmd,
|
||||
retval,
|
||||
p.stderr,
|
||||
)
|
||||
return False
|
||||
|
||||
platform_utils.rename(tmp_gitdir, self._work_git)
|
||||
tmp_gitdir = None
|
||||
return True
|
||||
finally:
|
||||
# Clean up the temporary directory created during the process,
|
||||
# as well as any stale ones left over from previous attempts.
|
||||
if tmp_gitdir and os.path.exists(tmp_gitdir):
|
||||
platform_utils.rmtree(tmp_gitdir)
|
||||
|
||||
age_threshold = 60 * 60 * 24 # 1 day in seconds
|
||||
now = time.time()
|
||||
for tmp_dir in glob.glob(
|
||||
os.path.join(self._superproject_path, f"{tmp_gitdir_prefix}*")
|
||||
):
|
||||
try:
|
||||
mtime = os.path.getmtime(tmp_dir)
|
||||
if now - mtime > age_threshold:
|
||||
platform_utils.rmtree(tmp_dir)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _Fetch(self):
|
||||
"""Fetches a superproject for the manifest based on |_remote_url|.
|
||||
@@ -230,8 +318,15 @@ class Superproject:
|
||||
if rev_commit:
|
||||
cmd.extend(["--negotiation-tip", rev_commit])
|
||||
|
||||
if self._branch:
|
||||
cmd += [self._branch + ":" + self._branch]
|
||||
if self.revision:
|
||||
# If revision is a commit hash, fetch it directly to avoid
|
||||
# creating a local branch of the same name.
|
||||
refspec = (
|
||||
self.revision
|
||||
if IsId(self.revision)
|
||||
else f"{self.revision}:{self.revision}"
|
||||
)
|
||||
cmd.append(refspec)
|
||||
p = GitCommand(
|
||||
None,
|
||||
cmd,
|
||||
@@ -258,7 +353,7 @@ class Superproject:
|
||||
Works only in git repositories.
|
||||
|
||||
Returns:
|
||||
data: data returned from 'git ls-tree ...' instead of None.
|
||||
data: data returned from 'git ls-tree ...'. None on error.
|
||||
"""
|
||||
if not os.path.exists(self._work_git):
|
||||
self._LogWarning(
|
||||
@@ -266,7 +361,7 @@ class Superproject:
|
||||
)
|
||||
return None
|
||||
data = None
|
||||
branch = "HEAD" if not self._branch else self._branch
|
||||
branch = "HEAD" if not self.revision else self.revision
|
||||
cmd = ["ls-tree", "-z", "-r", branch]
|
||||
|
||||
p = GitCommand(
|
||||
@@ -288,6 +383,7 @@ class Superproject:
|
||||
retval,
|
||||
p.stderr,
|
||||
)
|
||||
return None
|
||||
return data
|
||||
|
||||
def Sync(self, git_event_log):
|
||||
@@ -307,8 +403,6 @@ class Superproject:
|
||||
)
|
||||
return SyncResult(False, False)
|
||||
|
||||
_PrintBetaNotice()
|
||||
|
||||
should_exit = True
|
||||
if not self._remote_url:
|
||||
self._LogWarning(
|
||||
@@ -319,6 +413,8 @@ class Superproject:
|
||||
|
||||
if not self._Init():
|
||||
return SyncResult(False, should_exit)
|
||||
if IsId(self.revision) and self.commit_id:
|
||||
return SyncResult(True, False)
|
||||
if not self._Fetch():
|
||||
return SyncResult(False, should_exit)
|
||||
if not self._quiet:
|
||||
@@ -377,7 +473,8 @@ class Superproject:
|
||||
)
|
||||
return None
|
||||
manifest_str = self._manifest.ToXml(
|
||||
groups=self._manifest.GetGroupsStr(), omit_local=True
|
||||
filter_groups=self._manifest.GetManifestGroupsStr(),
|
||||
omit_local=True,
|
||||
).toxml()
|
||||
manifest_path = self._manifest_path
|
||||
try:
|
||||
@@ -452,16 +549,6 @@ class Superproject:
|
||||
return UpdateProjectsResult(manifest_path, False)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=10)
|
||||
def _PrintBetaNotice():
|
||||
"""Print the notice of beta status."""
|
||||
print(
|
||||
"NOTICE: --use-superproject is in beta; report any issues to the "
|
||||
"address described in `repo version`",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def _UseSuperprojectFromConfiguration():
|
||||
"""Returns the user choice of whether to use superproject."""
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
# Copyright (C) 2020 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Event logging in the git trace2 EVENT format."""
|
||||
|
||||
from git_command import GetEventTargetPath
|
||||
from git_command import RepoSourceVersion
|
||||
from git_trace2_event_log_base import BaseEventLog
|
||||
|
||||
@@ -68,6 +68,7 @@ class BaseEventLog:
|
||||
global p_init_count
|
||||
p_init_count += 1
|
||||
self._log = []
|
||||
self.verbose = False
|
||||
# Try to get session-id (sid) from environment (setup in repo launcher).
|
||||
KEY = "GIT_TRACE2_PARENT_SID"
|
||||
if env is None:
|
||||
@@ -130,10 +131,10 @@ class BaseEventLog:
|
||||
"time": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
def StartEvent(self):
|
||||
def StartEvent(self, argv):
|
||||
"""Append a 'start' event to the current log."""
|
||||
start_event = self._CreateEventDict("start")
|
||||
start_event["argv"] = sys.argv
|
||||
start_event["argv"] = argv
|
||||
self._log.append(start_event)
|
||||
|
||||
def ExitEvent(self, result):
|
||||
@@ -159,9 +160,11 @@ class BaseEventLog:
|
||||
name: Name of the primary command (ex: repo, git)
|
||||
subcommands: List of the sub-commands (ex: version, init, sync)
|
||||
"""
|
||||
command_event = self._CreateEventDict("command")
|
||||
command_event = self._CreateEventDict("cmd_name")
|
||||
name = f"{name}-"
|
||||
name += "-".join(subcommands)
|
||||
command_event["name"] = name
|
||||
command_event["subcommands"] = subcommands
|
||||
command_event["hierarchy"] = name
|
||||
self._log.append(command_event)
|
||||
|
||||
def LogConfigEvents(self, config, event_dict_name):
|
||||
@@ -307,10 +310,12 @@ class BaseEventLog:
|
||||
# ignore the attempt and continue to DGRAM below. Otherwise,
|
||||
# issue a warning.
|
||||
if err.errno != errno.EPROTOTYPE:
|
||||
print(
|
||||
f"repo: warning: git trace2 logging failed: {err}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if self.verbose:
|
||||
print(
|
||||
"repo: warning: git trace2 logging failed:",
|
||||
f"{err}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return None
|
||||
if socket_type == socket.SOCK_DGRAM or socket_type is None:
|
||||
try:
|
||||
@@ -320,18 +325,20 @@ class BaseEventLog:
|
||||
self._WriteLog(lambda bs: sock.sendto(bs, path))
|
||||
return f"af_unix:dgram:{path}"
|
||||
except OSError as err:
|
||||
print(
|
||||
f"repo: warning: git trace2 logging failed: {err}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if self.verbose:
|
||||
print(
|
||||
f"repo: warning: git trace2 logging failed: {err}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return None
|
||||
# Tried to open a socket but couldn't connect (SOCK_STREAM) or write
|
||||
# (SOCK_DGRAM).
|
||||
print(
|
||||
"repo: warning: git trace2 logging failed: could not write to "
|
||||
"socket",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if self.verbose:
|
||||
print(
|
||||
"repo: warning: git trace2 logging failed: could not"
|
||||
"write to socket",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return None
|
||||
|
||||
# Path is an absolute path
|
||||
@@ -346,9 +353,10 @@ class BaseEventLog:
|
||||
self._WriteLog(f.write)
|
||||
log_path = f.name
|
||||
except FileExistsError as err:
|
||||
print(
|
||||
"repo: warning: git trace2 logging failed: %r" % err,
|
||||
file=sys.stderr,
|
||||
)
|
||||
if self.verbose:
|
||||
print(
|
||||
"repo: warning: git trace2 logging failed: %r" % err,
|
||||
file=sys.stderr,
|
||||
)
|
||||
return None
|
||||
return log_path
|
||||
|
||||
@@ -22,6 +22,13 @@ from error import HookError
|
||||
from git_refs import HEAD
|
||||
|
||||
|
||||
# The API we've documented to hook authors. Keep in sync with repo-hooks.md.
|
||||
_API_ARGS = {
|
||||
"pre-upload": {"project_list", "worktree_list"},
|
||||
"post-sync": {"repo_topdir", "sync_duration_seconds"},
|
||||
}
|
||||
|
||||
|
||||
class RepoHook:
|
||||
"""A RepoHook contains information about a script to run as a hook.
|
||||
|
||||
@@ -56,6 +63,7 @@ class RepoHook:
|
||||
hooks_project,
|
||||
repo_topdir,
|
||||
manifest_url,
|
||||
bug_url=None,
|
||||
bypass_hooks=False,
|
||||
allow_all_hooks=False,
|
||||
ignore_hooks=False,
|
||||
@@ -75,6 +83,7 @@ class RepoHook:
|
||||
run with CWD as this directory.
|
||||
If you have a manifest, this is manifest.topdir.
|
||||
manifest_url: The URL to the manifest git repo.
|
||||
bug_url: The URL to report issues.
|
||||
bypass_hooks: If True, then 'Do not run the hook'.
|
||||
allow_all_hooks: If True, then 'Run the hook without prompting'.
|
||||
ignore_hooks: If True, then 'Do not abort action if hooks fail'.
|
||||
@@ -85,18 +94,18 @@ class RepoHook:
|
||||
self._hooks_project = hooks_project
|
||||
self._repo_topdir = repo_topdir
|
||||
self._manifest_url = manifest_url
|
||||
self._bug_url = bug_url
|
||||
self._bypass_hooks = bypass_hooks
|
||||
self._allow_all_hooks = allow_all_hooks
|
||||
self._ignore_hooks = ignore_hooks
|
||||
self._abort_if_user_denies = abort_if_user_denies
|
||||
|
||||
# Store the full path to the script for convenience.
|
||||
if self._hooks_project:
|
||||
self._script_fullpath = None
|
||||
if self._hooks_project and self._hooks_project.worktree:
|
||||
self._script_fullpath = os.path.join(
|
||||
self._hooks_project.worktree, self._hook_type + ".py"
|
||||
)
|
||||
else:
|
||||
self._script_fullpath = None
|
||||
|
||||
def _GetHash(self):
|
||||
"""Return a hash of the contents of the hooks directory.
|
||||
@@ -414,11 +423,26 @@ class RepoHook:
|
||||
ignore the result through the option combinations as listed in
|
||||
AddHookOptionGroup().
|
||||
"""
|
||||
# Make sure our own callers use the documented API.
|
||||
exp_kwargs = _API_ARGS.get(self._hook_type, set())
|
||||
got_kwargs = set(kwargs.keys())
|
||||
if exp_kwargs != got_kwargs:
|
||||
print(
|
||||
"repo internal error: "
|
||||
f"hook '{self._hook_type}' called incorrectly\n"
|
||||
f" got: {sorted(got_kwargs)}\n"
|
||||
f" expected: {sorted(exp_kwargs)}\n"
|
||||
f"Please file a bug: {self._bug_url}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return False
|
||||
|
||||
# Do not do anything in case bypass_hooks is set, or
|
||||
# no-op if there is no hooks project or if hook is disabled.
|
||||
if (
|
||||
self._bypass_hooks
|
||||
or not self._hooks_project
|
||||
or not self._script_fullpath
|
||||
or self._hook_type not in self._hooks_project.enabled_repo_hooks
|
||||
):
|
||||
return True
|
||||
@@ -472,6 +496,7 @@ class RepoHook:
|
||||
"manifest_url": manifest.manifestProject.GetRemote(
|
||||
"origin"
|
||||
).url,
|
||||
"bug_url": manifest.contactinfo.bugurl,
|
||||
}
|
||||
)
|
||||
return cls(*args, **kwargs)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (C) 2008 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -20,6 +19,7 @@ People shouldn't run this directly; instead, they should use the `repo` wrapper
|
||||
which takes care of execing this entry point.
|
||||
"""
|
||||
|
||||
import difflib
|
||||
import getpass
|
||||
import json
|
||||
import netrc
|
||||
@@ -30,6 +30,7 @@ import signal
|
||||
import sys
|
||||
import textwrap
|
||||
import time
|
||||
from typing import Optional
|
||||
import urllib.request
|
||||
|
||||
from repo_logging import RepoLogger
|
||||
@@ -45,7 +46,6 @@ from command import InteractiveCommand
|
||||
from command import MirrorSafeCommand
|
||||
from editor import Editor
|
||||
from error import DownloadError
|
||||
from error import GitcUnsupportedError
|
||||
from error import InvalidProjectGroupsError
|
||||
from error import ManifestInvalidRevisionError
|
||||
from error import ManifestParseError
|
||||
@@ -294,6 +294,102 @@ class _Repo:
|
||||
result = run()
|
||||
return result
|
||||
|
||||
def _autocorrect_command_name(
|
||||
self, name: str, config: RepoConfig
|
||||
) -> Optional[str]:
|
||||
"""Autocorrect command name based on user's git config."""
|
||||
|
||||
close_commands = difflib.get_close_matches(
|
||||
name, self.commands.keys(), n=5, cutoff=0.7
|
||||
)
|
||||
|
||||
if not close_commands:
|
||||
logger.error(
|
||||
"repo: '%s' is not a repo command. See 'repo help'.", name
|
||||
)
|
||||
return None
|
||||
|
||||
assumed = close_commands[0]
|
||||
autocorrect = config.GetString("help.autocorrect")
|
||||
|
||||
# If there are multiple close matches, git won't automatically run one.
|
||||
# We'll always prompt instead of guessing.
|
||||
if len(close_commands) > 1:
|
||||
autocorrect = "prompt"
|
||||
|
||||
# Handle git configuration boolean values:
|
||||
# 0, "false", "off", "no", "show": show suggestion (default)
|
||||
# 1, "true", "on", "yes", "immediate": run suggestion immediately
|
||||
# "never": don't run or show any suggested command
|
||||
# "prompt": show the suggestion and prompt for confirmation
|
||||
# positive number > 1: run suggestion after specified deciseconds
|
||||
if autocorrect is None:
|
||||
autocorrect = "0"
|
||||
|
||||
autocorrect = autocorrect.lower()
|
||||
|
||||
if autocorrect in ("0", "false", "off", "no", "show"):
|
||||
autocorrect = 0
|
||||
elif autocorrect in ("true", "on", "yes", "immediate"):
|
||||
autocorrect = -1 # immediate
|
||||
elif autocorrect == "never":
|
||||
return None
|
||||
elif autocorrect == "prompt":
|
||||
logger.warning(
|
||||
"You called a repo command named "
|
||||
"'%s', which does not exist.",
|
||||
name,
|
||||
)
|
||||
try:
|
||||
resp = input(f"Run '{assumed}' instead [y/N]? ")
|
||||
if resp.lower().startswith("y"):
|
||||
return assumed
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
pass
|
||||
return None
|
||||
else:
|
||||
try:
|
||||
autocorrect = int(autocorrect)
|
||||
except ValueError:
|
||||
autocorrect = 0
|
||||
|
||||
if autocorrect != 0:
|
||||
if autocorrect < 0:
|
||||
logger.warning(
|
||||
"You called a repo command named "
|
||||
"'%s', which does not exist.\n"
|
||||
"Continuing assuming that "
|
||||
"you meant '%s'.",
|
||||
name,
|
||||
assumed,
|
||||
)
|
||||
else:
|
||||
delay = autocorrect * 0.1
|
||||
logger.warning(
|
||||
"You called a repo command named "
|
||||
"'%s', which does not exist.\n"
|
||||
"Continuing in %.1f seconds, assuming "
|
||||
"that you meant '%s'.",
|
||||
name,
|
||||
delay,
|
||||
assumed,
|
||||
)
|
||||
try:
|
||||
time.sleep(delay)
|
||||
except KeyboardInterrupt:
|
||||
return None
|
||||
return assumed
|
||||
|
||||
logger.error(
|
||||
"repo: '%s' is not a repo command. See 'repo help'.", name
|
||||
)
|
||||
logger.warning(
|
||||
"The most similar command%s\n\t%s",
|
||||
"s are" if len(close_commands) > 1 else " is",
|
||||
"\n\t".join(close_commands),
|
||||
)
|
||||
return None
|
||||
|
||||
def _RunLong(self, name, gopts, argv, git_trace2_event_log):
|
||||
"""Execute the (longer running) requested subcommand."""
|
||||
result = 0
|
||||
@@ -308,24 +404,22 @@ class _Repo:
|
||||
outer_client=outer_client,
|
||||
)
|
||||
|
||||
if Wrapper().gitc_parse_clientdir(os.getcwd()):
|
||||
logger.error("GITC is not supported.")
|
||||
raise GitcUnsupportedError()
|
||||
if name not in self.commands:
|
||||
corrected_name = self._autocorrect_command_name(
|
||||
name, outer_client.globalConfig
|
||||
)
|
||||
if not corrected_name:
|
||||
return 1
|
||||
name = corrected_name
|
||||
|
||||
try:
|
||||
cmd = self.commands[name](
|
||||
repodir=self.repodir,
|
||||
client=repo_client,
|
||||
manifest=repo_client.manifest,
|
||||
outer_client=outer_client,
|
||||
outer_manifest=outer_client.manifest,
|
||||
git_event_log=git_trace2_event_log,
|
||||
)
|
||||
except KeyError:
|
||||
logger.error(
|
||||
"repo: '%s' is not a repo command. See 'repo help'.", name
|
||||
)
|
||||
return 1
|
||||
cmd = self.commands[name](
|
||||
repodir=self.repodir,
|
||||
client=repo_client,
|
||||
manifest=repo_client.manifest,
|
||||
outer_client=outer_client,
|
||||
outer_manifest=outer_client.manifest,
|
||||
git_event_log=git_trace2_event_log,
|
||||
)
|
||||
|
||||
Editor.globalConfig = cmd.client.globalConfig
|
||||
|
||||
@@ -343,6 +437,9 @@ class _Repo:
|
||||
)
|
||||
return 1
|
||||
|
||||
cmd.CommonValidateOptions(copts, cargs)
|
||||
git_trace2_event_log.verbose = copts.verbose
|
||||
|
||||
if gopts.pager is not False and not isinstance(cmd, InteractiveCommand):
|
||||
config = cmd.client.globalConfig
|
||||
if gopts.pager:
|
||||
@@ -357,7 +454,7 @@ class _Repo:
|
||||
start = time.time()
|
||||
cmd_event = cmd.event_log.Add(name, event_log.TASK_COMMAND, start)
|
||||
cmd.event_log.SetParent(cmd_event)
|
||||
git_trace2_event_log.StartEvent()
|
||||
git_trace2_event_log.StartEvent(["repo", name] + argv)
|
||||
git_trace2_event_log.CommandEvent(name="repo", subcommands=[name])
|
||||
|
||||
def execute_command_helper():
|
||||
@@ -365,7 +462,6 @@ class _Repo:
|
||||
Execute the subcommand.
|
||||
"""
|
||||
nonlocal result
|
||||
cmd.CommonValidateOptions(copts, cargs)
|
||||
cmd.ValidateOptions(copts, cargs)
|
||||
|
||||
this_manifest_only = copts.this_manifest_only
|
||||
|
||||
+6
-1
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "July 2022" "repo forall" "Repo Manual"
|
||||
.TH REPO "1" "May 2026" "repo forall" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo forall - manual page for repo forall
|
||||
.SH SYNOPSIS
|
||||
@@ -120,6 +120,11 @@ git command, use REPO_LREV.
|
||||
REPO_RREV is the name of the revision from the manifest, exactly as written in
|
||||
the manifest.
|
||||
.PP
|
||||
REPO_UPSTREAM is the name of the upstream branch as specified in the manifest.
|
||||
.PP
|
||||
REPO_DEST_BRANCH is the name of the destination branch for code review, as
|
||||
specified in the manifest.
|
||||
.PP
|
||||
REPO_COUNT is the total number of projects being iterated.
|
||||
.PP
|
||||
REPO_I is the current (1\-based) iteration count. Can be used in conjunction with
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "April 2025" "repo gc" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo gc - manual page for repo gc
|
||||
.SH SYNOPSIS
|
||||
.B repo
|
||||
\fI\,gc\/\fR
|
||||
.SH DESCRIPTION
|
||||
Summary
|
||||
.PP
|
||||
Cleaning up internal repo and Git state.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
\fB\-h\fR, \fB\-\-help\fR
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-n\fR, \fB\-\-dry\-run\fR
|
||||
do everything except actually delete
|
||||
.TP
|
||||
\fB\-y\fR, \fB\-\-yes\fR
|
||||
answer yes to all safe prompts
|
||||
.TP
|
||||
\fB\-\-repack\fR
|
||||
repack all projects that use partial clone with
|
||||
filter=blob:none
|
||||
.SS Logging options:
|
||||
.TP
|
||||
\fB\-v\fR, \fB\-\-verbose\fR
|
||||
show all output
|
||||
.TP
|
||||
\fB\-q\fR, \fB\-\-quiet\fR
|
||||
only show errors
|
||||
.SS Multi\-manifest options:
|
||||
.TP
|
||||
\fB\-\-outer\-manifest\fR
|
||||
operate starting at the outermost manifest
|
||||
.TP
|
||||
\fB\-\-no\-outer\-manifest\fR
|
||||
do not operate on outer manifests
|
||||
.TP
|
||||
\fB\-\-this\-manifest\-only\fR
|
||||
only operate on this (sub)manifest
|
||||
.TP
|
||||
\fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
|
||||
operate on this manifest and its submanifests
|
||||
.PP
|
||||
Run `repo help gc` to view the detailed manual.
|
||||
+21
-2
@@ -1,10 +1,10 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "July 2022" "repo info" "Repo Manual"
|
||||
.TH REPO "1" "May 2026" "repo info" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo info - manual page for repo info
|
||||
.SH SYNOPSIS
|
||||
.B repo
|
||||
\fI\,info \/\fR[\fI\,-dl\/\fR] [\fI\,-o \/\fR[\fI\,-c\/\fR]] [\fI\,<project>\/\fR...]
|
||||
\fI\,info \/\fR[\fI\,-dl\/\fR] [\fI\,-o \/\fR[\fI\,-c\/\fR]] [\fI\,--format=<format>\/\fR] [\fI\,<project>\/\fR...]
|
||||
.SH DESCRIPTION
|
||||
Summary
|
||||
.PP
|
||||
@@ -14,6 +14,10 @@ Get info on the manifest branch, current branch or unmerged branches
|
||||
\fB\-h\fR, \fB\-\-help\fR
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||
number of jobs to run in parallel (default: based on
|
||||
number of CPU cores)
|
||||
.TP
|
||||
\fB\-d\fR, \fB\-\-diff\fR
|
||||
show full info and commit diff including remote
|
||||
branches
|
||||
@@ -21,6 +25,18 @@ branches
|
||||
\fB\-o\fR, \fB\-\-overview\fR
|
||||
show overview of all local commits
|
||||
.TP
|
||||
\fB\-\-include\-summary\fR
|
||||
include manifest summary (default: true)
|
||||
.TP
|
||||
\fB\-\-no\-include\-summary\fR
|
||||
exclude manifest summary
|
||||
.TP
|
||||
\fB\-\-include\-projects\fR
|
||||
include project details (default: true)
|
||||
.TP
|
||||
\fB\-\-no\-include\-projects\fR
|
||||
exclude project details
|
||||
.TP
|
||||
\fB\-c\fR, \fB\-\-current\-branch\fR
|
||||
consider only checked out branches
|
||||
.TP
|
||||
@@ -29,6 +45,9 @@ consider all local branches
|
||||
.TP
|
||||
\fB\-l\fR, \fB\-\-local\-only\fR
|
||||
disable all remote operations
|
||||
.TP
|
||||
\fB\-\-format\fR=\fI\,FORMAT\/\fR
|
||||
output format: text, json (default: text)
|
||||
.SS Logging options:
|
||||
.TP
|
||||
\fB\-v\fR, \fB\-\-verbose\fR
|
||||
|
||||
+10
-1
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "October 2022" "repo init" "Repo Manual"
|
||||
.TH REPO "1" "September 2024" "repo init" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo init - manual page for repo init
|
||||
.SH SYNOPSIS
|
||||
@@ -28,6 +28,11 @@ manifest repository location
|
||||
\fB\-b\fR REVISION, \fB\-\-manifest\-branch\fR=\fI\,REVISION\/\fR
|
||||
manifest branch or revision (use HEAD for default)
|
||||
.TP
|
||||
\fB\-\-manifest\-upstream\-branch\fR=\fI\,BRANCH\/\fR
|
||||
when a commit is provided to \fB\-\-manifest\-branch\fR, this
|
||||
is the name of the git ref in which the commit can be
|
||||
found
|
||||
.TP
|
||||
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
|
||||
initial manifest file
|
||||
.TP
|
||||
@@ -163,6 +168,10 @@ The optional \fB\-b\fR argument can be used to select the manifest branch to che
|
||||
and use. If no branch is specified, the remote's default branch is used. This is
|
||||
equivalent to using \fB\-b\fR HEAD.
|
||||
.PP
|
||||
The optional \fB\-\-manifest\-upstream\-branch\fR argument can be used when a commit is
|
||||
provided to \fB\-\-manifest\-branch\fR (or \fB\-b\fR), to specify the name of the git ref in
|
||||
which the commit can be found.
|
||||
.PP
|
||||
The optional \fB\-m\fR argument can be used to specify an alternate manifest to be
|
||||
used. If no manifest is specified, the manifest default.xml will be used.
|
||||
.PP
|
||||
|
||||
+134
-61
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "April 2024" "repo manifest" "Repo Manual"
|
||||
.TH REPO "1" "April 2026" "repo manifest" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo manifest - manual page for repo manifest
|
||||
.SH SYNOPSIS
|
||||
@@ -30,8 +30,8 @@ if in \fB\-r\fR mode, do not write the dest\-branch field
|
||||
(only of use if the branch names for a sha1 manifest
|
||||
are sensitive)
|
||||
.TP
|
||||
\fB\-\-json\fR
|
||||
output manifest in JSON format (experimental)
|
||||
\fB\-\-format\fR=\fI\,FORMAT\/\fR
|
||||
output format: xml, json (default: xml)
|
||||
.TP
|
||||
\fB\-\-pretty\fR
|
||||
format output for humans to read
|
||||
@@ -78,6 +78,10 @@ set to the ref we were on when the manifest was generated. The 'dest\-branch'
|
||||
attribute is set to indicate the remote ref to push changes to via 'repo
|
||||
upload'.
|
||||
.PP
|
||||
Multiple output formats are supported via \fB\-\-format\fR. The default output is XML,
|
||||
and formats are generally "condensed". Use \fB\-\-pretty\fR for more human\-readable
|
||||
variations.
|
||||
.PP
|
||||
repo Manifest Format
|
||||
.PP
|
||||
A repo manifest describes the structure of a repo client; that is the
|
||||
@@ -127,6 +131,7 @@ include*)>
|
||||
<!ATTLIST default dest\-branch CDATA #IMPLIED>
|
||||
<!ATTLIST default upstream CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-j CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-j\-max CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-c CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-s CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-tags CDATA #IMPLIED>
|
||||
@@ -135,7 +140,7 @@ include*)>
|
||||
<!ATTLIST manifest\-server url CDATA #REQUIRED>
|
||||
.IP
|
||||
<!ELEMENT submanifest EMPTY>
|
||||
<!ATTLIST submanifest name ID #REQUIRED>
|
||||
<!ATTLIST submanifest name ID #REQUIRED>
|
||||
<!ATTLIST submanifest remote IDREF #IMPLIED>
|
||||
<!ATTLIST submanifest project CDATA #IMPLIED>
|
||||
<!ATTLIST submanifest manifest\-name CDATA #IMPLIED>
|
||||
@@ -160,15 +165,32 @@ IDREF #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST project revision
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST project dest\-branch
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST project groups
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST project sync\-c
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST project sync\-s
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST project sync\-tags
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST project upstream
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST project clone\-depth
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST project force\-path
|
||||
CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ATTLIST project dest\-branch CDATA #IMPLIED>
|
||||
<!ATTLIST project groups CDATA #IMPLIED>
|
||||
<!ATTLIST project sync\-c CDATA #IMPLIED>
|
||||
<!ATTLIST project sync\-s CDATA #IMPLIED>
|
||||
<!ATTLIST project sync\-tags CDATA #IMPLIED>
|
||||
<!ATTLIST project upstream CDATA #IMPLIED>
|
||||
<!ATTLIST project clone\-depth CDATA #IMPLIED>
|
||||
<!ATTLIST project force\-path CDATA #IMPLIED>
|
||||
<!ATTLIST project sync\-strategy CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT annotation EMPTY>
|
||||
<!ATTLIST annotation name CDATA #REQUIRED>
|
||||
@@ -180,26 +202,43 @@ CDATA #IMPLIED>
|
||||
<!ATTLIST copyfile dest CDATA #REQUIRED>
|
||||
.IP
|
||||
<!ELEMENT linkfile EMPTY>
|
||||
<!ATTLIST linkfile src CDATA #REQUIRED>
|
||||
<!ATTLIST linkfile src CDATA #REQUIRED>
|
||||
<!ATTLIST linkfile dest CDATA #REQUIRED>
|
||||
.TP
|
||||
<!ELEMENT extend\-project (annotation*,
|
||||
copyfile*,
|
||||
linkfile*)>
|
||||
.TP
|
||||
<!ATTLIST extend\-project name
|
||||
CDATA #REQUIRED>
|
||||
.TP
|
||||
<!ATTLIST extend\-project path
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST extend\-project dest\-path
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST extend\-project groups
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST extend\-project revision
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST extend\-project remote
|
||||
CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT extend\-project EMPTY>
|
||||
<!ATTLIST extend\-project name CDATA #REQUIRED>
|
||||
<!ATTLIST extend\-project path CDATA #IMPLIED>
|
||||
<!ATTLIST extend\-project dest\-path CDATA #IMPLIED>
|
||||
<!ATTLIST extend\-project groups CDATA #IMPLIED>
|
||||
<!ATTLIST extend\-project revision CDATA #IMPLIED>
|
||||
<!ATTLIST extend\-project remote CDATA #IMPLIED>
|
||||
<!ATTLIST extend\-project dest\-branch CDATA #IMPLIED>
|
||||
<!ATTLIST extend\-project upstream CDATA #IMPLIED>
|
||||
<!ATTLIST extend\-project upstream CDATA #IMPLIED>
|
||||
<!ATTLIST extend\-project base\-rev CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT remove\-project EMPTY>
|
||||
<!ATTLIST remove\-project name CDATA #IMPLIED>
|
||||
<!ATTLIST remove\-project path CDATA #IMPLIED>
|
||||
<!ATTLIST remove\-project optional CDATA #IMPLIED>
|
||||
<!ATTLIST remove\-project base\-rev CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT repo\-hooks EMPTY>
|
||||
<!ATTLIST repo\-hooks in\-project CDATA #REQUIRED>
|
||||
<!ATTLIST repo\-hooks in\-project CDATA #REQUIRED>
|
||||
<!ATTLIST repo\-hooks enabled\-list CDATA #REQUIRED>
|
||||
.IP
|
||||
<!ELEMENT superproject EMPTY>
|
||||
@@ -208,7 +247,7 @@ CDATA #IMPLIED>
|
||||
<!ATTLIST superproject revision CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT contactinfo EMPTY>
|
||||
<!ATTLIST contactinfo bugurl CDATA #REQUIRED>
|
||||
<!ATTLIST contactinfo bugurl CDATA #REQUIRED>
|
||||
.IP
|
||||
<!ELEMENT include EMPTY>
|
||||
<!ATTLIST include name CDATA #REQUIRED>
|
||||
@@ -288,7 +327,9 @@ when syncing a revision locked manifest in \fB\-c\fR mode to avoid having to syn
|
||||
entire ref space. Project elements not setting their own `upstream` will inherit
|
||||
this value.
|
||||
.PP
|
||||
Attribute `sync\-j`: Number of parallel jobs to use when synching.
|
||||
Attribute `sync\-j`: Number of parallel jobs to use when syncing.
|
||||
.PP
|
||||
Attribute `sync\-j\-max`: Maximum number of parallel jobs to use when syncing.
|
||||
.PP
|
||||
Attribute `sync\-c`: Set to true to only sync the given Git branch (specified in
|
||||
the `revision` attribute) rather than the whole ref space. Project elements
|
||||
@@ -304,25 +345,7 @@ Element manifest\-server
|
||||
At most one manifest\-server may be specified. The url attribute is used to
|
||||
specify the URL of a manifest server, which is an XML RPC service.
|
||||
.PP
|
||||
The manifest server should implement the following RPC methods:
|
||||
.IP
|
||||
GetApprovedManifest(branch, target)
|
||||
.PP
|
||||
Return a manifest in which each project is pegged to a known good revision for
|
||||
the current branch and target. This is used by repo sync when the \fB\-\-smart\-sync\fR
|
||||
option is given.
|
||||
.PP
|
||||
The target to use is defined by environment variables TARGET_PRODUCT and
|
||||
TARGET_BUILD_VARIANT. These variables are used to create a string of the form
|
||||
$TARGET_PRODUCT\-$TARGET_BUILD_VARIANT, e.g. passion\-userdebug. If one of those
|
||||
variables or both are not present, the program will call GetApprovedManifest
|
||||
without the target parameter and the manifest server should choose a reasonable
|
||||
default target.
|
||||
.IP
|
||||
GetManifest(tag)
|
||||
.PP
|
||||
Return a manifest in which each project is pegged to the revision at the
|
||||
specified tag. This is used by repo sync when the \fB\-\-smart\-tag\fR option is given.
|
||||
See the [smart sync documentation](./smart\-sync.md) for more details.
|
||||
.PP
|
||||
Element submanifest
|
||||
.PP
|
||||
@@ -374,7 +397,7 @@ supplied, `revision` is used.
|
||||
.PP
|
||||
`path` may not be an absolute path or use "." or ".." path components.
|
||||
.PP
|
||||
Attribute `groups`: List of additional groups to which all projects in the
|
||||
Attribute `groups`: Set of additional groups to which all projects in the
|
||||
included submanifest belong. This appends and recurses, meaning all projects in
|
||||
submanifests carry all parent submanifest groups. Same syntax as the
|
||||
corresponding element of `project`.
|
||||
@@ -436,7 +459,7 @@ Attribute `dest\-branch`: Name of a Git branch (e.g. `main`). When using `repo
|
||||
upload`, changes will be submitted for code review on this branch. If
|
||||
unspecified both here and in the default element, `revision` is used instead.
|
||||
.PP
|
||||
Attribute `groups`: List of groups to which this project belongs, whitespace or
|
||||
Attribute `groups`: Set of groups to which this project belongs, whitespace or
|
||||
comma separated. All projects belong to the group "all", and each project
|
||||
automatically belongs to a group of its name:`name` and path:`path`. E.g. for
|
||||
`<project name="monkeys" path="barrel\-of"/>`, that project definition is
|
||||
@@ -463,6 +486,21 @@ mirror repository according to its `path` attribute (if supplied) rather than
|
||||
the `name` attribute. This attribute only applies to the local mirrors syncing,
|
||||
it will be ignored when syncing the projects in a client working directory.
|
||||
.PP
|
||||
Attribute `sync\-strategy`: Set the sync strategy used when fetching this
|
||||
project. Currently the only supported value is `stateless`. When set to
|
||||
`stateless`, repo will run a reflog expiration and aggressive garbage collection
|
||||
at the end of the sync process. This is useful for projects that contain large
|
||||
binary files and use `clone\-depth="1"`, where garbage can accumulate as binaries
|
||||
are added, deleted, or modified across successive syncs.
|
||||
.PP
|
||||
During a stateless sync, repo checks the following before cleaning up: 1. The
|
||||
project does not share an object directory with other projects. 2. The working
|
||||
tree is clean (no uncommitted changes, no untracked files). 3. There are no
|
||||
unpushed local commits. 4. There is no Git stash.
|
||||
.PP
|
||||
If any of these conditions are not met, repo falls back to a standard sync
|
||||
without garbage collection.
|
||||
.PP
|
||||
Element extend\-project
|
||||
.PP
|
||||
Modify the attributes of the named project.
|
||||
@@ -472,6 +510,11 @@ of an existing project without completely replacing the existing project
|
||||
definition. This makes the local manifest more robust against changes to the
|
||||
original manifest.
|
||||
.PP
|
||||
The `extend\-project` element can also contain `annotation`, `copyfile`, and
|
||||
`linkfile` child elements. These are added to the project's definition. A
|
||||
`copyfile` or `linkfile` with a `dest` that already exists in the project will
|
||||
overwrite the original.
|
||||
.PP
|
||||
Attribute `path`: If specified, limit the change to projects checked out at the
|
||||
specified path, rather than all projects with the given name.
|
||||
.PP
|
||||
@@ -480,8 +523,8 @@ repo client where the Git working directory for this project should be placed.
|
||||
This is used to move a project in the checkout by overriding the existing `path`
|
||||
setting.
|
||||
.PP
|
||||
Attribute `groups`: List of additional groups to which this project belongs.
|
||||
Same syntax as the corresponding element of `project`.
|
||||
Attribute `groups`: Set of additional groups to which this project belongs. Same
|
||||
syntax as the corresponding element of `project`.
|
||||
.PP
|
||||
Attribute `revision`: If specified, overrides the revision of the original
|
||||
project. Same syntax as the corresponding element of `project`.
|
||||
@@ -495,21 +538,31 @@ project. Same syntax as the corresponding element of `project`.
|
||||
Attribute `upstream`: If specified, overrides the upstream of the original
|
||||
project. Same syntax as the corresponding element of `project`.
|
||||
.PP
|
||||
Attribute `base\-rev`: If specified, adds a check against the revision to be
|
||||
extended. Manifest parse will fail and give a list of mismatch extends if the
|
||||
revisions being extended have changed since base\-rev was set. Intended for use
|
||||
with layered manifests using hash revisions to prevent patch branches hiding
|
||||
newer upstream revisions. Also compares named refs like branches or tags but is
|
||||
misleading if branches are used as base\-rev. Same syntax as the corresponding
|
||||
element of `project`.
|
||||
.PP
|
||||
Element annotation
|
||||
.PP
|
||||
Zero or more annotation elements may be specified as children of a project or
|
||||
remote element. Each element describes a name\-value pair. For projects, this
|
||||
name\-value pair will be exported into each project's environment during a
|
||||
\&'forall' command, prefixed with `REPO__`. In addition, there is an optional
|
||||
attribute "keep" which accepts the case insensitive values "true" (default) or
|
||||
"false". This attribute determines whether or not the annotation will be kept
|
||||
when exported with the manifest subcommand.
|
||||
Zero or more annotation elements may be specified as children of a project
|
||||
element, an extend\-project element, or a remote element. Each element describes
|
||||
a name\-value pair. For projects, this name\-value pair will be exported into each
|
||||
project's environment during a 'forall' command, prefixed with `REPO__`. In
|
||||
addition, there is an optional attribute "keep" which accepts the case
|
||||
insensitive values "true" (default) or "false". This attribute determines
|
||||
whether or not the annotation will be kept when exported with the manifest
|
||||
subcommand.
|
||||
.PP
|
||||
Element copyfile
|
||||
.PP
|
||||
Zero or more copyfile elements may be specified as children of a project
|
||||
element. Each element describes a src\-dest pair of files; the "src" file will be
|
||||
copied to the "dest" place during `repo sync` command.
|
||||
element, or an extend\-project element. Each element describes a src\-dest pair of
|
||||
files; the "src" file will be copied to the "dest" place during `repo sync`
|
||||
command.
|
||||
.PP
|
||||
"src" is project relative, "dest" is relative to the top of the tree. Copying
|
||||
from paths outside of the project or to paths outside of the repo client is not
|
||||
@@ -520,10 +573,14 @@ Intermediate paths must not be symlinks either.
|
||||
.PP
|
||||
Parent directories of "dest" will be automatically created if missing.
|
||||
.PP
|
||||
The files are copied in the order they are specified in the manifests. If
|
||||
multiple elements specify the same source and destination, they will only be
|
||||
applied as one, based on the first occurence. Files are copied before any links
|
||||
specified via linkfile elements are created.
|
||||
.PP
|
||||
Element linkfile
|
||||
.PP
|
||||
It's just like copyfile and runs at the same time as copyfile but instead of
|
||||
copying it creates a symlink.
|
||||
It's just like copyfile, but instead of copying it creates a symlink.
|
||||
.PP
|
||||
The symlink is created at "dest" (relative to the top of the tree) and points to
|
||||
the path specified by "src" which is a path in the project.
|
||||
@@ -533,6 +590,11 @@ Parent directories of "dest" will be automatically created if missing.
|
||||
The symlink target may be a file or directory, but it may not point outside of
|
||||
the repo client.
|
||||
.PP
|
||||
The links are created in the order they are specified in the manifests. If
|
||||
multiple elements specify the same source and destination, they will only be
|
||||
applied as one, based on the first occurence. Links are created after any files
|
||||
specified via copyfile elements are copied.
|
||||
.PP
|
||||
Element remove\-project
|
||||
.PP
|
||||
Deletes a project from the internal manifest table, possibly allowing a
|
||||
@@ -556,6 +618,14 @@ Logic otherwise behaves like both are specified.
|
||||
Attribute `optional`: Set to true to ignore remove\-project elements with no
|
||||
matching `project` element.
|
||||
.PP
|
||||
Attribute `base\-rev`: If specified, adds a check against the revision to be
|
||||
removed. Manifest parse will fail and give a list of mismatch removes if the
|
||||
revisions being removed have changed since base\-rev was set. Intended for use
|
||||
with layered manifests using hash revisions to prevent patch branches hiding
|
||||
newer upstream revisions. Also compares named refs like branches or tags but is
|
||||
misleading if branches are used as base\-rev. Same syntax as the corresponding
|
||||
element of `project`.
|
||||
.PP
|
||||
Element repo\-hooks
|
||||
.PP
|
||||
NB: See the [practical documentation](./repo\-hooks.md) for using repo hooks.
|
||||
@@ -616,13 +686,16 @@ repository's root.
|
||||
"name" may not be an absolute path or use "." or ".." path components. These
|
||||
restrictions are not enforced for [Local Manifests].
|
||||
.PP
|
||||
Attribute `groups`: List of additional groups to which all projects in the
|
||||
Attribute `groups`: Set of additional groups to which all projects in the
|
||||
included manifest belong. This appends and recurses, meaning all projects in
|
||||
included manifests carry all parent include groups. Same syntax as the
|
||||
included manifests carry all parent include groups. This also applies to all
|
||||
extend\-project elements in the included manifests. Same syntax as the
|
||||
corresponding element of `project`.
|
||||
.PP
|
||||
Attribute `revision`: Name of a Git branch (e.g. `main` or `refs/heads/main`)
|
||||
default to which all projects in the included manifest belong.
|
||||
default to which all projects in the included manifest belong. This recurses,
|
||||
meaning it will apply to all projects in all manifests included as a result of
|
||||
this element.
|
||||
.PP
|
||||
Local Manifests
|
||||
.PP
|
||||
|
||||
+27
-3
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "April 2024" "repo smartsync" "Repo Manual"
|
||||
.TH REPO "1" "May 2026" "repo smartsync" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo smartsync - manual page for repo smartsync
|
||||
.SH SYNOPSIS
|
||||
@@ -20,11 +20,11 @@ number of CPU cores)
|
||||
.TP
|
||||
\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
|
||||
number of network jobs to run in parallel (defaults to
|
||||
\fB\-\-jobs\fR or 1)
|
||||
\fB\-\-jobs\fR or 1). Ignored unless \fB\-\-no\-interleaved\fR is set
|
||||
.TP
|
||||
\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
|
||||
number of local checkout jobs to run in parallel
|
||||
(defaults to \fB\-\-jobs\fR or 8)
|
||||
(defaults to \fB\-\-jobs\fR or 8). Ignored unless \fB\-\-nointerleaved\fR is set
|
||||
.TP
|
||||
\fB\-f\fR, \fB\-\-force\-broken\fR
|
||||
obsolete option (to be deleted in the future)
|
||||
@@ -47,6 +47,10 @@ force remove projects with uncommitted modifications
|
||||
if projects no longer exist in the manifest. WARNING:
|
||||
this may cause loss of data
|
||||
.TP
|
||||
\fB\-\-rebase\fR
|
||||
rebase local commits regardless of whether they are
|
||||
published
|
||||
.TP
|
||||
\fB\-l\fR, \fB\-\-local\-only\fR
|
||||
only update working tree, don't fetch
|
||||
.TP
|
||||
@@ -54,6 +58,12 @@ only update working tree, don't fetch
|
||||
use the existing manifest checkout as\-is. (do not
|
||||
update to the latest revision)
|
||||
.TP
|
||||
\fB\-\-interleaved\fR
|
||||
fetch and checkout projects in parallel (default)
|
||||
.TP
|
||||
\fB\-\-no\-interleaved\fR
|
||||
fetch and checkout projects in phases
|
||||
.TP
|
||||
\fB\-n\fR, \fB\-\-network\-only\fR
|
||||
fetch only, don't update working tree
|
||||
.TP
|
||||
@@ -91,6 +101,10 @@ implies \fB\-c\fR
|
||||
\fB\-\-no\-use\-superproject\fR
|
||||
disable use of manifest superprojects
|
||||
.TP
|
||||
\fB\-\-superproject\-revision\fR=\fI\,SUPERPROJECT_REVISION\/\fR
|
||||
sync to superproject revision (applies to outer
|
||||
manifest)
|
||||
.TP
|
||||
\fB\-\-tags\fR
|
||||
fetch tags
|
||||
.TP
|
||||
@@ -141,6 +155,16 @@ operate on this manifest and its submanifests
|
||||
.TP
|
||||
\fB\-\-no\-repo\-verify\fR
|
||||
do not verify repo source code
|
||||
.SS post\-sync hooks:
|
||||
.TP
|
||||
\fB\-\-no\-verify\fR
|
||||
Do not run the post\-sync hook.
|
||||
.TP
|
||||
\fB\-\-verify\fR
|
||||
Run the post\-sync hook without prompting.
|
||||
.TP
|
||||
\fB\-\-ignore\-hooks\fR
|
||||
Do not abort if post\-sync hooks fail.
|
||||
.PP
|
||||
Run `repo help smartsync` to view the detailed manual.
|
||||
.SH DETAILS
|
||||
|
||||
+27
-3
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "April 2024" "repo sync" "Repo Manual"
|
||||
.TH REPO "1" "May 2026" "repo sync" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo sync - manual page for repo sync
|
||||
.SH SYNOPSIS
|
||||
@@ -20,11 +20,11 @@ number of CPU cores)
|
||||
.TP
|
||||
\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
|
||||
number of network jobs to run in parallel (defaults to
|
||||
\fB\-\-jobs\fR or 1)
|
||||
\fB\-\-jobs\fR or 1). Ignored unless \fB\-\-no\-interleaved\fR is set
|
||||
.TP
|
||||
\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
|
||||
number of local checkout jobs to run in parallel
|
||||
(defaults to \fB\-\-jobs\fR or 8)
|
||||
(defaults to \fB\-\-jobs\fR or 8). Ignored unless \fB\-\-nointerleaved\fR is set
|
||||
.TP
|
||||
\fB\-f\fR, \fB\-\-force\-broken\fR
|
||||
obsolete option (to be deleted in the future)
|
||||
@@ -47,6 +47,10 @@ force remove projects with uncommitted modifications
|
||||
if projects no longer exist in the manifest. WARNING:
|
||||
this may cause loss of data
|
||||
.TP
|
||||
\fB\-\-rebase\fR
|
||||
rebase local commits regardless of whether they are
|
||||
published
|
||||
.TP
|
||||
\fB\-l\fR, \fB\-\-local\-only\fR
|
||||
only update working tree, don't fetch
|
||||
.TP
|
||||
@@ -54,6 +58,12 @@ only update working tree, don't fetch
|
||||
use the existing manifest checkout as\-is. (do not
|
||||
update to the latest revision)
|
||||
.TP
|
||||
\fB\-\-interleaved\fR
|
||||
fetch and checkout projects in parallel (default)
|
||||
.TP
|
||||
\fB\-\-no\-interleaved\fR
|
||||
fetch and checkout projects in phases
|
||||
.TP
|
||||
\fB\-n\fR, \fB\-\-network\-only\fR
|
||||
fetch only, don't update working tree
|
||||
.TP
|
||||
@@ -91,6 +101,10 @@ implies \fB\-c\fR
|
||||
\fB\-\-no\-use\-superproject\fR
|
||||
disable use of manifest superprojects
|
||||
.TP
|
||||
\fB\-\-superproject\-revision\fR=\fI\,SUPERPROJECT_REVISION\/\fR
|
||||
sync to superproject revision (applies to outer
|
||||
manifest)
|
||||
.TP
|
||||
\fB\-\-tags\fR
|
||||
fetch tags
|
||||
.TP
|
||||
@@ -148,6 +162,16 @@ operate on this manifest and its submanifests
|
||||
.TP
|
||||
\fB\-\-no\-repo\-verify\fR
|
||||
do not verify repo source code
|
||||
.SS post\-sync hooks:
|
||||
.TP
|
||||
\fB\-\-no\-verify\fR
|
||||
Do not run the post\-sync hook.
|
||||
.TP
|
||||
\fB\-\-verify\fR
|
||||
Run the post\-sync hook without prompting.
|
||||
.TP
|
||||
\fB\-\-ignore\-hooks\fR
|
||||
Do not abort if post\-sync hooks fail.
|
||||
.PP
|
||||
Run `repo help sync` to view the detailed manual.
|
||||
.SH DETAILS
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "November 2025" "repo wipe" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo wipe - manual page for repo wipe
|
||||
.SH SYNOPSIS
|
||||
.B repo
|
||||
\fI\,wipe <project>\/\fR...
|
||||
.SH DESCRIPTION
|
||||
Summary
|
||||
.PP
|
||||
Wipe projects from the worktree
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
\fB\-h\fR, \fB\-\-help\fR
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-f\fR, \fB\-\-force\fR
|
||||
force wipe shared projects and uncommitted changes
|
||||
.TP
|
||||
\fB\-\-force\-uncommitted\fR
|
||||
force wipe even if there are uncommitted changes
|
||||
.TP
|
||||
\fB\-\-force\-shared\fR
|
||||
force wipe even if the project shares an object
|
||||
directory
|
||||
.SS Logging options:
|
||||
.TP
|
||||
\fB\-v\fR, \fB\-\-verbose\fR
|
||||
show all output
|
||||
.TP
|
||||
\fB\-q\fR, \fB\-\-quiet\fR
|
||||
only show errors
|
||||
.SS Multi\-manifest options:
|
||||
.TP
|
||||
\fB\-\-outer\-manifest\fR
|
||||
operate starting at the outermost manifest
|
||||
.TP
|
||||
\fB\-\-no\-outer\-manifest\fR
|
||||
do not operate on outer manifests
|
||||
.TP
|
||||
\fB\-\-this\-manifest\-only\fR
|
||||
only operate on this (sub)manifest
|
||||
.TP
|
||||
\fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
|
||||
operate on this manifest and its submanifests
|
||||
.PP
|
||||
Run `repo help wipe` to view the detailed manual.
|
||||
.SH DETAILS
|
||||
.PP
|
||||
The 'repo wipe' command removes the specified projects from the worktree (the
|
||||
checked out source code) and deletes the project's git data from `.repo`.
|
||||
.PP
|
||||
This is a destructive operation and cannot be undone.
|
||||
.PP
|
||||
Projects can be specified either by name, or by a relative or absolute path to
|
||||
the project's local directory.
|
||||
.SH EXAMPLES
|
||||
.SS # Wipe the project "platform/build" by name:
|
||||
$ repo wipe platform/build
|
||||
.SS # Wipe the project at the path "build/make":
|
||||
$ repo wipe build/make
|
||||
+7
-1
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "April 2024" "repo" "Repo Manual"
|
||||
.TH REPO "1" "November 2025" "repo" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repository management tool built on top of git
|
||||
.SH SYNOPSIS
|
||||
@@ -79,6 +79,9 @@ Download and checkout a change
|
||||
forall
|
||||
Run a shell command in each project
|
||||
.TP
|
||||
gc
|
||||
Cleaning up internal repo and Git state.
|
||||
.TP
|
||||
grep
|
||||
Print lines matching a pattern
|
||||
.TP
|
||||
@@ -129,6 +132,9 @@ Upload changes for code review
|
||||
.TP
|
||||
version
|
||||
Display the version of repo
|
||||
.TP
|
||||
wipe
|
||||
Wipe projects from the worktree
|
||||
.PP
|
||||
See 'repo help <command>' for more information on a specific command.
|
||||
Bug reports: https://issues.gerritcodereview.com/issues/new?component=1370071
|
||||
|
||||
+166
-94
@@ -155,6 +155,7 @@ class _Default:
|
||||
upstreamExpr = None
|
||||
remote = None
|
||||
sync_j = None
|
||||
sync_j_max = None
|
||||
sync_c = False
|
||||
sync_s = False
|
||||
sync_tags = True
|
||||
@@ -255,7 +256,7 @@ class _XmlSubmanifest:
|
||||
project: a string, the name of the manifest project.
|
||||
revision: a string, the commitish.
|
||||
manifestName: a string, the submanifest file name.
|
||||
groups: a list of strings, the groups to add to all projects in the
|
||||
groups: a set of strings, the groups to add to all projects in the
|
||||
submanifest.
|
||||
default_groups: a list of strings, the default groups to sync.
|
||||
path: a string, the relative path for the submanifest checkout.
|
||||
@@ -281,7 +282,7 @@ class _XmlSubmanifest:
|
||||
self.project = project
|
||||
self.revision = revision
|
||||
self.manifestName = manifestName
|
||||
self.groups = groups
|
||||
self.groups = groups or set()
|
||||
self.default_groups = default_groups
|
||||
self.path = path
|
||||
self.parent = parent
|
||||
@@ -304,7 +305,7 @@ class _XmlSubmanifest:
|
||||
self.repo_client = RepoClient(
|
||||
parent.repodir,
|
||||
linkFile,
|
||||
parent_groups=",".join(groups) or "",
|
||||
parent_groups=groups,
|
||||
submanifest_path=os.path.join(parent.path_prefix, self.relpath),
|
||||
outer_client=outer_client,
|
||||
default_groups=default_groups,
|
||||
@@ -345,7 +346,7 @@ class _XmlSubmanifest:
|
||||
manifestName = self.manifestName or "default.xml"
|
||||
revision = self.revision or self.name
|
||||
path = self.path or revision.split("/")[-1]
|
||||
groups = self.groups or []
|
||||
groups = self.groups
|
||||
|
||||
return SubmanifestSpec(
|
||||
self.name, manifestUrl, manifestName, revision, path, groups
|
||||
@@ -359,9 +360,7 @@ class _XmlSubmanifest:
|
||||
|
||||
def GetGroupsStr(self):
|
||||
"""Returns the `groups` given for this submanifest."""
|
||||
if self.groups:
|
||||
return ",".join(self.groups)
|
||||
return ""
|
||||
return ",".join(sorted(self.groups))
|
||||
|
||||
def GetDefaultGroupsStr(self):
|
||||
"""Returns the `default-groups` given for this submanifest."""
|
||||
@@ -381,7 +380,7 @@ class SubmanifestSpec:
|
||||
self.manifestName = manifestName
|
||||
self.revision = revision
|
||||
self.path = path
|
||||
self.groups = groups or []
|
||||
self.groups = groups
|
||||
|
||||
|
||||
class XmlManifest:
|
||||
@@ -393,7 +392,7 @@ class XmlManifest:
|
||||
manifest_file,
|
||||
local_manifests=None,
|
||||
outer_client=None,
|
||||
parent_groups="",
|
||||
parent_groups=None,
|
||||
submanifest_path="",
|
||||
default_groups=None,
|
||||
):
|
||||
@@ -409,7 +408,8 @@ class XmlManifest:
|
||||
manifests. This will usually be
|
||||
|repodir|/|LOCAL_MANIFESTS_DIR_NAME|.
|
||||
outer_client: RepoClient of the outer manifest.
|
||||
parent_groups: a string, the groups to apply to this projects.
|
||||
parent_groups: a set of strings, the groups to apply to this
|
||||
manifest.
|
||||
submanifest_path: The submanifest root relative to the repo root.
|
||||
default_groups: a string, the default manifest groups to use.
|
||||
"""
|
||||
@@ -432,7 +432,7 @@ class XmlManifest:
|
||||
self.manifestFileOverrides = {}
|
||||
self.local_manifests = local_manifests
|
||||
self._load_local_manifests = True
|
||||
self.parent_groups = parent_groups
|
||||
self.parent_groups = parent_groups or set()
|
||||
self.default_groups = default_groups
|
||||
|
||||
if submanifest_path and not outer_client:
|
||||
@@ -567,21 +567,29 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
"""
|
||||
return [x for x in re.split(r"[,\s]+", field) if x]
|
||||
|
||||
def _ParseSet(self, field):
|
||||
"""Parse fields that contain flattened sets.
|
||||
|
||||
These are whitespace & comma separated. Empty elements will be
|
||||
discarded.
|
||||
"""
|
||||
return set(self._ParseList(field))
|
||||
|
||||
def ToXml(
|
||||
self,
|
||||
peg_rev=False,
|
||||
peg_rev_upstream=True,
|
||||
peg_rev_dest_branch=True,
|
||||
groups=None,
|
||||
filter_groups=None,
|
||||
omit_local=False,
|
||||
):
|
||||
"""Return the current manifest XML."""
|
||||
mp = self.manifestProject
|
||||
|
||||
if groups is None:
|
||||
groups = mp.manifest_groups
|
||||
if groups:
|
||||
groups = self._ParseList(groups)
|
||||
if filter_groups is None:
|
||||
filter_groups = mp.manifest_groups
|
||||
if filter_groups:
|
||||
filter_groups = self._ParseList(filter_groups)
|
||||
|
||||
doc = xml.dom.minidom.Document()
|
||||
root = doc.createElement("manifest")
|
||||
@@ -624,6 +632,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if d.sync_j is not None:
|
||||
have_default = True
|
||||
e.setAttribute("sync-j", "%d" % d.sync_j)
|
||||
if d.sync_j_max is not None:
|
||||
have_default = True
|
||||
e.setAttribute("sync-j-max", "%d" % d.sync_j_max)
|
||||
if d.sync_c:
|
||||
have_default = True
|
||||
e.setAttribute("sync-c", "true")
|
||||
@@ -654,7 +665,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
output_project(parent, parent_node, project)
|
||||
|
||||
def output_project(parent, parent_node, p):
|
||||
if not p.MatchesGroups(groups):
|
||||
if not p.MatchesGroups(filter_groups):
|
||||
return
|
||||
|
||||
if omit_local and self.IsFromLocalManifest(p):
|
||||
@@ -725,10 +736,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
le.setAttribute("dest", lf.dest)
|
||||
e.appendChild(le)
|
||||
|
||||
default_groups = ["all", "name:%s" % p.name, "path:%s" % p.relpath]
|
||||
egroups = [g for g in p.groups if g not in default_groups]
|
||||
if egroups:
|
||||
e.setAttribute("groups", ",".join(egroups))
|
||||
groups = p.groups - {"all", f"name:{p.name}", f"path:{p.relpath}"}
|
||||
if groups:
|
||||
e.setAttribute("groups", ",".join(sorted(groups)))
|
||||
|
||||
for a in p.annotations:
|
||||
if a.keep == "true":
|
||||
@@ -749,14 +759,17 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if p.clone_depth:
|
||||
e.setAttribute("clone-depth", str(p.clone_depth))
|
||||
|
||||
if p.sync_strategy:
|
||||
e.setAttribute("sync-strategy", str(p.sync_strategy))
|
||||
|
||||
self._output_manifest_project_extras(p, e)
|
||||
|
||||
if p.subprojects:
|
||||
subprojects = {subp.name for subp in p.subprojects}
|
||||
output_projects(p, e, list(sorted(subprojects)))
|
||||
output_projects(p, e, sorted(subprojects))
|
||||
|
||||
projects = {p.name for p in self._paths.values() if not p.parent}
|
||||
output_projects(None, root, list(sorted(projects)))
|
||||
output_projects(None, root, sorted(projects))
|
||||
|
||||
if self._repo_hooks_project:
|
||||
root.appendChild(doc.createTextNode(""))
|
||||
@@ -813,7 +826,6 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
"submanifest",
|
||||
# These are children of 'project' nodes.
|
||||
"annotation",
|
||||
"project",
|
||||
"copyfile",
|
||||
"linkfile",
|
||||
}
|
||||
@@ -1014,9 +1026,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
|
||||
def SetManifestOverride(self, path):
|
||||
"""Override manifestFile. The caller must call Unload()"""
|
||||
self._outer_client.manifest.manifestFileOverrides[
|
||||
self.path_prefix
|
||||
] = path
|
||||
self._outer_client.manifest.manifestFileOverrides[self.path_prefix] = (
|
||||
path
|
||||
)
|
||||
|
||||
@property
|
||||
def UseLocalManifests(self):
|
||||
@@ -1116,7 +1128,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
groups += f",platform-{platform.system().lower()}"
|
||||
return groups
|
||||
|
||||
def GetGroupsStr(self):
|
||||
def GetManifestGroupsStr(self):
|
||||
"""Returns the manifest group string that should be synced."""
|
||||
return (
|
||||
self.manifestProject.manifest_groups or self.GetDefaultGroupsStr()
|
||||
@@ -1171,12 +1183,12 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
b = b[len(R_HEADS) :]
|
||||
self.branch = b
|
||||
|
||||
parent_groups = self.parent_groups
|
||||
parent_groups = self.parent_groups.copy()
|
||||
if self.path_prefix:
|
||||
parent_groups = (
|
||||
parent_groups |= {
|
||||
f"{SUBMANIFEST_GROUP_PREFIX}:path:"
|
||||
f"{self.path_prefix},{parent_groups}"
|
||||
)
|
||||
f"{self.path_prefix}"
|
||||
}
|
||||
|
||||
# The manifestFile was specified by the user which is why we
|
||||
# allow include paths to point anywhere.
|
||||
@@ -1202,16 +1214,16 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
# Since local manifests are entirely managed by
|
||||
# the user, allow them to point anywhere the
|
||||
# user wants.
|
||||
local_group = (
|
||||
local_group = {
|
||||
f"{LOCAL_MANIFEST_GROUP_PREFIX}:"
|
||||
f"{local_file[:-4]}"
|
||||
)
|
||||
}
|
||||
nodes.append(
|
||||
self._ParseManifestXml(
|
||||
local,
|
||||
self.subdir,
|
||||
parent_groups=(
|
||||
f"{local_group},{parent_groups}"
|
||||
local_group | parent_groups
|
||||
),
|
||||
restrict_includes=False,
|
||||
)
|
||||
@@ -1262,7 +1274,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
self,
|
||||
path,
|
||||
include_root,
|
||||
parent_groups="",
|
||||
parent_groups=None,
|
||||
restrict_includes=True,
|
||||
parent_node=None,
|
||||
):
|
||||
@@ -1271,11 +1283,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
Args:
|
||||
path: The XML file to read & parse.
|
||||
include_root: The path to interpret include "name"s relative to.
|
||||
parent_groups: The groups to apply to this projects.
|
||||
parent_groups: The set of groups to apply to this manifest.
|
||||
restrict_includes: Whether to constrain the "name" attribute of
|
||||
includes.
|
||||
parent_node: The parent include node, to apply attribute to this
|
||||
projects.
|
||||
parent_node: The parent include node, to apply attributes to this
|
||||
manifest.
|
||||
|
||||
Returns:
|
||||
List of XML nodes.
|
||||
@@ -1299,6 +1311,14 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
|
||||
nodes = []
|
||||
for node in manifest.childNodes:
|
||||
if (
|
||||
parent_node
|
||||
and node.nodeName in ("include", "project")
|
||||
and not node.hasAttribute("revision")
|
||||
):
|
||||
node.setAttribute(
|
||||
"revision", parent_node.getAttribute("revision")
|
||||
)
|
||||
if node.nodeName == "include":
|
||||
name = self._reqatt(node, "name")
|
||||
if restrict_includes:
|
||||
@@ -1307,12 +1327,10 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
raise ManifestInvalidPathError(
|
||||
f'<include> invalid "name": {name}: {msg}'
|
||||
)
|
||||
include_groups = ""
|
||||
if parent_groups:
|
||||
include_groups = parent_groups
|
||||
include_groups = (parent_groups or set()).copy()
|
||||
if node.hasAttribute("groups"):
|
||||
include_groups = (
|
||||
node.getAttribute("groups") + "," + include_groups
|
||||
include_groups |= self._ParseSet(
|
||||
node.getAttribute("groups")
|
||||
)
|
||||
fp = os.path.join(include_root, name)
|
||||
if not os.path.isfile(fp):
|
||||
@@ -1328,33 +1346,23 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
)
|
||||
# should isolate this to the exact exception, but that's
|
||||
# tricky. actual parsing implementation may vary.
|
||||
except (
|
||||
KeyboardInterrupt,
|
||||
RuntimeError,
|
||||
SystemExit,
|
||||
ManifestParseError,
|
||||
):
|
||||
except (RuntimeError, ManifestParseError):
|
||||
raise
|
||||
except Exception as e:
|
||||
raise ManifestParseError(
|
||||
f"failed parsing included manifest {name}: {e}"
|
||||
)
|
||||
else:
|
||||
if parent_groups and node.nodeName == "project":
|
||||
nodeGroups = parent_groups
|
||||
if node.hasAttribute("groups"):
|
||||
nodeGroups = (
|
||||
node.getAttribute("groups") + "," + nodeGroups
|
||||
)
|
||||
node.setAttribute("groups", nodeGroups)
|
||||
if (
|
||||
parent_node
|
||||
and node.nodeName == "project"
|
||||
and not node.hasAttribute("revision")
|
||||
if parent_groups and node.nodeName in (
|
||||
"project",
|
||||
"extend-project",
|
||||
):
|
||||
node.setAttribute(
|
||||
"revision", parent_node.getAttribute("revision")
|
||||
)
|
||||
nodeGroups = parent_groups.copy()
|
||||
if node.hasAttribute("groups"):
|
||||
nodeGroups |= self._ParseSet(
|
||||
node.getAttribute("groups")
|
||||
)
|
||||
node.setAttribute("groups", ",".join(sorted(nodeGroups)))
|
||||
nodes.append(node)
|
||||
return nodes
|
||||
|
||||
@@ -1445,6 +1453,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
|
||||
repo_hooks_project = None
|
||||
enabled_repo_hooks = None
|
||||
failed_revision_changes = []
|
||||
for node in itertools.chain(*node_list):
|
||||
if node.nodeName == "project":
|
||||
project = self._ParseProject(node)
|
||||
@@ -1462,7 +1471,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
dest_path = node.getAttribute("dest-path")
|
||||
groups = node.getAttribute("groups")
|
||||
if groups:
|
||||
groups = self._ParseList(groups)
|
||||
groups = self._ParseSet(groups or "")
|
||||
revision = node.getAttribute("revision")
|
||||
remote_name = node.getAttribute("remote")
|
||||
if not remote_name:
|
||||
@@ -1471,6 +1480,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
remote = self._get_remote(node)
|
||||
dest_branch = node.getAttribute("dest-branch")
|
||||
upstream = node.getAttribute("upstream")
|
||||
base_revision = node.getAttribute("base-rev")
|
||||
|
||||
named_projects = self._projects[name]
|
||||
if dest_path and not path and len(named_projects) > 1:
|
||||
@@ -1482,8 +1492,23 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if path and p.relpath != path:
|
||||
continue
|
||||
if groups:
|
||||
p.groups.extend(groups)
|
||||
p.groups |= groups
|
||||
# Drop local groups so we don't mistakenly omit this
|
||||
# project from the superproject override manifest.
|
||||
p.groups = {
|
||||
g
|
||||
for g in p.groups
|
||||
if not g.startswith(LOCAL_MANIFEST_GROUP_PREFIX)
|
||||
}
|
||||
|
||||
if revision:
|
||||
if base_revision:
|
||||
if p.revisionExpr != base_revision:
|
||||
failed_revision_changes.append(
|
||||
"extend-project name %s mismatch base "
|
||||
"%s vs revision %s"
|
||||
% (name, base_revision, p.revisionExpr)
|
||||
)
|
||||
p.SetRevision(revision)
|
||||
|
||||
if remote_name:
|
||||
@@ -1505,6 +1530,14 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
p.UpdatePaths(relpath, worktree, gitdir, objdir)
|
||||
self._paths[p.relpath] = p
|
||||
|
||||
for n in node.childNodes:
|
||||
if n.nodeName == "copyfile":
|
||||
self._ParseCopyFile(p, n)
|
||||
elif n.nodeName == "linkfile":
|
||||
self._ParseLinkFile(p, n)
|
||||
elif n.nodeName == "annotation":
|
||||
self._ParseAnnotation(p, n)
|
||||
|
||||
if node.nodeName == "repo-hooks":
|
||||
# Only one project can be the hooks project
|
||||
if repo_hooks_project is not None:
|
||||
@@ -1558,6 +1591,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if node.nodeName == "remove-project":
|
||||
name = node.getAttribute("name")
|
||||
path = node.getAttribute("path")
|
||||
base_revision = node.getAttribute("base-rev")
|
||||
|
||||
# Name or path needed.
|
||||
if not name and not path:
|
||||
@@ -1571,6 +1605,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
for projname, projects in list(self._projects.items()):
|
||||
for p in projects:
|
||||
if name == projname and not path:
|
||||
if base_revision:
|
||||
if p.revisionExpr != base_revision:
|
||||
failed_revision_changes.append(
|
||||
"remove-project name %s mismatch base "
|
||||
"%s vs revision %s"
|
||||
% (name, base_revision, p.revisionExpr)
|
||||
)
|
||||
del self._paths[p.relpath]
|
||||
if not removed_project:
|
||||
del self._projects[name]
|
||||
@@ -1578,6 +1619,17 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
elif path == p.relpath and (
|
||||
name == projname or not name
|
||||
):
|
||||
if base_revision:
|
||||
if p.revisionExpr != base_revision:
|
||||
failed_revision_changes.append(
|
||||
"remove-project path %s mismatch base "
|
||||
"%s vs revision %s"
|
||||
% (
|
||||
p.relpath,
|
||||
base_revision,
|
||||
p.revisionExpr,
|
||||
)
|
||||
)
|
||||
self._projects[projname].remove(p)
|
||||
del self._paths[p.relpath]
|
||||
removed_project = p.name
|
||||
@@ -1597,6 +1649,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
"project: %s" % node.toxml()
|
||||
)
|
||||
|
||||
if failed_revision_changes:
|
||||
raise ManifestParseError(
|
||||
"revision base check failed, rebase patches and update "
|
||||
"base revs for: ",
|
||||
failed_revision_changes,
|
||||
)
|
||||
|
||||
# Store repo hooks project information.
|
||||
if repo_hooks_project:
|
||||
# Store a reference to the Project.
|
||||
@@ -1710,6 +1769,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
% (self.manifestFile, d.sync_j)
|
||||
)
|
||||
|
||||
d.sync_j_max = XmlInt(node, "sync-j-max", None)
|
||||
if d.sync_j_max is not None and d.sync_j_max <= 0:
|
||||
raise ManifestParseError(
|
||||
'%s: sync-j-max must be greater than 0, not "%s"'
|
||||
% (self.manifestFile, d.sync_j_max)
|
||||
)
|
||||
|
||||
d.sync_c = XmlBool(node, "sync-c", False)
|
||||
d.sync_s = XmlBool(node, "sync-s", False)
|
||||
d.sync_tags = XmlBool(node, "sync-tags", True)
|
||||
@@ -1772,7 +1838,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
groups = ""
|
||||
if node.hasAttribute("groups"):
|
||||
groups = node.getAttribute("groups")
|
||||
groups = self._ParseList(groups)
|
||||
groups = self._ParseSet(groups)
|
||||
default_groups = self._ParseList(node.getAttribute("default-groups"))
|
||||
path = node.getAttribute("path")
|
||||
if path == "":
|
||||
@@ -1875,17 +1941,14 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
% (self.manifestFile, clone_depth)
|
||||
)
|
||||
|
||||
sync_strategy = node.getAttribute("sync-strategy") or None
|
||||
|
||||
dest_branch = (
|
||||
node.getAttribute("dest-branch") or self._default.destBranchExpr
|
||||
)
|
||||
|
||||
upstream = node.getAttribute("upstream") or self._default.upstreamExpr
|
||||
|
||||
groups = ""
|
||||
if node.hasAttribute("groups"):
|
||||
groups = node.getAttribute("groups")
|
||||
groups = self._ParseList(groups)
|
||||
|
||||
if parent is None:
|
||||
(
|
||||
relpath,
|
||||
@@ -1900,8 +1963,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
parent, name, path
|
||||
)
|
||||
|
||||
default_groups = ["all", "name:%s" % name, "path:%s" % relpath]
|
||||
groups.extend(set(default_groups).difference(groups))
|
||||
groups = ""
|
||||
if node.hasAttribute("groups"):
|
||||
groups = node.getAttribute("groups")
|
||||
groups = self._ParseSet(groups)
|
||||
groups |= {"all", f"name:{name}", f"path:{relpath}"}
|
||||
|
||||
if self.IsMirror and node.hasAttribute("force-path"):
|
||||
if XmlBool(node, "force-path", False):
|
||||
@@ -1923,6 +1989,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
sync_s=sync_s,
|
||||
sync_tags=sync_tags,
|
||||
clone_depth=clone_depth,
|
||||
sync_strategy=sync_strategy,
|
||||
upstream=upstream,
|
||||
parent=parent,
|
||||
dest_branch=dest_branch,
|
||||
@@ -1933,11 +2000,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
for n in node.childNodes:
|
||||
if n.nodeName == "copyfile":
|
||||
self._ParseCopyFile(project, n)
|
||||
if n.nodeName == "linkfile":
|
||||
elif n.nodeName == "linkfile":
|
||||
self._ParseLinkFile(project, n)
|
||||
if n.nodeName == "annotation":
|
||||
elif n.nodeName == "annotation":
|
||||
self._ParseAnnotation(project, n)
|
||||
if n.nodeName == "project":
|
||||
elif n.nodeName == "project":
|
||||
project.subprojects.append(
|
||||
self._ParseProject(n, parent=project)
|
||||
)
|
||||
@@ -2021,7 +2088,12 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
path = path.rstrip("/")
|
||||
name = name.rstrip("/")
|
||||
relpath = self._JoinRelpath(parent.relpath, path)
|
||||
gitdir = os.path.join(parent.gitdir, "subprojects", "%s.git" % path)
|
||||
subprojects = os.path.join(parent.gitdir, "subprojects", f"{path}.git")
|
||||
modules = os.path.join(parent.gitdir, "modules", path)
|
||||
if platform_utils.isdir(subprojects):
|
||||
gitdir = subprojects
|
||||
else:
|
||||
gitdir = modules
|
||||
objdir = os.path.join(
|
||||
parent.gitdir, "subproject-objects", "%s.git" % name
|
||||
)
|
||||
@@ -2072,22 +2144,22 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
# implementation:
|
||||
# https://eclipse.googlesource.com/jgit/jgit/+/9110037e3e9461ff4dac22fee84ef3694ed57648/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java#884
|
||||
BAD_CODEPOINTS = {
|
||||
"\u200C", # ZERO WIDTH NON-JOINER
|
||||
"\u200D", # ZERO WIDTH JOINER
|
||||
"\u200E", # LEFT-TO-RIGHT MARK
|
||||
"\u200F", # RIGHT-TO-LEFT MARK
|
||||
"\u202A", # LEFT-TO-RIGHT EMBEDDING
|
||||
"\u202B", # RIGHT-TO-LEFT EMBEDDING
|
||||
"\u202C", # POP DIRECTIONAL FORMATTING
|
||||
"\u202D", # LEFT-TO-RIGHT OVERRIDE
|
||||
"\u202E", # RIGHT-TO-LEFT OVERRIDE
|
||||
"\u206A", # INHIBIT SYMMETRIC SWAPPING
|
||||
"\u206B", # ACTIVATE SYMMETRIC SWAPPING
|
||||
"\u206C", # INHIBIT ARABIC FORM SHAPING
|
||||
"\u206D", # ACTIVATE ARABIC FORM SHAPING
|
||||
"\u206E", # NATIONAL DIGIT SHAPES
|
||||
"\u206F", # NOMINAL DIGIT SHAPES
|
||||
"\uFEFF", # ZERO WIDTH NO-BREAK SPACE
|
||||
"\u200c", # ZERO WIDTH NON-JOINER
|
||||
"\u200d", # ZERO WIDTH JOINER
|
||||
"\u200e", # LEFT-TO-RIGHT MARK
|
||||
"\u200f", # RIGHT-TO-LEFT MARK
|
||||
"\u202a", # LEFT-TO-RIGHT EMBEDDING
|
||||
"\u202b", # RIGHT-TO-LEFT EMBEDDING
|
||||
"\u202c", # POP DIRECTIONAL FORMATTING
|
||||
"\u202d", # LEFT-TO-RIGHT OVERRIDE
|
||||
"\u202e", # RIGHT-TO-LEFT OVERRIDE
|
||||
"\u206a", # INHIBIT SYMMETRIC SWAPPING
|
||||
"\u206b", # ACTIVATE SYMMETRIC SWAPPING
|
||||
"\u206c", # INHIBIT ARABIC FORM SHAPING
|
||||
"\u206d", # ACTIVATE ARABIC FORM SHAPING
|
||||
"\u206e", # NATIONAL DIGIT SHAPES
|
||||
"\u206f", # NOMINAL DIGIT SHAPES
|
||||
"\ufeff", # ZERO WIDTH NO-BREAK SPACE
|
||||
}
|
||||
if BAD_CODEPOINTS & path_codepoints:
|
||||
# This message is more expansive than reality, but should be fine.
|
||||
|
||||
@@ -40,7 +40,7 @@ def RunPager(globalConfig):
|
||||
|
||||
|
||||
def TerminatePager():
|
||||
global pager_process, old_stdout, old_stderr
|
||||
global pager_process
|
||||
if pager_process:
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
|
||||
@@ -156,6 +156,12 @@ def remove(path, missing_ok=False):
|
||||
os.rmdir(longpath)
|
||||
else:
|
||||
os.remove(longpath)
|
||||
elif (
|
||||
e.errno == errno.EROFS
|
||||
and missing_ok
|
||||
and not os.path.exists(longpath)
|
||||
):
|
||||
pass
|
||||
elif missing_ok and e.errno == errno.ENOENT:
|
||||
pass
|
||||
else:
|
||||
@@ -216,6 +222,43 @@ def rmdir(path):
|
||||
os.rmdir(_makelongpath(path))
|
||||
|
||||
|
||||
def removedirs(path: str) -> None:
|
||||
"""Remove a directory tree of symlinks and empty directories.
|
||||
|
||||
Walks |path| bottom-up without following symlinks. Removes symlinks
|
||||
and empty directories. Stops at (and preserves) regular files and
|
||||
non-empty directories. Silently succeeds if |path| does not exist.
|
||||
|
||||
Availability: Unix, Windows.
|
||||
"""
|
||||
if islink(path):
|
||||
remove(path)
|
||||
return
|
||||
|
||||
if not isdir(path):
|
||||
return
|
||||
|
||||
for dirpath, dirnames, filenames in walk(path, topdown=False):
|
||||
for name in dirnames:
|
||||
entry = os.path.join(dirpath, name)
|
||||
if islink(entry):
|
||||
remove(entry)
|
||||
else:
|
||||
try:
|
||||
rmdir(entry)
|
||||
except OSError:
|
||||
pass
|
||||
for name in filenames:
|
||||
entry = os.path.join(dirpath, name)
|
||||
if islink(entry):
|
||||
remove(entry)
|
||||
|
||||
try:
|
||||
rmdir(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def isdir(path):
|
||||
"""os.path.isdir(path) wrapper with support for long paths on Windows.
|
||||
|
||||
|
||||
+38
-13
@@ -25,7 +25,10 @@ except ImportError:
|
||||
from repo_trace import IsTraceToStderr
|
||||
|
||||
|
||||
_TTY = sys.stderr.isatty()
|
||||
# Capture the original stderr stream. We use this exclusively for progress
|
||||
# updates to ensure we talk to the terminal even if stderr is redirected.
|
||||
_STDERR = sys.stderr
|
||||
_TTY = _STDERR.isatty()
|
||||
|
||||
# This will erase all content in the current line (wherever the cursor is).
|
||||
# It does not move the cursor, so this is usually followed by \r to move to
|
||||
@@ -100,6 +103,8 @@ class Progress:
|
||||
self._show = not delay
|
||||
self._units = units
|
||||
self._elide = elide and _TTY
|
||||
self._quiet = quiet
|
||||
self._ended = False
|
||||
|
||||
# Only show the active jobs section if we run more than one in parallel.
|
||||
self._show_jobs = False
|
||||
@@ -114,15 +119,14 @@ class Progress:
|
||||
)
|
||||
self._update_thread.daemon = True
|
||||
|
||||
# When quiet, never show any output. It's a bit hacky, but reusing the
|
||||
# existing logic that delays initial output keeps the rest of the class
|
||||
# clean. Basically we set the start time to years in the future.
|
||||
if quiet:
|
||||
self._show = False
|
||||
self._start += 2**32
|
||||
elif show_elapsed:
|
||||
if not quiet and show_elapsed:
|
||||
self._update_thread.start()
|
||||
|
||||
def update_total(self, new_total):
|
||||
"""Updates the total if the new total is larger."""
|
||||
if new_total > self._total:
|
||||
self._total = new_total
|
||||
|
||||
def _update_loop(self):
|
||||
while True:
|
||||
self.update(inc=0)
|
||||
@@ -132,11 +136,11 @@ class Progress:
|
||||
def _write(self, s):
|
||||
s = "\r" + s
|
||||
if self._elide:
|
||||
col = os.get_terminal_size(sys.stderr.fileno()).columns
|
||||
col = os.get_terminal_size(_STDERR.fileno()).columns
|
||||
if len(s) > col:
|
||||
s = s[: col - 1] + ".."
|
||||
sys.stderr.write(s)
|
||||
sys.stderr.flush()
|
||||
_STDERR.write(s)
|
||||
_STDERR.flush()
|
||||
|
||||
def start(self, name):
|
||||
self._active += 1
|
||||
@@ -155,12 +159,14 @@ class Progress:
|
||||
inc: The number of items completed.
|
||||
msg: The message to display. If None, use the last message.
|
||||
"""
|
||||
if self._ended:
|
||||
return
|
||||
self._done += inc
|
||||
if msg is None:
|
||||
msg = self._last_msg
|
||||
self._last_msg = msg
|
||||
|
||||
if not _TTY or IsTraceToStderr():
|
||||
if not _TTY or IsTraceToStderr() or self._quiet:
|
||||
return
|
||||
|
||||
elapsed_sec = time.time() - self._start
|
||||
@@ -200,9 +206,28 @@ class Progress:
|
||||
)
|
||||
)
|
||||
|
||||
def display_message(self, msg):
|
||||
"""Clears the current progress line and prints a message above it.
|
||||
|
||||
The progress bar is then redrawn on the next line.
|
||||
"""
|
||||
if not _TTY or IsTraceToStderr() or self._quiet:
|
||||
return
|
||||
|
||||
# Erase the current line, print the message with a newline,
|
||||
# and then immediately redraw the progress bar on the new line.
|
||||
_STDERR.write("\r" + CSI_ERASE_LINE)
|
||||
_STDERR.write(msg + "\n")
|
||||
_STDERR.flush()
|
||||
self.update(inc=0)
|
||||
|
||||
def end(self):
|
||||
if self._ended:
|
||||
return
|
||||
self._ended = True
|
||||
|
||||
self._update_event.set()
|
||||
if not _TTY or IsTraceToStderr() or not self._show:
|
||||
if not _TTY or IsTraceToStderr() or self._quiet:
|
||||
return
|
||||
|
||||
duration = duration_str(time.time() - self._start)
|
||||
|
||||
+703
-278
File diff suppressed because it is too large
Load Diff
+30
-2
@@ -1,4 +1,4 @@
|
||||
# Copyright 2023 The Android Open Source Project
|
||||
# Copyright (C) 2023 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -14,5 +14,33 @@
|
||||
|
||||
[tool.black]
|
||||
line-length = 80
|
||||
# NB: Keep in sync with tox.ini.
|
||||
target-version = ['py36', 'py37', 'py38', 'py39', 'py310', 'py311'] #, 'py312'
|
||||
|
||||
# Config file for the isort python module.
|
||||
# This is used to enforce import sorting standards.
|
||||
#
|
||||
# https://pycqa.github.io/isort/docs/configuration/options.html
|
||||
[tool.isort]
|
||||
# Be compatible with `black` since it also matches what we want.
|
||||
profile = 'black'
|
||||
|
||||
line_length = 80
|
||||
length_sort = false
|
||||
force_single_line = true
|
||||
lines_after_imports = 2
|
||||
from_first = false
|
||||
case_sensitive = false
|
||||
force_sort_within_sections = true
|
||||
order_by_type = false
|
||||
|
||||
# Ignore generated files.
|
||||
extend_skip_glob = '*_pb2.py'
|
||||
|
||||
# Allow importing multiple classes on a single line from these modules.
|
||||
# https://google.github.io/styleguide/pyguide#s2.2-imports
|
||||
single_line_exclusions = ['abc', 'collections.abc', 'typing']
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
markers = """
|
||||
skip_cq: Skip tests in the CQ. Should be rarely used!
|
||||
"""
|
||||
|
||||
Executable
+155
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (C) 2025 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Helper tool to check various metadata (e.g. licensing) in source files."""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
import util
|
||||
|
||||
|
||||
_FILE_HEADER_RE = re.compile(
|
||||
r"""# Copyright \(C\) 20[0-9]{2} The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2\.0 \(the "License"\);
|
||||
# you may not use this file except in compliance with the License\.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www\.apache\.org/licenses/LICENSE-2\.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License\.
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def check_license(path: Path, lines: list[str]) -> bool:
|
||||
"""Check license header."""
|
||||
# Enforce licensing on configs & scripts.
|
||||
if not (
|
||||
path.suffix in (".bash", ".cfg", ".ini", ".py", ".toml")
|
||||
or lines[0] in ("#!/bin/bash", "#!/bin/sh", "#!/usr/bin/env python3")
|
||||
):
|
||||
return True
|
||||
|
||||
# Extract the file header.
|
||||
header_lines = []
|
||||
for line in lines:
|
||||
if line.startswith("#"):
|
||||
header_lines.append(line)
|
||||
else:
|
||||
break
|
||||
if not header_lines:
|
||||
print(
|
||||
f"error: {path.relative_to(util.TOPDIR)}: "
|
||||
"missing file header (copyright+licensing)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return False
|
||||
|
||||
# Skip the shebang.
|
||||
if header_lines[0].startswith("#!"):
|
||||
header_lines.pop(0)
|
||||
|
||||
# If this file is imported into the tree, then leave it be.
|
||||
if header_lines[0] == "# DO NOT EDIT THIS FILE":
|
||||
return True
|
||||
|
||||
header = "".join(f"{x}\n" for x in header_lines)
|
||||
if not _FILE_HEADER_RE.match(header):
|
||||
print(
|
||||
f"error: {path.relative_to(util.TOPDIR)}: "
|
||||
"file header incorrectly formatted",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
"".join(f"> {x}\n" for x in header_lines), end="", file=sys.stderr
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_path(opts: argparse.Namespace, path: Path) -> bool:
|
||||
"""Check a single path."""
|
||||
try:
|
||||
data = path.read_text(encoding="utf-8")
|
||||
except FileNotFoundError:
|
||||
return True
|
||||
lines = data.splitlines()
|
||||
# NB: Use list comprehension and not a generator so we run all the checks.
|
||||
return all(
|
||||
[
|
||||
check_license(path, lines),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def check_paths(opts: argparse.Namespace, paths: list[Path]) -> bool:
|
||||
"""Check all the paths."""
|
||||
# NB: Use list comprehension and not a generator so we check all paths.
|
||||
return all([check_path(opts, x) for x in paths]) # noqa: C419
|
||||
|
||||
|
||||
def find_files(opts: argparse.Namespace) -> list[Path]:
|
||||
"""Find all the files in the source tree."""
|
||||
result = util.run(
|
||||
opts,
|
||||
["git", "ls-tree", "-r", "-z", "--name-only", "HEAD"],
|
||||
cwd=util.TOPDIR,
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
return [util.TOPDIR / x for x in result.stdout.split("\0")[:-1]]
|
||||
|
||||
|
||||
def get_parser() -> argparse.ArgumentParser:
|
||||
"""Get a CLI parser."""
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--dry-run",
|
||||
dest="dryrun",
|
||||
action="store_true",
|
||||
help="show everything that would be done",
|
||||
)
|
||||
parser.add_argument(
|
||||
"paths",
|
||||
nargs="*",
|
||||
help="the paths to scan",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
"""The main func!"""
|
||||
parser = get_parser()
|
||||
opts = parser.parse_args(argv)
|
||||
|
||||
paths = opts.paths
|
||||
if not opts.paths:
|
||||
paths = find_files(opts)
|
||||
|
||||
return 0 if check_paths(opts, paths) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
@@ -27,6 +27,9 @@ import sys
|
||||
import util
|
||||
|
||||
|
||||
assert sys.version_info >= (3, 9), "Release framework requires Python 3.9+"
|
||||
|
||||
|
||||
def sign(opts):
|
||||
"""Sign the launcher!"""
|
||||
output = ""
|
||||
@@ -82,18 +85,18 @@ Repo launcher bucket:
|
||||
gs://git-repo-downloads/
|
||||
|
||||
You should first upload it with a specific version:
|
||||
gsutil cp -a public-read {opts.launcher} gs://git-repo-downloads/repo-{version}
|
||||
gsutil cp -a public-read {opts.launcher}.asc gs://git-repo-downloads/repo-{version}.asc
|
||||
gcloud storage cp --predefined-acl=publicRead {opts.launcher} gs://git-repo-downloads/repo-{version}
|
||||
gcloud storage cp --predefined-acl=publicRead {opts.launcher}.asc gs://git-repo-downloads/repo-{version}.asc
|
||||
|
||||
Then to make it the public default:
|
||||
gsutil cp -a public-read gs://git-repo-downloads/repo-{version} gs://git-repo-downloads/repo
|
||||
gsutil cp -a public-read gs://git-repo-downloads/repo-{version}.asc gs://git-repo-downloads/repo.asc
|
||||
gcloud storage cp --predefined-acl=publicRead gs://git-repo-downloads/repo-{version} gs://git-repo-downloads/repo
|
||||
gcloud storage cp --predefined-acl=publicRead gs://git-repo-downloads/repo-{version}.asc gs://git-repo-downloads/repo.asc
|
||||
|
||||
NB: If a rollback is necessary, the GS bucket archives old versions, and may be
|
||||
accessed by specifying their unique id number.
|
||||
gsutil ls -la gs://git-repo-downloads/repo gs://git-repo-downloads/repo.asc
|
||||
gsutil cp -a public-read gs://git-repo-downloads/repo#<unique id> gs://git-repo-downloads/repo
|
||||
gsutil cp -a public-read gs://git-repo-downloads/repo.asc#<unique id> gs://git-repo-downloads/repo.asc
|
||||
gcloud storage ls --long --all-versions gs://git-repo-downloads/repo gs://git-repo-downloads/repo.asc
|
||||
gcloud storage cp --predefined-acl=publicRead gs://git-repo-downloads/repo#<unique id> gs://git-repo-downloads/repo
|
||||
gcloud storage cp --predefined-acl=publicRead gs://git-repo-downloads/repo.asc#<unique id> gs://git-repo-downloads/repo.asc
|
||||
""" # noqa: E501
|
||||
)
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ import sys
|
||||
import util
|
||||
|
||||
|
||||
assert sys.version_info >= (3, 9), "Release framework requires Python 3.9+"
|
||||
|
||||
|
||||
# We currently sign with the old DSA key as it's been around the longest.
|
||||
# We should transition to RSA by Jun 2020, and ECC by Jun 2021.
|
||||
KEYID = util.KEYID_DSA
|
||||
|
||||
@@ -24,7 +24,7 @@ from typing import List, Optional
|
||||
import urllib.request
|
||||
|
||||
|
||||
assert sys.version_info >= (3, 8), "Python 3.8+ required"
|
||||
assert sys.version_info >= (3, 9), "Release framework requires Python 3.9+"
|
||||
|
||||
|
||||
TOPDIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
@@ -27,9 +27,14 @@ import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import List
|
||||
|
||||
|
||||
TOPDIR = Path(__file__).resolve().parent.parent
|
||||
assert sys.version_info >= (3, 9), "Release framework requires Python 3.9+"
|
||||
|
||||
|
||||
THIS_FILE = Path(__file__).resolve()
|
||||
TOPDIR = THIS_FILE.parent.parent
|
||||
MANDIR = TOPDIR.joinpath("man")
|
||||
|
||||
# Load repo local modules.
|
||||
@@ -42,11 +47,29 @@ def worker(cmd, **kwargs):
|
||||
subprocess.run(cmd, **kwargs)
|
||||
|
||||
|
||||
def main(argv):
|
||||
def get_parser() -> argparse.ArgumentParser:
|
||||
"""Get argument parser."""
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.parse_args(argv)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--check",
|
||||
"--dry-run",
|
||||
action="store_const",
|
||||
const=True,
|
||||
help="Check if changes are necessary; don't actually change files",
|
||||
)
|
||||
return parser
|
||||
|
||||
if not shutil.which("help2man"):
|
||||
|
||||
def main(argv: List[str]) -> int:
|
||||
parser = get_parser()
|
||||
opts = parser.parse_args(argv)
|
||||
|
||||
help2man = ["help2man"]
|
||||
cipd_help2man = TOPDIR / ".cipd_bin/bin/help2man"
|
||||
if cipd_help2man.exists():
|
||||
help2man = [cipd_help2man]
|
||||
elif not shutil.which("help2man"):
|
||||
sys.exit("Please install help2man to continue.")
|
||||
|
||||
# Let repo know we're generating man pages so it can avoid some dynamic
|
||||
@@ -61,7 +84,7 @@ def main(argv):
|
||||
version = RepoSourceVersion()
|
||||
cmdlist = [
|
||||
[
|
||||
"help2man",
|
||||
*help2man,
|
||||
"-N",
|
||||
"-n",
|
||||
f"repo {cmd} - manual page for repo {cmd}",
|
||||
@@ -80,7 +103,7 @@ def main(argv):
|
||||
]
|
||||
cmdlist.append(
|
||||
[
|
||||
"help2man",
|
||||
*help2man,
|
||||
"-N",
|
||||
"-n",
|
||||
"repository management tool built on top of git",
|
||||
@@ -117,6 +140,7 @@ def main(argv):
|
||||
functools.partial(worker, cwd=tempdir, check=True), cmdlist
|
||||
)
|
||||
|
||||
ret = 0
|
||||
for tmp_path in MANDIR.glob("*.1.tmp"):
|
||||
path = tmp_path.parent / tmp_path.stem
|
||||
old_data = path.read_text() if path.exists() else ""
|
||||
@@ -133,7 +157,17 @@ def main(argv):
|
||||
)
|
||||
new_data = re.sub(r'^(\.TH REPO "1" ")([^"]+)', r"\1", data, flags=re.M)
|
||||
if old_data != new_data:
|
||||
path.write_text(data)
|
||||
if opts.check:
|
||||
ret = 1
|
||||
print(
|
||||
f"{THIS_FILE.name}: {path.name}: "
|
||||
"man page needs regenerating",
|
||||
file=sys.stderr,
|
||||
)
|
||||
else:
|
||||
path.write_text(data)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def replace_regex(data):
|
||||
@@ -147,7 +181,6 @@ def replace_regex(data):
|
||||
"""
|
||||
regex = (
|
||||
(r"(It was generated by help2man) [0-9.]+", r"\g<1>."),
|
||||
(r"^\033\[[0-9;]*m([^\033]*)\033\[m", r"\g<1>"),
|
||||
(r"^\.IP\n(.*:)\n", r".SS \g<1>\n"),
|
||||
(r"^\.PP\nDescription", r".SH DETAILS"),
|
||||
)
|
||||
|
||||
+7
-10
@@ -14,8 +14,9 @@
|
||||
|
||||
"""Random utility code for release tools."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
@@ -23,8 +24,9 @@ import sys
|
||||
assert sys.version_info >= (3, 6), "This module requires Python 3.6+"
|
||||
|
||||
|
||||
TOPDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
HOMEDIR = os.path.expanduser("~")
|
||||
THIS_FILE = Path(__file__).resolve()
|
||||
TOPDIR = THIS_FILE.parent.parent
|
||||
HOMEDIR = Path("~").expanduser()
|
||||
|
||||
|
||||
# These are the release keys we sign with.
|
||||
@@ -35,12 +37,7 @@ KEYID_ECC = "E1F9040D7A3F6DAFAC897CD3D3B95DA243E48A39"
|
||||
|
||||
def cmdstr(cmd):
|
||||
"""Get a nicely quoted shell command."""
|
||||
ret = []
|
||||
for arg in cmd:
|
||||
if not re.match(r"^[a-zA-Z0-9/_.=-]+$", arg):
|
||||
arg = f'"{arg}"'
|
||||
ret.append(arg)
|
||||
return " ".join(ret)
|
||||
return " ".join(shlex.quote(x) for x in cmd)
|
||||
|
||||
|
||||
def run(opts, cmd, check=True, **kwargs):
|
||||
@@ -58,7 +55,7 @@ def run(opts, cmd, check=True, **kwargs):
|
||||
def import_release_key(opts):
|
||||
"""Import the public key of the official release repo signing key."""
|
||||
# Extract the key from our repo launcher.
|
||||
launcher = getattr(opts, "launcher", os.path.join(TOPDIR, "repo"))
|
||||
launcher = getattr(opts, "launcher", TOPDIR / "repo")
|
||||
print(f'Importing keys from "{launcher}" launcher script')
|
||||
with open(launcher, encoding="utf-8") as fp:
|
||||
data = fp.read()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (C) 2008 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -27,6 +26,7 @@ import platform
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
# These should never be newer than the main.py version since this needs to be a
|
||||
@@ -56,9 +56,14 @@ class Trace:
|
||||
trace = Trace()
|
||||
|
||||
|
||||
def cmdstr(cmd):
|
||||
"""Get a nicely quoted shell command."""
|
||||
return " ".join(shlex.quote(x) for x in cmd)
|
||||
|
||||
|
||||
def exec_command(cmd):
|
||||
"""Execute |cmd| or return None on failure."""
|
||||
trace.print(":", " ".join(cmd))
|
||||
trace.print(":", cmdstr(cmd))
|
||||
try:
|
||||
if platform.system() == "Windows":
|
||||
ret = subprocess.call(cmd)
|
||||
@@ -124,7 +129,7 @@ if not REPO_REV:
|
||||
BUG_URL = "https://issues.gerritcodereview.com/issues/new?component=1370071"
|
||||
|
||||
# increment this whenever we make important changes to this script
|
||||
VERSION = (2, 45)
|
||||
VERSION = (2, 54)
|
||||
|
||||
# increment this if the MAINTAINER_KEYS block is modified
|
||||
KEYRING_VERSION = (2, 3)
|
||||
@@ -215,11 +220,8 @@ repodir = ".repo" # name of repo's private directory
|
||||
S_repo = "repo" # special repo repository
|
||||
S_manifests = "manifests" # special manifest repository
|
||||
REPO_MAIN = S_repo + "/main.py" # main script
|
||||
GITC_CONFIG_FILE = "/gitc/.config"
|
||||
GITC_FS_ROOT_DIR = "/gitc/manifest-rw/"
|
||||
|
||||
|
||||
import collections
|
||||
import errno
|
||||
import json
|
||||
import optparse
|
||||
@@ -235,12 +237,9 @@ home_dot_repo = os.path.join(repo_config_dir, ".repoconfig")
|
||||
gpg_dir = os.path.join(home_dot_repo, "gnupg")
|
||||
|
||||
|
||||
def GetParser(gitc_init=False):
|
||||
def GetParser():
|
||||
"""Setup the CLI parser."""
|
||||
if gitc_init:
|
||||
sys.exit("repo: fatal: GITC not supported.")
|
||||
else:
|
||||
usage = "repo init [options] [-u] url"
|
||||
usage = "repo init [options] [-u] url"
|
||||
|
||||
parser = optparse.OptionParser(usage=usage)
|
||||
InitParser(parser)
|
||||
@@ -282,6 +281,12 @@ def InitParser(parser):
|
||||
metavar="REVISION",
|
||||
help="manifest branch or revision (use HEAD for default)",
|
||||
)
|
||||
group.add_option(
|
||||
"--manifest-upstream-branch",
|
||||
help="when a commit is provided to --manifest-branch, this "
|
||||
"is the name of the git ref in which the commit can be found",
|
||||
metavar="BRANCH",
|
||||
)
|
||||
group.add_option(
|
||||
"-m",
|
||||
"--manifest-name",
|
||||
@@ -481,16 +486,6 @@ def InitParser(parser):
|
||||
return parser
|
||||
|
||||
|
||||
# This is a poor replacement for subprocess.run until we require Python 3.6+.
|
||||
RunResult = collections.namedtuple(
|
||||
"RunResult", ("returncode", "stdout", "stderr")
|
||||
)
|
||||
|
||||
|
||||
class RunError(Exception):
|
||||
"""Error when running a command failed."""
|
||||
|
||||
|
||||
def run_command(cmd, **kwargs):
|
||||
"""Run |cmd| and return its output."""
|
||||
check = kwargs.pop("check", False)
|
||||
@@ -515,7 +510,7 @@ def run_command(cmd, **kwargs):
|
||||
# Run & package the results.
|
||||
proc = subprocess.Popen(cmd, **kwargs)
|
||||
(stdout, stderr) = proc.communicate(input=cmd_input)
|
||||
dbg = ": " + " ".join(cmd)
|
||||
dbg = ": " + cmdstr(cmd)
|
||||
if cmd_input is not None:
|
||||
dbg += " 0<|"
|
||||
if stdout == subprocess.PIPE:
|
||||
@@ -525,7 +520,9 @@ def run_command(cmd, **kwargs):
|
||||
elif stderr == subprocess.STDOUT:
|
||||
dbg += " 2>&1"
|
||||
trace.print(dbg)
|
||||
ret = RunResult(proc.returncode, decode(stdout), decode(stderr))
|
||||
ret = subprocess.CompletedProcess(
|
||||
cmd, proc.returncode, decode(stdout), decode(stderr)
|
||||
)
|
||||
|
||||
# If things failed, print useful debugging output.
|
||||
if check and ret.returncode:
|
||||
@@ -546,56 +543,13 @@ def run_command(cmd, **kwargs):
|
||||
|
||||
_print_output("stdout", ret.stdout)
|
||||
_print_output("stderr", ret.stderr)
|
||||
raise RunError(ret)
|
||||
# This will raise subprocess.CalledProcessError for us.
|
||||
ret.check_returncode()
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
_gitc_manifest_dir = None
|
||||
|
||||
|
||||
def get_gitc_manifest_dir():
|
||||
global _gitc_manifest_dir
|
||||
if _gitc_manifest_dir is None:
|
||||
_gitc_manifest_dir = ""
|
||||
try:
|
||||
with open(GITC_CONFIG_FILE) as gitc_config:
|
||||
for line in gitc_config:
|
||||
match = re.match("gitc_dir=(?P<gitc_manifest_dir>.*)", line)
|
||||
if match:
|
||||
_gitc_manifest_dir = match.group("gitc_manifest_dir")
|
||||
except OSError:
|
||||
pass
|
||||
return _gitc_manifest_dir
|
||||
|
||||
|
||||
def gitc_parse_clientdir(gitc_fs_path):
|
||||
"""Parse a path in the GITC FS and return its client name.
|
||||
|
||||
Args:
|
||||
gitc_fs_path: A subdirectory path within the GITC_FS_ROOT_DIR.
|
||||
|
||||
Returns:
|
||||
The GITC client name.
|
||||
"""
|
||||
if gitc_fs_path == GITC_FS_ROOT_DIR:
|
||||
return None
|
||||
if not gitc_fs_path.startswith(GITC_FS_ROOT_DIR):
|
||||
manifest_dir = get_gitc_manifest_dir()
|
||||
if manifest_dir == "":
|
||||
return None
|
||||
if manifest_dir[-1] != "/":
|
||||
manifest_dir += "/"
|
||||
if gitc_fs_path == manifest_dir:
|
||||
return None
|
||||
if not gitc_fs_path.startswith(manifest_dir):
|
||||
return None
|
||||
return gitc_fs_path.split(manifest_dir)[1].split("/")[0]
|
||||
return gitc_fs_path.split(GITC_FS_ROOT_DIR)[1].split("/")[0]
|
||||
|
||||
|
||||
class CloneFailure(Exception):
|
||||
|
||||
"""Indicate the remote clone of repo itself failed."""
|
||||
|
||||
|
||||
@@ -632,9 +586,9 @@ def check_repo_rev(dst, rev, repo_verify=True, quiet=False):
|
||||
return (remote_ref, rev)
|
||||
|
||||
|
||||
def _Init(args, gitc_init=False):
|
||||
def _Init(args):
|
||||
"""Installs repo by cloning it over the network."""
|
||||
parser = GetParser(gitc_init=gitc_init)
|
||||
parser = GetParser()
|
||||
opt, args = parser.parse_args(args)
|
||||
if args:
|
||||
if not opt.manifest_url:
|
||||
@@ -714,15 +668,20 @@ def run_git(*args, **kwargs):
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
except RunError:
|
||||
except subprocess.CalledProcessError:
|
||||
raise CloneFailure()
|
||||
|
||||
|
||||
# The git version info broken down into components for easy analysis.
|
||||
# Similar to Python's sys.version_info.
|
||||
GitVersion = collections.namedtuple(
|
||||
"GitVersion", ("major", "minor", "micro", "full")
|
||||
)
|
||||
class GitVersion(NamedTuple):
|
||||
"""The git version info broken down into components for easy analysis.
|
||||
|
||||
Similar to Python's sys.version_info.
|
||||
"""
|
||||
|
||||
major: int
|
||||
minor: int
|
||||
micro: int
|
||||
full: int
|
||||
|
||||
|
||||
def ParseGitVersion(ver_str=None):
|
||||
@@ -888,10 +847,11 @@ def _GetRepoConfig(name):
|
||||
return None
|
||||
else:
|
||||
print(
|
||||
f"repo: error: git {' '.join(cmd)} failed:\n{ret.stderr}",
|
||||
f"repo: error: git {cmdstr(cmd)} failed:\n{ret.stderr}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise RunError()
|
||||
# This will raise subprocess.CalledProcessError for us.
|
||||
ret.check_returncode()
|
||||
|
||||
|
||||
def _InitHttp():
|
||||
@@ -1158,7 +1118,7 @@ class _Options:
|
||||
def _ExpandAlias(name):
|
||||
"""Look up user registered aliases."""
|
||||
# We don't resolve aliases for existing subcommands. This matches git.
|
||||
if name in {"gitc-init", "help", "init"}:
|
||||
if name in {"help", "init"}:
|
||||
return name, []
|
||||
|
||||
alias = _GetRepoConfig(f"alias.{name}")
|
||||
@@ -1286,10 +1246,6 @@ class Requirements:
|
||||
|
||||
|
||||
def _Usage():
|
||||
gitc_usage = ""
|
||||
if get_gitc_manifest_dir():
|
||||
gitc_usage = " gitc-init Initialize a GITC Client.\n"
|
||||
|
||||
print(
|
||||
"""usage: repo COMMAND [ARGS]
|
||||
|
||||
@@ -1298,9 +1254,7 @@ repo is not yet installed. Use "repo init" to install it here.
|
||||
The most commonly used repo commands are:
|
||||
|
||||
init Install repo in the current working directory
|
||||
"""
|
||||
+ gitc_usage
|
||||
+ """ help Display detailed help on a command
|
||||
help Display detailed help on a command
|
||||
|
||||
For access to the full online help, install repo ("repo init").
|
||||
"""
|
||||
@@ -1311,8 +1265,8 @@ For access to the full online help, install repo ("repo init").
|
||||
|
||||
def _Help(args):
|
||||
if args:
|
||||
if args[0] in {"init", "gitc-init"}:
|
||||
parser = GetParser(gitc_init=args[0] == "gitc-init")
|
||||
if args[0] in {"init"}:
|
||||
parser = GetParser()
|
||||
parser.print_help()
|
||||
sys.exit(0)
|
||||
else:
|
||||
@@ -1329,10 +1283,11 @@ def _Help(args):
|
||||
|
||||
def _Version():
|
||||
"""Show version information."""
|
||||
git_version = ParseGitVersion()
|
||||
print("<repo not installed>")
|
||||
print(f"repo launcher version {'.'.join(str(x) for x in VERSION)}")
|
||||
print(f" (from {__file__})")
|
||||
print(f"git {ParseGitVersion().full}")
|
||||
print(f"git {git_version.full}" if git_version else "git not installed")
|
||||
print(f"Python {sys.version}")
|
||||
uname = platform.uname()
|
||||
print(f"OS {uname.system} {uname.release} ({uname.version})")
|
||||
@@ -1365,11 +1320,11 @@ def _RunSelf(wrapper_path):
|
||||
my_main = os.path.join(my_dir, "main.py")
|
||||
my_git = os.path.join(my_dir, ".git")
|
||||
|
||||
if os.path.isfile(my_main) and os.path.isdir(my_git):
|
||||
if os.path.isfile(my_main):
|
||||
for name in ["git_config.py", "project.py", "subcmds"]:
|
||||
if not os.path.exists(os.path.join(my_dir, name)):
|
||||
return None, None
|
||||
return my_main, my_git
|
||||
return my_main, my_git if os.path.isdir(my_git) else None
|
||||
return None, None
|
||||
|
||||
|
||||
@@ -1400,23 +1355,11 @@ def main(orig_args):
|
||||
# We run this early as we run some git commands ourselves.
|
||||
SetGitTrace2ParentSid()
|
||||
|
||||
repo_main, rel_repo_dir = None, None
|
||||
# Don't use the local repo copy, make sure to switch to the gitc client first.
|
||||
if cmd != "gitc-init":
|
||||
repo_main, rel_repo_dir = _FindRepo()
|
||||
repo_main, rel_repo_dir = _FindRepo()
|
||||
|
||||
wrapper_path = os.path.abspath(__file__)
|
||||
my_main, my_git = _RunSelf(wrapper_path)
|
||||
|
||||
cwd = os.getcwd()
|
||||
if get_gitc_manifest_dir() and cwd.startswith(get_gitc_manifest_dir()):
|
||||
print(
|
||||
"error: repo cannot be used in the GITC local manifest directory."
|
||||
"\nIf you want to work on this GITC client please rerun this "
|
||||
"command from the corresponding client under /gitc/",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if not repo_main:
|
||||
# Only expand aliases here since we'll be parsing the CLI ourselves.
|
||||
# If we had repo_main, alias expansion would happen in main.py.
|
||||
@@ -1431,11 +1374,11 @@ def main(orig_args):
|
||||
_Version()
|
||||
if not cmd:
|
||||
_NotInstalled()
|
||||
if cmd == "init" or cmd == "gitc-init":
|
||||
if cmd == "init":
|
||||
if my_git:
|
||||
_SetDefaultsTo(my_git)
|
||||
try:
|
||||
_Init(args, gitc_init=(cmd == "gitc-init"))
|
||||
_Init(args)
|
||||
except CloneFailure:
|
||||
path = os.path.join(repodir, S_repo)
|
||||
print(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2019 The Android Open Source Project
|
||||
# Copyright (C) 2019 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,18 +15,80 @@
|
||||
|
||||
"""Wrapper to run linters and pytest with the right settings."""
|
||||
|
||||
import functools
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
# NB: While tests/* support Python >=3.6 to match requirements.json for `repo`,
|
||||
# the higher level runner logic does not need to be held back.
|
||||
assert sys.version_info >= (3, 9), "Test/release framework requires Python 3.9+"
|
||||
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
|
||||
def log_cmd(cmd: str, argv: list[str]) -> None:
|
||||
"""Log a debug message to make history easier to track."""
|
||||
print("+", cmd, shlex.join(argv), file=sys.stderr)
|
||||
|
||||
|
||||
@functools.lru_cache()
|
||||
def is_ci() -> bool:
|
||||
"""Whether we're running in our CI system."""
|
||||
return os.getenv("LUCI_CQ") == "yes"
|
||||
|
||||
|
||||
def run_pytest(argv: list[str]) -> int:
|
||||
"""Returns the exit code from pytest."""
|
||||
if is_ci():
|
||||
argv = ["-m", "not skip_cq"] + argv
|
||||
|
||||
log_cmd("pytest", argv)
|
||||
return subprocess.run(
|
||||
[sys.executable, "-m", "pytest"] + argv,
|
||||
check=False,
|
||||
cwd=ROOT_DIR,
|
||||
).returncode
|
||||
|
||||
|
||||
def run_pytest_py38(argv: list[str]) -> int:
|
||||
"""Returns the exit code from pytest under Python 3.8."""
|
||||
if is_ci():
|
||||
argv = ["-m", "not skip_cq"] + argv
|
||||
|
||||
log_cmd("[vpython 3.8] pytest", argv)
|
||||
try:
|
||||
return subprocess.run(
|
||||
[
|
||||
"vpython3",
|
||||
"-vpython-spec",
|
||||
"run_tests.vpython3.8",
|
||||
"-m",
|
||||
"pytest",
|
||||
]
|
||||
+ argv,
|
||||
check=False,
|
||||
cwd=ROOT_DIR,
|
||||
).returncode
|
||||
except FileNotFoundError:
|
||||
# Skip if the user doesn't have vpython from depot_tools.
|
||||
return 0
|
||||
|
||||
|
||||
def run_black():
|
||||
"""Returns the exit code from black."""
|
||||
argv = ["--version"]
|
||||
log_cmd("black", argv)
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "black"] + argv,
|
||||
check=True,
|
||||
cwd=ROOT_DIR,
|
||||
)
|
||||
|
||||
# Black by default only matches .py files. We have to list standalone
|
||||
# scripts manually.
|
||||
extra_programs = [
|
||||
@@ -35,35 +97,105 @@ def run_black():
|
||||
"release/update-hooks",
|
||||
"release/update-manpages",
|
||||
]
|
||||
argv = ["--diff", "--check", ROOT_DIR] + extra_programs
|
||||
log_cmd("black", argv)
|
||||
return subprocess.run(
|
||||
[sys.executable, "-m", "black", "--check", ROOT_DIR] + extra_programs,
|
||||
[sys.executable, "-m", "black"] + argv,
|
||||
check=False,
|
||||
cwd=ROOT_DIR,
|
||||
).returncode
|
||||
|
||||
|
||||
def run_flake8():
|
||||
"""Returns the exit code from flake8."""
|
||||
argv = ["--version"]
|
||||
log_cmd("flake8", argv)
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "flake8"] + argv,
|
||||
check=True,
|
||||
cwd=ROOT_DIR,
|
||||
)
|
||||
|
||||
argv = [ROOT_DIR]
|
||||
log_cmd("flake8", argv)
|
||||
return subprocess.run(
|
||||
[sys.executable, "-m", "flake8", ROOT_DIR], check=False
|
||||
[sys.executable, "-m", "flake8"] + argv,
|
||||
check=False,
|
||||
cwd=ROOT_DIR,
|
||||
).returncode
|
||||
|
||||
|
||||
def run_isort():
|
||||
"""Returns the exit code from isort."""
|
||||
argv = ["--version-number"]
|
||||
log_cmd("isort", argv)
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "isort"] + argv,
|
||||
check=True,
|
||||
cwd=ROOT_DIR,
|
||||
)
|
||||
|
||||
argv = ["--check", ROOT_DIR]
|
||||
log_cmd("isort", argv)
|
||||
return subprocess.run(
|
||||
[sys.executable, "-m", "isort", "--check", ROOT_DIR], check=False
|
||||
[sys.executable, "-m", "isort"] + argv,
|
||||
check=False,
|
||||
cwd=ROOT_DIR,
|
||||
).returncode
|
||||
|
||||
|
||||
def run_check_metadata():
|
||||
"""Returns the exit code from check-metadata."""
|
||||
argv = []
|
||||
log_cmd("release/check-metadata.py", argv)
|
||||
return subprocess.run(
|
||||
[sys.executable, "release/check-metadata.py"] + argv,
|
||||
check=False,
|
||||
cwd=ROOT_DIR,
|
||||
).returncode
|
||||
|
||||
|
||||
def run_update_manpages() -> int:
|
||||
"""Returns the exit code from release/update-manpages."""
|
||||
argv = ["--check"]
|
||||
log_cmd("release/update-manpages", argv)
|
||||
return subprocess.run(
|
||||
[sys.executable, "release/update-manpages"] + argv,
|
||||
check=False,
|
||||
cwd=ROOT_DIR,
|
||||
).returncode
|
||||
|
||||
|
||||
def cipd_bootstrap() -> None:
|
||||
"""Install packages via cipd if available."""
|
||||
argv = ["ensure", "-root", ".cipd_bin", "-ensure-file", "cipd_manifest.txt"]
|
||||
log_cmd("cipd", argv)
|
||||
try:
|
||||
subprocess.run(
|
||||
["cipd"] + argv,
|
||||
check=True,
|
||||
cwd=ROOT_DIR,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
# Skip if the user doesn't have cipd from depot_tools.
|
||||
return
|
||||
|
||||
|
||||
def main(argv):
|
||||
"""The main entry."""
|
||||
cipd_bootstrap()
|
||||
checks = (
|
||||
lambda: pytest.main(argv),
|
||||
functools.partial(run_pytest, argv),
|
||||
functools.partial(run_pytest_py38, argv),
|
||||
run_black,
|
||||
run_flake8,
|
||||
run_isort,
|
||||
run_check_metadata,
|
||||
run_update_manpages,
|
||||
)
|
||||
return 0 if all(not c() for c in checks) else 1
|
||||
# Run all the tests all the time to get full feedback. Don't exit on the
|
||||
# first error as that makes it more difficult to iterate in the CQ.
|
||||
return 1 if sum(c() for c in checks) else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+31
-26
@@ -5,97 +5,92 @@
|
||||
# List of available wheels:
|
||||
# https://chromium.googlesource.com/infra/infra/+/main/infra/tools/dockerbuild/wheels.md
|
||||
|
||||
python_version: "3.8"
|
||||
python_version: "3.11"
|
||||
|
||||
wheel: <
|
||||
name: "infra/python/wheels/pytest-py3"
|
||||
version: "version:6.2.2"
|
||||
version: "version:8.3.4"
|
||||
>
|
||||
|
||||
# Required by pytest==6.2.2
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/py-py2_py3"
|
||||
version: "version:1.10.0"
|
||||
version: "version:1.11.0"
|
||||
>
|
||||
|
||||
# Required by pytest==6.2.2
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/iniconfig-py3"
|
||||
version: "version:1.1.1"
|
||||
>
|
||||
|
||||
# Required by pytest==6.2.2
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/packaging-py3"
|
||||
version: "version:23.0"
|
||||
>
|
||||
|
||||
# Required by pytest==6.2.2
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/pluggy-py3"
|
||||
version: "version:0.13.1"
|
||||
version: "version:1.5.0"
|
||||
>
|
||||
|
||||
# Required by pytest==6.2.2
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/toml-py3"
|
||||
version: "version:0.10.1"
|
||||
>
|
||||
|
||||
# Required by pytest==6.2.2
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/pyparsing-py3"
|
||||
version: "version:3.0.7"
|
||||
>
|
||||
|
||||
# Required by pytest==6.2.2
|
||||
# Required by pytest==8.3.4 and flake8-bugbear==24.12.12
|
||||
wheel: <
|
||||
name: "infra/python/wheels/attrs-py2_py3"
|
||||
version: "version:21.4.0"
|
||||
>
|
||||
|
||||
# Required by packaging==16.8
|
||||
wheel: <
|
||||
name: "infra/python/wheels/six-py2_py3"
|
||||
version: "version:1.16.0"
|
||||
name: "infra/python/wheels/attrs-py3"
|
||||
version: "version:24.2.0"
|
||||
>
|
||||
|
||||
# NB: Keep in sync with constraints.txt.
|
||||
wheel: <
|
||||
name: "infra/python/wheels/black-py3"
|
||||
version: "version:23.1.0"
|
||||
version: "version:25.1.0"
|
||||
>
|
||||
|
||||
# Required by black==23.1.0
|
||||
# Required by black==25.1.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/mypy-extensions-py3"
|
||||
version: "version:0.4.3"
|
||||
>
|
||||
|
||||
# Required by black==23.1.0
|
||||
# Required by black==25.1.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/tomli-py3"
|
||||
version: "version:2.0.1"
|
||||
>
|
||||
|
||||
# Required by black==23.1.0
|
||||
# Required by black==25.1.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/platformdirs-py3"
|
||||
version: "version:2.5.2"
|
||||
>
|
||||
|
||||
# Required by black==23.1.0
|
||||
# Required by black==25.1.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/pathspec-py3"
|
||||
version: "version:0.9.0"
|
||||
>
|
||||
|
||||
# Required by black==23.1.0
|
||||
# Required by black==25.1.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/typing-extensions-py3"
|
||||
version: "version:4.3.0"
|
||||
>
|
||||
|
||||
# Required by black==23.1.0
|
||||
# Required by black==25.1.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/click-py3"
|
||||
version: "version:8.0.3"
|
||||
@@ -124,6 +119,16 @@ wheel: <
|
||||
version: "version:2.10.0"
|
||||
>
|
||||
|
||||
wheel: <
|
||||
name: "infra/python/wheels/flake8-bugbear-py3"
|
||||
version: "version:24.12.12"
|
||||
>
|
||||
|
||||
wheel: <
|
||||
name: "infra/python/wheels/flake8-comprehensions-py3"
|
||||
version: "version:3.16.0"
|
||||
>
|
||||
|
||||
wheel: <
|
||||
name: "infra/python/wheels/isort-py3"
|
||||
version: "version:5.10.1"
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# This is a vpython "spec" file.
|
||||
#
|
||||
# Read more about `vpython` and how to modify this file here:
|
||||
# https://chromium.googlesource.com/infra/infra/+/main/doc/users/vpython.md
|
||||
# List of available wheels:
|
||||
# https://chromium.googlesource.com/infra/infra/+/main/infra/tools/dockerbuild/wheels.md
|
||||
|
||||
python_version: "3.8"
|
||||
|
||||
wheel: <
|
||||
name: "infra/python/wheels/pytest-py3"
|
||||
version: "version:8.3.4"
|
||||
>
|
||||
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/py-py2_py3"
|
||||
version: "version:1.11.0"
|
||||
>
|
||||
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/iniconfig-py3"
|
||||
version: "version:1.1.1"
|
||||
>
|
||||
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/packaging-py3"
|
||||
version: "version:23.0"
|
||||
>
|
||||
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/pluggy-py3"
|
||||
version: "version:1.5.0"
|
||||
>
|
||||
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/toml-py3"
|
||||
version: "version:0.10.1"
|
||||
>
|
||||
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/tomli-py3"
|
||||
version: "version:2.1.0"
|
||||
>
|
||||
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/pyparsing-py3"
|
||||
version: "version:3.0.7"
|
||||
>
|
||||
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/attrs-py2_py3"
|
||||
version: "version:21.4.0"
|
||||
>
|
||||
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/exceptiongroup-py3"
|
||||
version: "version:1.1.2"
|
||||
>
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2019 The Android Open Source Project
|
||||
# Copyright (C) 2019 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the 'License");
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
|
||||
@@ -52,12 +52,12 @@ def _parse_ssh_version(ver_str=None):
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def version():
|
||||
"""return ssh version as a tuple"""
|
||||
"""Return ssh version as a tuple.
|
||||
|
||||
If ssh is not available, a FileNotFoundError will be raised.
|
||||
"""
|
||||
try:
|
||||
return _parse_ssh_version()
|
||||
except FileNotFoundError:
|
||||
print("fatal: ssh not installed", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(
|
||||
"fatal: unable to detect ssh version"
|
||||
@@ -102,9 +102,18 @@ class ProxyManager:
|
||||
self._clients = manager.list()
|
||||
# Path to directory for holding master sockets.
|
||||
self._sock_path = None
|
||||
# See if ssh is usable.
|
||||
self._ssh_installed = False
|
||||
|
||||
def __enter__(self):
|
||||
"""Enter a new context."""
|
||||
# Check which version of ssh is available.
|
||||
try:
|
||||
version()
|
||||
self._ssh_installed = True
|
||||
except FileNotFoundError:
|
||||
self._ssh_installed = False
|
||||
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
@@ -140,7 +149,7 @@ class ProxyManager:
|
||||
while True:
|
||||
try:
|
||||
procs.pop(0)
|
||||
except: # noqa: E722
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
def close(self):
|
||||
@@ -282,6 +291,9 @@ class ProxyManager:
|
||||
|
||||
def preconnect(self, url):
|
||||
"""If |uri| will create a ssh connection, setup the ssh master for it.""" # noqa: E501
|
||||
if not self._ssh_installed:
|
||||
return False
|
||||
|
||||
m = URI_ALL.match(url)
|
||||
if m:
|
||||
scheme = m.group(1)
|
||||
@@ -306,6 +318,9 @@ class ProxyManager:
|
||||
|
||||
This has all the master sockets so clients can talk to them.
|
||||
"""
|
||||
if not self._ssh_installed:
|
||||
return None
|
||||
|
||||
if self._sock_path is None:
|
||||
if not create:
|
||||
return None
|
||||
|
||||
+19
-13
@@ -48,7 +48,6 @@ It is equivalent to "git branch -D <branchname>".
|
||||
def _Options(self, p):
|
||||
p.add_option(
|
||||
"--all",
|
||||
dest="all",
|
||||
action="store_true",
|
||||
help="delete all branches in all projects",
|
||||
)
|
||||
@@ -70,8 +69,10 @@ It is equivalent to "git branch -D <branchname>".
|
||||
else:
|
||||
args.insert(0, "'All local branches'")
|
||||
|
||||
def _ExecuteOne(self, all_branches, nb, project):
|
||||
@classmethod
|
||||
def _ExecuteOne(cls, all_branches, nb, project_idx):
|
||||
"""Abandon one project."""
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
if all_branches:
|
||||
branches = project.GetBranches()
|
||||
else:
|
||||
@@ -89,10 +90,11 @@ It is equivalent to "git branch -D <branchname>".
|
||||
if status is not None:
|
||||
ret[name] = status
|
||||
|
||||
return (ret, project, errors)
|
||||
return (ret, project_idx, errors)
|
||||
|
||||
def Execute(self, opt, args):
|
||||
nb = args[0].split()
|
||||
self.TryOverrideManifestWithSmartSync()
|
||||
err = collections.defaultdict(list)
|
||||
success = collections.defaultdict(list)
|
||||
aggregate_errors = []
|
||||
@@ -102,7 +104,8 @@ It is equivalent to "git branch -D <branchname>".
|
||||
_RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
|
||||
|
||||
def _ProcessResults(_pool, pm, states):
|
||||
for results, project, errors in states:
|
||||
for results, project_idx, errors in states:
|
||||
project = all_projects[project_idx]
|
||||
for branch, status in results.items():
|
||||
if status:
|
||||
success[branch].append(project)
|
||||
@@ -111,15 +114,18 @@ It is equivalent to "git branch -D <branchname>".
|
||||
aggregate_errors.extend(errors)
|
||||
pm.update(msg="")
|
||||
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._ExecuteOne, opt.all, nb),
|
||||
all_projects,
|
||||
callback=_ProcessResults,
|
||||
output=Progress(
|
||||
f"Abandon {nb}", len(all_projects), quiet=opt.quiet
|
||||
),
|
||||
)
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = all_projects
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._ExecuteOne, opt.all, nb),
|
||||
range(len(all_projects)),
|
||||
callback=_ProcessResults,
|
||||
output=Progress(
|
||||
f"Abandon {nb}", len(all_projects), quiet=opt.quiet
|
||||
),
|
||||
chunksize=1,
|
||||
)
|
||||
|
||||
width = max(
|
||||
itertools.chain(
|
||||
|
||||
+30
-24
@@ -98,6 +98,22 @@ is shown, then the branch appears in all projects.
|
||||
"""
|
||||
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
|
||||
|
||||
@classmethod
|
||||
def _ExpandProjectToBranches(cls, project_idx):
|
||||
"""Expands a project into a list of branch names & associated info.
|
||||
|
||||
Args:
|
||||
project_idx: project.Project index
|
||||
|
||||
Returns:
|
||||
List[Tuple[str, git_config.Branch, int]]
|
||||
"""
|
||||
branches = []
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
for name, b in project.GetBranches().items():
|
||||
branches.append((name, b, project_idx))
|
||||
return branches
|
||||
|
||||
def Execute(self, opt, args):
|
||||
projects = self.GetProjects(
|
||||
args, all_manifests=not opt.this_manifest_only
|
||||
@@ -107,17 +123,20 @@ is shown, then the branch appears in all projects.
|
||||
project_cnt = len(projects)
|
||||
|
||||
def _ProcessResults(_pool, _output, results):
|
||||
for name, b in itertools.chain.from_iterable(results):
|
||||
for name, b, project_idx in itertools.chain.from_iterable(results):
|
||||
b.project = projects[project_idx]
|
||||
if name not in all_branches:
|
||||
all_branches[name] = BranchInfo(name)
|
||||
all_branches[name].add(b)
|
||||
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
expand_project_to_branches,
|
||||
projects,
|
||||
callback=_ProcessResults,
|
||||
)
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = projects
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
self._ExpandProjectToBranches,
|
||||
range(len(projects)),
|
||||
callback=_ProcessResults,
|
||||
)
|
||||
|
||||
names = sorted(all_branches)
|
||||
|
||||
@@ -148,7 +167,10 @@ is shown, then the branch appears in all projects.
|
||||
else:
|
||||
published = " "
|
||||
|
||||
hdr("%c%c %-*s" % (current, published, width, name))
|
||||
# A branch name can contain a percent sign, so we need to escape it.
|
||||
# Escape after f-string formatting to properly account for leading
|
||||
# spaces.
|
||||
hdr(f"{current}{published} {name:{width}}".replace("%", "%%"))
|
||||
out.write(" |")
|
||||
|
||||
_RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
|
||||
@@ -191,19 +213,3 @@ is shown, then the branch appears in all projects.
|
||||
else:
|
||||
out.write(" in all projects")
|
||||
out.nl()
|
||||
|
||||
|
||||
def expand_project_to_branches(project):
|
||||
"""Expands a project into a list of branch names & associated information.
|
||||
|
||||
Args:
|
||||
project: project.Project
|
||||
|
||||
Returns:
|
||||
List[Tuple[str, git_config.Branch]]
|
||||
"""
|
||||
branches = []
|
||||
for name, b in project.GetBranches().items():
|
||||
b.project = project
|
||||
branches.append((name, b))
|
||||
return branches
|
||||
|
||||
+19
-15
@@ -20,7 +20,6 @@ from command import DEFAULT_LOCAL_JOBS
|
||||
from error import GitError
|
||||
from error import RepoExitError
|
||||
from progress import Progress
|
||||
from project import Project
|
||||
from repo_logging import RepoLogger
|
||||
|
||||
|
||||
@@ -30,7 +29,7 @@ logger = RepoLogger(__file__)
|
||||
class CheckoutBranchResult(NamedTuple):
|
||||
# Whether the Project is on the branch (i.e. branch exists and no errors)
|
||||
result: bool
|
||||
project: Project
|
||||
project_idx: int
|
||||
error: Exception
|
||||
|
||||
|
||||
@@ -62,15 +61,17 @@ The command is equivalent to:
|
||||
if not args:
|
||||
self.Usage()
|
||||
|
||||
def _ExecuteOne(self, nb, project):
|
||||
@classmethod
|
||||
def _ExecuteOne(cls, nb, project_idx):
|
||||
"""Checkout one project."""
|
||||
error = None
|
||||
result = None
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
try:
|
||||
result = project.CheckoutBranch(nb)
|
||||
except GitError as e:
|
||||
error = e
|
||||
return CheckoutBranchResult(result, project, error)
|
||||
return CheckoutBranchResult(result, project_idx, error)
|
||||
|
||||
def Execute(self, opt, args):
|
||||
nb = args[0]
|
||||
@@ -83,22 +84,25 @@ The command is equivalent to:
|
||||
|
||||
def _ProcessResults(_pool, pm, results):
|
||||
for result in results:
|
||||
project = all_projects[result.project_idx]
|
||||
if result.error is not None:
|
||||
err.append(result.error)
|
||||
err_projects.append(result.project)
|
||||
err_projects.append(project)
|
||||
elif result.result:
|
||||
success.append(result.project)
|
||||
success.append(project)
|
||||
pm.update(msg="")
|
||||
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._ExecuteOne, nb),
|
||||
all_projects,
|
||||
callback=_ProcessResults,
|
||||
output=Progress(
|
||||
f"Checkout {nb}", len(all_projects), quiet=opt.quiet
|
||||
),
|
||||
)
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = all_projects
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._ExecuteOne, nb),
|
||||
range(len(all_projects)),
|
||||
callback=_ProcessResults,
|
||||
output=Progress(
|
||||
f"Checkout {nb}", len(all_projects), quiet=opt.quiet
|
||||
),
|
||||
)
|
||||
|
||||
if err_projects:
|
||||
for p in err_projects:
|
||||
|
||||
+16
-12
@@ -35,12 +35,12 @@ to the Unix 'patch' command.
|
||||
p.add_option(
|
||||
"-u",
|
||||
"--absolute",
|
||||
dest="absolute",
|
||||
action="store_true",
|
||||
help="paths are relative to the repository root",
|
||||
)
|
||||
|
||||
def _ExecuteOne(self, absolute, local, project):
|
||||
@classmethod
|
||||
def _ExecuteOne(cls, absolute, local, project_idx):
|
||||
"""Obtains the diff for a specific project.
|
||||
|
||||
Args:
|
||||
@@ -48,12 +48,13 @@ to the Unix 'patch' command.
|
||||
local: a boolean, if True, the path is relative to the local
|
||||
(sub)manifest. If false, the path is relative to the outermost
|
||||
manifest.
|
||||
project: Project to get status of.
|
||||
project_idx: Project index to get status of.
|
||||
|
||||
Returns:
|
||||
The status of the project.
|
||||
"""
|
||||
buf = io.StringIO()
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
ret = project.PrintWorkTreeDiff(absolute, output_redir=buf, local=local)
|
||||
return (ret, buf.getvalue())
|
||||
|
||||
@@ -71,12 +72,15 @@ to the Unix 'patch' command.
|
||||
ret = 1
|
||||
return ret
|
||||
|
||||
return self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(
|
||||
self._ExecuteOne, opt.absolute, opt.this_manifest_only
|
||||
),
|
||||
all_projects,
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
)
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = all_projects
|
||||
return self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(
|
||||
self._ExecuteOne, opt.absolute, opt.this_manifest_only
|
||||
),
|
||||
range(len(all_projects)),
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
chunksize=1,
|
||||
)
|
||||
|
||||
@@ -67,7 +67,9 @@ synced and their revisions won't be found.
|
||||
|
||||
def _Options(self, p):
|
||||
p.add_option(
|
||||
"--raw", dest="raw", action="store_true", help="display raw diff"
|
||||
"--raw",
|
||||
action="store_true",
|
||||
help="display raw diff",
|
||||
)
|
||||
p.add_option(
|
||||
"--no-color",
|
||||
@@ -78,7 +80,6 @@ synced and their revisions won't be found.
|
||||
)
|
||||
p.add_option(
|
||||
"--pretty-format",
|
||||
dest="pretty_format",
|
||||
action="store",
|
||||
metavar="<FORMAT>",
|
||||
help="print the log using a custom git pretty format string",
|
||||
@@ -233,9 +234,9 @@ synced and their revisions won't be found.
|
||||
)
|
||||
self.printRevision = self.out.nofmt_printer("revision", fg="yellow")
|
||||
else:
|
||||
self.printProject = (
|
||||
self.printAdded
|
||||
) = self.printRemoved = self.printRevision = self.printText
|
||||
self.printProject = self.printAdded = self.printRemoved = (
|
||||
self.printRevision
|
||||
) = self.printText
|
||||
|
||||
manifest1 = RepoClient(self.repodir)
|
||||
manifest1.Override(args[0], load_local_manifests=False)
|
||||
|
||||
@@ -60,7 +60,6 @@ If no project is specified try to use current directory as a project.
|
||||
p.add_option(
|
||||
"-r",
|
||||
"--revert",
|
||||
dest="revert",
|
||||
action="store_true",
|
||||
help="revert instead of checkout",
|
||||
)
|
||||
|
||||
+60
-58
@@ -15,7 +15,6 @@
|
||||
import errno
|
||||
import functools
|
||||
import io
|
||||
import multiprocessing
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
@@ -26,7 +25,6 @@ from color import Coloring
|
||||
from command import Command
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
from command import MirrorSafeCommand
|
||||
from command import WORKER_BATCH_SIZE
|
||||
from error import ManifestInvalidRevisionError
|
||||
from repo_logging import RepoLogger
|
||||
|
||||
@@ -104,6 +102,12 @@ revision to a locally executed git command, use REPO_LREV.
|
||||
REPO_RREV is the name of the revision from the manifest, exactly
|
||||
as written in the manifest.
|
||||
|
||||
REPO_UPSTREAM is the name of the upstream branch as specified in the
|
||||
manifest.
|
||||
|
||||
REPO_DEST_BRANCH is the name of the destination branch for code review,
|
||||
as specified in the manifest.
|
||||
|
||||
REPO_COUNT is the total number of projects being iterated.
|
||||
|
||||
REPO_I is the current (1-based) iteration count. Can be used in
|
||||
@@ -143,7 +147,6 @@ without iterating through the remaining projects.
|
||||
p.add_option(
|
||||
"-r",
|
||||
"--regex",
|
||||
dest="regex",
|
||||
action="store_true",
|
||||
help="execute the command only on projects matching regex or "
|
||||
"wildcard expression",
|
||||
@@ -151,7 +154,6 @@ without iterating through the remaining projects.
|
||||
p.add_option(
|
||||
"-i",
|
||||
"--inverse-regex",
|
||||
dest="inverse_regex",
|
||||
action="store_true",
|
||||
help="execute the command only on projects not matching regex or "
|
||||
"wildcard expression",
|
||||
@@ -159,22 +161,20 @@ without iterating through the remaining projects.
|
||||
p.add_option(
|
||||
"-g",
|
||||
"--groups",
|
||||
dest="groups",
|
||||
help="execute the command only on projects matching the specified "
|
||||
"groups",
|
||||
)
|
||||
p.add_option(
|
||||
"-c",
|
||||
"--command",
|
||||
help="command (and arguments) to execute",
|
||||
dest="command",
|
||||
help="command (and arguments) to execute",
|
||||
action="callback",
|
||||
callback=self._cmd_option,
|
||||
)
|
||||
p.add_option(
|
||||
"-e",
|
||||
"--abort-on-errors",
|
||||
dest="abort_on_errors",
|
||||
action="store_true",
|
||||
help="abort if a command exits unsuccessfully",
|
||||
)
|
||||
@@ -241,15 +241,8 @@ without iterating through the remaining projects.
|
||||
cmd.insert(cmd.index(cn) + 1, "--color")
|
||||
|
||||
mirror = self.manifest.IsMirror
|
||||
rc = 0
|
||||
|
||||
smart_sync_manifest_name = "smart_sync_override.xml"
|
||||
smart_sync_manifest_path = os.path.join(
|
||||
self.manifest.manifestProject.worktree, smart_sync_manifest_name
|
||||
)
|
||||
|
||||
if os.path.isfile(smart_sync_manifest_path):
|
||||
self.manifest.Override(smart_sync_manifest_path)
|
||||
self.TryOverrideManifestWithSmartSync()
|
||||
|
||||
if opt.regex:
|
||||
projects = self.FindProjects(args, all_manifests=all_trees)
|
||||
@@ -264,35 +257,44 @@ without iterating through the remaining projects.
|
||||
|
||||
os.environ["REPO_COUNT"] = str(len(projects))
|
||||
|
||||
def _ProcessResults(_pool, _output, results):
|
||||
rc = 0
|
||||
first = True
|
||||
for r, output in results:
|
||||
if output:
|
||||
if first:
|
||||
first = False
|
||||
elif opt.project_header:
|
||||
print()
|
||||
# To simplify the DoWorkWrapper, take care of automatic
|
||||
# newlines.
|
||||
end = "\n"
|
||||
if output[-1] == "\n":
|
||||
end = ""
|
||||
print(output, end=end)
|
||||
rc = rc or r
|
||||
if r != 0 and opt.abort_on_errors:
|
||||
raise Exception("Aborting due to previous error")
|
||||
return rc
|
||||
|
||||
try:
|
||||
config = self.manifest.manifestProject.config
|
||||
with multiprocessing.Pool(opt.jobs, InitWorker) as pool:
|
||||
results_it = pool.imap(
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = projects
|
||||
rc = self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(
|
||||
DoWorkWrapper, mirror, opt, cmd, shell, config
|
||||
self.DoWorkWrapper, mirror, opt, cmd, shell, config
|
||||
),
|
||||
enumerate(projects),
|
||||
chunksize=WORKER_BATCH_SIZE,
|
||||
range(len(projects)),
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
initializer=self.InitWorker,
|
||||
chunksize=1,
|
||||
)
|
||||
first = True
|
||||
for r, output in results_it:
|
||||
if output:
|
||||
if first:
|
||||
first = False
|
||||
elif opt.project_header:
|
||||
print()
|
||||
# To simplify the DoWorkWrapper, take care of automatic
|
||||
# newlines.
|
||||
end = "\n"
|
||||
if output[-1] == "\n":
|
||||
end = ""
|
||||
print(output, end=end)
|
||||
rc = rc or r
|
||||
if r != 0 and opt.abort_on_errors:
|
||||
raise Exception("Aborting due to previous error")
|
||||
except (KeyboardInterrupt, WorkerKeyboardInterrupt):
|
||||
# Catch KeyboardInterrupt raised inside and outside of workers
|
||||
rc = rc or errno.EINTR
|
||||
rc = errno.EINTR
|
||||
except Exception as e:
|
||||
# Catch any other exceptions raised
|
||||
logger.error(
|
||||
@@ -300,35 +302,35 @@ without iterating through the remaining projects.
|
||||
type(e).__name__,
|
||||
e,
|
||||
)
|
||||
rc = rc or getattr(e, "errno", 1)
|
||||
rc = getattr(e, "errno", 1)
|
||||
if rc != 0:
|
||||
sys.exit(rc)
|
||||
|
||||
@classmethod
|
||||
def InitWorker(cls):
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
@classmethod
|
||||
def DoWorkWrapper(cls, mirror, opt, cmd, shell, config, project_idx):
|
||||
"""A wrapper around the DoWork() method.
|
||||
|
||||
Catch the KeyboardInterrupt exceptions here and re-raise them as a
|
||||
different, ``Exception``-based exception to stop it flooding the console
|
||||
with stacktraces and making the parent hang indefinitely.
|
||||
|
||||
"""
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
try:
|
||||
return DoWork(project, mirror, opt, cmd, shell, project_idx, config)
|
||||
except KeyboardInterrupt:
|
||||
print("%s: Worker interrupted" % project.name)
|
||||
raise WorkerKeyboardInterrupt()
|
||||
|
||||
|
||||
class WorkerKeyboardInterrupt(Exception):
|
||||
"""Keyboard interrupt exception for worker processes."""
|
||||
|
||||
|
||||
def InitWorker():
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
|
||||
def DoWorkWrapper(mirror, opt, cmd, shell, config, args):
|
||||
"""A wrapper around the DoWork() method.
|
||||
|
||||
Catch the KeyboardInterrupt exceptions here and re-raise them as a
|
||||
different, ``Exception``-based exception to stop it flooding the console
|
||||
with stacktraces and making the parent hang indefinitely.
|
||||
|
||||
"""
|
||||
cnt, project = args
|
||||
try:
|
||||
return DoWork(project, mirror, opt, cmd, shell, cnt, config)
|
||||
except KeyboardInterrupt:
|
||||
print("%s: Worker interrupted" % project.name)
|
||||
raise WorkerKeyboardInterrupt()
|
||||
|
||||
|
||||
def DoWork(project, mirror, opt, cmd, shell, cnt, config):
|
||||
env = os.environ.copy()
|
||||
|
||||
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
# Copyright (C) 2024 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
from typing import List, Set
|
||||
|
||||
from command import Command
|
||||
from git_command import git_require
|
||||
from git_command import GitCommand
|
||||
import platform_utils
|
||||
from progress import Progress
|
||||
from project import Project
|
||||
|
||||
|
||||
class Gc(Command):
|
||||
COMMON = True
|
||||
helpSummary = "Cleaning up internal repo and Git state."
|
||||
helpUsage = """
|
||||
%prog
|
||||
"""
|
||||
|
||||
def _Options(self, p):
|
||||
p.add_option(
|
||||
"-n",
|
||||
"--dry-run",
|
||||
dest="dryrun",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="do everything except actually delete",
|
||||
)
|
||||
p.add_option(
|
||||
"-y",
|
||||
"--yes",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="answer yes to all safe prompts",
|
||||
)
|
||||
p.add_option(
|
||||
"--repack",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="repack all projects that use partial clone with "
|
||||
"filter=blob:none",
|
||||
)
|
||||
|
||||
def _find_git_to_delete(
|
||||
self, to_keep: Set[str], start_dir: str
|
||||
) -> Set[str]:
|
||||
"""Searches no longer needed ".git" directories.
|
||||
|
||||
Scans the file system starting from `start_dir` and removes all
|
||||
directories that end with ".git" that are not in the `to_keep` set.
|
||||
"""
|
||||
to_delete = set()
|
||||
for root, dirs, _ in platform_utils.walk(start_dir):
|
||||
for directory in dirs:
|
||||
if not directory.endswith(".git"):
|
||||
continue
|
||||
|
||||
path = os.path.join(root, directory)
|
||||
if path not in to_keep:
|
||||
to_delete.add(path)
|
||||
|
||||
return to_delete
|
||||
|
||||
def delete_unused_projects(self, projects: List[Project], opt):
|
||||
print(f"Scanning filesystem under {self.repodir}...")
|
||||
|
||||
project_paths = set()
|
||||
project_object_paths = set()
|
||||
|
||||
for project in projects:
|
||||
project_paths.add(project.gitdir)
|
||||
project_object_paths.add(project.objdir)
|
||||
|
||||
to_delete = self._find_git_to_delete(
|
||||
project_paths, os.path.join(self.repodir, "projects")
|
||||
)
|
||||
|
||||
to_delete.update(
|
||||
self._find_git_to_delete(
|
||||
project_object_paths,
|
||||
os.path.join(self.repodir, "project-objects"),
|
||||
)
|
||||
)
|
||||
|
||||
if not to_delete:
|
||||
print("Nothing to clean up.")
|
||||
return 0
|
||||
|
||||
print("Identified the following projects are no longer used:")
|
||||
print("\n".join(to_delete))
|
||||
print("")
|
||||
if not opt.yes:
|
||||
print(
|
||||
"If you proceed, any local commits in those projects will be "
|
||||
"destroyed!"
|
||||
)
|
||||
ask = input("Proceed? [y/N] ")
|
||||
if ask.lower() != "y":
|
||||
return 1
|
||||
|
||||
pm = Progress(
|
||||
"Deleting",
|
||||
len(to_delete),
|
||||
delay=False,
|
||||
quiet=opt.quiet,
|
||||
show_elapsed=True,
|
||||
elide=True,
|
||||
)
|
||||
|
||||
for path in to_delete:
|
||||
if opt.dryrun:
|
||||
print(f"\nWould have deleted ${path}")
|
||||
else:
|
||||
tmp_path = os.path.join(
|
||||
os.path.dirname(path),
|
||||
f"to_be_deleted_{os.path.basename(path)}",
|
||||
)
|
||||
platform_utils.rename(path, tmp_path)
|
||||
platform_utils.rmtree(tmp_path)
|
||||
pm.update(msg=path)
|
||||
pm.end()
|
||||
|
||||
return 0
|
||||
|
||||
def _generate_promisor_files(self, pack_dir: str):
|
||||
"""Generates promisor files for all pack files in the given directory.
|
||||
|
||||
Promisor files are empty files with the same name as the corresponding
|
||||
pack file but with the ".promisor" extension. They are used by Git.
|
||||
"""
|
||||
for root, _, files in platform_utils.walk(pack_dir):
|
||||
for file in files:
|
||||
if not file.endswith(".pack"):
|
||||
continue
|
||||
with open(os.path.join(root, f"{file[:-4]}promisor"), "w"):
|
||||
pass
|
||||
|
||||
def repack_projects(self, projects: List[Project], opt):
|
||||
repack_projects = []
|
||||
# Find all projects eligible for repacking:
|
||||
# - can't be shared
|
||||
# - have a specific fetch filter
|
||||
for project in projects:
|
||||
if project.config.GetBoolean("extensions.preciousObjects"):
|
||||
continue
|
||||
if not project.clone_depth:
|
||||
continue
|
||||
if project.manifest.CloneFilterForDepth != "blob:none":
|
||||
continue
|
||||
|
||||
repack_projects.append(project)
|
||||
|
||||
if opt.dryrun:
|
||||
print(f"Would have repacked {len(repack_projects)} projects.")
|
||||
return 0
|
||||
|
||||
pm = Progress(
|
||||
"Repacking (this will take a while)",
|
||||
len(repack_projects),
|
||||
delay=False,
|
||||
quiet=opt.quiet,
|
||||
show_elapsed=True,
|
||||
elide=True,
|
||||
)
|
||||
|
||||
for project in repack_projects:
|
||||
pm.update(msg=f"{project.name}")
|
||||
|
||||
pack_dir = os.path.join(project.gitdir, "tmp_repo_repack")
|
||||
if os.path.isdir(pack_dir):
|
||||
platform_utils.rmtree(pack_dir)
|
||||
os.mkdir(pack_dir)
|
||||
|
||||
# Prepare workspace for repacking - remove all unreachable refs and
|
||||
# their objects.
|
||||
GitCommand(
|
||||
project,
|
||||
["reflog", "expire", "--expire-unreachable=all"],
|
||||
verify_command=True,
|
||||
).Wait()
|
||||
pm.update(msg=f"{project.name} | gc", inc=0)
|
||||
GitCommand(
|
||||
project,
|
||||
["gc"],
|
||||
verify_command=True,
|
||||
).Wait()
|
||||
|
||||
# Get all objects that are reachable from the remote, and pack them.
|
||||
pm.update(msg=f"{project.name} | generating list of objects", inc=0)
|
||||
remote_objects_cmd = GitCommand(
|
||||
project,
|
||||
[
|
||||
"rev-list",
|
||||
"--objects",
|
||||
"--missing=allow-promisor",
|
||||
f"--remotes={project.remote.name}",
|
||||
"--filter=blob:none",
|
||||
"--tags",
|
||||
],
|
||||
capture_stdout=True,
|
||||
verify_command=True,
|
||||
)
|
||||
|
||||
# Get all local objects and pack them.
|
||||
local_head_objects_cmd = GitCommand(
|
||||
project,
|
||||
[
|
||||
"rev-list",
|
||||
"--objects",
|
||||
"--missing=allow-promisor",
|
||||
"HEAD^{tree}",
|
||||
],
|
||||
capture_stdout=True,
|
||||
verify_command=True,
|
||||
)
|
||||
local_objects_cmd = GitCommand(
|
||||
project,
|
||||
[
|
||||
"rev-list",
|
||||
"--objects",
|
||||
"--missing=allow-promisor",
|
||||
"--all",
|
||||
"--reflog",
|
||||
"--indexed-objects",
|
||||
"--not",
|
||||
f"--remotes={project.remote.name}",
|
||||
"--tags",
|
||||
],
|
||||
capture_stdout=True,
|
||||
verify_command=True,
|
||||
)
|
||||
|
||||
remote_objects_cmd.Wait()
|
||||
|
||||
pm.update(msg=f"{project.name} | remote repack", inc=0)
|
||||
GitCommand(
|
||||
project,
|
||||
["pack-objects", os.path.join(pack_dir, "pack")],
|
||||
input=remote_objects_cmd.stdout,
|
||||
capture_stderr=True,
|
||||
capture_stdout=True,
|
||||
verify_command=True,
|
||||
).Wait()
|
||||
|
||||
# create promisor file for each pack file
|
||||
self._generate_promisor_files(pack_dir)
|
||||
|
||||
local_head_objects_cmd.Wait()
|
||||
local_objects_cmd.Wait()
|
||||
|
||||
pm.update(msg=f"{project.name} | local repack", inc=0)
|
||||
GitCommand(
|
||||
project,
|
||||
["pack-objects", os.path.join(pack_dir, "pack")],
|
||||
input=local_head_objects_cmd.stdout + local_objects_cmd.stdout,
|
||||
capture_stderr=True,
|
||||
capture_stdout=True,
|
||||
verify_command=True,
|
||||
).Wait()
|
||||
|
||||
# Swap the old pack directory with the new one.
|
||||
platform_utils.rename(
|
||||
os.path.join(project.objdir, "objects", "pack"),
|
||||
os.path.join(project.objdir, "objects", "pack_old"),
|
||||
)
|
||||
platform_utils.rename(
|
||||
pack_dir,
|
||||
os.path.join(project.objdir, "objects", "pack"),
|
||||
)
|
||||
platform_utils.rmtree(
|
||||
os.path.join(project.objdir, "objects", "pack_old")
|
||||
)
|
||||
|
||||
pm.end()
|
||||
return 0
|
||||
|
||||
def Execute(self, opt, args):
|
||||
projects: List[Project] = self.GetProjects(
|
||||
args, all_manifests=not opt.this_manifest_only
|
||||
)
|
||||
|
||||
# If the user specified projects, fetch the global list separately
|
||||
# to avoid deleting untargeted projects.
|
||||
if args:
|
||||
all_projects = self.GetProjects(
|
||||
[], all_manifests=not opt.this_manifest_only
|
||||
)
|
||||
else:
|
||||
all_projects = projects
|
||||
|
||||
ret = self.delete_unused_projects(all_projects, opt)
|
||||
if ret != 0:
|
||||
return ret
|
||||
|
||||
if opt.repack:
|
||||
git_require((2, 17, 0), fail=True, msg="--repack")
|
||||
ret = self.repack_projects(projects, opt)
|
||||
|
||||
return ret
|
||||
+28
-24
@@ -23,7 +23,6 @@ from error import GitError
|
||||
from error import InvalidArgumentsError
|
||||
from error import SilentRepoExitError
|
||||
from git_command import GitCommand
|
||||
from project import Project
|
||||
from repo_logging import RepoLogger
|
||||
|
||||
|
||||
@@ -40,7 +39,7 @@ class GrepColoring(Coloring):
|
||||
class ExecuteOneResult(NamedTuple):
|
||||
"""Result from an execute instance."""
|
||||
|
||||
project: Project
|
||||
project_idx: int
|
||||
rc: int
|
||||
stdout: str
|
||||
stderr: str
|
||||
@@ -94,7 +93,7 @@ contain a line that matches both expressions:
|
||||
pt = getattr(parser.values, "cmd_argv", None)
|
||||
if pt is None:
|
||||
pt = []
|
||||
setattr(parser.values, "cmd_argv", pt)
|
||||
parser.values.cmd_argv = pt
|
||||
|
||||
if opt_str == "-(":
|
||||
pt.append("(")
|
||||
@@ -121,7 +120,6 @@ contain a line that matches both expressions:
|
||||
g.add_option(
|
||||
"-r",
|
||||
"--revision",
|
||||
dest="revision",
|
||||
action="append",
|
||||
metavar="TREEish",
|
||||
help="Search TREEish, instead of the work tree",
|
||||
@@ -262,8 +260,10 @@ contain a line that matches both expressions:
|
||||
help="Show only file names not containing matching lines",
|
||||
)
|
||||
|
||||
def _ExecuteOne(self, cmd_argv, project):
|
||||
@classmethod
|
||||
def _ExecuteOne(cls, cmd_argv, project_idx):
|
||||
"""Process one project."""
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
try:
|
||||
p = GitCommand(
|
||||
project,
|
||||
@@ -274,7 +274,7 @@ contain a line that matches both expressions:
|
||||
verify_command=True,
|
||||
)
|
||||
except GitError as e:
|
||||
return ExecuteOneResult(project, -1, None, str(e), e)
|
||||
return ExecuteOneResult(project_idx, -1, None, str(e), e)
|
||||
|
||||
try:
|
||||
error = None
|
||||
@@ -282,10 +282,12 @@ contain a line that matches both expressions:
|
||||
except GitError as e:
|
||||
rc = 1
|
||||
error = e
|
||||
return ExecuteOneResult(project, rc, p.stdout, p.stderr, error)
|
||||
return ExecuteOneResult(project_idx, rc, p.stdout, p.stderr, error)
|
||||
|
||||
@staticmethod
|
||||
def _ProcessResults(full_name, have_rev, opt, _pool, out, results):
|
||||
def _ProcessResults(
|
||||
full_name, have_rev, opt, projects, _pool, out, results
|
||||
):
|
||||
git_failed = False
|
||||
bad_rev = False
|
||||
have_match = False
|
||||
@@ -293,9 +295,10 @@ contain a line that matches both expressions:
|
||||
errors = []
|
||||
|
||||
for result in results:
|
||||
project = projects[result.project_idx]
|
||||
if result.rc < 0:
|
||||
git_failed = True
|
||||
out.project("--- project %s ---" % _RelPath(result.project))
|
||||
out.project("--- project %s ---" % _RelPath(project))
|
||||
out.nl()
|
||||
out.fail("%s", result.stderr)
|
||||
out.nl()
|
||||
@@ -311,9 +314,7 @@ contain a line that matches both expressions:
|
||||
):
|
||||
bad_rev = True
|
||||
else:
|
||||
out.project(
|
||||
"--- project %s ---" % _RelPath(result.project)
|
||||
)
|
||||
out.project("--- project %s ---" % _RelPath(project))
|
||||
out.nl()
|
||||
out.fail("%s", result.stderr.strip())
|
||||
out.nl()
|
||||
@@ -331,13 +332,13 @@ contain a line that matches both expressions:
|
||||
rev, line = line.split(":", 1)
|
||||
out.write("%s", rev)
|
||||
out.write(":")
|
||||
out.project(_RelPath(result.project))
|
||||
out.project(_RelPath(project))
|
||||
out.write("/")
|
||||
out.write("%s", line)
|
||||
out.nl()
|
||||
elif full_name:
|
||||
for line in r:
|
||||
out.project(_RelPath(result.project))
|
||||
out.project(_RelPath(project))
|
||||
out.write("/")
|
||||
out.write("%s", line)
|
||||
out.nl()
|
||||
@@ -381,16 +382,19 @@ contain a line that matches both expressions:
|
||||
cmd_argv.extend(opt.revision)
|
||||
cmd_argv.append("--")
|
||||
|
||||
git_failed, bad_rev, have_match, errors = self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._ExecuteOne, cmd_argv),
|
||||
projects,
|
||||
callback=functools.partial(
|
||||
self._ProcessResults, full_name, have_rev, opt
|
||||
),
|
||||
output=out,
|
||||
ordered=True,
|
||||
)
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = projects
|
||||
git_failed, bad_rev, have_match, errors = self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._ExecuteOne, cmd_argv),
|
||||
range(len(projects)),
|
||||
callback=functools.partial(
|
||||
self._ProcessResults, full_name, have_rev, opt, projects
|
||||
),
|
||||
output=out,
|
||||
ordered=True,
|
||||
chunksize=1,
|
||||
)
|
||||
|
||||
if git_failed:
|
||||
raise GrepCommandError(
|
||||
|
||||
+3
-5
@@ -59,7 +59,7 @@ Displays detailed usage information about a command.
|
||||
|
||||
def PrintAllCommandsBody(self):
|
||||
print("The complete list of recognized repo commands is:")
|
||||
commandNames = list(sorted(all_commands))
|
||||
commandNames = sorted(all_commands)
|
||||
self._PrintCommands(commandNames)
|
||||
print(
|
||||
"See 'repo help <command>' for more information on a "
|
||||
@@ -74,10 +74,8 @@ Displays detailed usage information about a command.
|
||||
def PrintCommonCommandsBody(self):
|
||||
print("The most commonly used repo commands are:")
|
||||
|
||||
commandNames = list(
|
||||
sorted(
|
||||
name for name, command in all_commands.items() if command.COMMON
|
||||
)
|
||||
commandNames = sorted(
|
||||
name for name, command in all_commands.items() if command.COMMON
|
||||
)
|
||||
self._PrintCommands(commandNames)
|
||||
|
||||
|
||||
+322
-121
@@ -12,14 +12,41 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import enum
|
||||
import functools
|
||||
import io
|
||||
import json
|
||||
import optparse
|
||||
import sys
|
||||
from typing import Any, Dict, List, NamedTuple
|
||||
|
||||
from color import Coloring
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
from command import PagedCommand
|
||||
from git_refs import R_HEADS
|
||||
from git_refs import R_M
|
||||
|
||||
|
||||
class BranchInfo(NamedTuple):
|
||||
"""Holds information about a branch in a project."""
|
||||
|
||||
relpath: str
|
||||
name: str
|
||||
commits: Any
|
||||
date: str
|
||||
is_current: bool
|
||||
|
||||
|
||||
class OutputFormat(enum.Enum):
|
||||
"""Type for the requested output format."""
|
||||
|
||||
# Human-readable text output.
|
||||
TEXT = enum.auto()
|
||||
|
||||
# Machine-readable JSON output.
|
||||
JSON = enum.auto()
|
||||
|
||||
|
||||
class _Coloring(Coloring):
|
||||
def __init__(self, config):
|
||||
Coloring.__init__(self, config, "status")
|
||||
@@ -27,10 +54,11 @@ class _Coloring(Coloring):
|
||||
|
||||
class Info(PagedCommand):
|
||||
COMMON = True
|
||||
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
|
||||
helpSummary = (
|
||||
"Get info on the manifest branch, current branch or unmerged branches"
|
||||
)
|
||||
helpUsage = "%prog [-dl] [-o [-c]] [<project>...]"
|
||||
helpUsage = "%prog [-dl] [-o [-c]] [--format=<format>] [<project>...]"
|
||||
|
||||
def _Options(self, p):
|
||||
p.add_option(
|
||||
@@ -43,14 +71,36 @@ class Info(PagedCommand):
|
||||
p.add_option(
|
||||
"-o",
|
||||
"--overview",
|
||||
dest="overview",
|
||||
action="store_true",
|
||||
help="show overview of all local commits",
|
||||
)
|
||||
p.add_option(
|
||||
"--include-summary",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="include manifest summary (default: true)",
|
||||
)
|
||||
p.add_option(
|
||||
"--no-include-summary",
|
||||
dest="include_summary",
|
||||
action="store_false",
|
||||
help="exclude manifest summary",
|
||||
)
|
||||
p.add_option(
|
||||
"--include-projects",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="include project details (default: true)",
|
||||
)
|
||||
p.add_option(
|
||||
"--no-include-projects",
|
||||
dest="include_projects",
|
||||
action="store_false",
|
||||
help="exclude project details",
|
||||
)
|
||||
p.add_option(
|
||||
"-c",
|
||||
"--current-branch",
|
||||
dest="current_branch",
|
||||
action="store_true",
|
||||
help="consider only checked out branches",
|
||||
)
|
||||
@@ -74,8 +124,39 @@ class Info(PagedCommand):
|
||||
action="store_true",
|
||||
help="disable all remote operations",
|
||||
)
|
||||
formats = tuple(x.lower() for x in OutputFormat.__members__.keys())
|
||||
p.add_option(
|
||||
"--format",
|
||||
default=OutputFormat.TEXT.name.lower(),
|
||||
choices=formats,
|
||||
help=f"output format: {', '.join(formats)} (default: %default)",
|
||||
)
|
||||
|
||||
def WantPager(self, opt):
|
||||
return OutputFormat[opt.format.upper()] == OutputFormat.TEXT
|
||||
|
||||
def ValidateOptions(self, opt, args):
|
||||
output_format = OutputFormat[opt.format.upper()]
|
||||
if output_format == OutputFormat.JSON:
|
||||
if opt.all:
|
||||
self.OptionParser.error("--diff is not supported with JSON")
|
||||
if opt.overview:
|
||||
self.OptionParser.error("--overview is not supported with JSON")
|
||||
|
||||
def Execute(self, opt, args):
|
||||
if not opt.this_manifest_only:
|
||||
self.manifest = self.manifest.outer_client
|
||||
|
||||
self.TryOverrideManifestWithSmartSync()
|
||||
|
||||
output_format = OutputFormat[opt.format.upper()]
|
||||
if output_format == OutputFormat.JSON:
|
||||
self._ExecuteJson(opt, args)
|
||||
else:
|
||||
self._ExecuteText(opt, args)
|
||||
|
||||
def _ExecuteText(self, opt, args) -> None:
|
||||
"""Output info as human-readable text."""
|
||||
self.out = _Coloring(self.client.globalConfig)
|
||||
self.heading = self.out.printer("heading", attr="bold")
|
||||
self.headtext = self.out.nofmt_printer("headtext", fg="yellow")
|
||||
@@ -86,152 +167,272 @@ class Info(PagedCommand):
|
||||
|
||||
self.opt = opt
|
||||
|
||||
if not opt.this_manifest_only:
|
||||
self.manifest = self.manifest.outer_client
|
||||
manifestConfig = self.manifest.manifestProject.config
|
||||
mergeBranch = manifestConfig.GetBranch("default").merge
|
||||
manifestGroups = self.manifest.GetGroupsStr()
|
||||
if opt.include_summary:
|
||||
self._printSummary()
|
||||
|
||||
self.heading("Manifest branch: ")
|
||||
if self.manifest.default.revisionExpr:
|
||||
self.headtext(self.manifest.default.revisionExpr)
|
||||
self.out.nl()
|
||||
self.heading("Manifest merge branch: ")
|
||||
# The manifest might not have a merge branch if it isn't in a git repo,
|
||||
# e.g. if `repo init --standalone-manifest` is used.
|
||||
self.headtext(mergeBranch or "")
|
||||
self.out.nl()
|
||||
self.heading("Manifest groups: ")
|
||||
self.headtext(manifestGroups)
|
||||
self.out.nl()
|
||||
|
||||
self.printSeparator()
|
||||
|
||||
if not opt.overview:
|
||||
if not opt.include_projects:
|
||||
return
|
||||
elif not opt.overview:
|
||||
self._printDiffInfo(opt, args)
|
||||
else:
|
||||
self._printCommitOverview(opt, args)
|
||||
|
||||
def _getSummaryData(self) -> Dict[str, Any]:
|
||||
"""Gather manifest summary data as a dict."""
|
||||
manifestConfig = self.manifest.manifestProject.config
|
||||
mergeBranch = manifestConfig.GetBranch("default").merge
|
||||
manifestGroups = self.manifest.GetManifestGroupsStr()
|
||||
sp = self.manifest.superproject
|
||||
srev = sp.commit_id if sp and sp.commit_id else None
|
||||
return {
|
||||
"manifest_branch": self.manifest.default.revisionExpr or "",
|
||||
"manifest_merge_branch": mergeBranch or "",
|
||||
"manifest_groups": manifestGroups,
|
||||
"superproject_revision": srev,
|
||||
}
|
||||
|
||||
def _getProjectData(self, project) -> Dict[str, Any]:
|
||||
"""Gather project data as a dict."""
|
||||
data = {
|
||||
"name": project.name,
|
||||
"mount_path": project.worktree,
|
||||
"current_revision": project.GetRevisionId(),
|
||||
"manifest_revision": project.revisionExpr,
|
||||
"local_branches": list(project.GetBranches()),
|
||||
}
|
||||
currentBranch = project.CurrentBranch
|
||||
if currentBranch:
|
||||
data["current_branch"] = currentBranch
|
||||
return data
|
||||
|
||||
def _ExecuteJson(self, opt, args) -> None:
|
||||
"""Output info as JSON."""
|
||||
result = {}
|
||||
if opt.include_summary:
|
||||
result["summary"] = self._getSummaryData()
|
||||
if opt.include_projects:
|
||||
projs = self.GetProjects(
|
||||
args, all_manifests=not opt.this_manifest_only
|
||||
)
|
||||
result["projects"] = [self._getProjectData(p) for p in projs]
|
||||
|
||||
json_settings = {
|
||||
# JSON style guide says Unicode characters are fully allowed.
|
||||
"ensure_ascii": False,
|
||||
# We use 2 space indent to match JSON style guide.
|
||||
"indent": 2,
|
||||
"separators": (",", ": "),
|
||||
"sort_keys": True,
|
||||
}
|
||||
sys.stdout.write(json.dumps(result, **json_settings) + "\n")
|
||||
|
||||
def _printSummary(self) -> None:
|
||||
"""Print manifest summary in text format."""
|
||||
data = self._getSummaryData()
|
||||
self.heading("Manifest branch: ")
|
||||
self.headtext(data["manifest_branch"])
|
||||
self.out.nl()
|
||||
self.heading("Manifest merge branch: ")
|
||||
self.headtext(data["manifest_merge_branch"])
|
||||
self.out.nl()
|
||||
self.heading("Manifest groups: ")
|
||||
self.headtext(data["manifest_groups"])
|
||||
self.out.nl()
|
||||
self.heading("Superproject revision: ")
|
||||
self.headtext(data["superproject_revision"] or "None")
|
||||
self.out.nl()
|
||||
self.printSeparator()
|
||||
|
||||
def printSeparator(self):
|
||||
self.text("----------------------------")
|
||||
self.out.nl()
|
||||
|
||||
@classmethod
|
||||
def _DiffHelper(cls, project_idx: int, opt: Any) -> str:
|
||||
"""Helper for ParallelContext to get diff info for a project."""
|
||||
buf = io.StringIO()
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
config = cls.get_parallel_context()["config"]
|
||||
|
||||
out = _Coloring(config)
|
||||
out.redirect(buf)
|
||||
|
||||
heading = out.printer("heading", attr="bold")
|
||||
headtext = out.nofmt_printer("headtext", fg="yellow")
|
||||
redtext = out.printer("redtext", fg="red")
|
||||
sha = out.printer("sha", fg="yellow")
|
||||
text = out.nofmt_printer("text")
|
||||
dimtext = out.printer("dimtext", attr="dim")
|
||||
|
||||
heading("Project: ")
|
||||
headtext(project.name)
|
||||
out.nl()
|
||||
|
||||
heading("Mount path: ")
|
||||
headtext(project.worktree)
|
||||
out.nl()
|
||||
|
||||
heading("Current revision: ")
|
||||
headtext(project.GetRevisionId())
|
||||
out.nl()
|
||||
|
||||
currentBranch = project.CurrentBranch
|
||||
if currentBranch:
|
||||
heading("Current branch: ")
|
||||
headtext(currentBranch)
|
||||
out.nl()
|
||||
|
||||
heading("Manifest revision: ")
|
||||
headtext(project.revisionExpr)
|
||||
out.nl()
|
||||
|
||||
localBranches = list(project.GetBranches().keys())
|
||||
heading("Local Branches: ")
|
||||
redtext(str(len(localBranches)))
|
||||
if localBranches:
|
||||
text(" [")
|
||||
text(", ".join(localBranches))
|
||||
text("]")
|
||||
out.nl()
|
||||
|
||||
if opt.all:
|
||||
if not opt.local:
|
||||
project.Sync_NetworkHalf(quiet=True, current_branch_only=True)
|
||||
|
||||
branch = project.manifest.manifestProject.config.GetBranch(
|
||||
"default"
|
||||
).merge
|
||||
if branch.startswith(R_HEADS):
|
||||
branch = branch[len(R_HEADS) :]
|
||||
logTarget = R_M + branch
|
||||
|
||||
bareTmp = project.bare_git._bare
|
||||
project.bare_git._bare = False
|
||||
localCommits = project.bare_git.rev_list(
|
||||
"--abbrev=8",
|
||||
"--abbrev-commit",
|
||||
"--pretty=oneline",
|
||||
logTarget + "..",
|
||||
"--",
|
||||
)
|
||||
|
||||
originCommits = project.bare_git.rev_list(
|
||||
"--abbrev=8",
|
||||
"--abbrev-commit",
|
||||
"--pretty=oneline",
|
||||
".." + logTarget,
|
||||
"--",
|
||||
)
|
||||
project.bare_git._bare = bareTmp
|
||||
|
||||
heading("Local Commits: ")
|
||||
redtext(str(len(localCommits)))
|
||||
dimtext(" (on current branch)")
|
||||
out.nl()
|
||||
|
||||
for c in localCommits:
|
||||
split = c.split()
|
||||
sha(split[0] + " ")
|
||||
text(" ".join(split[1:]))
|
||||
out.nl()
|
||||
|
||||
text("----------------------------")
|
||||
out.nl()
|
||||
|
||||
heading("Remote Commits: ")
|
||||
redtext(str(len(originCommits)))
|
||||
out.nl()
|
||||
|
||||
for c in originCommits:
|
||||
split = c.split()
|
||||
sha(split[0] + " ")
|
||||
text(" ".join(split[1:]))
|
||||
out.nl()
|
||||
|
||||
text("----------------------------")
|
||||
out.nl()
|
||||
|
||||
return buf.getvalue()
|
||||
|
||||
def _printDiffInfo(self, opt, args):
|
||||
# We let exceptions bubble up to main as they'll be well structured.
|
||||
projs = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
|
||||
|
||||
for p in projs:
|
||||
self.heading("Project: ")
|
||||
self.headtext(p.name)
|
||||
self.out.nl()
|
||||
def _ProcessResults(_pool, _output, results):
|
||||
for output in results:
|
||||
if output:
|
||||
print(output, end="")
|
||||
|
||||
self.heading("Mount path: ")
|
||||
self.headtext(p.worktree)
|
||||
self.out.nl()
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = projs
|
||||
self.get_parallel_context()[
|
||||
"config"
|
||||
] = self.manifest.manifestProject.config
|
||||
|
||||
self.heading("Current revision: ")
|
||||
self.headtext(p.GetRevisionId())
|
||||
self.out.nl()
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._DiffHelper, opt=opt),
|
||||
range(len(projs)),
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
chunksize=1,
|
||||
)
|
||||
|
||||
currentBranch = p.CurrentBranch
|
||||
if currentBranch:
|
||||
self.heading("Current branch: ")
|
||||
self.headtext(currentBranch)
|
||||
self.out.nl()
|
||||
@classmethod
|
||||
def _OverviewHelper(cls, project_idx: int, opt: Any) -> List[BranchInfo]:
|
||||
"""Helper to get overview of uploadable branches."""
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
|
||||
self.heading("Manifest revision: ")
|
||||
self.headtext(p.revisionExpr)
|
||||
self.out.nl()
|
||||
branches = []
|
||||
br = [project.GetUploadableBranch(x) for x in project.GetBranches()]
|
||||
br = [x for x in br if x]
|
||||
if opt.current_branch:
|
||||
br = [x for x in br if x.name == project.CurrentBranch]
|
||||
|
||||
localBranches = list(p.GetBranches().keys())
|
||||
self.heading("Local Branches: ")
|
||||
self.redtext(str(len(localBranches)))
|
||||
if localBranches:
|
||||
self.text(" [")
|
||||
self.text(", ".join(localBranches))
|
||||
self.text("]")
|
||||
self.out.nl()
|
||||
|
||||
if self.opt.all:
|
||||
self.findRemoteLocalDiff(p)
|
||||
|
||||
self.printSeparator()
|
||||
|
||||
def findRemoteLocalDiff(self, project):
|
||||
# Fetch all the latest commits.
|
||||
if not self.opt.local:
|
||||
project.Sync_NetworkHalf(quiet=True, current_branch_only=True)
|
||||
|
||||
branch = self.manifest.manifestProject.config.GetBranch("default").merge
|
||||
if branch.startswith(R_HEADS):
|
||||
branch = branch[len(R_HEADS) :]
|
||||
logTarget = R_M + branch
|
||||
|
||||
bareTmp = project.bare_git._bare
|
||||
project.bare_git._bare = False
|
||||
localCommits = project.bare_git.rev_list(
|
||||
"--abbrev=8",
|
||||
"--abbrev-commit",
|
||||
"--pretty=oneline",
|
||||
logTarget + "..",
|
||||
"--",
|
||||
)
|
||||
|
||||
originCommits = project.bare_git.rev_list(
|
||||
"--abbrev=8",
|
||||
"--abbrev-commit",
|
||||
"--pretty=oneline",
|
||||
".." + logTarget,
|
||||
"--",
|
||||
)
|
||||
project.bare_git._bare = bareTmp
|
||||
|
||||
self.heading("Local Commits: ")
|
||||
self.redtext(str(len(localCommits)))
|
||||
self.dimtext(" (on current branch)")
|
||||
self.out.nl()
|
||||
|
||||
for c in localCommits:
|
||||
split = c.split()
|
||||
self.sha(split[0] + " ")
|
||||
self.text(" ".join(split[1:]))
|
||||
self.out.nl()
|
||||
|
||||
self.printSeparator()
|
||||
|
||||
self.heading("Remote Commits: ")
|
||||
self.redtext(str(len(originCommits)))
|
||||
self.out.nl()
|
||||
|
||||
for c in originCommits:
|
||||
split = c.split()
|
||||
self.sha(split[0] + " ")
|
||||
self.text(" ".join(split[1:]))
|
||||
self.out.nl()
|
||||
for b in br:
|
||||
branches.append(
|
||||
BranchInfo(
|
||||
relpath=project.RelPath(local=opt.this_manifest_only),
|
||||
name=b.name,
|
||||
commits=b.commits,
|
||||
date=b.date,
|
||||
is_current=b.name == project.CurrentBranch,
|
||||
)
|
||||
)
|
||||
return branches
|
||||
|
||||
def _printCommitOverview(self, opt, args):
|
||||
projs = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
|
||||
|
||||
all_branches = []
|
||||
for project in self.GetProjects(
|
||||
args, all_manifests=not opt.this_manifest_only
|
||||
):
|
||||
br = [project.GetUploadableBranch(x) for x in project.GetBranches()]
|
||||
br = [x for x in br if x]
|
||||
if self.opt.current_branch:
|
||||
br = [x for x in br if x.name == project.CurrentBranch]
|
||||
all_branches.extend(br)
|
||||
|
||||
def _ProcessResults(_pool, _output, results):
|
||||
for branches in results:
|
||||
all_branches.extend(branches)
|
||||
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = projs
|
||||
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._OverviewHelper, opt=opt),
|
||||
range(len(projs)),
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
chunksize=1,
|
||||
)
|
||||
|
||||
if not all_branches:
|
||||
return
|
||||
|
||||
self.out.nl()
|
||||
self.heading("Projects Overview")
|
||||
project = None
|
||||
current_relpath = None
|
||||
|
||||
for branch in all_branches:
|
||||
if project != branch.project:
|
||||
project = branch.project
|
||||
if current_relpath != branch.relpath:
|
||||
current_relpath = branch.relpath
|
||||
self.out.nl()
|
||||
self.headtext(project.RelPath(local=opt.this_manifest_only))
|
||||
self.headtext(current_relpath)
|
||||
self.out.nl()
|
||||
|
||||
commits = branch.commits
|
||||
@@ -239,7 +440,7 @@ class Info(PagedCommand):
|
||||
self.text(
|
||||
"%s %-33s (%2d commit%s, %s)"
|
||||
% (
|
||||
branch.name == project.CurrentBranch and "*" or " ",
|
||||
branch.is_current and "*" or " ",
|
||||
branch.name,
|
||||
len(commits),
|
||||
len(commits) != 1 and "s" or "",
|
||||
|
||||
@@ -52,6 +52,10 @@ The optional -b argument can be used to select the manifest branch
|
||||
to checkout and use. If no branch is specified, the remote's default
|
||||
branch is used. This is equivalent to using -b HEAD.
|
||||
|
||||
The optional --manifest-upstream-branch argument can be used when a commit is
|
||||
provided to --manifest-branch (or -b), to specify the name of the git ref in
|
||||
which the commit can be found.
|
||||
|
||||
The optional -m argument can be used to specify an alternate manifest
|
||||
to be used. If no manifest is specified, the manifest default.xml
|
||||
will be used.
|
||||
@@ -123,6 +127,7 @@ to update the working directory files.
|
||||
return {
|
||||
"REPO_MANIFEST_URL": "manifest_url",
|
||||
"REPO_MIRROR_LOCATION": "reference",
|
||||
"REPO_GIT_LFS": "git_lfs",
|
||||
}
|
||||
|
||||
def _SyncManifest(self, opt):
|
||||
@@ -135,6 +140,7 @@ to update the working directory files.
|
||||
# manifest project is special and is created when instantiating the
|
||||
# manifest which happens before we parse options.
|
||||
self.manifest.manifestProject.clone_depth = opt.manifest_depth
|
||||
self.manifest.manifestProject.upstream = opt.manifest_upstream_branch
|
||||
clone_filter_for_depth = (
|
||||
"blob:none" if (_REPO_ALLOW_SHALLOW == "0") else None
|
||||
)
|
||||
@@ -317,6 +323,12 @@ to update the working directory files.
|
||||
" be used with --standalone-manifest."
|
||||
)
|
||||
|
||||
if opt.manifest_upstream_branch and opt.manifest_branch is None:
|
||||
self.OptionParser.error(
|
||||
"--manifest-upstream-branch cannot be used without "
|
||||
"--manifest-branch."
|
||||
)
|
||||
|
||||
if args:
|
||||
if opt.manifest_url:
|
||||
self.OptionParser.error(
|
||||
|
||||
@@ -40,7 +40,6 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
|
||||
p.add_option(
|
||||
"-r",
|
||||
"--regex",
|
||||
dest="regex",
|
||||
action="store_true",
|
||||
help="filter the project list based on regex or wildcard matching "
|
||||
"of strings",
|
||||
@@ -48,7 +47,6 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
|
||||
p.add_option(
|
||||
"-g",
|
||||
"--groups",
|
||||
dest="groups",
|
||||
help="filter the project list based on the groups the project is "
|
||||
"in",
|
||||
)
|
||||
@@ -61,21 +59,18 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
|
||||
p.add_option(
|
||||
"-n",
|
||||
"--name-only",
|
||||
dest="name_only",
|
||||
action="store_true",
|
||||
help="display only the name of the repository",
|
||||
)
|
||||
p.add_option(
|
||||
"-p",
|
||||
"--path-only",
|
||||
dest="path_only",
|
||||
action="store_true",
|
||||
help="display only the path of the repository",
|
||||
)
|
||||
p.add_option(
|
||||
"-f",
|
||||
"--fullpath",
|
||||
dest="fullpath",
|
||||
action="store_true",
|
||||
help="display the full work tree path instead of the relative path",
|
||||
)
|
||||
|
||||
+33
-7
@@ -12,7 +12,9 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import enum
|
||||
import json
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -23,6 +25,16 @@ from repo_logging import RepoLogger
|
||||
logger = RepoLogger(__file__)
|
||||
|
||||
|
||||
class OutputFormat(enum.Enum):
|
||||
"""Type for the requested output format."""
|
||||
|
||||
# Canonicalized manifest in XML format.
|
||||
XML = enum.auto()
|
||||
|
||||
# Canonicalized manifest in JSON format.
|
||||
JSON = enum.auto()
|
||||
|
||||
|
||||
class Manifest(PagedCommand):
|
||||
COMMON = False
|
||||
helpSummary = "Manifest inspection utility"
|
||||
@@ -42,6 +54,10 @@ revisions set to the current commit hash. These are known as
|
||||
In this case, the 'upstream' attribute is set to the ref we were on
|
||||
when the manifest was generated. The 'dest-branch' attribute is set
|
||||
to indicate the remote ref to push changes to via 'repo upload'.
|
||||
|
||||
Multiple output formats are supported via --format. The default output
|
||||
is XML, and formats are generally "condensed". Use --pretty for more
|
||||
human-readable variations.
|
||||
"""
|
||||
|
||||
@property
|
||||
@@ -86,11 +102,21 @@ to indicate the remote ref to push changes to via 'repo upload'.
|
||||
"(only of use if the branch names for a sha1 manifest are "
|
||||
"sensitive)",
|
||||
)
|
||||
# Replaced with --format=json. Kept for backwards compatibility.
|
||||
# Can delete in Jun 2026 or later.
|
||||
p.add_option(
|
||||
"--json",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="output manifest in JSON format (experimental)",
|
||||
action="store_const",
|
||||
dest="format",
|
||||
const=OutputFormat.JSON.name.lower(),
|
||||
help=optparse.SUPPRESS_HELP,
|
||||
)
|
||||
formats = tuple(x.lower() for x in OutputFormat.__members__.keys())
|
||||
p.add_option(
|
||||
"--format",
|
||||
default=OutputFormat.XML.name.lower(),
|
||||
choices=formats,
|
||||
help=f"output format: {', '.join(formats)} (default: %default)",
|
||||
)
|
||||
p.add_option(
|
||||
"--pretty",
|
||||
@@ -108,7 +134,6 @@ to indicate the remote ref to push changes to via 'repo upload'.
|
||||
p.add_option(
|
||||
"-o",
|
||||
"--output-file",
|
||||
dest="output_file",
|
||||
default="-",
|
||||
help="file to save the manifest to. (Filename prefix for "
|
||||
"multi-tree.)",
|
||||
@@ -121,6 +146,8 @@ to indicate the remote ref to push changes to via 'repo upload'.
|
||||
if opt.manifest_name:
|
||||
self.manifest.Override(opt.manifest_name, False)
|
||||
|
||||
output_format = OutputFormat[opt.format.upper()]
|
||||
|
||||
for manifest in self.ManifestList(opt):
|
||||
output_file = opt.output_file
|
||||
if output_file == "-":
|
||||
@@ -135,8 +162,7 @@ to indicate the remote ref to push changes to via 'repo upload'.
|
||||
|
||||
manifest.SetUseLocalManifests(not opt.ignore_local_manifests)
|
||||
|
||||
if opt.json:
|
||||
logger.warning("warning: --json is experimental!")
|
||||
if output_format == OutputFormat.JSON:
|
||||
doc = manifest.ToDict(
|
||||
peg_rev=opt.peg_rev,
|
||||
peg_rev_upstream=opt.peg_rev_upstream,
|
||||
@@ -152,7 +178,7 @@ to indicate the remote ref to push changes to via 'repo upload'.
|
||||
"separators": (",", ": ") if opt.pretty else (",", ":"),
|
||||
"sort_keys": True,
|
||||
}
|
||||
fd.write(json.dumps(doc, **json_settings))
|
||||
fd.write(json.dumps(doc, **json_settings) + "\n")
|
||||
else:
|
||||
manifest.Save(
|
||||
fd,
|
||||
|
||||
@@ -37,7 +37,6 @@ are displayed.
|
||||
p.add_option(
|
||||
"-c",
|
||||
"--current-branch",
|
||||
dest="current_branch",
|
||||
action="store_true",
|
||||
help="consider only checked out branches",
|
||||
)
|
||||
|
||||
+12
-8
@@ -27,8 +27,10 @@ class Prune(PagedCommand):
|
||||
"""
|
||||
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
|
||||
|
||||
def _ExecuteOne(self, project):
|
||||
@classmethod
|
||||
def _ExecuteOne(cls, project_idx):
|
||||
"""Process one project."""
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
return project.PruneHeads()
|
||||
|
||||
def Execute(self, opt, args):
|
||||
@@ -41,13 +43,15 @@ class Prune(PagedCommand):
|
||||
def _ProcessResults(_pool, _output, results):
|
||||
return list(itertools.chain.from_iterable(results))
|
||||
|
||||
all_branches = self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
self._ExecuteOne,
|
||||
projects,
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
)
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = projects
|
||||
all_branches = self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
self._ExecuteOne,
|
||||
range(len(projects)),
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
)
|
||||
|
||||
if not all_branches:
|
||||
return
|
||||
|
||||
@@ -47,21 +47,18 @@ branch but need to incorporate new upstream changes "underneath" them.
|
||||
g.add_option(
|
||||
"-i",
|
||||
"--interactive",
|
||||
dest="interactive",
|
||||
action="store_true",
|
||||
help="interactive rebase (single project only)",
|
||||
)
|
||||
|
||||
p.add_option(
|
||||
"--fail-fast",
|
||||
dest="fail_fast",
|
||||
action="store_true",
|
||||
help="stop rebasing after first error is hit",
|
||||
)
|
||||
p.add_option(
|
||||
"-f",
|
||||
"--force-rebase",
|
||||
dest="force_rebase",
|
||||
action="store_true",
|
||||
help="pass --force-rebase to git rebase",
|
||||
)
|
||||
@@ -74,27 +71,23 @@ branch but need to incorporate new upstream changes "underneath" them.
|
||||
)
|
||||
p.add_option(
|
||||
"--autosquash",
|
||||
dest="autosquash",
|
||||
action="store_true",
|
||||
help="pass --autosquash to git rebase",
|
||||
)
|
||||
p.add_option(
|
||||
"--whitespace",
|
||||
dest="whitespace",
|
||||
action="store",
|
||||
metavar="WS",
|
||||
help="pass --whitespace to git rebase",
|
||||
)
|
||||
p.add_option(
|
||||
"--auto-stash",
|
||||
dest="auto_stash",
|
||||
action="store_true",
|
||||
help="stash local modifications before starting",
|
||||
)
|
||||
p.add_option(
|
||||
"-m",
|
||||
"--onto-manifest",
|
||||
dest="onto_manifest",
|
||||
action="store_true",
|
||||
help="rebase onto the manifest version instead of upstream "
|
||||
"HEAD (this helps to make sure the local tree stays "
|
||||
|
||||
@@ -54,7 +54,6 @@ need to be performed by an end-user.
|
||||
)
|
||||
g.add_option(
|
||||
"--repo-upgraded",
|
||||
dest="repo_upgraded",
|
||||
action="store_true",
|
||||
help=optparse.SUPPRESS_HELP,
|
||||
)
|
||||
|
||||
@@ -46,7 +46,6 @@ The '%prog' command stages files to prepare the next commit.
|
||||
g.add_option(
|
||||
"-i",
|
||||
"--interactive",
|
||||
dest="interactive",
|
||||
action="store_true",
|
||||
help="use interactive staging",
|
||||
)
|
||||
|
||||
+26
-16
@@ -21,7 +21,6 @@ from error import RepoExitError
|
||||
from git_command import git
|
||||
from git_config import IsImmutable
|
||||
from progress import Progress
|
||||
from project import Project
|
||||
from repo_logging import RepoLogger
|
||||
|
||||
|
||||
@@ -29,7 +28,7 @@ logger = RepoLogger(__file__)
|
||||
|
||||
|
||||
class ExecuteOneResult(NamedTuple):
|
||||
project: Project
|
||||
project_idx: int
|
||||
error: Exception
|
||||
|
||||
|
||||
@@ -52,7 +51,6 @@ revision specified in the manifest.
|
||||
def _Options(self, p):
|
||||
p.add_option(
|
||||
"--all",
|
||||
dest="all",
|
||||
action="store_true",
|
||||
help="begin branch in all projects",
|
||||
)
|
||||
@@ -80,18 +78,20 @@ revision specified in the manifest.
|
||||
if not git.check_ref_format("heads/%s" % nb):
|
||||
self.OptionParser.error("'%s' is not a valid name" % nb)
|
||||
|
||||
def _ExecuteOne(self, revision, nb, project):
|
||||
@classmethod
|
||||
def _ExecuteOne(cls, revision, nb, default_revisionExpr, project_idx):
|
||||
"""Start one project."""
|
||||
# If the current revision is immutable, such as a SHA1, a tag or
|
||||
# a change, then we can't push back to it. Substitute with
|
||||
# dest_branch, if defined; or with manifest default revision instead.
|
||||
branch_merge = ""
|
||||
error = None
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
if IsImmutable(project.revisionExpr):
|
||||
if project.dest_branch:
|
||||
branch_merge = project.dest_branch
|
||||
else:
|
||||
branch_merge = self.manifest.default.revisionExpr
|
||||
branch_merge = default_revisionExpr
|
||||
|
||||
try:
|
||||
project.StartBranch(
|
||||
@@ -100,10 +100,11 @@ revision specified in the manifest.
|
||||
except Exception as e:
|
||||
logger.error("error: unable to checkout %s: %s", project.name, e)
|
||||
error = e
|
||||
return ExecuteOneResult(project, error)
|
||||
return ExecuteOneResult(project_idx, error)
|
||||
|
||||
def Execute(self, opt, args):
|
||||
nb = args[0]
|
||||
self.TryOverrideManifestWithSmartSync()
|
||||
err_projects = []
|
||||
err = []
|
||||
projects = []
|
||||
@@ -120,19 +121,28 @@ revision specified in the manifest.
|
||||
def _ProcessResults(_pool, pm, results):
|
||||
for result in results:
|
||||
if result.error:
|
||||
err_projects.append(result.project)
|
||||
project = all_projects[result.project_idx]
|
||||
err_projects.append(project)
|
||||
err.append(result.error)
|
||||
pm.update(msg="")
|
||||
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._ExecuteOne, opt.revision, nb),
|
||||
all_projects,
|
||||
callback=_ProcessResults,
|
||||
output=Progress(
|
||||
f"Starting {nb}", len(all_projects), quiet=opt.quiet
|
||||
),
|
||||
)
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = all_projects
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(
|
||||
self._ExecuteOne,
|
||||
opt.revision,
|
||||
nb,
|
||||
self.manifest.default.revisionExpr,
|
||||
),
|
||||
range(len(all_projects)),
|
||||
callback=_ProcessResults,
|
||||
output=Progress(
|
||||
f"Starting {nb}", len(all_projects), quiet=opt.quiet
|
||||
),
|
||||
chunksize=1,
|
||||
)
|
||||
|
||||
if err_projects:
|
||||
for p in err_projects:
|
||||
|
||||
+16
-12
@@ -82,13 +82,13 @@ the following meanings:
|
||||
p.add_option(
|
||||
"-o",
|
||||
"--orphans",
|
||||
dest="orphans",
|
||||
action="store_true",
|
||||
help="include objects in working directory outside of repo "
|
||||
"projects",
|
||||
)
|
||||
|
||||
def _StatusHelper(self, quiet, local, project):
|
||||
@classmethod
|
||||
def _StatusHelper(cls, quiet, local, project_idx):
|
||||
"""Obtains the status for a specific project.
|
||||
|
||||
Obtains the status for a project, redirecting the output to
|
||||
@@ -99,12 +99,13 @@ the following meanings:
|
||||
local: a boolean, if True, the path is relative to the local
|
||||
(sub)manifest. If false, the path is relative to the outermost
|
||||
manifest.
|
||||
project: Project to get status of.
|
||||
project_idx: Project index to get status of.
|
||||
|
||||
Returns:
|
||||
The status of the project.
|
||||
"""
|
||||
buf = io.StringIO()
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
ret = project.PrintWorkTreeStatus(
|
||||
quiet=quiet, output_redir=buf, local=local
|
||||
)
|
||||
@@ -143,15 +144,18 @@ the following meanings:
|
||||
ret += 1
|
||||
return ret
|
||||
|
||||
counter = self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(
|
||||
self._StatusHelper, opt.quiet, opt.this_manifest_only
|
||||
),
|
||||
all_projects,
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
)
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = all_projects
|
||||
counter = self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(
|
||||
self._StatusHelper, opt.quiet, opt.this_manifest_only
|
||||
),
|
||||
range(len(all_projects)),
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
chunksize=1,
|
||||
)
|
||||
|
||||
if not opt.quiet and len(all_projects) == counter:
|
||||
print("nothing to commit (working directory clean)")
|
||||
|
||||
+1237
-290
File diff suppressed because it is too large
Load Diff
+36
-21
@@ -27,6 +27,7 @@ from error import SilentRepoExitError
|
||||
from error import UploadError
|
||||
from git_command import GitCommand
|
||||
from git_refs import R_HEADS
|
||||
import git_superproject
|
||||
from hooks import RepoHook
|
||||
from project import ReviewableBranch
|
||||
from repo_logging import RepoLogger
|
||||
@@ -267,7 +268,6 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
"--cc",
|
||||
type="string",
|
||||
action="append",
|
||||
dest="cc",
|
||||
help="also send email to these email addresses",
|
||||
)
|
||||
p.add_option(
|
||||
@@ -281,7 +281,6 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
p.add_option(
|
||||
"-c",
|
||||
"--current-branch",
|
||||
dest="current_branch",
|
||||
action="store_true",
|
||||
help="upload current git branch",
|
||||
)
|
||||
@@ -310,7 +309,6 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
"-p",
|
||||
"--private",
|
||||
action="store_true",
|
||||
dest="private",
|
||||
default=False,
|
||||
help="upload as a private change (deprecated; use --wip)",
|
||||
)
|
||||
@@ -318,7 +316,6 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
"-w",
|
||||
"--wip",
|
||||
action="store_true",
|
||||
dest="wip",
|
||||
default=False,
|
||||
help="upload as a work-in-progress change",
|
||||
)
|
||||
@@ -603,19 +600,22 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
full_dest = destination
|
||||
if not full_dest.startswith(R_HEADS):
|
||||
full_dest = R_HEADS + full_dest
|
||||
full_revision = branch.project.revisionExpr
|
||||
if not full_revision.startswith(R_HEADS):
|
||||
full_revision = R_HEADS + full_revision
|
||||
|
||||
# If the merge branch of the local branch is different from
|
||||
# the project's revision AND destination, this might not be
|
||||
# intentional.
|
||||
if (
|
||||
merge_branch
|
||||
and merge_branch != branch.project.revisionExpr
|
||||
and merge_branch != full_revision
|
||||
and merge_branch != full_dest
|
||||
):
|
||||
print(
|
||||
f"For local branch {branch.name}: merge branch "
|
||||
f"{merge_branch} does not match destination branch "
|
||||
f"{destination}"
|
||||
f"{destination} and revision {branch.project.revisionExpr}"
|
||||
)
|
||||
print("skipping upload.")
|
||||
print(
|
||||
@@ -625,6 +625,16 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
branch.uploaded = False
|
||||
return
|
||||
|
||||
# If using superproject, add the root repo as a push option.
|
||||
manifest = branch.project.manifest
|
||||
push_options = list(opt.push_options)
|
||||
if git_superproject.UseSuperproject(None, manifest):
|
||||
sp = manifest.superproject
|
||||
if sp:
|
||||
r_id = sp.repo_id
|
||||
if r_id:
|
||||
push_options.append(f"custom-keyed-value=rootRepo:{r_id}")
|
||||
|
||||
branch.UploadForReview(
|
||||
people,
|
||||
dryrun=opt.dryrun,
|
||||
@@ -637,7 +647,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
ready=opt.ready,
|
||||
dest_branch=destination,
|
||||
validate_certs=opt.validate_certs,
|
||||
push_options=opt.push_options,
|
||||
push_options=push_options,
|
||||
patchset_description=opt.patchset_description,
|
||||
)
|
||||
|
||||
@@ -713,16 +723,17 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
merge_branch = p.stdout.strip()
|
||||
return merge_branch
|
||||
|
||||
@staticmethod
|
||||
def _GatherOne(opt, project):
|
||||
@classmethod
|
||||
def _GatherOne(cls, opt, project_idx):
|
||||
"""Figure out the upload status for |project|."""
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
if opt.current_branch:
|
||||
cbr = project.CurrentBranch
|
||||
up_branch = project.GetUploadableBranch(cbr)
|
||||
avail = [up_branch] if up_branch else None
|
||||
else:
|
||||
avail = project.GetUploadableBranches(opt.branch)
|
||||
return (project, avail)
|
||||
return (project_idx, avail)
|
||||
|
||||
def Execute(self, opt, args):
|
||||
projects = self.GetProjects(
|
||||
@@ -732,7 +743,8 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
def _ProcessResults(_pool, _out, results):
|
||||
pending = []
|
||||
for result in results:
|
||||
project, avail = result
|
||||
project_idx, avail = result
|
||||
project = projects[project_idx]
|
||||
if avail is None:
|
||||
logger.error(
|
||||
'repo: error: %s: Unable to upload branch "%s". '
|
||||
@@ -743,15 +755,17 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
project.manifest.branch,
|
||||
)
|
||||
elif avail:
|
||||
pending.append(result)
|
||||
pending.append((project, avail))
|
||||
return pending
|
||||
|
||||
pending = self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._GatherOne, opt),
|
||||
projects,
|
||||
callback=_ProcessResults,
|
||||
)
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = projects
|
||||
pending = self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._GatherOne, opt),
|
||||
range(len(projects)),
|
||||
callback=_ProcessResults,
|
||||
)
|
||||
|
||||
if not pending:
|
||||
if opt.branch is None:
|
||||
@@ -789,9 +803,10 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
project_list=pending_proj_names, worktree_list=pending_worktrees
|
||||
):
|
||||
if LocalSyncState(manifest).IsPartiallySynced():
|
||||
logger.error(
|
||||
"Partially synced tree detected. Syncing all projects "
|
||||
"may resolve issues you're seeing."
|
||||
logger.info(
|
||||
"Tip: A partially synced tree was detected. "
|
||||
"If this failure involves cross-project dependencies, "
|
||||
"a full `repo sync` might help."
|
||||
)
|
||||
ret = 1
|
||||
if ret:
|
||||
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
# Copyright (C) 2025 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import List
|
||||
|
||||
from command import Command
|
||||
from error import GitError
|
||||
from error import RepoExitError
|
||||
import platform_utils
|
||||
from project import DeleteWorktreeError
|
||||
|
||||
|
||||
class Error(RepoExitError):
|
||||
"""Exit error when wipe command fails."""
|
||||
|
||||
|
||||
class Wipe(Command):
|
||||
"""Delete projects from the worktree and .repo"""
|
||||
|
||||
COMMON = True
|
||||
helpSummary = "Wipe projects from the worktree"
|
||||
helpUsage = """
|
||||
%prog <project>...
|
||||
"""
|
||||
helpDescription = """
|
||||
The '%prog' command removes the specified projects from the worktree
|
||||
(the checked out source code) and deletes the project's git data from `.repo`.
|
||||
|
||||
This is a destructive operation and cannot be undone.
|
||||
|
||||
Projects can be specified either by name, or by a relative or absolute path
|
||||
to the project's local directory.
|
||||
|
||||
Examples:
|
||||
|
||||
# Wipe the project "platform/build" by name:
|
||||
$ repo wipe platform/build
|
||||
|
||||
# Wipe the project at the path "build/make":
|
||||
$ repo wipe build/make
|
||||
"""
|
||||
|
||||
def _Options(self, p):
|
||||
# TODO(crbug.com/gerrit/393383056): Add --broken option to scan and
|
||||
# wipe broken projects.
|
||||
p.add_option(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="force wipe shared projects and uncommitted changes",
|
||||
)
|
||||
p.add_option(
|
||||
"--force-uncommitted",
|
||||
action="store_true",
|
||||
help="force wipe even if there are uncommitted changes",
|
||||
)
|
||||
p.add_option(
|
||||
"--force-shared",
|
||||
action="store_true",
|
||||
help="force wipe even if the project shares an object directory",
|
||||
)
|
||||
|
||||
def ValidateOptions(self, opt, args: List[str]):
|
||||
if not args:
|
||||
self.Usage()
|
||||
|
||||
def Execute(self, opt, args: List[str]):
|
||||
# Get all projects to handle shared object directories.
|
||||
all_projects = self.GetProjects(None, all_manifests=True, groups="all")
|
||||
projects_to_wipe = self.GetProjects(args, all_manifests=True)
|
||||
relpaths_to_wipe = {p.relpath for p in projects_to_wipe}
|
||||
|
||||
# Build a map from objdir to the relpaths of projects that use it.
|
||||
objdir_map = {}
|
||||
for p in all_projects:
|
||||
objdir_map.setdefault(p.objdir, set()).add(p.relpath)
|
||||
|
||||
uncommitted_projects = []
|
||||
shared_objdirs = {}
|
||||
objdirs_to_delete = set()
|
||||
|
||||
for project in projects_to_wipe:
|
||||
if project == self.manifest.manifestProject:
|
||||
raise Error(
|
||||
f"error: cannot wipe the manifest project: {project.name}"
|
||||
)
|
||||
|
||||
try:
|
||||
if project.HasChanges():
|
||||
uncommitted_projects.append(project.name)
|
||||
except GitError:
|
||||
uncommitted_projects.append(f"{project.name} (corrupted)")
|
||||
|
||||
users = objdir_map.get(project.objdir, {project.relpath})
|
||||
is_shared = not users.issubset(relpaths_to_wipe)
|
||||
if is_shared:
|
||||
shared_objdirs.setdefault(project.objdir, set()).update(users)
|
||||
else:
|
||||
objdirs_to_delete.add(project.objdir)
|
||||
|
||||
block_uncommitted = uncommitted_projects and not (
|
||||
opt.force or opt.force_uncommitted
|
||||
)
|
||||
block_shared = shared_objdirs and not (opt.force or opt.force_shared)
|
||||
|
||||
if block_uncommitted or block_shared:
|
||||
error_messages = []
|
||||
if block_uncommitted:
|
||||
error_messages.append(
|
||||
"The following projects have uncommitted changes or are "
|
||||
"corrupted:\n"
|
||||
+ "\n".join(f" - {p}" for p in sorted(uncommitted_projects))
|
||||
)
|
||||
if block_shared:
|
||||
shared_dir_messages = []
|
||||
for objdir, users in sorted(shared_objdirs.items()):
|
||||
other_users = users - relpaths_to_wipe
|
||||
projects_to_wipe_in_dir = users & relpaths_to_wipe
|
||||
message = f"""Object directory {objdir} is shared by:
|
||||
Projects to be wiped: {', '.join(sorted(projects_to_wipe_in_dir))}
|
||||
Projects not to be wiped: {', '.join(sorted(other_users))}"""
|
||||
shared_dir_messages.append(message)
|
||||
error_messages.append(
|
||||
"The following projects have shared object directories:\n"
|
||||
+ "\n".join(sorted(shared_dir_messages))
|
||||
)
|
||||
|
||||
if block_uncommitted and block_shared:
|
||||
error_messages.append(
|
||||
"Use --force to wipe anyway, or --force-uncommitted and "
|
||||
"--force-shared to specify."
|
||||
)
|
||||
elif block_uncommitted:
|
||||
error_messages.append("Use --force-uncommitted to wipe anyway.")
|
||||
else:
|
||||
error_messages.append("Use --force-shared to wipe anyway.")
|
||||
|
||||
raise Error("\n\n".join(error_messages))
|
||||
|
||||
# If we are here, either there were no issues, or --force was used.
|
||||
# Proceed with wiping.
|
||||
successful_wipes = set()
|
||||
|
||||
for project in projects_to_wipe:
|
||||
try:
|
||||
# Force the delete here since we've already performed our
|
||||
# own safety checks above.
|
||||
project.DeleteWorktree(force=True, verbose=opt.verbose)
|
||||
successful_wipes.add(project.relpath)
|
||||
except DeleteWorktreeError as e:
|
||||
print(
|
||||
f"error: failed to wipe {project.name}: {e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
# Clean up object directories only if all projects using them were
|
||||
# successfully wiped.
|
||||
for objdir in objdirs_to_delete:
|
||||
users = objdir_map.get(objdir, set())
|
||||
# Check if every project that uses this objdir has been
|
||||
# successfully processed. If a project failed to be wiped, don't
|
||||
# delete the object directory, or we'll corrupt the remaining
|
||||
# project.
|
||||
if users.issubset(successful_wipes):
|
||||
if os.path.exists(objdir):
|
||||
if opt.verbose:
|
||||
print(
|
||||
f"Deleting objects directory: {objdir}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
platform_utils.rmtree(objdir)
|
||||
@@ -0,0 +1,20 @@
|
||||
# Repo Tests
|
||||
|
||||
There is a mixture of [pytest] & [Python unittest] in here. We adopted [pytest]
|
||||
later on but didn't migrate existing tests (since they still work). New tests
|
||||
should be written using [pytest] only.
|
||||
|
||||
## File layout
|
||||
|
||||
* `test_xxx.py`: Unittests for the `xxx` module in the main repo codebase.
|
||||
Modules that are in subdirs normalize the `/` into `_`.
|
||||
For example, [test_error.py](./test_error.py) is for the
|
||||
[error.py](../error.py) module, and
|
||||
[test_subcmds_forall.py](./test_subcmds_forall.py) is for the
|
||||
[subcmds/forall.py](../subcmds/forall.py) module.
|
||||
* [conftest.py](./conftest.py): Custom pytest fixtures for sharing.
|
||||
* [utils_for_test.py](./utils_for_test.py): Helpers for sharing in tests.
|
||||
|
||||
|
||||
[pytest]: https://pytest.org/
|
||||
[Python unittest]: https://docs.python.org/3/library/unittest.html#unittest.TestCase
|
||||
+12
-1
@@ -1,4 +1,4 @@
|
||||
# Copyright 2022 The Android Open Source Project
|
||||
# Copyright (C) 2022 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -18,6 +18,7 @@ import pathlib
|
||||
|
||||
import pytest
|
||||
|
||||
import color
|
||||
import platform_utils
|
||||
import repo_trace
|
||||
|
||||
@@ -28,6 +29,16 @@ def disable_repo_trace(tmp_path):
|
||||
repo_trace._TRACE_FILE = str(tmp_path / "TRACE_FILE_from_test")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def restore_default_coloring() -> None:
|
||||
"""Force disable color for reproducible test behavior.
|
||||
|
||||
The few tests that cover color behavior can still reset/change the color as
|
||||
needed.
|
||||
"""
|
||||
color.SetDefaultColoring("never")
|
||||
|
||||
|
||||
# adapted from pytest-home 0.5.1
|
||||
def _set_home(monkeypatch, path: pathlib.Path):
|
||||
"""
|
||||
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
gitc_dir=/test/usr/local/google/gitc
|
||||
+43
-43
@@ -14,61 +14,61 @@
|
||||
|
||||
"""Unittests for the color.py module."""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
import pytest
|
||||
import utils_for_test
|
||||
|
||||
import color
|
||||
import git_config
|
||||
|
||||
|
||||
def fixture(*paths):
|
||||
"""Return a path relative to test/fixtures."""
|
||||
return os.path.join(os.path.dirname(__file__), "fixtures", *paths)
|
||||
@pytest.fixture
|
||||
def coloring() -> color.Coloring:
|
||||
"""Create a Coloring object for testing."""
|
||||
config_fixture = utils_for_test.FIXTURES_DIR / "test.gitconfig"
|
||||
config = git_config.GitConfig(config_fixture)
|
||||
color.SetDefaultColoring("true")
|
||||
return color.Coloring(config, "status")
|
||||
|
||||
|
||||
class ColoringTests(unittest.TestCase):
|
||||
"""tests of the Coloring class."""
|
||||
def test_Color_Parse_all_params_none(coloring: color.Coloring) -> None:
|
||||
"""all params are None"""
|
||||
val = coloring._parse(None, None, None, None)
|
||||
assert val == ""
|
||||
|
||||
def setUp(self):
|
||||
"""Create a GitConfig object using the test.gitconfig fixture."""
|
||||
config_fixture = fixture("test.gitconfig")
|
||||
self.config = git_config.GitConfig(config_fixture)
|
||||
color.SetDefaultColoring("true")
|
||||
self.color = color.Coloring(self.config, "status")
|
||||
|
||||
def test_Color_Parse_all_params_none(self):
|
||||
"""all params are None"""
|
||||
val = self.color._parse(None, None, None, None)
|
||||
self.assertEqual("", val)
|
||||
def test_Color_Parse_first_parameter_none(coloring: color.Coloring) -> None:
|
||||
"""check fg & bg & attr"""
|
||||
val = coloring._parse(None, "black", "red", "ul")
|
||||
assert val == "\x1b[4;30;41m"
|
||||
|
||||
def test_Color_Parse_first_parameter_none(self):
|
||||
"""check fg & bg & attr"""
|
||||
val = self.color._parse(None, "black", "red", "ul")
|
||||
self.assertEqual("\x1b[4;30;41m", val)
|
||||
|
||||
def test_Color_Parse_one_entry(self):
|
||||
"""check fg"""
|
||||
val = self.color._parse("one", None, None, None)
|
||||
self.assertEqual("\033[33m", val)
|
||||
def test_Color_Parse_one_entry(coloring: color.Coloring) -> None:
|
||||
"""check fg"""
|
||||
val = coloring._parse("one", None, None, None)
|
||||
assert val == "\033[33m"
|
||||
|
||||
def test_Color_Parse_two_entry(self):
|
||||
"""check fg & bg"""
|
||||
val = self.color._parse("two", None, None, None)
|
||||
self.assertEqual("\033[35;46m", val)
|
||||
|
||||
def test_Color_Parse_three_entry(self):
|
||||
"""check fg & bg & attr"""
|
||||
val = self.color._parse("three", None, None, None)
|
||||
self.assertEqual("\033[4;30;41m", val)
|
||||
def test_Color_Parse_two_entry(coloring: color.Coloring) -> None:
|
||||
"""check fg & bg"""
|
||||
val = coloring._parse("two", None, None, None)
|
||||
assert val == "\033[35;46m"
|
||||
|
||||
def test_Color_Parse_reset_entry(self):
|
||||
"""check reset entry"""
|
||||
val = self.color._parse("reset", None, None, None)
|
||||
self.assertEqual("\033[m", val)
|
||||
|
||||
def test_Color_Parse_empty_entry(self):
|
||||
"""check empty entry"""
|
||||
val = self.color._parse("none", "blue", "white", "dim")
|
||||
self.assertEqual("\033[2;34;47m", val)
|
||||
val = self.color._parse("empty", "green", "white", "bold")
|
||||
self.assertEqual("\033[1;32;47m", val)
|
||||
def test_Color_Parse_three_entry(coloring: color.Coloring) -> None:
|
||||
"""check fg & bg & attr"""
|
||||
val = coloring._parse("three", None, None, None)
|
||||
assert val == "\033[4;30;41m"
|
||||
|
||||
|
||||
def test_Color_Parse_reset_entry(coloring: color.Coloring) -> None:
|
||||
"""check reset entry"""
|
||||
val = coloring._parse("reset", None, None, None)
|
||||
assert val == "\033[m"
|
||||
|
||||
|
||||
def test_Color_Parse_empty_entry(coloring: color.Coloring) -> None:
|
||||
"""check empty entry"""
|
||||
val = coloring._parse("none", "blue", "white", "dim")
|
||||
assert val == "\033[2;34;47m"
|
||||
val = coloring._parse("empty", "green", "white", "bold")
|
||||
assert val == "\033[1;32;47m"
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Copyright (C) 2026 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Unittests for the command.py module."""
|
||||
|
||||
from command import Command
|
||||
|
||||
|
||||
class FakeProject:
|
||||
"""Minimal project double for Command.GetProjects tests."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
relpath,
|
||||
*,
|
||||
gitdir=None,
|
||||
derived_subprojects=None,
|
||||
sync_s=False,
|
||||
):
|
||||
self.name = name
|
||||
self.relpath = relpath
|
||||
self.gitdir = gitdir or f"/git/{relpath}"
|
||||
self.sync_s = sync_s
|
||||
self.Exists = True
|
||||
self._derived_subprojects = derived_subprojects or []
|
||||
|
||||
def GetDerivedSubprojects(self):
|
||||
return list(self._derived_subprojects)
|
||||
|
||||
def MatchesGroups(self, _groups):
|
||||
return True
|
||||
|
||||
def RelPath(self, local=True):
|
||||
return self.relpath
|
||||
|
||||
|
||||
class FakeManifest:
|
||||
"""Minimal manifest double for Command.GetProjects tests."""
|
||||
|
||||
def __init__(self, projects):
|
||||
self.projects = projects
|
||||
|
||||
def GetManifestGroupsStr(self):
|
||||
return "default"
|
||||
|
||||
|
||||
def test_get_projects_keeps_derived_subprojects_for_repeated_repo():
|
||||
"""Derived subprojects are keyed by checkout path, not repo identity."""
|
||||
submodule_a = FakeProject(
|
||||
"submodule",
|
||||
"src/one/submodule",
|
||||
gitdir="/shared/modules/submodule.git",
|
||||
)
|
||||
submodule_b = FakeProject(
|
||||
"submodule",
|
||||
"src/two/submodule",
|
||||
gitdir="/shared/modules/submodule.git",
|
||||
)
|
||||
project_a = FakeProject(
|
||||
"project",
|
||||
"src/one",
|
||||
derived_subprojects=[submodule_a],
|
||||
sync_s=True,
|
||||
)
|
||||
project_b = FakeProject(
|
||||
"project",
|
||||
"src/two",
|
||||
derived_subprojects=[submodule_b],
|
||||
sync_s=True,
|
||||
)
|
||||
manifest = FakeManifest([project_a, project_b])
|
||||
cmd = Command(manifest=manifest)
|
||||
|
||||
projects = cmd.GetProjects([])
|
||||
|
||||
assert set(projects) == {project_a, project_b, submodule_a, submodule_b}
|
||||
+18
-29
@@ -14,43 +14,32 @@
|
||||
|
||||
"""Unittests for the editor.py module."""
|
||||
|
||||
import unittest
|
||||
import pytest
|
||||
|
||||
from editor import Editor
|
||||
|
||||
|
||||
class EditorTestCase(unittest.TestCase):
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_editor() -> None:
|
||||
"""Take care of resetting Editor state across tests."""
|
||||
|
||||
def setUp(self):
|
||||
self.setEditor(None)
|
||||
|
||||
def tearDown(self):
|
||||
self.setEditor(None)
|
||||
|
||||
@staticmethod
|
||||
def setEditor(editor):
|
||||
Editor._editor = editor
|
||||
Editor._editor = None
|
||||
yield
|
||||
Editor._editor = None
|
||||
|
||||
|
||||
class GetEditor(EditorTestCase):
|
||||
"""Check GetEditor behavior."""
|
||||
|
||||
def test_basic(self):
|
||||
"""Basic checking of _GetEditor."""
|
||||
self.setEditor(":")
|
||||
self.assertEqual(":", Editor._GetEditor())
|
||||
def test_basic() -> None:
|
||||
"""Basic checking of _GetEditor."""
|
||||
Editor._editor = ":"
|
||||
assert Editor._GetEditor() == ":"
|
||||
|
||||
|
||||
class EditString(EditorTestCase):
|
||||
"""Check EditString behavior."""
|
||||
def test_no_editor() -> None:
|
||||
"""Check behavior when no editor is available."""
|
||||
Editor._editor = ":"
|
||||
assert Editor.EditString("foo") == "foo"
|
||||
|
||||
def test_no_editor(self):
|
||||
"""Check behavior when no editor is available."""
|
||||
self.setEditor(":")
|
||||
self.assertEqual("foo", Editor.EditString("foo"))
|
||||
|
||||
def test_cat_editor(self):
|
||||
"""Check behavior when editor is `cat`."""
|
||||
self.setEditor("cat")
|
||||
self.assertEqual("foo", Editor.EditString("foo"))
|
||||
def test_cat_editor() -> None:
|
||||
"""Check behavior when editor is `cat`."""
|
||||
Editor._editor = "cat"
|
||||
assert Editor.EditString("foo") == "foo"
|
||||
|
||||
+34
-33
@@ -1,4 +1,4 @@
|
||||
# Copyright 2021 The Android Open Source Project
|
||||
# Copyright (C) 2021 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
import inspect
|
||||
import pickle
|
||||
import unittest
|
||||
from typing import Iterator, Type
|
||||
|
||||
import pytest
|
||||
|
||||
import command
|
||||
import error
|
||||
@@ -26,7 +28,7 @@ import project
|
||||
from subcmds import all_modules
|
||||
|
||||
|
||||
imports = all_modules + [
|
||||
_IMPORTS = all_modules + [
|
||||
error,
|
||||
project,
|
||||
git_command,
|
||||
@@ -35,36 +37,35 @@ imports = all_modules + [
|
||||
]
|
||||
|
||||
|
||||
class PickleTests(unittest.TestCase):
|
||||
"""Make sure all our custom exceptions can be pickled."""
|
||||
def get_exceptions() -> Iterator[Type[Exception]]:
|
||||
"""Return all our custom exceptions."""
|
||||
for entry in _IMPORTS:
|
||||
for name in dir(entry):
|
||||
cls = getattr(entry, name)
|
||||
if isinstance(cls, type) and issubclass(cls, Exception):
|
||||
yield cls
|
||||
|
||||
def getExceptions(self):
|
||||
"""Return all our custom exceptions."""
|
||||
for entry in imports:
|
||||
for name in dir(entry):
|
||||
cls = getattr(entry, name)
|
||||
if isinstance(cls, type) and issubclass(cls, Exception):
|
||||
yield cls
|
||||
|
||||
def testExceptionLookup(self):
|
||||
"""Make sure our introspection logic works."""
|
||||
classes = list(self.getExceptions())
|
||||
self.assertIn(error.HookError, classes)
|
||||
# Don't assert the exact number to avoid being a change-detector test.
|
||||
self.assertGreater(len(classes), 10)
|
||||
def test_exception_lookup() -> None:
|
||||
"""Make sure our introspection logic works."""
|
||||
classes = list(get_exceptions())
|
||||
assert error.HookError in classes
|
||||
# Don't assert the exact number to avoid being a change-detector test.
|
||||
assert len(classes) > 10
|
||||
|
||||
def testPickle(self):
|
||||
"""Try to pickle all the exceptions."""
|
||||
for cls in self.getExceptions():
|
||||
args = inspect.getfullargspec(cls.__init__).args[1:]
|
||||
obj = cls(*args)
|
||||
p = pickle.dumps(obj)
|
||||
try:
|
||||
newobj = pickle.loads(p)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
self.fail(
|
||||
"Class %s is unable to be pickled: %s\n"
|
||||
"Incomplete super().__init__(...) call?" % (cls, e)
|
||||
)
|
||||
self.assertIsInstance(newobj, cls)
|
||||
self.assertEqual(str(obj), str(newobj))
|
||||
|
||||
@pytest.mark.parametrize("cls", get_exceptions())
|
||||
def test_pickle(cls: Type[Exception]) -> None:
|
||||
"""Try to pickle all the exceptions."""
|
||||
args = inspect.getfullargspec(cls.__init__).args[1:]
|
||||
obj = cls(*args)
|
||||
p = pickle.dumps(obj)
|
||||
try:
|
||||
newobj = pickle.loads(p)
|
||||
except Exception as e:
|
||||
pytest.fail(
|
||||
f"Class {cls} is unable to be pickled: {e}\n"
|
||||
"Incomplete super().__init__(...) call?"
|
||||
)
|
||||
assert isinstance(newobj, cls)
|
||||
assert str(obj) == str(newobj)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2019 The Android Open Source Project
|
||||
# Copyright (C) 2019 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -21,6 +21,8 @@ import subprocess
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
import git_command
|
||||
import wrapper
|
||||
|
||||
@@ -263,6 +265,7 @@ class UserAgentUnitTest(unittest.TestCase):
|
||||
m = re.match(r"^[^ ]+$", os_name)
|
||||
self.assertIsNotNone(m)
|
||||
|
||||
@pytest.mark.skip_cq("TODO(b/266734831): Find out why this fails in CQ")
|
||||
def test_smoke_repo(self):
|
||||
"""Make sure repo UA returns something useful."""
|
||||
ua = git_command.user_agent.repo
|
||||
@@ -271,6 +274,7 @@ class UserAgentUnitTest(unittest.TestCase):
|
||||
m = re.match(r"^git-repo/[^ ]+ ([^ ]+) git/[^ ]+ Python/[0-9.]+", ua)
|
||||
self.assertIsNotNone(m)
|
||||
|
||||
@pytest.mark.skip_cq("TODO(b/266734831): Find out why this fails in CQ")
|
||||
def test_smoke_git(self):
|
||||
"""Make sure git UA returns something useful."""
|
||||
ua = git_command.user_agent.git
|
||||
|
||||
+208
-152
@@ -14,177 +14,233 @@
|
||||
|
||||
"""Unittests for the git_config.py module."""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import utils_for_test
|
||||
|
||||
import git_config
|
||||
|
||||
|
||||
def fixture(*paths):
|
||||
"""Return a path relative to test/fixtures."""
|
||||
return os.path.join(os.path.dirname(__file__), "fixtures", *paths)
|
||||
@pytest.fixture
|
||||
def readonly_config() -> git_config.GitConfig:
|
||||
"""Create a GitConfig object using the test.gitconfig fixture."""
|
||||
config_fixture = utils_for_test.FIXTURES_DIR / "test.gitconfig"
|
||||
return git_config.GitConfig(config_fixture)
|
||||
|
||||
|
||||
class GitConfigReadOnlyTests(unittest.TestCase):
|
||||
"""Read-only tests of the GitConfig class."""
|
||||
def test_get_string_with_empty_config_values(
|
||||
readonly_config: git_config.GitConfig,
|
||||
) -> None:
|
||||
"""Test config entries with no value.
|
||||
|
||||
def setUp(self):
|
||||
"""Create a GitConfig object using the test.gitconfig fixture."""
|
||||
config_fixture = fixture("test.gitconfig")
|
||||
self.config = git_config.GitConfig(config_fixture)
|
||||
[section]
|
||||
empty
|
||||
|
||||
def test_GetString_with_empty_config_values(self):
|
||||
"""
|
||||
Test config entries with no value.
|
||||
|
||||
[section]
|
||||
empty
|
||||
|
||||
"""
|
||||
val = self.config.GetString("section.empty")
|
||||
self.assertEqual(val, None)
|
||||
|
||||
def test_GetString_with_true_value(self):
|
||||
"""
|
||||
Test config entries with a string value.
|
||||
|
||||
[section]
|
||||
nonempty = true
|
||||
|
||||
"""
|
||||
val = self.config.GetString("section.nonempty")
|
||||
self.assertEqual(val, "true")
|
||||
|
||||
def test_GetString_from_missing_file(self):
|
||||
"""
|
||||
Test missing config file
|
||||
"""
|
||||
config_fixture = fixture("not.present.gitconfig")
|
||||
config = git_config.GitConfig(config_fixture)
|
||||
val = config.GetString("empty")
|
||||
self.assertEqual(val, None)
|
||||
|
||||
def test_GetBoolean_undefined(self):
|
||||
"""Test GetBoolean on key that doesn't exist."""
|
||||
self.assertIsNone(self.config.GetBoolean("section.missing"))
|
||||
|
||||
def test_GetBoolean_invalid(self):
|
||||
"""Test GetBoolean on invalid boolean value."""
|
||||
self.assertIsNone(self.config.GetBoolean("section.boolinvalid"))
|
||||
|
||||
def test_GetBoolean_true(self):
|
||||
"""Test GetBoolean on valid true boolean."""
|
||||
self.assertTrue(self.config.GetBoolean("section.booltrue"))
|
||||
|
||||
def test_GetBoolean_false(self):
|
||||
"""Test GetBoolean on valid false boolean."""
|
||||
self.assertFalse(self.config.GetBoolean("section.boolfalse"))
|
||||
|
||||
def test_GetInt_undefined(self):
|
||||
"""Test GetInt on key that doesn't exist."""
|
||||
self.assertIsNone(self.config.GetInt("section.missing"))
|
||||
|
||||
def test_GetInt_invalid(self):
|
||||
"""Test GetInt on invalid integer value."""
|
||||
self.assertIsNone(self.config.GetBoolean("section.intinvalid"))
|
||||
|
||||
def test_GetInt_valid(self):
|
||||
"""Test GetInt on valid integers."""
|
||||
TESTS = (
|
||||
("inthex", 16),
|
||||
("inthexk", 16384),
|
||||
("int", 10),
|
||||
("intk", 10240),
|
||||
("intm", 10485760),
|
||||
("intg", 10737418240),
|
||||
)
|
||||
for key, value in TESTS:
|
||||
self.assertEqual(value, self.config.GetInt(f"section.{key}"))
|
||||
"""
|
||||
val = readonly_config.GetString("section.empty")
|
||||
assert val is None
|
||||
|
||||
|
||||
class GitConfigReadWriteTests(unittest.TestCase):
|
||||
"""Read/write tests of the GitConfig class."""
|
||||
def test_get_string_with_true_value(
|
||||
readonly_config: git_config.GitConfig,
|
||||
) -> None:
|
||||
"""Test config entries with a string value.
|
||||
|
||||
def setUp(self):
|
||||
self.tmpfile = tempfile.NamedTemporaryFile()
|
||||
self.config = self.get_config()
|
||||
[section]
|
||||
nonempty = true
|
||||
|
||||
def get_config(self):
|
||||
"""Get a new GitConfig instance."""
|
||||
return git_config.GitConfig(self.tmpfile.name)
|
||||
"""
|
||||
val = readonly_config.GetString("section.nonempty")
|
||||
assert val == "true"
|
||||
|
||||
def test_SetString(self):
|
||||
"""Test SetString behavior."""
|
||||
# Set a value.
|
||||
self.assertIsNone(self.config.GetString("foo.bar"))
|
||||
self.config.SetString("foo.bar", "val")
|
||||
self.assertEqual("val", self.config.GetString("foo.bar"))
|
||||
|
||||
# Make sure the value was actually written out.
|
||||
config = self.get_config()
|
||||
self.assertEqual("val", config.GetString("foo.bar"))
|
||||
def test_get_string_from_missing_file() -> None:
|
||||
"""Test missing config file."""
|
||||
config_fixture = utils_for_test.FIXTURES_DIR / "not.present.gitconfig"
|
||||
config = git_config.GitConfig(config_fixture)
|
||||
val = config.GetString("empty")
|
||||
assert val is None
|
||||
|
||||
# Update the value.
|
||||
self.config.SetString("foo.bar", "valll")
|
||||
self.assertEqual("valll", self.config.GetString("foo.bar"))
|
||||
config = self.get_config()
|
||||
self.assertEqual("valll", config.GetString("foo.bar"))
|
||||
|
||||
# Delete the value.
|
||||
self.config.SetString("foo.bar", None)
|
||||
self.assertIsNone(self.config.GetString("foo.bar"))
|
||||
config = self.get_config()
|
||||
self.assertIsNone(config.GetString("foo.bar"))
|
||||
def test_get_boolean_undefined(readonly_config: git_config.GitConfig) -> None:
|
||||
"""Test GetBoolean on key that doesn't exist."""
|
||||
assert readonly_config.GetBoolean("section.missing") is None
|
||||
|
||||
def test_SetBoolean(self):
|
||||
"""Test SetBoolean behavior."""
|
||||
# Set a true value.
|
||||
self.assertIsNone(self.config.GetBoolean("foo.bar"))
|
||||
for val in (True, 1):
|
||||
self.config.SetBoolean("foo.bar", val)
|
||||
self.assertTrue(self.config.GetBoolean("foo.bar"))
|
||||
|
||||
# Make sure the value was actually written out.
|
||||
config = self.get_config()
|
||||
self.assertTrue(config.GetBoolean("foo.bar"))
|
||||
self.assertEqual("true", config.GetString("foo.bar"))
|
||||
def test_get_boolean_invalid(readonly_config: git_config.GitConfig) -> None:
|
||||
"""Test GetBoolean on invalid boolean value."""
|
||||
assert readonly_config.GetBoolean("section.boolinvalid") is None
|
||||
|
||||
# Set a false value.
|
||||
for val in (False, 0):
|
||||
self.config.SetBoolean("foo.bar", val)
|
||||
self.assertFalse(self.config.GetBoolean("foo.bar"))
|
||||
|
||||
# Make sure the value was actually written out.
|
||||
config = self.get_config()
|
||||
self.assertFalse(config.GetBoolean("foo.bar"))
|
||||
self.assertEqual("false", config.GetString("foo.bar"))
|
||||
def test_get_boolean_true(readonly_config: git_config.GitConfig) -> None:
|
||||
"""Test GetBoolean on valid true boolean."""
|
||||
assert readonly_config.GetBoolean("section.booltrue") is True
|
||||
|
||||
# Delete the value.
|
||||
self.config.SetBoolean("foo.bar", None)
|
||||
self.assertIsNone(self.config.GetBoolean("foo.bar"))
|
||||
config = self.get_config()
|
||||
self.assertIsNone(config.GetBoolean("foo.bar"))
|
||||
|
||||
def test_GetSyncAnalysisStateData(self):
|
||||
"""Test config entries with a sync state analysis data."""
|
||||
superproject_logging_data = {}
|
||||
superproject_logging_data["test"] = False
|
||||
options = type("options", (object,), {})()
|
||||
options.verbose = "true"
|
||||
options.mp_update = "false"
|
||||
TESTS = (
|
||||
("superproject.test", "false"),
|
||||
("options.verbose", "true"),
|
||||
("options.mpupdate", "false"),
|
||||
("main.version", "1"),
|
||||
)
|
||||
self.config.UpdateSyncAnalysisState(options, superproject_logging_data)
|
||||
sync_data = self.config.GetSyncAnalysisStateData()
|
||||
for key, value in TESTS:
|
||||
self.assertEqual(
|
||||
sync_data[f"{git_config.SYNC_STATE_PREFIX}{key}"], value
|
||||
)
|
||||
self.assertTrue(
|
||||
sync_data[f"{git_config.SYNC_STATE_PREFIX}main.synctime"]
|
||||
)
|
||||
def test_get_boolean_false(readonly_config: git_config.GitConfig) -> None:
|
||||
"""Test GetBoolean on valid false boolean."""
|
||||
assert readonly_config.GetBoolean("section.boolfalse") is False
|
||||
|
||||
|
||||
def test_get_int_undefined(readonly_config: git_config.GitConfig) -> None:
|
||||
"""Test GetInt on key that doesn't exist."""
|
||||
assert readonly_config.GetInt("section.missing") is None
|
||||
|
||||
|
||||
def test_get_int_invalid(readonly_config: git_config.GitConfig) -> None:
|
||||
"""Test GetInt on invalid integer value."""
|
||||
assert readonly_config.GetInt("section.intinvalid") is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"key, expected",
|
||||
(
|
||||
("inthex", 16),
|
||||
("inthexk", 16384),
|
||||
("int", 10),
|
||||
("intk", 10240),
|
||||
("intm", 10485760),
|
||||
("intg", 10737418240),
|
||||
),
|
||||
)
|
||||
def test_get_int_valid(
|
||||
readonly_config: git_config.GitConfig, key: str, expected: int
|
||||
) -> None:
|
||||
"""Test GetInt on valid integers."""
|
||||
assert readonly_config.GetInt(f"section.{key}") == expected
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rw_config_file(tmp_path: Path) -> Path:
|
||||
"""Return a path to a temporary config file."""
|
||||
return tmp_path / "config"
|
||||
|
||||
|
||||
def test_set_string(rw_config_file: Path) -> None:
|
||||
"""Test SetString behavior."""
|
||||
config = git_config.GitConfig(str(rw_config_file))
|
||||
|
||||
# Set a value.
|
||||
assert config.GetString("foo.bar") is None
|
||||
config.SetString("foo.bar", "val")
|
||||
assert config.GetString("foo.bar") == "val"
|
||||
|
||||
# Make sure the value was actually written out.
|
||||
config2 = git_config.GitConfig(str(rw_config_file))
|
||||
assert config2.GetString("foo.bar") == "val"
|
||||
|
||||
# Update the value.
|
||||
config.SetString("foo.bar", "valll")
|
||||
assert config.GetString("foo.bar") == "valll"
|
||||
config3 = git_config.GitConfig(str(rw_config_file))
|
||||
assert config3.GetString("foo.bar") == "valll"
|
||||
|
||||
# Delete the value.
|
||||
config.SetString("foo.bar", None)
|
||||
assert config.GetString("foo.bar") is None
|
||||
config4 = git_config.GitConfig(str(rw_config_file))
|
||||
assert config4.GetString("foo.bar") is None
|
||||
|
||||
|
||||
def test_set_boolean(rw_config_file: Path) -> None:
|
||||
"""Test SetBoolean behavior."""
|
||||
config = git_config.GitConfig(str(rw_config_file))
|
||||
|
||||
# Set a true value.
|
||||
assert config.GetBoolean("foo.bar") is None
|
||||
for val in (True, 1):
|
||||
config.SetBoolean("foo.bar", val)
|
||||
assert config.GetBoolean("foo.bar") is True
|
||||
|
||||
# Make sure the value was actually written out.
|
||||
config2 = git_config.GitConfig(str(rw_config_file))
|
||||
assert config2.GetBoolean("foo.bar") is True
|
||||
assert config2.GetString("foo.bar") == "true"
|
||||
|
||||
# Set a false value.
|
||||
for val in (False, 0):
|
||||
config.SetBoolean("foo.bar", val)
|
||||
assert config.GetBoolean("foo.bar") is False
|
||||
|
||||
# Make sure the value was actually written out.
|
||||
config3 = git_config.GitConfig(str(rw_config_file))
|
||||
assert config3.GetBoolean("foo.bar") is False
|
||||
assert config3.GetString("foo.bar") == "false"
|
||||
|
||||
# Delete the value.
|
||||
config.SetBoolean("foo.bar", None)
|
||||
assert config.GetBoolean("foo.bar") is None
|
||||
config4 = git_config.GitConfig(str(rw_config_file))
|
||||
assert config4.GetBoolean("foo.bar") is None
|
||||
|
||||
|
||||
def test_set_int(rw_config_file: Path) -> None:
|
||||
"""Test SetInt behavior."""
|
||||
config = git_config.GitConfig(str(rw_config_file))
|
||||
|
||||
# Set a value.
|
||||
assert config.GetInt("foo.bar") is None
|
||||
config.SetInt("foo.bar", 10)
|
||||
assert config.GetInt("foo.bar") == 10
|
||||
|
||||
# Make sure the value was actually written out.
|
||||
config2 = git_config.GitConfig(str(rw_config_file))
|
||||
assert config2.GetInt("foo.bar") == 10
|
||||
assert config2.GetString("foo.bar") == "10"
|
||||
|
||||
# Update the value.
|
||||
config.SetInt("foo.bar", 20)
|
||||
assert config.GetInt("foo.bar") == 20
|
||||
config3 = git_config.GitConfig(str(rw_config_file))
|
||||
assert config3.GetInt("foo.bar") == 20
|
||||
|
||||
# Delete the value.
|
||||
config.SetInt("foo.bar", None)
|
||||
assert config.GetInt("foo.bar") is None
|
||||
config4 = git_config.GitConfig(str(rw_config_file))
|
||||
assert config4.GetInt("foo.bar") is None
|
||||
|
||||
|
||||
def test_get_sync_analysis_state_data(rw_config_file: Path) -> None:
|
||||
"""Test config entries with a sync state analysis data."""
|
||||
config = git_config.GitConfig(str(rw_config_file))
|
||||
superproject_logging_data: dict[str, Any] = {"test": False}
|
||||
|
||||
class Options:
|
||||
"""Container for testing."""
|
||||
|
||||
options = Options()
|
||||
options.verbose = "true"
|
||||
options.mp_update = "false"
|
||||
|
||||
TESTS = (
|
||||
("superproject.test", "false"),
|
||||
("options.verbose", "true"),
|
||||
("options.mpupdate", "false"),
|
||||
("main.version", "1"),
|
||||
)
|
||||
config.UpdateSyncAnalysisState(options, superproject_logging_data)
|
||||
sync_data = config.GetSyncAnalysisStateData()
|
||||
for key, value in TESTS:
|
||||
assert sync_data[f"{git_config.SYNC_STATE_PREFIX}{key}"] == value
|
||||
assert sync_data[f"{git_config.SYNC_STATE_PREFIX}main.synctime"]
|
||||
|
||||
|
||||
def test_remote_save_with_push_url_without_projectname(
|
||||
rw_config_file: Path,
|
||||
) -> None:
|
||||
"""Test saving a remote pushUrl when projectname is unset."""
|
||||
config = git_config.GitConfig(str(rw_config_file))
|
||||
remote = config.GetRemote("origin")
|
||||
remote.url = "https://example.com/repo"
|
||||
remote.pushUrl = "ssh://example.com"
|
||||
remote.projectname = None
|
||||
|
||||
remote.Save()
|
||||
|
||||
written_config = git_config.GitConfig(str(rw_config_file))
|
||||
assert (
|
||||
written_config.GetString("remote.origin.pushurl") == "ssh://example.com"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# Copyright (C) 2026 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Unittests for the git_refs.py module."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
import utils_for_test
|
||||
|
||||
import git_refs
|
||||
|
||||
|
||||
def _run(repo, *args):
|
||||
return subprocess.run(
|
||||
["git", "-C", repo, *args],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
encoding="utf-8",
|
||||
check=True,
|
||||
).stdout.strip()
|
||||
|
||||
|
||||
def _init_repo(tmp_path, reftable=False):
|
||||
repo = os.path.join(tmp_path, "repo")
|
||||
ref_format = "reftable" if reftable else "files"
|
||||
utils_for_test.init_git_tree(repo, ref_format=ref_format)
|
||||
|
||||
Path(os.path.join(repo, "a")).write_text("1")
|
||||
_run(repo, "add", "a")
|
||||
_run(repo, "commit", "-q", "-m", "init")
|
||||
return repo
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reftable", [False, True])
|
||||
def test_reads_refs(tmp_path, reftable):
|
||||
if reftable and not utils_for_test.supports_reftable():
|
||||
pytest.skip("reftable not supported")
|
||||
|
||||
repo = _init_repo(tmp_path, reftable=reftable)
|
||||
gitdir = os.path.join(repo, ".git")
|
||||
refs = git_refs.GitRefs(gitdir)
|
||||
|
||||
branch = _run(repo, "symbolic-ref", "--short", "HEAD")
|
||||
head = _run(repo, "rev-parse", "HEAD")
|
||||
assert refs.symref("HEAD") == f"refs/heads/{branch}"
|
||||
assert refs.get("HEAD") == head
|
||||
assert refs.get(f"refs/heads/{branch}") == head
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reftable", [False, True])
|
||||
def test_updates_when_refs_change(tmp_path, reftable):
|
||||
if reftable and not utils_for_test.supports_reftable():
|
||||
pytest.skip("reftable not supported")
|
||||
|
||||
repo = _init_repo(tmp_path, reftable=reftable)
|
||||
gitdir = os.path.join(repo, ".git")
|
||||
refs = git_refs.GitRefs(gitdir)
|
||||
|
||||
head = _run(repo, "rev-parse", "HEAD")
|
||||
assert refs.get("refs/heads/topic") == ""
|
||||
_run(repo, "branch", "topic")
|
||||
assert refs.get("refs/heads/topic") == head
|
||||
_run(repo, "branch", "-D", "topic")
|
||||
assert refs.get("refs/heads/topic") == ""
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not utils_for_test.supports_refs_migrate(),
|
||||
reason="git refs migrate reftable support is required for this test",
|
||||
)
|
||||
def test_updates_when_storage_backend_toggles(tmp_path):
|
||||
repo = _init_repo(tmp_path, reftable=False)
|
||||
gitdir = os.path.join(repo, ".git")
|
||||
refs = git_refs.GitRefs(gitdir)
|
||||
|
||||
head = _run(repo, "rev-parse", "HEAD")
|
||||
assert refs.get("refs/heads/reftable-branch") == ""
|
||||
_run(repo, "refs", "migrate", "--ref-format=reftable")
|
||||
_run(repo, "branch", "reftable-branch")
|
||||
assert refs.get("refs/heads/reftable-branch") == head
|
||||
|
||||
assert refs.get("refs/heads/files-branch") == ""
|
||||
_run(repo, "refs", "migrate", "--ref-format=files")
|
||||
_run(repo, "branch", "files-branch")
|
||||
assert refs.get("refs/heads/files-branch") == head
|
||||
@@ -21,6 +21,7 @@ import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from test_manifest_xml import sort_attributes
|
||||
|
||||
import git_superproject
|
||||
@@ -145,6 +146,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
||||
)
|
||||
self.assertIsNone(manifest.superproject)
|
||||
|
||||
@pytest.mark.skip_cq("TODO(b/266734831): Find out why this takes 8m+ in CQ")
|
||||
def test_superproject_get_superproject_invalid_url(self):
|
||||
"""Test with an invalid url."""
|
||||
manifest = self.getXmlManifest(
|
||||
@@ -168,6 +170,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
||||
self.assertFalse(sync_result.success)
|
||||
self.assertTrue(sync_result.fatal)
|
||||
|
||||
@pytest.mark.skip_cq("TODO(b/266734831): Find out why this takes 8m+ in CQ")
|
||||
def test_superproject_get_superproject_invalid_branch(self):
|
||||
"""Test with an invalid branch."""
|
||||
manifest = self.getXmlManifest(
|
||||
@@ -187,7 +190,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
||||
),
|
||||
revision="refs/heads/main",
|
||||
)
|
||||
with mock.patch.object(self._superproject, "_branch", "junk"):
|
||||
with mock.patch.object(self._superproject, "revision", "junk"):
|
||||
sync_result = self._superproject.Sync(self.git_event_log)
|
||||
self.assertFalse(sync_result.success)
|
||||
self.assertTrue(sync_result.fatal)
|
||||
@@ -458,6 +461,62 @@ class SuperprojectTestCase(unittest.TestCase):
|
||||
"</manifest>",
|
||||
)
|
||||
|
||||
def test_Init_success(self):
|
||||
"""Test _Init succeeds and creates the work git dir."""
|
||||
self.assertFalse(os.path.exists(self._superproject._work_git))
|
||||
with mock.patch(
|
||||
"git_superproject.GitCommand", autospec=True
|
||||
) as mock_git_command:
|
||||
instance = mock_git_command.return_value
|
||||
instance.Wait.return_value = 0
|
||||
|
||||
self.assertTrue(self._superproject._Init())
|
||||
|
||||
mock_git_command.assert_called_once()
|
||||
args, kwargs = mock_git_command.call_args
|
||||
self.assertEqual(args[1][:2], ["init", "--bare"])
|
||||
|
||||
tmp_git_name = args[1][2]
|
||||
self.assertTrue(
|
||||
tmp_git_name.startswith(".tmp-superproject-initgitdir-")
|
||||
)
|
||||
self.assertTrue(os.path.exists(self._superproject._work_git))
|
||||
|
||||
tmp_git_path = os.path.join(
|
||||
self._superproject._superproject_path, tmp_git_name
|
||||
)
|
||||
self.assertFalse(os.path.exists(tmp_git_path))
|
||||
|
||||
def test_Init_already_exists(self):
|
||||
"""Test _Init returns early if the work git dir already exists."""
|
||||
os.mkdir(self._superproject._superproject_path)
|
||||
os.mkdir(self._superproject._work_git)
|
||||
with mock.patch(
|
||||
"git_superproject.GitCommand", autospec=True
|
||||
) as mock_git_command:
|
||||
self.assertTrue(self._superproject._Init())
|
||||
mock_git_command.assert_not_called()
|
||||
|
||||
def test_Init_failure_cleans_up(self):
|
||||
"""Test _Init cleans up the temporary directory if 'git init' fails."""
|
||||
with mock.patch(
|
||||
"git_superproject.GitCommand", autospec=True
|
||||
) as mock_git_command:
|
||||
instance = mock_git_command.return_value
|
||||
instance.Wait.return_value = 1
|
||||
instance.stderr = "mock git init failure"
|
||||
|
||||
self.assertFalse(self._superproject._Init())
|
||||
mock_git_command.assert_called_once()
|
||||
args, kwargs = mock_git_command.call_args
|
||||
tmp_git_name = args[1][2]
|
||||
|
||||
tmp_git_path = os.path.join(
|
||||
self._superproject._superproject_path, tmp_git_name
|
||||
)
|
||||
self.assertFalse(os.path.exists(tmp_git_path))
|
||||
self.assertFalse(os.path.exists(self._superproject._work_git))
|
||||
|
||||
def test_Fetch(self):
|
||||
manifest = self.getXmlManifest(
|
||||
"""
|
||||
|
||||
+417
-339
@@ -14,19 +14,28 @@
|
||||
|
||||
"""Unittests for the git_trace2_event_log.py module."""
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
from typing import Any, Dict, List, Optional
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
import git_trace2_event_log
|
||||
import platform_utils
|
||||
|
||||
|
||||
def serverLoggingThread(socket_path, server_ready, received_traces):
|
||||
def server_logging_thread(
|
||||
socket_path: str,
|
||||
server_ready: threading.Condition,
|
||||
received_traces: List[str],
|
||||
) -> None:
|
||||
"""Helper function to receive logs over a Unix domain socket.
|
||||
|
||||
Appends received messages on the provided socket and appends to
|
||||
@@ -55,356 +64,425 @@ def serverLoggingThread(socket_path, server_ready, received_traces):
|
||||
received_traces.extend(data.decode("utf-8").splitlines())
|
||||
|
||||
|
||||
class EventLogTestCase(unittest.TestCase):
|
||||
"""TestCase for the EventLog module."""
|
||||
PARENT_SID_KEY = "GIT_TRACE2_PARENT_SID"
|
||||
PARENT_SID_VALUE = "parent_sid"
|
||||
SELF_SID_REGEX = r"repo-\d+T\d+Z-.*"
|
||||
FULL_SID_REGEX = rf"^{PARENT_SID_VALUE}/{SELF_SID_REGEX}"
|
||||
|
||||
PARENT_SID_KEY = "GIT_TRACE2_PARENT_SID"
|
||||
PARENT_SID_VALUE = "parent_sid"
|
||||
SELF_SID_REGEX = r"repo-\d+T\d+Z-.*"
|
||||
FULL_SID_REGEX = rf"^{PARENT_SID_VALUE}/{SELF_SID_REGEX}"
|
||||
|
||||
def setUp(self):
|
||||
"""Load the event_log module every time."""
|
||||
self._event_log_module = None
|
||||
# By default we initialize with the expected case where
|
||||
# repo launches us (so GIT_TRACE2_PARENT_SID is set).
|
||||
env = {
|
||||
self.PARENT_SID_KEY: self.PARENT_SID_VALUE,
|
||||
}
|
||||
self._event_log_module = git_trace2_event_log.EventLog(env=env)
|
||||
self._log_data = None
|
||||
@pytest.fixture
|
||||
def event_log() -> git_trace2_event_log.EventLog:
|
||||
"""Fixture for the EventLog module."""
|
||||
# By default we initialize with the expected case where
|
||||
# repo launches us (so GIT_TRACE2_PARENT_SID is set).
|
||||
env = {PARENT_SID_KEY: PARENT_SID_VALUE}
|
||||
return git_trace2_event_log.EventLog(env=env)
|
||||
|
||||
def verifyCommonKeys(
|
||||
self, log_entry, expected_event_name=None, full_sid=True
|
||||
|
||||
def verify_common_keys(
|
||||
log_entry: Dict[str, Any],
|
||||
expected_event_name: Optional[str] = None,
|
||||
full_sid: bool = True,
|
||||
) -> None:
|
||||
"""Helper function to verify common event log keys."""
|
||||
assert "event" in log_entry
|
||||
assert "sid" in log_entry
|
||||
assert "thread" in log_entry
|
||||
assert "time" in log_entry
|
||||
|
||||
# Do basic data format validation.
|
||||
if expected_event_name:
|
||||
assert expected_event_name == log_entry["event"]
|
||||
if full_sid:
|
||||
assert re.match(FULL_SID_REGEX, log_entry["sid"])
|
||||
else:
|
||||
assert re.match(SELF_SID_REGEX, log_entry["sid"])
|
||||
assert re.match(r"^\d+-\d+-\d+T\d+:\d+:\d+\.\d+\+00:00$", log_entry["time"])
|
||||
|
||||
|
||||
def read_log(log_path: str) -> List[Dict[str, Any]]:
|
||||
"""Helper function to read log data into a list."""
|
||||
log_data = []
|
||||
with open(log_path, mode="rb") as f:
|
||||
for line in f:
|
||||
log_data.append(json.loads(line))
|
||||
return log_data
|
||||
|
||||
|
||||
def remove_prefix(s: str, prefix: str) -> str:
|
||||
"""Return a copy string after removing |prefix| from |s|, if present or
|
||||
the original string."""
|
||||
if s.startswith(prefix):
|
||||
return s[len(prefix) :]
|
||||
else:
|
||||
return s
|
||||
|
||||
|
||||
def test_initial_state_with_parent_sid(
|
||||
event_log: git_trace2_event_log.EventLog,
|
||||
) -> None:
|
||||
"""Test initial state when 'GIT_TRACE2_PARENT_SID' is set by parent."""
|
||||
assert re.match(FULL_SID_REGEX, event_log.full_sid)
|
||||
|
||||
|
||||
def test_initial_state_no_parent_sid() -> None:
|
||||
"""Test initial state when 'GIT_TRACE2_PARENT_SID' is not set."""
|
||||
# Setup an empty environment dict (no parent sid).
|
||||
event_log = git_trace2_event_log.EventLog(env={})
|
||||
assert re.match(SELF_SID_REGEX, event_log.full_sid)
|
||||
|
||||
|
||||
def test_version_event(event_log: git_trace2_event_log.EventLog) -> None:
|
||||
"""Test 'version' event data is valid.
|
||||
|
||||
Verify that the 'version' event is written even when no other
|
||||
events are added.
|
||||
|
||||
Expected event log:
|
||||
<version event>
|
||||
"""
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
log_path = event_log.Write(path=tempdir)
|
||||
log_data = read_log(log_path)
|
||||
|
||||
# A log with no added events should only have the version entry.
|
||||
assert len(log_data) == 1
|
||||
version_event = log_data[0]
|
||||
verify_common_keys(version_event, expected_event_name="version")
|
||||
# Check for 'version' event specific fields.
|
||||
assert "evt" in version_event
|
||||
assert "exe" in version_event
|
||||
# Verify "evt" version field is a string.
|
||||
assert isinstance(version_event["evt"], str)
|
||||
|
||||
|
||||
def test_start_event(event_log: git_trace2_event_log.EventLog) -> None:
|
||||
"""Test and validate 'start' event data is valid.
|
||||
|
||||
Expected event log:
|
||||
<version event>
|
||||
<start event>
|
||||
"""
|
||||
event_log.StartEvent([])
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
log_path = event_log.Write(path=tempdir)
|
||||
log_data = read_log(log_path)
|
||||
|
||||
assert len(log_data) == 2
|
||||
start_event = log_data[1]
|
||||
verify_common_keys(log_data[0], expected_event_name="version")
|
||||
verify_common_keys(start_event, expected_event_name="start")
|
||||
# Check for 'start' event specific fields.
|
||||
assert "argv" in start_event
|
||||
assert isinstance(start_event["argv"], list)
|
||||
|
||||
|
||||
def test_exit_event_result_none(
|
||||
event_log: git_trace2_event_log.EventLog,
|
||||
) -> None:
|
||||
"""Test 'exit' event data is valid when result is None.
|
||||
|
||||
We expect None result to be converted to 0 in the exit event data.
|
||||
|
||||
Expected event log:
|
||||
<version event>
|
||||
<exit event>
|
||||
"""
|
||||
event_log.ExitEvent(None)
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
log_path = event_log.Write(path=tempdir)
|
||||
log_data = read_log(log_path)
|
||||
|
||||
assert len(log_data) == 2
|
||||
exit_event = log_data[1]
|
||||
verify_common_keys(log_data[0], expected_event_name="version")
|
||||
verify_common_keys(exit_event, expected_event_name="exit")
|
||||
# Check for 'exit' event specific fields.
|
||||
assert "code" in exit_event
|
||||
# 'None' result should convert to 0 (successful) return code.
|
||||
assert exit_event["code"] == 0
|
||||
|
||||
|
||||
def test_exit_event_result_integer(
|
||||
event_log: git_trace2_event_log.EventLog,
|
||||
) -> None:
|
||||
"""Test 'exit' event data is valid when result is an integer.
|
||||
|
||||
Expected event log:
|
||||
<version event>
|
||||
<exit event>
|
||||
"""
|
||||
event_log.ExitEvent(2)
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
log_path = event_log.Write(path=tempdir)
|
||||
log_data = read_log(log_path)
|
||||
|
||||
assert len(log_data) == 2
|
||||
exit_event = log_data[1]
|
||||
verify_common_keys(log_data[0], expected_event_name="version")
|
||||
verify_common_keys(exit_event, expected_event_name="exit")
|
||||
# Check for 'exit' event specific fields.
|
||||
assert "code" in exit_event
|
||||
assert exit_event["code"] == 2
|
||||
|
||||
|
||||
def test_command_event(event_log: git_trace2_event_log.EventLog) -> None:
|
||||
"""Test and validate 'command' event data is valid.
|
||||
|
||||
Expected event log:
|
||||
<version event>
|
||||
<command event>
|
||||
"""
|
||||
event_log.CommandEvent(name="repo", subcommands=["init", "this"])
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
log_path = event_log.Write(path=tempdir)
|
||||
log_data = read_log(log_path)
|
||||
|
||||
assert len(log_data) == 2
|
||||
command_event = log_data[1]
|
||||
verify_common_keys(log_data[0], expected_event_name="version")
|
||||
verify_common_keys(command_event, expected_event_name="cmd_name")
|
||||
# Check for 'command' event specific fields.
|
||||
assert "name" in command_event
|
||||
assert command_event["name"] == "repo-init-this"
|
||||
|
||||
|
||||
def test_def_params_event_repo_config(
|
||||
event_log: git_trace2_event_log.EventLog,
|
||||
) -> None:
|
||||
"""Test 'def_params' event data outputs only repo config keys.
|
||||
|
||||
Expected event log:
|
||||
<version event>
|
||||
<def_param event>
|
||||
<def_param event>
|
||||
"""
|
||||
config = {
|
||||
"git.foo": "bar",
|
||||
"repo.partialclone": "true",
|
||||
"repo.partialclonefilter": "blob:none",
|
||||
}
|
||||
event_log.DefParamRepoEvents(config)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
log_path = event_log.Write(path=tempdir)
|
||||
log_data = read_log(log_path)
|
||||
|
||||
assert len(log_data) == 3
|
||||
def_param_events = log_data[1:]
|
||||
verify_common_keys(log_data[0], expected_event_name="version")
|
||||
|
||||
for event in def_param_events:
|
||||
verify_common_keys(event, expected_event_name="def_param")
|
||||
# Check for 'def_param' event specific fields.
|
||||
assert "param" in event
|
||||
assert "value" in event
|
||||
assert event["param"].startswith("repo.")
|
||||
|
||||
|
||||
def test_def_params_event_no_repo_config(
|
||||
event_log: git_trace2_event_log.EventLog,
|
||||
) -> None:
|
||||
"""Test 'def_params' event data won't output non-repo config keys.
|
||||
|
||||
Expected event log:
|
||||
<version event>
|
||||
"""
|
||||
config = {
|
||||
"git.foo": "bar",
|
||||
"git.core.foo2": "baz",
|
||||
}
|
||||
event_log.DefParamRepoEvents(config)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
log_path = event_log.Write(path=tempdir)
|
||||
log_data = read_log(log_path)
|
||||
|
||||
assert len(log_data) == 1
|
||||
verify_common_keys(log_data[0], expected_event_name="version")
|
||||
|
||||
|
||||
def test_data_event_config(event_log: git_trace2_event_log.EventLog) -> None:
|
||||
"""Test 'data' event data outputs all config keys.
|
||||
|
||||
Expected event log:
|
||||
<version event>
|
||||
<data event>
|
||||
<data event>
|
||||
"""
|
||||
config = {
|
||||
"git.foo": "bar",
|
||||
"repo.partialclone": "false",
|
||||
"repo.syncstate.superproject.hassuperprojecttag": "true",
|
||||
"repo.syncstate.superproject.sys.argv": ["--", "sync", "protobuf"],
|
||||
}
|
||||
prefix_value = "prefix"
|
||||
event_log.LogDataConfigEvents(config, prefix_value)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
log_path = event_log.Write(path=tempdir)
|
||||
log_data = read_log(log_path)
|
||||
|
||||
assert len(log_data) == 5
|
||||
data_events = log_data[1:]
|
||||
verify_common_keys(log_data[0], expected_event_name="version")
|
||||
|
||||
for event in data_events:
|
||||
verify_common_keys(event)
|
||||
# Check for 'data' event specific fields.
|
||||
assert "key" in event
|
||||
assert "value" in event
|
||||
key = event["key"]
|
||||
key = remove_prefix(key, f"{prefix_value}/")
|
||||
value = event["value"]
|
||||
assert event_log.GetDataEventName(value) == event["event"]
|
||||
assert key in config
|
||||
assert value == config[key]
|
||||
|
||||
|
||||
def test_error_event(event_log: git_trace2_event_log.EventLog) -> None:
|
||||
"""Test and validate 'error' event data is valid.
|
||||
|
||||
Expected event log:
|
||||
<version event>
|
||||
<error event>
|
||||
"""
|
||||
msg = "invalid option: --cahced"
|
||||
fmt = "invalid option: %s"
|
||||
event_log.ErrorEvent(msg, fmt)
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
log_path = event_log.Write(path=tempdir)
|
||||
log_data = read_log(log_path)
|
||||
|
||||
assert len(log_data) == 2
|
||||
error_event = log_data[1]
|
||||
verify_common_keys(log_data[0], expected_event_name="version")
|
||||
verify_common_keys(error_event, expected_event_name="error")
|
||||
# Check for 'error' event specific fields.
|
||||
assert "msg" in error_event
|
||||
assert "fmt" in error_event
|
||||
assert error_event["msg"] == f"RepoErrorEvent:{msg}"
|
||||
assert error_event["fmt"] == f"RepoErrorEvent:{fmt}"
|
||||
|
||||
|
||||
def test_write_with_filename(event_log: git_trace2_event_log.EventLog) -> None:
|
||||
"""Test Write() with a path to a file exits with None."""
|
||||
assert event_log.Write(path="path/to/file") is None
|
||||
|
||||
|
||||
def test_write_with_git_config(
|
||||
tmp_path,
|
||||
event_log: git_trace2_event_log.EventLog,
|
||||
) -> None:
|
||||
"""Test Write() uses the git config path when 'git config' call succeeds."""
|
||||
with mock.patch.object(
|
||||
event_log,
|
||||
"_GetEventTargetPath",
|
||||
return_value=str(tmp_path),
|
||||
):
|
||||
"""Helper function to verify common event log keys."""
|
||||
self.assertIn("event", log_entry)
|
||||
self.assertIn("sid", log_entry)
|
||||
self.assertIn("thread", log_entry)
|
||||
self.assertIn("time", log_entry)
|
||||
assert os.path.dirname(event_log.Write()) == str(tmp_path)
|
||||
|
||||
# Do basic data format validation.
|
||||
if expected_event_name:
|
||||
self.assertEqual(expected_event_name, log_entry["event"])
|
||||
if full_sid:
|
||||
self.assertRegex(log_entry["sid"], self.FULL_SID_REGEX)
|
||||
else:
|
||||
self.assertRegex(log_entry["sid"], self.SELF_SID_REGEX)
|
||||
self.assertRegex(
|
||||
log_entry["time"], r"^\d+-\d+-\d+T\d+:\d+:\d+\.\d+\+00:00$"
|
||||
|
||||
def test_write_no_git_config(event_log: git_trace2_event_log.EventLog) -> None:
|
||||
"""Test Write() with no git config variable present exits with None."""
|
||||
with mock.patch.object(event_log, "_GetEventTargetPath", return_value=None):
|
||||
assert event_log.Write() is None
|
||||
|
||||
|
||||
def test_write_non_string(event_log: git_trace2_event_log.EventLog) -> None:
|
||||
"""Test Write() with non-string type for |path| throws TypeError."""
|
||||
with pytest.raises(TypeError):
|
||||
event_log.Write(path=1234)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not hasattr(socket, "AF_UNIX"), reason="Requires AF_UNIX sockets"
|
||||
)
|
||||
def test_write_socket(event_log: git_trace2_event_log.EventLog) -> None:
|
||||
"""Test Write() with Unix domain socket and validate received traces."""
|
||||
received_traces: List[str] = []
|
||||
with tempfile.TemporaryDirectory(prefix="test_server_sockets") as tempdir:
|
||||
socket_path = os.path.join(tempdir, "server.sock")
|
||||
server_ready = threading.Condition()
|
||||
# Start "server" listening on Unix domain socket at socket_path.
|
||||
server_thread = threading.Thread(
|
||||
target=server_logging_thread,
|
||||
args=(socket_path, server_ready, received_traces),
|
||||
)
|
||||
try:
|
||||
server_thread.start()
|
||||
|
||||
def readLog(self, log_path):
|
||||
"""Helper function to read log data into a list."""
|
||||
log_data = []
|
||||
with open(log_path, mode="rb") as f:
|
||||
for line in f:
|
||||
log_data.append(json.loads(line))
|
||||
return log_data
|
||||
with server_ready:
|
||||
server_ready.wait(timeout=120)
|
||||
|
||||
def remove_prefix(self, s, prefix):
|
||||
"""Return a copy string after removing |prefix| from |s|, if present or
|
||||
the original string."""
|
||||
if s.startswith(prefix):
|
||||
return s[len(prefix) :]
|
||||
else:
|
||||
return s
|
||||
event_log.StartEvent([])
|
||||
path = event_log.Write(path=f"af_unix:{socket_path}")
|
||||
finally:
|
||||
server_thread.join(timeout=5)
|
||||
|
||||
def test_initial_state_with_parent_sid(self):
|
||||
"""Test initial state when 'GIT_TRACE2_PARENT_SID' is set by parent."""
|
||||
self.assertRegex(self._event_log_module.full_sid, self.FULL_SID_REGEX)
|
||||
assert path == f"af_unix:stream:{socket_path}"
|
||||
assert len(received_traces) == 2
|
||||
version_event = json.loads(received_traces[0])
|
||||
start_event = json.loads(received_traces[1])
|
||||
verify_common_keys(version_event, expected_event_name="version")
|
||||
verify_common_keys(start_event, expected_event_name="start")
|
||||
# Check for 'start' event specific fields.
|
||||
assert "argv" in start_event
|
||||
assert isinstance(start_event["argv"], list)
|
||||
|
||||
def test_initial_state_no_parent_sid(self):
|
||||
"""Test initial state when 'GIT_TRACE2_PARENT_SID' is not set."""
|
||||
# Setup an empty environment dict (no parent sid).
|
||||
self._event_log_module = git_trace2_event_log.EventLog(env={})
|
||||
self.assertRegex(self._event_log_module.full_sid, self.SELF_SID_REGEX)
|
||||
|
||||
def test_version_event(self):
|
||||
"""Test 'version' event data is valid.
|
||||
class TestEventLogVerbose:
|
||||
"""TestCase for the EventLog module verbose logging."""
|
||||
|
||||
Verify that the 'version' event is written even when no other
|
||||
events are addded.
|
||||
def test_write_socket_error_no_verbose(self) -> None:
|
||||
"""Test Write() suppression of socket errors when not verbose."""
|
||||
event_log = git_trace2_event_log.EventLog(env={})
|
||||
event_log.verbose = False
|
||||
with contextlib.redirect_stderr(
|
||||
io.StringIO()
|
||||
) as mock_stderr, mock.patch("socket.socket", side_effect=OSError):
|
||||
event_log.Write(path="af_unix:stream:/tmp/test_sock")
|
||||
assert mock_stderr.getvalue() == ""
|
||||
|
||||
Expected event log:
|
||||
<version event>
|
||||
"""
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
log_path = self._event_log_module.Write(path=tempdir)
|
||||
self._log_data = self.readLog(log_path)
|
||||
|
||||
# A log with no added events should only have the version entry.
|
||||
self.assertEqual(len(self._log_data), 1)
|
||||
version_event = self._log_data[0]
|
||||
self.verifyCommonKeys(version_event, expected_event_name="version")
|
||||
# Check for 'version' event specific fields.
|
||||
self.assertIn("evt", version_event)
|
||||
self.assertIn("exe", version_event)
|
||||
# Verify "evt" version field is a string.
|
||||
self.assertIsInstance(version_event["evt"], str)
|
||||
|
||||
def test_start_event(self):
|
||||
"""Test and validate 'start' event data is valid.
|
||||
|
||||
Expected event log:
|
||||
<version event>
|
||||
<start event>
|
||||
"""
|
||||
self._event_log_module.StartEvent()
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
log_path = self._event_log_module.Write(path=tempdir)
|
||||
self._log_data = self.readLog(log_path)
|
||||
|
||||
self.assertEqual(len(self._log_data), 2)
|
||||
start_event = self._log_data[1]
|
||||
self.verifyCommonKeys(self._log_data[0], expected_event_name="version")
|
||||
self.verifyCommonKeys(start_event, expected_event_name="start")
|
||||
# Check for 'start' event specific fields.
|
||||
self.assertIn("argv", start_event)
|
||||
self.assertTrue(isinstance(start_event["argv"], list))
|
||||
|
||||
def test_exit_event_result_none(self):
|
||||
"""Test 'exit' event data is valid when result is None.
|
||||
|
||||
We expect None result to be converted to 0 in the exit event data.
|
||||
|
||||
Expected event log:
|
||||
<version event>
|
||||
<exit event>
|
||||
"""
|
||||
self._event_log_module.ExitEvent(None)
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
log_path = self._event_log_module.Write(path=tempdir)
|
||||
self._log_data = self.readLog(log_path)
|
||||
|
||||
self.assertEqual(len(self._log_data), 2)
|
||||
exit_event = self._log_data[1]
|
||||
self.verifyCommonKeys(self._log_data[0], expected_event_name="version")
|
||||
self.verifyCommonKeys(exit_event, expected_event_name="exit")
|
||||
# Check for 'exit' event specific fields.
|
||||
self.assertIn("code", exit_event)
|
||||
# 'None' result should convert to 0 (successful) return code.
|
||||
self.assertEqual(exit_event["code"], 0)
|
||||
|
||||
def test_exit_event_result_integer(self):
|
||||
"""Test 'exit' event data is valid when result is an integer.
|
||||
|
||||
Expected event log:
|
||||
<version event>
|
||||
<exit event>
|
||||
"""
|
||||
self._event_log_module.ExitEvent(2)
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
log_path = self._event_log_module.Write(path=tempdir)
|
||||
self._log_data = self.readLog(log_path)
|
||||
|
||||
self.assertEqual(len(self._log_data), 2)
|
||||
exit_event = self._log_data[1]
|
||||
self.verifyCommonKeys(self._log_data[0], expected_event_name="version")
|
||||
self.verifyCommonKeys(exit_event, expected_event_name="exit")
|
||||
# Check for 'exit' event specific fields.
|
||||
self.assertIn("code", exit_event)
|
||||
self.assertEqual(exit_event["code"], 2)
|
||||
|
||||
def test_command_event(self):
|
||||
"""Test and validate 'command' event data is valid.
|
||||
|
||||
Expected event log:
|
||||
<version event>
|
||||
<command event>
|
||||
"""
|
||||
name = "repo"
|
||||
subcommands = ["init" "this"]
|
||||
self._event_log_module.CommandEvent(
|
||||
name="repo", subcommands=subcommands
|
||||
)
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
log_path = self._event_log_module.Write(path=tempdir)
|
||||
self._log_data = self.readLog(log_path)
|
||||
|
||||
self.assertEqual(len(self._log_data), 2)
|
||||
command_event = self._log_data[1]
|
||||
self.verifyCommonKeys(self._log_data[0], expected_event_name="version")
|
||||
self.verifyCommonKeys(command_event, expected_event_name="command")
|
||||
# Check for 'command' event specific fields.
|
||||
self.assertIn("name", command_event)
|
||||
self.assertIn("subcommands", command_event)
|
||||
self.assertEqual(command_event["name"], name)
|
||||
self.assertEqual(command_event["subcommands"], subcommands)
|
||||
|
||||
def test_def_params_event_repo_config(self):
|
||||
"""Test 'def_params' event data outputs only repo config keys.
|
||||
|
||||
Expected event log:
|
||||
<version event>
|
||||
<def_param event>
|
||||
<def_param event>
|
||||
"""
|
||||
config = {
|
||||
"git.foo": "bar",
|
||||
"repo.partialclone": "true",
|
||||
"repo.partialclonefilter": "blob:none",
|
||||
}
|
||||
self._event_log_module.DefParamRepoEvents(config)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
log_path = self._event_log_module.Write(path=tempdir)
|
||||
self._log_data = self.readLog(log_path)
|
||||
|
||||
self.assertEqual(len(self._log_data), 3)
|
||||
def_param_events = self._log_data[1:]
|
||||
self.verifyCommonKeys(self._log_data[0], expected_event_name="version")
|
||||
|
||||
for event in def_param_events:
|
||||
self.verifyCommonKeys(event, expected_event_name="def_param")
|
||||
# Check for 'def_param' event specific fields.
|
||||
self.assertIn("param", event)
|
||||
self.assertIn("value", event)
|
||||
self.assertTrue(event["param"].startswith("repo."))
|
||||
|
||||
def test_def_params_event_no_repo_config(self):
|
||||
"""Test 'def_params' event data won't output non-repo config keys.
|
||||
|
||||
Expected event log:
|
||||
<version event>
|
||||
"""
|
||||
config = {
|
||||
"git.foo": "bar",
|
||||
"git.core.foo2": "baz",
|
||||
}
|
||||
self._event_log_module.DefParamRepoEvents(config)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
log_path = self._event_log_module.Write(path=tempdir)
|
||||
self._log_data = self.readLog(log_path)
|
||||
|
||||
self.assertEqual(len(self._log_data), 1)
|
||||
self.verifyCommonKeys(self._log_data[0], expected_event_name="version")
|
||||
|
||||
def test_data_event_config(self):
|
||||
"""Test 'data' event data outputs all config keys.
|
||||
|
||||
Expected event log:
|
||||
<version event>
|
||||
<data event>
|
||||
<data event>
|
||||
"""
|
||||
config = {
|
||||
"git.foo": "bar",
|
||||
"repo.partialclone": "false",
|
||||
"repo.syncstate.superproject.hassuperprojecttag": "true",
|
||||
"repo.syncstate.superproject.sys.argv": ["--", "sync", "protobuf"],
|
||||
}
|
||||
prefix_value = "prefix"
|
||||
self._event_log_module.LogDataConfigEvents(config, prefix_value)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
log_path = self._event_log_module.Write(path=tempdir)
|
||||
self._log_data = self.readLog(log_path)
|
||||
|
||||
self.assertEqual(len(self._log_data), 5)
|
||||
data_events = self._log_data[1:]
|
||||
self.verifyCommonKeys(self._log_data[0], expected_event_name="version")
|
||||
|
||||
for event in data_events:
|
||||
self.verifyCommonKeys(event)
|
||||
# Check for 'data' event specific fields.
|
||||
self.assertIn("key", event)
|
||||
self.assertIn("value", event)
|
||||
key = event["key"]
|
||||
key = self.remove_prefix(key, f"{prefix_value}/")
|
||||
value = event["value"]
|
||||
self.assertEqual(
|
||||
self._event_log_module.GetDataEventName(value), event["event"]
|
||||
)
|
||||
self.assertTrue(key in config and value == config[key])
|
||||
|
||||
def test_error_event(self):
|
||||
"""Test and validate 'error' event data is valid.
|
||||
|
||||
Expected event log:
|
||||
<version event>
|
||||
<error event>
|
||||
"""
|
||||
msg = "invalid option: --cahced"
|
||||
fmt = "invalid option: %s"
|
||||
self._event_log_module.ErrorEvent(msg, fmt)
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
log_path = self._event_log_module.Write(path=tempdir)
|
||||
self._log_data = self.readLog(log_path)
|
||||
|
||||
self.assertEqual(len(self._log_data), 2)
|
||||
error_event = self._log_data[1]
|
||||
self.verifyCommonKeys(self._log_data[0], expected_event_name="version")
|
||||
self.verifyCommonKeys(error_event, expected_event_name="error")
|
||||
# Check for 'error' event specific fields.
|
||||
self.assertIn("msg", error_event)
|
||||
self.assertIn("fmt", error_event)
|
||||
self.assertEqual(error_event["msg"], f"RepoErrorEvent:{msg}")
|
||||
self.assertEqual(error_event["fmt"], f"RepoErrorEvent:{fmt}")
|
||||
|
||||
def test_write_with_filename(self):
|
||||
"""Test Write() with a path to a file exits with None."""
|
||||
self.assertIsNone(self._event_log_module.Write(path="path/to/file"))
|
||||
|
||||
def test_write_with_git_config(self):
|
||||
"""Test Write() uses the git config path when 'git config' call
|
||||
succeeds."""
|
||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||
with mock.patch.object(
|
||||
self._event_log_module,
|
||||
"_GetEventTargetPath",
|
||||
return_value=tempdir,
|
||||
):
|
||||
self.assertEqual(
|
||||
os.path.dirname(self._event_log_module.Write()), tempdir
|
||||
)
|
||||
|
||||
def test_write_no_git_config(self):
|
||||
"""Test Write() with no git config variable present exits with None."""
|
||||
with mock.patch.object(
|
||||
self._event_log_module, "_GetEventTargetPath", return_value=None
|
||||
def test_write_socket_error_verbose(self) -> None:
|
||||
"""Test Write() printing of socket errors when verbose."""
|
||||
event_log = git_trace2_event_log.EventLog(env={})
|
||||
event_log.verbose = True
|
||||
with contextlib.redirect_stderr(
|
||||
io.StringIO()
|
||||
) as mock_stderr, mock.patch(
|
||||
"socket.socket", side_effect=OSError("Mock error")
|
||||
):
|
||||
self.assertIsNone(self._event_log_module.Write())
|
||||
event_log.Write(path="af_unix:stream:/tmp/test_sock")
|
||||
assert (
|
||||
"git trace2 logging failed: Mock error"
|
||||
in mock_stderr.getvalue()
|
||||
)
|
||||
|
||||
def test_write_non_string(self):
|
||||
"""Test Write() with non-string type for |path| throws TypeError."""
|
||||
with self.assertRaises(TypeError):
|
||||
self._event_log_module.Write(path=1234)
|
||||
def test_write_file_error_no_verbose(self) -> None:
|
||||
"""Test Write() suppression of file errors when not verbose."""
|
||||
event_log = git_trace2_event_log.EventLog(env={})
|
||||
event_log.verbose = False
|
||||
with contextlib.redirect_stderr(
|
||||
io.StringIO()
|
||||
) as mock_stderr, mock.patch(
|
||||
"tempfile.NamedTemporaryFile", side_effect=FileExistsError
|
||||
):
|
||||
event_log.Write(path="/tmp")
|
||||
assert mock_stderr.getvalue() == ""
|
||||
|
||||
def test_write_socket(self):
|
||||
"""Test Write() with Unix domain socket for |path| and validate received
|
||||
traces."""
|
||||
received_traces = []
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="test_server_sockets"
|
||||
) as tempdir:
|
||||
socket_path = os.path.join(tempdir, "server.sock")
|
||||
server_ready = threading.Condition()
|
||||
# Start "server" listening on Unix domain socket at socket_path.
|
||||
try:
|
||||
server_thread = threading.Thread(
|
||||
target=serverLoggingThread,
|
||||
args=(socket_path, server_ready, received_traces),
|
||||
)
|
||||
server_thread.start()
|
||||
|
||||
with server_ready:
|
||||
server_ready.wait(timeout=120)
|
||||
|
||||
self._event_log_module.StartEvent()
|
||||
path = self._event_log_module.Write(
|
||||
path=f"af_unix:{socket_path}"
|
||||
)
|
||||
finally:
|
||||
server_thread.join(timeout=5)
|
||||
|
||||
self.assertEqual(path, f"af_unix:stream:{socket_path}")
|
||||
self.assertEqual(len(received_traces), 2)
|
||||
version_event = json.loads(received_traces[0])
|
||||
start_event = json.loads(received_traces[1])
|
||||
self.verifyCommonKeys(version_event, expected_event_name="version")
|
||||
self.verifyCommonKeys(start_event, expected_event_name="start")
|
||||
# Check for 'start' event specific fields.
|
||||
self.assertIn("argv", start_event)
|
||||
self.assertIsInstance(start_event["argv"], list)
|
||||
def test_write_file_error_verbose(self) -> None:
|
||||
"""Test Write() printing of file errors when verbose."""
|
||||
event_log = git_trace2_event_log.EventLog(env={})
|
||||
event_log.verbose = True
|
||||
with contextlib.redirect_stderr(
|
||||
io.StringIO()
|
||||
) as mock_stderr, mock.patch(
|
||||
"tempfile.NamedTemporaryFile",
|
||||
side_effect=FileExistsError("Mock error"),
|
||||
):
|
||||
event_log.Write(path="/tmp")
|
||||
assert (
|
||||
"git trace2 logging failed: FileExistsError"
|
||||
in mock_stderr.getvalue()
|
||||
)
|
||||
|
||||
+84
-32
@@ -14,42 +14,94 @@
|
||||
|
||||
"""Unittests for the hooks.py module."""
|
||||
|
||||
import unittest
|
||||
from io import StringIO
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
import hooks
|
||||
|
||||
|
||||
class RepoHookShebang(unittest.TestCase):
|
||||
"""Check shebang parsing in RepoHook."""
|
||||
@pytest.mark.parametrize(
|
||||
"data",
|
||||
(
|
||||
"",
|
||||
"#\n# foo\n",
|
||||
"# Bad shebang in script\n#!/foo\n",
|
||||
),
|
||||
)
|
||||
def test_no_shebang(data: str) -> None:
|
||||
"""Lines w/out shebangs should be rejected."""
|
||||
assert hooks.RepoHook._ExtractInterpFromShebang(data) is None
|
||||
|
||||
def test_no_shebang(self):
|
||||
"""Lines w/out shebangs should be rejected."""
|
||||
DATA = ("", "#\n# foo\n", "# Bad shebang in script\n#!/foo\n")
|
||||
for data in DATA:
|
||||
self.assertIsNone(hooks.RepoHook._ExtractInterpFromShebang(data))
|
||||
|
||||
def test_direct_interp(self):
|
||||
"""Lines whose shebang points directly to the interpreter."""
|
||||
DATA = (
|
||||
("#!/foo", "/foo"),
|
||||
("#! /foo", "/foo"),
|
||||
("#!/bin/foo ", "/bin/foo"),
|
||||
("#! /usr/foo ", "/usr/foo"),
|
||||
("#! /usr/foo -args", "/usr/foo"),
|
||||
)
|
||||
for shebang, interp in DATA:
|
||||
self.assertEqual(
|
||||
hooks.RepoHook._ExtractInterpFromShebang(shebang), interp
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"shebang, interp",
|
||||
(
|
||||
("#!/foo", "/foo"),
|
||||
("#! /foo", "/foo"),
|
||||
("#!/bin/foo ", "/bin/foo"),
|
||||
("#! /usr/foo ", "/usr/foo"),
|
||||
("#! /usr/foo -args", "/usr/foo"),
|
||||
),
|
||||
)
|
||||
def test_direct_interp(shebang: str, interp: str) -> None:
|
||||
"""Lines whose shebang points directly to the interpreter."""
|
||||
assert hooks.RepoHook._ExtractInterpFromShebang(shebang) == interp
|
||||
|
||||
def test_env_interp(self):
|
||||
"""Lines whose shebang launches through `env`."""
|
||||
DATA = (
|
||||
("#!/usr/bin/env foo", "foo"),
|
||||
("#!/bin/env foo", "foo"),
|
||||
("#! /bin/env /bin/foo ", "/bin/foo"),
|
||||
)
|
||||
for shebang, interp in DATA:
|
||||
self.assertEqual(
|
||||
hooks.RepoHook._ExtractInterpFromShebang(shebang), interp
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"shebang, interp",
|
||||
(
|
||||
("#!/usr/bin/env foo", "foo"),
|
||||
("#!/bin/env foo", "foo"),
|
||||
("#! /bin/env /bin/foo ", "/bin/foo"),
|
||||
),
|
||||
)
|
||||
def test_env_interp(shebang: str, interp: str) -> None:
|
||||
"""Lines whose shebang launches through `env`."""
|
||||
assert hooks.RepoHook._ExtractInterpFromShebang(shebang) == interp
|
||||
|
||||
|
||||
def test_post_sync_argument_validation() -> None:
|
||||
"""Test that post-sync hook requires exact API arguments."""
|
||||
|
||||
class FakeProject:
|
||||
|
||||
def __init__(self):
|
||||
self.worktree = "/some/path"
|
||||
self.enabled_repo_hooks = ["post-sync"]
|
||||
|
||||
hook = hooks.RepoHook(
|
||||
hook_type="post-sync",
|
||||
hooks_project=FakeProject(),
|
||||
repo_topdir="/topdir",
|
||||
manifest_url="https://gerrit",
|
||||
allow_all_hooks=True,
|
||||
)
|
||||
|
||||
old_stderr = sys.stderr
|
||||
sys.stderr = StringIO()
|
||||
|
||||
try:
|
||||
# Call with missing arg `sync_duration_seconds`
|
||||
res = hook.Run(repo_topdir="/topdir")
|
||||
assert res is False
|
||||
assert "hook 'post-sync' called incorrectly" in sys.stderr.getvalue()
|
||||
|
||||
# Mock _CheckHook and _ExecuteHook to test success path
|
||||
hook._CheckHook = lambda: None
|
||||
|
||||
executed_kwargs = {}
|
||||
|
||||
def fake_execute(**kw):
|
||||
executed_kwargs.update(kw)
|
||||
|
||||
hook._ExecuteHook = fake_execute
|
||||
|
||||
res = hook.Run(repo_topdir="/topdir", sync_duration_seconds=12.345)
|
||||
assert res is True
|
||||
assert executed_kwargs.get("sync_duration_seconds") == 12.345
|
||||
|
||||
finally:
|
||||
sys.stderr = old_stderr
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# Copyright (C) 2026 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for the main repo script and subcommand routing."""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from main import _Repo
|
||||
|
||||
|
||||
@pytest.fixture(name="repo")
|
||||
def fixture_repo():
|
||||
repo = _Repo("repodir")
|
||||
# Overriding the command list here ensures that we are only testing
|
||||
# against a fixed set of commands, reducing fragility to new
|
||||
# subcommands being added to the main repo tool.
|
||||
repo.commands = {"start": None, "sync": None, "smart": None}
|
||||
return repo
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_config")
|
||||
def fixture_mock_config():
|
||||
return mock.MagicMock()
|
||||
|
||||
|
||||
@mock.patch("time.sleep")
|
||||
def test_autocorrect_delay(mock_sleep, repo, mock_config):
|
||||
"""Test autocorrect with positive delay."""
|
||||
mock_config.GetString.return_value = "10"
|
||||
|
||||
res = repo._autocorrect_command_name("tart", mock_config)
|
||||
|
||||
mock_config.GetString.assert_called_with("help.autocorrect")
|
||||
mock_sleep.assert_called_with(1.0)
|
||||
assert res == "start"
|
||||
|
||||
|
||||
@mock.patch("time.sleep")
|
||||
def test_autocorrect_delay_one(mock_sleep, repo, mock_config):
|
||||
"""Test autocorrect with '1' (0.1s delay, not immediate)."""
|
||||
mock_config.GetString.return_value = "1"
|
||||
|
||||
res = repo._autocorrect_command_name("tart", mock_config)
|
||||
|
||||
mock_sleep.assert_called_with(0.1)
|
||||
assert res == "start"
|
||||
|
||||
|
||||
@mock.patch("time.sleep", side_effect=KeyboardInterrupt())
|
||||
def test_autocorrect_delay_interrupt(mock_sleep, repo, mock_config):
|
||||
"""Test autocorrect handles KeyboardInterrupt during delay."""
|
||||
mock_config.GetString.return_value = "10"
|
||||
|
||||
res = repo._autocorrect_command_name("tart", mock_config)
|
||||
|
||||
mock_sleep.assert_called_with(1.0)
|
||||
assert res is None
|
||||
|
||||
|
||||
@mock.patch("time.sleep")
|
||||
def test_autocorrect_immediate(mock_sleep, repo, mock_config):
|
||||
"""Test autocorrect with immediate/negative delay."""
|
||||
# Test numeric negative.
|
||||
mock_config.GetString.return_value = "-1"
|
||||
res = repo._autocorrect_command_name("tart", mock_config)
|
||||
mock_sleep.assert_not_called()
|
||||
assert res == "start"
|
||||
|
||||
# Test string boolean "true".
|
||||
mock_config.GetString.return_value = "true"
|
||||
res = repo._autocorrect_command_name("tart", mock_config)
|
||||
mock_sleep.assert_not_called()
|
||||
assert res == "start"
|
||||
|
||||
# Test string boolean "yes".
|
||||
mock_config.GetString.return_value = "YES"
|
||||
res = repo._autocorrect_command_name("tart", mock_config)
|
||||
mock_sleep.assert_not_called()
|
||||
assert res == "start"
|
||||
|
||||
# Test string boolean "immediate".
|
||||
mock_config.GetString.return_value = "Immediate"
|
||||
res = repo._autocorrect_command_name("tart", mock_config)
|
||||
mock_sleep.assert_not_called()
|
||||
assert res == "start"
|
||||
|
||||
|
||||
def test_autocorrect_zero_or_show(repo, mock_config):
|
||||
"""Test autocorrect with zero delay (suggestions only)."""
|
||||
# Test numeric zero.
|
||||
mock_config.GetString.return_value = "0"
|
||||
res = repo._autocorrect_command_name("tart", mock_config)
|
||||
assert res is None
|
||||
|
||||
# Test string boolean "false".
|
||||
mock_config.GetString.return_value = "False"
|
||||
res = repo._autocorrect_command_name("tart", mock_config)
|
||||
assert res is None
|
||||
|
||||
# Test string boolean "show".
|
||||
mock_config.GetString.return_value = "show"
|
||||
res = repo._autocorrect_command_name("tart", mock_config)
|
||||
assert res is None
|
||||
|
||||
|
||||
def test_autocorrect_never(repo, mock_config):
|
||||
"""Test autocorrect with 'never'."""
|
||||
mock_config.GetString.return_value = "never"
|
||||
res = repo._autocorrect_command_name("tart", mock_config)
|
||||
assert res is None
|
||||
|
||||
|
||||
@mock.patch("builtins.input", return_value="y")
|
||||
def test_autocorrect_prompt_yes(mock_input, repo, mock_config):
|
||||
"""Test autocorrect with prompt and user answers yes."""
|
||||
mock_config.GetString.return_value = "prompt"
|
||||
|
||||
res = repo._autocorrect_command_name("tart", mock_config)
|
||||
|
||||
assert res == "start"
|
||||
|
||||
|
||||
@mock.patch("builtins.input", return_value="n")
|
||||
def test_autocorrect_prompt_no(mock_input, repo, mock_config):
|
||||
"""Test autocorrect with prompt and user answers no."""
|
||||
mock_config.GetString.return_value = "prompt"
|
||||
|
||||
res = repo._autocorrect_command_name("tart", mock_config)
|
||||
|
||||
assert res is None
|
||||
|
||||
|
||||
@mock.patch("builtins.input", return_value="y")
|
||||
def test_autocorrect_multiple_candidates(mock_input, repo, mock_config):
|
||||
"""Test autocorrect with multiple matches forces a prompt."""
|
||||
mock_config.GetString.return_value = "10" # Normally just delay
|
||||
|
||||
# 'snart' matches both 'start' and 'smart' with > 0.7 ratio
|
||||
res = repo._autocorrect_command_name("snart", mock_config)
|
||||
|
||||
# Because there are multiple candidates, it should prompt
|
||||
mock_input.assert_called_once()
|
||||
assert res == "start"
|
||||
|
||||
|
||||
@mock.patch("builtins.input", side_effect=KeyboardInterrupt())
|
||||
def test_autocorrect_prompt_interrupt(mock_input, repo, mock_config):
|
||||
"""Test autocorrect with prompt and user interrupts."""
|
||||
mock_config.GetString.return_value = "prompt"
|
||||
|
||||
res = repo._autocorrect_command_name("tart", mock_config)
|
||||
|
||||
assert res is None
|
||||
+892
-440
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user