mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-07-17 14:17:01 +00:00
Compare commits
159 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 |
@@ -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>
|
||||||
|
|||||||
+1
-5
@@ -399,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:
|
||||||
@@ -547,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.
|
||||||
|
|||||||
+2
-1
@@ -1 +1,2 @@
|
|||||||
black<24
|
# NB: Keep in sync with run_tests.vpython3.
|
||||||
|
black<26
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ Instead, you should use standard Git workflows like [git worktree] or
|
|||||||
(e.g. a local mirror & a public review server) while avoiding duplicating
|
(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
|
||||||
|
|||||||
+82
-61
@@ -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,19 +97,21 @@ 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>
|
<!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>
|
||||||
@@ -116,7 +120,7 @@ following DTD:
|
|||||||
<!ATTLIST remove-project base-rev 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>
|
||||||
@@ -125,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>
|
||||||
@@ -211,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
|
||||||
@@ -231,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
|
||||||
@@ -304,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`.
|
||||||
@@ -372,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
|
||||||
@@ -403,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.
|
||||||
@@ -412,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.
|
||||||
|
|
||||||
@@ -420,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
|
||||||
@@ -446,19 +454,20 @@ 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
|
||||||
@@ -469,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.
|
||||||
@@ -482,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
|
||||||
@@ -579,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}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -111,10 +111,6 @@ class GitAuthError(RepoExitError):
|
|||||||
"""Cannot talk to remote due to auth issue."""
|
"""Cannot talk to remote due to auth issue."""
|
||||||
|
|
||||||
|
|
||||||
class GitcUnsupportedError(RepoExitError):
|
|
||||||
"""Gitc no longer supported."""
|
|
||||||
|
|
||||||
|
|
||||||
class UploadError(RepoError):
|
class UploadError(RepoError):
|
||||||
"""A bundle upload to Gerrit did not succeed."""
|
"""A bundle upload to Gerrit did not succeed."""
|
||||||
|
|
||||||
|
|||||||
+10
-11
@@ -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
|
||||||
@@ -48,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:
|
||||||
@@ -83,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",
|
||||||
@@ -96,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
|
||||||
|
|
||||||
@@ -238,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:
|
||||||
@@ -350,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,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -612,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
-20
@@ -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):
|
||||||
@@ -375,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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -68,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:
|
||||||
@@ -130,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):
|
||||||
@@ -159,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):
|
||||||
@@ -307,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:
|
||||||
@@ -320,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
|
||||||
@@ -346,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)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -294,6 +294,102 @@ class _Repo:
|
|||||||
result = run()
|
result = run()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def _autocorrect_command_name(
|
||||||
|
self, name: str, config: RepoConfig
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""Autocorrect command name based on user's git config."""
|
||||||
|
|
||||||
|
close_commands = difflib.get_close_matches(
|
||||||
|
name, self.commands.keys(), n=5, cutoff=0.7
|
||||||
|
)
|
||||||
|
|
||||||
|
if not close_commands:
|
||||||
|
logger.error(
|
||||||
|
"repo: '%s' is not a repo command. See 'repo help'.", name
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
assumed = close_commands[0]
|
||||||
|
autocorrect = config.GetString("help.autocorrect")
|
||||||
|
|
||||||
|
# If there are multiple close matches, git won't automatically run one.
|
||||||
|
# We'll always prompt instead of guessing.
|
||||||
|
if len(close_commands) > 1:
|
||||||
|
autocorrect = "prompt"
|
||||||
|
|
||||||
|
# Handle git configuration boolean values:
|
||||||
|
# 0, "false", "off", "no", "show": show suggestion (default)
|
||||||
|
# 1, "true", "on", "yes", "immediate": run suggestion immediately
|
||||||
|
# "never": don't run or show any suggested command
|
||||||
|
# "prompt": show the suggestion and prompt for confirmation
|
||||||
|
# positive number > 1: run suggestion after specified deciseconds
|
||||||
|
if autocorrect is None:
|
||||||
|
autocorrect = "0"
|
||||||
|
|
||||||
|
autocorrect = autocorrect.lower()
|
||||||
|
|
||||||
|
if autocorrect in ("0", "false", "off", "no", "show"):
|
||||||
|
autocorrect = 0
|
||||||
|
elif autocorrect in ("true", "on", "yes", "immediate"):
|
||||||
|
autocorrect = -1 # immediate
|
||||||
|
elif autocorrect == "never":
|
||||||
|
return None
|
||||||
|
elif autocorrect == "prompt":
|
||||||
|
logger.warning(
|
||||||
|
"You called a repo command named "
|
||||||
|
"'%s', which does not exist.",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
resp = input(f"Run '{assumed}' instead [y/N]? ")
|
||||||
|
if resp.lower().startswith("y"):
|
||||||
|
return assumed
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
autocorrect = int(autocorrect)
|
||||||
|
except ValueError:
|
||||||
|
autocorrect = 0
|
||||||
|
|
||||||
|
if autocorrect != 0:
|
||||||
|
if autocorrect < 0:
|
||||||
|
logger.warning(
|
||||||
|
"You called a repo command named "
|
||||||
|
"'%s', which does not exist.\n"
|
||||||
|
"Continuing assuming that "
|
||||||
|
"you meant '%s'.",
|
||||||
|
name,
|
||||||
|
assumed,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
delay = autocorrect * 0.1
|
||||||
|
logger.warning(
|
||||||
|
"You called a repo command named "
|
||||||
|
"'%s', which does not exist.\n"
|
||||||
|
"Continuing in %.1f seconds, assuming "
|
||||||
|
"that you meant '%s'.",
|
||||||
|
name,
|
||||||
|
delay,
|
||||||
|
assumed,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
time.sleep(delay)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
return None
|
||||||
|
return assumed
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
"repo: '%s' is not a repo command. See 'repo help'.", name
|
||||||
|
)
|
||||||
|
logger.warning(
|
||||||
|
"The most similar command%s\n\t%s",
|
||||||
|
"s are" if len(close_commands) > 1 else " is",
|
||||||
|
"\n\t".join(close_commands),
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
def _RunLong(self, name, gopts, argv, git_trace2_event_log):
|
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
|
||||||
@@ -308,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
|
||||||
|
|
||||||
@@ -343,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:
|
||||||
@@ -357,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():
|
||||||
@@ -365,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
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
|
.TH REPO "1" "April 2025" "repo gc" "Repo Manual"
|
||||||
|
.SH NAME
|
||||||
|
repo \- repo gc - manual page for repo gc
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B repo
|
||||||
|
\fI\,gc\/\fR
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Summary
|
||||||
|
.PP
|
||||||
|
Cleaning up internal repo and Git state.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
\fB\-h\fR, \fB\-\-help\fR
|
||||||
|
show this help message and exit
|
||||||
|
.TP
|
||||||
|
\fB\-n\fR, \fB\-\-dry\-run\fR
|
||||||
|
do everything except actually delete
|
||||||
|
.TP
|
||||||
|
\fB\-y\fR, \fB\-\-yes\fR
|
||||||
|
answer yes to all safe prompts
|
||||||
|
.TP
|
||||||
|
\fB\-\-repack\fR
|
||||||
|
repack all projects that use partial clone with
|
||||||
|
filter=blob:none
|
||||||
|
.SS Logging options:
|
||||||
|
.TP
|
||||||
|
\fB\-v\fR, \fB\-\-verbose\fR
|
||||||
|
show all output
|
||||||
|
.TP
|
||||||
|
\fB\-q\fR, \fB\-\-quiet\fR
|
||||||
|
only show errors
|
||||||
|
.SS Multi\-manifest options:
|
||||||
|
.TP
|
||||||
|
\fB\-\-outer\-manifest\fR
|
||||||
|
operate starting at the outermost manifest
|
||||||
|
.TP
|
||||||
|
\fB\-\-no\-outer\-manifest\fR
|
||||||
|
do not operate on outer manifests
|
||||||
|
.TP
|
||||||
|
\fB\-\-this\-manifest\-only\fR
|
||||||
|
only operate on this (sub)manifest
|
||||||
|
.TP
|
||||||
|
\fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
|
||||||
|
operate on this manifest and its submanifests
|
||||||
|
.PP
|
||||||
|
Run `repo help gc` to view the detailed manual.
|
||||||
+134
-61
@@ -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" "April 2024" "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,26 +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 #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>
|
||||||
.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>
|
||||||
@@ -208,7 +247,7 @@ 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>
|
||||||
@@ -288,7 +327,9 @@ when syncing a revision locked manifest in \fB\-c\fR mode to avoid having to syn
|
|||||||
entire ref space. Project elements not setting their own `upstream` will inherit
|
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
|
||||||
@@ -304,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
|
||||||
@@ -374,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`.
|
||||||
@@ -436,7 +459,7 @@ Attribute `dest\-branch`: Name of a Git branch (e.g. `main`). When using `repo
|
|||||||
upload`, changes will be submitted for code review on this branch. If
|
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
|
||||||
@@ -463,6 +486,21 @@ mirror repository according to its `path` attribute (if supplied) rather than
|
|||||||
the `name` attribute. This attribute only applies to the local mirrors syncing,
|
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.
|
||||||
@@ -472,6 +510,11 @@ of an existing project without completely replacing the existing project
|
|||||||
definition. This makes the local manifest more robust against changes to the
|
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
|
||||||
@@ -480,8 +523,8 @@ repo client where the Git working directory for this project should be placed.
|
|||||||
This is used to move a project in the checkout by overriding the existing `path`
|
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`.
|
||||||
@@ -495,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
|
||||||
@@ -520,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.
|
||||||
@@ -533,6 +590,11 @@ Parent directories of "dest" will be automatically created if missing.
|
|||||||
The symlink target may be a file or directory, but it may not point outside of
|
The 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 a project from the internal manifest table, possibly allowing a
|
Deletes a project from the internal manifest table, possibly allowing a
|
||||||
@@ -556,6 +618,14 @@ Logic otherwise behaves like both are specified.
|
|||||||
Attribute `optional`: Set to true to ignore remove\-project elements with no
|
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.
|
||||||
@@ -616,13 +686,16 @@ 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
|
||||||
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.
|
||||||
.PP
|
.PP
|
||||||
Local Manifests
|
Local Manifests
|
||||||
.PP
|
.PP
|
||||||
|
|||||||
+19
-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" "September 2024" "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)
|
||||||
@@ -58,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
|
||||||
@@ -145,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
|
||||||
|
|||||||
+19
-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" "September 2024" "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)
|
||||||
@@ -58,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
|
||||||
@@ -152,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
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
|
.TH REPO "1" "November 2025" "repo wipe" "Repo Manual"
|
||||||
|
.SH NAME
|
||||||
|
repo \- repo wipe - manual page for repo wipe
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B repo
|
||||||
|
\fI\,wipe <project>\/\fR...
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Summary
|
||||||
|
.PP
|
||||||
|
Wipe projects from the worktree
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
\fB\-h\fR, \fB\-\-help\fR
|
||||||
|
show this help message and exit
|
||||||
|
.TP
|
||||||
|
\fB\-f\fR, \fB\-\-force\fR
|
||||||
|
force wipe shared projects and uncommitted changes
|
||||||
|
.TP
|
||||||
|
\fB\-\-force\-uncommitted\fR
|
||||||
|
force wipe even if there are uncommitted changes
|
||||||
|
.TP
|
||||||
|
\fB\-\-force\-shared\fR
|
||||||
|
force wipe even if the project shares an object
|
||||||
|
directory
|
||||||
|
.SS Logging options:
|
||||||
|
.TP
|
||||||
|
\fB\-v\fR, \fB\-\-verbose\fR
|
||||||
|
show all output
|
||||||
|
.TP
|
||||||
|
\fB\-q\fR, \fB\-\-quiet\fR
|
||||||
|
only show errors
|
||||||
|
.SS Multi\-manifest options:
|
||||||
|
.TP
|
||||||
|
\fB\-\-outer\-manifest\fR
|
||||||
|
operate starting at the outermost manifest
|
||||||
|
.TP
|
||||||
|
\fB\-\-no\-outer\-manifest\fR
|
||||||
|
do not operate on outer manifests
|
||||||
|
.TP
|
||||||
|
\fB\-\-this\-manifest\-only\fR
|
||||||
|
only operate on this (sub)manifest
|
||||||
|
.TP
|
||||||
|
\fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
|
||||||
|
operate on this manifest and its submanifests
|
||||||
|
.PP
|
||||||
|
Run `repo help wipe` to view the detailed manual.
|
||||||
|
.SH DETAILS
|
||||||
|
.PP
|
||||||
|
The 'repo wipe' command removes the specified projects from the worktree (the
|
||||||
|
checked out source code) and deletes the project's git data from `.repo`.
|
||||||
|
.PP
|
||||||
|
This is a destructive operation and cannot be undone.
|
||||||
|
.PP
|
||||||
|
Projects can be specified either by name, or by a relative or absolute path to
|
||||||
|
the project's local directory.
|
||||||
|
.SH EXAMPLES
|
||||||
|
.SS # Wipe the project "platform/build" by name:
|
||||||
|
$ repo wipe platform/build
|
||||||
|
.SS # Wipe the project at the path "build/make":
|
||||||
|
$ repo wipe build/make
|
||||||
+7
-1
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "April 2024" "repo" "Repo Manual"
|
.TH REPO "1" "November 2025" "repo" "Repo Manual"
|
||||||
.SH NAME
|
.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,6 +79,9 @@ Download and checkout a change
|
|||||||
forall
|
forall
|
||||||
Run a shell command in each project
|
Run a shell command in each project
|
||||||
.TP
|
.TP
|
||||||
|
gc
|
||||||
|
Cleaning up internal repo and Git state.
|
||||||
|
.TP
|
||||||
grep
|
grep
|
||||||
Print lines matching a pattern
|
Print lines matching a pattern
|
||||||
.TP
|
.TP
|
||||||
@@ -129,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
|
||||||
|
|||||||
+131
-94
@@ -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,7 +432,7 @@ 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 submanifest_path and not outer_client:
|
if submanifest_path and not outer_client:
|
||||||
@@ -567,21 +567,29 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
"""
|
"""
|
||||||
return [x for x in re.split(r"[,\s]+", field) if x]
|
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")
|
||||||
@@ -624,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")
|
||||||
@@ -654,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):
|
||||||
@@ -725,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":
|
||||||
@@ -749,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(""))
|
||||||
@@ -813,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",
|
||||||
}
|
}
|
||||||
@@ -1014,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):
|
||||||
@@ -1116,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()
|
||||||
@@ -1171,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.
|
||||||
@@ -1202,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,
|
||||||
)
|
)
|
||||||
@@ -1262,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,
|
||||||
):
|
):
|
||||||
@@ -1271,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.
|
||||||
@@ -1299,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:
|
||||||
@@ -1307,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):
|
||||||
@@ -1328,33 +1346,23 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
)
|
)
|
||||||
# should isolate this to the exact exception, but that's
|
# 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
|
||||||
|
|
||||||
@@ -1463,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:
|
||||||
@@ -1484,7 +1492,15 @@ 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 base_revision:
|
||||||
if p.revisionExpr != base_revision:
|
if p.revisionExpr != base_revision:
|
||||||
@@ -1514,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:
|
||||||
@@ -1745,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)
|
||||||
@@ -1807,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 == "":
|
||||||
@@ -1910,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,
|
||||||
@@ -1935,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):
|
||||||
@@ -1958,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,
|
||||||
@@ -1968,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)
|
||||||
)
|
)
|
||||||
@@ -2056,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
|
||||||
)
|
)
|
||||||
@@ -2107,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.
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
+34
-4
@@ -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
|
||||||
@@ -101,6 +104,7 @@ class Progress:
|
|||||||
self._units = units
|
self._units = units
|
||||||
self._elide = elide and _TTY
|
self._elide = elide and _TTY
|
||||||
self._quiet = quiet
|
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
|
||||||
@@ -118,6 +122,11 @@ class Progress:
|
|||||||
if not quiet and show_elapsed:
|
if not quiet and 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)
|
||||||
@@ -127,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
|
||||||
@@ -150,6 +159,8 @@ 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
|
||||||
@@ -195,7 +206,26 @@ 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 self._quiet:
|
if not _TTY or IsTraceToStderr() or self._quiet:
|
||||||
return
|
return
|
||||||
|
|||||||
+658
-276
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
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from typing import List, Optional
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
assert sys.version_info >= (3, 8), "Python 3.8+ required"
|
assert sys.version_info >= (3, 9), "Release framework requires Python 3.9+"
|
||||||
|
|
||||||
|
|
||||||
TOPDIR = Path(__file__).resolve().parent.parent
|
TOPDIR = Path(__file__).resolve().parent.parent
|
||||||
|
|||||||
@@ -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 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");
|
||||||
@@ -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)
|
||||||
@@ -124,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, 48)
|
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)
|
||||||
@@ -215,11 +220,8 @@ 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
|
||||||
@@ -235,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)
|
||||||
@@ -487,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)
|
||||||
@@ -521,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:
|
||||||
@@ -531,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:
|
||||||
@@ -552,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."""
|
||||||
|
|
||||||
|
|
||||||
@@ -638,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:
|
||||||
@@ -720,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):
|
||||||
@@ -894,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():
|
||||||
@@ -1164,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}")
|
||||||
@@ -1292,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]
|
||||||
|
|
||||||
@@ -1304,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").
|
||||||
"""
|
"""
|
||||||
@@ -1317,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:
|
||||||
@@ -1335,10 +1283,11 @@ 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()
|
||||||
print(f"OS {uname.system} {uname.release} ({uname.version})")
|
print(f"OS {uname.system} {uname.release} ({uname.version})")
|
||||||
@@ -1371,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
|
||||||
|
|
||||||
|
|
||||||
@@ -1406,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.
|
||||||
@@ -1437,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(
|
||||||
|
|||||||
@@ -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,18 +15,80 @@
|
|||||||
|
|
||||||
"""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 = [
|
||||||
@@ -35,35 +97,94 @@ def run_black():
|
|||||||
"release/update-hooks",
|
"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
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -52,12 +52,12 @@ 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:
|
|
||||||
print("fatal: ssh not installed", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
print(
|
print(
|
||||||
"fatal: unable to detect ssh version"
|
"fatal: unable to detect ssh version"
|
||||||
@@ -102,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):
|
||||||
@@ -140,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):
|
||||||
@@ -282,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)
|
||||||
@@ -306,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
|
||||||
|
|||||||
@@ -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",
|
||||||
)
|
)
|
||||||
|
|||||||
+4
-1
@@ -167,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)
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ 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",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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",
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-5
@@ -141,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",
|
||||||
@@ -149,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",
|
||||||
@@ -157,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",
|
||||||
)
|
)
|
||||||
|
|||||||
+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
|
||||||
+1
-2
@@ -93,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("(")
|
||||||
@@ -120,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",
|
||||||
|
|||||||
+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()
|
||||||
|
|
||||||
|
|||||||
@@ -127,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):
|
||||||
|
|||||||
@@ -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",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -51,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",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ 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",
|
||||||
|
|||||||
+1026
-185
File diff suppressed because it is too large
Load Diff
+18
-10
@@ -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
|
||||||
@@ -267,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(
|
||||||
@@ -281,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",
|
||||||
)
|
)
|
||||||
@@ -310,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)",
|
||||||
)
|
)
|
||||||
@@ -318,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",
|
||||||
)
|
)
|
||||||
@@ -628,6 +625,16 @@ 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,
|
||||||
@@ -640,7 +647,7 @@ 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,
|
patchset_description=opt.patchset_description,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -737,8 +744,8 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
pending = []
|
pending = []
|
||||||
for result in results:
|
for result in results:
|
||||||
project_idx, avail = result
|
project_idx, avail = result
|
||||||
|
project = projects[project_idx]
|
||||||
if avail is None:
|
if avail is None:
|
||||||
project = projects[project_idx]
|
|
||||||
logger.error(
|
logger.error(
|
||||||
'repo: error: %s: Unable to upload branch "%s". '
|
'repo: error: %s: Unable to upload branch "%s". '
|
||||||
"You might be able to fix the branch by running:\n"
|
"You might be able to fix the branch by running:\n"
|
||||||
@@ -748,7 +755,7 @@ 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
|
||||||
|
|
||||||
with self.ParallelContext():
|
with self.ParallelContext():
|
||||||
@@ -796,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
|
||||||
+1
-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.
|
||||||
|
|||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
gitc_dir=/test/usr/local/google/gitc
|
|
||||||
+43
-43
@@ -14,61 +14,61 @@
|
|||||||
|
|
||||||
"""Unittests for the color.py module."""
|
"""Unittests for the color.py module."""
|
||||||
|
|
||||||
import os
|
import pytest
|
||||||
import unittest
|
import utils_for_test
|
||||||
|
|
||||||
import color
|
import color
|
||||||
import git_config
|
import git_config
|
||||||
|
|
||||||
|
|
||||||
def fixture(*paths):
|
@pytest.fixture
|
||||||
"""Return a path relative to test/fixtures."""
|
def coloring() -> color.Coloring:
|
||||||
return os.path.join(os.path.dirname(__file__), "fixtures", *paths)
|
"""Create a Coloring object for testing."""
|
||||||
|
config_fixture = utils_for_test.FIXTURES_DIR / "test.gitconfig"
|
||||||
|
config = git_config.GitConfig(config_fixture)
|
||||||
|
color.SetDefaultColoring("true")
|
||||||
|
return color.Coloring(config, "status")
|
||||||
|
|
||||||
|
|
||||||
class ColoringTests(unittest.TestCase):
|
def test_Color_Parse_all_params_none(coloring: color.Coloring) -> None:
|
||||||
"""tests of the Coloring class."""
|
"""all params are None"""
|
||||||
|
val = coloring._parse(None, None, None, None)
|
||||||
|
assert val == ""
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
"""Create a GitConfig object using the test.gitconfig fixture."""
|
|
||||||
config_fixture = fixture("test.gitconfig")
|
|
||||||
self.config = git_config.GitConfig(config_fixture)
|
|
||||||
color.SetDefaultColoring("true")
|
|
||||||
self.color = color.Coloring(self.config, "status")
|
|
||||||
|
|
||||||
def test_Color_Parse_all_params_none(self):
|
def test_Color_Parse_first_parameter_none(coloring: color.Coloring) -> None:
|
||||||
"""all params are None"""
|
"""check fg & bg & attr"""
|
||||||
val = self.color._parse(None, None, None, None)
|
val = coloring._parse(None, "black", "red", "ul")
|
||||||
self.assertEqual("", val)
|
assert val == "\x1b[4;30;41m"
|
||||||
|
|
||||||
def test_Color_Parse_first_parameter_none(self):
|
|
||||||
"""check fg & bg & attr"""
|
|
||||||
val = self.color._parse(None, "black", "red", "ul")
|
|
||||||
self.assertEqual("\x1b[4;30;41m", val)
|
|
||||||
|
|
||||||
def test_Color_Parse_one_entry(self):
|
def test_Color_Parse_one_entry(coloring: color.Coloring) -> None:
|
||||||
"""check fg"""
|
"""check fg"""
|
||||||
val = self.color._parse("one", None, None, None)
|
val = coloring._parse("one", None, None, None)
|
||||||
self.assertEqual("\033[33m", val)
|
assert val == "\033[33m"
|
||||||
|
|
||||||
def test_Color_Parse_two_entry(self):
|
|
||||||
"""check fg & bg"""
|
|
||||||
val = self.color._parse("two", None, None, None)
|
|
||||||
self.assertEqual("\033[35;46m", val)
|
|
||||||
|
|
||||||
def test_Color_Parse_three_entry(self):
|
def test_Color_Parse_two_entry(coloring: color.Coloring) -> None:
|
||||||
"""check fg & bg & attr"""
|
"""check fg & bg"""
|
||||||
val = self.color._parse("three", None, None, None)
|
val = coloring._parse("two", None, None, None)
|
||||||
self.assertEqual("\033[4;30;41m", val)
|
assert val == "\033[35;46m"
|
||||||
|
|
||||||
def test_Color_Parse_reset_entry(self):
|
|
||||||
"""check reset entry"""
|
|
||||||
val = self.color._parse("reset", None, None, None)
|
|
||||||
self.assertEqual("\033[m", val)
|
|
||||||
|
|
||||||
def test_Color_Parse_empty_entry(self):
|
def test_Color_Parse_three_entry(coloring: color.Coloring) -> None:
|
||||||
"""check empty entry"""
|
"""check fg & bg & attr"""
|
||||||
val = self.color._parse("none", "blue", "white", "dim")
|
val = coloring._parse("three", None, None, None)
|
||||||
self.assertEqual("\033[2;34;47m", val)
|
assert val == "\033[4;30;41m"
|
||||||
val = self.color._parse("empty", "green", "white", "bold")
|
|
||||||
self.assertEqual("\033[1;32;47m", val)
|
|
||||||
|
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
|
||||||
+822
-455
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||||
@@ -14,39 +14,35 @@
|
|||||||
|
|
||||||
"""Unittests for the platform_utils.py module."""
|
"""Unittests for the platform_utils.py module."""
|
||||||
|
|
||||||
import os
|
from pathlib import Path
|
||||||
import tempfile
|
|
||||||
import unittest
|
import pytest
|
||||||
|
|
||||||
import platform_utils
|
import platform_utils
|
||||||
|
|
||||||
|
|
||||||
class RemoveTests(unittest.TestCase):
|
def test_remove_missing_ok(tmp_path: Path) -> None:
|
||||||
"""Check remove() helper."""
|
"""Check missing_ok handling."""
|
||||||
|
path = tmp_path / "test"
|
||||||
|
|
||||||
def testMissingOk(self):
|
# Should not fail.
|
||||||
"""Check missing_ok handling."""
|
platform_utils.remove(path, missing_ok=True)
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
|
||||||
path = os.path.join(tmpdir, "test")
|
|
||||||
|
|
||||||
# Should not fail.
|
# Should fail.
|
||||||
platform_utils.remove(path, missing_ok=True)
|
with pytest.raises(OSError):
|
||||||
|
platform_utils.remove(path)
|
||||||
|
with pytest.raises(OSError):
|
||||||
|
platform_utils.remove(path, missing_ok=False)
|
||||||
|
|
||||||
# Should fail.
|
# Should not fail if it exists.
|
||||||
self.assertRaises(OSError, platform_utils.remove, path)
|
path.touch()
|
||||||
self.assertRaises(
|
platform_utils.remove(path, missing_ok=True)
|
||||||
OSError, platform_utils.remove, path, missing_ok=False
|
assert not path.exists()
|
||||||
)
|
|
||||||
|
|
||||||
# Should not fail if it exists.
|
path.touch()
|
||||||
open(path, "w").close()
|
platform_utils.remove(path)
|
||||||
platform_utils.remove(path, missing_ok=True)
|
assert not path.exists()
|
||||||
self.assertFalse(os.path.exists(path))
|
|
||||||
|
|
||||||
open(path, "w").close()
|
path.touch()
|
||||||
platform_utils.remove(path)
|
platform_utils.remove(path, missing_ok=False)
|
||||||
self.assertFalse(os.path.exists(path))
|
assert not path.exists()
|
||||||
|
|
||||||
open(path, "w").close()
|
|
||||||
platform_utils.remove(path, missing_ok=False)
|
|
||||||
self.assertFalse(os.path.exists(path))
|
|
||||||
|
|||||||
+282
-22
@@ -17,37 +17,22 @@
|
|||||||
import contextlib
|
import contextlib
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from typing import Optional
|
||||||
import unittest
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import utils_for_test
|
||||||
|
|
||||||
import error
|
import error
|
||||||
import git_command
|
|
||||||
import git_config
|
import git_config
|
||||||
import manifest_xml
|
import manifest_xml
|
||||||
import platform_utils
|
import platform_utils
|
||||||
import project
|
import project
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def TempGitTree():
|
|
||||||
"""Create a new empty git checkout for testing."""
|
|
||||||
with tempfile.TemporaryDirectory(prefix="repo-tests") as tempdir:
|
|
||||||
# Tests need to assume, that main is default branch at init,
|
|
||||||
# which is not supported in config until 2.28.
|
|
||||||
cmd = ["git", "init"]
|
|
||||||
if git_command.git_require((2, 28, 0)):
|
|
||||||
cmd += ["--initial-branch=main"]
|
|
||||||
else:
|
|
||||||
# Use template dir for init.
|
|
||||||
templatedir = tempfile.mkdtemp(prefix=".test-template")
|
|
||||||
with open(os.path.join(templatedir, "HEAD"), "w") as fp:
|
|
||||||
fp.write("ref: refs/heads/main\n")
|
|
||||||
cmd += ["--template", templatedir]
|
|
||||||
subprocess.check_call(cmd, cwd=tempdir)
|
|
||||||
yield tempdir
|
|
||||||
|
|
||||||
|
|
||||||
class FakeProject:
|
class FakeProject:
|
||||||
"""A fake for Project for basic functionality."""
|
"""A fake for Project for basic functionality."""
|
||||||
|
|
||||||
@@ -63,13 +48,16 @@ class FakeProject:
|
|||||||
)
|
)
|
||||||
self.config = git_config.GitConfig.ForRepository(gitdir=self.gitdir)
|
self.config = git_config.GitConfig.ForRepository(gitdir=self.gitdir)
|
||||||
|
|
||||||
|
def RelPath(self, local: Optional[bool] = None) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class ReviewableBranchTests(unittest.TestCase):
|
class ReviewableBranchTests(unittest.TestCase):
|
||||||
"""Check ReviewableBranch behavior."""
|
"""Check ReviewableBranch behavior."""
|
||||||
|
|
||||||
def test_smoke(self):
|
def test_smoke(self):
|
||||||
"""A quick run through everything."""
|
"""A quick run through everything."""
|
||||||
with TempGitTree() as tempdir:
|
with utils_for_test.TempGitTree() as tempdir:
|
||||||
fakeproj = FakeProject(tempdir)
|
fakeproj = FakeProject(tempdir)
|
||||||
|
|
||||||
# Generate some commits.
|
# Generate some commits.
|
||||||
@@ -116,6 +104,29 @@ class ProjectTests(unittest.TestCase):
|
|||||||
"abcd00%21%21_%2b",
|
"abcd00%21%21_%2b",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@unittest.skipUnless(
|
||||||
|
utils_for_test.supports_reftable(),
|
||||||
|
"git reftable support is required for this test",
|
||||||
|
)
|
||||||
|
def test_get_head_unborn_reftable(self):
|
||||||
|
with tempfile.TemporaryDirectory(prefix="repo-tests") as tempdir:
|
||||||
|
subprocess.check_call(
|
||||||
|
[
|
||||||
|
"git",
|
||||||
|
"-c",
|
||||||
|
"init.defaultRefFormat=reftable",
|
||||||
|
"init",
|
||||||
|
"-q",
|
||||||
|
tempdir,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
fakeproj = FakeProject(tempdir)
|
||||||
|
expected = subprocess.check_output(
|
||||||
|
["git", "-C", tempdir, "symbolic-ref", "-q", "HEAD"],
|
||||||
|
encoding="utf-8",
|
||||||
|
).strip()
|
||||||
|
self.assertEqual(expected, fakeproj.work_git.GetHead())
|
||||||
|
|
||||||
|
|
||||||
class CopyLinkTestCase(unittest.TestCase):
|
class CopyLinkTestCase(unittest.TestCase):
|
||||||
"""TestCase for stub repo client checkouts.
|
"""TestCase for stub repo client checkouts.
|
||||||
@@ -359,6 +370,7 @@ class MigrateWorkTreeTests(unittest.TestCase):
|
|||||||
"""Check _MigrateOldWorkTreeGitDir handling."""
|
"""Check _MigrateOldWorkTreeGitDir handling."""
|
||||||
|
|
||||||
_SYMLINKS = {
|
_SYMLINKS = {
|
||||||
|
# go/keep-sorted start
|
||||||
"config",
|
"config",
|
||||||
"description",
|
"description",
|
||||||
"hooks",
|
"hooks",
|
||||||
@@ -367,9 +379,11 @@ class MigrateWorkTreeTests(unittest.TestCase):
|
|||||||
"objects",
|
"objects",
|
||||||
"packed-refs",
|
"packed-refs",
|
||||||
"refs",
|
"refs",
|
||||||
|
"reftable",
|
||||||
"rr-cache",
|
"rr-cache",
|
||||||
"shallow",
|
"shallow",
|
||||||
"svn",
|
"svn",
|
||||||
|
# go/keep-sorted end
|
||||||
}
|
}
|
||||||
_FILES = {
|
_FILES = {
|
||||||
"COMMIT_EDITMSG",
|
"COMMIT_EDITMSG",
|
||||||
@@ -448,6 +462,25 @@ class MigrateWorkTreeTests(unittest.TestCase):
|
|||||||
for name in self._SYMLINKS:
|
for name in self._SYMLINKS:
|
||||||
self.assertTrue((dotgit / name).is_symlink())
|
self.assertTrue((dotgit / name).is_symlink())
|
||||||
|
|
||||||
|
def test_reftable_anchor_with_refs_dir(self):
|
||||||
|
"""Migrate when reftable/ and refs/ are directories."""
|
||||||
|
with self._simple_layout() as tempdir:
|
||||||
|
dotgit = tempdir / "src/test/.git"
|
||||||
|
(dotgit / "refs").unlink()
|
||||||
|
(dotgit / "refs").mkdir()
|
||||||
|
(dotgit / "refs" / "heads").write_text("dummy")
|
||||||
|
|
||||||
|
(dotgit / "reftable").unlink()
|
||||||
|
(dotgit / "reftable").mkdir()
|
||||||
|
(dotgit / "reftable" / "tables.list").write_text("dummy")
|
||||||
|
project.Project._MigrateOldWorkTreeGitDir(str(dotgit))
|
||||||
|
|
||||||
|
self.assertTrue(dotgit.is_symlink())
|
||||||
|
self.assertEqual(
|
||||||
|
os.readlink(dotgit),
|
||||||
|
os.path.normpath("../../.repo/projects/src/test.git"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ManifestPropertiesFetchedCorrectly(unittest.TestCase):
|
class ManifestPropertiesFetchedCorrectly(unittest.TestCase):
|
||||||
"""Ensure properties are fetched properly."""
|
"""Ensure properties are fetched properly."""
|
||||||
@@ -467,7 +500,7 @@ class ManifestPropertiesFetchedCorrectly(unittest.TestCase):
|
|||||||
def test_manifest_config_properties(self):
|
def test_manifest_config_properties(self):
|
||||||
"""Test we are fetching the manifest config properties correctly."""
|
"""Test we are fetching the manifest config properties correctly."""
|
||||||
|
|
||||||
with TempGitTree() as tempdir:
|
with utils_for_test.TempGitTree() as tempdir:
|
||||||
fakeproj = self.setUpManifest(tempdir)
|
fakeproj = self.setUpManifest(tempdir)
|
||||||
|
|
||||||
# Set property using the expected Set method, then ensure
|
# Set property using the expected Set method, then ensure
|
||||||
@@ -534,3 +567,230 @@ class ManifestPropertiesFetchedCorrectly(unittest.TestCase):
|
|||||||
|
|
||||||
fakeproj.config.SetString("manifest.platform", "auto")
|
fakeproj.config.SetString("manifest.platform", "auto")
|
||||||
self.assertEqual(fakeproj.manifest_platform, "auto")
|
self.assertEqual(fakeproj.manifest_platform, "auto")
|
||||||
|
|
||||||
|
|
||||||
|
class StatelessSyncTests(unittest.TestCase):
|
||||||
|
"""Tests for stateless sync strategy."""
|
||||||
|
|
||||||
|
def _get_project(self, tempdir):
|
||||||
|
manifest = mock.MagicMock()
|
||||||
|
manifest.manifestProject.depth = None
|
||||||
|
manifest.manifestProject.dissociate = False
|
||||||
|
manifest.manifestProject.clone_filter = None
|
||||||
|
manifest.is_multimanifest = False
|
||||||
|
manifest.manifestProject.config.GetBoolean.return_value = False
|
||||||
|
|
||||||
|
remote = mock.MagicMock()
|
||||||
|
remote.name = "origin"
|
||||||
|
remote.url = "http://"
|
||||||
|
|
||||||
|
proj = project.Project(
|
||||||
|
manifest=manifest,
|
||||||
|
name="test-project",
|
||||||
|
remote=remote,
|
||||||
|
gitdir=os.path.join(tempdir, ".git"),
|
||||||
|
objdir=os.path.join(tempdir, ".git"),
|
||||||
|
worktree=tempdir,
|
||||||
|
relpath="test-project",
|
||||||
|
revisionExpr="1234abcd",
|
||||||
|
revisionId=None,
|
||||||
|
sync_strategy="stateless",
|
||||||
|
)
|
||||||
|
proj._CheckForImmutableRevision = mock.MagicMock(return_value=False)
|
||||||
|
proj._LsRemote = mock.MagicMock(
|
||||||
|
return_value="1234abcd\trefs/heads/main\n"
|
||||||
|
)
|
||||||
|
proj.bare_git = mock.MagicMock()
|
||||||
|
proj.bare_git.rev_parse.return_value = "5678abcd"
|
||||||
|
proj.bare_git.rev_list.return_value = ["0"]
|
||||||
|
proj.IsDirty = mock.MagicMock(return_value=False)
|
||||||
|
proj.GetBranches = mock.MagicMock(return_value=[])
|
||||||
|
proj.DeleteWorktree = mock.MagicMock()
|
||||||
|
proj._InitGitDir = mock.MagicMock()
|
||||||
|
proj._RemoteFetch = mock.MagicMock(return_value=True)
|
||||||
|
proj._InitRemote = mock.MagicMock()
|
||||||
|
proj._InitMRef = mock.MagicMock()
|
||||||
|
return proj
|
||||||
|
|
||||||
|
def test_sync_network_half_stateless_prune_needed(self):
|
||||||
|
"""Test stateless sync queues prune when needed."""
|
||||||
|
with utils_for_test.TempGitTree() as tempdir:
|
||||||
|
proj = self._get_project(tempdir)
|
||||||
|
res = proj.Sync_NetworkHalf()
|
||||||
|
|
||||||
|
self.assertTrue(res.success)
|
||||||
|
proj.DeleteWorktree.assert_not_called()
|
||||||
|
self.assertTrue(proj.stateless_prune_needed)
|
||||||
|
proj._RemoteFetch.assert_called_once()
|
||||||
|
|
||||||
|
def test_sync_local_half_stateless_prune(self):
|
||||||
|
"""Test stateless GC pruning is queued in Sync_LocalHalf."""
|
||||||
|
with utils_for_test.TempGitTree() as tempdir:
|
||||||
|
proj = self._get_project(tempdir)
|
||||||
|
proj.stateless_prune_needed = True
|
||||||
|
|
||||||
|
proj._Checkout = mock.MagicMock()
|
||||||
|
proj._InitWorkTree = mock.MagicMock()
|
||||||
|
proj.IsRebaseInProgress = mock.MagicMock(return_value=False)
|
||||||
|
proj.IsCherryPickInProgress = mock.MagicMock(return_value=False)
|
||||||
|
proj.bare_ref = mock.MagicMock()
|
||||||
|
proj.bare_ref.all = {}
|
||||||
|
proj.GetRevisionId = mock.MagicMock(return_value="1234abcd")
|
||||||
|
proj._CopyAndLinkFiles = mock.MagicMock()
|
||||||
|
|
||||||
|
proj.work_git = mock.MagicMock()
|
||||||
|
proj.work_git.GetHead.return_value = "5678abcd"
|
||||||
|
|
||||||
|
syncbuf = project.SyncBuffer(proj.config)
|
||||||
|
|
||||||
|
with mock.patch("project.GitCommand") as mock_git_cmd:
|
||||||
|
mock_cmd_instance = mock.MagicMock()
|
||||||
|
mock_cmd_instance.Wait.return_value = 0
|
||||||
|
mock_git_cmd.return_value = mock_cmd_instance
|
||||||
|
|
||||||
|
proj.Sync_LocalHalf(syncbuf)
|
||||||
|
syncbuf.Finish()
|
||||||
|
|
||||||
|
self.assertEqual(mock_git_cmd.call_count, 2)
|
||||||
|
mock_git_cmd.assert_any_call(
|
||||||
|
proj, ["reflog", "expire", "--expire=all", "--all"], bare=True
|
||||||
|
)
|
||||||
|
mock_git_cmd.assert_any_call(
|
||||||
|
proj,
|
||||||
|
["gc", "--prune=now"],
|
||||||
|
bare=True,
|
||||||
|
capture_stdout=True,
|
||||||
|
capture_stderr=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_sync_network_half_stateless_skips_if_stash(self):
|
||||||
|
"""Test stateless sync skips if stash exists."""
|
||||||
|
with utils_for_test.TempGitTree() as tempdir:
|
||||||
|
proj = self._get_project(tempdir)
|
||||||
|
proj.HasStash = mock.MagicMock(return_value=True)
|
||||||
|
|
||||||
|
res = proj.Sync_NetworkHalf()
|
||||||
|
|
||||||
|
self.assertTrue(res.success)
|
||||||
|
self.assertFalse(getattr(proj, "stateless_prune_needed", False))
|
||||||
|
|
||||||
|
def test_sync_network_half_stateless_skips_if_local_commits(self):
|
||||||
|
"""Test stateless sync skips if there are local-only commits."""
|
||||||
|
with utils_for_test.TempGitTree() as tempdir:
|
||||||
|
proj = self._get_project(tempdir)
|
||||||
|
proj.bare_git.rev_list.return_value = ["1"]
|
||||||
|
|
||||||
|
res = proj.Sync_NetworkHalf()
|
||||||
|
|
||||||
|
self.assertTrue(res.success)
|
||||||
|
self.assertFalse(getattr(proj, "stateless_prune_needed", False))
|
||||||
|
|
||||||
|
|
||||||
|
class SyncOptimizationTests(unittest.TestCase):
|
||||||
|
"""Tests for sync optimization logic involving shallow clones."""
|
||||||
|
|
||||||
|
def _get_project(self, tempdir, depth=None):
|
||||||
|
manifest = mock.MagicMock()
|
||||||
|
manifest.manifestProject.depth = depth
|
||||||
|
manifest.manifestProject.dissociate = False
|
||||||
|
manifest.manifestProject.clone_filter = None
|
||||||
|
manifest.is_multimanifest = False
|
||||||
|
manifest.manifestProject.config.GetBoolean.return_value = False
|
||||||
|
manifest.IsMirror = False
|
||||||
|
|
||||||
|
remote = mock.MagicMock()
|
||||||
|
remote.name = "origin"
|
||||||
|
remote.url = "http://"
|
||||||
|
|
||||||
|
proj = project.Project(
|
||||||
|
manifest=manifest,
|
||||||
|
name="test-project",
|
||||||
|
remote=remote,
|
||||||
|
gitdir=os.path.join(tempdir, "gitdir"),
|
||||||
|
objdir=os.path.join(tempdir, "objdir"),
|
||||||
|
worktree=tempdir,
|
||||||
|
relpath="test-project",
|
||||||
|
revisionExpr="0123456789abcdef0123456789abcdef01234567",
|
||||||
|
revisionId=None,
|
||||||
|
)
|
||||||
|
proj._CheckForImmutableRevision = mock.MagicMock(return_value=True)
|
||||||
|
proj.DeleteWorktree = mock.MagicMock()
|
||||||
|
proj._InitGitDir = mock.MagicMock()
|
||||||
|
proj._InitRemote = mock.MagicMock()
|
||||||
|
proj._InitMRef = mock.MagicMock()
|
||||||
|
return proj
|
||||||
|
|
||||||
|
def test_sync_network_half_shallow_missing_fetches(self):
|
||||||
|
"""Test Sync_NetworkHalf fetches if shallow file is missing."""
|
||||||
|
with utils_for_test.TempGitTree() as tempdir:
|
||||||
|
proj = self._get_project(tempdir, depth=1)
|
||||||
|
# Ensure gitdir does not exist to simulate new project
|
||||||
|
if os.path.exists(proj.gitdir):
|
||||||
|
shutil.rmtree(proj.gitdir)
|
||||||
|
shallow_path = os.path.join(proj.gitdir, "shallow")
|
||||||
|
if os.path.exists(shallow_path):
|
||||||
|
os.unlink(shallow_path)
|
||||||
|
|
||||||
|
proj._RemoteFetch = mock.MagicMock(return_value=True)
|
||||||
|
|
||||||
|
res = proj.Sync_NetworkHalf(optimized_fetch=True)
|
||||||
|
|
||||||
|
self.assertTrue(res.success)
|
||||||
|
proj._RemoteFetch.assert_called_once()
|
||||||
|
|
||||||
|
def test_sync_network_half_shallow_exists_skips(self):
|
||||||
|
"""Test Sync_NetworkHalf skips fetch if shallow file exists."""
|
||||||
|
with utils_for_test.TempGitTree() as tempdir:
|
||||||
|
proj = self._get_project(tempdir, depth=1)
|
||||||
|
os.makedirs(proj.gitdir, exist_ok=True)
|
||||||
|
os.makedirs(proj.objdir, exist_ok=True)
|
||||||
|
with open(os.path.join(proj.gitdir, "shallow"), "w") as f:
|
||||||
|
f.write("")
|
||||||
|
|
||||||
|
proj._RemoteFetch = mock.MagicMock()
|
||||||
|
|
||||||
|
res = proj.Sync_NetworkHalf(optimized_fetch=True)
|
||||||
|
|
||||||
|
self.assertTrue(res.success)
|
||||||
|
proj._RemoteFetch.assert_not_called()
|
||||||
|
|
||||||
|
def test_remote_fetch_shallow_missing_fetches(self):
|
||||||
|
"""Test _RemoteFetch fetches if shallow file is missing."""
|
||||||
|
with utils_for_test.TempGitTree() as tempdir:
|
||||||
|
proj = self._get_project(tempdir, depth=1)
|
||||||
|
shallow_path = os.path.join(proj.gitdir, "shallow")
|
||||||
|
if os.path.exists(shallow_path):
|
||||||
|
os.unlink(shallow_path)
|
||||||
|
|
||||||
|
with mock.patch("project.GitCommand") as mock_git_cmd:
|
||||||
|
mock_cmd_instance = mock.MagicMock()
|
||||||
|
mock_cmd_instance.Wait.return_value = 0
|
||||||
|
mock_git_cmd.return_value = mock_cmd_instance
|
||||||
|
|
||||||
|
res = proj._RemoteFetch(
|
||||||
|
current_branch_only=True,
|
||||||
|
depth=1,
|
||||||
|
use_superproject=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(res)
|
||||||
|
mock_git_cmd.assert_called()
|
||||||
|
|
||||||
|
def test_remote_fetch_shallow_exists_skips(self):
|
||||||
|
"""Test _RemoteFetch skips fetch if shallow file exists."""
|
||||||
|
with utils_for_test.TempGitTree() as tempdir:
|
||||||
|
proj = self._get_project(tempdir, depth=1)
|
||||||
|
os.makedirs(proj.gitdir, exist_ok=True)
|
||||||
|
os.makedirs(proj.objdir, exist_ok=True)
|
||||||
|
with open(os.path.join(proj.gitdir, "shallow"), "w") as f:
|
||||||
|
f.write("")
|
||||||
|
|
||||||
|
with mock.patch("project.GitCommand") as mock_git_cmd:
|
||||||
|
res = proj._RemoteFetch(
|
||||||
|
current_branch_only=True,
|
||||||
|
depth=1,
|
||||||
|
use_superproject=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(res)
|
||||||
|
mock_git_cmd.assert_not_called()
|
||||||
|
|||||||
+77
-71
@@ -12,90 +12,96 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
"""Unit test for repo_logging module."""
|
"""Unittests for the repo_logging.py module."""
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
import unittest
|
import re
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from color import SetDefaultColoring
|
from color import SetDefaultColoring
|
||||||
from error import RepoExitError
|
from error import RepoExitError
|
||||||
from repo_logging import RepoLogger
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
|
|
||||||
class TestRepoLogger(unittest.TestCase):
|
@mock.patch.object(RepoLogger, "error")
|
||||||
@mock.patch.object(RepoLogger, "error")
|
def test_log_aggregated_errors_logs_aggregated_errors(mock_error) -> None:
|
||||||
def test_log_aggregated_errors_logs_aggregated_errors(self, mock_error):
|
"""Test if log_aggregated_errors logs a list of aggregated errors."""
|
||||||
"""Test if log_aggregated_errors logs a list of aggregated errors."""
|
logger = RepoLogger(__name__)
|
||||||
logger = RepoLogger(__name__)
|
logger.log_aggregated_errors(
|
||||||
logger.log_aggregated_errors(
|
RepoExitError(
|
||||||
RepoExitError(
|
aggregate_errors=[
|
||||||
aggregate_errors=[
|
Exception("foo"),
|
||||||
Exception("foo"),
|
Exception("bar"),
|
||||||
Exception("bar"),
|
Exception("baz"),
|
||||||
Exception("baz"),
|
Exception("hello"),
|
||||||
Exception("hello"),
|
Exception("world"),
|
||||||
Exception("world"),
|
Exception("test"),
|
||||||
Exception("test"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
mock_error.assert_has_calls(
|
|
||||||
[
|
|
||||||
mock.call("=" * 80),
|
|
||||||
mock.call(
|
|
||||||
"Repo command failed due to the following `%s` errors:",
|
|
||||||
"RepoExitError",
|
|
||||||
),
|
|
||||||
mock.call("foo\nbar\nbaz\nhello\nworld"),
|
|
||||||
mock.call("+%d additional errors...", 1),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@mock.patch.object(RepoLogger, "error")
|
mock_error.assert_has_calls(
|
||||||
def test_log_aggregated_errors_logs_single_error(self, mock_error):
|
[
|
||||||
"""Test if log_aggregated_errors logs empty aggregated_errors."""
|
mock.call("=" * 80),
|
||||||
|
mock.call(
|
||||||
|
"Repo command failed due to the following `%s` errors:",
|
||||||
|
"RepoExitError",
|
||||||
|
),
|
||||||
|
mock.call("foo\nbar\nbaz\nhello\nworld"),
|
||||||
|
mock.call("+%d additional errors...", 1),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(RepoLogger, "error")
|
||||||
|
def test_log_aggregated_errors_logs_single_error(mock_error) -> None:
|
||||||
|
"""Test if log_aggregated_errors logs empty aggregated_errors."""
|
||||||
|
logger = RepoLogger(__name__)
|
||||||
|
logger.log_aggregated_errors(RepoExitError())
|
||||||
|
|
||||||
|
mock_error.assert_has_calls(
|
||||||
|
[
|
||||||
|
mock.call("=" * 80),
|
||||||
|
mock.call("Repo command failed: %s", "RepoExitError"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"level",
|
||||||
|
(
|
||||||
|
logging.INFO,
|
||||||
|
logging.WARN,
|
||||||
|
logging.ERROR,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_log_with_format_string(level: int) -> None:
|
||||||
|
"""Test different log levels with format strings."""
|
||||||
|
name = logging.getLevelName(level)
|
||||||
|
|
||||||
|
# Set color output to "always" for consistent test results.
|
||||||
|
# This ensures the logger's behavior is uniform across different
|
||||||
|
# environments and git configurations.
|
||||||
|
SetDefaultColoring("always")
|
||||||
|
|
||||||
|
# Regex pattern to match optional ANSI color codes.
|
||||||
|
# \033 - Escape character
|
||||||
|
# \[ - Opening square bracket
|
||||||
|
# [0-9;]* - Zero or more digits or semicolons
|
||||||
|
# m - Ending 'm' character
|
||||||
|
# ? - Makes the entire group optional
|
||||||
|
opt_color = r"(\033\[[0-9;]*m)?"
|
||||||
|
|
||||||
|
output = io.StringIO()
|
||||||
|
|
||||||
|
with contextlib.redirect_stderr(output):
|
||||||
logger = RepoLogger(__name__)
|
logger = RepoLogger(__name__)
|
||||||
logger.log_aggregated_errors(RepoExitError())
|
logger.log(level, "%s", "100% pass")
|
||||||
|
|
||||||
mock_error.assert_has_calls(
|
assert re.search(
|
||||||
[
|
f"^{opt_color}100% pass{opt_color}$", output.getvalue().strip()
|
||||||
mock.call("=" * 80),
|
), f"failed for level {name}"
|
||||||
mock.call("Repo command failed: %s", "RepoExitError"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_log_with_format_string(self):
|
|
||||||
"""Test different log levels with format strings."""
|
|
||||||
|
|
||||||
# Set color output to "always" for consistent test results.
|
|
||||||
# This ensures the logger's behavior is uniform across different
|
|
||||||
# environments and git configurations.
|
|
||||||
SetDefaultColoring("always")
|
|
||||||
|
|
||||||
# Regex pattern to match optional ANSI color codes.
|
|
||||||
# \033 - Escape character
|
|
||||||
# \[ - Opening square bracket
|
|
||||||
# [0-9;]* - Zero or more digits or semicolons
|
|
||||||
# m - Ending 'm' character
|
|
||||||
# ? - Makes the entire group optional
|
|
||||||
opt_color = r"(\033\[[0-9;]*m)?"
|
|
||||||
|
|
||||||
for level in (logging.INFO, logging.WARN, logging.ERROR):
|
|
||||||
name = logging.getLevelName(level)
|
|
||||||
|
|
||||||
with self.subTest(level=level, name=name):
|
|
||||||
output = io.StringIO()
|
|
||||||
|
|
||||||
with contextlib.redirect_stderr(output):
|
|
||||||
logger = RepoLogger(__name__)
|
|
||||||
logger.log(level, "%s", "100% pass")
|
|
||||||
|
|
||||||
self.assertRegex(
|
|
||||||
output.getvalue().strip(),
|
|
||||||
f"^{opt_color}100% pass{opt_color}$",
|
|
||||||
f"failed for level {name}",
|
|
||||||
)
|
|
||||||
|
|||||||
+25
-34
@@ -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.
|
||||||
@@ -15,46 +15,37 @@
|
|||||||
"""Unittests for the repo_trace.py module."""
|
"""Unittests for the repo_trace.py module."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import unittest
|
|
||||||
from unittest import mock
|
import pytest
|
||||||
|
|
||||||
import repo_trace
|
import repo_trace
|
||||||
|
|
||||||
|
|
||||||
class TraceTests(unittest.TestCase):
|
def test_trace_max_size_enforced(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
"""Check Trace behavior."""
|
"""Check Trace behavior."""
|
||||||
|
content = "git chicken"
|
||||||
|
|
||||||
def testTrace_MaxSizeEnforced(self):
|
with repo_trace.Trace(content, first_trace=True):
|
||||||
content = "git chicken"
|
pass
|
||||||
|
first_trace_size = os.path.getsize(repo_trace._TRACE_FILE)
|
||||||
|
|
||||||
with repo_trace.Trace(content, first_trace=True):
|
with repo_trace.Trace(content):
|
||||||
pass
|
pass
|
||||||
first_trace_size = os.path.getsize(repo_trace._TRACE_FILE)
|
assert os.path.getsize(repo_trace._TRACE_FILE) > first_trace_size
|
||||||
|
|
||||||
with repo_trace.Trace(content):
|
# Check we clear everything if the last chunk is larger than _MAX_SIZE.
|
||||||
pass
|
monkeypatch.setattr(repo_trace, "_MAX_SIZE", 0)
|
||||||
self.assertGreater(
|
with repo_trace.Trace(content, first_trace=True):
|
||||||
os.path.getsize(repo_trace._TRACE_FILE), first_trace_size
|
pass
|
||||||
)
|
assert os.path.getsize(repo_trace._TRACE_FILE) == first_trace_size
|
||||||
|
|
||||||
# Check we clear everything is the last chunk is larger than _MAX_SIZE.
|
# Check we only clear the chunks we need to.
|
||||||
with mock.patch("repo_trace._MAX_SIZE", 0):
|
new_max = (first_trace_size + 1) / (1024 * 1024)
|
||||||
with repo_trace.Trace(content, first_trace=True):
|
monkeypatch.setattr(repo_trace, "_MAX_SIZE", new_max)
|
||||||
pass
|
with repo_trace.Trace(content, first_trace=True):
|
||||||
self.assertEqual(
|
pass
|
||||||
first_trace_size, os.path.getsize(repo_trace._TRACE_FILE)
|
assert os.path.getsize(repo_trace._TRACE_FILE) == first_trace_size * 2
|
||||||
)
|
|
||||||
|
|
||||||
# Check we only clear the chunks we need to.
|
with repo_trace.Trace(content, first_trace=True):
|
||||||
repo_trace._MAX_SIZE = (first_trace_size + 1) / (1024 * 1024)
|
pass
|
||||||
with repo_trace.Trace(content, first_trace=True):
|
assert os.path.getsize(repo_trace._TRACE_FILE) == first_trace_size * 2
|
||||||
pass
|
|
||||||
self.assertEqual(
|
|
||||||
first_trace_size * 2, os.path.getsize(repo_trace._TRACE_FILE)
|
|
||||||
)
|
|
||||||
|
|
||||||
with repo_trace.Trace(content, first_trace=True):
|
|
||||||
pass
|
|
||||||
self.assertEqual(
|
|
||||||
first_trace_size * 2, os.path.getsize(repo_trace._TRACE_FILE)
|
|
||||||
)
|
|
||||||
|
|||||||
+65
-46
@@ -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.
|
||||||
@@ -16,65 +16,84 @@
|
|||||||
|
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import subprocess
|
import subprocess
|
||||||
import unittest
|
from typing import Tuple
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
import ssh
|
import ssh
|
||||||
|
|
||||||
|
|
||||||
class SshTests(unittest.TestCase):
|
@pytest.fixture(autouse=True)
|
||||||
"""Tests the ssh functions."""
|
def clear_ssh_version_cache() -> None:
|
||||||
|
"""Clear the ssh version cache before each test."""
|
||||||
|
ssh.version.cache_clear()
|
||||||
|
|
||||||
def test_parse_ssh_version(self):
|
|
||||||
"""Check _parse_ssh_version() handling."""
|
|
||||||
ver = ssh._parse_ssh_version("Unknown\n")
|
|
||||||
self.assertEqual(ver, ())
|
|
||||||
ver = ssh._parse_ssh_version("OpenSSH_1.0\n")
|
|
||||||
self.assertEqual(ver, (1, 0))
|
|
||||||
ver = ssh._parse_ssh_version(
|
|
||||||
"OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.13, OpenSSL 1.0.1f 6 Jan 2014\n"
|
|
||||||
)
|
|
||||||
self.assertEqual(ver, (6, 6, 1))
|
|
||||||
ver = ssh._parse_ssh_version(
|
|
||||||
"OpenSSH_7.6p1 Ubuntu-4ubuntu0.3, OpenSSL 1.0.2n 7 Dec 2017\n"
|
|
||||||
)
|
|
||||||
self.assertEqual(ver, (7, 6))
|
|
||||||
ver = ssh._parse_ssh_version("OpenSSH_9.0p1, LibreSSL 3.3.6\n")
|
|
||||||
self.assertEqual(ver, (9, 0))
|
|
||||||
|
|
||||||
def test_version(self):
|
@pytest.mark.parametrize(
|
||||||
"""Check version() handling."""
|
"input_str, expected",
|
||||||
with mock.patch("ssh._run_ssh_version", return_value="OpenSSH_1.2\n"):
|
(
|
||||||
self.assertEqual(ssh.version(), (1, 2))
|
("Unknown\n", ()),
|
||||||
|
("OpenSSH_1.0\n", (1, 0)),
|
||||||
|
(
|
||||||
|
"OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.13, OpenSSL 1.0.1f 6 Jan 2014\n",
|
||||||
|
(6, 6, 1),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"OpenSSH_7.6p1 Ubuntu-4ubuntu0.3, OpenSSL 1.0.2n 7 Dec 2017\n",
|
||||||
|
(7, 6),
|
||||||
|
),
|
||||||
|
("OpenSSH_9.0p1, LibreSSL 3.3.6\n", (9, 0)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_parse_ssh_version(input_str: str, expected: Tuple[int, ...]) -> None:
|
||||||
|
"""Check _parse_ssh_version() handling."""
|
||||||
|
assert ssh._parse_ssh_version(input_str) == expected
|
||||||
|
|
||||||
def test_context_manager_empty(self):
|
|
||||||
"""Verify context manager with no clients works correctly."""
|
|
||||||
with multiprocessing.Manager() as manager:
|
|
||||||
with ssh.ProxyManager(manager):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_context_manager_child_cleanup(self):
|
def test_version() -> None:
|
||||||
"""Verify orphaned clients & masters get cleaned up."""
|
"""Check version() handling."""
|
||||||
with multiprocessing.Manager() as manager:
|
with mock.patch("ssh._run_ssh_version", return_value="OpenSSH_1.2\n"):
|
||||||
|
assert ssh.version() == (1, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_context_manager_empty() -> None:
|
||||||
|
"""Verify context manager with no clients works correctly."""
|
||||||
|
with multiprocessing.Manager() as manager:
|
||||||
|
with ssh.ProxyManager(manager):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_context_manager_child_cleanup() -> None:
|
||||||
|
"""Verify orphaned clients & masters get cleaned up."""
|
||||||
|
with multiprocessing.Manager() as manager:
|
||||||
|
with mock.patch("ssh.version", return_value=(1, 2)):
|
||||||
with ssh.ProxyManager(manager) as ssh_proxy:
|
with ssh.ProxyManager(manager) as ssh_proxy:
|
||||||
client = subprocess.Popen(["sleep", "964853320"])
|
client = subprocess.Popen(["sleep", "964853320"])
|
||||||
ssh_proxy.add_client(client)
|
ssh_proxy.add_client(client)
|
||||||
master = subprocess.Popen(["sleep", "964853321"])
|
master = subprocess.Popen(["sleep", "964853321"])
|
||||||
ssh_proxy.add_master(master)
|
ssh_proxy.add_master(master)
|
||||||
# If the process still exists, these will throw timeout errors.
|
# If the process still exists, these will throw timeout errors.
|
||||||
client.wait(0)
|
client.wait(0)
|
||||||
master.wait(0)
|
master.wait(0)
|
||||||
|
|
||||||
def test_ssh_sock(self):
|
|
||||||
"""Check sock() function."""
|
def test_ssh_sock(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
manager = multiprocessing.Manager()
|
"""Check sock() function."""
|
||||||
|
with multiprocessing.Manager() as manager:
|
||||||
proxy = ssh.ProxyManager(manager)
|
proxy = ssh.ProxyManager(manager)
|
||||||
with mock.patch("tempfile.mkdtemp", return_value="/tmp/foo"):
|
monkeypatch.setattr(
|
||||||
# Old ssh version uses port.
|
"tempfile.mkdtemp", lambda *args, **kwargs: "/tmp/foo"
|
||||||
with mock.patch("ssh.version", return_value=(6, 6)):
|
)
|
||||||
self.assertTrue(proxy.sock().endswith("%p"))
|
|
||||||
|
|
||||||
proxy._sock_path = None
|
# Old ssh version uses port.
|
||||||
# New ssh version uses hash.
|
with mock.patch("ssh.version", return_value=(6, 6)):
|
||||||
with mock.patch("ssh.version", return_value=(6, 7)):
|
with proxy as ssh_proxy:
|
||||||
self.assertTrue(proxy.sock().endswith("%C"))
|
assert ssh_proxy.sock().endswith("%p")
|
||||||
|
|
||||||
|
proxy._sock_path = None
|
||||||
|
# New ssh version uses hash.
|
||||||
|
with mock.patch("ssh.version", return_value=(6, 7)):
|
||||||
|
with proxy as ssh_proxy:
|
||||||
|
assert ssh_proxy.sock().endswith("%C")
|
||||||
|
proxy._sock_path = None
|
||||||
|
|||||||
+144
-57
@@ -15,77 +15,164 @@
|
|||||||
"""Unittests for the subcmds module (mostly __init__.py than subcommands)."""
|
"""Unittests for the subcmds module (mostly __init__.py than subcommands)."""
|
||||||
|
|
||||||
import optparse
|
import optparse
|
||||||
import unittest
|
from typing import Type
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from command import Command
|
||||||
import subcmds
|
import subcmds
|
||||||
|
|
||||||
|
|
||||||
class AllCommands(unittest.TestCase):
|
# NB: We don't test all subcommands as we want to avoid "change detection"
|
||||||
"""Check registered all_commands."""
|
# tests, so we just look for the most common/important ones here that are
|
||||||
|
# unlikely to ever change.
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cmd", ("cherry-pick", "help", "init", "start", "sync", "upload")
|
||||||
|
)
|
||||||
|
def test_required_basic(cmd: str) -> None:
|
||||||
|
"""Basic checking of registered commands."""
|
||||||
|
assert cmd in subcmds.all_commands
|
||||||
|
|
||||||
def test_required_basic(self):
|
|
||||||
"""Basic checking of registered commands."""
|
|
||||||
# NB: We don't test all subcommands as we want to avoid "change
|
|
||||||
# detection" tests, so we just look for the most common/important ones
|
|
||||||
# here that are unlikely to ever change.
|
|
||||||
for cmd in {"cherry-pick", "help", "init", "start", "sync", "upload"}:
|
|
||||||
self.assertIn(cmd, subcmds.all_commands)
|
|
||||||
|
|
||||||
def test_naming(self):
|
@pytest.mark.parametrize("name", subcmds.all_commands.keys())
|
||||||
"""Verify we don't add things that we shouldn't."""
|
def test_naming(name: str) -> None:
|
||||||
for cmd in subcmds.all_commands:
|
"""Verify we don't add things that we shouldn't."""
|
||||||
# Reject filename suffixes like "help.py".
|
# Reject filename suffixes like "help.py".
|
||||||
self.assertNotIn(".", cmd)
|
assert "." not in name
|
||||||
|
|
||||||
# Make sure all '_' were converted to '-'.
|
# Make sure all '_' were converted to '-'.
|
||||||
self.assertNotIn("_", cmd)
|
assert "_" not in name
|
||||||
|
|
||||||
# Reject internal python paths like "__init__".
|
# Reject internal python paths like "__init__".
|
||||||
self.assertFalse(cmd.startswith("__"))
|
assert not name.startswith("__")
|
||||||
|
|
||||||
def test_help_desc_style(self):
|
|
||||||
"""Force some consistency in option descriptions.
|
|
||||||
|
|
||||||
Python's optparse & argparse has a few default options like --help.
|
@pytest.mark.parametrize("name, cls", subcmds.all_commands.items())
|
||||||
Their option description text uses lowercase sentence fragments, so
|
def test_help_desc_style(name: str, cls: Type[Command]) -> None:
|
||||||
enforce our options follow the same style so UI is consistent.
|
"""Force some consistency in option descriptions.
|
||||||
|
|
||||||
We enforce:
|
Python's optparse & argparse has a few default options like --help.
|
||||||
* Text starts with lowercase.
|
Their option description text uses lowercase sentence fragments, so
|
||||||
* Text doesn't end with period.
|
enforce our options follow the same style so UI is consistent.
|
||||||
"""
|
|
||||||
|
We enforce:
|
||||||
|
* Text starts with lowercase.
|
||||||
|
* Text doesn't end with period.
|
||||||
|
"""
|
||||||
|
cmd = cls()
|
||||||
|
parser = cmd.OptionParser
|
||||||
|
for option in parser.option_list:
|
||||||
|
if option.help == optparse.SUPPRESS_HELP or not option.help:
|
||||||
|
continue
|
||||||
|
|
||||||
|
c = option.help[0]
|
||||||
|
assert c.lower() == c, (
|
||||||
|
f"subcmds/{name}.py: {option.get_opt_string()}: "
|
||||||
|
f'help text should start with lowercase: "{option.help}"'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert option.help[-1] != ".", (
|
||||||
|
f"subcmds/{name}.py: {option.get_opt_string()}: "
|
||||||
|
f'help text should not end in a period: "{option.help}"'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name, cls", subcmds.all_commands.items())
|
||||||
|
def test_cli_option_style(name: str, cls: Type[Command]) -> None:
|
||||||
|
"""Force some consistency in option flags."""
|
||||||
|
cmd = cls()
|
||||||
|
parser = cmd.OptionParser
|
||||||
|
for option in parser.option_list:
|
||||||
|
for opt in option._long_opts:
|
||||||
|
assert "_" not in opt, (
|
||||||
|
f"subcmds/{name}.py: {opt}: only use dashes in "
|
||||||
|
"options, not underscores"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_option_dest() -> None:
|
||||||
|
"""Block redundant dest= arguments."""
|
||||||
|
bad_opts: list[tuple[str, str]] = []
|
||||||
|
|
||||||
|
def _check_dest(opt: optparse.Option) -> None:
|
||||||
|
"""Check the dest= setting."""
|
||||||
|
# If the destination is not set, nothing to check.
|
||||||
|
# If long options are not set, then there's no implicit destination.
|
||||||
|
# If callback is used, then a destination might be needed because
|
||||||
|
# optparse cannot assume a value is always stored.
|
||||||
|
if opt.dest is None or not opt._long_opts or opt.callback:
|
||||||
|
return
|
||||||
|
|
||||||
|
long = opt._long_opts[0]
|
||||||
|
assert long.startswith("--")
|
||||||
|
# This matches optparse's behavior.
|
||||||
|
implicit_dest = long[2:].replace("-", "_")
|
||||||
|
if implicit_dest == opt.dest:
|
||||||
|
bad_opts.append((str(opt), opt.dest))
|
||||||
|
|
||||||
|
# Hook the option check list.
|
||||||
|
optparse.Option.CHECK_METHODS.insert(0, _check_dest)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Gather all the bad options up front so people can see all bad options
|
||||||
|
# instead of failing at the first one.
|
||||||
|
all_bad_opts: dict[str, list[tuple[str, str]]] = {}
|
||||||
for name, cls in subcmds.all_commands.items():
|
for name, cls in subcmds.all_commands.items():
|
||||||
|
bad_opts = []
|
||||||
cmd = cls()
|
cmd = cls()
|
||||||
parser = cmd.OptionParser
|
# Trigger construction of parser.
|
||||||
for option in parser.option_list:
|
_ = cmd.OptionParser
|
||||||
if option.help == optparse.SUPPRESS_HELP:
|
all_bad_opts[name] = bad_opts
|
||||||
continue
|
|
||||||
|
|
||||||
c = option.help[0]
|
errmsg = ""
|
||||||
self.assertEqual(
|
for name, bad_opts_list in sorted(all_bad_opts.items()):
|
||||||
c.lower(),
|
if bad_opts_list:
|
||||||
c,
|
if not errmsg:
|
||||||
msg=f"subcmds/{name}.py: {option.get_opt_string()}: "
|
errmsg = "Omit redundant dest= when defining options.\n"
|
||||||
f'help text should start with lowercase: "{option.help}"',
|
errmsg += f"\nSubcommand {name} (subcmds/{name}.py):\n"
|
||||||
|
errmsg += "".join(
|
||||||
|
f" {opt}: dest='{dest}'\n" for opt, dest in bad_opts_list
|
||||||
)
|
)
|
||||||
|
if errmsg:
|
||||||
|
pytest.fail(errmsg)
|
||||||
|
finally:
|
||||||
|
# Make sure we aren't popping the wrong stuff.
|
||||||
|
assert optparse.Option.CHECK_METHODS.pop(0) is _check_dest
|
||||||
|
|
||||||
self.assertNotEqual(
|
|
||||||
option.help[-1],
|
|
||||||
".",
|
|
||||||
msg=f"subcmds/{name}.py: {option.get_opt_string()}: "
|
|
||||||
f'help text should not end in a period: "{option.help}"',
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_cli_option_style(self):
|
@pytest.mark.parametrize("name, cls", subcmds.all_commands.items())
|
||||||
"""Force some consistency in option flags."""
|
def test_common_validate_options(name: str, cls: Type[Command]) -> None:
|
||||||
for name, cls in subcmds.all_commands.items():
|
"""Verify CommonValidateOptions sets up expected fields."""
|
||||||
cmd = cls()
|
cmd = cls()
|
||||||
parser = cmd.OptionParser
|
opts, args = cmd.OptionParser.parse_args([])
|
||||||
for option in parser.option_list:
|
|
||||||
for opt in option._long_opts:
|
# Verify the fields don't exist yet.
|
||||||
self.assertNotIn(
|
assert not hasattr(
|
||||||
"_",
|
opts, "verbose"
|
||||||
opt,
|
), f"{name}: has verbose before validation"
|
||||||
msg=f"subcmds/{name}.py: {opt}: only use dashes in "
|
assert not hasattr(opts, "quiet"), f"{name}: has quiet before validation"
|
||||||
"options, not underscores",
|
|
||||||
)
|
cmd.CommonValidateOptions(opts, args)
|
||||||
|
|
||||||
|
# Verify the fields exist now.
|
||||||
|
assert hasattr(opts, "verbose"), f"{name}: missing verbose after validation"
|
||||||
|
assert hasattr(opts, "quiet"), f"{name}: missing quiet after validation"
|
||||||
|
assert hasattr(
|
||||||
|
opts, "outer_manifest"
|
||||||
|
), f"{name}: missing outer_manifest after validation"
|
||||||
|
|
||||||
|
|
||||||
|
def test_attribute_error_repro() -> None:
|
||||||
|
"""Confirm that accessing verbose before CommonValidateOptions fails."""
|
||||||
|
from subcmds.sync import Sync
|
||||||
|
|
||||||
|
cmd = Sync()
|
||||||
|
opts, args = cmd.OptionParser.parse_args([])
|
||||||
|
|
||||||
|
# This confirms that without the fix in main.py, an AttributeError
|
||||||
|
# would be raised because CommonValidateOptions hasn't been called yet.
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
_ = opts.verbose
|
||||||
|
|
||||||
|
cmd.CommonValidateOptions(opts, args)
|
||||||
|
assert hasattr(opts, "verbose")
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# 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 forall subcmd."""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import io
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import utils_for_test
|
||||||
|
|
||||||
|
import manifest_xml
|
||||||
|
import project
|
||||||
|
import subcmds
|
||||||
|
|
||||||
|
|
||||||
|
def _create_manifest_with_8_projects(
|
||||||
|
topdir: Path,
|
||||||
|
) -> manifest_xml.XmlManifest:
|
||||||
|
"""Create a setup of 8 projects to execute forall."""
|
||||||
|
repodir = topdir / ".repo"
|
||||||
|
manifest_dir = repodir / "manifests"
|
||||||
|
manifest_file = repodir / manifest_xml.MANIFEST_FILE_NAME
|
||||||
|
|
||||||
|
repodir.mkdir()
|
||||||
|
manifest_dir.mkdir()
|
||||||
|
|
||||||
|
# Set up a manifest git dir for parsing to work.
|
||||||
|
gitdir = repodir / "manifests.git"
|
||||||
|
gitdir.mkdir()
|
||||||
|
(gitdir / "config").write_text(
|
||||||
|
"""[remote "origin"]
|
||||||
|
url = https://localhost:0/manifest
|
||||||
|
verbose = false
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add the manifest data.
|
||||||
|
manifest_file.write_text(
|
||||||
|
"""
|
||||||
|
<manifest>
|
||||||
|
<remote name="origin" fetch="http://localhost" />
|
||||||
|
<default remote="origin" revision="refs/heads/main" />
|
||||||
|
<project name="project1" path="tests/path1" />
|
||||||
|
<project name="project2" path="tests/path2" />
|
||||||
|
<project name="project3" path="tests/path3" />
|
||||||
|
<project name="project4" path="tests/path4" />
|
||||||
|
<project name="project5" path="tests/path5" />
|
||||||
|
<project name="project6" path="tests/path6" />
|
||||||
|
<project name="project7" path="tests/path7" />
|
||||||
|
<project name="project8" path="tests/path8" />
|
||||||
|
</manifest>
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set up 8 empty projects to match the manifest.
|
||||||
|
for x in range(1, 9):
|
||||||
|
(repodir / "projects" / "tests" / f"path{x}.git").mkdir(parents=True)
|
||||||
|
(repodir / "project-objects" / f"project{x}.git").mkdir(parents=True)
|
||||||
|
git_path = topdir / "tests" / f"path{x}"
|
||||||
|
utils_for_test.init_git_tree(git_path)
|
||||||
|
|
||||||
|
return manifest_xml.XmlManifest(str(repodir), str(manifest_file))
|
||||||
|
|
||||||
|
|
||||||
|
def test_forall_all_projects_called_once(tmp_path: Path) -> None:
|
||||||
|
"""Test that all projects get a command run once each."""
|
||||||
|
manifest = _create_manifest_with_8_projects(tmp_path)
|
||||||
|
|
||||||
|
cmd = subcmds.forall.Forall()
|
||||||
|
cmd.manifest = manifest
|
||||||
|
|
||||||
|
# Use echo project names as the test of forall.
|
||||||
|
opts, args = cmd.OptionParser.parse_args(["-c", "echo $REPO_PROJECT"])
|
||||||
|
opts.verbose = False
|
||||||
|
|
||||||
|
with contextlib.redirect_stdout(io.StringIO()) as stdout:
|
||||||
|
# Mock to not have the Execute fail on remote check.
|
||||||
|
with mock.patch.object(
|
||||||
|
project.Project, "GetRevisionId", return_value="refs/heads/main"
|
||||||
|
):
|
||||||
|
# Run the forall command.
|
||||||
|
cmd.Execute(opts, args)
|
||||||
|
|
||||||
|
output = stdout.getvalue()
|
||||||
|
# Verify that we got every project name in the output.
|
||||||
|
for x in range(1, 9):
|
||||||
|
assert f"project{x}" in output
|
||||||
|
|
||||||
|
# Split the captured output into lines to count them.
|
||||||
|
line_count = sum(1 for x in output.splitlines() if x)
|
||||||
|
# Verify that we didn't get more lines than expected.
|
||||||
|
assert line_count == 8
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Copyright (C) 2026 The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Unittests for the subcmds/gc.py module."""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from subcmds import gc
|
||||||
|
|
||||||
|
|
||||||
|
class GcCommand(unittest.TestCase):
|
||||||
|
"""Tests for gc command."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.cmd = gc.Gc()
|
||||||
|
self.opt, self.args = self.cmd.OptionParser.parse_args([])
|
||||||
|
self.opt.this_manifest_only = False
|
||||||
|
self.opt.repack = False
|
||||||
|
|
||||||
|
self.mock_get_projects = mock.patch.object(
|
||||||
|
self.cmd, "GetProjects"
|
||||||
|
).start()
|
||||||
|
|
||||||
|
self.mock_delete = mock.patch.object(
|
||||||
|
self.cmd, "delete_unused_projects", return_value=0
|
||||||
|
).start()
|
||||||
|
|
||||||
|
self.mock_repack = mock.patch.object(
|
||||||
|
self.cmd, "repack_projects", return_value=0
|
||||||
|
).start()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
mock.patch.stopall()
|
||||||
|
|
||||||
|
def test_gc_no_args(self):
|
||||||
|
"""Test gc without specific projects."""
|
||||||
|
self.mock_get_projects.return_value = ["all_projects"]
|
||||||
|
|
||||||
|
self.cmd.Execute(self.opt, [])
|
||||||
|
|
||||||
|
self.mock_get_projects.assert_called_once_with([], all_manifests=True)
|
||||||
|
self.mock_delete.assert_called_once_with(["all_projects"], self.opt)
|
||||||
|
self.mock_repack.assert_not_called()
|
||||||
|
|
||||||
|
def test_gc_with_args(self):
|
||||||
|
"""Test gc with specific projects uses all_projects for delete."""
|
||||||
|
self.mock_get_projects.side_effect = [["projA"], ["all_projects"]]
|
||||||
|
self.opt.repack = True
|
||||||
|
|
||||||
|
self.cmd.Execute(self.opt, ["projA"])
|
||||||
|
|
||||||
|
self.mock_get_projects.assert_has_calls(
|
||||||
|
[
|
||||||
|
mock.call(["projA"], all_manifests=True),
|
||||||
|
mock.call([], all_manifests=True),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.mock_delete.assert_called_once_with(["all_projects"], self.opt)
|
||||||
|
self.mock_repack.assert_called_once_with(["projA"], self.opt)
|
||||||
|
|
||||||
|
def test_gc_exit_on_delete_failure(self):
|
||||||
|
"""Test gc exits if delete_unused_projects fails."""
|
||||||
|
self.mock_get_projects.return_value = ["all_projects"]
|
||||||
|
self.mock_delete.return_value = 1
|
||||||
|
self.opt.repack = True
|
||||||
|
|
||||||
|
ret = self.cmd.Execute(self.opt, [])
|
||||||
|
self.assertEqual(ret, 1)
|
||||||
|
self.mock_repack.assert_not_called()
|
||||||
+27
-24
@@ -14,33 +14,36 @@
|
|||||||
|
|
||||||
"""Unittests for the subcmds/init.py module."""
|
"""Unittests for the subcmds/init.py module."""
|
||||||
|
|
||||||
import unittest
|
from typing import List
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from subcmds import init
|
from subcmds import init
|
||||||
|
|
||||||
|
|
||||||
class InitCommand(unittest.TestCase):
|
@pytest.mark.parametrize(
|
||||||
"""Check registered all_commands."""
|
"argv",
|
||||||
|
([],),
|
||||||
|
)
|
||||||
|
def test_cli_parser_good(argv: List[str]) -> None:
|
||||||
|
"""Check valid command line options."""
|
||||||
|
cmd = init.Init()
|
||||||
|
opts, args = cmd.OptionParser.parse_args(argv)
|
||||||
|
cmd.ValidateOptions(opts, args)
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.cmd = init.Init()
|
|
||||||
|
|
||||||
def test_cli_parser_good(self):
|
@pytest.mark.parametrize(
|
||||||
"""Check valid command line options."""
|
"argv",
|
||||||
ARGV = ([],)
|
(
|
||||||
for argv in ARGV:
|
# Too many arguments.
|
||||||
opts, args = self.cmd.OptionParser.parse_args(argv)
|
["url", "asdf"],
|
||||||
self.cmd.ValidateOptions(opts, args)
|
# Conflicting options.
|
||||||
|
["--mirror", "--archive"],
|
||||||
def test_cli_parser_bad(self):
|
),
|
||||||
"""Check invalid command line options."""
|
)
|
||||||
ARGV = (
|
def test_cli_parser_bad(argv: List[str]) -> None:
|
||||||
# Too many arguments.
|
"""Check invalid command line options."""
|
||||||
["url", "asdf"],
|
cmd = init.Init()
|
||||||
# Conflicting options.
|
opts, args = cmd.OptionParser.parse_args(argv)
|
||||||
["--mirror", "--archive"],
|
with pytest.raises(SystemExit):
|
||||||
)
|
cmd.ValidateOptions(opts, args)
|
||||||
for argv in ARGV:
|
|
||||||
opts, args = self.cmd.OptionParser.parse_args(argv)
|
|
||||||
with self.assertRaises(SystemExit):
|
|
||||||
self.cmd.ValidateOptions(opts, args)
|
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Unittests for the subcmds/manifest.py module."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import manifest_xml
|
||||||
|
from subcmds import manifest
|
||||||
|
|
||||||
|
|
||||||
|
_EXAMPLE_MANIFEST = """\
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<manifest>
|
||||||
|
<remote name="test-remote" fetch="http://localhost" />
|
||||||
|
<default remote="test-remote" revision="refs/heads/main" />
|
||||||
|
<project name="repohooks" path="src/repohooks"/>
|
||||||
|
<repo-hooks in-project="repohooks" enabled-list="a, b"/>
|
||||||
|
</manifest>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _get_cmd(repodir: Path) -> manifest.Manifest:
|
||||||
|
"""Instantiate a manifest command object to test."""
|
||||||
|
manifests_git = repodir / "manifests.git"
|
||||||
|
manifests_git.mkdir()
|
||||||
|
(manifests_git / "config").write_text(
|
||||||
|
"""
|
||||||
|
[remote "origin"]
|
||||||
|
\turl = http://localhost/manifest
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
client = manifest_xml.RepoClient(repodir=str(repodir))
|
||||||
|
git_event_log = mock.MagicMock(ErrorEvent=mock.Mock(return_value=None))
|
||||||
|
return manifest.Manifest(
|
||||||
|
repodir=client.repodir,
|
||||||
|
client=client,
|
||||||
|
manifest=client.manifest,
|
||||||
|
outer_client=client,
|
||||||
|
outer_manifest=client.manifest,
|
||||||
|
git_event_log=git_event_log,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_output_format_xml_file(tmp_path):
|
||||||
|
"""Test writing XML to a file."""
|
||||||
|
path = tmp_path / "manifest.xml"
|
||||||
|
path.write_text(_EXAMPLE_MANIFEST)
|
||||||
|
outpath = tmp_path / "output.xml"
|
||||||
|
cmd = _get_cmd(tmp_path)
|
||||||
|
opt, args = cmd.OptionParser.parse_args(["--output-file", str(outpath)])
|
||||||
|
cmd.Execute(opt, args)
|
||||||
|
# Normalize the output a bit as we don't exactly care.
|
||||||
|
normalize = lambda data: "\n".join(
|
||||||
|
x.strip() for x in data.splitlines() if x.strip()
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
normalize(outpath.read_text())
|
||||||
|
== """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<manifest>
|
||||||
|
<remote name="test-remote" fetch="http://localhost"/>
|
||||||
|
<default remote="test-remote" revision="refs/heads/main"/>
|
||||||
|
<project name="repohooks" path="src/repohooks"/>
|
||||||
|
<repo-hooks in-project="repohooks" enabled-list="a b"/>
|
||||||
|
</manifest>"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_output_format_xml_stdout(tmp_path, capsys):
|
||||||
|
"""Test writing XML to stdout."""
|
||||||
|
path = tmp_path / "manifest.xml"
|
||||||
|
path.write_text(_EXAMPLE_MANIFEST)
|
||||||
|
cmd = _get_cmd(tmp_path)
|
||||||
|
opt, args = cmd.OptionParser.parse_args(["--format", "xml"])
|
||||||
|
cmd.Execute(opt, args)
|
||||||
|
# Normalize the output a bit as we don't exactly care.
|
||||||
|
normalize = lambda data: "\n".join(
|
||||||
|
x.strip() for x in data.splitlines() if x.strip()
|
||||||
|
)
|
||||||
|
stdout = capsys.readouterr().out
|
||||||
|
assert (
|
||||||
|
normalize(stdout)
|
||||||
|
== """<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<manifest>
|
||||||
|
<remote name="test-remote" fetch="http://localhost"/>
|
||||||
|
<default remote="test-remote" revision="refs/heads/main"/>
|
||||||
|
<project name="repohooks" path="src/repohooks"/>
|
||||||
|
<repo-hooks in-project="repohooks" enabled-list="a b"/>
|
||||||
|
</manifest>"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_output_format_json(tmp_path, capsys):
|
||||||
|
"""Test writing JSON."""
|
||||||
|
path = tmp_path / "manifest.xml"
|
||||||
|
path.write_text(_EXAMPLE_MANIFEST)
|
||||||
|
cmd = _get_cmd(tmp_path)
|
||||||
|
opt, args = cmd.OptionParser.parse_args(["--format", "json"])
|
||||||
|
cmd.Execute(opt, args)
|
||||||
|
obj = json.loads(capsys.readouterr().out)
|
||||||
|
assert obj == {
|
||||||
|
"default": {"remote": "test-remote", "revision": "refs/heads/main"},
|
||||||
|
"project": [{"name": "repohooks", "path": "src/repohooks"}],
|
||||||
|
"remote": [{"fetch": "http://localhost", "name": "test-remote"}],
|
||||||
|
"repo-hooks": {"enabled-list": "a b", "in-project": "repohooks"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_output_format_json_pretty(tmp_path, capsys):
|
||||||
|
"""Test writing pretty JSON."""
|
||||||
|
path = tmp_path / "manifest.xml"
|
||||||
|
path.write_text(_EXAMPLE_MANIFEST)
|
||||||
|
cmd = _get_cmd(tmp_path)
|
||||||
|
opt, args = cmd.OptionParser.parse_args(["--format", "json", "--pretty"])
|
||||||
|
cmd.Execute(opt, args)
|
||||||
|
stdout = capsys.readouterr().out
|
||||||
|
assert (
|
||||||
|
stdout
|
||||||
|
== """\
|
||||||
|
{
|
||||||
|
"default": {
|
||||||
|
"remote": "test-remote",
|
||||||
|
"revision": "refs/heads/main"
|
||||||
|
},
|
||||||
|
"project": [
|
||||||
|
{
|
||||||
|
"name": "repohooks",
|
||||||
|
"path": "src/repohooks"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"remote": [
|
||||||
|
{
|
||||||
|
"fetch": "http://localhost",
|
||||||
|
"name": "test-remote"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repo-hooks": {
|
||||||
|
"enabled-list": "a b",
|
||||||
|
"in-project": "repohooks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
+567
-1
@@ -97,6 +97,35 @@ def test_cli_jobs(argv, jobs_manifest, jobs, jobs_net, jobs_check):
|
|||||||
"""Tests --jobs option behavior."""
|
"""Tests --jobs option behavior."""
|
||||||
mp = mock.MagicMock()
|
mp = mock.MagicMock()
|
||||||
mp.manifest.default.sync_j = jobs_manifest
|
mp.manifest.default.sync_j = jobs_manifest
|
||||||
|
mp.manifest.default.sync_j_max = None
|
||||||
|
|
||||||
|
cmd = sync.Sync()
|
||||||
|
opts, args = cmd.OptionParser.parse_args(argv)
|
||||||
|
cmd.ValidateOptions(opts, args)
|
||||||
|
|
||||||
|
with mock.patch.object(sync, "_rlimit_nofile", return_value=(256, 256)):
|
||||||
|
with mock.patch.object(os, "cpu_count", return_value=OS_CPU_COUNT):
|
||||||
|
cmd._ValidateOptionsWithManifest(opts, mp)
|
||||||
|
assert opts.jobs == jobs
|
||||||
|
assert opts.jobs_network == jobs_net
|
||||||
|
assert opts.jobs_checkout == jobs_check
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"argv, jobs_manifest, jobs_manifest_max, jobs, jobs_net, jobs_check",
|
||||||
|
[
|
||||||
|
(["--jobs=10"], None, 5, 5, 5, 5),
|
||||||
|
(["--jobs=10", "--jobs-network=10"], None, 5, 5, 5, 5),
|
||||||
|
(["--jobs=10", "--jobs-checkout=10"], None, 5, 5, 5, 5),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_cli_jobs_sync_j_max(
|
||||||
|
argv, jobs_manifest, jobs_manifest_max, jobs, jobs_net, jobs_check
|
||||||
|
):
|
||||||
|
"""Tests --jobs option behavior with sync-j-max."""
|
||||||
|
mp = mock.MagicMock()
|
||||||
|
mp.manifest.default.sync_j = jobs_manifest
|
||||||
|
mp.manifest.default.sync_j_max = jobs_manifest_max
|
||||||
|
|
||||||
cmd = sync.Sync()
|
cmd = sync.Sync()
|
||||||
opts, args = cmd.OptionParser.parse_args(argv)
|
opts, args = cmd.OptionParser.parse_args(argv)
|
||||||
@@ -305,8 +334,21 @@ class LocalSyncState(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class FakeProject:
|
class FakeProject:
|
||||||
def __init__(self, relpath):
|
def __init__(self, relpath, name=None, objdir=None):
|
||||||
self.relpath = relpath
|
self.relpath = relpath
|
||||||
|
self.name = name or relpath
|
||||||
|
self.objdir = objdir or relpath
|
||||||
|
self.worktree = relpath
|
||||||
|
|
||||||
|
self.use_git_worktrees = False
|
||||||
|
self.UseAlternates = False
|
||||||
|
self.manifest = mock.MagicMock()
|
||||||
|
self.manifest.GetProjectsWithName.return_value = [self]
|
||||||
|
self.config = mock.MagicMock()
|
||||||
|
self.EnableRepositoryExtension = mock.MagicMock()
|
||||||
|
|
||||||
|
def RelPath(self, local=None):
|
||||||
|
return self.relpath
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"project: {self.relpath}"
|
return f"project: {self.relpath}"
|
||||||
@@ -435,6 +477,115 @@ class GetPreciousObjectsState(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CheckForBloatedProjects(unittest.TestCase):
|
||||||
|
"""Tests for Sync._CheckForBloatedProjects."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.cmd = sync.Sync()
|
||||||
|
self.opt = mock.Mock()
|
||||||
|
self.opt.quiet = True
|
||||||
|
self.opt.jobs = 1
|
||||||
|
self.project = mock.MagicMock(clone_depth="1")
|
||||||
|
self.project.name = "project"
|
||||||
|
self.project.Exists = True
|
||||||
|
self.project.worktree = "worktree"
|
||||||
|
self.cmd.git_event_log = mock.MagicMock()
|
||||||
|
self.cmd._bloated_projects = []
|
||||||
|
|
||||||
|
@mock.patch("subcmds.sync.git_require")
|
||||||
|
def test_git_version_unsupported(self, mock_git_require):
|
||||||
|
"""Test that it returns early if git version is unsupported."""
|
||||||
|
mock_git_require.return_value = False
|
||||||
|
self.cmd._CheckForBloatedProjects([self.project], self.opt)
|
||||||
|
self.assertFalse(self.cmd.git_event_log.ErrorEvent.called)
|
||||||
|
|
||||||
|
@mock.patch("subcmds.sync.git_require")
|
||||||
|
def test_no_projects(self, mock_git_require):
|
||||||
|
"""Test that it returns early if no projects have clone_depth."""
|
||||||
|
mock_git_require.return_value = True
|
||||||
|
self.project.clone_depth = None
|
||||||
|
self.cmd._CheckForBloatedProjects([self.project], self.opt)
|
||||||
|
self.assertFalse(self.cmd.git_event_log.ErrorEvent.called)
|
||||||
|
|
||||||
|
@mock.patch("subcmds.sync.git_require")
|
||||||
|
@mock.patch("subcmds.sync.Progress")
|
||||||
|
def test_bloated_project_found(self, mock_progress, mock_git_require):
|
||||||
|
"""Test that it adds project to _bloated_projects."""
|
||||||
|
mock_git_require.return_value = True
|
||||||
|
|
||||||
|
self.cmd.get_parallel_context = mock.Mock(
|
||||||
|
return_value={"projects": [self.project]}
|
||||||
|
)
|
||||||
|
|
||||||
|
def mock_execute_in_parallel(
|
||||||
|
jobs, func, work_items, callback, **kwargs
|
||||||
|
):
|
||||||
|
callback(None, mock.Mock(), ["project"])
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.cmd.ExecuteInParallel = mock_execute_in_parallel
|
||||||
|
|
||||||
|
with mock.patch.object(self.cmd, "ParallelContext"):
|
||||||
|
self.cmd._CheckForBloatedProjects([self.project], self.opt)
|
||||||
|
|
||||||
|
self.assertEqual(self.cmd._bloated_projects, ["project"])
|
||||||
|
|
||||||
|
|
||||||
|
class GCProjectsTest(unittest.TestCase):
|
||||||
|
"""Tests for Sync._GCProjects."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.cmd = sync.Sync()
|
||||||
|
self.opt = mock.Mock()
|
||||||
|
self.opt.quiet = True
|
||||||
|
self.opt.auto_gc = True
|
||||||
|
self.opt.jobs = 1
|
||||||
|
self.project = mock.MagicMock()
|
||||||
|
self.project.name = "project"
|
||||||
|
self.project.objdir = "objdir"
|
||||||
|
self.project.gitdir = "gitdir"
|
||||||
|
self.project.bare_git = mock.MagicMock()
|
||||||
|
self.project.bare_git._project = self.project
|
||||||
|
self.cmd.git_event_log = mock.MagicMock()
|
||||||
|
|
||||||
|
@mock.patch("subcmds.sync.Progress")
|
||||||
|
def test_GCProjects_skip_gc(self, mock_progress):
|
||||||
|
"""Test that it skips GC if opt.auto_gc is False."""
|
||||||
|
self.opt.auto_gc = False
|
||||||
|
with mock.patch.object(
|
||||||
|
sync.Sync, "_SetPreciousObjectsState"
|
||||||
|
) as mock_set_state:
|
||||||
|
self.cmd._GCProjects([self.project], self.opt, None)
|
||||||
|
mock_set_state.assert_called_once_with(self.project, self.opt)
|
||||||
|
self.assertFalse(self.project.bare_git.gc.called)
|
||||||
|
|
||||||
|
@mock.patch("subcmds.sync.Progress")
|
||||||
|
def test_GCProjects_sequential(self, mock_progress):
|
||||||
|
"""Test sequential GC (jobs < 2)."""
|
||||||
|
with mock.patch.object(sync.Sync, "_SetPreciousObjectsState"):
|
||||||
|
self.cmd._GCProjects([self.project], self.opt, None)
|
||||||
|
self.project.bare_git.gc.assert_called_once_with(
|
||||||
|
"--auto", config={"gc.autoDetach": "false"}
|
||||||
|
)
|
||||||
|
# Verify that gc.autoDetach was not permanently set in config.
|
||||||
|
for call in self.project.config.SetString.call_args_list:
|
||||||
|
self.assertNotEqual(call.args[0], "gc.autoDetach")
|
||||||
|
|
||||||
|
@mock.patch("subcmds.sync.Progress")
|
||||||
|
def test_GCProjects_parallel(self, mock_progress):
|
||||||
|
"""Test parallel GC (jobs >= 2)."""
|
||||||
|
self.opt.jobs = 2
|
||||||
|
with mock.patch.object(sync.Sync, "_SetPreciousObjectsState"):
|
||||||
|
with mock.patch("subcmds.sync._threading.Thread") as mock_thread:
|
||||||
|
mock_t = mock.MagicMock()
|
||||||
|
mock_thread.return_value = mock_t
|
||||||
|
err_event = mock.Mock()
|
||||||
|
err_event.is_set.return_value = False
|
||||||
|
self.cmd._GCProjects([self.project], self.opt, err_event)
|
||||||
|
|
||||||
|
self.assertTrue(mock_thread.called)
|
||||||
|
|
||||||
|
|
||||||
class SyncCommand(unittest.TestCase):
|
class SyncCommand(unittest.TestCase):
|
||||||
"""Tests for cmd.Execute."""
|
"""Tests for cmd.Execute."""
|
||||||
|
|
||||||
@@ -513,3 +664,418 @@ class SyncCommand(unittest.TestCase):
|
|||||||
self.cmd.Execute(self.opt, [])
|
self.cmd.Execute(self.opt, [])
|
||||||
self.assertIn(self.sync_local_half_error, e.aggregate_errors)
|
self.assertIn(self.sync_local_half_error, e.aggregate_errors)
|
||||||
self.assertIn(self.sync_network_half_error, e.aggregate_errors)
|
self.assertIn(self.sync_network_half_error, e.aggregate_errors)
|
||||||
|
|
||||||
|
|
||||||
|
class SyncUpdateRepoProject(unittest.TestCase):
|
||||||
|
"""Tests for Sync._UpdateRepoProject."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Common setup."""
|
||||||
|
self.repodir = tempfile.mkdtemp(".repo")
|
||||||
|
self.manifest = manifest = mock.MagicMock(repodir=self.repodir)
|
||||||
|
# Create a repoProject with a mock Sync_NetworkHalf.
|
||||||
|
repoProject = mock.MagicMock(name="repo")
|
||||||
|
repoProject.Sync_NetworkHalf = mock.Mock(
|
||||||
|
return_value=SyncNetworkHalfResult(True, None)
|
||||||
|
)
|
||||||
|
manifest.repoProject = repoProject
|
||||||
|
manifest.IsArchive = False
|
||||||
|
manifest.CloneFilter = None
|
||||||
|
manifest.PartialCloneExclude = None
|
||||||
|
manifest.CloneFilterForDepth = None
|
||||||
|
|
||||||
|
git_event_log = mock.MagicMock(ErrorEvent=mock.Mock(return_value=None))
|
||||||
|
self.cmd = sync.Sync(manifest=manifest, git_event_log=git_event_log)
|
||||||
|
|
||||||
|
opt, _ = self.cmd.OptionParser.parse_args([])
|
||||||
|
opt.local_only = False
|
||||||
|
opt.repo_verify = False
|
||||||
|
opt.verbose = False
|
||||||
|
opt.quiet = True
|
||||||
|
opt.force_sync = False
|
||||||
|
opt.clone_bundle = False
|
||||||
|
opt.tags = False
|
||||||
|
opt.optimized_fetch = False
|
||||||
|
opt.retry_fetches = 0
|
||||||
|
opt.prune = False
|
||||||
|
self.opt = opt
|
||||||
|
self.errors = []
|
||||||
|
|
||||||
|
mock.patch.object(sync.Sync, "_GetCurrentBranchOnly").start()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
shutil.rmtree(self.repodir)
|
||||||
|
mock.patch.stopall()
|
||||||
|
|
||||||
|
def test_fetches_when_stale(self):
|
||||||
|
"""Test it fetches when the repo project is stale."""
|
||||||
|
self.manifest.repoProject.LastFetch = time.time() - (
|
||||||
|
sync._ONE_DAY_S + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
with mock.patch.object(sync, "_PostRepoFetch") as mock_post_fetch:
|
||||||
|
self.cmd._UpdateRepoProject(self.opt, self.manifest, self.errors)
|
||||||
|
self.manifest.repoProject.Sync_NetworkHalf.assert_called_once()
|
||||||
|
mock_post_fetch.assert_called_once()
|
||||||
|
self.assertEqual(self.errors, [])
|
||||||
|
|
||||||
|
def test_skips_when_fresh(self):
|
||||||
|
"""Test it skips fetch when repo project is fresh."""
|
||||||
|
self.manifest.repoProject.LastFetch = time.time()
|
||||||
|
|
||||||
|
with mock.patch.object(sync, "_PostRepoFetch") as mock_post_fetch:
|
||||||
|
self.cmd._UpdateRepoProject(self.opt, self.manifest, self.errors)
|
||||||
|
self.manifest.repoProject.Sync_NetworkHalf.assert_not_called()
|
||||||
|
mock_post_fetch.assert_not_called()
|
||||||
|
|
||||||
|
def test_skips_local_only(self):
|
||||||
|
"""Test it does nothing with --local-only."""
|
||||||
|
self.opt.local_only = True
|
||||||
|
self.manifest.repoProject.LastFetch = time.time() - (
|
||||||
|
sync._ONE_DAY_S + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
with mock.patch.object(sync, "_PostRepoFetch") as mock_post_fetch:
|
||||||
|
self.cmd._UpdateRepoProject(self.opt, self.manifest, self.errors)
|
||||||
|
self.manifest.repoProject.Sync_NetworkHalf.assert_not_called()
|
||||||
|
mock_post_fetch.assert_not_called()
|
||||||
|
|
||||||
|
def test_post_repo_fetch_skipped_on_env_var(self):
|
||||||
|
"""Test _PostRepoFetch is skipped when REPO_SKIP_SELF_UPDATE is set."""
|
||||||
|
self.manifest.repoProject.LastFetch = time.time()
|
||||||
|
|
||||||
|
with mock.patch.dict(os.environ, {"REPO_SKIP_SELF_UPDATE": "1"}):
|
||||||
|
with mock.patch.object(sync, "_PostRepoFetch") as mock_post_fetch:
|
||||||
|
self.cmd._UpdateRepoProject(
|
||||||
|
self.opt, self.manifest, self.errors
|
||||||
|
)
|
||||||
|
mock_post_fetch.assert_not_called()
|
||||||
|
|
||||||
|
def test_fetch_failure_is_handled(self):
|
||||||
|
"""Test that a fetch failure is recorded and doesn't crash."""
|
||||||
|
self.manifest.repoProject.LastFetch = time.time() - (
|
||||||
|
sync._ONE_DAY_S + 1
|
||||||
|
)
|
||||||
|
fetch_error = GitError("Fetch failed")
|
||||||
|
self.manifest.repoProject.Sync_NetworkHalf.return_value = (
|
||||||
|
SyncNetworkHalfResult(False, fetch_error)
|
||||||
|
)
|
||||||
|
|
||||||
|
with mock.patch.object(sync, "_PostRepoFetch") as mock_post_fetch:
|
||||||
|
self.cmd._UpdateRepoProject(self.opt, self.manifest, self.errors)
|
||||||
|
self.manifest.repoProject.Sync_NetworkHalf.assert_called_once()
|
||||||
|
mock_post_fetch.assert_not_called()
|
||||||
|
self.assertEqual(self.errors, [fetch_error])
|
||||||
|
|
||||||
|
|
||||||
|
class InterleavedSyncTest(unittest.TestCase):
|
||||||
|
"""Tests for interleaved sync."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up a sync command with mocks."""
|
||||||
|
self.repodir = tempfile.mkdtemp(".repo")
|
||||||
|
self.manifest = mock.MagicMock(repodir=self.repodir)
|
||||||
|
self.manifest.repoProject.LastFetch = time.time()
|
||||||
|
self.manifest.repoProject.worktree = self.repodir
|
||||||
|
self.manifest.manifestProject.worktree = self.repodir
|
||||||
|
self.manifest.IsArchive = False
|
||||||
|
self.manifest.CloneBundle = False
|
||||||
|
self.manifest.default.sync_j = 1
|
||||||
|
|
||||||
|
self.outer_client = mock.MagicMock()
|
||||||
|
self.outer_client.manifest.IsArchive = False
|
||||||
|
self.cmd = sync.Sync(
|
||||||
|
manifest=self.manifest, outer_client=self.outer_client
|
||||||
|
)
|
||||||
|
self.cmd.outer_manifest = self.manifest
|
||||||
|
|
||||||
|
# Mock projects.
|
||||||
|
self.projA = FakeProject("projA", objdir="objA")
|
||||||
|
self.projB = FakeProject("projB", objdir="objB")
|
||||||
|
self.projA_sub = FakeProject(
|
||||||
|
"projA/sub", name="projA_sub", objdir="objA_sub"
|
||||||
|
)
|
||||||
|
self.projC = FakeProject("projC", objdir="objC")
|
||||||
|
|
||||||
|
# Mock methods that are not part of the core interleaved sync logic.
|
||||||
|
mock.patch.object(self.cmd, "_UpdateAllManifestProjects").start()
|
||||||
|
mock.patch.object(self.cmd, "_UpdateProjectsRevisionId").start()
|
||||||
|
mock.patch.object(self.cmd, "_ValidateOptionsWithManifest").start()
|
||||||
|
mock.patch.object(sync, "_PostRepoUpgrade").start()
|
||||||
|
mock.patch.object(sync, "_PostRepoFetch").start()
|
||||||
|
|
||||||
|
# Mock parallel context for worker tests.
|
||||||
|
self.parallel_context_patcher = mock.patch(
|
||||||
|
"subcmds.sync.Sync.get_parallel_context"
|
||||||
|
)
|
||||||
|
self.mock_get_parallel_context = self.parallel_context_patcher.start()
|
||||||
|
self.sync_dict = {}
|
||||||
|
self.mock_context = {
|
||||||
|
"projects": [],
|
||||||
|
"sync_dict": self.sync_dict,
|
||||||
|
}
|
||||||
|
self.mock_get_parallel_context.return_value = self.mock_context
|
||||||
|
|
||||||
|
# Mock _GetCurrentBranchOnly for worker tests.
|
||||||
|
mock.patch.object(sync.Sync, "_GetCurrentBranchOnly").start()
|
||||||
|
|
||||||
|
self.cmd._fetch_times = mock.Mock()
|
||||||
|
self.cmd._local_sync_state = mock.Mock()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Clean up resources."""
|
||||||
|
shutil.rmtree(self.repodir)
|
||||||
|
mock.patch.stopall()
|
||||||
|
|
||||||
|
def test_interleaved_fail_fast(self):
|
||||||
|
"""Test that --fail-fast is respected in interleaved mode."""
|
||||||
|
opt, args = self.cmd.OptionParser.parse_args(
|
||||||
|
["--interleaved", "--fail-fast", "-j2"]
|
||||||
|
)
|
||||||
|
opt.quiet = True
|
||||||
|
|
||||||
|
# With projA/sub, _SafeCheckoutOrder creates two batches:
|
||||||
|
# 1. [projA, projB]
|
||||||
|
# 2. [projA/sub]
|
||||||
|
# We want to fail on the first batch and ensure the second isn't run.
|
||||||
|
all_projects = [self.projA, self.projB, self.projA_sub]
|
||||||
|
mock.patch.object(
|
||||||
|
self.cmd, "GetProjects", return_value=all_projects
|
||||||
|
).start()
|
||||||
|
|
||||||
|
# Mock ExecuteInParallel to simulate a failed run on the first batch of
|
||||||
|
# projects.
|
||||||
|
execute_mock = mock.patch.object(
|
||||||
|
self.cmd, "ExecuteInParallel", return_value=False
|
||||||
|
).start()
|
||||||
|
|
||||||
|
with self.assertRaises(sync.SyncFailFastError):
|
||||||
|
self.cmd._SyncInterleaved(
|
||||||
|
opt,
|
||||||
|
args,
|
||||||
|
[],
|
||||||
|
self.manifest,
|
||||||
|
self.manifest.manifestProject,
|
||||||
|
all_projects,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
execute_mock.assert_called_once()
|
||||||
|
|
||||||
|
def test_interleaved_shared_objdir_serial(self):
|
||||||
|
"""Test that projects with shared objdir are processed serially."""
|
||||||
|
opt, args = self.cmd.OptionParser.parse_args(["--interleaved", "-j4"])
|
||||||
|
opt.quiet = True
|
||||||
|
|
||||||
|
# Setup projects with a shared objdir.
|
||||||
|
self.projA.objdir = "common_objdir"
|
||||||
|
self.projC.objdir = "common_objdir"
|
||||||
|
|
||||||
|
all_projects = [self.projA, self.projB, self.projC]
|
||||||
|
mock.patch.object(
|
||||||
|
self.cmd, "GetProjects", return_value=all_projects
|
||||||
|
).start()
|
||||||
|
|
||||||
|
def execute_side_effect(jobs, target, work_items, **kwargs):
|
||||||
|
# The callback is a partial object. The first arg is the set we
|
||||||
|
# need to update to avoid the stall detection.
|
||||||
|
synced_relpaths_set = kwargs["callback"].args[0]
|
||||||
|
projects_in_pass = self.cmd.get_parallel_context()["projects"]
|
||||||
|
for item in work_items:
|
||||||
|
for project_idx in item:
|
||||||
|
synced_relpaths_set.add(
|
||||||
|
projects_in_pass[project_idx].relpath
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
execute_mock = mock.patch.object(
|
||||||
|
self.cmd, "ExecuteInParallel", side_effect=execute_side_effect
|
||||||
|
).start()
|
||||||
|
|
||||||
|
self.cmd._SyncInterleaved(
|
||||||
|
opt,
|
||||||
|
args,
|
||||||
|
[],
|
||||||
|
self.manifest,
|
||||||
|
self.manifest.manifestProject,
|
||||||
|
all_projects,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
execute_mock.assert_called_once()
|
||||||
|
jobs_arg, _, work_items = execute_mock.call_args.args
|
||||||
|
self.assertEqual(jobs_arg, 2)
|
||||||
|
work_items_sets = {frozenset(item) for item in work_items}
|
||||||
|
expected_sets = {frozenset([0, 2]), frozenset([1])}
|
||||||
|
self.assertEqual(work_items_sets, expected_sets)
|
||||||
|
|
||||||
|
def _get_opts(self, args=None):
|
||||||
|
"""Helper to get default options for worker tests."""
|
||||||
|
if args is None:
|
||||||
|
args = ["--interleaved"]
|
||||||
|
opt, _ = self.cmd.OptionParser.parse_args(args)
|
||||||
|
# Set defaults for options used by the worker.
|
||||||
|
opt.quiet = True
|
||||||
|
opt.verbose = False
|
||||||
|
opt.force_sync = False
|
||||||
|
opt.clone_bundle = False
|
||||||
|
opt.tags = False
|
||||||
|
opt.optimized_fetch = False
|
||||||
|
opt.retry_fetches = 0
|
||||||
|
opt.prune = False
|
||||||
|
opt.detach_head = False
|
||||||
|
opt.force_checkout = False
|
||||||
|
opt.rebase = False
|
||||||
|
return opt
|
||||||
|
|
||||||
|
def test_worker_successful_sync(self):
|
||||||
|
"""Test _SyncProjectList with a successful fetch and checkout."""
|
||||||
|
opt = self._get_opts()
|
||||||
|
project = self.projA
|
||||||
|
project.Sync_NetworkHalf = mock.Mock(
|
||||||
|
return_value=SyncNetworkHalfResult(error=None, remote_fetched=True)
|
||||||
|
)
|
||||||
|
project.Sync_LocalHalf = mock.Mock()
|
||||||
|
project.manifest.manifestProject.config = mock.MagicMock()
|
||||||
|
self.mock_context["projects"] = [project]
|
||||||
|
|
||||||
|
with mock.patch("subcmds.sync.SyncBuffer") as mock_sync_buffer:
|
||||||
|
mock_sync_buf_instance = mock.MagicMock()
|
||||||
|
mock_sync_buf_instance.Finish.return_value = True
|
||||||
|
mock_sync_buf_instance.errors = []
|
||||||
|
mock_sync_buffer.return_value = mock_sync_buf_instance
|
||||||
|
|
||||||
|
result_obj = self.cmd._SyncProjectList(opt, [0])
|
||||||
|
|
||||||
|
self.assertEqual(len(result_obj.results), 1)
|
||||||
|
result = result_obj.results[0]
|
||||||
|
self.assertTrue(result.fetch_success)
|
||||||
|
self.assertTrue(result.checkout_success)
|
||||||
|
self.assertEqual(result.fetch_errors, [])
|
||||||
|
self.assertEqual(result.checkout_errors, [])
|
||||||
|
project.Sync_NetworkHalf.assert_called_once()
|
||||||
|
project.Sync_LocalHalf.assert_called_once()
|
||||||
|
|
||||||
|
def test_worker_fetch_fails(self):
|
||||||
|
"""Test _SyncProjectList with a failed fetch."""
|
||||||
|
opt = self._get_opts()
|
||||||
|
project = self.projA
|
||||||
|
fetch_error = GitError("Fetch failed")
|
||||||
|
project.Sync_NetworkHalf = mock.Mock(
|
||||||
|
return_value=SyncNetworkHalfResult(
|
||||||
|
error=fetch_error, remote_fetched=False
|
||||||
|
)
|
||||||
|
)
|
||||||
|
project.Sync_LocalHalf = mock.Mock()
|
||||||
|
self.mock_context["projects"] = [project]
|
||||||
|
|
||||||
|
result_obj = self.cmd._SyncProjectList(opt, [0])
|
||||||
|
result = result_obj.results[0]
|
||||||
|
|
||||||
|
self.assertFalse(result.fetch_success)
|
||||||
|
self.assertFalse(result.checkout_success)
|
||||||
|
self.assertEqual(result.fetch_errors, [fetch_error])
|
||||||
|
self.assertEqual(result.checkout_errors, [])
|
||||||
|
project.Sync_NetworkHalf.assert_called_once()
|
||||||
|
project.Sync_LocalHalf.assert_not_called()
|
||||||
|
|
||||||
|
def test_worker_no_worktree(self):
|
||||||
|
"""Test interleaved sync does not checkout with no worktree."""
|
||||||
|
opt = self._get_opts()
|
||||||
|
project = self.projA
|
||||||
|
project.worktree = None
|
||||||
|
project.Sync_NetworkHalf = mock.Mock(
|
||||||
|
return_value=SyncNetworkHalfResult(error=None, remote_fetched=True)
|
||||||
|
)
|
||||||
|
project.Sync_LocalHalf = mock.Mock()
|
||||||
|
self.mock_context["projects"] = [project]
|
||||||
|
|
||||||
|
result_obj = self.cmd._SyncProjectList(opt, [0])
|
||||||
|
result = result_obj.results[0]
|
||||||
|
|
||||||
|
self.assertTrue(result.fetch_success)
|
||||||
|
self.assertTrue(result.checkout_success)
|
||||||
|
project.Sync_NetworkHalf.assert_called_once()
|
||||||
|
project.Sync_LocalHalf.assert_not_called()
|
||||||
|
|
||||||
|
def test_worker_fetch_fails_exception(self):
|
||||||
|
"""Test _SyncProjectList with an exception during fetch."""
|
||||||
|
opt = self._get_opts()
|
||||||
|
project = self.projA
|
||||||
|
fetch_error = GitError("Fetch failed")
|
||||||
|
project.Sync_NetworkHalf = mock.Mock(side_effect=fetch_error)
|
||||||
|
project.Sync_LocalHalf = mock.Mock()
|
||||||
|
self.mock_context["projects"] = [project]
|
||||||
|
|
||||||
|
result_obj = self.cmd._SyncProjectList(opt, [0])
|
||||||
|
result = result_obj.results[0]
|
||||||
|
|
||||||
|
self.assertFalse(result.fetch_success)
|
||||||
|
self.assertFalse(result.checkout_success)
|
||||||
|
self.assertEqual(result.fetch_errors, [fetch_error])
|
||||||
|
project.Sync_NetworkHalf.assert_called_once()
|
||||||
|
project.Sync_LocalHalf.assert_not_called()
|
||||||
|
|
||||||
|
def test_worker_checkout_fails(self):
|
||||||
|
"""Test _SyncProjectList with an exception during checkout."""
|
||||||
|
opt = self._get_opts()
|
||||||
|
project = self.projA
|
||||||
|
project.Sync_NetworkHalf = mock.Mock(
|
||||||
|
return_value=SyncNetworkHalfResult(error=None, remote_fetched=True)
|
||||||
|
)
|
||||||
|
checkout_error = GitError("Checkout failed")
|
||||||
|
project.Sync_LocalHalf = mock.Mock(side_effect=checkout_error)
|
||||||
|
project.manifest.manifestProject.config = mock.MagicMock()
|
||||||
|
self.mock_context["projects"] = [project]
|
||||||
|
|
||||||
|
with mock.patch("subcmds.sync.SyncBuffer"):
|
||||||
|
result_obj = self.cmd._SyncProjectList(opt, [0])
|
||||||
|
result = result_obj.results[0]
|
||||||
|
|
||||||
|
self.assertTrue(result.fetch_success)
|
||||||
|
self.assertFalse(result.checkout_success)
|
||||||
|
self.assertEqual(result.fetch_errors, [])
|
||||||
|
self.assertEqual(result.checkout_errors, [checkout_error])
|
||||||
|
project.Sync_NetworkHalf.assert_called_once()
|
||||||
|
project.Sync_LocalHalf.assert_called_once()
|
||||||
|
|
||||||
|
def test_worker_local_only(self):
|
||||||
|
"""Test _SyncProjectList with --local-only."""
|
||||||
|
opt = self._get_opts(["--interleaved", "--local-only"])
|
||||||
|
project = self.projA
|
||||||
|
project.Sync_NetworkHalf = mock.Mock()
|
||||||
|
project.Sync_LocalHalf = mock.Mock()
|
||||||
|
project.manifest.manifestProject.config = mock.MagicMock()
|
||||||
|
self.mock_context["projects"] = [project]
|
||||||
|
|
||||||
|
with mock.patch("subcmds.sync.SyncBuffer") as mock_sync_buffer:
|
||||||
|
mock_sync_buf_instance = mock.MagicMock()
|
||||||
|
mock_sync_buf_instance.Finish.return_value = True
|
||||||
|
mock_sync_buf_instance.errors = []
|
||||||
|
mock_sync_buffer.return_value = mock_sync_buf_instance
|
||||||
|
|
||||||
|
result_obj = self.cmd._SyncProjectList(opt, [0])
|
||||||
|
result = result_obj.results[0]
|
||||||
|
|
||||||
|
self.assertTrue(result.fetch_success)
|
||||||
|
self.assertTrue(result.checkout_success)
|
||||||
|
project.Sync_NetworkHalf.assert_not_called()
|
||||||
|
project.Sync_LocalHalf.assert_called_once()
|
||||||
|
|
||||||
|
def test_worker_network_only(self):
|
||||||
|
"""Test _SyncProjectList with --network-only."""
|
||||||
|
opt = self._get_opts(["--interleaved", "--network-only"])
|
||||||
|
project = self.projA
|
||||||
|
project.Sync_NetworkHalf = mock.Mock(
|
||||||
|
return_value=SyncNetworkHalfResult(error=None, remote_fetched=True)
|
||||||
|
)
|
||||||
|
project.Sync_LocalHalf = mock.Mock()
|
||||||
|
self.mock_context["projects"] = [project]
|
||||||
|
|
||||||
|
result_obj = self.cmd._SyncProjectList(opt, [0])
|
||||||
|
result = result_obj.results[0]
|
||||||
|
|
||||||
|
self.assertTrue(result.fetch_success)
|
||||||
|
self.assertTrue(result.checkout_success)
|
||||||
|
project.Sync_NetworkHalf.assert_called_once()
|
||||||
|
project.Sync_LocalHalf.assert_not_called()
|
||||||
|
|||||||
@@ -14,9 +14,10 @@
|
|||||||
|
|
||||||
"""Unittests for the subcmds/upload.py module."""
|
"""Unittests for the subcmds/upload.py module."""
|
||||||
|
|
||||||
import unittest
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from error import GitError
|
from error import GitError
|
||||||
from error import UploadError
|
from error import UploadError
|
||||||
from subcmds import upload
|
from subcmds import upload
|
||||||
@@ -26,45 +27,39 @@ class UnexpectedError(Exception):
|
|||||||
"""An exception not expected by upload command."""
|
"""An exception not expected by upload command."""
|
||||||
|
|
||||||
|
|
||||||
class UploadCommand(unittest.TestCase):
|
# A stub people list (reviewers, cc).
|
||||||
"""Check registered all_commands."""
|
_STUB_PEOPLE = ([], [])
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.cmd = upload.Upload()
|
|
||||||
self.branch = mock.MagicMock()
|
|
||||||
self.people = mock.MagicMock()
|
|
||||||
self.opt, _ = self.cmd.OptionParser.parse_args([])
|
|
||||||
mock.patch.object(
|
|
||||||
self.cmd, "_AppendAutoList", return_value=None
|
|
||||||
).start()
|
|
||||||
mock.patch.object(self.cmd, "git_event_log").start()
|
|
||||||
|
|
||||||
def tearDown(self):
|
@pytest.fixture
|
||||||
mock.patch.stopall()
|
def cmd() -> upload.Upload:
|
||||||
|
"""Fixture to provide an Upload command instance with mocked methods."""
|
||||||
|
cmd = upload.Upload()
|
||||||
|
with mock.patch.object(
|
||||||
|
cmd, "_AppendAutoList", return_value=None
|
||||||
|
), mock.patch.object(cmd, "git_event_log"):
|
||||||
|
yield cmd
|
||||||
|
|
||||||
def test_UploadAndReport_UploadError(self):
|
|
||||||
"""Check UploadExitError raised when UploadError encountered."""
|
|
||||||
side_effect = UploadError("upload error")
|
|
||||||
with mock.patch.object(
|
|
||||||
self.cmd, "_UploadBranch", side_effect=side_effect
|
|
||||||
):
|
|
||||||
with self.assertRaises(upload.UploadExitError):
|
|
||||||
self.cmd._UploadAndReport(self.opt, [self.branch], self.people)
|
|
||||||
|
|
||||||
def test_UploadAndReport_GitError(self):
|
def test_UploadAndReport_UploadError(cmd: upload.Upload) -> None:
|
||||||
"""Check UploadExitError raised when GitError encountered."""
|
"""Check UploadExitError raised when UploadError encountered."""
|
||||||
side_effect = GitError("some git error")
|
opt, _ = cmd.OptionParser.parse_args([])
|
||||||
with mock.patch.object(
|
with mock.patch.object(cmd, "_UploadBranch", side_effect=UploadError("")):
|
||||||
self.cmd, "_UploadBranch", side_effect=side_effect
|
with pytest.raises(upload.UploadExitError):
|
||||||
):
|
cmd._UploadAndReport(opt, [mock.MagicMock()], _STUB_PEOPLE)
|
||||||
with self.assertRaises(upload.UploadExitError):
|
|
||||||
self.cmd._UploadAndReport(self.opt, [self.branch], self.people)
|
|
||||||
|
|
||||||
def test_UploadAndReport_UnhandledError(self):
|
|
||||||
"""Check UnexpectedError passed through."""
|
def test_UploadAndReport_GitError(cmd: upload.Upload) -> None:
|
||||||
side_effect = UnexpectedError("some os error")
|
"""Check UploadExitError raised when GitError encountered."""
|
||||||
with mock.patch.object(
|
opt, _ = cmd.OptionParser.parse_args([])
|
||||||
self.cmd, "_UploadBranch", side_effect=side_effect
|
with mock.patch.object(cmd, "_UploadBranch", side_effect=GitError("")):
|
||||||
):
|
with pytest.raises(upload.UploadExitError):
|
||||||
with self.assertRaises(type(side_effect)):
|
cmd._UploadAndReport(opt, [mock.MagicMock()], _STUB_PEOPLE)
|
||||||
self.cmd._UploadAndReport(self.opt, [self.branch], self.people)
|
|
||||||
|
|
||||||
|
def test_UploadAndReport_UnhandledError(cmd: upload.Upload) -> None:
|
||||||
|
"""Check UnexpectedError passed through."""
|
||||||
|
opt, _ = cmd.OptionParser.parse_args([])
|
||||||
|
with mock.patch.object(cmd, "_UploadBranch", side_effect=UnexpectedError):
|
||||||
|
with pytest.raises(UnexpectedError):
|
||||||
|
cmd._UploadAndReport(opt, [mock.MagicMock()], _STUB_PEOPLE)
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
# 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 shutil
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import project
|
||||||
|
from subcmds import wipe
|
||||||
|
|
||||||
|
|
||||||
|
def _create_mock_project(tempdir, name, objdir_path=None, has_changes=False):
|
||||||
|
"""Creates a mock project with necessary attributes and directories."""
|
||||||
|
worktree = os.path.join(tempdir, name)
|
||||||
|
gitdir = os.path.join(tempdir, ".repo/projects", f"{name}.git")
|
||||||
|
if objdir_path:
|
||||||
|
objdir = objdir_path
|
||||||
|
else:
|
||||||
|
objdir = os.path.join(tempdir, ".repo/project-objects", f"{name}.git")
|
||||||
|
|
||||||
|
os.makedirs(worktree, exist_ok=True)
|
||||||
|
os.makedirs(gitdir, exist_ok=True)
|
||||||
|
os.makedirs(objdir, exist_ok=True)
|
||||||
|
|
||||||
|
proj = project.Project(
|
||||||
|
manifest=mock.MagicMock(),
|
||||||
|
name=name,
|
||||||
|
remote=mock.MagicMock(),
|
||||||
|
gitdir=gitdir,
|
||||||
|
objdir=objdir,
|
||||||
|
worktree=worktree,
|
||||||
|
relpath=name,
|
||||||
|
revisionExpr="main",
|
||||||
|
revisionId="abcd",
|
||||||
|
)
|
||||||
|
|
||||||
|
proj.HasChanges = mock.MagicMock(return_value=has_changes)
|
||||||
|
|
||||||
|
def side_effect_delete_worktree(force=False, verbose=False):
|
||||||
|
if os.path.exists(proj.worktree):
|
||||||
|
shutil.rmtree(proj.worktree)
|
||||||
|
if os.path.exists(proj.gitdir):
|
||||||
|
shutil.rmtree(proj.gitdir)
|
||||||
|
return True
|
||||||
|
|
||||||
|
proj.DeleteWorktree = mock.MagicMock(
|
||||||
|
side_effect=side_effect_delete_worktree
|
||||||
|
)
|
||||||
|
|
||||||
|
return proj
|
||||||
|
|
||||||
|
|
||||||
|
def _run_wipe(all_projects, projects_to_wipe_names, options=None):
|
||||||
|
"""Helper to run the Wipe command with mocked projects."""
|
||||||
|
cmd = wipe.Wipe()
|
||||||
|
cmd.manifest = mock.MagicMock()
|
||||||
|
|
||||||
|
def get_projects_mock(projects, all_manifests=False, **kwargs):
|
||||||
|
if projects is None:
|
||||||
|
return all_projects
|
||||||
|
names_to_find = set(projects)
|
||||||
|
return [p for p in all_projects if p.name in names_to_find]
|
||||||
|
|
||||||
|
cmd.GetProjects = mock.MagicMock(side_effect=get_projects_mock)
|
||||||
|
|
||||||
|
if options is None:
|
||||||
|
options = []
|
||||||
|
|
||||||
|
opts = cmd.OptionParser.parse_args(options + projects_to_wipe_names)[0]
|
||||||
|
cmd.CommonValidateOptions(opts, projects_to_wipe_names)
|
||||||
|
cmd.ValidateOptions(opts, projects_to_wipe_names)
|
||||||
|
cmd.Execute(opts, projects_to_wipe_names)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wipe_single_unshared_project(tmp_path):
|
||||||
|
"""Test wiping a single project that is not shared."""
|
||||||
|
p1 = _create_mock_project(str(tmp_path), "project/one")
|
||||||
|
_run_wipe([p1], ["project/one"])
|
||||||
|
|
||||||
|
assert not os.path.exists(p1.worktree)
|
||||||
|
assert not os.path.exists(p1.gitdir)
|
||||||
|
assert not os.path.exists(p1.objdir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wipe_multiple_unshared_projects(tmp_path):
|
||||||
|
"""Test wiping multiple projects that are not shared."""
|
||||||
|
p1 = _create_mock_project(str(tmp_path), "project/one")
|
||||||
|
p2 = _create_mock_project(str(tmp_path), "project/two")
|
||||||
|
_run_wipe([p1, p2], ["project/one", "project/two"])
|
||||||
|
|
||||||
|
assert not os.path.exists(p1.worktree)
|
||||||
|
assert not os.path.exists(p1.gitdir)
|
||||||
|
assert not os.path.exists(p1.objdir)
|
||||||
|
assert not os.path.exists(p2.worktree)
|
||||||
|
assert not os.path.exists(p2.gitdir)
|
||||||
|
assert not os.path.exists(p2.objdir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wipe_shared_project_no_force_raises_error(tmp_path):
|
||||||
|
"""Test that wiping a shared project without --force raises an error."""
|
||||||
|
shared_objdir = os.path.join(
|
||||||
|
str(tmp_path), ".repo/project-objects", "shared.git"
|
||||||
|
)
|
||||||
|
p1 = _create_mock_project(
|
||||||
|
str(tmp_path), "project/one", objdir_path=shared_objdir
|
||||||
|
)
|
||||||
|
p2 = _create_mock_project(
|
||||||
|
str(tmp_path), "project/two", objdir_path=shared_objdir
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(wipe.Error) as e:
|
||||||
|
_run_wipe([p1, p2], ["project/one"])
|
||||||
|
|
||||||
|
assert "shared object directories" in str(e.value)
|
||||||
|
assert "project/one" in str(e.value)
|
||||||
|
assert "project/two" in str(e.value)
|
||||||
|
|
||||||
|
assert os.path.exists(p1.worktree)
|
||||||
|
assert os.path.exists(p1.gitdir)
|
||||||
|
assert os.path.exists(p2.worktree)
|
||||||
|
assert os.path.exists(p2.gitdir)
|
||||||
|
assert os.path.exists(shared_objdir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wipe_shared_project_with_force(tmp_path):
|
||||||
|
"""Test wiping a shared project with --force."""
|
||||||
|
shared_objdir = os.path.join(
|
||||||
|
str(tmp_path), ".repo/project-objects", "shared.git"
|
||||||
|
)
|
||||||
|
p1 = _create_mock_project(
|
||||||
|
str(tmp_path), "project/one", objdir_path=shared_objdir
|
||||||
|
)
|
||||||
|
p2 = _create_mock_project(
|
||||||
|
str(tmp_path), "project/two", objdir_path=shared_objdir
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_wipe([p1, p2], ["project/one"], options=["--force"])
|
||||||
|
|
||||||
|
assert not os.path.exists(p1.worktree)
|
||||||
|
assert not os.path.exists(p1.gitdir)
|
||||||
|
assert os.path.exists(shared_objdir)
|
||||||
|
assert os.path.exists(p2.worktree)
|
||||||
|
assert os.path.exists(p2.gitdir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wipe_all_sharing_projects(tmp_path):
|
||||||
|
"""Test wiping all projects that share an object directory."""
|
||||||
|
shared_objdir = os.path.join(
|
||||||
|
str(tmp_path), ".repo/project-objects", "shared.git"
|
||||||
|
)
|
||||||
|
p1 = _create_mock_project(
|
||||||
|
str(tmp_path), "project/one", objdir_path=shared_objdir
|
||||||
|
)
|
||||||
|
p2 = _create_mock_project(
|
||||||
|
str(tmp_path), "project/two", objdir_path=shared_objdir
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_wipe([p1, p2], ["project/one", "project/two"])
|
||||||
|
|
||||||
|
assert not os.path.exists(p1.worktree)
|
||||||
|
assert not os.path.exists(p1.gitdir)
|
||||||
|
assert not os.path.exists(p2.worktree)
|
||||||
|
assert not os.path.exists(p2.gitdir)
|
||||||
|
assert not os.path.exists(shared_objdir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wipe_with_uncommitted_changes_raises_error(tmp_path):
|
||||||
|
"""Test wiping a project with uncommitted changes raises an error."""
|
||||||
|
p1 = _create_mock_project(str(tmp_path), "project/one", has_changes=True)
|
||||||
|
|
||||||
|
with pytest.raises(wipe.Error) as e:
|
||||||
|
_run_wipe([p1], ["project/one"])
|
||||||
|
|
||||||
|
assert "uncommitted changes" in str(e.value)
|
||||||
|
assert "project/one" in str(e.value)
|
||||||
|
|
||||||
|
assert os.path.exists(p1.worktree)
|
||||||
|
assert os.path.exists(p1.gitdir)
|
||||||
|
assert os.path.exists(p1.objdir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wipe_with_uncommitted_changes_with_force(tmp_path):
|
||||||
|
"""Test wiping a project with uncommitted changes with --force."""
|
||||||
|
p1 = _create_mock_project(str(tmp_path), "project/one", has_changes=True)
|
||||||
|
_run_wipe([p1], ["project/one"], options=["--force"])
|
||||||
|
|
||||||
|
assert not os.path.exists(p1.worktree)
|
||||||
|
assert not os.path.exists(p1.gitdir)
|
||||||
|
assert not os.path.exists(p1.objdir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wipe_uncommitted_and_shared_raises_combined_error(tmp_path):
|
||||||
|
"""Test that uncommitted and shared projects raise a combined error."""
|
||||||
|
shared_objdir = os.path.join(
|
||||||
|
str(tmp_path), ".repo/project-objects", "shared.git"
|
||||||
|
)
|
||||||
|
p1 = _create_mock_project(
|
||||||
|
str(tmp_path),
|
||||||
|
"project/one",
|
||||||
|
objdir_path=shared_objdir,
|
||||||
|
has_changes=True,
|
||||||
|
)
|
||||||
|
p2 = _create_mock_project(
|
||||||
|
str(tmp_path), "project/two", objdir_path=shared_objdir
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(wipe.Error) as e:
|
||||||
|
_run_wipe([p1, p2], ["project/one"])
|
||||||
|
|
||||||
|
assert "uncommitted changes" in str(e.value)
|
||||||
|
assert "shared object directories" in str(e.value)
|
||||||
|
assert "project/one" in str(e.value)
|
||||||
|
assert "project/two" in str(e.value)
|
||||||
|
|
||||||
|
assert os.path.exists(p1.worktree)
|
||||||
|
assert os.path.exists(p1.gitdir)
|
||||||
|
assert os.path.exists(p2.worktree)
|
||||||
|
assert os.path.exists(p2.gitdir)
|
||||||
|
assert os.path.exists(shared_objdir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wipe_shared_project_with_force_shared(tmp_path):
|
||||||
|
"""Test wiping a shared project with --force-shared."""
|
||||||
|
shared_objdir = os.path.join(
|
||||||
|
str(tmp_path), ".repo/project-objects", "shared.git"
|
||||||
|
)
|
||||||
|
p1 = _create_mock_project(
|
||||||
|
str(tmp_path), "project/one", objdir_path=shared_objdir
|
||||||
|
)
|
||||||
|
p2 = _create_mock_project(
|
||||||
|
str(tmp_path), "project/two", objdir_path=shared_objdir
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_wipe([p1, p2], ["project/one"], options=["--force-shared"])
|
||||||
|
|
||||||
|
assert not os.path.exists(p1.worktree)
|
||||||
|
assert not os.path.exists(p1.gitdir)
|
||||||
|
assert os.path.exists(shared_objdir)
|
||||||
|
assert os.path.exists(p2.worktree)
|
||||||
|
assert os.path.exists(p2.gitdir)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wipe_with_uncommitted_changes_with_force_uncommitted(tmp_path):
|
||||||
|
"""Test wiping uncommitted changes with --force-uncommitted."""
|
||||||
|
p1 = _create_mock_project(str(tmp_path), "project/one", has_changes=True)
|
||||||
|
_run_wipe([p1], ["project/one"], options=["--force-uncommitted"])
|
||||||
|
|
||||||
|
assert not os.path.exists(p1.worktree)
|
||||||
|
assert not os.path.exists(p1.gitdir)
|
||||||
|
assert not os.path.exists(p1.objdir)
|
||||||
@@ -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.
|
||||||
@@ -14,15 +14,10 @@
|
|||||||
|
|
||||||
"""Unittests for the update_manpages module."""
|
"""Unittests for the update_manpages module."""
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from release import update_manpages
|
from release import update_manpages
|
||||||
|
|
||||||
|
|
||||||
class UpdateManpagesTest(unittest.TestCase):
|
def test_replace_regex() -> None:
|
||||||
"""Tests the update-manpages code."""
|
"""Check that replace_regex works."""
|
||||||
|
data = "\n\033[1mSummary\033[m\n"
|
||||||
def test_replace_regex(self):
|
assert update_manpages.replace_regex(data) == "\nSummary\n"
|
||||||
"""Check that replace_regex works."""
|
|
||||||
data = "\n\033[1mSummary\033[m\n"
|
|
||||||
self.assertEqual(update_manpages.replace_regex(data), "\nSummary\n")
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user