mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-07-17 14:17:01 +00:00
Compare commits
225 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 1e19f7dd61 | |||
| d8b4101eae | |||
| 1c53b0fa44 | |||
| e5ae870a2f | |||
| e59e2ae757 | |||
| c44ad09309 | |||
| 4592a63de5 | |||
| 0444ddf78e | |||
| 9bf8236c24 | |||
| 87f52f308c | |||
| 562cea7758 | |||
| eede374e3e | |||
| 2c5fb84d35 | |||
| 12f6dc49e9 | |||
| 5591d99ee2 | |||
| 9d865454aa | |||
| cbd78a9194 | |||
| 46819a78a1 | |||
| 159389f0da | |||
| 4406642e20 | |||
| 73356f1d5c | |||
| 09fc214a79 | |||
| 3762b17e98 | |||
| ae419e1e01 | |||
| a3a7372612 | |||
| fff1d2d74c | |||
| 4b01a242d8 | |||
| 46790229fc | |||
| edadb25c02 | |||
| 96edb9b573 | |||
| 5554572f02 | |||
| 97ca50f5f9 | |||
| 8896b68926 | |||
| fec8cd6704 | |||
| b8139bdcf8 | |||
| 26fa3180fb | |||
| d379e77f44 | |||
| 4217a82bec | |||
| 208f344950 | |||
| 138c8a9ff5 | |||
| 9b57aa00f6 | |||
| b1d1ece2fb | |||
| 449b23b698 | |||
| e5fb6e585f | |||
| 48e4137eba | |||
| 172c58398b | |||
| aa506db8a7 | |||
| 14c61d2c9d |
@@ -12,5 +12,6 @@ extend-ignore =
|
|||||||
# E731: do not assign a lambda expression, use a def
|
# E731: do not assign a lambda expression, use a def
|
||||||
E731,
|
E731,
|
||||||
exclude =
|
exclude =
|
||||||
|
.venv,
|
||||||
venv,
|
venv,
|
||||||
.tox,
|
.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!
|
Thanks for your contribution!
|
||||||
Unfortunately, we don't use GitHub pull requests to manage code
|
Unfortunately, we don't use GitHub pull requests to manage code
|
||||||
contributions to this repository.
|
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.
|
which provides full instructions on how to get involved.
|
||||||
|
|||||||
@@ -27,6 +27,6 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
python -m pip install tox tox-gh-actions
|
python -m pip install pytest
|
||||||
- name: Test with tox
|
- name: Run tests
|
||||||
run: tox
|
run: python -m pytest
|
||||||
|
|||||||
-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">
|
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
|
||||||
<path>/git-repo</path>
|
<path>/git-repo</path>
|
||||||
</pydev_pathproperty>
|
</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_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
|
||||||
</pydev_project>
|
</pydev_project>
|
||||||
|
|||||||
@@ -43,17 +43,12 @@ probably need to split up your commit to finer grained pieces.
|
|||||||
|
|
||||||
Lint any changes by running:
|
Lint any changes by running:
|
||||||
```sh
|
```sh
|
||||||
$ tox -e lint -- file.py
|
$ flake8
|
||||||
```
|
```
|
||||||
|
|
||||||
And format with:
|
And format with:
|
||||||
```sh
|
```sh
|
||||||
$ tox -e format -- file.py
|
$ black file.py
|
||||||
```
|
|
||||||
|
|
||||||
Or format everything:
|
|
||||||
```sh
|
|
||||||
$ tox -e format
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Repo uses [black](https://black.readthedocs.io/) with line length of 80 as its
|
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/
|
[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
|
[flake8 documentation]: https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html#in-line-ignoring-errors
|
||||||
|
|
||||||
|
|
||||||
## Running tests
|
## Running tests
|
||||||
|
|
||||||
We use [pytest](https://pytest.org/) and [tox](https://tox.readthedocs.io/) for
|
We use [pytest](https://pytest.org/) for running tests. You should make sure to
|
||||||
running tests. You should make sure to install those first.
|
install that first.
|
||||||
|
|
||||||
To run the full suite against all supported Python versions, simply execute:
|
|
||||||
```sh
|
|
||||||
$ tox -p auto
|
|
||||||
```
|
|
||||||
|
|
||||||
We have [`./run_tests`](./run_tests) which is a simple wrapper around `pytest`:
|
We have [`./run_tests`](./run_tests) which is a simple wrapper around `pytest`:
|
||||||
```sh
|
```sh
|
||||||
@@ -143,7 +134,7 @@ they have right to redistribute your work under the Apache License:
|
|||||||
|
|
||||||
Ensure you have obtained an HTTP password to authenticate:
|
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
|
Ensure that you have the local commit hook installed to automatically
|
||||||
add a ChangeId to your commits:
|
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>
|
* Docs: <https://source.android.com/source/using-repo.html>
|
||||||
* [repo Manifest Format](./docs/manifest-format.md)
|
* [repo Manifest Format](./docs/manifest-format.md)
|
||||||
* [repo Hooks](./docs/repo-hooks.md)
|
* [repo Hooks](./docs/repo-hooks.md)
|
||||||
* [Submitting patches](./SUBMITTING_PATCHES.md)
|
* [Contributing](./CONTRIBUTING.md)
|
||||||
* Running Repo in [Microsoft Windows](./docs/windows.md)
|
* Running Repo in [Microsoft Windows](./docs/windows.md)
|
||||||
* GitHub mirror: <https://github.com/GerritCodeReview/git-repo>
|
* GitHub mirror: <https://github.com/GerritCodeReview/git-repo>
|
||||||
* Postsubmit tests: <https://github.com/GerritCodeReview/git-repo/actions>
|
* Postsubmit tests: <https://github.com/GerritCodeReview/git-repo/actions>
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ class Coloring:
|
|||||||
if have_fg:
|
if have_fg:
|
||||||
bg = a
|
bg = a
|
||||||
else:
|
else:
|
||||||
|
have_fg = True
|
||||||
fg = a
|
fg = a
|
||||||
elif is_attr(a):
|
elif is_attr(a):
|
||||||
attr = a
|
attr = a
|
||||||
|
|||||||
+51
-9
@@ -12,6 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import optparse
|
import optparse
|
||||||
import os
|
import os
|
||||||
@@ -70,6 +71,14 @@ class Command:
|
|||||||
# migrated subcommands can set it to False.
|
# migrated subcommands can set it to False.
|
||||||
MULTI_MANIFEST_SUPPORT = True
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
repodir=None,
|
repodir=None,
|
||||||
@@ -242,9 +251,39 @@ class Command:
|
|||||||
"""Perform the action, after option parsing is complete."""
|
"""Perform the action, after option parsing is complete."""
|
||||||
raise NotImplementedError
|
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(
|
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.
|
"""Helper for managing parallel execution boiler plate.
|
||||||
|
|
||||||
@@ -269,6 +308,9 @@ class Command:
|
|||||||
output: An output manager. May be progress.Progess or
|
output: An output manager. May be progress.Progess or
|
||||||
color.Coloring.
|
color.Coloring.
|
||||||
ordered: Whether the jobs should be processed in order.
|
ordered: Whether the jobs should be processed in order.
|
||||||
|
chunksize: The number of jobs processed in batch by parallel
|
||||||
|
workers.
|
||||||
|
initializer: Worker initializer.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The |callback| function's results are returned.
|
The |callback| function's results are returned.
|
||||||
@@ -278,12 +320,16 @@ class Command:
|
|||||||
if len(inputs) == 1 or jobs == 1:
|
if len(inputs) == 1 or jobs == 1:
|
||||||
return callback(None, output, (func(x) for x in inputs))
|
return callback(None, output, (func(x) for x in inputs))
|
||||||
else:
|
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
|
submit = pool.imap if ordered else pool.imap_unordered
|
||||||
return callback(
|
return callback(
|
||||||
pool,
|
pool,
|
||||||
output,
|
output,
|
||||||
submit(func, inputs, chunksize=WORKER_BATCH_SIZE),
|
submit(func, inputs, chunksize=chunksize),
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
if isinstance(output, progress.Progress):
|
if isinstance(output, progress.Progress):
|
||||||
@@ -353,7 +399,7 @@ class Command:
|
|||||||
result = []
|
result = []
|
||||||
|
|
||||||
if not groups:
|
if not groups:
|
||||||
groups = manifest.GetGroupsStr()
|
groups = manifest.GetManifestGroupsStr()
|
||||||
groups = [x for x in re.split(r"[,\s]+", groups) if x]
|
groups = [x for x in re.split(r"[,\s]+", groups) if x]
|
||||||
|
|
||||||
if not args:
|
if not args:
|
||||||
@@ -501,7 +547,3 @@ class MirrorSafeCommand:
|
|||||||
"""Command permits itself to run within a mirror, and does not require a
|
"""Command permits itself to run within a mirror, and does not require a
|
||||||
working directory.
|
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");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# 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
|
(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 content. However, this can run into problems if different remotes use
|
||||||
the same path on their respective servers. Best to avoid that.
|
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.
|
* `subproject-objects/`: Like `project-objects/`, but for git submodules.
|
||||||
* `worktrees/`: Bare checkouts of every project synced by the manifest. The
|
* `worktrees/`: Bare checkouts of every project synced by the manifest. The
|
||||||
filesystem layout matches the `<project name=...` setting in the manifest
|
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 dest-branch CDATA #IMPLIED>
|
||||||
<!ATTLIST default upstream CDATA #IMPLIED>
|
<!ATTLIST default upstream CDATA #IMPLIED>
|
||||||
<!ATTLIST default sync-j CDATA #IMPLIED>
|
<!ATTLIST default sync-j CDATA #IMPLIED>
|
||||||
|
<!ATTLIST default sync-j-max CDATA #IMPLIED>
|
||||||
<!ATTLIST default sync-c CDATA #IMPLIED>
|
<!ATTLIST default sync-c CDATA #IMPLIED>
|
||||||
<!ATTLIST default sync-s CDATA #IMPLIED>
|
<!ATTLIST default sync-s CDATA #IMPLIED>
|
||||||
<!ATTLIST default sync-tags CDATA #IMPLIED>
|
<!ATTLIST default sync-tags CDATA #IMPLIED>
|
||||||
@@ -59,7 +60,7 @@ following DTD:
|
|||||||
<!ATTLIST manifest-server url CDATA #REQUIRED>
|
<!ATTLIST manifest-server url CDATA #REQUIRED>
|
||||||
|
|
||||||
<!ELEMENT submanifest EMPTY>
|
<!ELEMENT submanifest EMPTY>
|
||||||
<!ATTLIST submanifest name ID #REQUIRED>
|
<!ATTLIST submanifest name ID #REQUIRED>
|
||||||
<!ATTLIST submanifest remote IDREF #IMPLIED>
|
<!ATTLIST submanifest remote IDREF #IMPLIED>
|
||||||
<!ATTLIST submanifest project CDATA #IMPLIED>
|
<!ATTLIST submanifest project CDATA #IMPLIED>
|
||||||
<!ATTLIST submanifest manifest-name CDATA #IMPLIED>
|
<!ATTLIST submanifest manifest-name CDATA #IMPLIED>
|
||||||
@@ -72,18 +73,19 @@ following DTD:
|
|||||||
project*,
|
project*,
|
||||||
copyfile*,
|
copyfile*,
|
||||||
linkfile*)>
|
linkfile*)>
|
||||||
<!ATTLIST project name CDATA #REQUIRED>
|
<!ATTLIST project name CDATA #REQUIRED>
|
||||||
<!ATTLIST project path CDATA #IMPLIED>
|
<!ATTLIST project path CDATA #IMPLIED>
|
||||||
<!ATTLIST project remote IDREF #IMPLIED>
|
<!ATTLIST project remote IDREF #IMPLIED>
|
||||||
<!ATTLIST project revision CDATA #IMPLIED>
|
<!ATTLIST project revision CDATA #IMPLIED>
|
||||||
<!ATTLIST project dest-branch CDATA #IMPLIED>
|
<!ATTLIST project dest-branch CDATA #IMPLIED>
|
||||||
<!ATTLIST project groups CDATA #IMPLIED>
|
<!ATTLIST project groups CDATA #IMPLIED>
|
||||||
<!ATTLIST project sync-c CDATA #IMPLIED>
|
<!ATTLIST project sync-c CDATA #IMPLIED>
|
||||||
<!ATTLIST project sync-s CDATA #IMPLIED>
|
<!ATTLIST project sync-s CDATA #IMPLIED>
|
||||||
<!ATTLIST project sync-tags CDATA #IMPLIED>
|
<!ATTLIST project sync-tags CDATA #IMPLIED>
|
||||||
<!ATTLIST project upstream CDATA #IMPLIED>
|
<!ATTLIST project upstream CDATA #IMPLIED>
|
||||||
<!ATTLIST project clone-depth CDATA #IMPLIED>
|
<!ATTLIST project clone-depth CDATA #IMPLIED>
|
||||||
<!ATTLIST project force-path CDATA #IMPLIED>
|
<!ATTLIST project force-path CDATA #IMPLIED>
|
||||||
|
<!ATTLIST project sync-strategy CDATA #IMPLIED>
|
||||||
|
|
||||||
<!ELEMENT annotation EMPTY>
|
<!ELEMENT annotation EMPTY>
|
||||||
<!ATTLIST annotation name CDATA #REQUIRED>
|
<!ATTLIST annotation name CDATA #REQUIRED>
|
||||||
@@ -95,26 +97,30 @@ following DTD:
|
|||||||
<!ATTLIST copyfile dest CDATA #REQUIRED>
|
<!ATTLIST copyfile dest CDATA #REQUIRED>
|
||||||
|
|
||||||
<!ELEMENT linkfile EMPTY>
|
<!ELEMENT linkfile EMPTY>
|
||||||
<!ATTLIST linkfile src CDATA #REQUIRED>
|
<!ATTLIST linkfile src CDATA #REQUIRED>
|
||||||
<!ATTLIST linkfile dest CDATA #REQUIRED>
|
<!ATTLIST linkfile dest CDATA #REQUIRED>
|
||||||
|
|
||||||
<!ELEMENT extend-project EMPTY>
|
<!ELEMENT extend-project (annotation*,
|
||||||
<!ATTLIST extend-project name CDATA #REQUIRED>
|
copyfile*,
|
||||||
<!ATTLIST extend-project path CDATA #IMPLIED>
|
linkfile*)>
|
||||||
<!ATTLIST extend-project dest-path CDATA #IMPLIED>
|
<!ATTLIST extend-project name CDATA #REQUIRED>
|
||||||
<!ATTLIST extend-project groups CDATA #IMPLIED>
|
<!ATTLIST extend-project path CDATA #IMPLIED>
|
||||||
<!ATTLIST extend-project revision CDATA #IMPLIED>
|
<!ATTLIST extend-project dest-path CDATA #IMPLIED>
|
||||||
<!ATTLIST extend-project remote 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 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>
|
<!ELEMENT remove-project EMPTY>
|
||||||
<!ATTLIST remove-project name CDATA #IMPLIED>
|
<!ATTLIST remove-project name CDATA #IMPLIED>
|
||||||
<!ATTLIST remove-project path CDATA #IMPLIED>
|
<!ATTLIST remove-project path CDATA #IMPLIED>
|
||||||
<!ATTLIST remove-project optional CDATA #IMPLIED>
|
<!ATTLIST remove-project optional CDATA #IMPLIED>
|
||||||
|
<!ATTLIST remove-project base-rev CDATA #IMPLIED>
|
||||||
|
|
||||||
<!ELEMENT repo-hooks EMPTY>
|
<!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>
|
<!ATTLIST repo-hooks enabled-list CDATA #REQUIRED>
|
||||||
|
|
||||||
<!ELEMENT superproject EMPTY>
|
<!ELEMENT superproject EMPTY>
|
||||||
@@ -123,7 +129,7 @@ following DTD:
|
|||||||
<!ATTLIST superproject revision CDATA #IMPLIED>
|
<!ATTLIST superproject revision CDATA #IMPLIED>
|
||||||
|
|
||||||
<!ELEMENT contactinfo EMPTY>
|
<!ELEMENT contactinfo EMPTY>
|
||||||
<!ATTLIST contactinfo bugurl CDATA #REQUIRED>
|
<!ATTLIST contactinfo bugurl CDATA #REQUIRED>
|
||||||
|
|
||||||
<!ELEMENT include EMPTY>
|
<!ELEMENT include EMPTY>
|
||||||
<!ATTLIST include name CDATA #REQUIRED>
|
<!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
|
-c mode to avoid having to sync the entire ref space. Project elements
|
||||||
not setting their own `upstream` will inherit this value.
|
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
|
Attribute `sync-c`: Set to true to only sync the given Git
|
||||||
branch (specified in the `revision` attribute) rather than the
|
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
|
is used to specify the URL of a manifest server, which is an
|
||||||
XML RPC service.
|
XML RPC service.
|
||||||
|
|
||||||
The manifest server should implement the following RPC methods:
|
See the [smart sync documentation](./smart-sync.md) for more details.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
### Element submanifest
|
### 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.
|
`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
|
in the included submanifest belong. This appends and recurses, meaning
|
||||||
all projects in submanifests carry all parent submanifest groups.
|
all projects in submanifests carry all parent submanifest groups.
|
||||||
Same syntax as the corresponding element of `project`.
|
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
|
review on this branch. If unspecified both here and in the
|
||||||
default element, `revision` is used instead.
|
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
|
whitespace or comma separated. All projects belong to the group
|
||||||
"all", and each project automatically belongs to a group of
|
"all", and each project automatically belongs to a group of
|
||||||
its name:`name` and path:`path`. E.g. for
|
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
|
local mirrors syncing, it will be ignored when syncing the projects in a
|
||||||
client working directory.
|
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
|
### Element extend-project
|
||||||
|
|
||||||
Modify the attributes of the named 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
|
existing project definition. This makes the local manifest more robust
|
||||||
against changes to the original manifest.
|
against changes to the original manifest.
|
||||||
|
|
||||||
|
The `extend-project` element can also contain `annotation`, `copyfile`, and
|
||||||
|
`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
|
Attribute `path`: If specified, limit the change to projects checked out
|
||||||
at the specified path, rather than all projects with the given name.
|
at the specified path, rather than all projects with the given name.
|
||||||
|
|
||||||
@@ -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
|
should be placed. This is used to move a project in the checkout by
|
||||||
overriding the existing `path` setting.
|
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`.
|
belongs. Same syntax as the corresponding element of `project`.
|
||||||
|
|
||||||
Attribute `revision`: If specified, overrides the revision of the original
|
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
|
Attribute `upstream`: If specified, overrides the upstream of the original
|
||||||
project. Same syntax as the corresponding element of `project`.
|
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
|
### Element annotation
|
||||||
|
|
||||||
Zero or more annotation elements may be specified as children of a
|
Zero or more annotation elements may be specified as children of a
|
||||||
project or remote element. Each element describes a name-value pair.
|
project element, an extend-project element, or a remote element. Each
|
||||||
For projects, this name-value pair will be exported into each project's
|
element describes a name-value pair. For projects, this name-value pair
|
||||||
environment during a 'forall' command, prefixed with `REPO__`. In addition,
|
will be exported into each project's environment during a 'forall'
|
||||||
there is an optional attribute "keep" which accepts the case insensitive values
|
command, prefixed with `REPO__`. In addition, there is an optional
|
||||||
"true" (default) or "false". This attribute determines whether or not the
|
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.
|
annotation will be kept when exported with the manifest subcommand.
|
||||||
|
|
||||||
### Element copyfile
|
### Element copyfile
|
||||||
|
|
||||||
Zero or more copyfile elements may be specified as children of a
|
Zero or more copyfile elements may be specified as children of a
|
||||||
project element. Each element describes a src-dest pair of files;
|
project element, or an extend-project element. Each element describes a
|
||||||
the "src" file will be copied to the "dest" place during `repo sync`
|
src-dest pair of files; the "src" file will be copied to the "dest"
|
||||||
command.
|
place during `repo sync` command.
|
||||||
|
|
||||||
"src" is project relative, "dest" is relative to the top of the tree.
|
"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
|
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.
|
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
|
### Element linkfile
|
||||||
|
|
||||||
It's just like copyfile and runs at the same time as copyfile but
|
It's just like copyfile, but instead of copying it creates a symlink.
|
||||||
instead of copying it creates a symlink.
|
|
||||||
|
|
||||||
The symlink is created at "dest" (relative to the top of the tree) and
|
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.
|
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
|
The symlink target may be a file or directory, but it may not point outside
|
||||||
of the repo client.
|
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
|
### Element remove-project
|
||||||
|
|
||||||
Deletes a project from the internal manifest table, possibly
|
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
|
Attribute `optional`: Set to true to ignore remove-project elements with no
|
||||||
matching `project` element.
|
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
|
### Element repo-hooks
|
||||||
|
|
||||||
NB: See the [practical documentation](./repo-hooks.md) for using 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.
|
"name" may not be an absolute path or use "." or ".." path components.
|
||||||
These restrictions are not enforced for [Local Manifests].
|
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
|
in the included manifest belong. This appends and recurses, meaning
|
||||||
all projects in included manifests carry all parent include groups.
|
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`.
|
Same syntax as the corresponding element of `project`.
|
||||||
|
|
||||||
Attribute `revision`: Name of a Git branch (e.g. `main` or `refs/heads/main`)
|
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}
|
## Local Manifests {#local-manifests}
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,8 @@ you have newer versions installed, your choices are:
|
|||||||
|
|
||||||
* Modify the [repo launcher]'s shebang to suite your environment.
|
* Modify the [repo launcher]'s shebang to suite your environment.
|
||||||
* Download an older version of the [repo launcher] and don't upgrade it.
|
* Download an older version of the [repo launcher] and don't upgrade it.
|
||||||
Be aware that there is no guarantee old repo launchers are WILL work with
|
Be aware that we do not guarantee old repo launchers will work with current
|
||||||
current versions of repo. Bug reports using old launchers will not be
|
versions of repo. Bug reports using old launchers will not be accepted.
|
||||||
accepted.
|
|
||||||
|
|
||||||
## When to drop support
|
## When to drop support
|
||||||
|
|
||||||
|
|||||||
+30
-8
@@ -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 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
|
### Force an update
|
||||||
|
|
||||||
The `repo selfupdate` command can be used to force an immediate update.
|
The `repo selfupdate` command can be used to force an immediate update.
|
||||||
@@ -202,7 +205,7 @@ still support them.
|
|||||||
Things in italics are things we used to care about but probably don't anymore.
|
Things in italics are things we used to care about but probably don't anymore.
|
||||||
|
|
||||||
| Date | EOL | [Git][rel-g] | [Python][rel-p] | [SSH][rel-o] | [Ubuntu][rel-u] / [Debian][rel-d] | Git | Python | SSH |
|
| Date | EOL | [Git][rel-g] | [Python][rel-p] | [SSH][rel-o] | [Ubuntu][rel-u] / [Debian][rel-d] | Git | Python | SSH |
|
||||||
|:--------:|:------------:|:------------:|:---------------:|:------------:|-----------------------------------|-----|--------|-----|
|
|:--------:|:------------:|:------------:|:---------------:|:------------:|-----------------------------------|:---:|:------:|:---:|
|
||||||
| Apr 2008 | | | | 5.0 |
|
| Apr 2008 | | | | 5.0 |
|
||||||
| Jun 2008 | | | | 5.1 |
|
| Jun 2008 | | | | 5.1 |
|
||||||
| Oct 2008 | *Oct 2013* | | 2.6.0 | | *10.04 Lucid* - 10.10 Maverick / *Squeeze* |
|
| Oct 2008 | *Oct 2013* | | 2.6.0 | | *10.04 Lucid* - 10.10 Maverick / *Squeeze* |
|
||||||
@@ -241,7 +244,7 @@ Things in italics are things we used to care about but probably don't anymore.
|
|||||||
| Feb 2014 | *Dec 2014* | **1.9.0** | | | *14.04 Trusty* |
|
| Feb 2014 | *Dec 2014* | **1.9.0** | | | *14.04 Trusty* |
|
||||||
| Mar 2014 | *Mar 2019* | | *3.4.0* | | *14.04 Trusty* - 15.10 Wily / *Jessie* |
|
| Mar 2014 | *Mar 2019* | | *3.4.0* | | *14.04 Trusty* - 15.10 Wily / *Jessie* |
|
||||||
| Mar 2014 | | | | 6.6 | *14.04 Trusty* - 14.10 Utopic |
|
| Mar 2014 | | | | 6.6 | *14.04 Trusty* - 14.10 Utopic |
|
||||||
| Apr 2014 | *Apr 2022* | | | | *14.04 Trusty* | 1.9.1 | 2.7.5 3.4.0 | 6.6 |
|
| Apr 2014 | *Apr 2024* | | | | *14.04 Trusty* | 1.9.1 | 2.7.5 3.4.0 | 6.6 |
|
||||||
| May 2014 | *Dec 2014* | 2.0.0 |
|
| May 2014 | *Dec 2014* | 2.0.0 |
|
||||||
| Aug 2014 | *Dec 2014* | *2.1.0* | | | 14.10 Utopic - 15.04 Vivid / *Jessie* |
|
| Aug 2014 | *Dec 2014* | *2.1.0* | | | 14.10 Utopic - 15.04 Vivid / *Jessie* |
|
||||||
| Oct 2014 | | | | 6.7 | 15.04 Vivid |
|
| Oct 2014 | | | | 6.7 | 15.04 Vivid |
|
||||||
@@ -262,7 +265,7 @@ Things in italics are things we used to care about but probably don't anymore.
|
|||||||
| Jan 2016 | *Jul 2017* | *2.7.0* | | | *16.04 Xenial* |
|
| Jan 2016 | *Jul 2017* | *2.7.0* | | | *16.04 Xenial* |
|
||||||
| Feb 2016 | | | | 7.2 | *16.04 Xenial* |
|
| Feb 2016 | | | | 7.2 | *16.04 Xenial* |
|
||||||
| Mar 2016 | *Jul 2017* | 2.8.0 |
|
| Mar 2016 | *Jul 2017* | 2.8.0 |
|
||||||
| Apr 2016 | *Apr 2024* | | | | *16.04 Xenial* | 2.7.4 | 2.7.11 3.5.1 | 7.2 |
|
| Apr 2016 | *Apr 2026* | | | | *16.04 Xenial* | 2.7.4 | 2.7.11 3.5.1 | 7.2 |
|
||||||
| Jun 2016 | *Jul 2017* | 2.9.0 | | | 16.10 Yakkety |
|
| Jun 2016 | *Jul 2017* | 2.9.0 | | | 16.10 Yakkety |
|
||||||
| Jul 2016 | | | | 7.3 | 16.10 Yakkety |
|
| Jul 2016 | | | | 7.3 | 16.10 Yakkety |
|
||||||
| Sep 2016 | *Sep 2017* | 2.10.0 |
|
| Sep 2016 | *Sep 2017* | 2.10.0 |
|
||||||
@@ -312,14 +315,33 @@ Things in italics are things we used to care about but probably don't anymore.
|
|||||||
| Oct 2020 | | | | | 20.10 Groovy | 2.27.0 | 2.7.18 3.8.6 | 8.3 |
|
| Oct 2020 | | | | | 20.10 Groovy | 2.27.0 | 2.7.18 3.8.6 | 8.3 |
|
||||||
| Oct 2020 | **Oct 2025** | | 3.9.0 | | 21.04 Hirsute / **Bullseye** |
|
| Oct 2020 | **Oct 2025** | | 3.9.0 | | 21.04 Hirsute / **Bullseye** |
|
||||||
| Dec 2020 | *Mar 2021* | 2.30.0 | | | 21.04 Hirsute / **Bullseye** |
|
| Dec 2020 | *Mar 2021* | 2.30.0 | | | 21.04 Hirsute / **Bullseye** |
|
||||||
| Mar 2021 | | 2.31.0 |
|
| Mar 2021 | | 2.31.0 | | 8.5 |
|
||||||
| Mar 2021 | | | | 8.5 |
|
|
||||||
| Apr 2021 | | | | 8.6 |
|
| Apr 2021 | | | | 8.6 |
|
||||||
| Apr 2021 | *Jan 2022* | | | | 21.04 Hirsute | 2.30.2 | 2.7.18 3.9.4 | 8.4 |
|
| Apr 2021 | *Jan 2022* | | | | 21.04 Hirsute | 2.30.2 | 2.7.18 3.9.4 | 8.4 |
|
||||||
| Jun 2021 | | 2.32.0 |
|
| Jun 2021 | | 2.32.0 |
|
||||||
| Aug 2021 | | 2.33.0 |
|
| Aug 2021 | | 2.33.0 | | 8.7 |
|
||||||
| Aug 2021 | | | | 8.7 |
|
|
||||||
| Aug 2021 | **Aug 2026** | | | | **Debian 11 Bullseye** | 2.30.2 | 2.7.18 3.9.2 | 8.4 |
|
| Aug 2021 | **Aug 2026** | | | | **Debian 11 Bullseye** | 2.30.2 | 2.7.18 3.9.2 | 8.4 |
|
||||||
|
| Sep 2021 | | | | 8.8 |
|
||||||
|
| Oct 2021 | | 2.34.0 | 3.10.0 | | **22.04 Jammy** |
|
||||||
|
| Jan 2022 | | 2.35.0 |
|
||||||
|
| Feb 2022 | | | | 8.9 | **22.04 Jammy** |
|
||||||
|
| Apr 2022 | | 2.36.0 | | 9.0 |
|
||||||
|
| Apr 2022 | **Apr 2032** | | | | **22.04 Jammy** | 2.34.1 | 2.7.18 3.10.6 | 8.9 |
|
||||||
|
| Jun 2022 | | 2.37.0 |
|
||||||
|
| Oct 2022 | | 2.38.0 | | 9.1 |
|
||||||
|
| Oct 2022 | | | 3.11.0 | | **Bookworm** |
|
||||||
|
| Dec 2022 | | 2.39.0 | | | **Bookworm** |
|
||||||
|
| Feb 2023 | | | | 9.2 | **Bookworm** |
|
||||||
|
| Mar 2023 | | 2.40.0 | | 9.3 |
|
||||||
|
| Jun 2023 | | 2.41.0 |
|
||||||
|
| Jun 2023 | **Jun 2028** | | | | **Debian 12 Bookworm** | 2.39.2 | 3.11.2 | 9.2 |
|
||||||
|
| Aug 2023 | | 2.42.0 | | 9.4 |
|
||||||
|
| Oct 2023 | | | 3.12.0 | 9.5 |
|
||||||
|
| Nov 2022 | | 2.43.0 |
|
||||||
|
| Dec 2023 | | | | 9.6 |
|
||||||
|
| Feb 2024 | | 2.44.0 |
|
||||||
|
| Mar 2024 | | | | 9.7 |
|
||||||
|
| Oct 2024 | | | 3.13.0 |
|
||||||
| **Date** | **EOL** | **[Git][rel-g]** | **[Python][rel-p]** | **[SSH][rel-o]** | **[Ubuntu][rel-u] / [Debian][rel-d]** | **Git** | **Python** | **SSH** |
|
| **Date** | **EOL** | **[Git][rel-g]** | **[Python][rel-p]** | **[SSH][rel-o]** | **[Ubuntu][rel-u] / [Debian][rel-d]** | **Git** | **Python** | **SSH** |
|
||||||
|
|
||||||
|
|
||||||
@@ -328,7 +350,7 @@ Things in italics are things we used to care about but probably don't anymore.
|
|||||||
[rel-g]: https://en.wikipedia.org/wiki/Git#Releases
|
[rel-g]: https://en.wikipedia.org/wiki/Git#Releases
|
||||||
[rel-o]: https://www.openssh.com/releasenotes.html
|
[rel-o]: https://www.openssh.com/releasenotes.html
|
||||||
[rel-p]: https://en.wikipedia.org/wiki/History_of_Python#Table_of_versions
|
[rel-p]: https://en.wikipedia.org/wiki/History_of_Python#Table_of_versions
|
||||||
[rel-u]: https://en.wikipedia.org/wiki/Ubuntu_version_history#Table_of_versions
|
[rel-u]: https://wiki.ubuntu.com/Releases
|
||||||
[example announcement]: https://groups.google.com/d/topic/repo-discuss/UGBNismWo1M/discussion
|
[example announcement]: https://groups.google.com/d/topic/repo-discuss/UGBNismWo1M/discussion
|
||||||
[repo-discuss@googlegroups.com]: https://groups.google.com/forum/#!forum/repo-discuss
|
[repo-discuss@googlegroups.com]: https://groups.google.com/forum/#!forum/repo-discuss
|
||||||
[go/repo-release]: https://goto.google.com/repo-release
|
[go/repo-release]: https://goto.google.com/repo-release
|
||||||
|
|||||||
@@ -133,3 +133,43 @@ def main(project_list, worktree_list=None, **kwargs):
|
|||||||
kwargs: Leave this here for forward-compatibility.
|
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, **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.
|
||||||
|
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)`.
|
||||||
+5
-2
@@ -50,8 +50,11 @@ Git worktrees (see the previous section for more info).
|
|||||||
Repo will use symlinks heavily internally.
|
Repo will use symlinks heavily internally.
|
||||||
On *NIX platforms, this isn't an issue, but Windows makes it a bit difficult.
|
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
|
The easiest method to allow users to create symlinks is by enabling
|
||||||
answer is to run your shell as an Administrator and invoke repo/git in that.
|
[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.
|
This isn't a great solution, but Windows doesn't make this easy, so here we are.
|
||||||
|
|
||||||
|
|||||||
@@ -107,8 +107,8 @@ class GitError(RepoError):
|
|||||||
return self.message
|
return self.message
|
||||||
|
|
||||||
|
|
||||||
class GitcUnsupportedError(RepoExitError):
|
class GitAuthError(RepoExitError):
|
||||||
"""Gitc no longer supported."""
|
"""Cannot talk to remote due to auth issue."""
|
||||||
|
|
||||||
|
|
||||||
class UploadError(RepoError):
|
class UploadError(RepoError):
|
||||||
|
|||||||
+10
-2
@@ -168,8 +168,10 @@ class EventLog:
|
|||||||
f.write("\n")
|
f.write("\n")
|
||||||
|
|
||||||
|
|
||||||
# An integer id that is unique across this invocation of the program.
|
# An integer id that is unique across this invocation of the program, to be set
|
||||||
_EVENT_ID = multiprocessing.Value("i", 1)
|
# 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():
|
def _NextEventId():
|
||||||
@@ -178,6 +180,12 @@ def _NextEventId():
|
|||||||
Returns:
|
Returns:
|
||||||
A unique, to this invocation of the program, integer id.
|
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():
|
with _EVENT_ID.get_lock():
|
||||||
val = _EVENT_ID.value
|
val = _EVENT_ID.value
|
||||||
_EVENT_ID.value += 1
|
_EVENT_ID.value += 1
|
||||||
|
|||||||
+21
-28
@@ -22,7 +22,6 @@ from typing import Any, Optional
|
|||||||
|
|
||||||
from error import GitError
|
from error import GitError
|
||||||
from error import RepoExitError
|
from error import RepoExitError
|
||||||
from git_refs import HEAD
|
|
||||||
from git_trace2_event_log_base import BaseEventLog
|
from git_trace2_event_log_base import BaseEventLog
|
||||||
import platform_utils
|
import platform_utils
|
||||||
from repo_logging import RepoLogger
|
from repo_logging import RepoLogger
|
||||||
@@ -33,17 +32,6 @@ from wrapper import Wrapper
|
|||||||
|
|
||||||
|
|
||||||
GIT = "git"
|
GIT = "git"
|
||||||
# NB: These do not need to be kept in sync with the repo launcher script.
|
|
||||||
# These may be much newer as it allows the repo launcher to roll between
|
|
||||||
# different repo releases while source versions might require a newer git.
|
|
||||||
#
|
|
||||||
# The soft version is when we start warning users that the version is old and
|
|
||||||
# we'll be dropping support for it. We'll refuse to work with versions older
|
|
||||||
# than the hard version.
|
|
||||||
#
|
|
||||||
# git-1.7 is in (EOL) Ubuntu Precise. git-1.9 is in Ubuntu Trusty.
|
|
||||||
MIN_GIT_VERSION_SOFT = (1, 9, 1)
|
|
||||||
MIN_GIT_VERSION_HARD = (1, 7, 2)
|
|
||||||
GIT_DIR = "GIT_DIR"
|
GIT_DIR = "GIT_DIR"
|
||||||
|
|
||||||
LAST_GITDIR = None
|
LAST_GITDIR = None
|
||||||
@@ -59,7 +47,7 @@ logger = RepoLogger(__file__)
|
|||||||
|
|
||||||
|
|
||||||
class _GitCall:
|
class _GitCall:
|
||||||
@functools.lru_cache(maxsize=None)
|
@functools.lru_cache(maxsize=None) # noqa: B019
|
||||||
def version_tuple(self):
|
def version_tuple(self):
|
||||||
ret = Wrapper().ParseGitVersion()
|
ret = Wrapper().ParseGitVersion()
|
||||||
if ret is None:
|
if ret is None:
|
||||||
@@ -94,7 +82,7 @@ def RepoSourceVersion():
|
|||||||
proj = os.path.dirname(os.path.abspath(__file__))
|
proj = os.path.dirname(os.path.abspath(__file__))
|
||||||
env[GIT_DIR] = os.path.join(proj, ".git")
|
env[GIT_DIR] = os.path.join(proj, ".git")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[GIT, "describe", HEAD],
|
[GIT, "describe", "HEAD"],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
@@ -107,7 +95,7 @@ def RepoSourceVersion():
|
|||||||
ver = ver[1:]
|
ver = ver[1:]
|
||||||
else:
|
else:
|
||||||
ver = "unknown"
|
ver = "unknown"
|
||||||
setattr(RepoSourceVersion, "version", ver)
|
RepoSourceVersion.version = ver
|
||||||
|
|
||||||
return ver
|
return ver
|
||||||
|
|
||||||
@@ -135,6 +123,8 @@ def GetEventTargetPath():
|
|||||||
if retval == 0:
|
if retval == 0:
|
||||||
# Strip trailing carriage-return in path.
|
# Strip trailing carriage-return in path.
|
||||||
path = p.stdout.rstrip("\n")
|
path = p.stdout.rstrip("\n")
|
||||||
|
if path == "":
|
||||||
|
return None
|
||||||
elif retval != 1:
|
elif retval != 1:
|
||||||
# `git config --get` is documented to produce an exit status of `1`
|
# `git config --get` is documented to produce an exit status of `1`
|
||||||
# if the requested variable is not present in the configuration.
|
# if the requested variable is not present in the configuration.
|
||||||
@@ -247,9 +237,9 @@ def _build_env(
|
|||||||
s = p + " " + s
|
s = p + " " + s
|
||||||
env["GIT_CONFIG_PARAMETERS"] = s
|
env["GIT_CONFIG_PARAMETERS"] = s
|
||||||
if "GIT_ALLOW_PROTOCOL" not in env:
|
if "GIT_ALLOW_PROTOCOL" not in env:
|
||||||
env[
|
env["GIT_ALLOW_PROTOCOL"] = (
|
||||||
"GIT_ALLOW_PROTOCOL"
|
"file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc"
|
||||||
] = "file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc"
|
)
|
||||||
env["GIT_HTTP_USER_AGENT"] = user_agent.git
|
env["GIT_HTTP_USER_AGENT"] = user_agent.git
|
||||||
|
|
||||||
if objdir:
|
if objdir:
|
||||||
@@ -322,12 +312,15 @@ class GitCommand:
|
|||||||
cwd = None
|
cwd = None
|
||||||
command_name = cmdv[0]
|
command_name = cmdv[0]
|
||||||
command.append(command_name)
|
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
|
if command_name in ("fetch", "clone"):
|
||||||
# TTY.
|
env["GIT_TERMINAL_PROMPT"] = "0"
|
||||||
if sys.stderr.isatty() and command_name in ("fetch", "clone"):
|
# Need to use the --progress flag for fetch/clone so output will be
|
||||||
if "--progress" not in cmdv and "--quiet" not in cmdv:
|
# displayed as by default git only does progress output if stderr is
|
||||||
command.append("--progress")
|
# a TTY.
|
||||||
|
if sys.stderr.isatty():
|
||||||
|
if "--progress" not in cmdv and "--quiet" not in cmdv:
|
||||||
|
command.append("--progress")
|
||||||
command.extend(cmdv[1:])
|
command.extend(cmdv[1:])
|
||||||
|
|
||||||
event_log = (
|
event_log = (
|
||||||
@@ -356,9 +349,9 @@ class GitCommand:
|
|||||||
"Project": e.project,
|
"Project": e.project,
|
||||||
"CommandName": command_name,
|
"CommandName": command_name,
|
||||||
"Message": str(e),
|
"Message": str(e),
|
||||||
"ReturnCode": str(e.git_rc)
|
"ReturnCode": (
|
||||||
if e.git_rc is not None
|
str(e.git_rc) if e.git_rc is not None else None
|
||||||
else None,
|
),
|
||||||
"IsError": log_as_error,
|
"IsError": log_as_error,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -618,7 +611,7 @@ class GitCommandError(GitError):
|
|||||||
self.git_stderr = git_stderr
|
self.git_stderr = git_stderr
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@functools.lru_cache(maxsize=None)
|
@functools.lru_cache(maxsize=None) # noqa: B019
|
||||||
def suggestion(self):
|
def suggestion(self):
|
||||||
"""Returns helpful next steps for the given stderr."""
|
"""Returns helpful next steps for the given stderr."""
|
||||||
if not self.git_stderr:
|
if not self.git_stderr:
|
||||||
|
|||||||
+22
-2
@@ -42,7 +42,7 @@ SYNC_STATE_PREFIX = "repo.syncstate."
|
|||||||
|
|
||||||
ID_RE = re.compile(r"^[0-9a-f]{40}$")
|
ID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||||
|
|
||||||
REVIEW_CACHE = dict()
|
REVIEW_CACHE = {}
|
||||||
|
|
||||||
|
|
||||||
def IsChange(rev):
|
def IsChange(rev):
|
||||||
@@ -90,6 +90,20 @@ class GitConfig:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _getUserConfig():
|
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")
|
return os.path.expanduser("~/.gitconfig")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -97,7 +111,7 @@ class GitConfig:
|
|||||||
return cls(configfile=os.path.join(gitdir, "config"), defaults=defaults)
|
return cls(configfile=os.path.join(gitdir, "config"), defaults=defaults)
|
||||||
|
|
||||||
def __init__(self, configfile, defaults=None, jsonFile=None):
|
def __init__(self, configfile, defaults=None, jsonFile=None):
|
||||||
self.file = configfile
|
self.file = str(configfile)
|
||||||
self.defaults = defaults
|
self.defaults = defaults
|
||||||
self._cache_dict = None
|
self._cache_dict = None
|
||||||
self._section_dict = None
|
self._section_dict = None
|
||||||
@@ -208,6 +222,12 @@ class GitConfig:
|
|||||||
value = "true" if value else "false"
|
value = "true" if value else "false"
|
||||||
self.SetString(name, value)
|
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]:
|
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.
|
"""Get the first value for a key, or None if it is not defined.
|
||||||
|
|
||||||
|
|||||||
+88
-57
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from git_command import GitCommand
|
||||||
import platform_utils
|
import platform_utils
|
||||||
from repo_trace import Trace
|
from repo_trace import Trace
|
||||||
|
|
||||||
@@ -86,9 +87,8 @@ class GitRefs:
|
|||||||
self._symref = {}
|
self._symref = {}
|
||||||
self._mtime = {}
|
self._mtime = {}
|
||||||
|
|
||||||
self._ReadPackedRefs()
|
self._ReadRefs()
|
||||||
self._ReadLoose("refs/")
|
self._ReadSymbolicRef(HEAD)
|
||||||
self._ReadLoose1(os.path.join(self._gitdir, HEAD), HEAD)
|
|
||||||
|
|
||||||
scan = self._symref
|
scan = self._symref
|
||||||
attempts = 0
|
attempts = 0
|
||||||
@@ -102,64 +102,95 @@ class GitRefs:
|
|||||||
scan = scan_next
|
scan = scan_next
|
||||||
attempts += 1
|
attempts += 1
|
||||||
|
|
||||||
def _ReadPackedRefs(self):
|
self._TrackMtime(HEAD)
|
||||||
path = os.path.join(self._gitdir, "packed-refs")
|
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:
|
try:
|
||||||
fd = open(path)
|
self._mtime[name] = os.path.getmtime(path)
|
||||||
mtime = os.path.getmtime(path)
|
|
||||||
except OSError:
|
except OSError:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def _TrackTreeMtimes(self, root: str) -> None:
|
||||||
|
"""Recursively track modification times for a directory tree."""
|
||||||
|
root_path = os.path.join(self._gitdir, root)
|
||||||
try:
|
try:
|
||||||
for line in fd:
|
if not platform_utils.isdir(root_path):
|
||||||
line = str(line)
|
return
|
||||||
if line[0] == "#":
|
except OSError:
|
||||||
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):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
to_scan = [root]
|
||||||
ref_id = ref_id.decode()
|
while to_scan:
|
||||||
except AttributeError:
|
name = to_scan.pop()
|
||||||
pass
|
self._TrackMtime(name)
|
||||||
if not ref_id:
|
path = os.path.join(self._gitdir, name)
|
||||||
return
|
if not platform_utils.isdir(path):
|
||||||
ref_id = ref_id[:-1]
|
continue
|
||||||
|
|
||||||
if ref_id.startswith("ref: "):
|
for child in platform_utils.listdir(path):
|
||||||
self._symref[name] = ref_id[5:]
|
child_name = os.path.join(name, child)
|
||||||
else:
|
child_path = os.path.join(self._gitdir, child_name)
|
||||||
self._phyref[name] = ref_id
|
if platform_utils.isdir(child_path):
|
||||||
self._mtime[name] = mtime
|
to_scan.append(child_name)
|
||||||
|
else:
|
||||||
|
self._TrackMtime(child_name)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
#
|
|
||||||
# Copyright (C) 2009 The Android Open Source Project
|
# Copyright (C) 2009 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");
|
||||||
|
|||||||
+104
-32
@@ -23,16 +23,20 @@ Examples:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
import glob
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import time
|
import time
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
from git_command import git_require
|
from git_command import git_require
|
||||||
from git_command import GitCommand
|
from git_command import GitCommand
|
||||||
from git_config import RepoConfig
|
from git_config import RepoConfig
|
||||||
from git_refs import GitRefs
|
from git_refs import GitRefs
|
||||||
|
import platform_utils
|
||||||
|
|
||||||
|
|
||||||
_SUPERPROJECT_GIT_NAME = "superproject.git"
|
_SUPERPROJECT_GIT_NAME = "superproject.git"
|
||||||
@@ -128,6 +132,30 @@ class Superproject:
|
|||||||
"""Set the _print_messages attribute."""
|
"""Set the _print_messages attribute."""
|
||||||
self._print_messages = value
|
self._print_messages = value
|
||||||
|
|
||||||
|
@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
|
@property
|
||||||
def project_commit_ids(self):
|
def project_commit_ids(self):
|
||||||
"""Returns a dictionary of projects and their commit ids."""
|
"""Returns a dictionary of projects and their commit ids."""
|
||||||
@@ -140,12 +168,33 @@ class Superproject:
|
|||||||
self._manifest_path if os.path.exists(self._manifest_path) else None
|
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):
|
def _LogMessage(self, fmt, *inputs):
|
||||||
"""Logs message to stderr and _git_event_log."""
|
"""Logs message to stderr and _git_event_log."""
|
||||||
message = f"{self._LogMessagePrefix()} {fmt.format(*inputs)}"
|
message = f"{self._LogMessagePrefix()} {fmt.format(*inputs)}"
|
||||||
if self._print_messages:
|
if self._print_messages:
|
||||||
print(message, file=sys.stderr)
|
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):
|
def _LogMessagePrefix(self):
|
||||||
"""Returns the prefix string to be logged in each log message"""
|
"""Returns the prefix string to be logged in each log message"""
|
||||||
@@ -169,30 +218,63 @@ class Superproject:
|
|||||||
"""
|
"""
|
||||||
if not os.path.exists(self._superproject_path):
|
if not os.path.exists(self._superproject_path):
|
||||||
os.mkdir(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(
|
print(
|
||||||
"%s: Performing initial setup for superproject; this might "
|
"%s: Performing initial setup for superproject; this might "
|
||||||
"take several minutes." % self._work_git
|
"take several minutes." % self._work_git
|
||||||
)
|
)
|
||||||
cmd = ["init", "--bare", self._work_git_name]
|
|
||||||
p = GitCommand(
|
tmp_gitdir_prefix = ".tmp-superproject-initgitdir-"
|
||||||
None,
|
tmp_gitdir = tempfile.mkdtemp(
|
||||||
cmd,
|
prefix=tmp_gitdir_prefix,
|
||||||
cwd=self._superproject_path,
|
dir=self._superproject_path,
|
||||||
capture_stdout=True,
|
|
||||||
capture_stderr=True,
|
|
||||||
)
|
)
|
||||||
retval = p.Wait()
|
tmp_git_name = os.path.basename(tmp_gitdir)
|
||||||
if retval:
|
|
||||||
self._LogWarning(
|
try:
|
||||||
"git init call failed, command: git {}, "
|
cmd = ["init", "--bare", tmp_git_name]
|
||||||
"return code: {}, stderr: {}",
|
p = GitCommand(
|
||||||
|
None,
|
||||||
cmd,
|
cmd,
|
||||||
retval,
|
cwd=self._superproject_path,
|
||||||
p.stderr,
|
capture_stdout=True,
|
||||||
|
capture_stderr=True,
|
||||||
)
|
)
|
||||||
return False
|
retval = p.Wait()
|
||||||
return True
|
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):
|
def _Fetch(self):
|
||||||
"""Fetches a superproject for the manifest based on |_remote_url|.
|
"""Fetches a superproject for the manifest based on |_remote_url|.
|
||||||
@@ -258,7 +340,7 @@ class Superproject:
|
|||||||
Works only in git repositories.
|
Works only in git repositories.
|
||||||
|
|
||||||
Returns:
|
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):
|
if not os.path.exists(self._work_git):
|
||||||
self._LogWarning(
|
self._LogWarning(
|
||||||
@@ -288,6 +370,7 @@ class Superproject:
|
|||||||
retval,
|
retval,
|
||||||
p.stderr,
|
p.stderr,
|
||||||
)
|
)
|
||||||
|
return None
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def Sync(self, git_event_log):
|
def Sync(self, git_event_log):
|
||||||
@@ -307,8 +390,6 @@ class Superproject:
|
|||||||
)
|
)
|
||||||
return SyncResult(False, False)
|
return SyncResult(False, False)
|
||||||
|
|
||||||
_PrintBetaNotice()
|
|
||||||
|
|
||||||
should_exit = True
|
should_exit = True
|
||||||
if not self._remote_url:
|
if not self._remote_url:
|
||||||
self._LogWarning(
|
self._LogWarning(
|
||||||
@@ -377,7 +458,8 @@ class Superproject:
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
manifest_str = self._manifest.ToXml(
|
manifest_str = self._manifest.ToXml(
|
||||||
groups=self._manifest.GetGroupsStr(), omit_local=True
|
filter_groups=self._manifest.GetManifestGroupsStr(),
|
||||||
|
omit_local=True,
|
||||||
).toxml()
|
).toxml()
|
||||||
manifest_path = self._manifest_path
|
manifest_path = self._manifest_path
|
||||||
try:
|
try:
|
||||||
@@ -452,16 +534,6 @@ class Superproject:
|
|||||||
return UpdateProjectsResult(manifest_path, False)
|
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)
|
@functools.lru_cache(maxsize=None)
|
||||||
def _UseSuperprojectFromConfiguration():
|
def _UseSuperprojectFromConfiguration():
|
||||||
"""Returns the user choice of whether to use superproject."""
|
"""Returns the user choice of whether to use superproject."""
|
||||||
|
|||||||
@@ -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 GetEventTargetPath
|
||||||
from git_command import RepoSourceVersion
|
from git_command import RepoSourceVersion
|
||||||
from git_trace2_event_log_base import BaseEventLog
|
from git_trace2_event_log_base import BaseEventLog
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ import tempfile
|
|||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
# Timeout when sending events via socket (applies to connect, send)
|
||||||
|
SOCK_TIMEOUT = 0.5 # in seconds
|
||||||
# BaseEventLog __init__ Counter that is consistent within the same process
|
# BaseEventLog __init__ Counter that is consistent within the same process
|
||||||
p_init_count = 0
|
p_init_count = 0
|
||||||
|
|
||||||
@@ -66,6 +68,7 @@ class BaseEventLog:
|
|||||||
global p_init_count
|
global p_init_count
|
||||||
p_init_count += 1
|
p_init_count += 1
|
||||||
self._log = []
|
self._log = []
|
||||||
|
self.verbose = False
|
||||||
# Try to get session-id (sid) from environment (setup in repo launcher).
|
# Try to get session-id (sid) from environment (setup in repo launcher).
|
||||||
KEY = "GIT_TRACE2_PARENT_SID"
|
KEY = "GIT_TRACE2_PARENT_SID"
|
||||||
if env is None:
|
if env is None:
|
||||||
@@ -128,10 +131,10 @@ class BaseEventLog:
|
|||||||
"time": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
"time": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def StartEvent(self):
|
def StartEvent(self, argv):
|
||||||
"""Append a 'start' event to the current log."""
|
"""Append a 'start' event to the current log."""
|
||||||
start_event = self._CreateEventDict("start")
|
start_event = self._CreateEventDict("start")
|
||||||
start_event["argv"] = sys.argv
|
start_event["argv"] = argv
|
||||||
self._log.append(start_event)
|
self._log.append(start_event)
|
||||||
|
|
||||||
def ExitEvent(self, result):
|
def ExitEvent(self, result):
|
||||||
@@ -157,9 +160,11 @@ class BaseEventLog:
|
|||||||
name: Name of the primary command (ex: repo, git)
|
name: Name of the primary command (ex: repo, git)
|
||||||
subcommands: List of the sub-commands (ex: version, init, sync)
|
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["name"] = name
|
||||||
command_event["subcommands"] = subcommands
|
command_event["hierarchy"] = name
|
||||||
self._log.append(command_event)
|
self._log.append(command_event)
|
||||||
|
|
||||||
def LogConfigEvents(self, config, event_dict_name):
|
def LogConfigEvents(self, config, event_dict_name):
|
||||||
@@ -296,6 +301,7 @@ class BaseEventLog:
|
|||||||
with socket.socket(
|
with socket.socket(
|
||||||
socket.AF_UNIX, socket.SOCK_STREAM
|
socket.AF_UNIX, socket.SOCK_STREAM
|
||||||
) as sock:
|
) as sock:
|
||||||
|
sock.settimeout(SOCK_TIMEOUT)
|
||||||
sock.connect(path)
|
sock.connect(path)
|
||||||
self._WriteLog(sock.sendall)
|
self._WriteLog(sock.sendall)
|
||||||
return f"af_unix:stream:{path}"
|
return f"af_unix:stream:{path}"
|
||||||
@@ -304,10 +310,12 @@ class BaseEventLog:
|
|||||||
# ignore the attempt and continue to DGRAM below. Otherwise,
|
# ignore the attempt and continue to DGRAM below. Otherwise,
|
||||||
# issue a warning.
|
# issue a warning.
|
||||||
if err.errno != errno.EPROTOTYPE:
|
if err.errno != errno.EPROTOTYPE:
|
||||||
print(
|
if self.verbose:
|
||||||
f"repo: warning: git trace2 logging failed: {err}",
|
print(
|
||||||
file=sys.stderr,
|
"repo: warning: git trace2 logging failed:",
|
||||||
)
|
f"{err}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
if socket_type == socket.SOCK_DGRAM or socket_type is None:
|
if socket_type == socket.SOCK_DGRAM or socket_type is None:
|
||||||
try:
|
try:
|
||||||
@@ -317,18 +325,20 @@ class BaseEventLog:
|
|||||||
self._WriteLog(lambda bs: sock.sendto(bs, path))
|
self._WriteLog(lambda bs: sock.sendto(bs, path))
|
||||||
return f"af_unix:dgram:{path}"
|
return f"af_unix:dgram:{path}"
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
print(
|
if self.verbose:
|
||||||
f"repo: warning: git trace2 logging failed: {err}",
|
print(
|
||||||
file=sys.stderr,
|
f"repo: warning: git trace2 logging failed: {err}",
|
||||||
)
|
file=sys.stderr,
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
# Tried to open a socket but couldn't connect (SOCK_STREAM) or write
|
# Tried to open a socket but couldn't connect (SOCK_STREAM) or write
|
||||||
# (SOCK_DGRAM).
|
# (SOCK_DGRAM).
|
||||||
print(
|
if self.verbose:
|
||||||
"repo: warning: git trace2 logging failed: could not write to "
|
print(
|
||||||
"socket",
|
"repo: warning: git trace2 logging failed: could not"
|
||||||
file=sys.stderr,
|
"write to socket",
|
||||||
)
|
file=sys.stderr,
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Path is an absolute path
|
# Path is an absolute path
|
||||||
@@ -343,9 +353,10 @@ class BaseEventLog:
|
|||||||
self._WriteLog(f.write)
|
self._WriteLog(f.write)
|
||||||
log_path = f.name
|
log_path = f.name
|
||||||
except FileExistsError as err:
|
except FileExistsError as err:
|
||||||
print(
|
if self.verbose:
|
||||||
"repo: warning: git trace2 logging failed: %r" % err,
|
print(
|
||||||
file=sys.stderr,
|
"repo: warning: git trace2 logging failed: %r" % err,
|
||||||
)
|
file=sys.stderr,
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
return log_path
|
return log_path
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ from error import HookError
|
|||||||
from git_refs import HEAD
|
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"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class RepoHook:
|
class RepoHook:
|
||||||
"""A RepoHook contains information about a script to run as a hook.
|
"""A RepoHook contains information about a script to run as a hook.
|
||||||
|
|
||||||
@@ -56,6 +63,7 @@ class RepoHook:
|
|||||||
hooks_project,
|
hooks_project,
|
||||||
repo_topdir,
|
repo_topdir,
|
||||||
manifest_url,
|
manifest_url,
|
||||||
|
bug_url=None,
|
||||||
bypass_hooks=False,
|
bypass_hooks=False,
|
||||||
allow_all_hooks=False,
|
allow_all_hooks=False,
|
||||||
ignore_hooks=False,
|
ignore_hooks=False,
|
||||||
@@ -75,6 +83,7 @@ class RepoHook:
|
|||||||
run with CWD as this directory.
|
run with CWD as this directory.
|
||||||
If you have a manifest, this is manifest.topdir.
|
If you have a manifest, this is manifest.topdir.
|
||||||
manifest_url: The URL to the manifest git repo.
|
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'.
|
bypass_hooks: If True, then 'Do not run the hook'.
|
||||||
allow_all_hooks: If True, then 'Run the hook without prompting'.
|
allow_all_hooks: If True, then 'Run the hook without prompting'.
|
||||||
ignore_hooks: If True, then 'Do not abort action if hooks fail'.
|
ignore_hooks: If True, then 'Do not abort action if hooks fail'.
|
||||||
@@ -85,18 +94,18 @@ class RepoHook:
|
|||||||
self._hooks_project = hooks_project
|
self._hooks_project = hooks_project
|
||||||
self._repo_topdir = repo_topdir
|
self._repo_topdir = repo_topdir
|
||||||
self._manifest_url = manifest_url
|
self._manifest_url = manifest_url
|
||||||
|
self._bug_url = bug_url
|
||||||
self._bypass_hooks = bypass_hooks
|
self._bypass_hooks = bypass_hooks
|
||||||
self._allow_all_hooks = allow_all_hooks
|
self._allow_all_hooks = allow_all_hooks
|
||||||
self._ignore_hooks = ignore_hooks
|
self._ignore_hooks = ignore_hooks
|
||||||
self._abort_if_user_denies = abort_if_user_denies
|
self._abort_if_user_denies = abort_if_user_denies
|
||||||
|
|
||||||
# Store the full path to the script for convenience.
|
# 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._script_fullpath = os.path.join(
|
||||||
self._hooks_project.worktree, self._hook_type + ".py"
|
self._hooks_project.worktree, self._hook_type + ".py"
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
self._script_fullpath = None
|
|
||||||
|
|
||||||
def _GetHash(self):
|
def _GetHash(self):
|
||||||
"""Return a hash of the contents of the hooks directory.
|
"""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
|
ignore the result through the option combinations as listed in
|
||||||
AddHookOptionGroup().
|
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
|
# Do not do anything in case bypass_hooks is set, or
|
||||||
# no-op if there is no hooks project or if hook is disabled.
|
# no-op if there is no hooks project or if hook is disabled.
|
||||||
if (
|
if (
|
||||||
self._bypass_hooks
|
self._bypass_hooks
|
||||||
or not self._hooks_project
|
or not self._hooks_project
|
||||||
|
or not self._script_fullpath
|
||||||
or self._hook_type not in self._hooks_project.enabled_repo_hooks
|
or self._hook_type not in self._hooks_project.enabled_repo_hooks
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
@@ -472,6 +496,7 @@ class RepoHook:
|
|||||||
"manifest_url": manifest.manifestProject.GetRemote(
|
"manifest_url": manifest.manifestProject.GetRemote(
|
||||||
"origin"
|
"origin"
|
||||||
).url,
|
).url,
|
||||||
|
"bug_url": manifest.contactinfo.bugurl,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return cls(*args, **kwargs)
|
return cls(*args, **kwargs)
|
||||||
|
|||||||
+5
-3
@@ -1,5 +1,8 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# From Gerrit Code Review 3.10.0 d5403dbf335ba7d48977fc95170c3f7027c34659
|
# DO NOT EDIT THIS FILE
|
||||||
|
# All updates should be sent upstream: https://gerrit.googlesource.com/gerrit/
|
||||||
|
# This is synced from commit: 62f5bbea67f6dafa6e22a601a0c298214c510caf
|
||||||
|
# DO NOT EDIT THIS FILE
|
||||||
#
|
#
|
||||||
# Part of Gerrit Code Review (https://www.gerritcodereview.com/)
|
# Part of Gerrit Code Review (https://www.gerritcodereview.com/)
|
||||||
#
|
#
|
||||||
@@ -31,8 +34,7 @@ if test ! -f "$1" ; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Do not create a change id if requested
|
# Do not create a change id if requested
|
||||||
create_setting=$(git config --get gerrit.createChangeId)
|
case "$(git config --get gerrit.createChangeId)" in
|
||||||
case "$create_setting" in
|
|
||||||
false)
|
false)
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
|
|||||||
+17
-30
@@ -1,33 +1,25 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
# DO NOT EDIT THIS FILE
|
||||||
|
# All updates should be sent upstream: https://github.com/git/git
|
||||||
|
# This is synced from commit: 00e10ef10e161a913893b8cb33aa080d4ca5baa6
|
||||||
|
# DO NOT EDIT THIS FILE
|
||||||
#
|
#
|
||||||
# An example hook script to verify if you are on battery, in case you
|
# An example hook script to verify if you are on battery, in case you
|
||||||
# are running Windows, Linux or OS X. Called by git-gc --auto with no
|
# are running Linux or OS X. Called by git-gc --auto with no arguments.
|
||||||
# arguments. The hook should exit with non-zero status after issuing an
|
# The hook should exit with non-zero status after issuing an appropriate
|
||||||
# appropriate message if it wants to stop the auto repacking.
|
# message if it wants to stop the auto repacking.
|
||||||
|
|
||||||
# This program is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 2 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
#
|
||||||
# This program is distributed in the hope that it will be useful,
|
# This hook is stored in the contrib/hooks directory. Your distribution
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# may have put this somewhere else. If you want to use this hook, you
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# should make this script executable then link to it in the repository
|
||||||
# GNU General Public License for more details.
|
# you would like to use it in.
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU General Public License
|
# For example, if the hook is stored in
|
||||||
# along with this program; if not, write to the Free Software
|
# /usr/share/git-core/contrib/hooks/pre-auto-gc-battery:
|
||||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
#
|
||||||
|
# cd /path/to/your/repository.git
|
||||||
if uname -s | grep -q "_NT-"
|
# ln -sf /usr/share/git-core/contrib/hooks/pre-auto-gc-battery \
|
||||||
then
|
# hooks/pre-auto-gc
|
||||||
if test -x $SYSTEMROOT/System32/Wbem/wmic
|
|
||||||
then
|
|
||||||
STATUS=$(wmic path win32_battery get batterystatus /format:list | tr -d '\r\n')
|
|
||||||
[ "$STATUS" = "BatteryStatus=2" ] && exit 0 || exit 1
|
|
||||||
fi
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if test -x /sbin/on_ac_power && (/sbin/on_ac_power;test $? -ne 1)
|
if test -x /sbin/on_ac_power && (/sbin/on_ac_power;test $? -ne 1)
|
||||||
then
|
then
|
||||||
@@ -48,11 +40,6 @@ elif test -x /usr/bin/pmset && /usr/bin/pmset -g batt |
|
|||||||
grep -q "drawing from 'AC Power'"
|
grep -q "drawing from 'AC Power'"
|
||||||
then
|
then
|
||||||
exit 0
|
exit 0
|
||||||
elif test -d /sys/bus/acpi/drivers/battery && test 0 = \
|
|
||||||
"$(find /sys/bus/acpi/drivers/battery/ -type l | wc -l)";
|
|
||||||
then
|
|
||||||
# No battery exists.
|
|
||||||
exit 0
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Auto packing deferred; not on AC"
|
echo "Auto packing deferred; not on AC"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
#
|
|
||||||
# Copyright (C) 2008 The Android Open Source Project
|
# Copyright (C) 2008 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");
|
||||||
@@ -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.
|
which takes care of execing this entry point.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import difflib
|
||||||
import getpass
|
import getpass
|
||||||
import json
|
import json
|
||||||
import netrc
|
import netrc
|
||||||
@@ -30,6 +30,7 @@ import signal
|
|||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
import time
|
import time
|
||||||
|
from typing import Optional
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
from repo_logging import RepoLogger
|
from repo_logging import RepoLogger
|
||||||
@@ -45,7 +46,6 @@ from command import InteractiveCommand
|
|||||||
from command import MirrorSafeCommand
|
from command import MirrorSafeCommand
|
||||||
from editor import Editor
|
from editor import Editor
|
||||||
from error import DownloadError
|
from error import DownloadError
|
||||||
from error import GitcUnsupportedError
|
|
||||||
from error import InvalidProjectGroupsError
|
from error import InvalidProjectGroupsError
|
||||||
from error import ManifestInvalidRevisionError
|
from error import ManifestInvalidRevisionError
|
||||||
from error import ManifestParseError
|
from error import ManifestParseError
|
||||||
@@ -270,10 +270,14 @@ class _Repo:
|
|||||||
self._PrintHelp(short=True)
|
self._PrintHelp(short=True)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
run = lambda: self._RunLong(name, gopts, argv) or 0
|
git_trace2_event_log = EventLog()
|
||||||
|
run = (
|
||||||
|
lambda: self._RunLong(name, gopts, argv, git_trace2_event_log) or 0
|
||||||
|
)
|
||||||
with Trace(
|
with Trace(
|
||||||
"starting new command: %s",
|
"starting new command: %s [sid=%s]",
|
||||||
", ".join([name] + argv),
|
", ".join([name] + argv),
|
||||||
|
git_trace2_event_log.full_sid,
|
||||||
first_trace=True,
|
first_trace=True,
|
||||||
):
|
):
|
||||||
if gopts.trace_python:
|
if gopts.trace_python:
|
||||||
@@ -290,12 +294,107 @@ class _Repo:
|
|||||||
result = run()
|
result = run()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _RunLong(self, name, gopts, argv):
|
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."""
|
"""Execute the (longer running) requested subcommand."""
|
||||||
result = 0
|
result = 0
|
||||||
SetDefaultColoring(gopts.color)
|
SetDefaultColoring(gopts.color)
|
||||||
|
|
||||||
git_trace2_event_log = EventLog()
|
|
||||||
outer_client = RepoClient(self.repodir)
|
outer_client = RepoClient(self.repodir)
|
||||||
repo_client = outer_client
|
repo_client = outer_client
|
||||||
if gopts.submanifest_path:
|
if gopts.submanifest_path:
|
||||||
@@ -305,24 +404,22 @@ class _Repo:
|
|||||||
outer_client=outer_client,
|
outer_client=outer_client,
|
||||||
)
|
)
|
||||||
|
|
||||||
if Wrapper().gitc_parse_clientdir(os.getcwd()):
|
if name not in self.commands:
|
||||||
logger.error("GITC is not supported.")
|
corrected_name = self._autocorrect_command_name(
|
||||||
raise GitcUnsupportedError()
|
name, outer_client.globalConfig
|
||||||
|
)
|
||||||
|
if not corrected_name:
|
||||||
|
return 1
|
||||||
|
name = corrected_name
|
||||||
|
|
||||||
try:
|
cmd = self.commands[name](
|
||||||
cmd = self.commands[name](
|
repodir=self.repodir,
|
||||||
repodir=self.repodir,
|
client=repo_client,
|
||||||
client=repo_client,
|
manifest=repo_client.manifest,
|
||||||
manifest=repo_client.manifest,
|
outer_client=outer_client,
|
||||||
outer_client=outer_client,
|
outer_manifest=outer_client.manifest,
|
||||||
outer_manifest=outer_client.manifest,
|
git_event_log=git_trace2_event_log,
|
||||||
git_event_log=git_trace2_event_log,
|
)
|
||||||
)
|
|
||||||
except KeyError:
|
|
||||||
logger.error(
|
|
||||||
"repo: '%s' is not a repo command. See 'repo help'.", name
|
|
||||||
)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
Editor.globalConfig = cmd.client.globalConfig
|
Editor.globalConfig = cmd.client.globalConfig
|
||||||
|
|
||||||
@@ -340,6 +437,9 @@ class _Repo:
|
|||||||
)
|
)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
cmd.CommonValidateOptions(copts, cargs)
|
||||||
|
git_trace2_event_log.verbose = copts.verbose
|
||||||
|
|
||||||
if gopts.pager is not False and not isinstance(cmd, InteractiveCommand):
|
if gopts.pager is not False and not isinstance(cmd, InteractiveCommand):
|
||||||
config = cmd.client.globalConfig
|
config = cmd.client.globalConfig
|
||||||
if gopts.pager:
|
if gopts.pager:
|
||||||
@@ -354,7 +454,7 @@ class _Repo:
|
|||||||
start = time.time()
|
start = time.time()
|
||||||
cmd_event = cmd.event_log.Add(name, event_log.TASK_COMMAND, start)
|
cmd_event = cmd.event_log.Add(name, event_log.TASK_COMMAND, start)
|
||||||
cmd.event_log.SetParent(cmd_event)
|
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])
|
git_trace2_event_log.CommandEvent(name="repo", subcommands=[name])
|
||||||
|
|
||||||
def execute_command_helper():
|
def execute_command_helper():
|
||||||
@@ -362,7 +462,6 @@ class _Repo:
|
|||||||
Execute the subcommand.
|
Execute the subcommand.
|
||||||
"""
|
"""
|
||||||
nonlocal result
|
nonlocal result
|
||||||
cmd.CommonValidateOptions(copts, cargs)
|
|
||||||
cmd.ValidateOptions(copts, cargs)
|
cmd.ValidateOptions(copts, cargs)
|
||||||
|
|
||||||
this_manifest_only = copts.this_manifest_only
|
this_manifest_only = copts.this_manifest_only
|
||||||
@@ -422,7 +521,7 @@ class _Repo:
|
|||||||
error_info = json.dumps(
|
error_info = json.dumps(
|
||||||
{
|
{
|
||||||
"ErrorType": type(error).__name__,
|
"ErrorType": type(error).__name__,
|
||||||
"Project": project,
|
"Project": str(project),
|
||||||
"Message": str(error),
|
"Message": str(error),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "July 2022" "repo gitc-delete" "Repo Manual"
|
.TH REPO "1" "April 2025" "repo gc" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo gitc-delete - manual page for repo gitc-delete
|
repo \- repo gc - manual page for repo gc
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
.B repo
|
.B repo
|
||||||
\fI\,gitc-delete\/\fR
|
\fI\,gc\/\fR
|
||||||
.SH DESCRIPTION
|
.SH DESCRIPTION
|
||||||
Summary
|
Summary
|
||||||
.PP
|
.PP
|
||||||
Delete a GITC Client.
|
Cleaning up internal repo and Git state.
|
||||||
.SH OPTIONS
|
.SH OPTIONS
|
||||||
.TP
|
.TP
|
||||||
\fB\-h\fR, \fB\-\-help\fR
|
\fB\-h\fR, \fB\-\-help\fR
|
||||||
show this help message and exit
|
show this help message and exit
|
||||||
.TP
|
.TP
|
||||||
\fB\-f\fR, \fB\-\-force\fR
|
\fB\-n\fR, \fB\-\-dry\-run\fR
|
||||||
force the deletion (no prompt)
|
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:
|
.SS Logging options:
|
||||||
.TP
|
.TP
|
||||||
\fB\-v\fR, \fB\-\-verbose\fR
|
\fB\-v\fR, \fB\-\-verbose\fR
|
||||||
@@ -37,8 +44,4 @@ only operate on this (sub)manifest
|
|||||||
\fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
|
\fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
|
||||||
operate on this manifest and its submanifests
|
operate on this manifest and its submanifests
|
||||||
.PP
|
.PP
|
||||||
Run `repo help gitc\-delete` to view the detailed manual.
|
Run `repo help gc` to view the detailed manual.
|
||||||
.SH DETAILS
|
|
||||||
.PP
|
|
||||||
This subcommand deletes the current GITC client, deleting the GITC manifest and
|
|
||||||
all locally downloaded sources.
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
|
||||||
.TH REPO "1" "October 2022" "repo gitc-init" "Repo Manual"
|
|
||||||
.SH NAME
|
|
||||||
repo \- repo gitc-init - manual page for repo gitc-init
|
|
||||||
.SH SYNOPSIS
|
|
||||||
.B repo
|
|
||||||
\fI\,gitc-init \/\fR[\fI\,options\/\fR] [\fI\,client name\/\fR]
|
|
||||||
.SH DESCRIPTION
|
|
||||||
Summary
|
|
||||||
.PP
|
|
||||||
Initialize a GITC Client.
|
|
||||||
.SH OPTIONS
|
|
||||||
.TP
|
|
||||||
\fB\-h\fR, \fB\-\-help\fR
|
|
||||||
show this help message and exit
|
|
||||||
.SS Logging options:
|
|
||||||
.TP
|
|
||||||
\fB\-v\fR, \fB\-\-verbose\fR
|
|
||||||
show all output
|
|
||||||
.TP
|
|
||||||
\fB\-q\fR, \fB\-\-quiet\fR
|
|
||||||
only show errors
|
|
||||||
.SS Manifest options:
|
|
||||||
.TP
|
|
||||||
\fB\-u\fR URL, \fB\-\-manifest\-url\fR=\fI\,URL\/\fR
|
|
||||||
manifest repository location
|
|
||||||
.TP
|
|
||||||
\fB\-b\fR REVISION, \fB\-\-manifest\-branch\fR=\fI\,REVISION\/\fR
|
|
||||||
manifest branch or revision (use HEAD for default)
|
|
||||||
.TP
|
|
||||||
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
|
|
||||||
initial manifest file
|
|
||||||
.TP
|
|
||||||
\fB\-g\fR GROUP, \fB\-\-groups\fR=\fI\,GROUP\/\fR
|
|
||||||
restrict manifest projects to ones with specified
|
|
||||||
group(s) [default|all|G1,G2,G3|G4,\-G5,\-G6]
|
|
||||||
.TP
|
|
||||||
\fB\-p\fR PLATFORM, \fB\-\-platform\fR=\fI\,PLATFORM\/\fR
|
|
||||||
restrict manifest projects to ones with a specified
|
|
||||||
platform group [auto|all|none|linux|darwin|...]
|
|
||||||
.TP
|
|
||||||
\fB\-\-submodules\fR
|
|
||||||
sync any submodules associated with the manifest repo
|
|
||||||
.TP
|
|
||||||
\fB\-\-standalone\-manifest\fR
|
|
||||||
download the manifest as a static file rather then
|
|
||||||
create a git checkout of the manifest repo
|
|
||||||
.TP
|
|
||||||
\fB\-\-manifest\-depth\fR=\fI\,DEPTH\/\fR
|
|
||||||
create a shallow clone of the manifest repo with given
|
|
||||||
depth (0 for full clone); see git clone (default: 0)
|
|
||||||
.SS Manifest (only) checkout options:
|
|
||||||
.TP
|
|
||||||
\fB\-\-current\-branch\fR
|
|
||||||
fetch only current manifest branch from server
|
|
||||||
(default)
|
|
||||||
.TP
|
|
||||||
\fB\-\-no\-current\-branch\fR
|
|
||||||
fetch all manifest branches from server
|
|
||||||
.TP
|
|
||||||
\fB\-\-tags\fR
|
|
||||||
fetch tags in the manifest
|
|
||||||
.TP
|
|
||||||
\fB\-\-no\-tags\fR
|
|
||||||
don't fetch tags in the manifest
|
|
||||||
.SS Checkout modes:
|
|
||||||
.TP
|
|
||||||
\fB\-\-mirror\fR
|
|
||||||
create a replica of the remote repositories rather
|
|
||||||
than a client working directory
|
|
||||||
.TP
|
|
||||||
\fB\-\-archive\fR
|
|
||||||
checkout an archive instead of a git repository for
|
|
||||||
each project. See git archive.
|
|
||||||
.TP
|
|
||||||
\fB\-\-worktree\fR
|
|
||||||
use git\-worktree to manage projects
|
|
||||||
.SS Project checkout optimizations:
|
|
||||||
.TP
|
|
||||||
\fB\-\-reference\fR=\fI\,DIR\/\fR
|
|
||||||
location of mirror directory
|
|
||||||
.TP
|
|
||||||
\fB\-\-dissociate\fR
|
|
||||||
dissociate from reference mirrors after clone
|
|
||||||
.TP
|
|
||||||
\fB\-\-depth\fR=\fI\,DEPTH\/\fR
|
|
||||||
create a shallow clone with given depth; see git clone
|
|
||||||
.TP
|
|
||||||
\fB\-\-partial\-clone\fR
|
|
||||||
perform partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
|
|
||||||
.TP
|
|
||||||
\fB\-\-no\-partial\-clone\fR
|
|
||||||
disable use of partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
|
|
||||||
.TP
|
|
||||||
\fB\-\-partial\-clone\-exclude\fR=\fI\,PARTIAL_CLONE_EXCLUDE\/\fR
|
|
||||||
exclude the specified projects (a comma\-delimited
|
|
||||||
project names) from partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
|
|
||||||
.TP
|
|
||||||
\fB\-\-clone\-filter\fR=\fI\,CLONE_FILTER\/\fR
|
|
||||||
filter for use with \fB\-\-partial\-clone\fR [default:
|
|
||||||
blob:none]
|
|
||||||
.TP
|
|
||||||
\fB\-\-use\-superproject\fR
|
|
||||||
use the manifest superproject to sync projects;
|
|
||||||
implies \fB\-c\fR
|
|
||||||
.TP
|
|
||||||
\fB\-\-no\-use\-superproject\fR
|
|
||||||
disable use of manifest superprojects
|
|
||||||
.TP
|
|
||||||
\fB\-\-clone\-bundle\fR
|
|
||||||
enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if
|
|
||||||
not \fB\-\-partial\-clone\fR)
|
|
||||||
.TP
|
|
||||||
\fB\-\-no\-clone\-bundle\fR
|
|
||||||
disable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if
|
|
||||||
\fB\-\-partial\-clone\fR)
|
|
||||||
.TP
|
|
||||||
\fB\-\-git\-lfs\fR
|
|
||||||
enable Git LFS support
|
|
||||||
.TP
|
|
||||||
\fB\-\-no\-git\-lfs\fR
|
|
||||||
disable Git LFS support
|
|
||||||
.SS repo Version options:
|
|
||||||
.TP
|
|
||||||
\fB\-\-repo\-url\fR=\fI\,URL\/\fR
|
|
||||||
repo repository location ($REPO_URL)
|
|
||||||
.TP
|
|
||||||
\fB\-\-repo\-rev\fR=\fI\,REV\/\fR
|
|
||||||
repo branch or revision ($REPO_REV)
|
|
||||||
.TP
|
|
||||||
\fB\-\-no\-repo\-verify\fR
|
|
||||||
do not verify repo source code
|
|
||||||
.SS Other options:
|
|
||||||
.TP
|
|
||||||
\fB\-\-config\-name\fR
|
|
||||||
Always prompt for name/e\-mail
|
|
||||||
.SS GITC options:
|
|
||||||
.TP
|
|
||||||
\fB\-f\fR MANIFEST_FILE, \fB\-\-manifest\-file\fR=\fI\,MANIFEST_FILE\/\fR
|
|
||||||
Optional manifest file to use for this GITC client.
|
|
||||||
.TP
|
|
||||||
\fB\-c\fR GITC_CLIENT, \fB\-\-gitc\-client\fR=\fI\,GITC_CLIENT\/\fR
|
|
||||||
Name of the gitc_client instance to create or modify.
|
|
||||||
.SS Multi\-manifest:
|
|
||||||
.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 gitc\-init` to view the detailed manual.
|
|
||||||
.SH DETAILS
|
|
||||||
.PP
|
|
||||||
The 'repo gitc\-init' command is ran to initialize a new GITC client for use with
|
|
||||||
the GITC file system.
|
|
||||||
.PP
|
|
||||||
This command will setup the client directory, initialize repo, just like repo
|
|
||||||
init does, and then downloads the manifest collection and installs it in the
|
|
||||||
\&.repo/directory of the GITC client.
|
|
||||||
.PP
|
|
||||||
Once this is done, a GITC manifest is generated by pulling the HEAD SHA for each
|
|
||||||
project and generates the properly formatted XML file and installs it as
|
|
||||||
\&.manifest in the GITC client directory.
|
|
||||||
.PP
|
|
||||||
The \fB\-c\fR argument is required to specify the GITC client name.
|
|
||||||
.PP
|
|
||||||
The optional \fB\-f\fR argument can be used to specify the manifest file to use for
|
|
||||||
this GITC client.
|
|
||||||
+10
-1
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "October 2022" "repo init" "Repo Manual"
|
.TH REPO "1" "September 2024" "repo init" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo init - manual page for repo init
|
repo \- repo init - manual page for repo init
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -28,6 +28,11 @@ manifest repository location
|
|||||||
\fB\-b\fR REVISION, \fB\-\-manifest\-branch\fR=\fI\,REVISION\/\fR
|
\fB\-b\fR REVISION, \fB\-\-manifest\-branch\fR=\fI\,REVISION\/\fR
|
||||||
manifest branch or revision (use HEAD for default)
|
manifest branch or revision (use HEAD for default)
|
||||||
.TP
|
.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
|
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
|
||||||
initial manifest file
|
initial manifest file
|
||||||
.TP
|
.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
|
and use. If no branch is specified, the remote's default branch is used. This is
|
||||||
equivalent to using \fB\-b\fR HEAD.
|
equivalent to using \fB\-b\fR HEAD.
|
||||||
.PP
|
.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
|
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.
|
used. If no manifest is specified, the manifest default.xml will be used.
|
||||||
.PP
|
.PP
|
||||||
|
|||||||
+155
-66
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "October 2022" "repo manifest" "Repo Manual"
|
.TH REPO "1" "April 2026" "repo manifest" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo manifest - manual page for repo manifest
|
repo \- repo manifest - manual page for repo manifest
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -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
|
(only of use if the branch names for a sha1 manifest
|
||||||
are sensitive)
|
are sensitive)
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-json\fR
|
\fB\-\-format\fR=\fI\,FORMAT\/\fR
|
||||||
output manifest in JSON format (experimental)
|
output format: xml, json (default: xml)
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-pretty\fR
|
\fB\-\-pretty\fR
|
||||||
format output for humans to read
|
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
|
attribute is set to indicate the remote ref to push changes to via 'repo
|
||||||
upload'.
|
upload'.
|
||||||
.PP
|
.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
|
repo Manifest Format
|
||||||
.PP
|
.PP
|
||||||
A repo manifest describes the structure of a repo client; that is the
|
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 dest\-branch CDATA #IMPLIED>
|
||||||
<!ATTLIST default upstream CDATA #IMPLIED>
|
<!ATTLIST default upstream CDATA #IMPLIED>
|
||||||
<!ATTLIST default sync\-j CDATA #IMPLIED>
|
<!ATTLIST default sync\-j CDATA #IMPLIED>
|
||||||
|
<!ATTLIST default sync\-j\-max CDATA #IMPLIED>
|
||||||
<!ATTLIST default sync\-c CDATA #IMPLIED>
|
<!ATTLIST default sync\-c CDATA #IMPLIED>
|
||||||
<!ATTLIST default sync\-s CDATA #IMPLIED>
|
<!ATTLIST default sync\-s CDATA #IMPLIED>
|
||||||
<!ATTLIST default sync\-tags CDATA #IMPLIED>
|
<!ATTLIST default sync\-tags CDATA #IMPLIED>
|
||||||
@@ -135,7 +140,7 @@ include*)>
|
|||||||
<!ATTLIST manifest\-server url CDATA #REQUIRED>
|
<!ATTLIST manifest\-server url CDATA #REQUIRED>
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT submanifest EMPTY>
|
<!ELEMENT submanifest EMPTY>
|
||||||
<!ATTLIST submanifest name ID #REQUIRED>
|
<!ATTLIST submanifest name ID #REQUIRED>
|
||||||
<!ATTLIST submanifest remote IDREF #IMPLIED>
|
<!ATTLIST submanifest remote IDREF #IMPLIED>
|
||||||
<!ATTLIST submanifest project CDATA #IMPLIED>
|
<!ATTLIST submanifest project CDATA #IMPLIED>
|
||||||
<!ATTLIST submanifest manifest\-name CDATA #IMPLIED>
|
<!ATTLIST submanifest manifest\-name CDATA #IMPLIED>
|
||||||
@@ -160,15 +165,32 @@ IDREF #IMPLIED>
|
|||||||
.TP
|
.TP
|
||||||
<!ATTLIST project revision
|
<!ATTLIST project revision
|
||||||
CDATA #IMPLIED>
|
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
|
.IP
|
||||||
<!ATTLIST project dest\-branch CDATA #IMPLIED>
|
<!ATTLIST project sync\-strategy 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>
|
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT annotation EMPTY>
|
<!ELEMENT annotation EMPTY>
|
||||||
<!ATTLIST annotation name CDATA #REQUIRED>
|
<!ATTLIST annotation name CDATA #REQUIRED>
|
||||||
@@ -180,25 +202,43 @@ CDATA #IMPLIED>
|
|||||||
<!ATTLIST copyfile dest CDATA #REQUIRED>
|
<!ATTLIST copyfile dest CDATA #REQUIRED>
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT linkfile EMPTY>
|
<!ELEMENT linkfile EMPTY>
|
||||||
<!ATTLIST linkfile src CDATA #REQUIRED>
|
<!ATTLIST linkfile src CDATA #REQUIRED>
|
||||||
<!ATTLIST linkfile dest CDATA #REQUIRED>
|
<!ATTLIST linkfile dest CDATA #REQUIRED>
|
||||||
|
.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
|
.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 dest\-branch CDATA #IMPLIED>
|
||||||
<!ATTLIST extend\-project upstream CDATA #IMPLIED>
|
<!ATTLIST extend\-project upstream CDATA #IMPLIED>
|
||||||
|
<!ATTLIST extend\-project base\-rev CDATA #IMPLIED>
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT remove\-project EMPTY>
|
<!ELEMENT remove\-project EMPTY>
|
||||||
<!ATTLIST remove\-project name CDATA #REQUIRED>
|
<!ATTLIST remove\-project name CDATA #IMPLIED>
|
||||||
<!ATTLIST remove\-project optional CDATA #IMPLIED>
|
<!ATTLIST remove\-project path CDATA #IMPLIED>
|
||||||
|
<!ATTLIST remove\-project optional CDATA #IMPLIED>
|
||||||
|
<!ATTLIST remove\-project base\-rev CDATA #IMPLIED>
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT repo\-hooks EMPTY>
|
<!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>
|
<!ATTLIST repo\-hooks enabled\-list CDATA #REQUIRED>
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT superproject EMPTY>
|
<!ELEMENT superproject EMPTY>
|
||||||
@@ -207,11 +247,12 @@ CDATA #IMPLIED>
|
|||||||
<!ATTLIST superproject revision CDATA #IMPLIED>
|
<!ATTLIST superproject revision CDATA #IMPLIED>
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT contactinfo EMPTY>
|
<!ELEMENT contactinfo EMPTY>
|
||||||
<!ATTLIST contactinfo bugurl CDATA #REQUIRED>
|
<!ATTLIST contactinfo bugurl CDATA #REQUIRED>
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT include EMPTY>
|
<!ELEMENT include EMPTY>
|
||||||
<!ATTLIST include name CDATA #REQUIRED>
|
<!ATTLIST include name CDATA #REQUIRED>
|
||||||
<!ATTLIST include groups CDATA #IMPLIED>
|
<!ATTLIST include groups CDATA #IMPLIED>
|
||||||
|
<!ATTLIST include revision CDATA #IMPLIED>
|
||||||
.PP
|
.PP
|
||||||
]>
|
]>
|
||||||
```
|
```
|
||||||
@@ -286,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
|
entire ref space. Project elements not setting their own `upstream` will inherit
|
||||||
this value.
|
this value.
|
||||||
.PP
|
.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
|
.PP
|
||||||
Attribute `sync\-c`: Set to true to only sync the given Git branch (specified in
|
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
|
the `revision` attribute) rather than the whole ref space. Project elements
|
||||||
@@ -302,25 +345,7 @@ Element manifest\-server
|
|||||||
At most one manifest\-server may be specified. The url attribute is used to
|
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.
|
specify the URL of a manifest server, which is an XML RPC service.
|
||||||
.PP
|
.PP
|
||||||
The manifest server should implement the following RPC methods:
|
See the [smart sync documentation](./smart\-sync.md) for more details.
|
||||||
.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.
|
|
||||||
.PP
|
.PP
|
||||||
Element submanifest
|
Element submanifest
|
||||||
.PP
|
.PP
|
||||||
@@ -372,7 +397,7 @@ supplied, `revision` is used.
|
|||||||
.PP
|
.PP
|
||||||
`path` may not be an absolute path or use "." or ".." path components.
|
`path` may not be an absolute path or use "." or ".." path components.
|
||||||
.PP
|
.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
|
included submanifest belong. This appends and recurses, meaning all projects in
|
||||||
submanifests carry all parent submanifest groups. Same syntax as the
|
submanifests carry all parent submanifest groups. Same syntax as the
|
||||||
corresponding element of `project`.
|
corresponding element of `project`.
|
||||||
@@ -434,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
|
upload`, changes will be submitted for code review on this branch. If
|
||||||
unspecified both here and in the default element, `revision` is used instead.
|
unspecified both here and in the default element, `revision` is used instead.
|
||||||
.PP
|
.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
|
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
|
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
|
`<project name="monkeys" path="barrel\-of"/>`, that project definition is
|
||||||
@@ -461,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,
|
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.
|
it will be ignored when syncing the projects in a client working directory.
|
||||||
.PP
|
.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
|
Element extend\-project
|
||||||
.PP
|
.PP
|
||||||
Modify the attributes of the named project.
|
Modify the attributes of the named project.
|
||||||
@@ -470,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
|
definition. This makes the local manifest more robust against changes to the
|
||||||
original manifest.
|
original manifest.
|
||||||
.PP
|
.PP
|
||||||
|
The `extend\-project` element can also contain `annotation`, `copyfile`, and
|
||||||
|
`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
|
Attribute `path`: If specified, limit the change to projects checked out at the
|
||||||
specified path, rather than all projects with the given name.
|
specified path, rather than all projects with the given name.
|
||||||
.PP
|
.PP
|
||||||
@@ -478,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`
|
This is used to move a project in the checkout by overriding the existing `path`
|
||||||
setting.
|
setting.
|
||||||
.PP
|
.PP
|
||||||
Attribute `groups`: List of additional groups to which this project belongs.
|
Attribute `groups`: Set of additional groups to which this project belongs. Same
|
||||||
Same syntax as the corresponding element of `project`.
|
syntax as the corresponding element of `project`.
|
||||||
.PP
|
.PP
|
||||||
Attribute `revision`: If specified, overrides the revision of the original
|
Attribute `revision`: If specified, overrides the revision of the original
|
||||||
project. Same syntax as the corresponding element of `project`.
|
project. Same syntax as the corresponding element of `project`.
|
||||||
@@ -493,21 +538,31 @@ project. Same syntax as the corresponding element of `project`.
|
|||||||
Attribute `upstream`: If specified, overrides the upstream of the original
|
Attribute `upstream`: If specified, overrides the upstream of the original
|
||||||
project. Same syntax as the corresponding element of `project`.
|
project. Same syntax as the corresponding element of `project`.
|
||||||
.PP
|
.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
|
Element annotation
|
||||||
.PP
|
.PP
|
||||||
Zero or more annotation elements may be specified as children of a project or
|
Zero or more annotation elements may be specified as children of a project
|
||||||
remote element. Each element describes a name\-value pair. For projects, this
|
element, an extend\-project element, or a remote element. Each element describes
|
||||||
name\-value pair will be exported into each project's environment during a
|
a name\-value pair. For projects, this name\-value pair will be exported into each
|
||||||
\&'forall' command, prefixed with `REPO__`. In addition, there is an optional
|
project's environment during a 'forall' command, prefixed with `REPO__`. In
|
||||||
attribute "keep" which accepts the case insensitive values "true" (default) or
|
addition, there is an optional attribute "keep" which accepts the case
|
||||||
"false". This attribute determines whether or not the annotation will be kept
|
insensitive values "true" (default) or "false". This attribute determines
|
||||||
when exported with the manifest subcommand.
|
whether or not the annotation will be kept when exported with the manifest
|
||||||
|
subcommand.
|
||||||
.PP
|
.PP
|
||||||
Element copyfile
|
Element copyfile
|
||||||
.PP
|
.PP
|
||||||
Zero or more copyfile elements may be specified as children of a project
|
Zero or more copyfile elements may be specified as children of a project
|
||||||
element. Each element describes a src\-dest pair of files; the "src" file will be
|
element, or an extend\-project element. Each element describes a src\-dest pair of
|
||||||
copied to the "dest" place during `repo sync` command.
|
files; the "src" file will be copied to the "dest" place during `repo sync`
|
||||||
|
command.
|
||||||
.PP
|
.PP
|
||||||
"src" is project relative, "dest" is relative to the top of the tree. Copying
|
"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
|
from paths outside of the project or to paths outside of the repo client is not
|
||||||
@@ -518,10 +573,14 @@ Intermediate paths must not be symlinks either.
|
|||||||
.PP
|
.PP
|
||||||
Parent directories of "dest" will be automatically created if missing.
|
Parent directories of "dest" will be automatically created if missing.
|
||||||
.PP
|
.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
|
Element linkfile
|
||||||
.PP
|
.PP
|
||||||
It's just like copyfile and runs at the same time as copyfile but instead of
|
It's just like copyfile, but instead of copying it creates a symlink.
|
||||||
copying it creates a symlink.
|
|
||||||
.PP
|
.PP
|
||||||
The symlink is created at "dest" (relative to the top of the tree) and points to
|
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.
|
the path specified by "src" which is a path in the project.
|
||||||
@@ -531,18 +590,42 @@ 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 symlink target may be a file or directory, but it may not point outside of
|
||||||
the repo client.
|
the repo client.
|
||||||
.PP
|
.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
|
Element remove\-project
|
||||||
.PP
|
.PP
|
||||||
Deletes the named project from the internal manifest table, possibly allowing a
|
Deletes a project from the internal manifest table, possibly allowing a
|
||||||
subsequent project element in the same manifest file to replace the project with
|
subsequent project element in the same manifest file to replace the project with
|
||||||
a different source.
|
a different source.
|
||||||
.PP
|
.PP
|
||||||
This element is mostly useful in a local manifest file, where the user can
|
This element is mostly useful in a local manifest file, where the user can
|
||||||
remove a project, and possibly replace it with their own definition.
|
remove a project, and possibly replace it with their own definition.
|
||||||
.PP
|
.PP
|
||||||
|
The project `name` or project `path` can be used to specify the remove target
|
||||||
|
meaning one of them is required. If only name is specified, all projects with
|
||||||
|
that name are removed.
|
||||||
|
.PP
|
||||||
|
If both name and path are specified, only projects with the same name and path
|
||||||
|
are removed, meaning projects with the same name but in other locations are
|
||||||
|
kept.
|
||||||
|
.PP
|
||||||
|
If only path is specified, a matching project is removed regardless of its name.
|
||||||
|
Logic otherwise behaves like both are specified.
|
||||||
|
.PP
|
||||||
Attribute `optional`: Set to true to ignore remove\-project elements with no
|
Attribute `optional`: Set to true to ignore remove\-project elements with no
|
||||||
matching `project` element.
|
matching `project` element.
|
||||||
.PP
|
.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
|
Element repo\-hooks
|
||||||
.PP
|
.PP
|
||||||
NB: See the [practical documentation](./repo\-hooks.md) for using repo hooks.
|
NB: See the [practical documentation](./repo\-hooks.md) for using repo hooks.
|
||||||
@@ -603,12 +686,18 @@ repository's root.
|
|||||||
"name" may not be an absolute path or use "." or ".." path components. These
|
"name" may not be an absolute path or use "." or ".." path components. These
|
||||||
restrictions are not enforced for [Local Manifests].
|
restrictions are not enforced for [Local Manifests].
|
||||||
.PP
|
.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 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`.
|
corresponding element of `project`.
|
||||||
.PP
|
.PP
|
||||||
Local Manifests
|
Attribute `revision`: Name of a Git branch (e.g. `main` or `refs/heads/main`)
|
||||||
|
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
|
.PP
|
||||||
Additional remotes and projects may be added through local manifest files stored
|
Additional remotes and projects may be added through local manifest files stored
|
||||||
in `$TOP_DIR/.repo/local_manifests/*.xml`.
|
in `$TOP_DIR/.repo/local_manifests/*.xml`.
|
||||||
|
|||||||
+28
-3
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "November 2022" "repo smartsync" "Repo Manual"
|
.TH REPO "1" "August 2025" "repo smartsync" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo smartsync - manual page for repo smartsync
|
repo \- repo smartsync - manual page for repo smartsync
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -20,11 +20,11 @@ number of CPU cores)
|
|||||||
.TP
|
.TP
|
||||||
\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
|
\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
|
||||||
number of network jobs to run in parallel (defaults to
|
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
|
.TP
|
||||||
\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
|
\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
|
||||||
number of local checkout jobs to run in parallel
|
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
|
.TP
|
||||||
\fB\-f\fR, \fB\-\-force\-broken\fR
|
\fB\-f\fR, \fB\-\-force\-broken\fR
|
||||||
obsolete option (to be deleted in the future)
|
obsolete option (to be deleted in the future)
|
||||||
@@ -37,11 +37,20 @@ overwrite an existing git directory if it needs to
|
|||||||
point to a different object directory. WARNING: this
|
point to a different object directory. WARNING: this
|
||||||
may cause loss of data
|
may cause loss of data
|
||||||
.TP
|
.TP
|
||||||
|
\fB\-\-force\-checkout\fR
|
||||||
|
force checkout even if it results in throwing away
|
||||||
|
uncommitted modifications. WARNING: this may cause
|
||||||
|
loss of data
|
||||||
|
.TP
|
||||||
\fB\-\-force\-remove\-dirty\fR
|
\fB\-\-force\-remove\-dirty\fR
|
||||||
force remove projects with uncommitted modifications
|
force remove projects with uncommitted modifications
|
||||||
if projects no longer exist in the manifest. WARNING:
|
if projects no longer exist in the manifest. WARNING:
|
||||||
this may cause loss of data
|
this may cause loss of data
|
||||||
.TP
|
.TP
|
||||||
|
\fB\-\-rebase\fR
|
||||||
|
rebase local commits regardless of whether they are
|
||||||
|
published
|
||||||
|
.TP
|
||||||
\fB\-l\fR, \fB\-\-local\-only\fR
|
\fB\-l\fR, \fB\-\-local\-only\fR
|
||||||
only update working tree, don't fetch
|
only update working tree, don't fetch
|
||||||
.TP
|
.TP
|
||||||
@@ -49,6 +58,12 @@ only update working tree, don't fetch
|
|||||||
use the existing manifest checkout as\-is. (do not
|
use the existing manifest checkout as\-is. (do not
|
||||||
update to the latest revision)
|
update to the latest revision)
|
||||||
.TP
|
.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
|
\fB\-n\fR, \fB\-\-network\-only\fR
|
||||||
fetch only, don't update working tree
|
fetch only, don't update working tree
|
||||||
.TP
|
.TP
|
||||||
@@ -136,6 +151,16 @@ operate on this manifest and its submanifests
|
|||||||
.TP
|
.TP
|
||||||
\fB\-\-no\-repo\-verify\fR
|
\fB\-\-no\-repo\-verify\fR
|
||||||
do not verify repo source code
|
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
|
.PP
|
||||||
Run `repo help smartsync` to view the detailed manual.
|
Run `repo help smartsync` to view the detailed manual.
|
||||||
.SH DETAILS
|
.SH DETAILS
|
||||||
|
|||||||
+33
-3
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "November 2022" "repo sync" "Repo Manual"
|
.TH REPO "1" "August 2025" "repo sync" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo sync - manual page for repo sync
|
repo \- repo sync - manual page for repo sync
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -20,11 +20,11 @@ number of CPU cores)
|
|||||||
.TP
|
.TP
|
||||||
\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
|
\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
|
||||||
number of network jobs to run in parallel (defaults to
|
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
|
.TP
|
||||||
\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
|
\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
|
||||||
number of local checkout jobs to run in parallel
|
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
|
.TP
|
||||||
\fB\-f\fR, \fB\-\-force\-broken\fR
|
\fB\-f\fR, \fB\-\-force\-broken\fR
|
||||||
obsolete option (to be deleted in the future)
|
obsolete option (to be deleted in the future)
|
||||||
@@ -37,11 +37,20 @@ overwrite an existing git directory if it needs to
|
|||||||
point to a different object directory. WARNING: this
|
point to a different object directory. WARNING: this
|
||||||
may cause loss of data
|
may cause loss of data
|
||||||
.TP
|
.TP
|
||||||
|
\fB\-\-force\-checkout\fR
|
||||||
|
force checkout even if it results in throwing away
|
||||||
|
uncommitted modifications. WARNING: this may cause
|
||||||
|
loss of data
|
||||||
|
.TP
|
||||||
\fB\-\-force\-remove\-dirty\fR
|
\fB\-\-force\-remove\-dirty\fR
|
||||||
force remove projects with uncommitted modifications
|
force remove projects with uncommitted modifications
|
||||||
if projects no longer exist in the manifest. WARNING:
|
if projects no longer exist in the manifest. WARNING:
|
||||||
this may cause loss of data
|
this may cause loss of data
|
||||||
.TP
|
.TP
|
||||||
|
\fB\-\-rebase\fR
|
||||||
|
rebase local commits regardless of whether they are
|
||||||
|
published
|
||||||
|
.TP
|
||||||
\fB\-l\fR, \fB\-\-local\-only\fR
|
\fB\-l\fR, \fB\-\-local\-only\fR
|
||||||
only update working tree, don't fetch
|
only update working tree, don't fetch
|
||||||
.TP
|
.TP
|
||||||
@@ -49,6 +58,12 @@ only update working tree, don't fetch
|
|||||||
use the existing manifest checkout as\-is. (do not
|
use the existing manifest checkout as\-is. (do not
|
||||||
update to the latest revision)
|
update to the latest revision)
|
||||||
.TP
|
.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
|
\fB\-n\fR, \fB\-\-network\-only\fR
|
||||||
fetch only, don't update working tree
|
fetch only, don't update working tree
|
||||||
.TP
|
.TP
|
||||||
@@ -143,6 +158,16 @@ operate on this manifest and its submanifests
|
|||||||
.TP
|
.TP
|
||||||
\fB\-\-no\-repo\-verify\fR
|
\fB\-\-no\-repo\-verify\fR
|
||||||
do not verify repo source code
|
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
|
.PP
|
||||||
Run `repo help sync` to view the detailed manual.
|
Run `repo help sync` to view the detailed manual.
|
||||||
.SH DETAILS
|
.SH DETAILS
|
||||||
@@ -185,6 +210,11 @@ The \fB\-\-force\-sync\fR option can be used to overwrite existing git directori
|
|||||||
they have previously been linked to a different object directory. WARNING: This
|
they have previously been linked to a different object directory. WARNING: This
|
||||||
may cause data to be lost since refs may be removed when overwriting.
|
may cause data to be lost since refs may be removed when overwriting.
|
||||||
.PP
|
.PP
|
||||||
|
The \fB\-\-force\-checkout\fR option can be used to force git to switch revs even if the
|
||||||
|
index or the working tree differs from HEAD, and if there are untracked files.
|
||||||
|
WARNING: This may cause data to be lost since uncommitted changes may be
|
||||||
|
removed.
|
||||||
|
.PP
|
||||||
The \fB\-\-force\-remove\-dirty\fR option can be used to remove previously used projects
|
The \fB\-\-force\-remove\-dirty\fR option can be used to remove previously used projects
|
||||||
with uncommitted changes. WARNING: This may cause data to be lost since
|
with uncommitted changes. WARNING: This may cause data to be lost since
|
||||||
uncommitted changes may be removed with projects that no longer exist in the
|
uncommitted changes may be removed with projects that no longer exist in the
|
||||||
|
|||||||
+15
-3
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "August 2022" "repo upload" "Repo Manual"
|
.TH REPO "1" "June 2024" "repo upload" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo upload - manual page for repo upload
|
repo \- repo upload - manual page for repo upload
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -18,8 +18,11 @@ show this help message and exit
|
|||||||
number of jobs to run in parallel (default: based on
|
number of jobs to run in parallel (default: based on
|
||||||
number of CPU cores)
|
number of CPU cores)
|
||||||
.TP
|
.TP
|
||||||
\fB\-t\fR
|
\fB\-t\fR, \fB\-\-topic\-branch\fR
|
||||||
send local branch name to Gerrit Code Review
|
set the topic to the local branch name
|
||||||
|
.TP
|
||||||
|
\fB\-\-topic\fR=\fI\,TOPIC\/\fR
|
||||||
|
set topic for the change
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-hashtag\fR=\fI\,HASHTAGS\/\fR, \fB\-\-ht\fR=\fI\,HASHTAGS\/\fR
|
\fB\-\-hashtag\fR=\fI\,HASHTAGS\/\fR, \fB\-\-ht\fR=\fI\,HASHTAGS\/\fR
|
||||||
add hashtags (comma delimited) to the review
|
add hashtags (comma delimited) to the review
|
||||||
@@ -30,6 +33,9 @@ add local branch name as a hashtag
|
|||||||
\fB\-l\fR LABELS, \fB\-\-label\fR=\fI\,LABELS\/\fR
|
\fB\-l\fR LABELS, \fB\-\-label\fR=\fI\,LABELS\/\fR
|
||||||
add a label when uploading
|
add a label when uploading
|
||||||
.TP
|
.TP
|
||||||
|
\fB\-\-pd\fR=\fI\,PATCHSET_DESCRIPTION\/\fR, \fB\-\-patchset\-description\fR=\fI\,PATCHSET_DESCRIPTION\/\fR
|
||||||
|
description for patchset
|
||||||
|
.TP
|
||||||
\fB\-\-re\fR=\fI\,REVIEWERS\/\fR, \fB\-\-reviewers\fR=\fI\,REVIEWERS\/\fR
|
\fB\-\-re\fR=\fI\,REVIEWERS\/\fR, \fB\-\-reviewers\fR=\fI\,REVIEWERS\/\fR
|
||||||
request reviews from these people
|
request reviews from these people
|
||||||
.TP
|
.TP
|
||||||
@@ -198,6 +204,12 @@ review.URL.uploadnotify:
|
|||||||
Control e\-mail notifications when uploading.
|
Control e\-mail notifications when uploading.
|
||||||
https://gerrit\-review.googlesource.com/Documentation/user\-upload.html#notify
|
https://gerrit\-review.googlesource.com/Documentation/user\-upload.html#notify
|
||||||
.PP
|
.PP
|
||||||
|
review.URL.uploadwarningthreshold:
|
||||||
|
.PP
|
||||||
|
Repo will warn you if you are attempting to upload a large number of commits in
|
||||||
|
one or more branches. By default, the threshold is five commits. This option
|
||||||
|
allows you to override the warning threshold to a different value.
|
||||||
|
.PP
|
||||||
References
|
References
|
||||||
.PP
|
.PP
|
||||||
Gerrit Code Review: https://www.gerritcodereview.com/
|
Gerrit Code Review: https://www.gerritcodereview.com/
|
||||||
|
|||||||
@@ -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
|
||||||
+6
-6
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "June 2023" "repo" "Repo Manual"
|
.TH REPO "1" "November 2025" "repo" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repository management tool built on top of git
|
repo \- repository management tool built on top of git
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -79,11 +79,8 @@ Download and checkout a change
|
|||||||
forall
|
forall
|
||||||
Run a shell command in each project
|
Run a shell command in each project
|
||||||
.TP
|
.TP
|
||||||
gitc\-delete
|
gc
|
||||||
Delete a GITC Client.
|
Cleaning up internal repo and Git state.
|
||||||
.TP
|
|
||||||
gitc\-init
|
|
||||||
Initialize a GITC Client.
|
|
||||||
.TP
|
.TP
|
||||||
grep
|
grep
|
||||||
Print lines matching a pattern
|
Print lines matching a pattern
|
||||||
@@ -135,6 +132,9 @@ Upload changes for code review
|
|||||||
.TP
|
.TP
|
||||||
version
|
version
|
||||||
Display the version of repo
|
Display the version of repo
|
||||||
|
.TP
|
||||||
|
wipe
|
||||||
|
Wipe projects from the worktree
|
||||||
.PP
|
.PP
|
||||||
See 'repo help <command>' for more information on a specific command.
|
See 'repo help <command>' for more information on a specific command.
|
||||||
Bug reports: https://issues.gerritcodereview.com/issues/new?component=1370071
|
Bug reports: https://issues.gerritcodereview.com/issues/new?component=1370071
|
||||||
|
|||||||
+168
-102
@@ -133,8 +133,8 @@ def normalize_url(url: str) -> str:
|
|||||||
url = url.rstrip("/")
|
url = url.rstrip("/")
|
||||||
parsed_url = urllib.parse.urlparse(url)
|
parsed_url = urllib.parse.urlparse(url)
|
||||||
|
|
||||||
# This matches patterns like "git@github.com:foo/bar".
|
# This matches patterns like "git@github.com:foo".
|
||||||
scp_like_url_re = r"^[^:]+@[^:]+:[^/]+/"
|
scp_like_url_re = r"^[^/:]+@[^/:]+:[^/]+"
|
||||||
|
|
||||||
# If our URL is missing a schema and matches git's
|
# If our URL is missing a schema and matches git's
|
||||||
# SCP-like syntax we should convert it to a proper
|
# SCP-like syntax we should convert it to a proper
|
||||||
@@ -155,6 +155,7 @@ class _Default:
|
|||||||
upstreamExpr = None
|
upstreamExpr = None
|
||||||
remote = None
|
remote = None
|
||||||
sync_j = None
|
sync_j = None
|
||||||
|
sync_j_max = None
|
||||||
sync_c = False
|
sync_c = False
|
||||||
sync_s = False
|
sync_s = False
|
||||||
sync_tags = True
|
sync_tags = True
|
||||||
@@ -255,7 +256,7 @@ class _XmlSubmanifest:
|
|||||||
project: a string, the name of the manifest project.
|
project: a string, the name of the manifest project.
|
||||||
revision: a string, the commitish.
|
revision: a string, the commitish.
|
||||||
manifestName: a string, the submanifest file name.
|
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.
|
submanifest.
|
||||||
default_groups: a list of strings, the default groups to sync.
|
default_groups: a list of strings, the default groups to sync.
|
||||||
path: a string, the relative path for the submanifest checkout.
|
path: a string, the relative path for the submanifest checkout.
|
||||||
@@ -281,7 +282,7 @@ class _XmlSubmanifest:
|
|||||||
self.project = project
|
self.project = project
|
||||||
self.revision = revision
|
self.revision = revision
|
||||||
self.manifestName = manifestName
|
self.manifestName = manifestName
|
||||||
self.groups = groups
|
self.groups = groups or set()
|
||||||
self.default_groups = default_groups
|
self.default_groups = default_groups
|
||||||
self.path = path
|
self.path = path
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
@@ -304,7 +305,7 @@ class _XmlSubmanifest:
|
|||||||
self.repo_client = RepoClient(
|
self.repo_client = RepoClient(
|
||||||
parent.repodir,
|
parent.repodir,
|
||||||
linkFile,
|
linkFile,
|
||||||
parent_groups=",".join(groups) or "",
|
parent_groups=groups,
|
||||||
submanifest_path=os.path.join(parent.path_prefix, self.relpath),
|
submanifest_path=os.path.join(parent.path_prefix, self.relpath),
|
||||||
outer_client=outer_client,
|
outer_client=outer_client,
|
||||||
default_groups=default_groups,
|
default_groups=default_groups,
|
||||||
@@ -345,7 +346,7 @@ class _XmlSubmanifest:
|
|||||||
manifestName = self.manifestName or "default.xml"
|
manifestName = self.manifestName or "default.xml"
|
||||||
revision = self.revision or self.name
|
revision = self.revision or self.name
|
||||||
path = self.path or revision.split("/")[-1]
|
path = self.path or revision.split("/")[-1]
|
||||||
groups = self.groups or []
|
groups = self.groups
|
||||||
|
|
||||||
return SubmanifestSpec(
|
return SubmanifestSpec(
|
||||||
self.name, manifestUrl, manifestName, revision, path, groups
|
self.name, manifestUrl, manifestName, revision, path, groups
|
||||||
@@ -359,9 +360,7 @@ class _XmlSubmanifest:
|
|||||||
|
|
||||||
def GetGroupsStr(self):
|
def GetGroupsStr(self):
|
||||||
"""Returns the `groups` given for this submanifest."""
|
"""Returns the `groups` given for this submanifest."""
|
||||||
if self.groups:
|
return ",".join(sorted(self.groups))
|
||||||
return ",".join(self.groups)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def GetDefaultGroupsStr(self):
|
def GetDefaultGroupsStr(self):
|
||||||
"""Returns the `default-groups` given for this submanifest."""
|
"""Returns the `default-groups` given for this submanifest."""
|
||||||
@@ -381,7 +380,7 @@ class SubmanifestSpec:
|
|||||||
self.manifestName = manifestName
|
self.manifestName = manifestName
|
||||||
self.revision = revision
|
self.revision = revision
|
||||||
self.path = path
|
self.path = path
|
||||||
self.groups = groups or []
|
self.groups = groups
|
||||||
|
|
||||||
|
|
||||||
class XmlManifest:
|
class XmlManifest:
|
||||||
@@ -393,7 +392,7 @@ class XmlManifest:
|
|||||||
manifest_file,
|
manifest_file,
|
||||||
local_manifests=None,
|
local_manifests=None,
|
||||||
outer_client=None,
|
outer_client=None,
|
||||||
parent_groups="",
|
parent_groups=None,
|
||||||
submanifest_path="",
|
submanifest_path="",
|
||||||
default_groups=None,
|
default_groups=None,
|
||||||
):
|
):
|
||||||
@@ -409,7 +408,8 @@ class XmlManifest:
|
|||||||
manifests. This will usually be
|
manifests. This will usually be
|
||||||
|repodir|/|LOCAL_MANIFESTS_DIR_NAME|.
|
|repodir|/|LOCAL_MANIFESTS_DIR_NAME|.
|
||||||
outer_client: RepoClient of the outer manifest.
|
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.
|
submanifest_path: The submanifest root relative to the repo root.
|
||||||
default_groups: a string, the default manifest groups to use.
|
default_groups: a string, the default manifest groups to use.
|
||||||
"""
|
"""
|
||||||
@@ -432,14 +432,9 @@ class XmlManifest:
|
|||||||
self.manifestFileOverrides = {}
|
self.manifestFileOverrides = {}
|
||||||
self.local_manifests = local_manifests
|
self.local_manifests = local_manifests
|
||||||
self._load_local_manifests = True
|
self._load_local_manifests = True
|
||||||
self.parent_groups = parent_groups
|
self.parent_groups = parent_groups or set()
|
||||||
self.default_groups = default_groups
|
self.default_groups = default_groups
|
||||||
|
|
||||||
if outer_client and self.isGitcClient:
|
|
||||||
raise ManifestParseError(
|
|
||||||
"Multi-manifest is incompatible with `gitc-init`"
|
|
||||||
)
|
|
||||||
|
|
||||||
if submanifest_path and not outer_client:
|
if submanifest_path and not outer_client:
|
||||||
# If passing a submanifest_path, there must be an outer_client.
|
# If passing a submanifest_path, there must be an outer_client.
|
||||||
raise ManifestParseError(f"Bad call to {self.__class__.__name__}")
|
raise ManifestParseError(f"Bad call to {self.__class__.__name__}")
|
||||||
@@ -572,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]
|
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(
|
def ToXml(
|
||||||
self,
|
self,
|
||||||
peg_rev=False,
|
peg_rev=False,
|
||||||
peg_rev_upstream=True,
|
peg_rev_upstream=True,
|
||||||
peg_rev_dest_branch=True,
|
peg_rev_dest_branch=True,
|
||||||
groups=None,
|
filter_groups=None,
|
||||||
omit_local=False,
|
omit_local=False,
|
||||||
):
|
):
|
||||||
"""Return the current manifest XML."""
|
"""Return the current manifest XML."""
|
||||||
mp = self.manifestProject
|
mp = self.manifestProject
|
||||||
|
|
||||||
if groups is None:
|
if filter_groups is None:
|
||||||
groups = mp.manifest_groups
|
filter_groups = mp.manifest_groups
|
||||||
if groups:
|
if filter_groups:
|
||||||
groups = self._ParseList(groups)
|
filter_groups = self._ParseList(filter_groups)
|
||||||
|
|
||||||
doc = xml.dom.minidom.Document()
|
doc = xml.dom.minidom.Document()
|
||||||
root = doc.createElement("manifest")
|
root = doc.createElement("manifest")
|
||||||
@@ -629,6 +632,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
if d.sync_j is not None:
|
if d.sync_j is not None:
|
||||||
have_default = True
|
have_default = True
|
||||||
e.setAttribute("sync-j", "%d" % d.sync_j)
|
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:
|
if d.sync_c:
|
||||||
have_default = True
|
have_default = True
|
||||||
e.setAttribute("sync-c", "true")
|
e.setAttribute("sync-c", "true")
|
||||||
@@ -659,7 +665,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
output_project(parent, parent_node, project)
|
output_project(parent, parent_node, project)
|
||||||
|
|
||||||
def output_project(parent, parent_node, p):
|
def output_project(parent, parent_node, p):
|
||||||
if not p.MatchesGroups(groups):
|
if not p.MatchesGroups(filter_groups):
|
||||||
return
|
return
|
||||||
|
|
||||||
if omit_local and self.IsFromLocalManifest(p):
|
if omit_local and self.IsFromLocalManifest(p):
|
||||||
@@ -730,10 +736,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
le.setAttribute("dest", lf.dest)
|
le.setAttribute("dest", lf.dest)
|
||||||
e.appendChild(le)
|
e.appendChild(le)
|
||||||
|
|
||||||
default_groups = ["all", "name:%s" % p.name, "path:%s" % p.relpath]
|
groups = p.groups - {"all", f"name:{p.name}", f"path:{p.relpath}"}
|
||||||
egroups = [g for g in p.groups if g not in default_groups]
|
if groups:
|
||||||
if egroups:
|
e.setAttribute("groups", ",".join(sorted(groups)))
|
||||||
e.setAttribute("groups", ",".join(egroups))
|
|
||||||
|
|
||||||
for a in p.annotations:
|
for a in p.annotations:
|
||||||
if a.keep == "true":
|
if a.keep == "true":
|
||||||
@@ -754,14 +759,17 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
if p.clone_depth:
|
if p.clone_depth:
|
||||||
e.setAttribute("clone-depth", str(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)
|
self._output_manifest_project_extras(p, e)
|
||||||
|
|
||||||
if p.subprojects:
|
if p.subprojects:
|
||||||
subprojects = {subp.name for subp in 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}
|
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:
|
if self._repo_hooks_project:
|
||||||
root.appendChild(doc.createTextNode(""))
|
root.appendChild(doc.createTextNode(""))
|
||||||
@@ -818,7 +826,6 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
"submanifest",
|
"submanifest",
|
||||||
# These are children of 'project' nodes.
|
# These are children of 'project' nodes.
|
||||||
"annotation",
|
"annotation",
|
||||||
"project",
|
|
||||||
"copyfile",
|
"copyfile",
|
||||||
"linkfile",
|
"linkfile",
|
||||||
}
|
}
|
||||||
@@ -1019,9 +1026,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
|
|
||||||
def SetManifestOverride(self, path):
|
def SetManifestOverride(self, path):
|
||||||
"""Override manifestFile. The caller must call Unload()"""
|
"""Override manifestFile. The caller must call Unload()"""
|
||||||
self._outer_client.manifest.manifestFileOverrides[
|
self._outer_client.manifest.manifestFileOverrides[self.path_prefix] = (
|
||||||
self.path_prefix
|
path
|
||||||
] = path
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def UseLocalManifests(self):
|
def UseLocalManifests(self):
|
||||||
@@ -1121,7 +1128,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
groups += f",platform-{platform.system().lower()}"
|
groups += f",platform-{platform.system().lower()}"
|
||||||
return groups
|
return groups
|
||||||
|
|
||||||
def GetGroupsStr(self):
|
def GetManifestGroupsStr(self):
|
||||||
"""Returns the manifest group string that should be synced."""
|
"""Returns the manifest group string that should be synced."""
|
||||||
return (
|
return (
|
||||||
self.manifestProject.manifest_groups or self.GetDefaultGroupsStr()
|
self.manifestProject.manifest_groups or self.GetDefaultGroupsStr()
|
||||||
@@ -1176,12 +1183,12 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
b = b[len(R_HEADS) :]
|
b = b[len(R_HEADS) :]
|
||||||
self.branch = b
|
self.branch = b
|
||||||
|
|
||||||
parent_groups = self.parent_groups
|
parent_groups = self.parent_groups.copy()
|
||||||
if self.path_prefix:
|
if self.path_prefix:
|
||||||
parent_groups = (
|
parent_groups |= {
|
||||||
f"{SUBMANIFEST_GROUP_PREFIX}:path:"
|
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
|
# The manifestFile was specified by the user which is why we
|
||||||
# allow include paths to point anywhere.
|
# allow include paths to point anywhere.
|
||||||
@@ -1207,16 +1214,16 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
# Since local manifests are entirely managed by
|
# Since local manifests are entirely managed by
|
||||||
# the user, allow them to point anywhere the
|
# the user, allow them to point anywhere the
|
||||||
# user wants.
|
# user wants.
|
||||||
local_group = (
|
local_group = {
|
||||||
f"{LOCAL_MANIFEST_GROUP_PREFIX}:"
|
f"{LOCAL_MANIFEST_GROUP_PREFIX}:"
|
||||||
f"{local_file[:-4]}"
|
f"{local_file[:-4]}"
|
||||||
)
|
}
|
||||||
nodes.append(
|
nodes.append(
|
||||||
self._ParseManifestXml(
|
self._ParseManifestXml(
|
||||||
local,
|
local,
|
||||||
self.subdir,
|
self.subdir,
|
||||||
parent_groups=(
|
parent_groups=(
|
||||||
f"{local_group},{parent_groups}"
|
local_group | parent_groups
|
||||||
),
|
),
|
||||||
restrict_includes=False,
|
restrict_includes=False,
|
||||||
)
|
)
|
||||||
@@ -1267,7 +1274,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
self,
|
self,
|
||||||
path,
|
path,
|
||||||
include_root,
|
include_root,
|
||||||
parent_groups="",
|
parent_groups=None,
|
||||||
restrict_includes=True,
|
restrict_includes=True,
|
||||||
parent_node=None,
|
parent_node=None,
|
||||||
):
|
):
|
||||||
@@ -1276,11 +1283,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
Args:
|
Args:
|
||||||
path: The XML file to read & parse.
|
path: The XML file to read & parse.
|
||||||
include_root: The path to interpret include "name"s relative to.
|
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
|
restrict_includes: Whether to constrain the "name" attribute of
|
||||||
includes.
|
includes.
|
||||||
parent_node: The parent include node, to apply attribute to this
|
parent_node: The parent include node, to apply attributes to this
|
||||||
projects.
|
manifest.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of XML nodes.
|
List of XML nodes.
|
||||||
@@ -1304,6 +1311,14 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
|
|
||||||
nodes = []
|
nodes = []
|
||||||
for node in manifest.childNodes:
|
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":
|
if node.nodeName == "include":
|
||||||
name = self._reqatt(node, "name")
|
name = self._reqatt(node, "name")
|
||||||
if restrict_includes:
|
if restrict_includes:
|
||||||
@@ -1312,12 +1327,10 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
raise ManifestInvalidPathError(
|
raise ManifestInvalidPathError(
|
||||||
f'<include> invalid "name": {name}: {msg}'
|
f'<include> invalid "name": {name}: {msg}'
|
||||||
)
|
)
|
||||||
include_groups = ""
|
include_groups = (parent_groups or set()).copy()
|
||||||
if parent_groups:
|
|
||||||
include_groups = parent_groups
|
|
||||||
if node.hasAttribute("groups"):
|
if node.hasAttribute("groups"):
|
||||||
include_groups = (
|
include_groups |= self._ParseSet(
|
||||||
node.getAttribute("groups") + "," + include_groups
|
node.getAttribute("groups")
|
||||||
)
|
)
|
||||||
fp = os.path.join(include_root, name)
|
fp = os.path.join(include_root, name)
|
||||||
if not os.path.isfile(fp):
|
if not os.path.isfile(fp):
|
||||||
@@ -1333,33 +1346,23 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
)
|
)
|
||||||
# should isolate this to the exact exception, but that's
|
# should isolate this to the exact exception, but that's
|
||||||
# tricky. actual parsing implementation may vary.
|
# tricky. actual parsing implementation may vary.
|
||||||
except (
|
except (RuntimeError, ManifestParseError):
|
||||||
KeyboardInterrupt,
|
|
||||||
RuntimeError,
|
|
||||||
SystemExit,
|
|
||||||
ManifestParseError,
|
|
||||||
):
|
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ManifestParseError(
|
raise ManifestParseError(
|
||||||
f"failed parsing included manifest {name}: {e}"
|
f"failed parsing included manifest {name}: {e}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if parent_groups and node.nodeName == "project":
|
if parent_groups and node.nodeName in (
|
||||||
nodeGroups = parent_groups
|
"project",
|
||||||
if node.hasAttribute("groups"):
|
"extend-project",
|
||||||
nodeGroups = (
|
|
||||||
node.getAttribute("groups") + "," + nodeGroups
|
|
||||||
)
|
|
||||||
node.setAttribute("groups", nodeGroups)
|
|
||||||
if (
|
|
||||||
parent_node
|
|
||||||
and node.nodeName == "project"
|
|
||||||
and not node.hasAttribute("revision")
|
|
||||||
):
|
):
|
||||||
node.setAttribute(
|
nodeGroups = parent_groups.copy()
|
||||||
"revision", parent_node.getAttribute("revision")
|
if node.hasAttribute("groups"):
|
||||||
)
|
nodeGroups |= self._ParseSet(
|
||||||
|
node.getAttribute("groups")
|
||||||
|
)
|
||||||
|
node.setAttribute("groups", ",".join(sorted(nodeGroups)))
|
||||||
nodes.append(node)
|
nodes.append(node)
|
||||||
return nodes
|
return nodes
|
||||||
|
|
||||||
@@ -1450,6 +1453,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
|
|
||||||
repo_hooks_project = None
|
repo_hooks_project = None
|
||||||
enabled_repo_hooks = None
|
enabled_repo_hooks = None
|
||||||
|
failed_revision_changes = []
|
||||||
for node in itertools.chain(*node_list):
|
for node in itertools.chain(*node_list):
|
||||||
if node.nodeName == "project":
|
if node.nodeName == "project":
|
||||||
project = self._ParseProject(node)
|
project = self._ParseProject(node)
|
||||||
@@ -1467,7 +1471,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
dest_path = node.getAttribute("dest-path")
|
dest_path = node.getAttribute("dest-path")
|
||||||
groups = node.getAttribute("groups")
|
groups = node.getAttribute("groups")
|
||||||
if groups:
|
if groups:
|
||||||
groups = self._ParseList(groups)
|
groups = self._ParseSet(groups or "")
|
||||||
revision = node.getAttribute("revision")
|
revision = node.getAttribute("revision")
|
||||||
remote_name = node.getAttribute("remote")
|
remote_name = node.getAttribute("remote")
|
||||||
if not remote_name:
|
if not remote_name:
|
||||||
@@ -1476,6 +1480,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
remote = self._get_remote(node)
|
remote = self._get_remote(node)
|
||||||
dest_branch = node.getAttribute("dest-branch")
|
dest_branch = node.getAttribute("dest-branch")
|
||||||
upstream = node.getAttribute("upstream")
|
upstream = node.getAttribute("upstream")
|
||||||
|
base_revision = node.getAttribute("base-rev")
|
||||||
|
|
||||||
named_projects = self._projects[name]
|
named_projects = self._projects[name]
|
||||||
if dest_path and not path and len(named_projects) > 1:
|
if dest_path and not path and len(named_projects) > 1:
|
||||||
@@ -1487,8 +1492,23 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
if path and p.relpath != path:
|
if path and p.relpath != path:
|
||||||
continue
|
continue
|
||||||
if groups:
|
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 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)
|
p.SetRevision(revision)
|
||||||
|
|
||||||
if remote_name:
|
if remote_name:
|
||||||
@@ -1510,6 +1530,14 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
p.UpdatePaths(relpath, worktree, gitdir, objdir)
|
p.UpdatePaths(relpath, worktree, gitdir, objdir)
|
||||||
self._paths[p.relpath] = p
|
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":
|
if node.nodeName == "repo-hooks":
|
||||||
# Only one project can be the hooks project
|
# Only one project can be the hooks project
|
||||||
if repo_hooks_project is not None:
|
if repo_hooks_project is not None:
|
||||||
@@ -1563,6 +1591,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
if node.nodeName == "remove-project":
|
if node.nodeName == "remove-project":
|
||||||
name = node.getAttribute("name")
|
name = node.getAttribute("name")
|
||||||
path = node.getAttribute("path")
|
path = node.getAttribute("path")
|
||||||
|
base_revision = node.getAttribute("base-rev")
|
||||||
|
|
||||||
# Name or path needed.
|
# Name or path needed.
|
||||||
if not name and not path:
|
if not name and not path:
|
||||||
@@ -1576,6 +1605,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
for projname, projects in list(self._projects.items()):
|
for projname, projects in list(self._projects.items()):
|
||||||
for p in projects:
|
for p in projects:
|
||||||
if name == projname and not path:
|
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]
|
del self._paths[p.relpath]
|
||||||
if not removed_project:
|
if not removed_project:
|
||||||
del self._projects[name]
|
del self._projects[name]
|
||||||
@@ -1583,6 +1619,17 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
elif path == p.relpath and (
|
elif path == p.relpath and (
|
||||||
name == projname or not name
|
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)
|
self._projects[projname].remove(p)
|
||||||
del self._paths[p.relpath]
|
del self._paths[p.relpath]
|
||||||
removed_project = p.name
|
removed_project = p.name
|
||||||
@@ -1602,6 +1649,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
"project: %s" % node.toxml()
|
"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.
|
# Store repo hooks project information.
|
||||||
if repo_hooks_project:
|
if repo_hooks_project:
|
||||||
# Store a reference to the Project.
|
# Store a reference to the Project.
|
||||||
@@ -1715,6 +1769,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
% (self.manifestFile, d.sync_j)
|
% (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_c = XmlBool(node, "sync-c", False)
|
||||||
d.sync_s = XmlBool(node, "sync-s", False)
|
d.sync_s = XmlBool(node, "sync-s", False)
|
||||||
d.sync_tags = XmlBool(node, "sync-tags", True)
|
d.sync_tags = XmlBool(node, "sync-tags", True)
|
||||||
@@ -1777,7 +1838,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
groups = ""
|
groups = ""
|
||||||
if node.hasAttribute("groups"):
|
if node.hasAttribute("groups"):
|
||||||
groups = node.getAttribute("groups")
|
groups = node.getAttribute("groups")
|
||||||
groups = self._ParseList(groups)
|
groups = self._ParseSet(groups)
|
||||||
default_groups = self._ParseList(node.getAttribute("default-groups"))
|
default_groups = self._ParseList(node.getAttribute("default-groups"))
|
||||||
path = node.getAttribute("path")
|
path = node.getAttribute("path")
|
||||||
if path == "":
|
if path == "":
|
||||||
@@ -1880,17 +1941,14 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
% (self.manifestFile, clone_depth)
|
% (self.manifestFile, clone_depth)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sync_strategy = node.getAttribute("sync-strategy") or None
|
||||||
|
|
||||||
dest_branch = (
|
dest_branch = (
|
||||||
node.getAttribute("dest-branch") or self._default.destBranchExpr
|
node.getAttribute("dest-branch") or self._default.destBranchExpr
|
||||||
)
|
)
|
||||||
|
|
||||||
upstream = node.getAttribute("upstream") or self._default.upstreamExpr
|
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:
|
if parent is None:
|
||||||
(
|
(
|
||||||
relpath,
|
relpath,
|
||||||
@@ -1905,8 +1963,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
parent, name, path
|
parent, name, path
|
||||||
)
|
)
|
||||||
|
|
||||||
default_groups = ["all", "name:%s" % name, "path:%s" % relpath]
|
groups = ""
|
||||||
groups.extend(set(default_groups).difference(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 self.IsMirror and node.hasAttribute("force-path"):
|
||||||
if XmlBool(node, "force-path", False):
|
if XmlBool(node, "force-path", False):
|
||||||
@@ -1928,6 +1989,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
sync_s=sync_s,
|
sync_s=sync_s,
|
||||||
sync_tags=sync_tags,
|
sync_tags=sync_tags,
|
||||||
clone_depth=clone_depth,
|
clone_depth=clone_depth,
|
||||||
|
sync_strategy=sync_strategy,
|
||||||
upstream=upstream,
|
upstream=upstream,
|
||||||
parent=parent,
|
parent=parent,
|
||||||
dest_branch=dest_branch,
|
dest_branch=dest_branch,
|
||||||
@@ -1938,11 +2000,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
for n in node.childNodes:
|
for n in node.childNodes:
|
||||||
if n.nodeName == "copyfile":
|
if n.nodeName == "copyfile":
|
||||||
self._ParseCopyFile(project, n)
|
self._ParseCopyFile(project, n)
|
||||||
if n.nodeName == "linkfile":
|
elif n.nodeName == "linkfile":
|
||||||
self._ParseLinkFile(project, n)
|
self._ParseLinkFile(project, n)
|
||||||
if n.nodeName == "annotation":
|
elif n.nodeName == "annotation":
|
||||||
self._ParseAnnotation(project, n)
|
self._ParseAnnotation(project, n)
|
||||||
if n.nodeName == "project":
|
elif n.nodeName == "project":
|
||||||
project.subprojects.append(
|
project.subprojects.append(
|
||||||
self._ParseProject(n, parent=project)
|
self._ParseProject(n, parent=project)
|
||||||
)
|
)
|
||||||
@@ -2026,7 +2088,12 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
path = path.rstrip("/")
|
path = path.rstrip("/")
|
||||||
name = name.rstrip("/")
|
name = name.rstrip("/")
|
||||||
relpath = self._JoinRelpath(parent.relpath, path)
|
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(
|
objdir = os.path.join(
|
||||||
parent.gitdir, "subproject-objects", "%s.git" % name
|
parent.gitdir, "subproject-objects", "%s.git" % name
|
||||||
)
|
)
|
||||||
@@ -2077,22 +2144,22 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
# implementation:
|
# implementation:
|
||||||
# https://eclipse.googlesource.com/jgit/jgit/+/9110037e3e9461ff4dac22fee84ef3694ed57648/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java#884
|
# https://eclipse.googlesource.com/jgit/jgit/+/9110037e3e9461ff4dac22fee84ef3694ed57648/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java#884
|
||||||
BAD_CODEPOINTS = {
|
BAD_CODEPOINTS = {
|
||||||
"\u200C", # ZERO WIDTH NON-JOINER
|
"\u200c", # ZERO WIDTH NON-JOINER
|
||||||
"\u200D", # ZERO WIDTH JOINER
|
"\u200d", # ZERO WIDTH JOINER
|
||||||
"\u200E", # LEFT-TO-RIGHT MARK
|
"\u200e", # LEFT-TO-RIGHT MARK
|
||||||
"\u200F", # RIGHT-TO-LEFT MARK
|
"\u200f", # RIGHT-TO-LEFT MARK
|
||||||
"\u202A", # LEFT-TO-RIGHT EMBEDDING
|
"\u202a", # LEFT-TO-RIGHT EMBEDDING
|
||||||
"\u202B", # RIGHT-TO-LEFT EMBEDDING
|
"\u202b", # RIGHT-TO-LEFT EMBEDDING
|
||||||
"\u202C", # POP DIRECTIONAL FORMATTING
|
"\u202c", # POP DIRECTIONAL FORMATTING
|
||||||
"\u202D", # LEFT-TO-RIGHT OVERRIDE
|
"\u202d", # LEFT-TO-RIGHT OVERRIDE
|
||||||
"\u202E", # RIGHT-TO-LEFT OVERRIDE
|
"\u202e", # RIGHT-TO-LEFT OVERRIDE
|
||||||
"\u206A", # INHIBIT SYMMETRIC SWAPPING
|
"\u206a", # INHIBIT SYMMETRIC SWAPPING
|
||||||
"\u206B", # ACTIVATE SYMMETRIC SWAPPING
|
"\u206b", # ACTIVATE SYMMETRIC SWAPPING
|
||||||
"\u206C", # INHIBIT ARABIC FORM SHAPING
|
"\u206c", # INHIBIT ARABIC FORM SHAPING
|
||||||
"\u206D", # ACTIVATE ARABIC FORM SHAPING
|
"\u206d", # ACTIVATE ARABIC FORM SHAPING
|
||||||
"\u206E", # NATIONAL DIGIT SHAPES
|
"\u206e", # NATIONAL DIGIT SHAPES
|
||||||
"\u206F", # NOMINAL DIGIT SHAPES
|
"\u206f", # NOMINAL DIGIT SHAPES
|
||||||
"\uFEFF", # ZERO WIDTH NO-BREAK SPACE
|
"\ufeff", # ZERO WIDTH NO-BREAK SPACE
|
||||||
}
|
}
|
||||||
if BAD_CODEPOINTS & path_codepoints:
|
if BAD_CODEPOINTS & path_codepoints:
|
||||||
# This message is more expansive than reality, but should be fine.
|
# This message is more expansive than reality, but should be fine.
|
||||||
@@ -2290,7 +2357,6 @@ class RepoClient(XmlManifest):
|
|||||||
submanifest_path: The submanifest root relative to the repo root.
|
submanifest_path: The submanifest root relative to the repo root.
|
||||||
**kwargs: Additional keyword arguments, passed to XmlManifest.
|
**kwargs: Additional keyword arguments, passed to XmlManifest.
|
||||||
"""
|
"""
|
||||||
self.isGitcClient = False
|
|
||||||
submanifest_path = submanifest_path or ""
|
submanifest_path = submanifest_path or ""
|
||||||
if submanifest_path:
|
if submanifest_path:
|
||||||
self._CheckLocalPath(submanifest_path)
|
self._CheckLocalPath(submanifest_path)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ def RunPager(globalConfig):
|
|||||||
|
|
||||||
|
|
||||||
def TerminatePager():
|
def TerminatePager():
|
||||||
global pager_process, old_stdout, old_stderr
|
global pager_process
|
||||||
if pager_process:
|
if pager_process:
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|||||||
+6
-29
@@ -156,6 +156,12 @@ def remove(path, missing_ok=False):
|
|||||||
os.rmdir(longpath)
|
os.rmdir(longpath)
|
||||||
else:
|
else:
|
||||||
os.remove(longpath)
|
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:
|
elif missing_ok and e.errno == errno.ENOENT:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
@@ -251,32 +257,3 @@ def readlink(path):
|
|||||||
return platform_utils_win32.readlink(_makelongpath(path))
|
return platform_utils_win32.readlink(_makelongpath(path))
|
||||||
else:
|
else:
|
||||||
return os.readlink(path)
|
return os.readlink(path)
|
||||||
|
|
||||||
|
|
||||||
def realpath(path):
|
|
||||||
"""Return the canonical path of the specified filename, eliminating
|
|
||||||
any symbolic links encountered in the path.
|
|
||||||
|
|
||||||
Availability: Windows, Unix.
|
|
||||||
"""
|
|
||||||
if isWindows():
|
|
||||||
current_path = os.path.abspath(path)
|
|
||||||
path_tail = []
|
|
||||||
for c in range(0, 100): # Avoid cycles
|
|
||||||
if islink(current_path):
|
|
||||||
target = readlink(current_path)
|
|
||||||
current_path = os.path.join(
|
|
||||||
os.path.dirname(current_path), target
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
basename = os.path.basename(current_path)
|
|
||||||
if basename == "":
|
|
||||||
path_tail.append(current_path)
|
|
||||||
break
|
|
||||||
path_tail.append(basename)
|
|
||||||
current_path = os.path.dirname(current_path)
|
|
||||||
path_tail.reverse()
|
|
||||||
result = os.path.normpath(os.path.join(*path_tail))
|
|
||||||
return result
|
|
||||||
else:
|
|
||||||
return os.path.realpath(path)
|
|
||||||
|
|||||||
+38
-13
@@ -25,7 +25,10 @@ except ImportError:
|
|||||||
from repo_trace import IsTraceToStderr
|
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).
|
# 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
|
# 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._show = not delay
|
||||||
self._units = units
|
self._units = units
|
||||||
self._elide = elide and _TTY
|
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.
|
# Only show the active jobs section if we run more than one in parallel.
|
||||||
self._show_jobs = False
|
self._show_jobs = False
|
||||||
@@ -114,15 +119,14 @@ class Progress:
|
|||||||
)
|
)
|
||||||
self._update_thread.daemon = True
|
self._update_thread.daemon = True
|
||||||
|
|
||||||
# When quiet, never show any output. It's a bit hacky, but reusing the
|
if not quiet and show_elapsed:
|
||||||
# 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:
|
|
||||||
self._update_thread.start()
|
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):
|
def _update_loop(self):
|
||||||
while True:
|
while True:
|
||||||
self.update(inc=0)
|
self.update(inc=0)
|
||||||
@@ -132,11 +136,11 @@ class Progress:
|
|||||||
def _write(self, s):
|
def _write(self, s):
|
||||||
s = "\r" + s
|
s = "\r" + s
|
||||||
if self._elide:
|
if self._elide:
|
||||||
col = os.get_terminal_size(sys.stderr.fileno()).columns
|
col = os.get_terminal_size(_STDERR.fileno()).columns
|
||||||
if len(s) > col:
|
if len(s) > col:
|
||||||
s = s[: col - 1] + ".."
|
s = s[: col - 1] + ".."
|
||||||
sys.stderr.write(s)
|
_STDERR.write(s)
|
||||||
sys.stderr.flush()
|
_STDERR.flush()
|
||||||
|
|
||||||
def start(self, name):
|
def start(self, name):
|
||||||
self._active += 1
|
self._active += 1
|
||||||
@@ -155,12 +159,14 @@ class Progress:
|
|||||||
inc: The number of items completed.
|
inc: The number of items completed.
|
||||||
msg: The message to display. If None, use the last message.
|
msg: The message to display. If None, use the last message.
|
||||||
"""
|
"""
|
||||||
|
if self._ended:
|
||||||
|
return
|
||||||
self._done += inc
|
self._done += inc
|
||||||
if msg is None:
|
if msg is None:
|
||||||
msg = self._last_msg
|
msg = self._last_msg
|
||||||
self._last_msg = msg
|
self._last_msg = msg
|
||||||
|
|
||||||
if not _TTY or IsTraceToStderr():
|
if not _TTY or IsTraceToStderr() or self._quiet:
|
||||||
return
|
return
|
||||||
|
|
||||||
elapsed_sec = time.time() - self._start
|
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):
|
def end(self):
|
||||||
|
if self._ended:
|
||||||
|
return
|
||||||
|
self._ended = True
|
||||||
|
|
||||||
self._update_event.set()
|
self._update_event.set()
|
||||||
if not _TTY or IsTraceToStderr() or not self._show:
|
if not _TTY or IsTraceToStderr() or self._quiet:
|
||||||
return
|
return
|
||||||
|
|
||||||
duration = duration_str(time.time() - self._start)
|
duration = duration_str(time.time() - self._start)
|
||||||
|
|||||||
+850
-324
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");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -14,5 +14,33 @@
|
|||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 80
|
line-length = 80
|
||||||
# NB: Keep in sync with tox.ini.
|
|
||||||
target-version = ['py36', 'py37', 'py38', 'py39', 'py310', 'py311'] #, 'py312'
|
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
|
import util
|
||||||
|
|
||||||
|
|
||||||
|
assert sys.version_info >= (3, 9), "Release framework requires Python 3.9+"
|
||||||
|
|
||||||
|
|
||||||
def sign(opts):
|
def sign(opts):
|
||||||
"""Sign the launcher!"""
|
"""Sign the launcher!"""
|
||||||
output = ""
|
output = ""
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ import sys
|
|||||||
import util
|
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 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.
|
# We should transition to RSA by Jun 2020, and ECC by Jun 2021.
|
||||||
KEYID = util.KEYID_DSA
|
KEYID = util.KEYID_DSA
|
||||||
|
|||||||
Executable
+143
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Helper tool for updating hooks from their various upstreams."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
from typing import List, Optional
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
|
assert sys.version_info >= (3, 9), "Release framework requires Python 3.9+"
|
||||||
|
|
||||||
|
|
||||||
|
TOPDIR = Path(__file__).resolve().parent.parent
|
||||||
|
HOOKS_DIR = TOPDIR / "hooks"
|
||||||
|
|
||||||
|
|
||||||
|
def update_hook_commit_msg() -> None:
|
||||||
|
"""Update commit-msg hook from Gerrit."""
|
||||||
|
hook = HOOKS_DIR / "commit-msg"
|
||||||
|
print(
|
||||||
|
f"{hook.name}: Updating from https://gerrit.googlesource.com/gerrit/"
|
||||||
|
"+/HEAD/resources/com/google/gerrit/server/tools/root/hooks/commit-msg"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the current commit.
|
||||||
|
url = "https://gerrit.googlesource.com/gerrit/+/HEAD?format=JSON"
|
||||||
|
with urllib.request.urlopen(url) as fp:
|
||||||
|
data = fp.read()
|
||||||
|
# Discard the xss protection.
|
||||||
|
data = data.split(b"\n", 1)[1]
|
||||||
|
data = json.loads(data)
|
||||||
|
commit = data["commit"]
|
||||||
|
|
||||||
|
# Fetch the data for that commit.
|
||||||
|
url = (
|
||||||
|
f"https://gerrit.googlesource.com/gerrit/+/{commit}/"
|
||||||
|
"resources/com/google/gerrit/server/tools/root/hooks/commit-msg"
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(f"{url}?format=TEXT") as fp:
|
||||||
|
data = fp.read()
|
||||||
|
|
||||||
|
# gitiles base64 encodes text data.
|
||||||
|
data = base64.b64decode(data)
|
||||||
|
|
||||||
|
# Inject header into the hook.
|
||||||
|
lines = data.split(b"\n")
|
||||||
|
lines = (
|
||||||
|
lines[:1]
|
||||||
|
+ [
|
||||||
|
b"# DO NOT EDIT THIS FILE",
|
||||||
|
(
|
||||||
|
b"# All updates should be sent upstream: "
|
||||||
|
b"https://gerrit.googlesource.com/gerrit/"
|
||||||
|
),
|
||||||
|
f"# This is synced from commit: {commit}".encode("utf-8"),
|
||||||
|
b"# DO NOT EDIT THIS FILE",
|
||||||
|
]
|
||||||
|
+ lines[1:]
|
||||||
|
)
|
||||||
|
data = b"\n".join(lines)
|
||||||
|
|
||||||
|
# Update the hook.
|
||||||
|
hook.write_bytes(data)
|
||||||
|
hook.chmod(0o755)
|
||||||
|
|
||||||
|
|
||||||
|
def update_hook_pre_auto_gc() -> None:
|
||||||
|
"""Update pre-auto-gc hook from git."""
|
||||||
|
hook = HOOKS_DIR / "pre-auto-gc"
|
||||||
|
print(
|
||||||
|
f"{hook.name}: Updating from https://github.com/git/git/"
|
||||||
|
"HEAD/contrib/hooks/pre-auto-gc-battery"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the current commit.
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
}
|
||||||
|
url = "https://api.github.com/repos/git/git/git/refs/heads/master"
|
||||||
|
req = urllib.request.Request(url, headers=headers)
|
||||||
|
with urllib.request.urlopen(req) as fp:
|
||||||
|
data = fp.read()
|
||||||
|
data = json.loads(data)
|
||||||
|
|
||||||
|
# Fetch the data for that commit.
|
||||||
|
commit = data["object"]["sha"]
|
||||||
|
url = (
|
||||||
|
f"https://raw.githubusercontent.com/git/git/{commit}/"
|
||||||
|
"contrib/hooks/pre-auto-gc-battery"
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(url) as fp:
|
||||||
|
data = fp.read()
|
||||||
|
|
||||||
|
# Inject header into the hook.
|
||||||
|
lines = data.split(b"\n")
|
||||||
|
lines = (
|
||||||
|
lines[:1]
|
||||||
|
+ [
|
||||||
|
b"# DO NOT EDIT THIS FILE",
|
||||||
|
(
|
||||||
|
b"# All updates should be sent upstream: "
|
||||||
|
b"https://github.com/git/git/"
|
||||||
|
),
|
||||||
|
f"# This is synced from commit: {commit}".encode("utf-8"),
|
||||||
|
b"# DO NOT EDIT THIS FILE",
|
||||||
|
]
|
||||||
|
+ lines[1:]
|
||||||
|
)
|
||||||
|
data = b"\n".join(lines)
|
||||||
|
|
||||||
|
# Update the hook.
|
||||||
|
hook.write_bytes(data)
|
||||||
|
hook.chmod(0o755)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Optional[List[str]] = None) -> Optional[int]:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.parse_args(argv)
|
||||||
|
|
||||||
|
update_hook_commit_msg()
|
||||||
|
update_hook_pre_auto_gc()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv[1:]))
|
||||||
@@ -27,9 +27,15 @@ import shutil
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
TOPDIR = Path(__file__).resolve().parent.parent
|
# NB: This script is currently imported by tests/ to unittest some logic.
|
||||||
|
assert sys.version_info >= (3, 6), "Python 3.6+ required"
|
||||||
|
|
||||||
|
|
||||||
|
THIS_FILE = Path(__file__).resolve()
|
||||||
|
TOPDIR = THIS_FILE.parent.parent
|
||||||
MANDIR = TOPDIR.joinpath("man")
|
MANDIR = TOPDIR.joinpath("man")
|
||||||
|
|
||||||
# Load repo local modules.
|
# Load repo local modules.
|
||||||
@@ -42,9 +48,23 @@ def worker(cmd, **kwargs):
|
|||||||
subprocess.run(cmd, **kwargs)
|
subprocess.run(cmd, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def main(argv):
|
def get_parser() -> argparse.ArgumentParser:
|
||||||
|
"""Get argument parser."""
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: List[str]) -> int:
|
||||||
|
parser = get_parser()
|
||||||
|
opts = parser.parse_args(argv)
|
||||||
|
|
||||||
if not shutil.which("help2man"):
|
if not shutil.which("help2man"):
|
||||||
sys.exit("Please install help2man to continue.")
|
sys.exit("Please install help2man to continue.")
|
||||||
@@ -117,6 +137,7 @@ def main(argv):
|
|||||||
functools.partial(worker, cwd=tempdir, check=True), cmdlist
|
functools.partial(worker, cwd=tempdir, check=True), cmdlist
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ret = 0
|
||||||
for tmp_path in MANDIR.glob("*.1.tmp"):
|
for tmp_path in MANDIR.glob("*.1.tmp"):
|
||||||
path = tmp_path.parent / tmp_path.stem
|
path = tmp_path.parent / tmp_path.stem
|
||||||
old_data = path.read_text() if path.exists() else ""
|
old_data = path.read_text() if path.exists() else ""
|
||||||
@@ -133,7 +154,17 @@ def main(argv):
|
|||||||
)
|
)
|
||||||
new_data = re.sub(r'^(\.TH REPO "1" ")([^"]+)', r"\1", data, flags=re.M)
|
new_data = re.sub(r'^(\.TH REPO "1" ")([^"]+)', r"\1", data, flags=re.M)
|
||||||
if old_data != new_data:
|
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):
|
def replace_regex(data):
|
||||||
|
|||||||
+7
-10
@@ -14,8 +14,9 @@
|
|||||||
|
|
||||||
"""Random utility code for release tools."""
|
"""Random utility code for release tools."""
|
||||||
|
|
||||||
import os
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -23,8 +24,9 @@ import sys
|
|||||||
assert sys.version_info >= (3, 6), "This module requires Python 3.6+"
|
assert sys.version_info >= (3, 6), "This module requires Python 3.6+"
|
||||||
|
|
||||||
|
|
||||||
TOPDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
THIS_FILE = Path(__file__).resolve()
|
||||||
HOMEDIR = os.path.expanduser("~")
|
TOPDIR = THIS_FILE.parent.parent
|
||||||
|
HOMEDIR = Path("~").expanduser()
|
||||||
|
|
||||||
|
|
||||||
# These are the release keys we sign with.
|
# These are the release keys we sign with.
|
||||||
@@ -35,12 +37,7 @@ KEYID_ECC = "E1F9040D7A3F6DAFAC897CD3D3B95DA243E48A39"
|
|||||||
|
|
||||||
def cmdstr(cmd):
|
def cmdstr(cmd):
|
||||||
"""Get a nicely quoted shell command."""
|
"""Get a nicely quoted shell command."""
|
||||||
ret = []
|
return " ".join(shlex.quote(x) for x in cmd)
|
||||||
for arg in cmd:
|
|
||||||
if not re.match(r"^[a-zA-Z0-9/_.=-]+$", arg):
|
|
||||||
arg = f'"{arg}"'
|
|
||||||
ret.append(arg)
|
|
||||||
return " ".join(ret)
|
|
||||||
|
|
||||||
|
|
||||||
def run(opts, cmd, check=True, **kwargs):
|
def run(opts, cmd, check=True, **kwargs):
|
||||||
@@ -58,7 +55,7 @@ def run(opts, cmd, check=True, **kwargs):
|
|||||||
def import_release_key(opts):
|
def import_release_key(opts):
|
||||||
"""Import the public key of the official release repo signing key."""
|
"""Import the public key of the official release repo signing key."""
|
||||||
# Extract the key from our repo launcher.
|
# 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')
|
print(f'Importing keys from "{launcher}" launcher script')
|
||||||
with open(launcher, encoding="utf-8") as fp:
|
with open(launcher, encoding="utf-8") as fp:
|
||||||
data = fp.read()
|
data = fp.read()
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
#
|
|
||||||
# Copyright (C) 2008 The Android Open Source Project
|
# Copyright (C) 2008 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");
|
||||||
@@ -27,6 +26,7 @@ import platform
|
|||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
|
||||||
# These should never be newer than the main.py version since this needs to be a
|
# These should never be newer than the main.py version since this needs to be a
|
||||||
@@ -56,9 +56,14 @@ class Trace:
|
|||||||
trace = 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):
|
def exec_command(cmd):
|
||||||
"""Execute |cmd| or return None on failure."""
|
"""Execute |cmd| or return None on failure."""
|
||||||
trace.print(":", " ".join(cmd))
|
trace.print(":", cmdstr(cmd))
|
||||||
try:
|
try:
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
ret = subprocess.call(cmd)
|
ret = subprocess.call(cmd)
|
||||||
@@ -79,7 +84,7 @@ def check_python_version():
|
|||||||
major = ver.major
|
major = ver.major
|
||||||
minor = ver.minor
|
minor = ver.minor
|
||||||
|
|
||||||
# Try to re-exec the version specific Python 3 if needed.
|
# Try to re-exec the version specific Python if needed.
|
||||||
if (major, minor) < MIN_PYTHON_VERSION_SOFT:
|
if (major, minor) < MIN_PYTHON_VERSION_SOFT:
|
||||||
# Python makes releases ~once a year, so try our min version +10 to help
|
# Python makes releases ~once a year, so try our min version +10 to help
|
||||||
# bridge the gap. This is the fallback anyways so perf isn't critical.
|
# bridge the gap. This is the fallback anyways so perf isn't critical.
|
||||||
@@ -96,36 +101,10 @@ def check_python_version():
|
|||||||
break
|
break
|
||||||
reexec(f"python{min_major}.{min_minor - inc}")
|
reexec(f"python{min_major}.{min_minor - inc}")
|
||||||
|
|
||||||
# Try the generic Python 3 wrapper, but only if it's new enough. If it
|
|
||||||
# isn't, we want to just give up below and make the user resolve things.
|
|
||||||
try:
|
|
||||||
proc = subprocess.Popen(
|
|
||||||
[
|
|
||||||
"python3",
|
|
||||||
"-c",
|
|
||||||
"import sys; "
|
|
||||||
"print(sys.version_info.major, sys.version_info.minor)",
|
|
||||||
],
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
)
|
|
||||||
(output, _) = proc.communicate()
|
|
||||||
python3_ver = tuple(int(x) for x in output.decode("utf-8").split())
|
|
||||||
except (OSError, subprocess.CalledProcessError):
|
|
||||||
python3_ver = None
|
|
||||||
|
|
||||||
# If the python3 version looks like it's new enough, give it a try.
|
|
||||||
if (
|
|
||||||
python3_ver
|
|
||||||
and python3_ver >= MIN_PYTHON_VERSION_HARD
|
|
||||||
and python3_ver != (major, minor)
|
|
||||||
):
|
|
||||||
reexec("python3")
|
|
||||||
|
|
||||||
# We're still here, so diagnose things for the user.
|
# We're still here, so diagnose things for the user.
|
||||||
if (major, minor) < MIN_PYTHON_VERSION_HARD:
|
if (major, minor) < MIN_PYTHON_VERSION_HARD:
|
||||||
print(
|
print(
|
||||||
"repo: error: Python 3 version is too old; "
|
"repo: error: Python version is too old; "
|
||||||
"Please use Python {}.{} or newer.".format(
|
"Please use Python {}.{} or newer.".format(
|
||||||
*MIN_PYTHON_VERSION_HARD
|
*MIN_PYTHON_VERSION_HARD
|
||||||
),
|
),
|
||||||
@@ -150,7 +129,7 @@ if not REPO_REV:
|
|||||||
BUG_URL = "https://issues.gerritcodereview.com/issues/new?component=1370071"
|
BUG_URL = "https://issues.gerritcodereview.com/issues/new?component=1370071"
|
||||||
|
|
||||||
# increment this whenever we make important changes to this script
|
# increment this whenever we make important changes to this script
|
||||||
VERSION = (2, 40)
|
VERSION = (2, 54)
|
||||||
|
|
||||||
# increment this if the MAINTAINER_KEYS block is modified
|
# increment this if the MAINTAINER_KEYS block is modified
|
||||||
KEYRING_VERSION = (2, 3)
|
KEYRING_VERSION = (2, 3)
|
||||||
@@ -236,18 +215,13 @@ GIT = "git" # our git command
|
|||||||
# NB: The version of git that the repo launcher requires may be much older than
|
# NB: The version of git that the repo launcher requires may be much older than
|
||||||
# the version of git that the main repo source tree requires. Keeping this at
|
# the version of git that the main repo source tree requires. Keeping this at
|
||||||
# an older version also makes it easier for users to upgrade/rollback as needed.
|
# an older version also makes it easier for users to upgrade/rollback as needed.
|
||||||
#
|
MIN_GIT_VERSION = (1, 7, 9) # minimum supported git version
|
||||||
# git-1.7 is in (EOL) Ubuntu Precise.
|
|
||||||
MIN_GIT_VERSION = (1, 7, 2) # minimum supported git version
|
|
||||||
repodir = ".repo" # name of repo's private directory
|
repodir = ".repo" # name of repo's private directory
|
||||||
S_repo = "repo" # special repo repository
|
S_repo = "repo" # special repo repository
|
||||||
S_manifests = "manifests" # special manifest repository
|
S_manifests = "manifests" # special manifest repository
|
||||||
REPO_MAIN = S_repo + "/main.py" # main script
|
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 errno
|
||||||
import json
|
import json
|
||||||
import optparse
|
import optparse
|
||||||
@@ -263,12 +237,9 @@ home_dot_repo = os.path.join(repo_config_dir, ".repoconfig")
|
|||||||
gpg_dir = os.path.join(home_dot_repo, "gnupg")
|
gpg_dir = os.path.join(home_dot_repo, "gnupg")
|
||||||
|
|
||||||
|
|
||||||
def GetParser(gitc_init=False):
|
def GetParser():
|
||||||
"""Setup the CLI parser."""
|
"""Setup the CLI parser."""
|
||||||
if gitc_init:
|
usage = "repo init [options] [-u] url"
|
||||||
sys.exit("repo: fatal: GITC not supported.")
|
|
||||||
else:
|
|
||||||
usage = "repo init [options] [-u] url"
|
|
||||||
|
|
||||||
parser = optparse.OptionParser(usage=usage)
|
parser = optparse.OptionParser(usage=usage)
|
||||||
InitParser(parser)
|
InitParser(parser)
|
||||||
@@ -310,6 +281,12 @@ def InitParser(parser):
|
|||||||
metavar="REVISION",
|
metavar="REVISION",
|
||||||
help="manifest branch or revision (use HEAD for default)",
|
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(
|
group.add_option(
|
||||||
"-m",
|
"-m",
|
||||||
"--manifest-name",
|
"--manifest-name",
|
||||||
@@ -509,16 +486,6 @@ def InitParser(parser):
|
|||||||
return 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):
|
def run_command(cmd, **kwargs):
|
||||||
"""Run |cmd| and return its output."""
|
"""Run |cmd| and return its output."""
|
||||||
check = kwargs.pop("check", False)
|
check = kwargs.pop("check", False)
|
||||||
@@ -543,7 +510,7 @@ def run_command(cmd, **kwargs):
|
|||||||
# Run & package the results.
|
# Run & package the results.
|
||||||
proc = subprocess.Popen(cmd, **kwargs)
|
proc = subprocess.Popen(cmd, **kwargs)
|
||||||
(stdout, stderr) = proc.communicate(input=cmd_input)
|
(stdout, stderr) = proc.communicate(input=cmd_input)
|
||||||
dbg = ": " + " ".join(cmd)
|
dbg = ": " + cmdstr(cmd)
|
||||||
if cmd_input is not None:
|
if cmd_input is not None:
|
||||||
dbg += " 0<|"
|
dbg += " 0<|"
|
||||||
if stdout == subprocess.PIPE:
|
if stdout == subprocess.PIPE:
|
||||||
@@ -553,7 +520,9 @@ def run_command(cmd, **kwargs):
|
|||||||
elif stderr == subprocess.STDOUT:
|
elif stderr == subprocess.STDOUT:
|
||||||
dbg += " 2>&1"
|
dbg += " 2>&1"
|
||||||
trace.print(dbg)
|
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 things failed, print useful debugging output.
|
||||||
if check and ret.returncode:
|
if check and ret.returncode:
|
||||||
@@ -574,56 +543,13 @@ def run_command(cmd, **kwargs):
|
|||||||
|
|
||||||
_print_output("stdout", ret.stdout)
|
_print_output("stdout", ret.stdout)
|
||||||
_print_output("stderr", ret.stderr)
|
_print_output("stderr", ret.stderr)
|
||||||
raise RunError(ret)
|
# This will raise subprocess.CalledProcessError for us.
|
||||||
|
ret.check_returncode()
|
||||||
|
|
||||||
return ret
|
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):
|
class CloneFailure(Exception):
|
||||||
|
|
||||||
"""Indicate the remote clone of repo itself failed."""
|
"""Indicate the remote clone of repo itself failed."""
|
||||||
|
|
||||||
|
|
||||||
@@ -660,9 +586,9 @@ def check_repo_rev(dst, rev, repo_verify=True, quiet=False):
|
|||||||
return (remote_ref, rev)
|
return (remote_ref, rev)
|
||||||
|
|
||||||
|
|
||||||
def _Init(args, gitc_init=False):
|
def _Init(args):
|
||||||
"""Installs repo by cloning it over the network."""
|
"""Installs repo by cloning it over the network."""
|
||||||
parser = GetParser(gitc_init=gitc_init)
|
parser = GetParser()
|
||||||
opt, args = parser.parse_args(args)
|
opt, args = parser.parse_args(args)
|
||||||
if args:
|
if args:
|
||||||
if not opt.manifest_url:
|
if not opt.manifest_url:
|
||||||
@@ -742,15 +668,20 @@ def run_git(*args, **kwargs):
|
|||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except RunError:
|
except subprocess.CalledProcessError:
|
||||||
raise CloneFailure()
|
raise CloneFailure()
|
||||||
|
|
||||||
|
|
||||||
# The git version info broken down into components for easy analysis.
|
class GitVersion(NamedTuple):
|
||||||
# Similar to Python's sys.version_info.
|
"""The git version info broken down into components for easy analysis.
|
||||||
GitVersion = collections.namedtuple(
|
|
||||||
"GitVersion", ("major", "minor", "micro", "full")
|
Similar to Python's sys.version_info.
|
||||||
)
|
"""
|
||||||
|
|
||||||
|
major: int
|
||||||
|
minor: int
|
||||||
|
micro: int
|
||||||
|
full: int
|
||||||
|
|
||||||
|
|
||||||
def ParseGitVersion(ver_str=None):
|
def ParseGitVersion(ver_str=None):
|
||||||
@@ -916,10 +847,11 @@ def _GetRepoConfig(name):
|
|||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
f"repo: error: git {' '.join(cmd)} failed:\n{ret.stderr}",
|
f"repo: error: git {cmdstr(cmd)} failed:\n{ret.stderr}",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
raise RunError()
|
# This will raise subprocess.CalledProcessError for us.
|
||||||
|
ret.check_returncode()
|
||||||
|
|
||||||
|
|
||||||
def _InitHttp():
|
def _InitHttp():
|
||||||
@@ -1186,7 +1118,7 @@ class _Options:
|
|||||||
def _ExpandAlias(name):
|
def _ExpandAlias(name):
|
||||||
"""Look up user registered aliases."""
|
"""Look up user registered aliases."""
|
||||||
# We don't resolve aliases for existing subcommands. This matches git.
|
# 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, []
|
return name, []
|
||||||
|
|
||||||
alias = _GetRepoConfig(f"alias.{name}")
|
alias = _GetRepoConfig(f"alias.{name}")
|
||||||
@@ -1245,7 +1177,6 @@ class Requirements:
|
|||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
except OSError:
|
except OSError:
|
||||||
# NB: EnvironmentError is used for Python 2 & 3 compatibility.
|
|
||||||
# If we couldn't open the file, assume it's an old source tree.
|
# If we couldn't open the file, assume it's an old source tree.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -1265,13 +1196,13 @@ class Requirements:
|
|||||||
|
|
||||||
return cls(json_data)
|
return cls(json_data)
|
||||||
|
|
||||||
def _get_soft_ver(self, pkg):
|
def get_soft_ver(self, pkg):
|
||||||
"""Return the soft version for |pkg| if it exists."""
|
"""Return the soft version for |pkg| if it exists."""
|
||||||
return self.requirements.get(pkg, {}).get("soft", ())
|
return tuple(self.requirements.get(pkg, {}).get("soft", ()))
|
||||||
|
|
||||||
def _get_hard_ver(self, pkg):
|
def get_hard_ver(self, pkg):
|
||||||
"""Return the hard version for |pkg| if it exists."""
|
"""Return the hard version for |pkg| if it exists."""
|
||||||
return self.requirements.get(pkg, {}).get("hard", ())
|
return tuple(self.requirements.get(pkg, {}).get("hard", ()))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _format_ver(ver):
|
def _format_ver(ver):
|
||||||
@@ -1281,8 +1212,8 @@ class Requirements:
|
|||||||
def assert_ver(self, pkg, curr_ver):
|
def assert_ver(self, pkg, curr_ver):
|
||||||
"""Verify |pkg|'s |curr_ver| is new enough."""
|
"""Verify |pkg|'s |curr_ver| is new enough."""
|
||||||
curr_ver = tuple(curr_ver)
|
curr_ver = tuple(curr_ver)
|
||||||
soft_ver = tuple(self._get_soft_ver(pkg))
|
soft_ver = tuple(self.get_soft_ver(pkg))
|
||||||
hard_ver = tuple(self._get_hard_ver(pkg))
|
hard_ver = tuple(self.get_hard_ver(pkg))
|
||||||
if curr_ver < hard_ver:
|
if curr_ver < hard_ver:
|
||||||
print(
|
print(
|
||||||
f'repo: error: Your version of "{pkg}" '
|
f'repo: error: Your version of "{pkg}" '
|
||||||
@@ -1315,10 +1246,6 @@ class Requirements:
|
|||||||
|
|
||||||
|
|
||||||
def _Usage():
|
def _Usage():
|
||||||
gitc_usage = ""
|
|
||||||
if get_gitc_manifest_dir():
|
|
||||||
gitc_usage = " gitc-init Initialize a GITC Client.\n"
|
|
||||||
|
|
||||||
print(
|
print(
|
||||||
"""usage: repo COMMAND [ARGS]
|
"""usage: repo COMMAND [ARGS]
|
||||||
|
|
||||||
@@ -1327,9 +1254,7 @@ repo is not yet installed. Use "repo init" to install it here.
|
|||||||
The most commonly used repo commands are:
|
The most commonly used repo commands are:
|
||||||
|
|
||||||
init Install repo in the current working directory
|
init Install repo in the current working directory
|
||||||
"""
|
help Display detailed help on a command
|
||||||
+ gitc_usage
|
|
||||||
+ """ help Display detailed help on a command
|
|
||||||
|
|
||||||
For access to the full online help, install repo ("repo init").
|
For access to the full online help, install repo ("repo init").
|
||||||
"""
|
"""
|
||||||
@@ -1340,8 +1265,8 @@ For access to the full online help, install repo ("repo init").
|
|||||||
|
|
||||||
def _Help(args):
|
def _Help(args):
|
||||||
if args:
|
if args:
|
||||||
if args[0] in {"init", "gitc-init"}:
|
if args[0] in {"init"}:
|
||||||
parser = GetParser(gitc_init=args[0] == "gitc-init")
|
parser = GetParser()
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
else:
|
else:
|
||||||
@@ -1358,19 +1283,16 @@ def _Help(args):
|
|||||||
|
|
||||||
def _Version():
|
def _Version():
|
||||||
"""Show version information."""
|
"""Show version information."""
|
||||||
|
git_version = ParseGitVersion()
|
||||||
print("<repo not installed>")
|
print("<repo not installed>")
|
||||||
print(f"repo launcher version {'.'.join(str(x) for x in VERSION)}")
|
print(f"repo launcher version {'.'.join(str(x) for x in VERSION)}")
|
||||||
print(f" (from {__file__})")
|
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}")
|
print(f"Python {sys.version}")
|
||||||
uname = platform.uname()
|
uname = platform.uname()
|
||||||
if sys.version_info.major < 3:
|
print(f"OS {uname.system} {uname.release} ({uname.version})")
|
||||||
# Python 3 returns a named tuple, but Python 2 is simpler.
|
processor = uname.processor if uname.processor else "unknown"
|
||||||
print(uname)
|
print(f"CPU {uname.machine} ({processor})")
|
||||||
else:
|
|
||||||
print(f"OS {uname.system} {uname.release} ({uname.version})")
|
|
||||||
processor = uname.processor if uname.processor else "unknown"
|
|
||||||
print(f"CPU {uname.machine} ({processor})")
|
|
||||||
print("Bug reports:", BUG_URL)
|
print("Bug reports:", BUG_URL)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
@@ -1398,11 +1320,11 @@ def _RunSelf(wrapper_path):
|
|||||||
my_main = os.path.join(my_dir, "main.py")
|
my_main = os.path.join(my_dir, "main.py")
|
||||||
my_git = os.path.join(my_dir, ".git")
|
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"]:
|
for name in ["git_config.py", "project.py", "subcmds"]:
|
||||||
if not os.path.exists(os.path.join(my_dir, name)):
|
if not os.path.exists(os.path.join(my_dir, name)):
|
||||||
return None, None
|
return None, None
|
||||||
return my_main, my_git
|
return my_main, my_git if os.path.isdir(my_git) else None
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
@@ -1433,23 +1355,11 @@ def main(orig_args):
|
|||||||
# We run this early as we run some git commands ourselves.
|
# We run this early as we run some git commands ourselves.
|
||||||
SetGitTrace2ParentSid()
|
SetGitTrace2ParentSid()
|
||||||
|
|
||||||
repo_main, rel_repo_dir = None, None
|
repo_main, rel_repo_dir = _FindRepo()
|
||||||
# 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()
|
|
||||||
|
|
||||||
wrapper_path = os.path.abspath(__file__)
|
wrapper_path = os.path.abspath(__file__)
|
||||||
my_main, my_git = _RunSelf(wrapper_path)
|
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:
|
if not repo_main:
|
||||||
# Only expand aliases here since we'll be parsing the CLI ourselves.
|
# Only expand aliases here since we'll be parsing the CLI ourselves.
|
||||||
# If we had repo_main, alias expansion would happen in main.py.
|
# If we had repo_main, alias expansion would happen in main.py.
|
||||||
@@ -1464,11 +1374,11 @@ def main(orig_args):
|
|||||||
_Version()
|
_Version()
|
||||||
if not cmd:
|
if not cmd:
|
||||||
_NotInstalled()
|
_NotInstalled()
|
||||||
if cmd == "init" or cmd == "gitc-init":
|
if cmd == "init":
|
||||||
if my_git:
|
if my_git:
|
||||||
_SetDefaultsTo(my_git)
|
_SetDefaultsTo(my_git)
|
||||||
try:
|
try:
|
||||||
_Init(args, gitc_init=(cmd == "gitc-init"))
|
_Init(args)
|
||||||
except CloneFailure:
|
except CloneFailure:
|
||||||
path = os.path.join(repodir, S_repo)
|
path = os.path.join(repodir, S_repo)
|
||||||
print(
|
print(
|
||||||
|
|||||||
+2
-2
@@ -39,8 +39,8 @@ class _LogColoring(Coloring):
|
|||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
super().__init__(config, "logs")
|
super().__init__(config, "logs")
|
||||||
self.error = self.colorer("error", fg="red")
|
self.error = self.nofmt_colorer("error", fg="red")
|
||||||
self.warning = self.colorer("warn", fg="yellow")
|
self.warning = self.nofmt_colorer("warn", fg="yellow")
|
||||||
self.levelMap = {
|
self.levelMap = {
|
||||||
"WARNING": self.warning,
|
"WARNING": self.warning,
|
||||||
"ERROR": self.error,
|
"ERROR": self.error,
|
||||||
|
|||||||
+7
-5
@@ -46,12 +46,14 @@
|
|||||||
|
|
||||||
# Supported git versions.
|
# Supported git versions.
|
||||||
#
|
#
|
||||||
# git-1.7.2 is in Debian Squeeze.
|
|
||||||
# git-1.7.9 is in Ubuntu Precise.
|
|
||||||
# git-1.9.1 is in Ubuntu Trusty.
|
# git-1.9.1 is in Ubuntu Trusty.
|
||||||
# git-1.7.10 is in Debian Wheezy.
|
# git-2.1.4 is in Debian Jessie.
|
||||||
|
# git-2.7.4 is in Ubuntu Xenial.
|
||||||
|
# git-2.11.0 is in Debian Stretch.
|
||||||
|
# git-2.17.0 is in Ubuntu Bionic.
|
||||||
|
# git-2.20.1 is in Debian Buster.
|
||||||
"git": {
|
"git": {
|
||||||
"hard": [1, 7, 2],
|
"hard": [1, 9, 1],
|
||||||
"soft": [1, 9, 1]
|
"soft": [2, 7, 4]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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 not use this file except in compliance with the License.
|
||||||
@@ -15,54 +15,176 @@
|
|||||||
|
|
||||||
"""Wrapper to run linters and pytest with the right settings."""
|
"""Wrapper to run linters and pytest with the right settings."""
|
||||||
|
|
||||||
|
import functools
|
||||||
import os
|
import os
|
||||||
|
import shlex
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
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__))
|
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():
|
def run_black():
|
||||||
"""Returns the exit code from 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
|
# Black by default only matches .py files. We have to list standalone
|
||||||
# scripts manually.
|
# scripts manually.
|
||||||
extra_programs = [
|
extra_programs = [
|
||||||
"repo",
|
"repo",
|
||||||
"run_tests",
|
"run_tests",
|
||||||
|
"release/update-hooks",
|
||||||
"release/update-manpages",
|
"release/update-manpages",
|
||||||
]
|
]
|
||||||
|
argv = ["--diff", "--check", ROOT_DIR] + extra_programs
|
||||||
|
log_cmd("black", argv)
|
||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
[sys.executable, "-m", "black", "--check", ROOT_DIR] + extra_programs,
|
[sys.executable, "-m", "black"] + argv,
|
||||||
check=False,
|
check=False,
|
||||||
|
cwd=ROOT_DIR,
|
||||||
).returncode
|
).returncode
|
||||||
|
|
||||||
|
|
||||||
def run_flake8():
|
def run_flake8():
|
||||||
"""Returns the exit code from 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(
|
return subprocess.run(
|
||||||
[sys.executable, "-m", "flake8", ROOT_DIR], check=False
|
[sys.executable, "-m", "flake8"] + argv,
|
||||||
|
check=False,
|
||||||
|
cwd=ROOT_DIR,
|
||||||
).returncode
|
).returncode
|
||||||
|
|
||||||
|
|
||||||
def run_isort():
|
def run_isort():
|
||||||
"""Returns the exit code from 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(
|
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."""
|
||||||
|
# Allow this to fail on CI, but not local devs.
|
||||||
|
if is_ci() and not shutil.which("help2man"):
|
||||||
|
print("update-manpages: help2man not found; skipping test")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
argv = ["--check"]
|
||||||
|
log_cmd("release/update-manpages", argv)
|
||||||
|
return subprocess.run(
|
||||||
|
[sys.executable, "release/update-manpages"] + argv,
|
||||||
|
check=False,
|
||||||
|
cwd=ROOT_DIR,
|
||||||
).returncode
|
).returncode
|
||||||
|
|
||||||
|
|
||||||
def main(argv):
|
def main(argv):
|
||||||
"""The main entry."""
|
"""The main entry."""
|
||||||
checks = (
|
checks = (
|
||||||
lambda: pytest.main(argv),
|
functools.partial(run_pytest, argv),
|
||||||
|
functools.partial(run_pytest_py38, argv),
|
||||||
run_black,
|
run_black,
|
||||||
run_flake8,
|
run_flake8,
|
||||||
run_isort,
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+31
-26
@@ -5,97 +5,92 @@
|
|||||||
# List of available wheels:
|
# List of available wheels:
|
||||||
# https://chromium.googlesource.com/infra/infra/+/main/infra/tools/dockerbuild/wheels.md
|
# https://chromium.googlesource.com/infra/infra/+/main/infra/tools/dockerbuild/wheels.md
|
||||||
|
|
||||||
python_version: "3.8"
|
python_version: "3.11"
|
||||||
|
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/pytest-py3"
|
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: <
|
wheel: <
|
||||||
name: "infra/python/wheels/py-py2_py3"
|
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: <
|
wheel: <
|
||||||
name: "infra/python/wheels/iniconfig-py3"
|
name: "infra/python/wheels/iniconfig-py3"
|
||||||
version: "version:1.1.1"
|
version: "version:1.1.1"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by pytest==6.2.2
|
# Required by pytest==8.3.4
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/packaging-py3"
|
name: "infra/python/wheels/packaging-py3"
|
||||||
version: "version:23.0"
|
version: "version:23.0"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by pytest==6.2.2
|
# Required by pytest==8.3.4
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/pluggy-py3"
|
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: <
|
wheel: <
|
||||||
name: "infra/python/wheels/toml-py3"
|
name: "infra/python/wheels/toml-py3"
|
||||||
version: "version:0.10.1"
|
version: "version:0.10.1"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by pytest==6.2.2
|
# Required by pytest==8.3.4
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/pyparsing-py3"
|
name: "infra/python/wheels/pyparsing-py3"
|
||||||
version: "version:3.0.7"
|
version: "version:3.0.7"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by pytest==6.2.2
|
# Required by pytest==8.3.4 and flake8-bugbear==24.12.12
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/attrs-py2_py3"
|
name: "infra/python/wheels/attrs-py3"
|
||||||
version: "version:21.4.0"
|
version: "version:24.2.0"
|
||||||
>
|
|
||||||
|
|
||||||
# Required by packaging==16.8
|
|
||||||
wheel: <
|
|
||||||
name: "infra/python/wheels/six-py2_py3"
|
|
||||||
version: "version:1.16.0"
|
|
||||||
>
|
>
|
||||||
|
|
||||||
|
# NB: Keep in sync with constraints.txt.
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/black-py3"
|
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: <
|
wheel: <
|
||||||
name: "infra/python/wheels/mypy-extensions-py3"
|
name: "infra/python/wheels/mypy-extensions-py3"
|
||||||
version: "version:0.4.3"
|
version: "version:0.4.3"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by black==23.1.0
|
# Required by black==25.1.0
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/tomli-py3"
|
name: "infra/python/wheels/tomli-py3"
|
||||||
version: "version:2.0.1"
|
version: "version:2.0.1"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by black==23.1.0
|
# Required by black==25.1.0
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/platformdirs-py3"
|
name: "infra/python/wheels/platformdirs-py3"
|
||||||
version: "version:2.5.2"
|
version: "version:2.5.2"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by black==23.1.0
|
# Required by black==25.1.0
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/pathspec-py3"
|
name: "infra/python/wheels/pathspec-py3"
|
||||||
version: "version:0.9.0"
|
version: "version:0.9.0"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by black==23.1.0
|
# Required by black==25.1.0
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/typing-extensions-py3"
|
name: "infra/python/wheels/typing-extensions-py3"
|
||||||
version: "version:4.3.0"
|
version: "version:4.3.0"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by black==23.1.0
|
# Required by black==25.1.0
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/click-py3"
|
name: "infra/python/wheels/click-py3"
|
||||||
version: "version:8.0.3"
|
version: "version:8.0.3"
|
||||||
@@ -124,6 +119,16 @@ wheel: <
|
|||||||
version: "version:2.10.0"
|
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: <
|
wheel: <
|
||||||
name: "infra/python/wheels/isort-py3"
|
name: "infra/python/wheels/isort-py3"
|
||||||
version: "version:5.10.1"
|
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
|
#!/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 not use this file except in compliance with the License.
|
||||||
# You may obtain a copy of the License at
|
# You may obtain a copy of the License at
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from git_command import git
|
||||||
import platform_utils
|
import platform_utils
|
||||||
from repo_trace import Trace
|
from repo_trace import Trace
|
||||||
|
|
||||||
@@ -51,14 +52,18 @@ def _parse_ssh_version(ver_str=None):
|
|||||||
|
|
||||||
@functools.lru_cache(maxsize=None)
|
@functools.lru_cache(maxsize=None)
|
||||||
def version():
|
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:
|
try:
|
||||||
return _parse_ssh_version()
|
return _parse_ssh_version()
|
||||||
except FileNotFoundError:
|
except subprocess.CalledProcessError as e:
|
||||||
print("fatal: ssh not installed", file=sys.stderr)
|
print(
|
||||||
sys.exit(1)
|
"fatal: unable to detect ssh version"
|
||||||
except subprocess.CalledProcessError:
|
f" (code={e.returncode}, output={e.stdout})",
|
||||||
print("fatal: unable to detect ssh version", file=sys.stderr)
|
file=sys.stderr,
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -97,9 +102,18 @@ class ProxyManager:
|
|||||||
self._clients = manager.list()
|
self._clients = manager.list()
|
||||||
# Path to directory for holding master sockets.
|
# Path to directory for holding master sockets.
|
||||||
self._sock_path = None
|
self._sock_path = None
|
||||||
|
# See if ssh is usable.
|
||||||
|
self._ssh_installed = False
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
"""Enter a new context."""
|
"""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
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback):
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
@@ -135,7 +149,7 @@ class ProxyManager:
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
procs.pop(0)
|
procs.pop(0)
|
||||||
except: # noqa: E722
|
except IndexError:
|
||||||
break
|
break
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
@@ -207,7 +221,33 @@ class ProxyManager:
|
|||||||
# and print to the log there.
|
# and print to the log there.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
command = command_base[:1] + ["-M", "-N"] + command_base[1:]
|
# Git protocol V2 is a new feature in git 2.18.0, made default in
|
||||||
|
# git 2.26.0
|
||||||
|
# It is faster and more efficient than V1.
|
||||||
|
# To enable it when using SSH, the environment variable GIT_PROTOCOL
|
||||||
|
# must be set in the SSH side channel when establishing the connection
|
||||||
|
# to the git server.
|
||||||
|
# See https://git-scm.com/docs/protocol-v2#_ssh_and_file_transport
|
||||||
|
# Normally git does this by itself. But here, where the SSH connection
|
||||||
|
# is established manually over ControlMaster via the repo-tool, it must
|
||||||
|
# be passed in explicitly instead.
|
||||||
|
# Based on https://git-scm.com/docs/gitprotocol-pack#_extra_parameters,
|
||||||
|
# GIT_PROTOCOL is considered an "Extra Parameter" and must be ignored
|
||||||
|
# by servers that do not understand it. This means that it is safe to
|
||||||
|
# set it even when connecting to older servers.
|
||||||
|
# It should also be safe to set the environment variable for older
|
||||||
|
# local git versions, since it is only part of the ssh side channel.
|
||||||
|
git_protocol_version = _get_git_protocol_version()
|
||||||
|
ssh_git_protocol_args = [
|
||||||
|
"-o",
|
||||||
|
f"SetEnv GIT_PROTOCOL=version={git_protocol_version}",
|
||||||
|
]
|
||||||
|
|
||||||
|
command = (
|
||||||
|
command_base[:1]
|
||||||
|
+ ["-M", "-N", *ssh_git_protocol_args]
|
||||||
|
+ command_base[1:]
|
||||||
|
)
|
||||||
p = None
|
p = None
|
||||||
try:
|
try:
|
||||||
with Trace("Call to ssh: %s", " ".join(command)):
|
with Trace("Call to ssh: %s", " ".join(command)):
|
||||||
@@ -251,6 +291,9 @@ class ProxyManager:
|
|||||||
|
|
||||||
def preconnect(self, url):
|
def preconnect(self, url):
|
||||||
"""If |uri| will create a ssh connection, setup the ssh master for it.""" # noqa: E501
|
"""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)
|
m = URI_ALL.match(url)
|
||||||
if m:
|
if m:
|
||||||
scheme = m.group(1)
|
scheme = m.group(1)
|
||||||
@@ -275,6 +318,9 @@ class ProxyManager:
|
|||||||
|
|
||||||
This has all the master sockets so clients can talk to them.
|
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 self._sock_path is None:
|
||||||
if not create:
|
if not create:
|
||||||
return None
|
return None
|
||||||
@@ -289,3 +335,32 @@ class ProxyManager:
|
|||||||
tempfile.mkdtemp("", "ssh-", tmp_dir), "master-" + tokens
|
tempfile.mkdtemp("", "ssh-", tmp_dir), "master-" + tokens
|
||||||
)
|
)
|
||||||
return self._sock_path
|
return self._sock_path
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=1)
|
||||||
|
def _get_git_protocol_version() -> str:
|
||||||
|
"""Return the git protocol version.
|
||||||
|
|
||||||
|
The version is found by first reading the global git config.
|
||||||
|
If no git config for protocol version exists, try to deduce the default
|
||||||
|
protocol version based on the git version.
|
||||||
|
|
||||||
|
See https://git-scm.com/docs/gitprotocol-v2 for details.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return subprocess.check_output(
|
||||||
|
["git", "config", "--get", "--global", "protocol.version"],
|
||||||
|
encoding="utf-8",
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
).strip()
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
if e.returncode == 1:
|
||||||
|
# Exit code 1 means that the git config key was not found.
|
||||||
|
# Try to imitate the defaults that git would have used.
|
||||||
|
git_version = git.version_tuple()
|
||||||
|
if git_version >= (2, 26, 0):
|
||||||
|
# Since git version 2.26, protocol v2 is the default.
|
||||||
|
return "2"
|
||||||
|
return "1"
|
||||||
|
# Other exit codes indicate error with reading the config.
|
||||||
|
raise
|
||||||
|
|||||||
+18
-13
@@ -48,7 +48,6 @@ It is equivalent to "git branch -D <branchname>".
|
|||||||
def _Options(self, p):
|
def _Options(self, p):
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--all",
|
"--all",
|
||||||
dest="all",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="delete all branches in all projects",
|
help="delete all branches in all projects",
|
||||||
)
|
)
|
||||||
@@ -70,8 +69,10 @@ It is equivalent to "git branch -D <branchname>".
|
|||||||
else:
|
else:
|
||||||
args.insert(0, "'All local branches'")
|
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."""
|
"""Abandon one project."""
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
if all_branches:
|
if all_branches:
|
||||||
branches = project.GetBranches()
|
branches = project.GetBranches()
|
||||||
else:
|
else:
|
||||||
@@ -89,7 +90,7 @@ It is equivalent to "git branch -D <branchname>".
|
|||||||
if status is not None:
|
if status is not None:
|
||||||
ret[name] = status
|
ret[name] = status
|
||||||
|
|
||||||
return (ret, project, errors)
|
return (ret, project_idx, errors)
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
nb = args[0].split()
|
nb = args[0].split()
|
||||||
@@ -102,7 +103,8 @@ It is equivalent to "git branch -D <branchname>".
|
|||||||
_RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
|
_RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
|
||||||
|
|
||||||
def _ProcessResults(_pool, pm, states):
|
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():
|
for branch, status in results.items():
|
||||||
if status:
|
if status:
|
||||||
success[branch].append(project)
|
success[branch].append(project)
|
||||||
@@ -111,15 +113,18 @@ It is equivalent to "git branch -D <branchname>".
|
|||||||
aggregate_errors.extend(errors)
|
aggregate_errors.extend(errors)
|
||||||
pm.update(msg="")
|
pm.update(msg="")
|
||||||
|
|
||||||
self.ExecuteInParallel(
|
with self.ParallelContext():
|
||||||
opt.jobs,
|
self.get_parallel_context()["projects"] = all_projects
|
||||||
functools.partial(self._ExecuteOne, opt.all, nb),
|
self.ExecuteInParallel(
|
||||||
all_projects,
|
opt.jobs,
|
||||||
callback=_ProcessResults,
|
functools.partial(self._ExecuteOne, opt.all, nb),
|
||||||
output=Progress(
|
range(len(all_projects)),
|
||||||
f"Abandon {nb}", len(all_projects), quiet=opt.quiet
|
callback=_ProcessResults,
|
||||||
),
|
output=Progress(
|
||||||
)
|
f"Abandon {nb}", len(all_projects), quiet=opt.quiet
|
||||||
|
),
|
||||||
|
chunksize=1,
|
||||||
|
)
|
||||||
|
|
||||||
width = max(
|
width = max(
|
||||||
itertools.chain(
|
itertools.chain(
|
||||||
|
|||||||
+30
-24
@@ -98,6 +98,22 @@ is shown, then the branch appears in all projects.
|
|||||||
"""
|
"""
|
||||||
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
|
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):
|
def Execute(self, opt, args):
|
||||||
projects = self.GetProjects(
|
projects = self.GetProjects(
|
||||||
args, all_manifests=not opt.this_manifest_only
|
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)
|
project_cnt = len(projects)
|
||||||
|
|
||||||
def _ProcessResults(_pool, _output, results):
|
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:
|
if name not in all_branches:
|
||||||
all_branches[name] = BranchInfo(name)
|
all_branches[name] = BranchInfo(name)
|
||||||
all_branches[name].add(b)
|
all_branches[name].add(b)
|
||||||
|
|
||||||
self.ExecuteInParallel(
|
with self.ParallelContext():
|
||||||
opt.jobs,
|
self.get_parallel_context()["projects"] = projects
|
||||||
expand_project_to_branches,
|
self.ExecuteInParallel(
|
||||||
projects,
|
opt.jobs,
|
||||||
callback=_ProcessResults,
|
self._ExpandProjectToBranches,
|
||||||
)
|
range(len(projects)),
|
||||||
|
callback=_ProcessResults,
|
||||||
|
)
|
||||||
|
|
||||||
names = sorted(all_branches)
|
names = sorted(all_branches)
|
||||||
|
|
||||||
@@ -148,7 +167,10 @@ is shown, then the branch appears in all projects.
|
|||||||
else:
|
else:
|
||||||
published = " "
|
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(" |")
|
out.write(" |")
|
||||||
|
|
||||||
_RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
|
_RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
|
||||||
@@ -191,19 +213,3 @@ is shown, then the branch appears in all projects.
|
|||||||
else:
|
else:
|
||||||
out.write(" in all projects")
|
out.write(" in all projects")
|
||||||
out.nl()
|
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 GitError
|
||||||
from error import RepoExitError
|
from error import RepoExitError
|
||||||
from progress import Progress
|
from progress import Progress
|
||||||
from project import Project
|
|
||||||
from repo_logging import RepoLogger
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ logger = RepoLogger(__file__)
|
|||||||
class CheckoutBranchResult(NamedTuple):
|
class CheckoutBranchResult(NamedTuple):
|
||||||
# Whether the Project is on the branch (i.e. branch exists and no errors)
|
# Whether the Project is on the branch (i.e. branch exists and no errors)
|
||||||
result: bool
|
result: bool
|
||||||
project: Project
|
project_idx: int
|
||||||
error: Exception
|
error: Exception
|
||||||
|
|
||||||
|
|
||||||
@@ -62,15 +61,17 @@ The command is equivalent to:
|
|||||||
if not args:
|
if not args:
|
||||||
self.Usage()
|
self.Usage()
|
||||||
|
|
||||||
def _ExecuteOne(self, nb, project):
|
@classmethod
|
||||||
|
def _ExecuteOne(cls, nb, project_idx):
|
||||||
"""Checkout one project."""
|
"""Checkout one project."""
|
||||||
error = None
|
error = None
|
||||||
result = None
|
result = None
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
try:
|
try:
|
||||||
result = project.CheckoutBranch(nb)
|
result = project.CheckoutBranch(nb)
|
||||||
except GitError as e:
|
except GitError as e:
|
||||||
error = e
|
error = e
|
||||||
return CheckoutBranchResult(result, project, error)
|
return CheckoutBranchResult(result, project_idx, error)
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
nb = args[0]
|
nb = args[0]
|
||||||
@@ -83,22 +84,25 @@ The command is equivalent to:
|
|||||||
|
|
||||||
def _ProcessResults(_pool, pm, results):
|
def _ProcessResults(_pool, pm, results):
|
||||||
for result in results:
|
for result in results:
|
||||||
|
project = all_projects[result.project_idx]
|
||||||
if result.error is not None:
|
if result.error is not None:
|
||||||
err.append(result.error)
|
err.append(result.error)
|
||||||
err_projects.append(result.project)
|
err_projects.append(project)
|
||||||
elif result.result:
|
elif result.result:
|
||||||
success.append(result.project)
|
success.append(project)
|
||||||
pm.update(msg="")
|
pm.update(msg="")
|
||||||
|
|
||||||
self.ExecuteInParallel(
|
with self.ParallelContext():
|
||||||
opt.jobs,
|
self.get_parallel_context()["projects"] = all_projects
|
||||||
functools.partial(self._ExecuteOne, nb),
|
self.ExecuteInParallel(
|
||||||
all_projects,
|
opt.jobs,
|
||||||
callback=_ProcessResults,
|
functools.partial(self._ExecuteOne, nb),
|
||||||
output=Progress(
|
range(len(all_projects)),
|
||||||
f"Checkout {nb}", len(all_projects), quiet=opt.quiet
|
callback=_ProcessResults,
|
||||||
),
|
output=Progress(
|
||||||
)
|
f"Checkout {nb}", len(all_projects), quiet=opt.quiet
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if err_projects:
|
if err_projects:
|
||||||
for p in err_projects:
|
for p in err_projects:
|
||||||
|
|||||||
+16
-12
@@ -35,12 +35,12 @@ to the Unix 'patch' command.
|
|||||||
p.add_option(
|
p.add_option(
|
||||||
"-u",
|
"-u",
|
||||||
"--absolute",
|
"--absolute",
|
||||||
dest="absolute",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="paths are relative to the repository root",
|
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.
|
"""Obtains the diff for a specific project.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -48,12 +48,13 @@ to the Unix 'patch' command.
|
|||||||
local: a boolean, if True, the path is relative to the local
|
local: a boolean, if True, the path is relative to the local
|
||||||
(sub)manifest. If false, the path is relative to the outermost
|
(sub)manifest. If false, the path is relative to the outermost
|
||||||
manifest.
|
manifest.
|
||||||
project: Project to get status of.
|
project_idx: Project index to get status of.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The status of the project.
|
The status of the project.
|
||||||
"""
|
"""
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
ret = project.PrintWorkTreeDiff(absolute, output_redir=buf, local=local)
|
ret = project.PrintWorkTreeDiff(absolute, output_redir=buf, local=local)
|
||||||
return (ret, buf.getvalue())
|
return (ret, buf.getvalue())
|
||||||
|
|
||||||
@@ -71,12 +72,15 @@ to the Unix 'patch' command.
|
|||||||
ret = 1
|
ret = 1
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
return self.ExecuteInParallel(
|
with self.ParallelContext():
|
||||||
opt.jobs,
|
self.get_parallel_context()["projects"] = all_projects
|
||||||
functools.partial(
|
return self.ExecuteInParallel(
|
||||||
self._ExecuteOne, opt.absolute, opt.this_manifest_only
|
opt.jobs,
|
||||||
),
|
functools.partial(
|
||||||
all_projects,
|
self._ExecuteOne, opt.absolute, opt.this_manifest_only
|
||||||
callback=_ProcessResults,
|
),
|
||||||
ordered=True,
|
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):
|
def _Options(self, p):
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--raw", dest="raw", action="store_true", help="display raw diff"
|
"--raw",
|
||||||
|
action="store_true",
|
||||||
|
help="display raw diff",
|
||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--no-color",
|
"--no-color",
|
||||||
@@ -78,7 +80,6 @@ synced and their revisions won't be found.
|
|||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--pretty-format",
|
"--pretty-format",
|
||||||
dest="pretty_format",
|
|
||||||
action="store",
|
action="store",
|
||||||
metavar="<FORMAT>",
|
metavar="<FORMAT>",
|
||||||
help="print the log using a custom git pretty format string",
|
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")
|
self.printRevision = self.out.nofmt_printer("revision", fg="yellow")
|
||||||
else:
|
else:
|
||||||
self.printProject = (
|
self.printProject = self.printAdded = self.printRemoved = (
|
||||||
self.printAdded
|
self.printRevision
|
||||||
) = self.printRemoved = self.printRevision = self.printText
|
) = self.printText
|
||||||
|
|
||||||
manifest1 = RepoClient(self.repodir)
|
manifest1 = RepoClient(self.repodir)
|
||||||
manifest1.Override(args[0], load_local_manifests=False)
|
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(
|
p.add_option(
|
||||||
"-r",
|
"-r",
|
||||||
"--revert",
|
"--revert",
|
||||||
dest="revert",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="revert instead of checkout",
|
help="revert instead of checkout",
|
||||||
)
|
)
|
||||||
|
|||||||
+53
-51
@@ -15,7 +15,6 @@
|
|||||||
import errno
|
import errno
|
||||||
import functools
|
import functools
|
||||||
import io
|
import io
|
||||||
import multiprocessing
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
@@ -26,7 +25,6 @@ from color import Coloring
|
|||||||
from command import Command
|
from command import Command
|
||||||
from command import DEFAULT_LOCAL_JOBS
|
from command import DEFAULT_LOCAL_JOBS
|
||||||
from command import MirrorSafeCommand
|
from command import MirrorSafeCommand
|
||||||
from command import WORKER_BATCH_SIZE
|
|
||||||
from error import ManifestInvalidRevisionError
|
from error import ManifestInvalidRevisionError
|
||||||
from repo_logging import RepoLogger
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
@@ -143,7 +141,6 @@ without iterating through the remaining projects.
|
|||||||
p.add_option(
|
p.add_option(
|
||||||
"-r",
|
"-r",
|
||||||
"--regex",
|
"--regex",
|
||||||
dest="regex",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="execute the command only on projects matching regex or "
|
help="execute the command only on projects matching regex or "
|
||||||
"wildcard expression",
|
"wildcard expression",
|
||||||
@@ -151,7 +148,6 @@ without iterating through the remaining projects.
|
|||||||
p.add_option(
|
p.add_option(
|
||||||
"-i",
|
"-i",
|
||||||
"--inverse-regex",
|
"--inverse-regex",
|
||||||
dest="inverse_regex",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="execute the command only on projects not matching regex or "
|
help="execute the command only on projects not matching regex or "
|
||||||
"wildcard expression",
|
"wildcard expression",
|
||||||
@@ -159,22 +155,20 @@ without iterating through the remaining projects.
|
|||||||
p.add_option(
|
p.add_option(
|
||||||
"-g",
|
"-g",
|
||||||
"--groups",
|
"--groups",
|
||||||
dest="groups",
|
|
||||||
help="execute the command only on projects matching the specified "
|
help="execute the command only on projects matching the specified "
|
||||||
"groups",
|
"groups",
|
||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"-c",
|
"-c",
|
||||||
"--command",
|
"--command",
|
||||||
help="command (and arguments) to execute",
|
|
||||||
dest="command",
|
dest="command",
|
||||||
|
help="command (and arguments) to execute",
|
||||||
action="callback",
|
action="callback",
|
||||||
callback=self._cmd_option,
|
callback=self._cmd_option,
|
||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"-e",
|
"-e",
|
||||||
"--abort-on-errors",
|
"--abort-on-errors",
|
||||||
dest="abort_on_errors",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="abort if a command exits unsuccessfully",
|
help="abort if a command exits unsuccessfully",
|
||||||
)
|
)
|
||||||
@@ -241,7 +235,6 @@ without iterating through the remaining projects.
|
|||||||
cmd.insert(cmd.index(cn) + 1, "--color")
|
cmd.insert(cmd.index(cn) + 1, "--color")
|
||||||
|
|
||||||
mirror = self.manifest.IsMirror
|
mirror = self.manifest.IsMirror
|
||||||
rc = 0
|
|
||||||
|
|
||||||
smart_sync_manifest_name = "smart_sync_override.xml"
|
smart_sync_manifest_name = "smart_sync_override.xml"
|
||||||
smart_sync_manifest_path = os.path.join(
|
smart_sync_manifest_path = os.path.join(
|
||||||
@@ -264,35 +257,44 @@ without iterating through the remaining projects.
|
|||||||
|
|
||||||
os.environ["REPO_COUNT"] = str(len(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:
|
try:
|
||||||
config = self.manifest.manifestProject.config
|
config = self.manifest.manifestProject.config
|
||||||
with multiprocessing.Pool(opt.jobs, InitWorker) as pool:
|
with self.ParallelContext():
|
||||||
results_it = pool.imap(
|
self.get_parallel_context()["projects"] = projects
|
||||||
|
rc = self.ExecuteInParallel(
|
||||||
|
opt.jobs,
|
||||||
functools.partial(
|
functools.partial(
|
||||||
DoWorkWrapper, mirror, opt, cmd, shell, config
|
self.DoWorkWrapper, mirror, opt, cmd, shell, config
|
||||||
),
|
),
|
||||||
enumerate(projects),
|
range(len(projects)),
|
||||||
chunksize=WORKER_BATCH_SIZE,
|
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):
|
except (KeyboardInterrupt, WorkerKeyboardInterrupt):
|
||||||
# Catch KeyboardInterrupt raised inside and outside of workers
|
# Catch KeyboardInterrupt raised inside and outside of workers
|
||||||
rc = rc or errno.EINTR
|
rc = errno.EINTR
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Catch any other exceptions raised
|
# Catch any other exceptions raised
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -300,35 +302,35 @@ without iterating through the remaining projects.
|
|||||||
type(e).__name__,
|
type(e).__name__,
|
||||||
e,
|
e,
|
||||||
)
|
)
|
||||||
rc = rc or getattr(e, "errno", 1)
|
rc = getattr(e, "errno", 1)
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
sys.exit(rc)
|
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):
|
class WorkerKeyboardInterrupt(Exception):
|
||||||
"""Keyboard interrupt exception for worker processes."""
|
"""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):
|
def DoWork(project, mirror, opt, cmd, shell, cnt, config):
|
||||||
env = os.environ.copy()
|
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 InvalidArgumentsError
|
||||||
from error import SilentRepoExitError
|
from error import SilentRepoExitError
|
||||||
from git_command import GitCommand
|
from git_command import GitCommand
|
||||||
from project import Project
|
|
||||||
from repo_logging import RepoLogger
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
|
|
||||||
@@ -40,7 +39,7 @@ class GrepColoring(Coloring):
|
|||||||
class ExecuteOneResult(NamedTuple):
|
class ExecuteOneResult(NamedTuple):
|
||||||
"""Result from an execute instance."""
|
"""Result from an execute instance."""
|
||||||
|
|
||||||
project: Project
|
project_idx: int
|
||||||
rc: int
|
rc: int
|
||||||
stdout: str
|
stdout: str
|
||||||
stderr: str
|
stderr: str
|
||||||
@@ -94,7 +93,7 @@ contain a line that matches both expressions:
|
|||||||
pt = getattr(parser.values, "cmd_argv", None)
|
pt = getattr(parser.values, "cmd_argv", None)
|
||||||
if pt is None:
|
if pt is None:
|
||||||
pt = []
|
pt = []
|
||||||
setattr(parser.values, "cmd_argv", pt)
|
parser.values.cmd_argv = pt
|
||||||
|
|
||||||
if opt_str == "-(":
|
if opt_str == "-(":
|
||||||
pt.append("(")
|
pt.append("(")
|
||||||
@@ -121,7 +120,6 @@ contain a line that matches both expressions:
|
|||||||
g.add_option(
|
g.add_option(
|
||||||
"-r",
|
"-r",
|
||||||
"--revision",
|
"--revision",
|
||||||
dest="revision",
|
|
||||||
action="append",
|
action="append",
|
||||||
metavar="TREEish",
|
metavar="TREEish",
|
||||||
help="Search TREEish, instead of the work tree",
|
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",
|
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."""
|
"""Process one project."""
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
try:
|
try:
|
||||||
p = GitCommand(
|
p = GitCommand(
|
||||||
project,
|
project,
|
||||||
@@ -274,7 +274,7 @@ contain a line that matches both expressions:
|
|||||||
verify_command=True,
|
verify_command=True,
|
||||||
)
|
)
|
||||||
except GitError as e:
|
except GitError as e:
|
||||||
return ExecuteOneResult(project, -1, None, str(e), e)
|
return ExecuteOneResult(project_idx, -1, None, str(e), e)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
error = None
|
error = None
|
||||||
@@ -282,10 +282,12 @@ contain a line that matches both expressions:
|
|||||||
except GitError as e:
|
except GitError as e:
|
||||||
rc = 1
|
rc = 1
|
||||||
error = e
|
error = e
|
||||||
return ExecuteOneResult(project, rc, p.stdout, p.stderr, error)
|
return ExecuteOneResult(project_idx, rc, p.stdout, p.stderr, error)
|
||||||
|
|
||||||
@staticmethod
|
@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
|
git_failed = False
|
||||||
bad_rev = False
|
bad_rev = False
|
||||||
have_match = False
|
have_match = False
|
||||||
@@ -293,9 +295,10 @@ contain a line that matches both expressions:
|
|||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
|
project = projects[result.project_idx]
|
||||||
if result.rc < 0:
|
if result.rc < 0:
|
||||||
git_failed = True
|
git_failed = True
|
||||||
out.project("--- project %s ---" % _RelPath(result.project))
|
out.project("--- project %s ---" % _RelPath(project))
|
||||||
out.nl()
|
out.nl()
|
||||||
out.fail("%s", result.stderr)
|
out.fail("%s", result.stderr)
|
||||||
out.nl()
|
out.nl()
|
||||||
@@ -311,9 +314,7 @@ contain a line that matches both expressions:
|
|||||||
):
|
):
|
||||||
bad_rev = True
|
bad_rev = True
|
||||||
else:
|
else:
|
||||||
out.project(
|
out.project("--- project %s ---" % _RelPath(project))
|
||||||
"--- project %s ---" % _RelPath(result.project)
|
|
||||||
)
|
|
||||||
out.nl()
|
out.nl()
|
||||||
out.fail("%s", result.stderr.strip())
|
out.fail("%s", result.stderr.strip())
|
||||||
out.nl()
|
out.nl()
|
||||||
@@ -331,13 +332,13 @@ contain a line that matches both expressions:
|
|||||||
rev, line = line.split(":", 1)
|
rev, line = line.split(":", 1)
|
||||||
out.write("%s", rev)
|
out.write("%s", rev)
|
||||||
out.write(":")
|
out.write(":")
|
||||||
out.project(_RelPath(result.project))
|
out.project(_RelPath(project))
|
||||||
out.write("/")
|
out.write("/")
|
||||||
out.write("%s", line)
|
out.write("%s", line)
|
||||||
out.nl()
|
out.nl()
|
||||||
elif full_name:
|
elif full_name:
|
||||||
for line in r:
|
for line in r:
|
||||||
out.project(_RelPath(result.project))
|
out.project(_RelPath(project))
|
||||||
out.write("/")
|
out.write("/")
|
||||||
out.write("%s", line)
|
out.write("%s", line)
|
||||||
out.nl()
|
out.nl()
|
||||||
@@ -381,16 +382,19 @@ contain a line that matches both expressions:
|
|||||||
cmd_argv.extend(opt.revision)
|
cmd_argv.extend(opt.revision)
|
||||||
cmd_argv.append("--")
|
cmd_argv.append("--")
|
||||||
|
|
||||||
git_failed, bad_rev, have_match, errors = self.ExecuteInParallel(
|
with self.ParallelContext():
|
||||||
opt.jobs,
|
self.get_parallel_context()["projects"] = projects
|
||||||
functools.partial(self._ExecuteOne, cmd_argv),
|
git_failed, bad_rev, have_match, errors = self.ExecuteInParallel(
|
||||||
projects,
|
opt.jobs,
|
||||||
callback=functools.partial(
|
functools.partial(self._ExecuteOne, cmd_argv),
|
||||||
self._ProcessResults, full_name, have_rev, opt
|
range(len(projects)),
|
||||||
),
|
callback=functools.partial(
|
||||||
output=out,
|
self._ProcessResults, full_name, have_rev, opt, projects
|
||||||
ordered=True,
|
),
|
||||||
)
|
output=out,
|
||||||
|
ordered=True,
|
||||||
|
chunksize=1,
|
||||||
|
)
|
||||||
|
|
||||||
if git_failed:
|
if git_failed:
|
||||||
raise GrepCommandError(
|
raise GrepCommandError(
|
||||||
|
|||||||
+3
-5
@@ -59,7 +59,7 @@ Displays detailed usage information about a command.
|
|||||||
|
|
||||||
def PrintAllCommandsBody(self):
|
def PrintAllCommandsBody(self):
|
||||||
print("The complete list of recognized repo commands is:")
|
print("The complete list of recognized repo commands is:")
|
||||||
commandNames = list(sorted(all_commands))
|
commandNames = sorted(all_commands)
|
||||||
self._PrintCommands(commandNames)
|
self._PrintCommands(commandNames)
|
||||||
print(
|
print(
|
||||||
"See 'repo help <command>' for more information on a "
|
"See 'repo help <command>' for more information on a "
|
||||||
@@ -74,10 +74,8 @@ Displays detailed usage information about a command.
|
|||||||
def PrintCommonCommandsBody(self):
|
def PrintCommonCommandsBody(self):
|
||||||
print("The most commonly used repo commands are:")
|
print("The most commonly used repo commands are:")
|
||||||
|
|
||||||
commandNames = list(
|
commandNames = sorted(
|
||||||
sorted(
|
name for name, command in all_commands.items() if command.COMMON
|
||||||
name for name, command in all_commands.items() if command.COMMON
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self._PrintCommands(commandNames)
|
self._PrintCommands(commandNames)
|
||||||
|
|
||||||
|
|||||||
+6
-3
@@ -43,14 +43,12 @@ class Info(PagedCommand):
|
|||||||
p.add_option(
|
p.add_option(
|
||||||
"-o",
|
"-o",
|
||||||
"--overview",
|
"--overview",
|
||||||
dest="overview",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="show overview of all local commits",
|
help="show overview of all local commits",
|
||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"-c",
|
"-c",
|
||||||
"--current-branch",
|
"--current-branch",
|
||||||
dest="current_branch",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="consider only checked out branches",
|
help="consider only checked out branches",
|
||||||
)
|
)
|
||||||
@@ -90,7 +88,7 @@ class Info(PagedCommand):
|
|||||||
self.manifest = self.manifest.outer_client
|
self.manifest = self.manifest.outer_client
|
||||||
manifestConfig = self.manifest.manifestProject.config
|
manifestConfig = self.manifest.manifestProject.config
|
||||||
mergeBranch = manifestConfig.GetBranch("default").merge
|
mergeBranch = manifestConfig.GetBranch("default").merge
|
||||||
manifestGroups = self.manifest.GetGroupsStr()
|
manifestGroups = self.manifest.GetManifestGroupsStr()
|
||||||
|
|
||||||
self.heading("Manifest branch: ")
|
self.heading("Manifest branch: ")
|
||||||
if self.manifest.default.revisionExpr:
|
if self.manifest.default.revisionExpr:
|
||||||
@@ -104,6 +102,11 @@ class Info(PagedCommand):
|
|||||||
self.heading("Manifest groups: ")
|
self.heading("Manifest groups: ")
|
||||||
self.headtext(manifestGroups)
|
self.headtext(manifestGroups)
|
||||||
self.out.nl()
|
self.out.nl()
|
||||||
|
sp = self.manifest.superproject
|
||||||
|
srev = sp.commit_id if sp and sp.commit_id else "None"
|
||||||
|
self.heading("Superproject revision: ")
|
||||||
|
self.headtext(srev)
|
||||||
|
self.out.nl()
|
||||||
|
|
||||||
self.printSeparator()
|
self.printSeparator()
|
||||||
|
|
||||||
|
|||||||
+20
-6
@@ -21,10 +21,9 @@ from command import MirrorSafeCommand
|
|||||||
from error import RepoUnhandledExceptionError
|
from error import RepoUnhandledExceptionError
|
||||||
from error import UpdateManifestError
|
from error import UpdateManifestError
|
||||||
from git_command import git_require
|
from git_command import git_require
|
||||||
from git_command import MIN_GIT_VERSION_HARD
|
|
||||||
from git_command import MIN_GIT_VERSION_SOFT
|
|
||||||
from repo_logging import RepoLogger
|
from repo_logging import RepoLogger
|
||||||
from wrapper import Wrapper
|
from wrapper import Wrapper
|
||||||
|
from wrapper import WrapperDir
|
||||||
|
|
||||||
|
|
||||||
logger = RepoLogger(__file__)
|
logger = RepoLogger(__file__)
|
||||||
@@ -53,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
|
to checkout and use. If no branch is specified, the remote's default
|
||||||
branch is used. This is equivalent to using -b HEAD.
|
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
|
The optional -m argument can be used to specify an alternate manifest
|
||||||
to be used. If no manifest is specified, the manifest default.xml
|
to be used. If no manifest is specified, the manifest default.xml
|
||||||
will be used.
|
will be used.
|
||||||
@@ -124,6 +127,7 @@ to update the working directory files.
|
|||||||
return {
|
return {
|
||||||
"REPO_MANIFEST_URL": "manifest_url",
|
"REPO_MANIFEST_URL": "manifest_url",
|
||||||
"REPO_MIRROR_LOCATION": "reference",
|
"REPO_MIRROR_LOCATION": "reference",
|
||||||
|
"REPO_GIT_LFS": "git_lfs",
|
||||||
}
|
}
|
||||||
|
|
||||||
def _SyncManifest(self, opt):
|
def _SyncManifest(self, opt):
|
||||||
@@ -136,6 +140,7 @@ to update the working directory files.
|
|||||||
# manifest project is special and is created when instantiating the
|
# manifest project is special and is created when instantiating the
|
||||||
# manifest which happens before we parse options.
|
# manifest which happens before we parse options.
|
||||||
self.manifest.manifestProject.clone_depth = opt.manifest_depth
|
self.manifest.manifestProject.clone_depth = opt.manifest_depth
|
||||||
|
self.manifest.manifestProject.upstream = opt.manifest_upstream_branch
|
||||||
clone_filter_for_depth = (
|
clone_filter_for_depth = (
|
||||||
"blob:none" if (_REPO_ALLOW_SHALLOW == "0") else None
|
"blob:none" if (_REPO_ALLOW_SHALLOW == "0") else None
|
||||||
)
|
)
|
||||||
@@ -318,6 +323,12 @@ to update the working directory files.
|
|||||||
" be used with --standalone-manifest."
|
" 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 args:
|
||||||
if opt.manifest_url:
|
if opt.manifest_url:
|
||||||
self.OptionParser.error(
|
self.OptionParser.error(
|
||||||
@@ -331,13 +342,17 @@ to update the working directory files.
|
|||||||
self.OptionParser.error("too many arguments to init")
|
self.OptionParser.error("too many arguments to init")
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
git_require(MIN_GIT_VERSION_HARD, fail=True)
|
wrapper = Wrapper()
|
||||||
if not git_require(MIN_GIT_VERSION_SOFT):
|
|
||||||
|
reqs = wrapper.Requirements.from_dir(WrapperDir())
|
||||||
|
git_require(reqs.get_hard_ver("git"), fail=True)
|
||||||
|
min_git_version_soft = reqs.get_soft_ver("git")
|
||||||
|
if not git_require(min_git_version_soft):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"repo: warning: git-%s+ will soon be required; "
|
"repo: warning: git-%s+ will soon be required; "
|
||||||
"please upgrade your version of git to maintain "
|
"please upgrade your version of git to maintain "
|
||||||
"support.",
|
"support.",
|
||||||
".".join(str(x) for x in MIN_GIT_VERSION_SOFT),
|
".".join(str(x) for x in min_git_version_soft),
|
||||||
)
|
)
|
||||||
|
|
||||||
rp = self.manifest.repoProject
|
rp = self.manifest.repoProject
|
||||||
@@ -350,7 +365,6 @@ to update the working directory files.
|
|||||||
|
|
||||||
# Handle new --repo-rev requests.
|
# Handle new --repo-rev requests.
|
||||||
if opt.repo_rev:
|
if opt.repo_rev:
|
||||||
wrapper = Wrapper()
|
|
||||||
try:
|
try:
|
||||||
remote_ref, rev = wrapper.check_repo_rev(
|
remote_ref, rev = wrapper.check_repo_rev(
|
||||||
rp.worktree,
|
rp.worktree,
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
|
|||||||
p.add_option(
|
p.add_option(
|
||||||
"-r",
|
"-r",
|
||||||
"--regex",
|
"--regex",
|
||||||
dest="regex",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="filter the project list based on regex or wildcard matching "
|
help="filter the project list based on regex or wildcard matching "
|
||||||
"of strings",
|
"of strings",
|
||||||
@@ -48,7 +47,6 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
|
|||||||
p.add_option(
|
p.add_option(
|
||||||
"-g",
|
"-g",
|
||||||
"--groups",
|
"--groups",
|
||||||
dest="groups",
|
|
||||||
help="filter the project list based on the groups the project is "
|
help="filter the project list based on the groups the project is "
|
||||||
"in",
|
"in",
|
||||||
)
|
)
|
||||||
@@ -61,21 +59,18 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
|
|||||||
p.add_option(
|
p.add_option(
|
||||||
"-n",
|
"-n",
|
||||||
"--name-only",
|
"--name-only",
|
||||||
dest="name_only",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="display only the name of the repository",
|
help="display only the name of the repository",
|
||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"-p",
|
"-p",
|
||||||
"--path-only",
|
"--path-only",
|
||||||
dest="path_only",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="display only the path of the repository",
|
help="display only the path of the repository",
|
||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"-f",
|
"-f",
|
||||||
"--fullpath",
|
"--fullpath",
|
||||||
dest="fullpath",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="display the full work tree path instead of the relative path",
|
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
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import enum
|
||||||
import json
|
import json
|
||||||
|
import optparse
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -23,6 +25,16 @@ from repo_logging import RepoLogger
|
|||||||
logger = RepoLogger(__file__)
|
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):
|
class Manifest(PagedCommand):
|
||||||
COMMON = False
|
COMMON = False
|
||||||
helpSummary = "Manifest inspection utility"
|
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
|
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
|
when the manifest was generated. The 'dest-branch' attribute is set
|
||||||
to indicate the remote ref to push changes to via 'repo upload'.
|
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
|
@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 "
|
"(only of use if the branch names for a sha1 manifest are "
|
||||||
"sensitive)",
|
"sensitive)",
|
||||||
)
|
)
|
||||||
|
# Replaced with --format=json. Kept for backwards compatibility.
|
||||||
|
# Can delete in Jun 2026 or later.
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--json",
|
"--json",
|
||||||
default=False,
|
action="store_const",
|
||||||
action="store_true",
|
dest="format",
|
||||||
help="output manifest in JSON format (experimental)",
|
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(
|
p.add_option(
|
||||||
"--pretty",
|
"--pretty",
|
||||||
@@ -108,7 +134,6 @@ to indicate the remote ref to push changes to via 'repo upload'.
|
|||||||
p.add_option(
|
p.add_option(
|
||||||
"-o",
|
"-o",
|
||||||
"--output-file",
|
"--output-file",
|
||||||
dest="output_file",
|
|
||||||
default="-",
|
default="-",
|
||||||
help="file to save the manifest to. (Filename prefix for "
|
help="file to save the manifest to. (Filename prefix for "
|
||||||
"multi-tree.)",
|
"multi-tree.)",
|
||||||
@@ -121,6 +146,8 @@ to indicate the remote ref to push changes to via 'repo upload'.
|
|||||||
if opt.manifest_name:
|
if opt.manifest_name:
|
||||||
self.manifest.Override(opt.manifest_name, False)
|
self.manifest.Override(opt.manifest_name, False)
|
||||||
|
|
||||||
|
output_format = OutputFormat[opt.format.upper()]
|
||||||
|
|
||||||
for manifest in self.ManifestList(opt):
|
for manifest in self.ManifestList(opt):
|
||||||
output_file = opt.output_file
|
output_file = opt.output_file
|
||||||
if 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)
|
manifest.SetUseLocalManifests(not opt.ignore_local_manifests)
|
||||||
|
|
||||||
if opt.json:
|
if output_format == OutputFormat.JSON:
|
||||||
logger.warning("warning: --json is experimental!")
|
|
||||||
doc = manifest.ToDict(
|
doc = manifest.ToDict(
|
||||||
peg_rev=opt.peg_rev,
|
peg_rev=opt.peg_rev,
|
||||||
peg_rev_upstream=opt.peg_rev_upstream,
|
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 (",", ":"),
|
"separators": (",", ": ") if opt.pretty else (",", ":"),
|
||||||
"sort_keys": True,
|
"sort_keys": True,
|
||||||
}
|
}
|
||||||
fd.write(json.dumps(doc, **json_settings))
|
fd.write(json.dumps(doc, **json_settings) + "\n")
|
||||||
else:
|
else:
|
||||||
manifest.Save(
|
manifest.Save(
|
||||||
fd,
|
fd,
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ are displayed.
|
|||||||
p.add_option(
|
p.add_option(
|
||||||
"-c",
|
"-c",
|
||||||
"--current-branch",
|
"--current-branch",
|
||||||
dest="current_branch",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="consider only checked out branches",
|
help="consider only checked out branches",
|
||||||
)
|
)
|
||||||
|
|||||||
+12
-8
@@ -27,8 +27,10 @@ class Prune(PagedCommand):
|
|||||||
"""
|
"""
|
||||||
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
|
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
|
||||||
|
|
||||||
def _ExecuteOne(self, project):
|
@classmethod
|
||||||
|
def _ExecuteOne(cls, project_idx):
|
||||||
"""Process one project."""
|
"""Process one project."""
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
return project.PruneHeads()
|
return project.PruneHeads()
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
@@ -41,13 +43,15 @@ class Prune(PagedCommand):
|
|||||||
def _ProcessResults(_pool, _output, results):
|
def _ProcessResults(_pool, _output, results):
|
||||||
return list(itertools.chain.from_iterable(results))
|
return list(itertools.chain.from_iterable(results))
|
||||||
|
|
||||||
all_branches = self.ExecuteInParallel(
|
with self.ParallelContext():
|
||||||
opt.jobs,
|
self.get_parallel_context()["projects"] = projects
|
||||||
self._ExecuteOne,
|
all_branches = self.ExecuteInParallel(
|
||||||
projects,
|
opt.jobs,
|
||||||
callback=_ProcessResults,
|
self._ExecuteOne,
|
||||||
ordered=True,
|
range(len(projects)),
|
||||||
)
|
callback=_ProcessResults,
|
||||||
|
ordered=True,
|
||||||
|
)
|
||||||
|
|
||||||
if not all_branches:
|
if not all_branches:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -47,21 +47,18 @@ branch but need to incorporate new upstream changes "underneath" them.
|
|||||||
g.add_option(
|
g.add_option(
|
||||||
"-i",
|
"-i",
|
||||||
"--interactive",
|
"--interactive",
|
||||||
dest="interactive",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="interactive rebase (single project only)",
|
help="interactive rebase (single project only)",
|
||||||
)
|
)
|
||||||
|
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--fail-fast",
|
"--fail-fast",
|
||||||
dest="fail_fast",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="stop rebasing after first error is hit",
|
help="stop rebasing after first error is hit",
|
||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"-f",
|
"-f",
|
||||||
"--force-rebase",
|
"--force-rebase",
|
||||||
dest="force_rebase",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="pass --force-rebase to git rebase",
|
help="pass --force-rebase to git rebase",
|
||||||
)
|
)
|
||||||
@@ -74,27 +71,23 @@ branch but need to incorporate new upstream changes "underneath" them.
|
|||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--autosquash",
|
"--autosquash",
|
||||||
dest="autosquash",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="pass --autosquash to git rebase",
|
help="pass --autosquash to git rebase",
|
||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--whitespace",
|
"--whitespace",
|
||||||
dest="whitespace",
|
|
||||||
action="store",
|
action="store",
|
||||||
metavar="WS",
|
metavar="WS",
|
||||||
help="pass --whitespace to git rebase",
|
help="pass --whitespace to git rebase",
|
||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--auto-stash",
|
"--auto-stash",
|
||||||
dest="auto_stash",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="stash local modifications before starting",
|
help="stash local modifications before starting",
|
||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"-m",
|
"-m",
|
||||||
"--onto-manifest",
|
"--onto-manifest",
|
||||||
dest="onto_manifest",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="rebase onto the manifest version instead of upstream "
|
help="rebase onto the manifest version instead of upstream "
|
||||||
"HEAD (this helps to make sure the local tree stays "
|
"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(
|
g.add_option(
|
||||||
"--repo-upgraded",
|
"--repo-upgraded",
|
||||||
dest="repo_upgraded",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help=optparse.SUPPRESS_HELP,
|
help=optparse.SUPPRESS_HELP,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ The '%prog' command stages files to prepare the next commit.
|
|||||||
g.add_option(
|
g.add_option(
|
||||||
"-i",
|
"-i",
|
||||||
"--interactive",
|
"--interactive",
|
||||||
dest="interactive",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="use interactive staging",
|
help="use interactive staging",
|
||||||
)
|
)
|
||||||
|
|||||||
+25
-16
@@ -21,7 +21,6 @@ from error import RepoExitError
|
|||||||
from git_command import git
|
from git_command import git
|
||||||
from git_config import IsImmutable
|
from git_config import IsImmutable
|
||||||
from progress import Progress
|
from progress import Progress
|
||||||
from project import Project
|
|
||||||
from repo_logging import RepoLogger
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
|
|
||||||
@@ -29,7 +28,7 @@ logger = RepoLogger(__file__)
|
|||||||
|
|
||||||
|
|
||||||
class ExecuteOneResult(NamedTuple):
|
class ExecuteOneResult(NamedTuple):
|
||||||
project: Project
|
project_idx: int
|
||||||
error: Exception
|
error: Exception
|
||||||
|
|
||||||
|
|
||||||
@@ -52,7 +51,6 @@ revision specified in the manifest.
|
|||||||
def _Options(self, p):
|
def _Options(self, p):
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--all",
|
"--all",
|
||||||
dest="all",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="begin branch in all projects",
|
help="begin branch in all projects",
|
||||||
)
|
)
|
||||||
@@ -80,18 +78,20 @@ revision specified in the manifest.
|
|||||||
if not git.check_ref_format("heads/%s" % nb):
|
if not git.check_ref_format("heads/%s" % nb):
|
||||||
self.OptionParser.error("'%s' is not a valid name" % 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."""
|
"""Start one project."""
|
||||||
# If the current revision is immutable, such as a SHA1, a tag or
|
# 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
|
# a change, then we can't push back to it. Substitute with
|
||||||
# dest_branch, if defined; or with manifest default revision instead.
|
# dest_branch, if defined; or with manifest default revision instead.
|
||||||
branch_merge = ""
|
branch_merge = ""
|
||||||
error = None
|
error = None
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
if IsImmutable(project.revisionExpr):
|
if IsImmutable(project.revisionExpr):
|
||||||
if project.dest_branch:
|
if project.dest_branch:
|
||||||
branch_merge = project.dest_branch
|
branch_merge = project.dest_branch
|
||||||
else:
|
else:
|
||||||
branch_merge = self.manifest.default.revisionExpr
|
branch_merge = default_revisionExpr
|
||||||
|
|
||||||
try:
|
try:
|
||||||
project.StartBranch(
|
project.StartBranch(
|
||||||
@@ -100,7 +100,7 @@ revision specified in the manifest.
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("error: unable to checkout %s: %s", project.name, e)
|
logger.error("error: unable to checkout %s: %s", project.name, e)
|
||||||
error = e
|
error = e
|
||||||
return ExecuteOneResult(project, error)
|
return ExecuteOneResult(project_idx, error)
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
nb = args[0]
|
nb = args[0]
|
||||||
@@ -120,19 +120,28 @@ revision specified in the manifest.
|
|||||||
def _ProcessResults(_pool, pm, results):
|
def _ProcessResults(_pool, pm, results):
|
||||||
for result in results:
|
for result in results:
|
||||||
if result.error:
|
if result.error:
|
||||||
err_projects.append(result.project)
|
project = all_projects[result.project_idx]
|
||||||
|
err_projects.append(project)
|
||||||
err.append(result.error)
|
err.append(result.error)
|
||||||
pm.update(msg="")
|
pm.update(msg="")
|
||||||
|
|
||||||
self.ExecuteInParallel(
|
with self.ParallelContext():
|
||||||
opt.jobs,
|
self.get_parallel_context()["projects"] = all_projects
|
||||||
functools.partial(self._ExecuteOne, opt.revision, nb),
|
self.ExecuteInParallel(
|
||||||
all_projects,
|
opt.jobs,
|
||||||
callback=_ProcessResults,
|
functools.partial(
|
||||||
output=Progress(
|
self._ExecuteOne,
|
||||||
f"Starting {nb}", len(all_projects), quiet=opt.quiet
|
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:
|
if err_projects:
|
||||||
for p in err_projects:
|
for p in err_projects:
|
||||||
|
|||||||
+16
-12
@@ -82,13 +82,13 @@ the following meanings:
|
|||||||
p.add_option(
|
p.add_option(
|
||||||
"-o",
|
"-o",
|
||||||
"--orphans",
|
"--orphans",
|
||||||
dest="orphans",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="include objects in working directory outside of repo "
|
help="include objects in working directory outside of repo "
|
||||||
"projects",
|
"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 specific project.
|
||||||
|
|
||||||
Obtains the status for a project, redirecting the output to
|
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
|
local: a boolean, if True, the path is relative to the local
|
||||||
(sub)manifest. If false, the path is relative to the outermost
|
(sub)manifest. If false, the path is relative to the outermost
|
||||||
manifest.
|
manifest.
|
||||||
project: Project to get status of.
|
project_idx: Project index to get status of.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The status of the project.
|
The status of the project.
|
||||||
"""
|
"""
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
ret = project.PrintWorkTreeStatus(
|
ret = project.PrintWorkTreeStatus(
|
||||||
quiet=quiet, output_redir=buf, local=local
|
quiet=quiet, output_redir=buf, local=local
|
||||||
)
|
)
|
||||||
@@ -143,15 +144,18 @@ the following meanings:
|
|||||||
ret += 1
|
ret += 1
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
counter = self.ExecuteInParallel(
|
with self.ParallelContext():
|
||||||
opt.jobs,
|
self.get_parallel_context()["projects"] = all_projects
|
||||||
functools.partial(
|
counter = self.ExecuteInParallel(
|
||||||
self._StatusHelper, opt.quiet, opt.this_manifest_only
|
opt.jobs,
|
||||||
),
|
functools.partial(
|
||||||
all_projects,
|
self._StatusHelper, opt.quiet, opt.this_manifest_only
|
||||||
callback=_ProcessResults,
|
),
|
||||||
ordered=True,
|
range(len(all_projects)),
|
||||||
)
|
callback=_ProcessResults,
|
||||||
|
ordered=True,
|
||||||
|
chunksize=1,
|
||||||
|
)
|
||||||
|
|
||||||
if not opt.quiet and len(all_projects) == counter:
|
if not opt.quiet and len(all_projects) == counter:
|
||||||
print("nothing to commit (working directory clean)")
|
print("nothing to commit (working directory clean)")
|
||||||
|
|||||||
+1214
-288
File diff suppressed because it is too large
Load Diff
+56
-57
@@ -27,6 +27,7 @@ from error import SilentRepoExitError
|
|||||||
from error import UploadError
|
from error import UploadError
|
||||||
from git_command import GitCommand
|
from git_command import GitCommand
|
||||||
from git_refs import R_HEADS
|
from git_refs import R_HEADS
|
||||||
|
import git_superproject
|
||||||
from hooks import RepoHook
|
from hooks import RepoHook
|
||||||
from project import ReviewableBranch
|
from project import ReviewableBranch
|
||||||
from repo_logging import RepoLogger
|
from repo_logging import RepoLogger
|
||||||
@@ -218,9 +219,14 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
def _Options(self, p):
|
def _Options(self, p):
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"-t",
|
"-t",
|
||||||
|
"--topic-branch",
|
||||||
dest="auto_topic",
|
dest="auto_topic",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="send local branch name to Gerrit Code Review",
|
help="set the topic to the local branch name",
|
||||||
|
)
|
||||||
|
p.add_option(
|
||||||
|
"--topic",
|
||||||
|
help="set topic for the change",
|
||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--hashtag",
|
"--hashtag",
|
||||||
@@ -244,6 +250,12 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
default=[],
|
default=[],
|
||||||
help="add a label when uploading",
|
help="add a label when uploading",
|
||||||
)
|
)
|
||||||
|
p.add_option(
|
||||||
|
"--pd",
|
||||||
|
"--patchset-description",
|
||||||
|
dest="patchset_description",
|
||||||
|
help="description for patchset",
|
||||||
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--re",
|
"--re",
|
||||||
"--reviewers",
|
"--reviewers",
|
||||||
@@ -256,7 +268,6 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
"--cc",
|
"--cc",
|
||||||
type="string",
|
type="string",
|
||||||
action="append",
|
action="append",
|
||||||
dest="cc",
|
|
||||||
help="also send email to these email addresses",
|
help="also send email to these email addresses",
|
||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
@@ -270,7 +281,6 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
p.add_option(
|
p.add_option(
|
||||||
"-c",
|
"-c",
|
||||||
"--current-branch",
|
"--current-branch",
|
||||||
dest="current_branch",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="upload current git branch",
|
help="upload current git branch",
|
||||||
)
|
)
|
||||||
@@ -299,7 +309,6 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
"-p",
|
"-p",
|
||||||
"--private",
|
"--private",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
dest="private",
|
|
||||||
default=False,
|
default=False,
|
||||||
help="upload as a private change (deprecated; use --wip)",
|
help="upload as a private change (deprecated; use --wip)",
|
||||||
)
|
)
|
||||||
@@ -307,7 +316,6 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
"-w",
|
"-w",
|
||||||
"--wip",
|
"--wip",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
dest="wip",
|
|
||||||
default=False,
|
default=False,
|
||||||
help="upload as a work-in-progress change",
|
help="upload as a work-in-progress change",
|
||||||
)
|
)
|
||||||
@@ -543,42 +551,14 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
people = copy.deepcopy(original_people)
|
people = copy.deepcopy(original_people)
|
||||||
self._AppendAutoList(branch, people)
|
self._AppendAutoList(branch, people)
|
||||||
|
|
||||||
# Check if there are local changes that may have been forgotten.
|
|
||||||
changes = branch.project.UncommitedFiles()
|
|
||||||
if opt.ignore_untracked_files:
|
|
||||||
untracked = set(branch.project.UntrackedFiles())
|
|
||||||
changes = [x for x in changes if x not in untracked]
|
|
||||||
|
|
||||||
if changes:
|
|
||||||
key = "review.%s.autoupload" % branch.project.remote.review
|
|
||||||
answer = branch.project.config.GetBoolean(key)
|
|
||||||
|
|
||||||
# If they want to auto upload, let's not ask because it
|
|
||||||
# could be automated.
|
|
||||||
if answer is None:
|
|
||||||
print()
|
|
||||||
print(
|
|
||||||
"Uncommitted changes in %s (did you forget to "
|
|
||||||
"amend?):" % branch.project.name
|
|
||||||
)
|
|
||||||
print("\n".join(changes))
|
|
||||||
print("Continue uploading? (y/N) ", end="", flush=True)
|
|
||||||
if opt.yes:
|
|
||||||
print("<--yes>")
|
|
||||||
a = "yes"
|
|
||||||
else:
|
|
||||||
a = sys.stdin.readline().strip().lower()
|
|
||||||
if a not in ("y", "yes", "t", "true", "on"):
|
|
||||||
print("skipping upload", file=sys.stderr)
|
|
||||||
branch.uploaded = False
|
|
||||||
branch.error = "User aborted"
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check if topic branches should be sent to the server during
|
# Check if topic branches should be sent to the server during
|
||||||
# upload.
|
# upload.
|
||||||
if opt.auto_topic is not True:
|
if opt.topic is None:
|
||||||
key = "review.%s.uploadtopic" % branch.project.remote.review
|
if opt.auto_topic is not True:
|
||||||
opt.auto_topic = branch.project.config.GetBoolean(key)
|
key = "review.%s.uploadtopic" % branch.project.remote.review
|
||||||
|
opt.auto_topic = branch.project.config.GetBoolean(key)
|
||||||
|
if opt.auto_topic:
|
||||||
|
opt.topic = branch.name
|
||||||
|
|
||||||
def _ExpandCommaList(value):
|
def _ExpandCommaList(value):
|
||||||
"""Split |value| up into comma delimited entries."""
|
"""Split |value| up into comma delimited entries."""
|
||||||
@@ -620,19 +600,22 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
full_dest = destination
|
full_dest = destination
|
||||||
if not full_dest.startswith(R_HEADS):
|
if not full_dest.startswith(R_HEADS):
|
||||||
full_dest = R_HEADS + full_dest
|
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
|
# If the merge branch of the local branch is different from
|
||||||
# the project's revision AND destination, this might not be
|
# the project's revision AND destination, this might not be
|
||||||
# intentional.
|
# intentional.
|
||||||
if (
|
if (
|
||||||
merge_branch
|
merge_branch
|
||||||
and merge_branch != branch.project.revisionExpr
|
and merge_branch != full_revision
|
||||||
and merge_branch != full_dest
|
and merge_branch != full_dest
|
||||||
):
|
):
|
||||||
print(
|
print(
|
||||||
f"For local branch {branch.name}: merge branch "
|
f"For local branch {branch.name}: merge branch "
|
||||||
f"{merge_branch} does not match destination branch "
|
f"{merge_branch} does not match destination branch "
|
||||||
f"{destination}"
|
f"{destination} and revision {branch.project.revisionExpr}"
|
||||||
)
|
)
|
||||||
print("skipping upload.")
|
print("skipping upload.")
|
||||||
print(
|
print(
|
||||||
@@ -642,10 +625,20 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
branch.uploaded = False
|
branch.uploaded = False
|
||||||
return
|
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(
|
branch.UploadForReview(
|
||||||
people,
|
people,
|
||||||
dryrun=opt.dryrun,
|
dryrun=opt.dryrun,
|
||||||
auto_topic=opt.auto_topic,
|
topic=opt.topic,
|
||||||
hashtags=hashtags,
|
hashtags=hashtags,
|
||||||
labels=labels,
|
labels=labels,
|
||||||
private=opt.private,
|
private=opt.private,
|
||||||
@@ -654,7 +647,8 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
ready=opt.ready,
|
ready=opt.ready,
|
||||||
dest_branch=destination,
|
dest_branch=destination,
|
||||||
validate_certs=opt.validate_certs,
|
validate_certs=opt.validate_certs,
|
||||||
push_options=opt.push_options,
|
push_options=push_options,
|
||||||
|
patchset_description=opt.patchset_description,
|
||||||
)
|
)
|
||||||
|
|
||||||
branch.uploaded = True
|
branch.uploaded = True
|
||||||
@@ -729,16 +723,17 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
merge_branch = p.stdout.strip()
|
merge_branch = p.stdout.strip()
|
||||||
return merge_branch
|
return merge_branch
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def _GatherOne(opt, project):
|
def _GatherOne(cls, opt, project_idx):
|
||||||
"""Figure out the upload status for |project|."""
|
"""Figure out the upload status for |project|."""
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
if opt.current_branch:
|
if opt.current_branch:
|
||||||
cbr = project.CurrentBranch
|
cbr = project.CurrentBranch
|
||||||
up_branch = project.GetUploadableBranch(cbr)
|
up_branch = project.GetUploadableBranch(cbr)
|
||||||
avail = [up_branch] if up_branch else None
|
avail = [up_branch] if up_branch else None
|
||||||
else:
|
else:
|
||||||
avail = project.GetUploadableBranches(opt.branch)
|
avail = project.GetUploadableBranches(opt.branch)
|
||||||
return (project, avail)
|
return (project_idx, avail)
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
projects = self.GetProjects(
|
projects = self.GetProjects(
|
||||||
@@ -748,7 +743,8 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
def _ProcessResults(_pool, _out, results):
|
def _ProcessResults(_pool, _out, results):
|
||||||
pending = []
|
pending = []
|
||||||
for result in results:
|
for result in results:
|
||||||
project, avail = result
|
project_idx, avail = result
|
||||||
|
project = projects[project_idx]
|
||||||
if avail is None:
|
if avail is None:
|
||||||
logger.error(
|
logger.error(
|
||||||
'repo: error: %s: Unable to upload branch "%s". '
|
'repo: error: %s: Unable to upload branch "%s". '
|
||||||
@@ -759,15 +755,17 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
project.manifest.branch,
|
project.manifest.branch,
|
||||||
)
|
)
|
||||||
elif avail:
|
elif avail:
|
||||||
pending.append(result)
|
pending.append((project, avail))
|
||||||
return pending
|
return pending
|
||||||
|
|
||||||
pending = self.ExecuteInParallel(
|
with self.ParallelContext():
|
||||||
opt.jobs,
|
self.get_parallel_context()["projects"] = projects
|
||||||
functools.partial(self._GatherOne, opt),
|
pending = self.ExecuteInParallel(
|
||||||
projects,
|
opt.jobs,
|
||||||
callback=_ProcessResults,
|
functools.partial(self._GatherOne, opt),
|
||||||
)
|
range(len(projects)),
|
||||||
|
callback=_ProcessResults,
|
||||||
|
)
|
||||||
|
|
||||||
if not pending:
|
if not pending:
|
||||||
if opt.branch is None:
|
if opt.branch is None:
|
||||||
@@ -805,9 +803,10 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
project_list=pending_proj_names, worktree_list=pending_worktrees
|
project_list=pending_proj_names, worktree_list=pending_worktrees
|
||||||
):
|
):
|
||||||
if LocalSyncState(manifest).IsPartiallySynced():
|
if LocalSyncState(manifest).IsPartiallySynced():
|
||||||
logger.error(
|
logger.info(
|
||||||
"Partially synced tree detected. Syncing all projects "
|
"Tip: A partially synced tree was detected. "
|
||||||
"may resolve issues you're seeing."
|
"If this failure involves cross-project dependencies, "
|
||||||
|
"a full `repo sync` might help."
|
||||||
)
|
)
|
||||||
ret = 1
|
ret = 1
|
||||||
if ret:
|
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
|
||||||
+10
-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");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -72,3 +72,12 @@ def tmp_home_dir(monkeypatch, tmp_path_factory):
|
|||||||
the function scope.
|
the function scope.
|
||||||
"""
|
"""
|
||||||
return _set_home(monkeypatch, tmp_path_factory.mktemp("home"))
|
return _set_home(monkeypatch, tmp_path_factory.mktemp("home"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def setup_user_identity(monkeysession, scope="session"):
|
||||||
|
"""Set env variables for author and committer name and email."""
|
||||||
|
monkeysession.setenv("GIT_AUTHOR_NAME", "Foo Bar")
|
||||||
|
monkeysession.setenv("GIT_COMMITTER_NAME", "Foo Bar")
|
||||||
|
monkeysession.setenv("GIT_AUTHOR_EMAIL", "foo@bar.baz")
|
||||||
|
monkeysession.setenv("GIT_COMMITTER_EMAIL", "foo@bar.baz")
|
||||||
|
|||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
gitc_dir=/test/usr/local/google/gitc
|
|
||||||
Vendored
+8
@@ -11,3 +11,11 @@
|
|||||||
intk = 10k
|
intk = 10k
|
||||||
intm = 10m
|
intm = 10m
|
||||||
intg = 10g
|
intg = 10g
|
||||||
|
|
||||||
|
[color "status"]
|
||||||
|
one = yellow
|
||||||
|
two = magenta cyan
|
||||||
|
three = black red ul
|
||||||
|
reset = reset
|
||||||
|
none
|
||||||
|
empty =
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Unittests for the color.py module."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import utils_for_test
|
||||||
|
|
||||||
|
import color
|
||||||
|
import git_config
|
||||||
|
|
||||||
|
|
||||||
|
@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")
|
||||||
|
|
||||||
|
|
||||||
|
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 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_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(coloring: color.Coloring) -> None:
|
||||||
|
"""check fg & bg"""
|
||||||
|
val = coloring._parse("two", None, None, None)
|
||||||
|
assert val == "\033[35;46m"
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
+18
-29
@@ -14,43 +14,32 @@
|
|||||||
|
|
||||||
"""Unittests for the editor.py module."""
|
"""Unittests for the editor.py module."""
|
||||||
|
|
||||||
import unittest
|
import pytest
|
||||||
|
|
||||||
from editor import Editor
|
from editor import Editor
|
||||||
|
|
||||||
|
|
||||||
class EditorTestCase(unittest.TestCase):
|
@pytest.fixture(autouse=True)
|
||||||
|
def reset_editor() -> None:
|
||||||
"""Take care of resetting Editor state across tests."""
|
"""Take care of resetting Editor state across tests."""
|
||||||
|
Editor._editor = None
|
||||||
def setUp(self):
|
yield
|
||||||
self.setEditor(None)
|
Editor._editor = None
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.setEditor(None)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def setEditor(editor):
|
|
||||||
Editor._editor = editor
|
|
||||||
|
|
||||||
|
|
||||||
class GetEditor(EditorTestCase):
|
def test_basic() -> None:
|
||||||
"""Check GetEditor behavior."""
|
"""Basic checking of _GetEditor."""
|
||||||
|
Editor._editor = ":"
|
||||||
def test_basic(self):
|
assert Editor._GetEditor() == ":"
|
||||||
"""Basic checking of _GetEditor."""
|
|
||||||
self.setEditor(":")
|
|
||||||
self.assertEqual(":", Editor._GetEditor())
|
|
||||||
|
|
||||||
|
|
||||||
class EditString(EditorTestCase):
|
def test_no_editor() -> None:
|
||||||
"""Check EditString behavior."""
|
"""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):
|
def test_cat_editor() -> None:
|
||||||
"""Check behavior when editor is `cat`."""
|
"""Check behavior when editor is `cat`."""
|
||||||
self.setEditor("cat")
|
Editor._editor = "cat"
|
||||||
self.assertEqual("foo", Editor.EditString("foo"))
|
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");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -16,7 +16,9 @@
|
|||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import pickle
|
import pickle
|
||||||
import unittest
|
from typing import Iterator, Type
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
import command
|
import command
|
||||||
import error
|
import error
|
||||||
@@ -26,7 +28,7 @@ import project
|
|||||||
from subcmds import all_modules
|
from subcmds import all_modules
|
||||||
|
|
||||||
|
|
||||||
imports = all_modules + [
|
_IMPORTS = all_modules + [
|
||||||
error,
|
error,
|
||||||
project,
|
project,
|
||||||
git_command,
|
git_command,
|
||||||
@@ -35,36 +37,35 @@ imports = all_modules + [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PickleTests(unittest.TestCase):
|
def get_exceptions() -> Iterator[Type[Exception]]:
|
||||||
"""Make sure all our custom exceptions can be pickled."""
|
"""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):
|
def test_exception_lookup() -> None:
|
||||||
"""Make sure our introspection logic works."""
|
"""Make sure our introspection logic works."""
|
||||||
classes = list(self.getExceptions())
|
classes = list(get_exceptions())
|
||||||
self.assertIn(error.HookError, classes)
|
assert error.HookError in classes
|
||||||
# Don't assert the exact number to avoid being a change-detector test.
|
# Don't assert the exact number to avoid being a change-detector test.
|
||||||
self.assertGreater(len(classes), 10)
|
assert len(classes) > 10
|
||||||
|
|
||||||
def testPickle(self):
|
|
||||||
"""Try to pickle all the exceptions."""
|
@pytest.mark.parametrize("cls", get_exceptions())
|
||||||
for cls in self.getExceptions():
|
def test_pickle(cls: Type[Exception]) -> None:
|
||||||
args = inspect.getfullargspec(cls.__init__).args[1:]
|
"""Try to pickle all the exceptions."""
|
||||||
obj = cls(*args)
|
args = inspect.getfullargspec(cls.__init__).args[1:]
|
||||||
p = pickle.dumps(obj)
|
obj = cls(*args)
|
||||||
try:
|
p = pickle.dumps(obj)
|
||||||
newobj = pickle.loads(p)
|
try:
|
||||||
except Exception as e: # pylint: disable=broad-except
|
newobj = pickle.loads(p)
|
||||||
self.fail(
|
except Exception as e:
|
||||||
"Class %s is unable to be pickled: %s\n"
|
pytest.fail(
|
||||||
"Incomplete super().__init__(...) call?" % (cls, e)
|
f"Class {cls} is unable to be pickled: {e}\n"
|
||||||
)
|
"Incomplete super().__init__(...) call?"
|
||||||
self.assertIsInstance(newobj, cls)
|
)
|
||||||
self.assertEqual(str(obj), str(newobj))
|
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");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -21,6 +21,8 @@ import subprocess
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
import git_command
|
import git_command
|
||||||
import wrapper
|
import wrapper
|
||||||
|
|
||||||
@@ -263,6 +265,7 @@ class UserAgentUnitTest(unittest.TestCase):
|
|||||||
m = re.match(r"^[^ ]+$", os_name)
|
m = re.match(r"^[^ ]+$", os_name)
|
||||||
self.assertIsNotNone(m)
|
self.assertIsNotNone(m)
|
||||||
|
|
||||||
|
@pytest.mark.skip_cq("TODO(b/266734831): Find out why this fails in CQ")
|
||||||
def test_smoke_repo(self):
|
def test_smoke_repo(self):
|
||||||
"""Make sure repo UA returns something useful."""
|
"""Make sure repo UA returns something useful."""
|
||||||
ua = git_command.user_agent.repo
|
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)
|
m = re.match(r"^git-repo/[^ ]+ ([^ ]+) git/[^ ]+ Python/[0-9.]+", ua)
|
||||||
self.assertIsNotNone(m)
|
self.assertIsNotNone(m)
|
||||||
|
|
||||||
|
@pytest.mark.skip_cq("TODO(b/266734831): Find out why this fails in CQ")
|
||||||
def test_smoke_git(self):
|
def test_smoke_git(self):
|
||||||
"""Make sure git UA returns something useful."""
|
"""Make sure git UA returns something useful."""
|
||||||
ua = git_command.user_agent.git
|
ua = git_command.user_agent.git
|
||||||
|
|||||||
+190
-152
@@ -14,177 +14,215 @@
|
|||||||
|
|
||||||
"""Unittests for the git_config.py module."""
|
"""Unittests for the git_config.py module."""
|
||||||
|
|
||||||
import os
|
from pathlib import Path
|
||||||
import tempfile
|
from typing import Any
|
||||||
import unittest
|
|
||||||
|
import pytest
|
||||||
|
import utils_for_test
|
||||||
|
|
||||||
import git_config
|
import git_config
|
||||||
|
|
||||||
|
|
||||||
def fixture(*paths):
|
@pytest.fixture
|
||||||
"""Return a path relative to test/fixtures."""
|
def readonly_config() -> git_config.GitConfig:
|
||||||
return os.path.join(os.path.dirname(__file__), "fixtures", *paths)
|
"""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):
|
def test_get_string_with_empty_config_values(
|
||||||
"""Read-only tests of the GitConfig class."""
|
readonly_config: git_config.GitConfig,
|
||||||
|
) -> None:
|
||||||
|
"""Test config entries with no value.
|
||||||
|
|
||||||
def setUp(self):
|
[section]
|
||||||
"""Create a GitConfig object using the test.gitconfig fixture."""
|
empty
|
||||||
config_fixture = fixture("test.gitconfig")
|
|
||||||
self.config = git_config.GitConfig(config_fixture)
|
|
||||||
|
|
||||||
def test_GetString_with_empty_config_values(self):
|
"""
|
||||||
"""
|
val = readonly_config.GetString("section.empty")
|
||||||
Test config entries with no value.
|
assert val is None
|
||||||
|
|
||||||
[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}"))
|
|
||||||
|
|
||||||
|
|
||||||
class GitConfigReadWriteTests(unittest.TestCase):
|
def test_get_string_with_true_value(
|
||||||
"""Read/write tests of the GitConfig class."""
|
readonly_config: git_config.GitConfig,
|
||||||
|
) -> None:
|
||||||
|
"""Test config entries with a string value.
|
||||||
|
|
||||||
def setUp(self):
|
[section]
|
||||||
self.tmpfile = tempfile.NamedTemporaryFile()
|
nonempty = true
|
||||||
self.config = self.get_config()
|
|
||||||
|
|
||||||
def get_config(self):
|
"""
|
||||||
"""Get a new GitConfig instance."""
|
val = readonly_config.GetString("section.nonempty")
|
||||||
return git_config.GitConfig(self.tmpfile.name)
|
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.
|
def test_get_string_from_missing_file() -> None:
|
||||||
config = self.get_config()
|
"""Test missing config file."""
|
||||||
self.assertEqual("val", config.GetString("foo.bar"))
|
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.
|
def test_get_boolean_undefined(readonly_config: git_config.GitConfig) -> None:
|
||||||
self.config.SetString("foo.bar", None)
|
"""Test GetBoolean on key that doesn't exist."""
|
||||||
self.assertIsNone(self.config.GetString("foo.bar"))
|
assert readonly_config.GetBoolean("section.missing") is None
|
||||||
config = self.get_config()
|
|
||||||
self.assertIsNone(config.GetString("foo.bar"))
|
|
||||||
|
|
||||||
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.
|
def test_get_boolean_invalid(readonly_config: git_config.GitConfig) -> None:
|
||||||
config = self.get_config()
|
"""Test GetBoolean on invalid boolean value."""
|
||||||
self.assertTrue(config.GetBoolean("foo.bar"))
|
assert readonly_config.GetBoolean("section.boolinvalid") is None
|
||||||
self.assertEqual("true", config.GetString("foo.bar"))
|
|
||||||
|
|
||||||
# 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.
|
def test_get_boolean_true(readonly_config: git_config.GitConfig) -> None:
|
||||||
config = self.get_config()
|
"""Test GetBoolean on valid true boolean."""
|
||||||
self.assertFalse(config.GetBoolean("foo.bar"))
|
assert readonly_config.GetBoolean("section.booltrue") is True
|
||||||
self.assertEqual("false", config.GetString("foo.bar"))
|
|
||||||
|
|
||||||
# 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):
|
def test_get_boolean_false(readonly_config: git_config.GitConfig) -> None:
|
||||||
"""Test config entries with a sync state analysis data."""
|
"""Test GetBoolean on valid false boolean."""
|
||||||
superproject_logging_data = {}
|
assert readonly_config.GetBoolean("section.boolfalse") is False
|
||||||
superproject_logging_data["test"] = False
|
|
||||||
options = type("options", (object,), {})()
|
|
||||||
options.verbose = "true"
|
def test_get_int_undefined(readonly_config: git_config.GitConfig) -> None:
|
||||||
options.mp_update = "false"
|
"""Test GetInt on key that doesn't exist."""
|
||||||
TESTS = (
|
assert readonly_config.GetInt("section.missing") is None
|
||||||
("superproject.test", "false"),
|
|
||||||
("options.verbose", "true"),
|
|
||||||
("options.mpupdate", "false"),
|
def test_get_int_invalid(readonly_config: git_config.GitConfig) -> None:
|
||||||
("main.version", "1"),
|
"""Test GetInt on invalid integer value."""
|
||||||
)
|
assert readonly_config.GetInt("section.intinvalid") is None
|
||||||
self.config.UpdateSyncAnalysisState(options, superproject_logging_data)
|
|
||||||
sync_data = self.config.GetSyncAnalysisStateData()
|
|
||||||
for key, value in TESTS:
|
@pytest.mark.parametrize(
|
||||||
self.assertEqual(
|
"key, expected",
|
||||||
sync_data[f"{git_config.SYNC_STATE_PREFIX}{key}"], value
|
(
|
||||||
)
|
("inthex", 16),
|
||||||
self.assertTrue(
|
("inthexk", 16384),
|
||||||
sync_data[f"{git_config.SYNC_STATE_PREFIX}main.synctime"]
|
("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"]
|
||||||
|
|||||||
@@ -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
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
from test_manifest_xml import sort_attributes
|
from test_manifest_xml import sort_attributes
|
||||||
|
|
||||||
import git_superproject
|
import git_superproject
|
||||||
@@ -145,6 +146,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertIsNone(manifest.superproject)
|
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):
|
def test_superproject_get_superproject_invalid_url(self):
|
||||||
"""Test with an invalid url."""
|
"""Test with an invalid url."""
|
||||||
manifest = self.getXmlManifest(
|
manifest = self.getXmlManifest(
|
||||||
@@ -168,6 +170,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
|||||||
self.assertFalse(sync_result.success)
|
self.assertFalse(sync_result.success)
|
||||||
self.assertTrue(sync_result.fatal)
|
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):
|
def test_superproject_get_superproject_invalid_branch(self):
|
||||||
"""Test with an invalid branch."""
|
"""Test with an invalid branch."""
|
||||||
manifest = self.getXmlManifest(
|
manifest = self.getXmlManifest(
|
||||||
@@ -458,6 +461,62 @@ class SuperprojectTestCase(unittest.TestCase):
|
|||||||
"</manifest>",
|
"</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):
|
def test_Fetch(self):
|
||||||
manifest = self.getXmlManifest(
|
manifest = self.getXmlManifest(
|
||||||
"""
|
"""
|
||||||
|
|||||||
+417
-339
@@ -14,19 +14,28 @@
|
|||||||
|
|
||||||
"""Unittests for the git_trace2_event_log.py module."""
|
"""Unittests for the git_trace2_event_log.py module."""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import socket
|
import socket
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
import unittest
|
from typing import Any, Dict, List, Optional
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
import git_trace2_event_log
|
import git_trace2_event_log
|
||||||
import platform_utils
|
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.
|
"""Helper function to receive logs over a Unix domain socket.
|
||||||
|
|
||||||
Appends received messages on the provided socket and appends to
|
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())
|
received_traces.extend(data.decode("utf-8").splitlines())
|
||||||
|
|
||||||
|
|
||||||
class EventLogTestCase(unittest.TestCase):
|
PARENT_SID_KEY = "GIT_TRACE2_PARENT_SID"
|
||||||
"""TestCase for the EventLog module."""
|
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):
|
@pytest.fixture
|
||||||
"""Load the event_log module every time."""
|
def event_log() -> git_trace2_event_log.EventLog:
|
||||||
self._event_log_module = None
|
"""Fixture for the EventLog module."""
|
||||||
# By default we initialize with the expected case where
|
# By default we initialize with the expected case where
|
||||||
# repo launches us (so GIT_TRACE2_PARENT_SID is set).
|
# repo launches us (so GIT_TRACE2_PARENT_SID is set).
|
||||||
env = {
|
env = {PARENT_SID_KEY: PARENT_SID_VALUE}
|
||||||
self.PARENT_SID_KEY: self.PARENT_SID_VALUE,
|
return git_trace2_event_log.EventLog(env=env)
|
||||||
}
|
|
||||||
self._event_log_module = git_trace2_event_log.EventLog(env=env)
|
|
||||||
self._log_data = None
|
|
||||||
|
|
||||||
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."""
|
assert os.path.dirname(event_log.Write()) == str(tmp_path)
|
||||||
self.assertIn("event", log_entry)
|
|
||||||
self.assertIn("sid", log_entry)
|
|
||||||
self.assertIn("thread", log_entry)
|
|
||||||
self.assertIn("time", log_entry)
|
|
||||||
|
|
||||||
# Do basic data format validation.
|
|
||||||
if expected_event_name:
|
def test_write_no_git_config(event_log: git_trace2_event_log.EventLog) -> None:
|
||||||
self.assertEqual(expected_event_name, log_entry["event"])
|
"""Test Write() with no git config variable present exits with None."""
|
||||||
if full_sid:
|
with mock.patch.object(event_log, "_GetEventTargetPath", return_value=None):
|
||||||
self.assertRegex(log_entry["sid"], self.FULL_SID_REGEX)
|
assert event_log.Write() is None
|
||||||
else:
|
|
||||||
self.assertRegex(log_entry["sid"], self.SELF_SID_REGEX)
|
|
||||||
self.assertRegex(
|
def test_write_non_string(event_log: git_trace2_event_log.EventLog) -> None:
|
||||||
log_entry["time"], r"^\d+-\d+-\d+T\d+:\d+:\d+\.\d+\+00:00$"
|
"""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):
|
with server_ready:
|
||||||
"""Helper function to read log data into a list."""
|
server_ready.wait(timeout=120)
|
||||||
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(self, s, prefix):
|
event_log.StartEvent([])
|
||||||
"""Return a copy string after removing |prefix| from |s|, if present or
|
path = event_log.Write(path=f"af_unix:{socket_path}")
|
||||||
the original string."""
|
finally:
|
||||||
if s.startswith(prefix):
|
server_thread.join(timeout=5)
|
||||||
return s[len(prefix) :]
|
|
||||||
else:
|
|
||||||
return s
|
|
||||||
|
|
||||||
def test_initial_state_with_parent_sid(self):
|
assert path == f"af_unix:stream:{socket_path}"
|
||||||
"""Test initial state when 'GIT_TRACE2_PARENT_SID' is set by parent."""
|
assert len(received_traces) == 2
|
||||||
self.assertRegex(self._event_log_module.full_sid, self.FULL_SID_REGEX)
|
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):
|
class TestEventLogVerbose:
|
||||||
"""Test 'version' event data is valid.
|
"""TestCase for the EventLog module verbose logging."""
|
||||||
|
|
||||||
Verify that the 'version' event is written even when no other
|
def test_write_socket_error_no_verbose(self) -> None:
|
||||||
events are addded.
|
"""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:
|
def test_write_socket_error_verbose(self) -> None:
|
||||||
<version event>
|
"""Test Write() printing of socket errors when verbose."""
|
||||||
"""
|
event_log = git_trace2_event_log.EventLog(env={})
|
||||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
event_log.verbose = True
|
||||||
log_path = self._event_log_module.Write(path=tempdir)
|
with contextlib.redirect_stderr(
|
||||||
self._log_data = self.readLog(log_path)
|
io.StringIO()
|
||||||
|
) as mock_stderr, mock.patch(
|
||||||
# A log with no added events should only have the version entry.
|
"socket.socket", side_effect=OSError("Mock error")
|
||||||
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
|
|
||||||
):
|
):
|
||||||
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):
|
def test_write_file_error_no_verbose(self) -> None:
|
||||||
"""Test Write() with non-string type for |path| throws TypeError."""
|
"""Test Write() suppression of file errors when not verbose."""
|
||||||
with self.assertRaises(TypeError):
|
event_log = git_trace2_event_log.EventLog(env={})
|
||||||
self._event_log_module.Write(path=1234)
|
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):
|
def test_write_file_error_verbose(self) -> None:
|
||||||
"""Test Write() with Unix domain socket for |path| and validate received
|
"""Test Write() printing of file errors when verbose."""
|
||||||
traces."""
|
event_log = git_trace2_event_log.EventLog(env={})
|
||||||
received_traces = []
|
event_log.verbose = True
|
||||||
with tempfile.TemporaryDirectory(
|
with contextlib.redirect_stderr(
|
||||||
prefix="test_server_sockets"
|
io.StringIO()
|
||||||
) as tempdir:
|
) as mock_stderr, mock.patch(
|
||||||
socket_path = os.path.join(tempdir, "server.sock")
|
"tempfile.NamedTemporaryFile",
|
||||||
server_ready = threading.Condition()
|
side_effect=FileExistsError("Mock error"),
|
||||||
# Start "server" listening on Unix domain socket at socket_path.
|
):
|
||||||
try:
|
event_log.Write(path="/tmp")
|
||||||
server_thread = threading.Thread(
|
assert (
|
||||||
target=serverLoggingThread,
|
"git trace2 logging failed: FileExistsError"
|
||||||
args=(socket_path, server_ready, received_traces),
|
in mock_stderr.getvalue()
|
||||||
)
|
)
|
||||||
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)
|
|
||||||
|
|||||||
+37
-32
@@ -14,42 +14,47 @@
|
|||||||
|
|
||||||
"""Unittests for the hooks.py module."""
|
"""Unittests for the hooks.py module."""
|
||||||
|
|
||||||
import unittest
|
import pytest
|
||||||
|
|
||||||
import hooks
|
import hooks
|
||||||
|
|
||||||
|
|
||||||
class RepoHookShebang(unittest.TestCase):
|
@pytest.mark.parametrize(
|
||||||
"""Check shebang parsing in RepoHook."""
|
"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):
|
@pytest.mark.parametrize(
|
||||||
"""Lines whose shebang points directly to the interpreter."""
|
"shebang, interp",
|
||||||
DATA = (
|
(
|
||||||
("#!/foo", "/foo"),
|
("#!/foo", "/foo"),
|
||||||
("#! /foo", "/foo"),
|
("#! /foo", "/foo"),
|
||||||
("#!/bin/foo ", "/bin/foo"),
|
("#!/bin/foo ", "/bin/foo"),
|
||||||
("#! /usr/foo ", "/usr/foo"),
|
("#! /usr/foo ", "/usr/foo"),
|
||||||
("#! /usr/foo -args", "/usr/foo"),
|
("#! /usr/foo -args", "/usr/foo"),
|
||||||
)
|
),
|
||||||
for shebang, interp in DATA:
|
)
|
||||||
self.assertEqual(
|
def test_direct_interp(shebang: str, interp: str) -> None:
|
||||||
hooks.RepoHook._ExtractInterpFromShebang(shebang), interp
|
"""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`."""
|
@pytest.mark.parametrize(
|
||||||
DATA = (
|
"shebang, interp",
|
||||||
("#!/usr/bin/env foo", "foo"),
|
(
|
||||||
("#!/bin/env foo", "foo"),
|
("#!/usr/bin/env foo", "foo"),
|
||||||
("#! /bin/env /bin/foo ", "/bin/foo"),
|
("#!/bin/env foo", "foo"),
|
||||||
)
|
("#! /bin/env /bin/foo ", "/bin/foo"),
|
||||||
for shebang, interp in DATA:
|
),
|
||||||
self.assertEqual(
|
)
|
||||||
hooks.RepoHook._ExtractInterpFromShebang(shebang), interp
|
def test_env_interp(shebang: str, interp: str) -> None:
|
||||||
)
|
"""Lines whose shebang launches through `env`."""
|
||||||
|
assert hooks.RepoHook._ExtractInterpFromShebang(shebang) == interp
|
||||||
|
|||||||
@@ -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
|
||||||
+923
-424
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