mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-07-16 21:57:00 +00:00
Compare commits
312 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d54384a5e | |||
| 1b4e7a04be | |||
| a94c9e2b52 | |||
| 51021fb209 | |||
| d453273f06 | |||
| 0129ac1c46 | |||
| bf8c59f4a0 | |||
| 003f0407fc | |||
| 1db50e49fb | |||
| c97a306fe7 | |||
| 5534f164d6 | |||
| 67e52a120b | |||
| d5037230e9 | |||
| 7cc99b24c1 | |||
| 12ad396f67 | |||
| a6bc1b7cf0 | |||
| a98e422113 | |||
| 5d8585012f | |||
| e8deeabf2a | |||
| 11428ae984 | |||
| 27d2232eb3 | |||
| e3eadd3728 | |||
| 12cfc6036a | |||
| 134eeb024b | |||
| 03fb18109f | |||
| 5af71ce907 | |||
| baa281d99e | |||
| 7e9079b7cf | |||
| 61bd6b3ccb | |||
| 32e7327ca6 | |||
| 4f707ff91e | |||
| e2671c184e | |||
| b43a20b859 | |||
| 8869a30283 | |||
| 3b0eebeccf | |||
| 00991bfb42 | |||
| e8338b54bd | |||
| 951666fb23 | |||
| 854b330967 | |||
| 654690e1b8 | |||
| ac2be4c089 | |||
| 3d819e8e3e | |||
| 573983948a | |||
| 3f3c681a02 | |||
| 242e97d9dd | |||
| ade45de770 | |||
| 0251fb33c4 | |||
| 0176586544 | |||
| 582804a59e | |||
| afc3d55d39 | |||
| f24bc7aed5 | |||
| 83b8ebdbbe | |||
| a0abfd7339 | |||
| 403fedfeb5 | |||
| f14c577fce | |||
| 67881c0c3b | |||
| 551087cd98 | |||
| 8da56a0cc5 | |||
| 0f01cd24e9 | |||
| 1ee98667cc | |||
| 6f9622fe1c | |||
| 5cb0251248 | |||
| a214fd31bd | |||
| 62cd0de6cf | |||
| b60512a75a | |||
| 5d88972390 | |||
| 3c0e67bbc5 | |||
| 3b7b20ac1d | |||
| e71a8c6dd8 | |||
| c687b5df9e | |||
| 1dd9c57a28 | |||
| 4525c2e0ad | |||
| 45dcd738b7 | |||
| 1dad86dc00 | |||
| 622a5bf9c2 | |||
| 871e4c7ed1 | |||
| 5b0b5513d6 | |||
| b5991d7128 | |||
| 7f87c54043 | |||
| 50c6226075 | |||
| 1e4b2887a7 | |||
| 31b4b19387 | |||
| 2b6de52a36 | |||
| 91ec998598 | |||
| 08964a1658 | |||
| 3073a90046 | |||
| 75773b8b9d | |||
| 412367bfaf | |||
| 47c24b5c40 | |||
| be33106ffc | |||
| 5998c0b506 | |||
| 877ef91be2 | |||
| 4ab2284a94 | |||
| 1afe96a7e9 | |||
| 2719a8e203 | |||
| e4872ac8ba | |||
| 4623264809 | |||
| 67383bdba9 | |||
| d30414bb53 | |||
| 80d1a5ad3e | |||
| c615c964fb | |||
| 5ed12ec81d | |||
| 58a59fdfbc | |||
| 38d2fe11b9 | |||
| 854fe440f2 | |||
| d534a5537f | |||
| a64149a7a7 | |||
| 3e6acf2778 | |||
| a6e1a59ac1 | |||
| 380bf9546e | |||
| d9cc0a1526 | |||
| 8c3585f367 | |||
| 239fad7146 | |||
| d3eec0acdd | |||
| 7f7d70efe4 | |||
| 720bd1e96b | |||
| 25858c8b16 | |||
| 52bab0ba27 | |||
| 2e6d0881d9 | |||
| 74edacd8e5 | |||
| 5d95ba8d85 | |||
| 82d500eb7a | |||
| 21269c3eed | |||
| 99b5a17f2c | |||
| df3c4017f9 | |||
| f7a3f99dc9 | |||
| 6b8e9fc8db | |||
| 7b6ffed4ae | |||
| b4b323a8bd | |||
| f91f4462e6 | |||
| 85352825ff | |||
| b262d0e461 | |||
| 044e52e236 | |||
| 0cb88a8d79 | |||
| 08815ad3eb | |||
| 3c8bae27ec | |||
| 06338abe79 | |||
| 8d37f61471 | |||
| 1acbc14c34 | |||
| c448ba9cc7 | |||
| 21cbcc54e9 | |||
| 0f200bb3a1 | |||
| c8da28c3ed | |||
| c061593a12 | |||
| a94457d1ce | |||
| 97dc5c1bd9 | |||
| 0214730c9a | |||
| daebd6cbc2 | |||
| 3667de1d0f | |||
| 85ee1738e6 | |||
| f070331a4c | |||
| 9ecb80ba26 | |||
| dc8185f2a9 | |||
| 59b81c84de | |||
| 507d463600 | |||
| cd391e77d0 | |||
| 8310436be0 | |||
| d5087392ed | |||
| 91f428058d | |||
| 243df2042e | |||
| 4b94e773ef | |||
| fc901b92bb | |||
| 8d5f032611 | |||
| 99eca45eb2 | |||
| 66685f07ec | |||
| cf9a2a2a76 | |||
| 5ae8292fea | |||
| dfdf577e98 | |||
| 747ec83f58 | |||
| 1711bc23c0 | |||
| db111d3924 | |||
| 3405446a4e | |||
| 41a27eb854 | |||
| d93fe60e89 | |||
| 61224d01fa | |||
| 13d6588bf6 | |||
| 9500aca754 | |||
| e8a7b9d596 | |||
| cf411b3f03 | |||
| 1feecbd91e | |||
| 616e314902 | |||
| fafd1ec23e | |||
| b1613d741e | |||
| ab2d321104 | |||
| aada468916 | |||
| 1d5098617e | |||
| e219c78fe5 | |||
| f9f4df62e0 | |||
| ebdf0409d2 | |||
| 303bd963d5 | |||
| ae384f8623 | |||
| 70a4e643e6 | |||
| 8da4861b38 | |||
| 39ffd9977e | |||
| 584863fb5e | |||
| 454fdaf119 | |||
| f7f9dd4deb | |||
| 70ee4dd313 | |||
| cfe3095e50 | |||
| 621de7ed12 | |||
| d7ebdf56be | |||
| fabab4e245 | |||
| b577444a90 | |||
| 1e19f7dd61 | |||
| d8b4101eae | |||
| 1c53b0fa44 | |||
| e5ae870a2f | |||
| e59e2ae757 | |||
| c44ad09309 | |||
| 4592a63de5 | |||
| 0444ddf78e | |||
| 9bf8236c24 | |||
| 87f52f308c | |||
| 562cea7758 | |||
| eede374e3e | |||
| 2c5fb84d35 | |||
| 12f6dc49e9 | |||
| 5591d99ee2 | |||
| 9d865454aa | |||
| cbd78a9194 | |||
| 46819a78a1 | |||
| 159389f0da | |||
| 4406642e20 | |||
| 73356f1d5c | |||
| 09fc214a79 | |||
| 3762b17e98 | |||
| ae419e1e01 | |||
| a3a7372612 | |||
| fff1d2d74c | |||
| 4b01a242d8 | |||
| 46790229fc | |||
| edadb25c02 | |||
| 96edb9b573 | |||
| 5554572f02 | |||
| 97ca50f5f9 | |||
| 8896b68926 | |||
| fec8cd6704 | |||
| b8139bdcf8 | |||
| 26fa3180fb | |||
| d379e77f44 | |||
| 4217a82bec | |||
| 208f344950 | |||
| 138c8a9ff5 | |||
| 9b57aa00f6 | |||
| b1d1ece2fb | |||
| 449b23b698 | |||
| e5fb6e585f | |||
| 48e4137eba | |||
| 172c58398b | |||
| aa506db8a7 | |||
| 14c61d2c9d | |||
| 4c80921d22 | |||
| f56484c05b | |||
| a50c4e3bc0 | |||
| 0dd0a830b0 | |||
| 9f0ef5d926 | |||
| c287428b37 | |||
| c984e8d4f6 | |||
| 6d821124e0 | |||
| 560a79727f | |||
| 8a6d1724d9 | |||
| 3652b497bb | |||
| 89f761cfef | |||
| d32b2dcd15 | |||
| b32ccbb66b | |||
| b99272c601 | |||
| b0430b5bc5 | |||
| 1fd5c4bdf2 | |||
| 9267d58727 | |||
| ae824fb2fc | |||
| 034950b9ee | |||
| 0bcffd8656 | |||
| 7393f6bc41 | |||
| 8dd8521854 | |||
| 49c9b06838 | |||
| 3d58d219cb | |||
| c0aad7de18 | |||
| d4aee6570b | |||
| 024df06ec1 | |||
| 45809e51ca | |||
| 331c5dd3e7 | |||
| e848e9f72c | |||
| 1544afe460 | |||
| 3b8f9535c7 | |||
| 8f4f98582e | |||
| 8bc5000423 | |||
| 6a7f73bb9a | |||
| 23d063bdcd | |||
| ce0ed799b6 | |||
| 2844a5f3cc | |||
| 47944bbe2e | |||
| 83c66ec661 | |||
| 87058c6ca5 | |||
| b5644160b7 | |||
| aadd12cb08 | |||
| b8fd19215f | |||
| 7a1f1f70f0 | |||
| c993c5068e | |||
| c3d7c8536c | |||
| 880c621dc6 | |||
| da6ae1da8b | |||
| 5771897459 | |||
| 56a5a01c65 | |||
| e9cb391117 | |||
| 25d6c7cc10 | |||
| f19b310f15 | |||
| 712e62b9b0 | |||
| daf2ad38eb | |||
| b861511db9 | |||
| e914ec293a | |||
| 1e9f7b9e9e | |||
| 1dbf8b4346 |
@@ -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
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# GitHub actions workflow.
|
||||||
|
# https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions
|
||||||
|
|
||||||
|
# https://github.com/superbrothers/close-pull-request
|
||||||
|
name: Close Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: superbrothers/close-pull-request@v3
|
||||||
|
with:
|
||||||
|
comment: >
|
||||||
|
Thanks for your contribution!
|
||||||
|
Unfortunately, we don't use GitHub pull requests to manage code
|
||||||
|
contributions to this repository.
|
||||||
|
Instead, please see [README.md](../blob/HEAD/CONTRIBUTING.md)
|
||||||
|
which provides full instructions on how to get involved.
|
||||||
@@ -13,8 +13,9 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
# ubuntu-20.04 is the last version that supports python 3.6
|
||||||
python-version: ['3.6', '3.7', '3.8', '3.9', '3.10']
|
os: [ubuntu-20.04, macos-latest, windows-latest]
|
||||||
|
python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12']
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -26,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
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ __pycache__
|
|||||||
/dist
|
/dist
|
||||||
.repopickle_*
|
.repopickle_*
|
||||||
/repoc
|
/repoc
|
||||||
|
/.cipd_bin
|
||||||
/.tox
|
/.tox
|
||||||
/.venv
|
/.venv
|
||||||
|
|
||||||
|
|||||||
-41
@@ -1,41 +0,0 @@
|
|||||||
# Copyright 2023 The Android Open Source Project
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
# Config file for the isort python module.
|
|
||||||
# This is used to enforce import sorting standards.
|
|
||||||
#
|
|
||||||
# https://pycqa.github.io/isort/docs/configuration/options.html
|
|
||||||
|
|
||||||
[settings]
|
|
||||||
# Be compatible with `black` since it also matches what we want.
|
|
||||||
profile = black
|
|
||||||
|
|
||||||
line_length = 80
|
|
||||||
length_sort = false
|
|
||||||
force_single_line = true
|
|
||||||
lines_after_imports = 2
|
|
||||||
from_first = false
|
|
||||||
case_sensitive = false
|
|
||||||
force_sort_within_sections = true
|
|
||||||
order_by_type = false
|
|
||||||
|
|
||||||
# Ignore generated files.
|
|
||||||
extend_skip_glob = *_pb2.py
|
|
||||||
|
|
||||||
# Allow importing multiple classes on a single line from these modules.
|
|
||||||
# https://google.github.io/styleguide/pyguide#s2.2-imports
|
|
||||||
single_line_exclusions =
|
|
||||||
abc,
|
|
||||||
collections.abc,
|
|
||||||
typing,
|
|
||||||
@@ -5,6 +5,5 @@
|
|||||||
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
|
<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>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ However there are some differences, so please review and familiarize
|
|||||||
yourself with the following relevant bits.
|
yourself with the following relevant bits.
|
||||||
|
|
||||||
|
|
||||||
## Make separate commits for logically separate changes.
|
## Make separate commits for logically separate changes
|
||||||
|
|
||||||
Unless your patch is really trivial, you should not be sending out a patch that
|
Unless your patch is really trivial, you should not be sending out a patch that
|
||||||
was generated between your working tree and your commit head.
|
was generated between your working tree and your commit head.
|
||||||
@@ -43,17 +43,12 @@ probably need to split up your commit to finer grained pieces.
|
|||||||
|
|
||||||
Lint any changes by running:
|
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
|
||||||
@@ -127,7 +118,7 @@ your patch. It is virtually impossible to remove a patch once it
|
|||||||
has been applied and pushed out.
|
has been applied and pushed out.
|
||||||
|
|
||||||
|
|
||||||
## Sending your patches.
|
## Sending your patches
|
||||||
|
|
||||||
Do not email your patches to anyone.
|
Do not email your patches to anyone.
|
||||||
|
|
||||||
@@ -143,7 +134,7 @@ they have right to redistribute your work under the Apache License:
|
|||||||
|
|
||||||
Ensure you have obtained an HTTP password to authenticate:
|
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>
|
||||||
@@ -49,6 +49,43 @@ $ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.bin/repo
|
|||||||
$ chmod a+rx ~/.bin/repo
|
$ chmod a+rx ~/.bin/repo
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Shell Completion
|
||||||
|
|
||||||
|
Repo includes completion scripts for Bash and Zsh.
|
||||||
|
|
||||||
|
### Bash
|
||||||
|
|
||||||
|
To enable completion in Bash, source `completion.bash` in your `~/.bashrc`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
source /path/to/git-repo/completion.bash
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zsh
|
||||||
|
|
||||||
|
To enable completion in Zsh, you can either:
|
||||||
|
|
||||||
|
1. Copy or symlink `completion.zsh` to a file named `_repo` in a directory in your `$fpath`:
|
||||||
|
```sh
|
||||||
|
mkdir -p ~/.zsh/completion
|
||||||
|
# You can copy the file:
|
||||||
|
cp /path/to/git-repo/completion.zsh ~/.zsh/completion/_repo
|
||||||
|
# Or symlink it:
|
||||||
|
ln -s /path/to/git-repo/completion.zsh ~/.zsh/completion/_repo
|
||||||
|
```
|
||||||
|
Then add that directory to your `fpath` in `~/.zshrc` before `compinit`:
|
||||||
|
```zsh
|
||||||
|
fpath=(~/.zsh/completion $fpath)
|
||||||
|
autoload -Uz compinit
|
||||||
|
compinit
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Or source the file directly and call `compdef` in your `~/.zshrc`:
|
||||||
|
```zsh
|
||||||
|
source /path/to/git-repo/completion.zsh
|
||||||
|
compdef _repo repo
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
[new-bug]: https://issues.gerritcodereview.com/issues/new?component=1370071
|
[new-bug]: https://issues.gerritcodereview.com/issues/new?component=1370071
|
||||||
[issue tracker]: https://issues.gerritcodereview.com/issues?q=is:open%20componentid:1370071
|
[issue tracker]: https://issues.gerritcodereview.com/issues?q=is:open%20componentid:1370071
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Copyright (C) 2026 The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# This file contains version pins of a few tools used by run_tests.
|
||||||
|
|
||||||
|
# Pin resolved versions in the repo, to reduce trust in the CIPD backend.
|
||||||
|
#
|
||||||
|
# Most of these tools are generated via builders at
|
||||||
|
# https://ci.chromium.org/p/infra/g/infra/console
|
||||||
|
#
|
||||||
|
# For these, the git revision is the one of
|
||||||
|
# https://chromium.googlesource.com/infra/infra.git.
|
||||||
|
#
|
||||||
|
# To regenerate them (after modifying this file):
|
||||||
|
# cipd ensure-file-resolve -ensure-file cipd_manifest.txt
|
||||||
|
$ResolvedVersions cipd_manifest.versions
|
||||||
|
|
||||||
|
# Supported platforms. Feel free to add more as long as the tools work.
|
||||||
|
$VerifiedPlatform linux-amd64 linux-arm64 mac-amd64 mac-arm64
|
||||||
|
|
||||||
|
infra/3pp/tools/help2man/${platform} version:3@1.49.3
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# This file is auto-generated by 'cipd ensure-file-resolve'.
|
||||||
|
# Do not modify manually. All changes will be overwritten.
|
||||||
|
|
||||||
|
infra/3pp/tools/help2man/linux-amd64
|
||||||
|
version:3@1.49.3
|
||||||
|
82ySLC4--mvjnmuwj_j_2odHLj5zFaNFpWvfCyMunY0C
|
||||||
|
|
||||||
|
infra/3pp/tools/help2man/linux-arm64
|
||||||
|
version:3@1.49.3
|
||||||
|
szfWfjI4RyCT6gIB48CeUOGG6_Un4wfbIb0Vvs5M9MUC
|
||||||
|
|
||||||
|
infra/3pp/tools/help2man/mac-amd64
|
||||||
|
version:3@1.49.3
|
||||||
|
t7iyQx5TnGbuJfj6zMTvjPdBl-Cb_w5papUh80NI2S8C
|
||||||
|
|
||||||
|
infra/3pp/tools/help2man/mac-arm64
|
||||||
|
version:3@1.49.3
|
||||||
|
t5eD-eLKRpnSifZdbi072U8bnPfGye1pIMmQ07nxJ7kC
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import pager
|
import pager
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ def _Color(fg=None, bg=None, attr=None):
|
|||||||
DEFAULT = None
|
DEFAULT = None
|
||||||
|
|
||||||
|
|
||||||
def SetDefaultColoring(state):
|
def SetDefaultColoring(state: Optional[str]) -> None:
|
||||||
"""Set coloring behavior to |state|.
|
"""Set coloring behavior to |state|.
|
||||||
|
|
||||||
This is useful for overriding config options via the command line.
|
This is useful for overriding config options via the command line.
|
||||||
@@ -97,13 +98,13 @@ def SetDefaultColoring(state):
|
|||||||
state = state.lower()
|
state = state.lower()
|
||||||
if state in ("auto",):
|
if state in ("auto",):
|
||||||
DEFAULT = state
|
DEFAULT = state
|
||||||
elif state in ("always", "yes", "true", True):
|
elif state in ("always", "yes", "true"):
|
||||||
DEFAULT = "always"
|
DEFAULT = "always"
|
||||||
elif state in ("never", "no", "false", False):
|
elif state in ("never", "no", "false"):
|
||||||
DEFAULT = "never"
|
DEFAULT = "never"
|
||||||
|
|
||||||
|
|
||||||
class Coloring(object):
|
class Coloring:
|
||||||
def __init__(self, config, section_type):
|
def __init__(self, config, section_type):
|
||||||
self._section = "color.%s" % section_type
|
self._section = "color.%s" % section_type
|
||||||
self._config = config
|
self._config = config
|
||||||
@@ -194,7 +195,7 @@ class Coloring(object):
|
|||||||
if not opt:
|
if not opt:
|
||||||
return _Color(fg, bg, attr)
|
return _Color(fg, bg, attr)
|
||||||
|
|
||||||
v = self._config.GetString("%s.%s" % (self._section, opt))
|
v = self._config.GetString(f"{self._section}.{opt}")
|
||||||
if v is None:
|
if v is None:
|
||||||
return _Color(fg, bg, attr)
|
return _Color(fg, bg, attr)
|
||||||
|
|
||||||
@@ -210,6 +211,7 @@ class Coloring(object):
|
|||||||
if have_fg:
|
if have_fg:
|
||||||
bg = a
|
bg = a
|
||||||
else:
|
else:
|
||||||
|
have_fg = True
|
||||||
fg = a
|
fg = a
|
||||||
elif is_attr(a):
|
elif is_attr(a):
|
||||||
attr = a
|
attr = a
|
||||||
|
|||||||
+78
-15
@@ -12,6 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import optparse
|
import optparse
|
||||||
import os
|
import os
|
||||||
@@ -46,7 +47,7 @@ class UsageError(RepoExitError):
|
|||||||
"""Exception thrown with invalid command usage."""
|
"""Exception thrown with invalid command usage."""
|
||||||
|
|
||||||
|
|
||||||
class Command(object):
|
class Command:
|
||||||
"""Base class for any command line action in repo."""
|
"""Base class for any command line action in repo."""
|
||||||
|
|
||||||
# Singleton for all commands to track overall repo command execution and
|
# Singleton for all commands to track overall repo command execution and
|
||||||
@@ -70,6 +71,14 @@ class Command(object):
|
|||||||
# migrated subcommands can set it to False.
|
# migrated subcommands can set it to False.
|
||||||
MULTI_MANIFEST_SUPPORT = True
|
MULTI_MANIFEST_SUPPORT = True
|
||||||
|
|
||||||
|
# Shared data across parallel execution workers.
|
||||||
|
_parallel_context = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_parallel_context(cls):
|
||||||
|
assert cls._parallel_context is not None
|
||||||
|
return cls._parallel_context
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
repodir=None,
|
repodir=None,
|
||||||
@@ -92,6 +101,11 @@ class Command(object):
|
|||||||
def WantPager(self, _opt):
|
def WantPager(self, _opt):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_multiprocessing_active() -> bool:
|
||||||
|
"""Whether the current process is a worker in a pool."""
|
||||||
|
return multiprocessing.current_process().name != "MainProcess"
|
||||||
|
|
||||||
def ReadEnvironmentOptions(self, opts):
|
def ReadEnvironmentOptions(self, opts):
|
||||||
"""Set options from environment variables."""
|
"""Set options from environment variables."""
|
||||||
|
|
||||||
@@ -242,9 +256,39 @@ class Command(object):
|
|||||||
"""Perform the action, after option parsing is complete."""
|
"""Perform the action, after option parsing is complete."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def ParallelContext(cls):
|
||||||
|
"""Obtains the context, which is shared to ExecuteInParallel workers.
|
||||||
|
|
||||||
|
Callers can store data in the context dict before invocation of
|
||||||
|
ExecuteInParallel. The dict will then be shared to child workers of
|
||||||
|
ExecuteInParallel.
|
||||||
|
"""
|
||||||
|
assert cls._parallel_context is None
|
||||||
|
cls._parallel_context = {}
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
cls._parallel_context = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _InitParallelWorker(cls, context, initializer):
|
||||||
|
cls._parallel_context = context
|
||||||
|
if initializer:
|
||||||
|
initializer()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
def ExecuteInParallel(
|
def ExecuteInParallel(
|
||||||
jobs, func, inputs, callback, output=None, ordered=False
|
cls,
|
||||||
|
jobs,
|
||||||
|
func,
|
||||||
|
inputs,
|
||||||
|
callback,
|
||||||
|
output=None,
|
||||||
|
ordered=False,
|
||||||
|
chunksize=WORKER_BATCH_SIZE,
|
||||||
|
initializer=None,
|
||||||
):
|
):
|
||||||
"""Helper for managing parallel execution boiler plate.
|
"""Helper for managing parallel execution boiler plate.
|
||||||
|
|
||||||
@@ -269,6 +313,9 @@ class Command(object):
|
|||||||
output: An output manager. May be progress.Progess or
|
output: An output manager. May be progress.Progess or
|
||||||
color.Coloring.
|
color.Coloring.
|
||||||
ordered: Whether the jobs should be processed in order.
|
ordered: Whether the jobs should be processed in order.
|
||||||
|
chunksize: The number of jobs processed in batch by parallel
|
||||||
|
workers.
|
||||||
|
initializer: Worker initializer.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The |callback| function's results are returned.
|
The |callback| function's results are returned.
|
||||||
@@ -278,19 +325,23 @@ class Command(object):
|
|||||||
if len(inputs) == 1 or jobs == 1:
|
if len(inputs) == 1 or jobs == 1:
|
||||||
return callback(None, output, (func(x) for x in inputs))
|
return callback(None, output, (func(x) for x in inputs))
|
||||||
else:
|
else:
|
||||||
with multiprocessing.Pool(jobs) as pool:
|
with multiprocessing.Pool(
|
||||||
|
jobs,
|
||||||
|
initializer=cls._InitParallelWorker,
|
||||||
|
initargs=(cls._parallel_context, initializer),
|
||||||
|
) as pool:
|
||||||
submit = pool.imap if ordered else pool.imap_unordered
|
submit = pool.imap if ordered else pool.imap_unordered
|
||||||
return callback(
|
return callback(
|
||||||
pool,
|
pool,
|
||||||
output,
|
output,
|
||||||
submit(func, inputs, chunksize=WORKER_BATCH_SIZE),
|
submit(func, inputs, chunksize=chunksize),
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
if isinstance(output, progress.Progress):
|
if isinstance(output, progress.Progress):
|
||||||
output.end()
|
output.end()
|
||||||
|
|
||||||
def _ResetPathToProjectMap(self, projects):
|
def _ResetPathToProjectMap(self, projects):
|
||||||
self._by_path = dict((p.worktree, p) for p in projects)
|
self._by_path = {p.worktree: p for p in projects}
|
||||||
|
|
||||||
def _UpdatePathToProjectMap(self, project):
|
def _UpdatePathToProjectMap(self, project):
|
||||||
self._by_path[project.worktree] = project
|
self._by_path[project.worktree] = project
|
||||||
@@ -353,7 +404,7 @@ class Command(object):
|
|||||||
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:
|
||||||
@@ -361,7 +412,8 @@ class Command(object):
|
|||||||
for project in all_projects_list:
|
for project in all_projects_list:
|
||||||
if submodules_ok or project.sync_s:
|
if submodules_ok or project.sync_s:
|
||||||
derived_projects.update(
|
derived_projects.update(
|
||||||
(p.name, p) for p in project.GetDerivedSubprojects()
|
(p.RelPath(local=False), p)
|
||||||
|
for p in project.GetDerivedSubprojects()
|
||||||
)
|
)
|
||||||
all_projects_list.extend(derived_projects.values())
|
all_projects_list.extend(derived_projects.values())
|
||||||
for project in all_projects_list:
|
for project in all_projects_list:
|
||||||
@@ -465,6 +517,22 @@ class Command(object):
|
|||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def GetSmartSyncOverridePath(self, manifest=None) -> str:
|
||||||
|
"""Return the path where smart sync writes its override manifest."""
|
||||||
|
if manifest is None:
|
||||||
|
manifest = self.manifest
|
||||||
|
return os.path.join(
|
||||||
|
manifest.manifestProject.worktree, "smart_sync_override.xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
def TryOverrideManifestWithSmartSync(self, manifest=None) -> None:
|
||||||
|
"""Override manifest with smart_sync_override.xml if it exists."""
|
||||||
|
if manifest is None:
|
||||||
|
manifest = self.manifest
|
||||||
|
smart_sync_manifest_path = self.GetSmartSyncOverridePath(manifest)
|
||||||
|
if os.path.isfile(smart_sync_manifest_path):
|
||||||
|
manifest.Override(smart_sync_manifest_path)
|
||||||
|
|
||||||
def ManifestList(self, opt):
|
def ManifestList(self, opt):
|
||||||
"""Yields all of the manifests to traverse.
|
"""Yields all of the manifests to traverse.
|
||||||
|
|
||||||
@@ -476,8 +544,7 @@ class Command(object):
|
|||||||
top = self.manifest
|
top = self.manifest
|
||||||
yield top
|
yield top
|
||||||
if not opt.this_manifest_only:
|
if not opt.this_manifest_only:
|
||||||
for child in top.all_children:
|
yield from top.all_children
|
||||||
yield child
|
|
||||||
|
|
||||||
|
|
||||||
class InteractiveCommand(Command):
|
class InteractiveCommand(Command):
|
||||||
@@ -498,11 +565,7 @@ class PagedCommand(Command):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class MirrorSafeCommand(object):
|
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(object):
|
|
||||||
"""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.
|
||||||
|
|||||||
+449
@@ -0,0 +1,449 @@
|
|||||||
|
#compdef _repo repo
|
||||||
|
|
||||||
|
# Copyright (C) 2026 The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
_repo() {
|
||||||
|
local context state line
|
||||||
|
typeset -A opt_args
|
||||||
|
|
||||||
|
local -a subcommands
|
||||||
|
subcommands=(
|
||||||
|
'abandon:Abandon a development branch'
|
||||||
|
'branches:View current topic branches'
|
||||||
|
'checkout:Checkout a branch'
|
||||||
|
'cherry-pick:Cherry-pick a change'
|
||||||
|
'diff:Show changes between commit and working tree'
|
||||||
|
'diffmanifests:Show differences between two manifests'
|
||||||
|
'download:Download and find a change'
|
||||||
|
'forall:Run a shell command in each project'
|
||||||
|
'gc:Garbage collect'
|
||||||
|
'grep:Print lines matching a pattern'
|
||||||
|
'help:Display help about a command'
|
||||||
|
'info:Get info about the manifest'
|
||||||
|
'init:Initialize a repo client'
|
||||||
|
'list:List projects and their names'
|
||||||
|
'manifest:Manifest management'
|
||||||
|
'overview:Display overview of topic branches'
|
||||||
|
'prune:Prune topic branches'
|
||||||
|
'rebase:Rebase local branches'
|
||||||
|
'selfupdate:Update repo'
|
||||||
|
'smartsync:Update working tree to latest known good revision'
|
||||||
|
'stage:Stage file(s) for commit'
|
||||||
|
'start:Start a new branch for development'
|
||||||
|
'status:Show the working tree status'
|
||||||
|
'sync:Update working tree'
|
||||||
|
'upload:Upload changes to the review server'
|
||||||
|
'version:Display version'
|
||||||
|
'wipe:Wipe uncommitted changes'
|
||||||
|
)
|
||||||
|
|
||||||
|
local -a global_opts
|
||||||
|
global_opts=(
|
||||||
|
'(-h --help)'{-h,--help}'[Show help message]'
|
||||||
|
'--help-all[Show help message for all commands]'
|
||||||
|
'(-p --paginate)'{-p,--paginate}'[Pipe all output into less]'
|
||||||
|
'--no-pager[Do not pipe output into a pager]'
|
||||||
|
'--color=[Control color output]:color:(always never auto)'
|
||||||
|
'--trace[Trace git command execution]'
|
||||||
|
'--trace-to-stderr[Trace git command execution to stderr]'
|
||||||
|
'--trace-python[Trace python execution]'
|
||||||
|
'--time[Time execute of command]'
|
||||||
|
'--version[Show version]'
|
||||||
|
'--show-toplevel[Show top level of workspace]'
|
||||||
|
'--event-log=[File to log events to]:file:_files'
|
||||||
|
'--git-trace2-event-log=[File to log git trace2 events to]:file:_files'
|
||||||
|
'--submanifest-path=[Path to submanifest]:path:_files'
|
||||||
|
)
|
||||||
|
|
||||||
|
_arguments -C \
|
||||||
|
${global_opts} \
|
||||||
|
'1: :->cmds' \
|
||||||
|
'*:: :->subcmds'
|
||||||
|
|
||||||
|
case ${state} in
|
||||||
|
cmds)
|
||||||
|
_describe -t commands 'repo command' subcommands
|
||||||
|
;;
|
||||||
|
subcmds)
|
||||||
|
local cmd=${words[1]}
|
||||||
|
curcontext="${curcontext%:*}-${cmd}:"
|
||||||
|
|
||||||
|
local -a common_opts
|
||||||
|
common_opts=(
|
||||||
|
'(-h --help)'{-h,--help}'[Show help message]'
|
||||||
|
'(-v --verbose)'{-v,--verbose}'[Show verbose messages]'
|
||||||
|
'(-q --quiet)'{-q,--quiet}'[Show only errors]'
|
||||||
|
'(-j --jobs)'{-j,--jobs=}'[Number of jobs to run in parallel]:jobs:'
|
||||||
|
'--outer-manifest[Use outer manifest]'
|
||||||
|
'--no-outer-manifest[Do not use outer manifest]'
|
||||||
|
'--this-manifest-only[Only use this manifest]'
|
||||||
|
'--no-this-manifest-only[Do not only use this manifest]'
|
||||||
|
'--all-manifests[Use all manifests]'
|
||||||
|
)
|
||||||
|
|
||||||
|
case ${cmd} in
|
||||||
|
abandon)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'--all[Abandon all branches]' \
|
||||||
|
'1: :->branch' \
|
||||||
|
'*: :->project'
|
||||||
|
;;
|
||||||
|
branches)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'*: :->project'
|
||||||
|
;;
|
||||||
|
checkout)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'1: :->branch' \
|
||||||
|
'*: :->project'
|
||||||
|
;;
|
||||||
|
cherry-pick)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'1: :_message "sha1"'
|
||||||
|
;;
|
||||||
|
diff)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'(-u --absolute)'{-u,--absolute}'[Show absolute paths]' \
|
||||||
|
'*: :->project'
|
||||||
|
;;
|
||||||
|
diffmanifests)
|
||||||
|
_arguments \
|
||||||
|
'--raw[Show raw diff]' \
|
||||||
|
'--no-color[Do not show color]' \
|
||||||
|
'--pretty-format=[Pretty format]:format:' \
|
||||||
|
'1: :_files -g "*.xml"' \
|
||||||
|
'2: :_files -g "*.xml"'
|
||||||
|
;;
|
||||||
|
download)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'(-b --branch)'{-b,--branch=}'[One or more branches to check]:branch:' \
|
||||||
|
'(-c --cherry-pick)'{-c,--cherry-pick}'[Cherry-pick the change]' \
|
||||||
|
'(-x --record-origin)'{-x,--record-origin}'[Record origin of cherry-pick]' \
|
||||||
|
'(-r --revert)'{-r,--revert}'[Revert the change]' \
|
||||||
|
'(-f --ff-only)'{-f,--ff-only}'[Force fast-forward]' \
|
||||||
|
'*: :_message "change[/patchset]"'
|
||||||
|
;;
|
||||||
|
forall)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'(-r --regex)'{-r,--regex}'[Execute command only on projects matching regex]' \
|
||||||
|
'(-i --inverse-regex)'{-i,--inverse-regex}'[Execute command only on projects not matching regex]' \
|
||||||
|
'(-g --groups)'{-g,--groups=}'[Execute command only on projects matching groups]:groups:' \
|
||||||
|
'(-c --command)'{-c,--command}'[Command to execute]' \
|
||||||
|
'(-e --abort-on-errors)'{-e,--abort-on-errors}'[Abort on errors]' \
|
||||||
|
'--ignore-missing[Ignore missing projects]' \
|
||||||
|
'--interactive[Run interactively]' \
|
||||||
|
'-p[Show project headers]' \
|
||||||
|
'*: :->project'
|
||||||
|
;;
|
||||||
|
gc)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'(-n --dry-run)'{-n,--dry-run}'[Dry run]' \
|
||||||
|
'(-y --yes)'{-y,--yes}'[Answer yes to all prompts]' \
|
||||||
|
'--repack[Repack objects]'
|
||||||
|
;;
|
||||||
|
grep)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'--cached[Search cached files]' \
|
||||||
|
'(-r --revision)'{-r,--revision=}'[Search in revision]:revision:' \
|
||||||
|
'-e[Pattern]' \
|
||||||
|
'(-i --ignore-case)'{-i,--ignore-case}'[Ignore case]' \
|
||||||
|
'(-a --text)'{-a,--text}'[Treat all files as text]' \
|
||||||
|
'-I[Do not match binary files]' \
|
||||||
|
'(-w --word-regexp)'{-w,--word-regexp}'[Match word boundaries]' \
|
||||||
|
'(-v --invert-match)'{-v,--invert-match}'[Invert match]' \
|
||||||
|
'(-G --basic-regexp)'{-G,--basic-regexp}'[Basic regexp]' \
|
||||||
|
'(-E --extended-regexp)'{-E,--extended-regexp}'[Extended regexp]' \
|
||||||
|
'(-F --fixed-strings)'{-F,--fixed-strings}'[Fixed strings]' \
|
||||||
|
'--all-match[All match]' \
|
||||||
|
'--and[And]' \
|
||||||
|
'--or[Or]' \
|
||||||
|
'--not[Not]' \
|
||||||
|
'-([Open paren]' \
|
||||||
|
'-)[Close paren]' \
|
||||||
|
'-n[Line number]' \
|
||||||
|
'-C[Context]:lines:' \
|
||||||
|
'-B[Before context]:lines:' \
|
||||||
|
'-A[After context]:lines:' \
|
||||||
|
'-l[Name only]' \
|
||||||
|
'--name-only[Name only]' \
|
||||||
|
'--files-with-matches[Files with matches]' \
|
||||||
|
'-L[Files without match]' \
|
||||||
|
'--files-without-match[Files without match]' \
|
||||||
|
'1: :->pattern' \
|
||||||
|
'*: :->project'
|
||||||
|
;;
|
||||||
|
help)
|
||||||
|
_arguments \
|
||||||
|
'(-a --all)'{-a,--all}'[Show all commands]' \
|
||||||
|
'--help-all[Show help message for all commands]' \
|
||||||
|
'1: :->help_cmds'
|
||||||
|
;;
|
||||||
|
info)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'(-d --diff)'{-d,--diff}'[Show diff]' \
|
||||||
|
'(-o --overview)'{-o,--overview}'[Show overview]' \
|
||||||
|
'(-c --current-branch)'{-c,--current-branch}'[Show current branch]' \
|
||||||
|
'--no-current-branch[Do not show current branch]' \
|
||||||
|
'(-l --local-only)'{-l,--local-only}'[Local only]' \
|
||||||
|
'*: :->project'
|
||||||
|
;;
|
||||||
|
init)
|
||||||
|
_arguments \
|
||||||
|
'(-u --manifest-url)'{-u,--manifest-url=}'[Manifest URL]:url:' \
|
||||||
|
'(-b --manifest-branch)'{-b,--manifest-branch=}'[Manifest branch]:branch:' \
|
||||||
|
'--manifest-upstream-branch=[Manifest upstream branch]:branch:' \
|
||||||
|
'(-m --manifest-name)'{-m,--manifest-name=}'[Manifest name]:file:_files -g "*.xml"' \
|
||||||
|
'(-g --groups)'{-g,--groups=}'[Restrict manifest projects to groups]:groups:' \
|
||||||
|
'(-p --platform)'{-p,--platform=}'[Restrict manifest projects to platform]:platform:' \
|
||||||
|
'--submodules[Sync submodules]' \
|
||||||
|
'--standalone-manifest[Standalone manifest]' \
|
||||||
|
'--manifest-depth=[Manifest depth]:depth:' \
|
||||||
|
'(-c --current-branch)'{-c,--current-branch}'[Sync current branch only]' \
|
||||||
|
'--no-current-branch[Do not sync current branch only]' \
|
||||||
|
'--tags[Sync tags]' \
|
||||||
|
'--no-tags[Do not sync tags]' \
|
||||||
|
'--mirror[Mirror]' \
|
||||||
|
'--archive[Archive]' \
|
||||||
|
'--worktree[Worktree]' \
|
||||||
|
'--reference=[Reference repository]:repository:_files -/' \
|
||||||
|
'--dissociate[Dissociate from reference]' \
|
||||||
|
'--depth=[Depth]:depth:' \
|
||||||
|
'--partial-clone[Partial clone]' \
|
||||||
|
'--no-partial-clone[Do not partial clone]' \
|
||||||
|
'--partial-clone-exclude=[Exclude from partial clone]:projects:' \
|
||||||
|
'--clone-filter=[Clone filter]:filter:' \
|
||||||
|
'--use-superproject[Use superproject]' \
|
||||||
|
'--no-use-superproject[Do not use superproject]' \
|
||||||
|
'--clone-bundle[Use clone bundle]' \
|
||||||
|
'--no-clone-bundle[Do not use clone bundle]' \
|
||||||
|
'--git-lfs[Use git lfs]' \
|
||||||
|
'--no-git-lfs[Do not use git lfs]' \
|
||||||
|
'--repo-url=[Repo URL]:url:' \
|
||||||
|
'--repo-rev=[Repo revision]:revision:' \
|
||||||
|
'--no-repo-verify[Do not verify repo]' \
|
||||||
|
'--config-name[Use config name]'
|
||||||
|
;;
|
||||||
|
list)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'(-r --regex)'{-r,--regex}'[Filter by regex]' \
|
||||||
|
'(-g --groups)'{-g,--groups=}'[Filter by groups]:groups:' \
|
||||||
|
'(-a --all)'{-a,--all}'[Show all projects]' \
|
||||||
|
'(-n --name-only)'{-n,--name-only}'[Show only names]' \
|
||||||
|
'(-p --path-only)'{-p,--path-only}'[Show only paths]' \
|
||||||
|
'(-f --fullpath)'{-f,--fullpath}'[Show full paths]' \
|
||||||
|
'--relative-to=[Show paths relative to]:path:_files -/' \
|
||||||
|
'*: :->project'
|
||||||
|
;;
|
||||||
|
manifest)
|
||||||
|
_arguments \
|
||||||
|
'(-r --revision-as-HEAD)'{-r,--revision-as-HEAD}'[Save revisions as current HEAD]' \
|
||||||
|
'(-m --manifest-name)'{-m,--manifest-name=}'[Manifest name]:file:_files -g "*.xml"' \
|
||||||
|
'--suppress-upstream-revision[Suppress upstream revision]' \
|
||||||
|
'--suppress-dest-branch[Suppress dest branch]' \
|
||||||
|
'--format=[Output format]:format:(xml json)' \
|
||||||
|
'--pretty[Pretty print]' \
|
||||||
|
'--no-local-manifests[Ignore local manifests]' \
|
||||||
|
'(-o --output-file)'{-o,--output-file=}'[Output file]:file:_files'
|
||||||
|
;;
|
||||||
|
overview)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'(-c --current-branch)'{-c,--current-branch}'[Show current branch]' \
|
||||||
|
'--no-current-branch[Do not show current branch]' \
|
||||||
|
'*: :->project'
|
||||||
|
;;
|
||||||
|
prune)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'*: :->project'
|
||||||
|
;;
|
||||||
|
rebase)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'--fail-fast[Fail fast]' \
|
||||||
|
'(-f --force-rebase)'{-f,--force-rebase}'[Force rebase]' \
|
||||||
|
'--no-ff[No fast forward]' \
|
||||||
|
'--autosquash[Autosquash]' \
|
||||||
|
'--whitespace=[Whitespace option]:option:' \
|
||||||
|
'--auto-stash[Auto stash]' \
|
||||||
|
'(-m --onto-manifest)'{-m,--onto-manifest}'[Rebase onto manifest]' \
|
||||||
|
'(-i --interactive)'{-i,--interactive}'[Interactive rebase]' \
|
||||||
|
'*: :->project'
|
||||||
|
;;
|
||||||
|
selfupdate)
|
||||||
|
_arguments \
|
||||||
|
'--no-repo-verify[Do not verify repo]'
|
||||||
|
;;
|
||||||
|
smartsync | sync)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'--jobs-network=[Number of network jobs]:jobs:' \
|
||||||
|
'--jobs-checkout=[Number of checkout jobs]:jobs:' \
|
||||||
|
'(-f --force-broken)'{-f,--force-broken}'[Continue sync even if a project fails]' \
|
||||||
|
'--fail-fast[Fail fast]' \
|
||||||
|
'--force-sync[Overwrite existing git directories]' \
|
||||||
|
'--force-checkout[Force checkout]' \
|
||||||
|
'--force-remove-dirty[Force remove dirty]' \
|
||||||
|
'--rebase[Rebase local branches]' \
|
||||||
|
'(-l --local-only)'{-l,--local-only}'[Only use local files]' \
|
||||||
|
'--no-manifest-update[Do not update manifest]' \
|
||||||
|
'--nmu[Do not update manifest]' \
|
||||||
|
'--interleaved[Interleave output]' \
|
||||||
|
'--no-interleaved[Do not interleave output]' \
|
||||||
|
'(-n --network-only)'{-n,--network-only}'[Only fetch]' \
|
||||||
|
'(-d --detach)'{-d,--detach}'[Detach HEAD]' \
|
||||||
|
'(-c --current-branch)'{-c,--current-branch}'[Fetch only current branch]' \
|
||||||
|
'--no-current-branch[Do not fetch only current branch]' \
|
||||||
|
'(-m --manifest-name)'{-m,--manifest-name=}'[Manifest name]:file:_files -g "*.xml"' \
|
||||||
|
'--clone-bundle[Use clone bundle]' \
|
||||||
|
'--no-clone-bundle[Do not use clone bundle]' \
|
||||||
|
'(-u --manifest-server-username)'{-u,--manifest-server-username=}'[Username for manifest server]:username:' \
|
||||||
|
'(-p --manifest-server-password)'{-p,--manifest-server-password=}'[Password for manifest server]:password:' \
|
||||||
|
'--fetch-submodules[Fetch submodules]' \
|
||||||
|
'--use-superproject[Use superproject]' \
|
||||||
|
'--no-use-superproject[Do not use superproject]' \
|
||||||
|
'--tags[Sync tags]' \
|
||||||
|
'--no-tags[Do not sync tags]' \
|
||||||
|
'--optimized-fetch[Optimized fetch]' \
|
||||||
|
'--retry-fetches=[Retry fetches]:count:' \
|
||||||
|
'--prune[Prune]' \
|
||||||
|
'--no-prune[Do not prune]' \
|
||||||
|
'--auto-gc[Run auto gc]' \
|
||||||
|
'--no-auto-gc[Do not run auto gc]' \
|
||||||
|
'(-s --smart-sync)'{-s,--smart-sync}'[Smart sync]' \
|
||||||
|
'(-t --smart-tag)'{-t,--smart-tag=}'[Smart tag]:tag:' \
|
||||||
|
'--no-repo-verify[Do not verify repo]' \
|
||||||
|
'--no-verify[Do not verify]' \
|
||||||
|
'--verify[Verify]' \
|
||||||
|
'--ignore-hooks[Ignore hooks]' \
|
||||||
|
'*: :->project'
|
||||||
|
;;
|
||||||
|
stage)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'(-i --interactive)'{-i,--interactive}'[Interactive staging]' \
|
||||||
|
'*: :->project'
|
||||||
|
;;
|
||||||
|
start)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'--all[Start branch in all projects]' \
|
||||||
|
'(-r --rev --revision)'{-r,--rev=,--revision=}'[Revision]:revision:' \
|
||||||
|
'--head[Head]' \
|
||||||
|
'--HEAD[Head]' \
|
||||||
|
'1: :->newbranch' \
|
||||||
|
'*: :->project'
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'(-o --orphans)'{-o,--orphans}'[Show orphans]' \
|
||||||
|
'*: :->project'
|
||||||
|
;;
|
||||||
|
upload)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'(-t --topic-branch)'{-t,--topic-branch}'[Topic branch]' \
|
||||||
|
'--topic=[Topic]:topic:' \
|
||||||
|
'--hashtag=[Hashtag]:hashtag:' \
|
||||||
|
'--ht=[Hashtag]:hashtag:' \
|
||||||
|
'--hashtag-branch[Hashtag branch]' \
|
||||||
|
'--htb[Hashtag branch]' \
|
||||||
|
'(-l --label)'{-l,--label=}'[Label]:label:' \
|
||||||
|
'--pd=[Patchset description]:description:' \
|
||||||
|
'--patchset-description=[Patchset description]:description:' \
|
||||||
|
'--re=[Reviewers]:reviewers:' \
|
||||||
|
'--reviewers=[Reviewers]:reviewers:' \
|
||||||
|
'--cc=[CC]:cc:' \
|
||||||
|
'--br=[Branch]:branch:' \
|
||||||
|
'--branch=[Branch]:branch:' \
|
||||||
|
'(-c --current-branch)'{-c,--current-branch}'[Upload current branch]' \
|
||||||
|
'--no-current-branch[Do not upload current branch]' \
|
||||||
|
'--ne[No emails]' \
|
||||||
|
'--no-emails[No emails]' \
|
||||||
|
'(-p --private)'{-p,--private}'[Private]' \
|
||||||
|
'(-w --wip)'{-w,--wip}'[Work in progress]' \
|
||||||
|
'(-r --ready)'{-r,--ready}'[Ready]' \
|
||||||
|
'(-o --push-option)'{-o,--push-option=}'[Push option]:option:' \
|
||||||
|
'(-D --destination --dest)'{-D,--destination=,--dest=}'[Destination]:destination:' \
|
||||||
|
'(-n --dry-run)'{-n,--dry-run}'[Dry run]' \
|
||||||
|
'(-y --yes)'{-y,--yes}'[Answer yes to all prompts]' \
|
||||||
|
'--ignore-untracked-files[Ignore untracked files]' \
|
||||||
|
'--no-ignore-untracked-files[Do not ignore untracked files]' \
|
||||||
|
'--no-cert-checks[Do not check certificates]' \
|
||||||
|
'--no-verify[Do not verify]' \
|
||||||
|
'--verify[Verify]' \
|
||||||
|
'--ignore-hooks[Ignore hooks]' \
|
||||||
|
'*: :->project'
|
||||||
|
;;
|
||||||
|
version)
|
||||||
|
_arguments ${common_opts}
|
||||||
|
;;
|
||||||
|
wipe)
|
||||||
|
_arguments \
|
||||||
|
${common_opts} \
|
||||||
|
'(-f --force)'{-f,--force}'[Force wipe]' \
|
||||||
|
'--force-uncommitted[Force uncommitted]' \
|
||||||
|
'--force-shared[Force shared]' \
|
||||||
|
'*: :->project'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Handle states for positional arguments.
|
||||||
|
case ${state} in
|
||||||
|
branch)
|
||||||
|
[[ ${PREFIX} != -* ]] && _repo_branches
|
||||||
|
;;
|
||||||
|
project)
|
||||||
|
[[ ${PREFIX} != -* ]] && _repo_projects
|
||||||
|
;;
|
||||||
|
pattern)
|
||||||
|
_message 'pattern'
|
||||||
|
;;
|
||||||
|
newbranch)
|
||||||
|
_message 'new branch name'
|
||||||
|
;;
|
||||||
|
help_cmds)
|
||||||
|
_describe -t commands 'repo command' subcommands
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
_repo_branches() {
|
||||||
|
local -a branches
|
||||||
|
branches=(${(f)"$(_call_program branches repo branches 2>/dev/null | sed -E 's/^.*[[:space:]]([^[:space:]]+)[[:space:]]+\|.*$/\1/' | awk '{print $1}')"})
|
||||||
|
_describe -t branches 'branch' branches
|
||||||
|
}
|
||||||
|
|
||||||
|
_repo_projects() {
|
||||||
|
local -a projects
|
||||||
|
projects=(${(f)"$(_call_program projects repo list -n 2>/dev/null)"})
|
||||||
|
_describe -t projects 'project' projects
|
||||||
|
}
|
||||||
|
|
||||||
|
_repo "$@"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# NB: Keep in sync with run_tests.vpython3.
|
||||||
|
black<26
|
||||||
@@ -141,7 +141,7 @@ Instead, you should use standard Git workflows like [git worktree] or
|
|||||||
(e.g. a local mirror & a public review server) while avoiding duplicating
|
(e.g. a local mirror & a public review server) while avoiding duplicating
|
||||||
the content. However, this can run into problems if different remotes use
|
the content. However, this can run into problems if different remotes use
|
||||||
the same path on their respective servers. Best to avoid that.
|
the same path on their respective servers. Best to avoid that.
|
||||||
* `subprojects/`: Like `projects/`, but for git submodules.
|
* `modules/`: Like `projects/`, but for git submodules.
|
||||||
* `subproject-objects/`: Like `project-objects/`, but for git submodules.
|
* `subproject-objects/`: Like `project-objects/`, but for git submodules.
|
||||||
* `worktrees/`: Bare checkouts of every project synced by the manifest. The
|
* `worktrees/`: Bare checkouts of every project synced by the manifest. The
|
||||||
filesystem layout matches the `<project name=...` setting in the manifest
|
filesystem layout matches the `<project name=...` setting in the manifest
|
||||||
|
|||||||
+99
-60
@@ -51,6 +51,7 @@ following DTD:
|
|||||||
<!ATTLIST default dest-branch CDATA #IMPLIED>
|
<!ATTLIST default dest-branch CDATA #IMPLIED>
|
||||||
<!ATTLIST default upstream CDATA #IMPLIED>
|
<!ATTLIST default upstream CDATA #IMPLIED>
|
||||||
<!ATTLIST default sync-j CDATA #IMPLIED>
|
<!ATTLIST default sync-j CDATA #IMPLIED>
|
||||||
|
<!ATTLIST default sync-j-max CDATA #IMPLIED>
|
||||||
<!ATTLIST default sync-c CDATA #IMPLIED>
|
<!ATTLIST default sync-c CDATA #IMPLIED>
|
||||||
<!ATTLIST default sync-s CDATA #IMPLIED>
|
<!ATTLIST default sync-s CDATA #IMPLIED>
|
||||||
<!ATTLIST default sync-tags CDATA #IMPLIED>
|
<!ATTLIST default sync-tags CDATA #IMPLIED>
|
||||||
@@ -59,7 +60,7 @@ following DTD:
|
|||||||
<!ATTLIST manifest-server url CDATA #REQUIRED>
|
<!ATTLIST manifest-server url CDATA #REQUIRED>
|
||||||
|
|
||||||
<!ELEMENT submanifest EMPTY>
|
<!ELEMENT submanifest EMPTY>
|
||||||
<!ATTLIST submanifest name ID #REQUIRED>
|
<!ATTLIST submanifest name ID #REQUIRED>
|
||||||
<!ATTLIST submanifest remote IDREF #IMPLIED>
|
<!ATTLIST submanifest remote IDREF #IMPLIED>
|
||||||
<!ATTLIST submanifest project CDATA #IMPLIED>
|
<!ATTLIST submanifest project CDATA #IMPLIED>
|
||||||
<!ATTLIST submanifest manifest-name CDATA #IMPLIED>
|
<!ATTLIST submanifest manifest-name CDATA #IMPLIED>
|
||||||
@@ -72,18 +73,19 @@ following DTD:
|
|||||||
project*,
|
project*,
|
||||||
copyfile*,
|
copyfile*,
|
||||||
linkfile*)>
|
linkfile*)>
|
||||||
<!ATTLIST project name CDATA #REQUIRED>
|
<!ATTLIST project name CDATA #REQUIRED>
|
||||||
<!ATTLIST project path CDATA #IMPLIED>
|
<!ATTLIST project path CDATA #IMPLIED>
|
||||||
<!ATTLIST project remote IDREF #IMPLIED>
|
<!ATTLIST project remote IDREF #IMPLIED>
|
||||||
<!ATTLIST project revision CDATA #IMPLIED>
|
<!ATTLIST project revision CDATA #IMPLIED>
|
||||||
<!ATTLIST project dest-branch CDATA #IMPLIED>
|
<!ATTLIST project dest-branch CDATA #IMPLIED>
|
||||||
<!ATTLIST project groups CDATA #IMPLIED>
|
<!ATTLIST project groups CDATA #IMPLIED>
|
||||||
<!ATTLIST project sync-c CDATA #IMPLIED>
|
<!ATTLIST project sync-c CDATA #IMPLIED>
|
||||||
<!ATTLIST project sync-s CDATA #IMPLIED>
|
<!ATTLIST project sync-s CDATA #IMPLIED>
|
||||||
<!ATTLIST project sync-tags CDATA #IMPLIED>
|
<!ATTLIST project sync-tags CDATA #IMPLIED>
|
||||||
<!ATTLIST project upstream CDATA #IMPLIED>
|
<!ATTLIST project upstream CDATA #IMPLIED>
|
||||||
<!ATTLIST project clone-depth CDATA #IMPLIED>
|
<!ATTLIST project clone-depth CDATA #IMPLIED>
|
||||||
<!ATTLIST project force-path CDATA #IMPLIED>
|
<!ATTLIST project force-path CDATA #IMPLIED>
|
||||||
|
<!ATTLIST project sync-strategy CDATA #IMPLIED>
|
||||||
|
|
||||||
<!ELEMENT annotation EMPTY>
|
<!ELEMENT annotation EMPTY>
|
||||||
<!ATTLIST annotation name CDATA #REQUIRED>
|
<!ATTLIST annotation name CDATA #REQUIRED>
|
||||||
@@ -95,26 +97,30 @@ following DTD:
|
|||||||
<!ATTLIST copyfile dest CDATA #REQUIRED>
|
<!ATTLIST copyfile dest CDATA #REQUIRED>
|
||||||
|
|
||||||
<!ELEMENT linkfile EMPTY>
|
<!ELEMENT linkfile EMPTY>
|
||||||
<!ATTLIST linkfile src CDATA #REQUIRED>
|
<!ATTLIST linkfile src CDATA #REQUIRED>
|
||||||
<!ATTLIST linkfile dest CDATA #REQUIRED>
|
<!ATTLIST linkfile dest CDATA #REQUIRED>
|
||||||
|
|
||||||
<!ELEMENT extend-project EMPTY>
|
<!ELEMENT extend-project (annotation*,
|
||||||
<!ATTLIST extend-project name CDATA #REQUIRED>
|
copyfile*,
|
||||||
<!ATTLIST extend-project path CDATA #IMPLIED>
|
linkfile*)>
|
||||||
<!ATTLIST extend-project dest-path CDATA #IMPLIED>
|
<!ATTLIST extend-project name CDATA #REQUIRED>
|
||||||
<!ATTLIST extend-project groups CDATA #IMPLIED>
|
<!ATTLIST extend-project path CDATA #IMPLIED>
|
||||||
<!ATTLIST extend-project revision CDATA #IMPLIED>
|
<!ATTLIST extend-project dest-path CDATA #IMPLIED>
|
||||||
<!ATTLIST extend-project remote CDATA #IMPLIED>
|
<!ATTLIST extend-project groups CDATA #IMPLIED>
|
||||||
|
<!ATTLIST extend-project revision CDATA #IMPLIED>
|
||||||
|
<!ATTLIST extend-project remote CDATA #IMPLIED>
|
||||||
<!ATTLIST extend-project dest-branch CDATA #IMPLIED>
|
<!ATTLIST extend-project dest-branch CDATA #IMPLIED>
|
||||||
<!ATTLIST extend-project upstream CDATA #IMPLIED>
|
<!ATTLIST extend-project upstream CDATA #IMPLIED>
|
||||||
|
<!ATTLIST extend-project base-rev CDATA #IMPLIED>
|
||||||
|
|
||||||
<!ELEMENT remove-project EMPTY>
|
<!ELEMENT remove-project EMPTY>
|
||||||
<!ATTLIST remove-project name CDATA #IMPLIED>
|
<!ATTLIST remove-project name CDATA #IMPLIED>
|
||||||
<!ATTLIST remove-project path CDATA #IMPLIED>
|
<!ATTLIST remove-project path CDATA #IMPLIED>
|
||||||
<!ATTLIST remove-project optional CDATA #IMPLIED>
|
<!ATTLIST remove-project optional CDATA #IMPLIED>
|
||||||
|
<!ATTLIST remove-project base-rev CDATA #IMPLIED>
|
||||||
|
|
||||||
<!ELEMENT repo-hooks EMPTY>
|
<!ELEMENT repo-hooks EMPTY>
|
||||||
<!ATTLIST repo-hooks in-project CDATA #REQUIRED>
|
<!ATTLIST repo-hooks in-project CDATA #REQUIRED>
|
||||||
<!ATTLIST repo-hooks enabled-list CDATA #REQUIRED>
|
<!ATTLIST repo-hooks enabled-list CDATA #REQUIRED>
|
||||||
|
|
||||||
<!ELEMENT superproject EMPTY>
|
<!ELEMENT superproject EMPTY>
|
||||||
@@ -123,7 +129,7 @@ following DTD:
|
|||||||
<!ATTLIST superproject revision CDATA #IMPLIED>
|
<!ATTLIST superproject revision CDATA #IMPLIED>
|
||||||
|
|
||||||
<!ELEMENT contactinfo EMPTY>
|
<!ELEMENT contactinfo EMPTY>
|
||||||
<!ATTLIST contactinfo bugurl CDATA #REQUIRED>
|
<!ATTLIST contactinfo bugurl CDATA #REQUIRED>
|
||||||
|
|
||||||
<!ELEMENT include EMPTY>
|
<!ELEMENT include EMPTY>
|
||||||
<!ATTLIST include name CDATA #REQUIRED>
|
<!ATTLIST include name CDATA #REQUIRED>
|
||||||
@@ -209,7 +215,9 @@ can be found. Used when syncing a revision locked manifest in
|
|||||||
-c mode to avoid having to sync the entire ref space. Project elements
|
-c mode to avoid having to sync the entire ref space. Project elements
|
||||||
not setting their own `upstream` will inherit this value.
|
not setting their own `upstream` will inherit this value.
|
||||||
|
|
||||||
Attribute `sync-j`: Number of parallel jobs to use when synching.
|
Attribute `sync-j`: Number of parallel jobs to use when syncing.
|
||||||
|
|
||||||
|
Attribute `sync-j-max`: Maximum number of parallel jobs to use when syncing.
|
||||||
|
|
||||||
Attribute `sync-c`: Set to true to only sync the given Git
|
Attribute `sync-c`: Set to true to only sync the given Git
|
||||||
branch (specified in the `revision` attribute) rather than the
|
branch (specified in the `revision` attribute) rather than the
|
||||||
@@ -229,26 +237,7 @@ At most one manifest-server may be specified. The url attribute
|
|||||||
is used to specify the URL of a manifest server, which is an
|
is used to specify the URL of a manifest server, which is an
|
||||||
XML RPC service.
|
XML RPC service.
|
||||||
|
|
||||||
The manifest server should implement the following RPC methods:
|
See the [smart sync documentation](./smart-sync.md) for more details.
|
||||||
|
|
||||||
GetApprovedManifest(branch, target)
|
|
||||||
|
|
||||||
Return a manifest in which each project is pegged to a known good revision
|
|
||||||
for the current branch and target. This is used by repo sync when the
|
|
||||||
--smart-sync option is given.
|
|
||||||
|
|
||||||
The target to use is defined by environment variables TARGET_PRODUCT
|
|
||||||
and TARGET_BUILD_VARIANT. These variables are used to create a string
|
|
||||||
of the form $TARGET_PRODUCT-$TARGET_BUILD_VARIANT, e.g. passion-userdebug.
|
|
||||||
If one of those variables or both are not present, the program will call
|
|
||||||
GetApprovedManifest without the target parameter and the manifest server
|
|
||||||
should choose a reasonable default target.
|
|
||||||
|
|
||||||
GetManifest(tag)
|
|
||||||
|
|
||||||
Return a manifest in which each project is pegged to the revision at
|
|
||||||
the specified tag. This is used by repo sync when the --smart-tag option
|
|
||||||
is given.
|
|
||||||
|
|
||||||
|
|
||||||
### Element submanifest
|
### Element submanifest
|
||||||
@@ -302,7 +291,7 @@ should be placed. If not supplied, `revision` is used.
|
|||||||
|
|
||||||
`path` may not be an absolute path or use "." or ".." path components.
|
`path` may not be an absolute path or use "." or ".." path components.
|
||||||
|
|
||||||
Attribute `groups`: List of additional groups to which all projects
|
Attribute `groups`: Set of additional groups to which all projects
|
||||||
in the included submanifest belong. This appends and recurses, meaning
|
in the included submanifest belong. This appends and recurses, meaning
|
||||||
all projects in submanifests carry all parent submanifest groups.
|
all projects in submanifests carry all parent submanifest groups.
|
||||||
Same syntax as the corresponding element of `project`.
|
Same syntax as the corresponding element of `project`.
|
||||||
@@ -370,7 +359,7 @@ When using `repo upload`, changes will be submitted for code
|
|||||||
review on this branch. If unspecified both here and in the
|
review on this branch. If unspecified both here and in the
|
||||||
default element, `revision` is used instead.
|
default element, `revision` is used instead.
|
||||||
|
|
||||||
Attribute `groups`: List of groups to which this project belongs,
|
Attribute `groups`: Set of groups to which this project belongs,
|
||||||
whitespace or comma separated. All projects belong to the group
|
whitespace or comma separated. All projects belong to the group
|
||||||
"all", and each project automatically belongs to a group of
|
"all", and each project automatically belongs to a group of
|
||||||
its name:`name` and path:`path`. E.g. for
|
its name:`name` and path:`path`. E.g. for
|
||||||
@@ -401,6 +390,22 @@ rather than the `name` attribute. This attribute only applies to the
|
|||||||
local mirrors syncing, it will be ignored when syncing the projects in a
|
local mirrors syncing, it will be ignored when syncing the projects in a
|
||||||
client working directory.
|
client working directory.
|
||||||
|
|
||||||
|
Attribute `sync-strategy`: Set the sync strategy used when fetching this
|
||||||
|
project. Currently the only supported value is `stateless`. When set to
|
||||||
|
`stateless`, repo will run a reflog expiration and aggressive garbage collection
|
||||||
|
at the end of the sync process. This is useful for projects that contain
|
||||||
|
large binary files and use `clone-depth="1"`, where garbage can accumulate
|
||||||
|
as binaries are added, deleted, or modified across successive syncs.
|
||||||
|
|
||||||
|
During a stateless sync, repo checks the following before cleaning up:
|
||||||
|
1. The project does not share an object directory with other projects.
|
||||||
|
2. The working tree is clean (no uncommitted changes, no untracked files).
|
||||||
|
3. There are no unpushed local commits.
|
||||||
|
4. There is no Git stash.
|
||||||
|
|
||||||
|
If any of these conditions are not met, repo falls back to a standard
|
||||||
|
sync without garbage collection.
|
||||||
|
|
||||||
### Element extend-project
|
### Element extend-project
|
||||||
|
|
||||||
Modify the attributes of the named project.
|
Modify the attributes of the named project.
|
||||||
@@ -410,6 +415,11 @@ attributes of an existing project without completely replacing the
|
|||||||
existing project definition. This makes the local manifest more robust
|
existing project definition. This makes the local manifest more robust
|
||||||
against changes to the original manifest.
|
against changes to the original manifest.
|
||||||
|
|
||||||
|
The `extend-project` element can also contain `annotation`, `copyfile`, and
|
||||||
|
`linkfile` child elements. These are added to the project's definition. A
|
||||||
|
`copyfile` or `linkfile` with a `dest` that already exists in the project
|
||||||
|
will overwrite the original.
|
||||||
|
|
||||||
Attribute `path`: If specified, limit the change to projects checked out
|
Attribute `path`: If specified, limit the change to projects checked out
|
||||||
at the specified path, rather than all projects with the given name.
|
at the specified path, rather than all projects with the given name.
|
||||||
|
|
||||||
@@ -418,7 +428,7 @@ of the repo client where the Git working directory for this project
|
|||||||
should be placed. This is used to move a project in the checkout by
|
should be placed. This is used to move a project in the checkout by
|
||||||
overriding the existing `path` setting.
|
overriding the existing `path` setting.
|
||||||
|
|
||||||
Attribute `groups`: List of additional groups to which this project
|
Attribute `groups`: Set of additional groups to which this project
|
||||||
belongs. Same syntax as the corresponding element of `project`.
|
belongs. Same syntax as the corresponding element of `project`.
|
||||||
|
|
||||||
Attribute `revision`: If specified, overrides the revision of the original
|
Attribute `revision`: If specified, overrides the revision of the original
|
||||||
@@ -433,22 +443,31 @@ project. Same syntax as the corresponding element of `project`.
|
|||||||
Attribute `upstream`: If specified, overrides the upstream of the original
|
Attribute `upstream`: If specified, overrides the upstream of the original
|
||||||
project. Same syntax as the corresponding element of `project`.
|
project. Same syntax as the corresponding element of `project`.
|
||||||
|
|
||||||
|
Attribute `base-rev`: If specified, adds a check against the revision
|
||||||
|
to be extended. Manifest parse will fail and give a list of mismatch extends
|
||||||
|
if the revisions being extended have changed since base-rev was set.
|
||||||
|
Intended for use with layered manifests using hash revisions to prevent
|
||||||
|
patch branches hiding newer upstream revisions. Also compares named refs
|
||||||
|
like branches or tags but is misleading if branches are used as base-rev.
|
||||||
|
Same syntax as the corresponding element of `project`.
|
||||||
|
|
||||||
### Element annotation
|
### Element annotation
|
||||||
|
|
||||||
Zero or more annotation elements may be specified as children of a
|
Zero or more annotation elements may be specified as children of a
|
||||||
project or remote element. Each element describes a name-value pair.
|
project element, an extend-project element, or a remote element. Each
|
||||||
For projects, this name-value pair will be exported into each project's
|
element describes a name-value pair. For projects, this name-value pair
|
||||||
environment during a 'forall' command, prefixed with `REPO__`. In addition,
|
will be exported into each project's environment during a 'forall'
|
||||||
there is an optional attribute "keep" which accepts the case insensitive values
|
command, prefixed with `REPO__`. In addition, there is an optional
|
||||||
"true" (default) or "false". This attribute determines whether or not the
|
attribute "keep" which accepts the case insensitive values "true"
|
||||||
|
(default) or "false". This attribute determines whether or not the
|
||||||
annotation will be kept when exported with the manifest subcommand.
|
annotation will be kept when exported with the manifest subcommand.
|
||||||
|
|
||||||
### Element copyfile
|
### Element copyfile
|
||||||
|
|
||||||
Zero or more copyfile elements may be specified as children of a
|
Zero or more copyfile elements may be specified as children of a
|
||||||
project element. Each element describes a src-dest pair of files;
|
project element, or an extend-project element. Each element describes a
|
||||||
the "src" file will be copied to the "dest" place during `repo sync`
|
src-dest pair of files; the "src" file will be copied to the "dest"
|
||||||
command.
|
place during `repo sync` command.
|
||||||
|
|
||||||
"src" is project relative, "dest" is relative to the top of the tree.
|
"src" is project relative, "dest" is relative to the top of the tree.
|
||||||
Copying from paths outside of the project or to paths outside of the repo
|
Copying from paths outside of the project or to paths outside of the repo
|
||||||
@@ -459,10 +478,14 @@ Intermediate paths must not be symlinks either.
|
|||||||
|
|
||||||
Parent directories of "dest" will be automatically created if missing.
|
Parent directories of "dest" will be automatically created if missing.
|
||||||
|
|
||||||
|
The files are copied in the order they are specified in the manifests.
|
||||||
|
If multiple elements specify the same source and destination, they will
|
||||||
|
only be applied as one, based on the first occurence. Files are copied
|
||||||
|
before any links specified via linkfile elements are created.
|
||||||
|
|
||||||
### Element linkfile
|
### Element linkfile
|
||||||
|
|
||||||
It's just like copyfile and runs at the same time as copyfile but
|
It's just like copyfile, but instead of copying it creates a symlink.
|
||||||
instead of copying it creates a symlink.
|
|
||||||
|
|
||||||
The symlink is created at "dest" (relative to the top of the tree) and
|
The symlink is created at "dest" (relative to the top of the tree) and
|
||||||
points to the path specified by "src" which is a path in the project.
|
points to the path specified by "src" which is a path in the project.
|
||||||
@@ -472,6 +495,11 @@ Parent directories of "dest" will be automatically created if missing.
|
|||||||
The symlink target may be a file or directory, but it may not point outside
|
The symlink target may be a file or directory, but it may not point outside
|
||||||
of the repo client.
|
of the repo client.
|
||||||
|
|
||||||
|
The links are created in the order they are specified in the manifests.
|
||||||
|
If multiple elements specify the same source and destination, they will
|
||||||
|
only be applied as one, based on the first occurence. Links are created
|
||||||
|
after any files specified via copyfile elements are copied.
|
||||||
|
|
||||||
### Element remove-project
|
### Element remove-project
|
||||||
|
|
||||||
Deletes a project from the internal manifest table, possibly
|
Deletes a project from the internal manifest table, possibly
|
||||||
@@ -496,6 +524,14 @@ name. Logic otherwise behaves like both are specified.
|
|||||||
Attribute `optional`: Set to true to ignore remove-project elements with no
|
Attribute `optional`: Set to true to ignore remove-project elements with no
|
||||||
matching `project` element.
|
matching `project` element.
|
||||||
|
|
||||||
|
Attribute `base-rev`: If specified, adds a check against the revision
|
||||||
|
to be removed. Manifest parse will fail and give a list of mismatch removes
|
||||||
|
if the revisions being removed have changed since base-rev was set.
|
||||||
|
Intended for use with layered manifests using hash revisions to prevent
|
||||||
|
patch branches hiding newer upstream revisions. Also compares named refs
|
||||||
|
like branches or tags but is misleading if branches are used as base-rev.
|
||||||
|
Same syntax as the corresponding element of `project`.
|
||||||
|
|
||||||
### Element repo-hooks
|
### Element repo-hooks
|
||||||
|
|
||||||
NB: See the [practical documentation](./repo-hooks.md) for using repo hooks.
|
NB: See the [practical documentation](./repo-hooks.md) for using repo hooks.
|
||||||
@@ -561,13 +597,16 @@ the manifest repository's root.
|
|||||||
"name" may not be an absolute path or use "." or ".." path components.
|
"name" may not be an absolute path or use "." or ".." path components.
|
||||||
These restrictions are not enforced for [Local Manifests].
|
These restrictions are not enforced for [Local Manifests].
|
||||||
|
|
||||||
Attribute `groups`: List of additional groups to which all projects
|
Attribute `groups`: Set of additional groups to which all projects
|
||||||
in the included manifest belong. This appends and recurses, meaning
|
in the included manifest belong. This appends and recurses, meaning
|
||||||
all projects in included manifests carry all parent include groups.
|
all projects in included manifests carry all parent include groups.
|
||||||
|
This also applies to all extend-project elements in the included manifests.
|
||||||
Same syntax as the corresponding element of `project`.
|
Same syntax as the corresponding element of `project`.
|
||||||
|
|
||||||
Attribute `revision`: Name of a Git branch (e.g. `main` or `refs/heads/main`)
|
Attribute `revision`: Name of a Git branch (e.g. `main` or `refs/heads/main`)
|
||||||
default to which all projects in the included manifest belong.
|
default to which all projects in the included manifest belong. This recurses,
|
||||||
|
meaning it will apply to all projects in all manifests included as a result of
|
||||||
|
this element.
|
||||||
|
|
||||||
## Local Manifests {#local-manifests}
|
## Local Manifests {#local-manifests}
|
||||||
|
|
||||||
|
|||||||
+78
-33
@@ -1,47 +1,92 @@
|
|||||||
# Supported Python Versions
|
# Supported Python Versions
|
||||||
|
|
||||||
With Python 2.7 officially going EOL on [01 Jan 2020](https://pythonclock.org/),
|
This documents the current supported Python versions, and tries to provide
|
||||||
we need a support plan for the repo project itself.
|
guidance for when we decide to drop support for older versions.
|
||||||
Inevitably, there will be a long tail of users who still want to use Python 2 on
|
|
||||||
their old LTS/corp systems and have little power to change the system.
|
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
* Python 3.6 (released Dec 2016) is required by default starting with repo-2.x.
|
* Python 3.6 (released Dec 2016) is required starting with repo-2.0.
|
||||||
* Older versions of Python (e.g. v2.7) may use the legacy feature-frozen branch
|
* Older versions of Python (e.g. v2.7) may use old releases via the repo-1.x
|
||||||
based on repo-1.x.
|
branch, but no support is provided.
|
||||||
|
|
||||||
## Overview
|
## repo hooks
|
||||||
|
|
||||||
We provide a branch for Python 2 users that is feature-frozen.
|
|
||||||
Bugfixes may be added on a best-effort basis or from the community, but largely
|
|
||||||
no new features will be added, nor is support guaranteed.
|
|
||||||
|
|
||||||
Users can select this during `repo init` time via the [repo launcher].
|
|
||||||
Otherwise the default branches (e.g. stable & main) will be used which will
|
|
||||||
require Python 3.
|
|
||||||
|
|
||||||
This means the [repo launcher] needs to support both Python 2 & Python 3, but
|
|
||||||
since it doesn't import any other repo code, this shouldn't be too problematic.
|
|
||||||
|
|
||||||
The main branch will require Python 3.6 at a minimum.
|
|
||||||
If the system has an older version of Python 3, then users will have to select
|
|
||||||
the legacy Python 2 branch instead.
|
|
||||||
|
|
||||||
### repo hooks
|
|
||||||
|
|
||||||
Projects that use [repo hooks] run on independent schedules.
|
Projects that use [repo hooks] run on independent schedules.
|
||||||
They might migrate to Python 3 earlier or later than us.
|
Since it's not possible to detect what version of Python the hooks were written
|
||||||
To support them, we'll probe the shebang of the hook script and if we find an
|
or tested against, we always import & exec them with the active Python version.
|
||||||
interpreter in there that indicates a different version than repo is currently
|
|
||||||
running under, we'll attempt to reexec ourselves under that.
|
|
||||||
|
|
||||||
For example, a hook with a header like `#!/usr/bin/python2` will have repo
|
If the user's Python is too new for the [repo hooks], then it is up to the hooks
|
||||||
execute `/usr/bin/python2` to execute the hook code specifically if repo is
|
maintainer to update.
|
||||||
currently running Python 3.
|
|
||||||
|
|
||||||
For more details, consult the [repo hooks] documentation.
|
## Repo launcher
|
||||||
|
|
||||||
|
The [repo launcher] is an independent script that can support older versions of
|
||||||
|
Python without holding back the rest of the codebase.
|
||||||
|
If it detects the current version of Python is too old, it will try to reexec
|
||||||
|
via a newer version of Python via standard `pythonX.Y` interpreter names.
|
||||||
|
|
||||||
|
However, this is provided as a nicety when it is not onerous, and there is no
|
||||||
|
official support for older versions of Python than the rest of the codebase.
|
||||||
|
|
||||||
|
If your default python interpreters are too old to run the launcher even though
|
||||||
|
you have newer versions installed, your choices are:
|
||||||
|
|
||||||
|
* Modify the [repo launcher]'s shebang to suite your environment.
|
||||||
|
* Download an older version of the [repo launcher] and don't upgrade it.
|
||||||
|
Be aware that we do not guarantee old repo launchers will work with current
|
||||||
|
versions of repo. Bug reports using old launchers will not be accepted.
|
||||||
|
|
||||||
|
## When to drop support
|
||||||
|
|
||||||
|
So far, Python 3.6 has provided most of the interesting features that we want
|
||||||
|
(e.g. typing & f-strings), and there haven't been features in newer versions
|
||||||
|
that are critical to us.
|
||||||
|
|
||||||
|
That said, let's assume we need functionality that only exists in Python 3.7.
|
||||||
|
How do we decide when it's acceptable to drop Python 3.6?
|
||||||
|
|
||||||
|
1. Review the [Project References](./release-process.md#project-references) to
|
||||||
|
see what major distros are using the previous version of Python, and when
|
||||||
|
they go EOL. Generally we care about Ubuntu LTS & current/previous Debian
|
||||||
|
stable versions.
|
||||||
|
* If they're all EOL already, then go for it, drop support.
|
||||||
|
* If they aren't EOL, start a thread on [repo-discuss] to see how the user
|
||||||
|
base feels about the proposal.
|
||||||
|
1. Update the "soft" versions in the codebase. This will start warning users
|
||||||
|
that the older version is deprecated.
|
||||||
|
* Update [repo](/repo) if the launcher needs updating.
|
||||||
|
This only helps with people who download newer launchers.
|
||||||
|
* Update [main.py](/main.py) for the main codebase.
|
||||||
|
This warns for everyone regardless of [repo launcher] version.
|
||||||
|
* Update [requirements.json](/requirements.json).
|
||||||
|
This allows [repo launcher] to display warnings/errors without having
|
||||||
|
to execute the new codebase. This helps in case of syntax or module
|
||||||
|
changes where older versions won't even be able to import the new code.
|
||||||
|
1. After some grace period (ideally at least 2 quarters after the first release
|
||||||
|
with the updated soft requirements), update the "hard" versions, and then
|
||||||
|
start using the new functionality.
|
||||||
|
|
||||||
|
## Python 2.7 & 3.0-3.5
|
||||||
|
|
||||||
|
> **There is no support for these versions.**
|
||||||
|
> **Do not file bugs if you are using old Python versions.**
|
||||||
|
> **Any such reports will be marked invalid and ignored.**
|
||||||
|
> **Upgrade your distro and/or runtime instead.**
|
||||||
|
|
||||||
|
Fetch an old version of the [repo launcher]:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ curl https://storage.googleapis.com/git-repo-downloads/repo-2.32 > ~/.bin/repo-2.32
|
||||||
|
$ chmod a+rx ~/.bin/repo-2.32
|
||||||
|
```
|
||||||
|
|
||||||
|
Then initialize an old version of repo:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ repo-2.32 init --repo-rev=repo-1 ...
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
[repo-discuss]: https://groups.google.com/forum/#!forum/repo-discuss
|
||||||
[repo hooks]: ./repo-hooks.md
|
[repo hooks]: ./repo-hooks.md
|
||||||
[repo launcher]: ../repo
|
[repo launcher]: ../repo
|
||||||
|
|||||||
+30
-8
@@ -96,6 +96,9 @@ If that tag is valid, then repo will warn and use that commit instead.
|
|||||||
|
|
||||||
If that tag cannot be verified, it gives up and forces the user to resolve.
|
If that tag cannot be verified, it gives up and forces the user to resolve.
|
||||||
|
|
||||||
|
If env variable `REPO_SKIP_SELF_UPDATE` is defined, this will
|
||||||
|
bypass the self update algorithm.
|
||||||
|
|
||||||
### Force an update
|
### Force an update
|
||||||
|
|
||||||
The `repo selfupdate` command can be used to force an immediate update.
|
The `repo selfupdate` command can be used to force an immediate update.
|
||||||
@@ -202,7 +205,7 @@ still support them.
|
|||||||
Things in italics are things we used to care about but probably don't anymore.
|
Things in italics are things we used to care about but probably don't anymore.
|
||||||
|
|
||||||
| Date | EOL | [Git][rel-g] | [Python][rel-p] | [SSH][rel-o] | [Ubuntu][rel-u] / [Debian][rel-d] | Git | Python | SSH |
|
| Date | EOL | [Git][rel-g] | [Python][rel-p] | [SSH][rel-o] | [Ubuntu][rel-u] / [Debian][rel-d] | Git | Python | SSH |
|
||||||
|:--------:|:------------:|:------------:|:---------------:|:------------:|-----------------------------------|-----|--------|-----|
|
|:--------:|:------------:|:------------:|:---------------:|:------------:|-----------------------------------|:---:|:------:|:---:|
|
||||||
| Apr 2008 | | | | 5.0 |
|
| Apr 2008 | | | | 5.0 |
|
||||||
| Jun 2008 | | | | 5.1 |
|
| Jun 2008 | | | | 5.1 |
|
||||||
| Oct 2008 | *Oct 2013* | | 2.6.0 | | *10.04 Lucid* - 10.10 Maverick / *Squeeze* |
|
| Oct 2008 | *Oct 2013* | | 2.6.0 | | *10.04 Lucid* - 10.10 Maverick / *Squeeze* |
|
||||||
@@ -241,7 +244,7 @@ Things in italics are things we used to care about but probably don't anymore.
|
|||||||
| Feb 2014 | *Dec 2014* | **1.9.0** | | | *14.04 Trusty* |
|
| Feb 2014 | *Dec 2014* | **1.9.0** | | | *14.04 Trusty* |
|
||||||
| Mar 2014 | *Mar 2019* | | *3.4.0* | | *14.04 Trusty* - 15.10 Wily / *Jessie* |
|
| Mar 2014 | *Mar 2019* | | *3.4.0* | | *14.04 Trusty* - 15.10 Wily / *Jessie* |
|
||||||
| Mar 2014 | | | | 6.6 | *14.04 Trusty* - 14.10 Utopic |
|
| Mar 2014 | | | | 6.6 | *14.04 Trusty* - 14.10 Utopic |
|
||||||
| Apr 2014 | *Apr 2022* | | | | *14.04 Trusty* | 1.9.1 | 2.7.5 3.4.0 | 6.6 |
|
| Apr 2014 | *Apr 2024* | | | | *14.04 Trusty* | 1.9.1 | 2.7.5 3.4.0 | 6.6 |
|
||||||
| May 2014 | *Dec 2014* | 2.0.0 |
|
| May 2014 | *Dec 2014* | 2.0.0 |
|
||||||
| Aug 2014 | *Dec 2014* | *2.1.0* | | | 14.10 Utopic - 15.04 Vivid / *Jessie* |
|
| Aug 2014 | *Dec 2014* | *2.1.0* | | | 14.10 Utopic - 15.04 Vivid / *Jessie* |
|
||||||
| Oct 2014 | | | | 6.7 | 15.04 Vivid |
|
| Oct 2014 | | | | 6.7 | 15.04 Vivid |
|
||||||
@@ -262,7 +265,7 @@ Things in italics are things we used to care about but probably don't anymore.
|
|||||||
| Jan 2016 | *Jul 2017* | *2.7.0* | | | *16.04 Xenial* |
|
| Jan 2016 | *Jul 2017* | *2.7.0* | | | *16.04 Xenial* |
|
||||||
| Feb 2016 | | | | 7.2 | *16.04 Xenial* |
|
| Feb 2016 | | | | 7.2 | *16.04 Xenial* |
|
||||||
| Mar 2016 | *Jul 2017* | 2.8.0 |
|
| Mar 2016 | *Jul 2017* | 2.8.0 |
|
||||||
| Apr 2016 | *Apr 2024* | | | | *16.04 Xenial* | 2.7.4 | 2.7.11 3.5.1 | 7.2 |
|
| Apr 2016 | *Apr 2026* | | | | *16.04 Xenial* | 2.7.4 | 2.7.11 3.5.1 | 7.2 |
|
||||||
| Jun 2016 | *Jul 2017* | 2.9.0 | | | 16.10 Yakkety |
|
| Jun 2016 | *Jul 2017* | 2.9.0 | | | 16.10 Yakkety |
|
||||||
| Jul 2016 | | | | 7.3 | 16.10 Yakkety |
|
| Jul 2016 | | | | 7.3 | 16.10 Yakkety |
|
||||||
| Sep 2016 | *Sep 2017* | 2.10.0 |
|
| Sep 2016 | *Sep 2017* | 2.10.0 |
|
||||||
@@ -312,14 +315,33 @@ Things in italics are things we used to care about but probably don't anymore.
|
|||||||
| Oct 2020 | | | | | 20.10 Groovy | 2.27.0 | 2.7.18 3.8.6 | 8.3 |
|
| Oct 2020 | | | | | 20.10 Groovy | 2.27.0 | 2.7.18 3.8.6 | 8.3 |
|
||||||
| Oct 2020 | **Oct 2025** | | 3.9.0 | | 21.04 Hirsute / **Bullseye** |
|
| Oct 2020 | **Oct 2025** | | 3.9.0 | | 21.04 Hirsute / **Bullseye** |
|
||||||
| Dec 2020 | *Mar 2021* | 2.30.0 | | | 21.04 Hirsute / **Bullseye** |
|
| Dec 2020 | *Mar 2021* | 2.30.0 | | | 21.04 Hirsute / **Bullseye** |
|
||||||
| Mar 2021 | | 2.31.0 |
|
| Mar 2021 | | 2.31.0 | | 8.5 |
|
||||||
| Mar 2021 | | | | 8.5 |
|
|
||||||
| Apr 2021 | | | | 8.6 |
|
| Apr 2021 | | | | 8.6 |
|
||||||
| Apr 2021 | *Jan 2022* | | | | 21.04 Hirsute | 2.30.2 | 2.7.18 3.9.4 | 8.4 |
|
| Apr 2021 | *Jan 2022* | | | | 21.04 Hirsute | 2.30.2 | 2.7.18 3.9.4 | 8.4 |
|
||||||
| Jun 2021 | | 2.32.0 |
|
| Jun 2021 | | 2.32.0 |
|
||||||
| Aug 2021 | | 2.33.0 |
|
| Aug 2021 | | 2.33.0 | | 8.7 |
|
||||||
| Aug 2021 | | | | 8.7 |
|
|
||||||
| Aug 2021 | **Aug 2026** | | | | **Debian 11 Bullseye** | 2.30.2 | 2.7.18 3.9.2 | 8.4 |
|
| Aug 2021 | **Aug 2026** | | | | **Debian 11 Bullseye** | 2.30.2 | 2.7.18 3.9.2 | 8.4 |
|
||||||
|
| Sep 2021 | | | | 8.8 |
|
||||||
|
| Oct 2021 | | 2.34.0 | 3.10.0 | | **22.04 Jammy** |
|
||||||
|
| Jan 2022 | | 2.35.0 |
|
||||||
|
| Feb 2022 | | | | 8.9 | **22.04 Jammy** |
|
||||||
|
| Apr 2022 | | 2.36.0 | | 9.0 |
|
||||||
|
| Apr 2022 | **Apr 2032** | | | | **22.04 Jammy** | 2.34.1 | 2.7.18 3.10.6 | 8.9 |
|
||||||
|
| Jun 2022 | | 2.37.0 |
|
||||||
|
| Oct 2022 | | 2.38.0 | | 9.1 |
|
||||||
|
| Oct 2022 | | | 3.11.0 | | **Bookworm** |
|
||||||
|
| Dec 2022 | | 2.39.0 | | | **Bookworm** |
|
||||||
|
| Feb 2023 | | | | 9.2 | **Bookworm** |
|
||||||
|
| Mar 2023 | | 2.40.0 | | 9.3 |
|
||||||
|
| Jun 2023 | | 2.41.0 |
|
||||||
|
| Jun 2023 | **Jun 2028** | | | | **Debian 12 Bookworm** | 2.39.2 | 3.11.2 | 9.2 |
|
||||||
|
| Aug 2023 | | 2.42.0 | | 9.4 |
|
||||||
|
| Oct 2023 | | | 3.12.0 | 9.5 |
|
||||||
|
| Nov 2022 | | 2.43.0 |
|
||||||
|
| Dec 2023 | | | | 9.6 |
|
||||||
|
| Feb 2024 | | 2.44.0 |
|
||||||
|
| Mar 2024 | | | | 9.7 |
|
||||||
|
| Oct 2024 | | | 3.13.0 |
|
||||||
| **Date** | **EOL** | **[Git][rel-g]** | **[Python][rel-p]** | **[SSH][rel-o]** | **[Ubuntu][rel-u] / [Debian][rel-d]** | **Git** | **Python** | **SSH** |
|
| **Date** | **EOL** | **[Git][rel-g]** | **[Python][rel-p]** | **[SSH][rel-o]** | **[Ubuntu][rel-u] / [Debian][rel-d]** | **Git** | **Python** | **SSH** |
|
||||||
|
|
||||||
|
|
||||||
@@ -328,7 +350,7 @@ Things in italics are things we used to care about but probably don't anymore.
|
|||||||
[rel-g]: https://en.wikipedia.org/wiki/Git#Releases
|
[rel-g]: https://en.wikipedia.org/wiki/Git#Releases
|
||||||
[rel-o]: https://www.openssh.com/releasenotes.html
|
[rel-o]: https://www.openssh.com/releasenotes.html
|
||||||
[rel-p]: https://en.wikipedia.org/wiki/History_of_Python#Table_of_versions
|
[rel-p]: https://en.wikipedia.org/wiki/History_of_Python#Table_of_versions
|
||||||
[rel-u]: https://en.wikipedia.org/wiki/Ubuntu_version_history#Table_of_versions
|
[rel-u]: https://wiki.ubuntu.com/Releases
|
||||||
[example announcement]: https://groups.google.com/d/topic/repo-discuss/UGBNismWo1M/discussion
|
[example announcement]: https://groups.google.com/d/topic/repo-discuss/UGBNismWo1M/discussion
|
||||||
[repo-discuss@googlegroups.com]: https://groups.google.com/forum/#!forum/repo-discuss
|
[repo-discuss@googlegroups.com]: https://groups.google.com/forum/#!forum/repo-discuss
|
||||||
[go/repo-release]: https://goto.google.com/repo-release
|
[go/repo-release]: https://goto.google.com/repo-release
|
||||||
|
|||||||
@@ -133,3 +133,44 @@ 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, sync_duration_seconds=None, **kwargs):
|
||||||
|
"""Main function invoked directly by repo.
|
||||||
|
|
||||||
|
We must use the name "main" as that is what repo requires.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repo_topdir: The absolute path to the top-level directory of the repo workspace.
|
||||||
|
sync_duration_seconds: The duration of the sync operation in seconds.
|
||||||
|
kwargs: Leave this here for forward-compatibility.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# repo Smart Syncing
|
||||||
|
|
||||||
|
Repo normally fetches & syncs manifests from the same URL specified during
|
||||||
|
`repo init`, and that often fetches the latest revisions of all projects in
|
||||||
|
the manifest. This flow works well for tracking and developing with the
|
||||||
|
latest code, but often it's desirable to sync to other points. For example,
|
||||||
|
to get a local build matching a specific release or build to reproduce bugs
|
||||||
|
reported by other people.
|
||||||
|
|
||||||
|
Repo's sync subcommand has support for fetching manifests from a server over
|
||||||
|
an XML-RPC connection. The local configuration and network API are defined by
|
||||||
|
repo, but individual projects have to host their own server for the client to
|
||||||
|
communicate with.
|
||||||
|
|
||||||
|
This process is called "smart syncing" -- instead of blindly fetching the latest
|
||||||
|
revision of all projects and getting an unknown state to develop against, the
|
||||||
|
client passes a request to the server and is given a matching manifest that
|
||||||
|
typically specifies specific commits for every project to fetch a known source
|
||||||
|
state.
|
||||||
|
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
## Manifest Configuration
|
||||||
|
|
||||||
|
The manifest specifies the server to communicate with via the
|
||||||
|
the [`<manifest-server>` element](manifest-format.md#Element-manifest_server)
|
||||||
|
element. This is how the client knows what service to talk to.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<manifest-server url="https://example.com/your/manifest/server/url" />
|
||||||
|
```
|
||||||
|
|
||||||
|
If the URL starts with `persistent-`, then the
|
||||||
|
[`git-remote-persistent-https` helper](https://github.com/git/git/blob/HEAD/contrib/persistent-https/README)
|
||||||
|
is used to communicate with the server.
|
||||||
|
|
||||||
|
## Credentials
|
||||||
|
|
||||||
|
Credentials may be specified directly in typical `username:password`
|
||||||
|
[URI syntax](https://en.wikipedia.org/wiki/URI#Syntax) in the
|
||||||
|
`<manifest-server>` element directly in the manifest.
|
||||||
|
|
||||||
|
If they are not specified, `repo sync` has `--manifest-server-username=USERNAME`
|
||||||
|
and `--manifest-server-password=PASSWORD` options.
|
||||||
|
|
||||||
|
If those are not used, then repo will look up the host in your
|
||||||
|
[`~/.netrc`](https://docs.python.org/3/library/netrc.html) database.
|
||||||
|
|
||||||
|
When making the connection, cookies matching the host are automatically loaded
|
||||||
|
from the cookiejar specified in
|
||||||
|
[Git's `http.cookiefile` setting](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpcookieFile).
|
||||||
|
|
||||||
|
## Manifest Server
|
||||||
|
|
||||||
|
Unfortunately, there are no public reference implementations. Google has an
|
||||||
|
internal one for Android, but it is written using Google's internal systems,
|
||||||
|
so wouldn't be that helpful as a reference.
|
||||||
|
|
||||||
|
That said, the XML-RPC API is pretty simple, so any standard XML-RPC server
|
||||||
|
example would do. Google's internal server uses Python's
|
||||||
|
[xmlrpc.server.SimpleXMLRPCDispatcher](https://docs.python.org/3/library/xmlrpc.server.html).
|
||||||
|
|
||||||
|
## Network API
|
||||||
|
|
||||||
|
The manifest server should implement the following RPC methods.
|
||||||
|
|
||||||
|
### GetApprovedManifest
|
||||||
|
|
||||||
|
> `GetApprovedManifest(branch: str, target: Optional[str]) -> str`
|
||||||
|
|
||||||
|
The meaning of `branch` and `target` is not strictly defined. The server may
|
||||||
|
interpret them however it wants. The recommended interpretation is that the
|
||||||
|
`branch` matches the manifest branch, and `target` is an identifier for your
|
||||||
|
project that matches something users would build.
|
||||||
|
|
||||||
|
See the client section below for how repo typically generates these values.
|
||||||
|
|
||||||
|
The server will return a manifest or an error. If it's an error, repo will
|
||||||
|
show the output directly to the user to provide a limited feedback channel.
|
||||||
|
|
||||||
|
If the user's request is ambiguous and could match multiple manifests, the
|
||||||
|
server has to decide whether to pick one automatically (and silently such that
|
||||||
|
the user won't know there were multiple matches), or return an error and force
|
||||||
|
the user to be more specific.
|
||||||
|
|
||||||
|
### GetManifest
|
||||||
|
|
||||||
|
> `GetManifest(tag: str) -> str`
|
||||||
|
|
||||||
|
The meaning of `tag` is not strictly defined. Projects are encouraged to use
|
||||||
|
a system where the tag matches a unique source state.
|
||||||
|
|
||||||
|
See the client section below for how repo typically generates these values.
|
||||||
|
|
||||||
|
The server will return a manifest or an error. If it's an error, repo will
|
||||||
|
show the output directly to the user to provide a limited feedback channel.
|
||||||
|
|
||||||
|
If the user's request is ambiguous and could match multiple manifests, the
|
||||||
|
server has to decide whether to pick one automatically (and silently such that
|
||||||
|
the user won't know there were multiple matches), or return an error and force
|
||||||
|
the user to be more specific.
|
||||||
|
|
||||||
|
## Client Options
|
||||||
|
|
||||||
|
Once repo has successfully downloaded the manifest from the server, it saves a
|
||||||
|
copy into `.repo/manifests/smart_sync_override.xml` so users can examine it.
|
||||||
|
The next time `repo sync` is run, this file is automatically replaced or removed
|
||||||
|
based on the current set of options.
|
||||||
|
|
||||||
|
### --smart-sync
|
||||||
|
|
||||||
|
Repo will call `GetApprovedManifest(branch[, target])`.
|
||||||
|
|
||||||
|
The `branch` is determined by the current manifest branch as specified by
|
||||||
|
`--manifest-branch=BRANCH` when running `repo init`.
|
||||||
|
|
||||||
|
The `target` is defined by environment variables in the order below. If none
|
||||||
|
of them match, then `target` is omitted. These variables were decided as they
|
||||||
|
match the settings Android build environments automatically setup.
|
||||||
|
|
||||||
|
1. `${SYNC_TARGET}`: If defined, the value is used directly.
|
||||||
|
2. `${TARGET_PRODUCT}-${TARGET_RELEASE}-${TARGET_BUILD_VARIANT}`: If these
|
||||||
|
variables are all defined, then they are merged with `-` and used.
|
||||||
|
3. `${TARGET_PRODUCT}-${TARGET_BUILD_VARIANT}`: If these variables are all
|
||||||
|
defined, then they are merged with `-` and used.
|
||||||
|
|
||||||
|
### --smart-tag=TAG
|
||||||
|
|
||||||
|
Repo will call `GetManifest(TAG)`.
|
||||||
+6
-3
@@ -50,8 +50,11 @@ Git worktrees (see the previous section for more info).
|
|||||||
Repo will use symlinks heavily internally.
|
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.
|
||||||
|
|
||||||
@@ -142,7 +145,7 @@ You will have to specify this flag every time you upload.
|
|||||||
|
|
||||||
[mintty]: https://github.com/mintty/mintty/issues/56
|
[mintty]: https://github.com/mintty/mintty/issues/56
|
||||||
|
|
||||||
### repohooks always fail with an close_fds error.
|
### repohooks always fail with an close_fds error
|
||||||
|
|
||||||
When using the [reference repohooks project][repohooks] included in AOSP,
|
When using the [reference repohooks project][repohooks] included in AOSP,
|
||||||
you might see errors like this when running `repo upload`:
|
you might see errors like this when running `repo upload`:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from error import EditorError
|
|||||||
import platform_utils
|
import platform_utils
|
||||||
|
|
||||||
|
|
||||||
class Editor(object):
|
class Editor:
|
||||||
"""Manages the user's preferred text editor."""
|
"""Manages the user's preferred text editor."""
|
||||||
|
|
||||||
_editor = None
|
_editor = None
|
||||||
@@ -104,9 +104,7 @@ least one of these before using this command.""", # noqa: E501
|
|||||||
try:
|
try:
|
||||||
rc = subprocess.Popen(args, shell=shell).wait()
|
rc = subprocess.Popen(args, shell=shell).wait()
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise EditorError(
|
raise EditorError(f"editor failed, {str(e)}: {editor} {path}")
|
||||||
"editor failed, %s: %s %s" % (str(e), editor, path)
|
|
||||||
)
|
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
raise EditorError(
|
raise EditorError(
|
||||||
"editor failed with exit status %d: %s %s"
|
"editor failed with exit status %d: %s %s"
|
||||||
|
|||||||
@@ -107,8 +107,8 @@ class GitError(RepoError):
|
|||||||
return self.message
|
return self.message
|
||||||
|
|
||||||
|
|
||||||
class GitcUnsupportedError(RepoExitError):
|
class GitAuthError(RepoExitError):
|
||||||
"""Gitc no longer supported."""
|
"""Cannot talk to remote due to auth issue."""
|
||||||
|
|
||||||
|
|
||||||
class UploadError(RepoError):
|
class UploadError(RepoError):
|
||||||
|
|||||||
+11
-3
@@ -21,7 +21,7 @@ TASK_SYNC_NETWORK = "sync-network"
|
|||||||
TASK_SYNC_LOCAL = "sync-local"
|
TASK_SYNC_LOCAL = "sync-local"
|
||||||
|
|
||||||
|
|
||||||
class EventLog(object):
|
class EventLog:
|
||||||
"""Event log that records events that occurred during a repo invocation.
|
"""Event log that records events that occurred during a repo invocation.
|
||||||
|
|
||||||
Events are written to the log as a consecutive JSON entries, one per line.
|
Events are written to the log as a consecutive JSON entries, one per line.
|
||||||
@@ -168,8 +168,10 @@ class EventLog(object):
|
|||||||
f.write("\n")
|
f.write("\n")
|
||||||
|
|
||||||
|
|
||||||
# An integer id that is unique across this invocation of the program.
|
# An integer id that is unique across this invocation of the program, to be set
|
||||||
_EVENT_ID = multiprocessing.Value("i", 1)
|
# by the first Add event. We can't set it here since it results in leaked
|
||||||
|
# resources (see: https://issues.gerritcodereview.com/353656374).
|
||||||
|
_EVENT_ID = None
|
||||||
|
|
||||||
|
|
||||||
def _NextEventId():
|
def _NextEventId():
|
||||||
@@ -178,6 +180,12 @@ def _NextEventId():
|
|||||||
Returns:
|
Returns:
|
||||||
A unique, to this invocation of the program, integer id.
|
A unique, to this invocation of the program, integer id.
|
||||||
"""
|
"""
|
||||||
|
global _EVENT_ID
|
||||||
|
if _EVENT_ID is None:
|
||||||
|
# There is a small chance of race condition - two parallel processes
|
||||||
|
# setting up _EVENT_ID. However, we expect TASK_COMMAND to happen before
|
||||||
|
# mp kicks in.
|
||||||
|
_EVENT_ID = multiprocessing.Value("i", 1)
|
||||||
with _EVENT_ID.get_lock():
|
with _EVENT_ID.get_lock():
|
||||||
val = _EVENT_ID.value
|
val = _EVENT_ID.value
|
||||||
_EVENT_ID.value += 1
|
_EVENT_ID.value += 1
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def fetch_file(url, verbose=False):
|
|||||||
"""
|
"""
|
||||||
scheme = urlparse(url).scheme
|
scheme = urlparse(url).scheme
|
||||||
if scheme == "gs":
|
if scheme == "gs":
|
||||||
cmd = ["gsutil", "cat", url]
|
cmd = ["gcloud", "storage", "cat", url]
|
||||||
errors = []
|
errors = []
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
@@ -42,7 +42,7 @@ def fetch_file(url, verbose=False):
|
|||||||
)
|
)
|
||||||
if result.stderr and verbose:
|
if result.stderr and verbose:
|
||||||
print(
|
print(
|
||||||
'warning: non-fatal error running "gsutil": %s'
|
'warning: non-fatal error running "gcloud storage": %s'
|
||||||
% result.stderr,
|
% result.stderr,
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
@@ -50,7 +50,8 @@ def fetch_file(url, verbose=False):
|
|||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
errors.append(e)
|
errors.append(e)
|
||||||
print(
|
print(
|
||||||
'fatal: error running "gsutil": %s' % e.stderr, file=sys.stderr
|
'fatal: error running "gcloud storage": %s' % e.stderr,
|
||||||
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
raise FetchFileError(aggregate_errors=errors)
|
raise FetchFileError(aggregate_errors=errors)
|
||||||
with urlopen(url) as f:
|
with urlopen(url) as f:
|
||||||
|
|||||||
+256
-58
@@ -13,15 +13,18 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from typing import Any, Optional
|
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
|
||||||
import platform_utils
|
import platform_utils
|
||||||
|
from repo_logging import RepoLogger
|
||||||
from repo_trace import IsTrace
|
from repo_trace import IsTrace
|
||||||
from repo_trace import REPO_TRACE
|
from repo_trace import REPO_TRACE
|
||||||
from repo_trace import Trace
|
from repo_trace import Trace
|
||||||
@@ -29,35 +32,27 @@ from wrapper import Wrapper
|
|||||||
|
|
||||||
|
|
||||||
GIT = "git"
|
GIT = "git"
|
||||||
# NB: These do not need to be kept in sync with the repo launcher script.
|
|
||||||
# These may be much newer as it allows the repo launcher to roll between
|
|
||||||
# different repo releases while source versions might require a newer git.
|
|
||||||
#
|
|
||||||
# The soft version is when we start warning users that the version is old and
|
|
||||||
# we'll be dropping support for it. We'll refuse to work with versions older
|
|
||||||
# than the hard version.
|
|
||||||
#
|
|
||||||
# git-1.7 is in (EOL) Ubuntu Precise. git-1.9 is in Ubuntu Trusty.
|
|
||||||
MIN_GIT_VERSION_SOFT = (1, 9, 1)
|
|
||||||
MIN_GIT_VERSION_HARD = (1, 7, 2)
|
|
||||||
GIT_DIR = "GIT_DIR"
|
GIT_DIR = "GIT_DIR"
|
||||||
|
|
||||||
LAST_GITDIR = None
|
LAST_GITDIR = None
|
||||||
LAST_CWD = None
|
LAST_CWD = None
|
||||||
DEFAULT_GIT_FAIL_MESSAGE = "git command failure"
|
DEFAULT_GIT_FAIL_MESSAGE = "git command failure"
|
||||||
|
ERROR_EVENT_LOGGING_PREFIX = "RepoGitCommandError"
|
||||||
# Common line length limit
|
# Common line length limit
|
||||||
GIT_ERROR_STDOUT_LINES = 1
|
GIT_ERROR_STDOUT_LINES = 1
|
||||||
GIT_ERROR_STDERR_LINES = 1
|
GIT_ERROR_STDERR_LINES = 10
|
||||||
INVALID_GIT_EXIT_CODE = 126
|
INVALID_GIT_EXIT_CODE = 126
|
||||||
|
|
||||||
|
logger = RepoLogger(__file__)
|
||||||
|
|
||||||
class _GitCall(object):
|
|
||||||
@functools.lru_cache(maxsize=None)
|
class _GitCall:
|
||||||
|
@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:
|
||||||
msg = "fatal: unable to detect git version"
|
msg = "fatal: unable to detect git version"
|
||||||
print(msg, file=sys.stderr)
|
logger.error(msg)
|
||||||
raise GitRequireError(msg)
|
raise GitRequireError(msg)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -67,7 +62,7 @@ class _GitCall(object):
|
|||||||
def fun(*cmdv):
|
def fun(*cmdv):
|
||||||
command = [name]
|
command = [name]
|
||||||
command.extend(cmdv)
|
command.extend(cmdv)
|
||||||
return GitCommand(None, command).Wait() == 0
|
return GitCommand(None, command, add_event_log=False).Wait() == 0
|
||||||
|
|
||||||
return fun
|
return fun
|
||||||
|
|
||||||
@@ -87,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",
|
||||||
@@ -100,12 +95,50 @@ 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
|
||||||
|
|
||||||
|
|
||||||
class UserAgent(object):
|
@functools.lru_cache(maxsize=None)
|
||||||
|
def GetEventTargetPath():
|
||||||
|
"""Get the 'trace2.eventtarget' path from git configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
path: git config's 'trace2.eventtarget' path if it exists, or None
|
||||||
|
"""
|
||||||
|
path = None
|
||||||
|
cmd = ["config", "--get", "trace2.eventtarget"]
|
||||||
|
# TODO(https://crbug.com/gerrit/13706): Use GitConfig when it supports
|
||||||
|
# system git config variables.
|
||||||
|
p = GitCommand(
|
||||||
|
None,
|
||||||
|
cmd,
|
||||||
|
capture_stdout=True,
|
||||||
|
capture_stderr=True,
|
||||||
|
bare=True,
|
||||||
|
add_event_log=False,
|
||||||
|
)
|
||||||
|
retval = p.Wait()
|
||||||
|
if retval == 0:
|
||||||
|
# Strip trailing carriage-return in path.
|
||||||
|
path = p.stdout.rstrip("\n")
|
||||||
|
if path == "":
|
||||||
|
return None
|
||||||
|
elif retval != 1:
|
||||||
|
# `git config --get` is documented to produce an exit status of `1`
|
||||||
|
# if the requested variable is not present in the configuration.
|
||||||
|
# Report any other return value as an error.
|
||||||
|
logger.error(
|
||||||
|
"repo: error: 'git config --get' call failed with return code: "
|
||||||
|
"%r, stderr: %r",
|
||||||
|
retval,
|
||||||
|
p.stderr,
|
||||||
|
)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
class UserAgent:
|
||||||
"""Mange User-Agent settings when talking to external services
|
"""Mange User-Agent settings when talking to external services
|
||||||
|
|
||||||
We follow the style as documented here:
|
We follow the style as documented here:
|
||||||
@@ -153,12 +186,10 @@ class UserAgent(object):
|
|||||||
def git(self):
|
def git(self):
|
||||||
"""The UA when running git."""
|
"""The UA when running git."""
|
||||||
if self._git_ua is None:
|
if self._git_ua is None:
|
||||||
self._git_ua = "git/%s (%s) git-repo/%s" % (
|
self._git_ua = (
|
||||||
git.version_tuple().full,
|
f"git/{git.version_tuple().full} ({self.os}) "
|
||||||
self.os,
|
f"git-repo/{RepoSourceVersion()}"
|
||||||
RepoSourceVersion(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return self._git_ua
|
return self._git_ua
|
||||||
|
|
||||||
|
|
||||||
@@ -173,8 +204,8 @@ def git_require(min_version, fail=False, msg=""):
|
|||||||
need = ".".join(map(str, min_version))
|
need = ".".join(map(str, min_version))
|
||||||
if msg:
|
if msg:
|
||||||
msg = " for " + msg
|
msg = " for " + msg
|
||||||
error_msg = "fatal: git %s or later required%s" % (need, msg)
|
error_msg = f"fatal: git {need} or later required{msg}"
|
||||||
print(error_msg, file=sys.stderr)
|
logger.error(error_msg)
|
||||||
raise GitRequireError(error_msg)
|
raise GitRequireError(error_msg)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -200,15 +231,15 @@ def _build_env(
|
|||||||
env["GIT_SSH"] = ssh_proxy.proxy
|
env["GIT_SSH"] = ssh_proxy.proxy
|
||||||
env["GIT_SSH_VARIANT"] = "ssh"
|
env["GIT_SSH_VARIANT"] = "ssh"
|
||||||
if "http_proxy" in env and "darwin" == sys.platform:
|
if "http_proxy" in env and "darwin" == sys.platform:
|
||||||
s = "'http.proxy=%s'" % (env["http_proxy"],)
|
s = f"'http.proxy={env['http_proxy']}'"
|
||||||
p = env.get("GIT_CONFIG_PARAMETERS")
|
p = env.get("GIT_CONFIG_PARAMETERS")
|
||||||
if p is not None:
|
if p is not None:
|
||||||
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:
|
||||||
@@ -229,7 +260,7 @@ def _build_env(
|
|||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
class GitCommand(object):
|
class GitCommand:
|
||||||
"""Wrapper around a single git invocation."""
|
"""Wrapper around a single git invocation."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -247,6 +278,8 @@ class GitCommand(object):
|
|||||||
gitdir=None,
|
gitdir=None,
|
||||||
objdir=None,
|
objdir=None,
|
||||||
verify_command=False,
|
verify_command=False,
|
||||||
|
add_event_log=True,
|
||||||
|
log_as_error=True,
|
||||||
):
|
):
|
||||||
if project:
|
if project:
|
||||||
if not cwd:
|
if not cwd:
|
||||||
@@ -257,6 +290,7 @@ class GitCommand(object):
|
|||||||
self.project = project
|
self.project = project
|
||||||
self.cmdv = cmdv
|
self.cmdv = cmdv
|
||||||
self.verify_command = verify_command
|
self.verify_command = verify_command
|
||||||
|
self.stdout, self.stderr = None, None
|
||||||
|
|
||||||
# Git on Windows wants its paths only using / for reliability.
|
# Git on Windows wants its paths only using / for reliability.
|
||||||
if platform_utils.isWindows():
|
if platform_utils.isWindows():
|
||||||
@@ -276,15 +310,70 @@ class GitCommand(object):
|
|||||||
command = [GIT]
|
command = [GIT]
|
||||||
if bare:
|
if bare:
|
||||||
cwd = None
|
cwd = None
|
||||||
command.append(cmdv[0])
|
command_name = cmdv[0]
|
||||||
# Need to use the --progress flag for fetch/clone so output will be
|
command.append(command_name)
|
||||||
# displayed as by default git only does progress output if stderr is a
|
|
||||||
# TTY.
|
if command_name in ("fetch", "clone"):
|
||||||
if sys.stderr.isatty() and cmdv[0] in ("fetch", "clone"):
|
env["GIT_TERMINAL_PROMPT"] = "0"
|
||||||
if "--progress" not in cmdv and "--quiet" not in cmdv:
|
# Need to use the --progress flag for fetch/clone so output will be
|
||||||
command.append("--progress")
|
# displayed as by default git only does progress output if stderr is
|
||||||
|
# a TTY.
|
||||||
|
if sys.stderr.isatty():
|
||||||
|
if "--progress" not in cmdv and "--quiet" not in cmdv:
|
||||||
|
command.append("--progress")
|
||||||
command.extend(cmdv[1:])
|
command.extend(cmdv[1:])
|
||||||
|
|
||||||
|
event_log = (
|
||||||
|
BaseEventLog(env=env, add_init_count=True)
|
||||||
|
if add_event_log
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._RunCommand(
|
||||||
|
command,
|
||||||
|
env,
|
||||||
|
capture_stdout=capture_stdout,
|
||||||
|
capture_stderr=capture_stderr,
|
||||||
|
merge_output=merge_output,
|
||||||
|
ssh_proxy=ssh_proxy,
|
||||||
|
cwd=cwd,
|
||||||
|
input=input,
|
||||||
|
)
|
||||||
|
self.VerifyCommand()
|
||||||
|
except GitCommandError as e:
|
||||||
|
if event_log is not None:
|
||||||
|
error_info = json.dumps(
|
||||||
|
{
|
||||||
|
"ErrorType": type(e).__name__,
|
||||||
|
"Project": e.project,
|
||||||
|
"CommandName": command_name,
|
||||||
|
"Message": str(e),
|
||||||
|
"ReturnCode": (
|
||||||
|
str(e.git_rc) if e.git_rc is not None else None
|
||||||
|
),
|
||||||
|
"IsError": log_as_error,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
event_log.ErrorEvent(
|
||||||
|
f"{ERROR_EVENT_LOGGING_PREFIX}:{error_info}"
|
||||||
|
)
|
||||||
|
event_log.Write(GetEventTargetPath())
|
||||||
|
if isinstance(e, GitPopenCommandError):
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _RunCommand(
|
||||||
|
self,
|
||||||
|
command,
|
||||||
|
env,
|
||||||
|
capture_stdout=False,
|
||||||
|
capture_stderr=False,
|
||||||
|
merge_output=False,
|
||||||
|
ssh_proxy=None,
|
||||||
|
cwd=None,
|
||||||
|
input=None,
|
||||||
|
):
|
||||||
|
# Set subprocess.PIPE for streams that need to be captured.
|
||||||
stdin = subprocess.PIPE if input else None
|
stdin = subprocess.PIPE if input else None
|
||||||
stdout = subprocess.PIPE if capture_stdout else None
|
stdout = subprocess.PIPE if capture_stdout else None
|
||||||
stderr = (
|
stderr = (
|
||||||
@@ -293,6 +382,30 @@ class GitCommand(object):
|
|||||||
else (subprocess.PIPE if capture_stderr else None)
|
else (subprocess.PIPE if capture_stderr else None)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# tee_stderr acts like a tee command for stderr, in that, it captures
|
||||||
|
# stderr from the subprocess and streams it back to sys.stderr, while
|
||||||
|
# keeping a copy in-memory.
|
||||||
|
# This allows us to store stderr logs from the subprocess into
|
||||||
|
# GitCommandError.
|
||||||
|
# Certain git operations, such as `git push`, writes diagnostic logs,
|
||||||
|
# such as, progress bar for pushing, into stderr. To ensure we don't
|
||||||
|
# break git's UX, we need to write to sys.stderr as we read from the
|
||||||
|
# subprocess. Setting encoding or errors makes subprocess return
|
||||||
|
# io.TextIOWrapper, which is line buffered. To avoid line-buffering
|
||||||
|
# while tee-ing stderr, we unset these kwargs. See GitCommand._Tee
|
||||||
|
# for tee-ing between the streams.
|
||||||
|
# We tee stderr iff the caller doesn't want to capture any stream to
|
||||||
|
# not disrupt the existing flow.
|
||||||
|
# See go/tee-repo-stderr for more context.
|
||||||
|
tee_stderr = False
|
||||||
|
kwargs = {"encoding": "utf-8", "errors": "backslashreplace"}
|
||||||
|
if not (stdin or stdout or stderr):
|
||||||
|
tee_stderr = True
|
||||||
|
# stderr will be written back to sys.stderr even though it is
|
||||||
|
# piped here.
|
||||||
|
stderr = subprocess.PIPE
|
||||||
|
kwargs = {}
|
||||||
|
|
||||||
dbg = ""
|
dbg = ""
|
||||||
if IsTrace():
|
if IsTrace():
|
||||||
global LAST_CWD
|
global LAST_CWD
|
||||||
@@ -339,17 +452,16 @@ class GitCommand(object):
|
|||||||
command,
|
command,
|
||||||
cwd=cwd,
|
cwd=cwd,
|
||||||
env=env,
|
env=env,
|
||||||
encoding="utf-8",
|
|
||||||
errors="backslashreplace",
|
|
||||||
stdin=stdin,
|
stdin=stdin,
|
||||||
stdout=stdout,
|
stdout=stdout,
|
||||||
stderr=stderr,
|
stderr=stderr,
|
||||||
|
**kwargs,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise GitCommandError(
|
raise GitPopenCommandError(
|
||||||
message="%s: %s" % (command[1], e),
|
message=f"{command[1]}: {e}",
|
||||||
project=project.name if project else None,
|
project=self.project.name if self.project else None,
|
||||||
command_args=cmdv,
|
command_args=self.cmdv,
|
||||||
)
|
)
|
||||||
|
|
||||||
if ssh_proxy:
|
if ssh_proxy:
|
||||||
@@ -358,12 +470,45 @@ class GitCommand(object):
|
|||||||
self.process = p
|
self.process = p
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.stdout, self.stderr = p.communicate(input=input)
|
if tee_stderr:
|
||||||
|
# tee_stderr streams stderr to sys.stderr while capturing
|
||||||
|
# a copy within self.stderr. tee_stderr is only enabled
|
||||||
|
# when the caller wants to pipe no stream.
|
||||||
|
self.stderr = self._Tee(p.stderr, sys.stderr)
|
||||||
|
else:
|
||||||
|
self.stdout, self.stderr = p.communicate(input=input)
|
||||||
finally:
|
finally:
|
||||||
if ssh_proxy:
|
if ssh_proxy:
|
||||||
ssh_proxy.remove_client(p)
|
ssh_proxy.remove_client(p)
|
||||||
self.rc = p.wait()
|
self.rc = p.wait()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _Tee(in_stream, out_stream):
|
||||||
|
"""Writes text from in_stream to out_stream while recording in buffer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
in_stream: I/O stream to be read from.
|
||||||
|
out_stream: I/O stream to write to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A str containing everything read from the in_stream.
|
||||||
|
"""
|
||||||
|
buffer = ""
|
||||||
|
read_size = 1024 if sys.version_info < (3, 7) else -1
|
||||||
|
chunk = in_stream.read1(read_size)
|
||||||
|
while chunk:
|
||||||
|
# Convert to str.
|
||||||
|
if not hasattr(chunk, "encode"):
|
||||||
|
chunk = chunk.decode("utf-8", "backslashreplace")
|
||||||
|
|
||||||
|
buffer += chunk
|
||||||
|
out_stream.write(chunk)
|
||||||
|
out_stream.flush()
|
||||||
|
|
||||||
|
chunk = in_stream.read1(read_size)
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _GetBasicEnv():
|
def _GetBasicEnv():
|
||||||
"""Return a basic env for running git under.
|
"""Return a basic env for running git under.
|
||||||
@@ -383,16 +528,14 @@ class GitCommand(object):
|
|||||||
env.pop(key, None)
|
env.pop(key, None)
|
||||||
return env
|
return env
|
||||||
|
|
||||||
def Wait(self):
|
def VerifyCommand(self):
|
||||||
if not self.verify_command or self.rc == 0:
|
if self.rc == 0:
|
||||||
return self.rc
|
return None
|
||||||
|
|
||||||
stdout = (
|
stdout = (
|
||||||
"\n".join(self.stdout.split("\n")[:GIT_ERROR_STDOUT_LINES])
|
"\n".join(self.stdout.split("\n")[:GIT_ERROR_STDOUT_LINES])
|
||||||
if self.stdout
|
if self.stdout
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
stderr = (
|
stderr = (
|
||||||
"\n".join(self.stderr.split("\n")[:GIT_ERROR_STDERR_LINES])
|
"\n".join(self.stderr.split("\n")[:GIT_ERROR_STDERR_LINES])
|
||||||
if self.stderr
|
if self.stderr
|
||||||
@@ -407,6 +550,11 @@ class GitCommand(object):
|
|||||||
git_stderr=stderr,
|
git_stderr=stderr,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def Wait(self):
|
||||||
|
if self.verify_command:
|
||||||
|
self.VerifyCommand()
|
||||||
|
return self.rc
|
||||||
|
|
||||||
|
|
||||||
class GitRequireError(RepoExitError):
|
class GitRequireError(RepoExitError):
|
||||||
"""Error raised when git version is unavailable or invalid."""
|
"""Error raised when git version is unavailable or invalid."""
|
||||||
@@ -423,6 +571,29 @@ class GitCommandError(GitError):
|
|||||||
raised exclusively from non-zero exit codes returned from git commands.
|
raised exclusively from non-zero exit codes returned from git commands.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Tuples with error formats and suggestions for those errors.
|
||||||
|
_ERROR_TO_SUGGESTION = [
|
||||||
|
(
|
||||||
|
re.compile("couldn't find remote ref .*"),
|
||||||
|
"Check if the provided ref exists in the remote.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
re.compile("unable to access '.*': .*"),
|
||||||
|
(
|
||||||
|
"Please make sure you have the correct access rights and the "
|
||||||
|
"repository exists."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
re.compile("'.*' does not appear to be a git repository"),
|
||||||
|
"Are you running this repo command outside of a repo workspace?",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
re.compile("not a git repository"),
|
||||||
|
"Are you running this repo command outside of a repo workspace?",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
message: str = DEFAULT_GIT_FAIL_MESSAGE,
|
message: str = DEFAULT_GIT_FAIL_MESSAGE,
|
||||||
@@ -439,13 +610,40 @@ class GitCommandError(GitError):
|
|||||||
self.git_stdout = git_stdout
|
self.git_stdout = git_stdout
|
||||||
self.git_stderr = git_stderr
|
self.git_stderr = git_stderr
|
||||||
|
|
||||||
|
@property
|
||||||
|
@functools.lru_cache(maxsize=None) # noqa: B019
|
||||||
|
def suggestion(self):
|
||||||
|
"""Returns helpful next steps for the given stderr."""
|
||||||
|
if not self.git_stderr:
|
||||||
|
return self.git_stderr
|
||||||
|
|
||||||
|
for err, suggestion in self._ERROR_TO_SUGGESTION:
|
||||||
|
if err.search(self.git_stderr):
|
||||||
|
return suggestion
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
args = "[]" if not self.command_args else " ".join(self.command_args)
|
args = "[]" if not self.command_args else " ".join(self.command_args)
|
||||||
error_type = type(self).__name__
|
error_type = type(self).__name__
|
||||||
return f"""{error_type}: {self.message}
|
string = f"{error_type}: '{args}' on {self.project} failed"
|
||||||
Project: {self.project}
|
|
||||||
Args: {args}
|
if self.message != DEFAULT_GIT_FAIL_MESSAGE:
|
||||||
Stdout:
|
string += f": {self.message}"
|
||||||
{self.git_stdout}
|
|
||||||
Stderr:
|
if self.git_stdout:
|
||||||
{self.git_stderr}"""
|
string += f"\nstdout: {self.git_stdout}"
|
||||||
|
|
||||||
|
if self.git_stderr:
|
||||||
|
string += f"\nstderr: {self.git_stderr}"
|
||||||
|
|
||||||
|
if self.suggestion:
|
||||||
|
string += f"\nsuggestion: {self.suggestion}"
|
||||||
|
|
||||||
|
return string
|
||||||
|
|
||||||
|
|
||||||
|
class GitPopenCommandError(GitError):
|
||||||
|
"""
|
||||||
|
Error raised when subprocess.Popen fails for a GitCommand
|
||||||
|
"""
|
||||||
|
|||||||
+44
-23
@@ -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):
|
||||||
@@ -70,7 +70,7 @@ def _key(name):
|
|||||||
return ".".join(parts)
|
return ".".join(parts)
|
||||||
|
|
||||||
|
|
||||||
class GitConfig(object):
|
class GitConfig:
|
||||||
_ForUser = None
|
_ForUser = None
|
||||||
|
|
||||||
_ForSystem = None
|
_ForSystem = None
|
||||||
@@ -90,6 +90,20 @@ class GitConfig(object):
|
|||||||
|
|
||||||
@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(object):
|
|||||||
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
|
||||||
@@ -180,7 +194,7 @@ class GitConfig(object):
|
|||||||
config_dict[key] = self.GetString(key)
|
config_dict[key] = self.GetString(key)
|
||||||
return config_dict
|
return config_dict
|
||||||
|
|
||||||
def GetBoolean(self, name: str) -> Union[str, None]:
|
def GetBoolean(self, name: str) -> Union[bool, None]:
|
||||||
"""Returns a boolean from the configuration file.
|
"""Returns a boolean from the configuration file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -208,6 +222,12 @@ class GitConfig(object):
|
|||||||
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.
|
||||||
|
|
||||||
@@ -370,7 +390,7 @@ class GitConfig(object):
|
|||||||
with Trace(": parsing %s", self.file):
|
with Trace(": parsing %s", self.file):
|
||||||
with open(self._json) as fd:
|
with open(self._json) as fd:
|
||||||
return json.load(fd)
|
return json.load(fd)
|
||||||
except (IOError, ValueError):
|
except (OSError, ValueError):
|
||||||
platform_utils.remove(self._json, missing_ok=True)
|
platform_utils.remove(self._json, missing_ok=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -378,7 +398,7 @@ class GitConfig(object):
|
|||||||
try:
|
try:
|
||||||
with open(self._json, "w") as fd:
|
with open(self._json, "w") as fd:
|
||||||
json.dump(cache, fd, indent=2)
|
json.dump(cache, fd, indent=2)
|
||||||
except (IOError, TypeError):
|
except (OSError, TypeError):
|
||||||
platform_utils.remove(self._json, missing_ok=True)
|
platform_utils.remove(self._json, missing_ok=True)
|
||||||
|
|
||||||
def _ReadGit(self):
|
def _ReadGit(self):
|
||||||
@@ -418,7 +438,7 @@ class GitConfig(object):
|
|||||||
if p.Wait() == 0:
|
if p.Wait() == 0:
|
||||||
return p.stdout
|
return p.stdout
|
||||||
else:
|
else:
|
||||||
raise GitError("git config %s: %s" % (str(args), p.stderr))
|
raise GitError(f"git {' '.join(command)}: {p.stderr}")
|
||||||
|
|
||||||
|
|
||||||
class RepoConfig(GitConfig):
|
class RepoConfig(GitConfig):
|
||||||
@@ -430,7 +450,7 @@ class RepoConfig(GitConfig):
|
|||||||
return os.path.join(repo_config_dir, ".repoconfig/config")
|
return os.path.join(repo_config_dir, ".repoconfig/config")
|
||||||
|
|
||||||
|
|
||||||
class RefSpec(object):
|
class RefSpec:
|
||||||
"""A Git refspec line, split into its components:
|
"""A Git refspec line, split into its components:
|
||||||
|
|
||||||
forced: True if the line starts with '+'
|
forced: True if the line starts with '+'
|
||||||
@@ -541,7 +561,7 @@ def GetUrlCookieFile(url, quiet):
|
|||||||
yield cookiefile, None
|
yield cookiefile, None
|
||||||
|
|
||||||
|
|
||||||
class Remote(object):
|
class Remote:
|
||||||
"""Configuration options related to a remote."""
|
"""Configuration options related to a remote."""
|
||||||
|
|
||||||
def __init__(self, config, name):
|
def __init__(self, config, name):
|
||||||
@@ -651,13 +671,11 @@ class Remote(object):
|
|||||||
userEmail, host, port
|
userEmail, host, port
|
||||||
)
|
)
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
raise UploadError("%s: %s" % (self.review, str(e)))
|
raise UploadError(f"{self.review}: {str(e)}")
|
||||||
except urllib.error.URLError as e:
|
except urllib.error.URLError as e:
|
||||||
raise UploadError("%s: %s" % (self.review, str(e)))
|
raise UploadError(f"{self.review}: {str(e)}")
|
||||||
except http.client.HTTPException as e:
|
except http.client.HTTPException as e:
|
||||||
raise UploadError(
|
raise UploadError(f"{self.review}: {e.__class__.__name__}")
|
||||||
"%s: %s" % (self.review, e.__class__.__name__)
|
|
||||||
)
|
|
||||||
|
|
||||||
REVIEW_CACHE[u] = self._review_url
|
REVIEW_CACHE[u] = self._review_url
|
||||||
return self._review_url + self.projectname
|
return self._review_url + self.projectname
|
||||||
@@ -666,7 +684,7 @@ class Remote(object):
|
|||||||
username = self._config.GetString("review.%s.username" % self.review)
|
username = self._config.GetString("review.%s.username" % self.review)
|
||||||
if username is None:
|
if username is None:
|
||||||
username = userEmail.split("@")[0]
|
username = userEmail.split("@")[0]
|
||||||
return "ssh://%s@%s:%s/" % (username, host, port)
|
return f"ssh://{username}@{host}:{port}/"
|
||||||
|
|
||||||
def ToLocal(self, rev):
|
def ToLocal(self, rev):
|
||||||
"""Convert a remote revision string to something we have locally."""
|
"""Convert a remote revision string to something we have locally."""
|
||||||
@@ -706,7 +724,10 @@ class Remote(object):
|
|||||||
def Save(self):
|
def Save(self):
|
||||||
"""Save this remote to the configuration."""
|
"""Save this remote to the configuration."""
|
||||||
self._Set("url", self.url)
|
self._Set("url", self.url)
|
||||||
if self.pushUrl is not None:
|
# projectname is initialized for projects listed in the manifest, but
|
||||||
|
# not for others (e.g. the manifest project). This class is used for
|
||||||
|
# all of them.
|
||||||
|
if self.pushUrl is not None and self.projectname is not None:
|
||||||
self._Set("pushurl", self.pushUrl + "/" + self.projectname)
|
self._Set("pushurl", self.pushUrl + "/" + self.projectname)
|
||||||
else:
|
else:
|
||||||
self._Set("pushurl", self.pushUrl)
|
self._Set("pushurl", self.pushUrl)
|
||||||
@@ -715,15 +736,15 @@ class Remote(object):
|
|||||||
self._Set("fetch", list(map(str, self.fetch)))
|
self._Set("fetch", list(map(str, self.fetch)))
|
||||||
|
|
||||||
def _Set(self, key, value):
|
def _Set(self, key, value):
|
||||||
key = "remote.%s.%s" % (self.name, key)
|
key = f"remote.{self.name}.{key}"
|
||||||
return self._config.SetString(key, value)
|
return self._config.SetString(key, value)
|
||||||
|
|
||||||
def _Get(self, key, all_keys=False):
|
def _Get(self, key, all_keys=False):
|
||||||
key = "remote.%s.%s" % (self.name, key)
|
key = f"remote.{self.name}.{key}"
|
||||||
return self._config.GetString(key, all_keys=all_keys)
|
return self._config.GetString(key, all_keys=all_keys)
|
||||||
|
|
||||||
|
|
||||||
class Branch(object):
|
class Branch:
|
||||||
"""Configuration options related to a single branch."""
|
"""Configuration options related to a single branch."""
|
||||||
|
|
||||||
def __init__(self, config, name):
|
def __init__(self, config, name):
|
||||||
@@ -762,11 +783,11 @@ class Branch(object):
|
|||||||
fd.write("\tmerge = %s\n" % self.merge)
|
fd.write("\tmerge = %s\n" % self.merge)
|
||||||
|
|
||||||
def _Set(self, key, value):
|
def _Set(self, key, value):
|
||||||
key = "branch.%s.%s" % (self.name, key)
|
key = f"branch.{self.name}.{key}"
|
||||||
return self._config.SetString(key, value)
|
return self._config.SetString(key, value)
|
||||||
|
|
||||||
def _Get(self, key, all_keys=False):
|
def _Get(self, key, all_keys=False):
|
||||||
key = "branch.%s.%s" % (self.name, key)
|
key = f"branch.{self.name}.{key}"
|
||||||
return self._config.GetString(key, all_keys=all_keys)
|
return self._config.GetString(key, all_keys=all_keys)
|
||||||
|
|
||||||
|
|
||||||
@@ -795,8 +816,8 @@ class SyncAnalysisState:
|
|||||||
to be logged.
|
to be logged.
|
||||||
"""
|
"""
|
||||||
self._config = config
|
self._config = config
|
||||||
now = datetime.datetime.utcnow()
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
self._Set("main.synctime", now.isoformat(timespec="microseconds") + "Z")
|
self._Set("main.synctime", now.isoformat(timespec="microseconds"))
|
||||||
self._Set("main.version", "1")
|
self._Set("main.version", "1")
|
||||||
self._Set("sys.argv", sys.argv)
|
self._Set("sys.argv", sys.argv)
|
||||||
for key, value in superproject_logging_data.items():
|
for key, value in superproject_logging_data.items():
|
||||||
|
|||||||
+89
-60
@@ -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
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ R_WORKTREE_M = R_WORKTREE + "m/"
|
|||||||
R_M = "refs/remotes/m/"
|
R_M = "refs/remotes/m/"
|
||||||
|
|
||||||
|
|
||||||
class GitRefs(object):
|
class GitRefs:
|
||||||
def __init__(self, gitdir):
|
def __init__(self, gitdir):
|
||||||
self._gitdir = gitdir
|
self._gitdir = gitdir
|
||||||
self._phyref = None
|
self._phyref = None
|
||||||
@@ -86,9 +87,8 @@ class GitRefs(object):
|
|||||||
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,66 +102,95 @@ class GitRefs(object):
|
|||||||
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")
|
||||||
try:
|
self._TrackMtime("packed-refs")
|
||||||
fd = open(path, "r")
|
self._TrackTreeMtimes("refs")
|
||||||
mtime = os.path.getmtime(path)
|
self._TrackTreeMtimes("reftable")
|
||||||
except IOError:
|
|
||||||
|
@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
|
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:
|
||||||
|
self._mtime[name] = 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");
|
||||||
|
|||||||
+133
-44
@@ -23,16 +23,21 @@ 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 IsId
|
||||||
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"
|
||||||
@@ -66,12 +71,12 @@ class UpdateProjectsResult(NamedTuple):
|
|||||||
fatal: bool
|
fatal: bool
|
||||||
|
|
||||||
|
|
||||||
class Superproject(object):
|
class Superproject:
|
||||||
"""Get commit ids from superproject.
|
"""Get commit ids from superproject.
|
||||||
|
|
||||||
Initializes a local copy of a superproject for the manifest. This allows
|
Initializes a bare local copy of a superproject for the manifest. This
|
||||||
lookup of commit ids for all projects. It contains _project_commit_ids which
|
allows lookup of commit ids for all projects. It contains
|
||||||
is a dictionary with project/commit id entries.
|
_project_commit_ids which is a dictionary with project/commit id entries.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -96,7 +101,7 @@ class Superproject(object):
|
|||||||
self._manifest = manifest
|
self._manifest = manifest
|
||||||
self.name = name
|
self.name = name
|
||||||
self.remote = remote
|
self.remote = remote
|
||||||
self.revision = self._branch = revision
|
self.revision = revision
|
||||||
self._repodir = manifest.repodir
|
self._repodir = manifest.repodir
|
||||||
self._superproject_dir = superproject_dir
|
self._superproject_dir = superproject_dir
|
||||||
self._superproject_path = manifest.SubmanifestInfoDir(
|
self._superproject_path = manifest.SubmanifestInfoDir(
|
||||||
@@ -128,6 +133,34 @@ class Superproject(object):
|
|||||||
"""Set the _print_messages attribute."""
|
"""Set the _print_messages attribute."""
|
||||||
self._print_messages = value
|
self._print_messages = value
|
||||||
|
|
||||||
|
def SetRevisionId(self, revision_id: str) -> None:
|
||||||
|
"""Set the revisionId of the superproject to sync to."""
|
||||||
|
self.revision = revision_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def commit_id(self):
|
||||||
|
"""Returns the commit ID of the superproject checkout."""
|
||||||
|
cmd = ["rev-parse", self.revision]
|
||||||
|
p = GitCommand(
|
||||||
|
None, # project
|
||||||
|
cmd,
|
||||||
|
gitdir=self._work_git,
|
||||||
|
bare=True,
|
||||||
|
capture_stdout=True,
|
||||||
|
capture_stderr=True,
|
||||||
|
)
|
||||||
|
retval = p.Wait()
|
||||||
|
if retval != 0:
|
||||||
|
self._LogWarning(
|
||||||
|
"git rev-parse call failed, command: git {}, "
|
||||||
|
"return code: {}, stderr: {}",
|
||||||
|
cmd,
|
||||||
|
retval,
|
||||||
|
p.stderr,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
return p.stdout
|
||||||
|
|
||||||
@property
|
@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,17 +173,39 @@ class Superproject(object):
|
|||||||
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"""
|
||||||
return (
|
return (
|
||||||
f"repo superproject branch: {self._branch} url: {self._remote_url}"
|
f"repo superproject revision: {self.revision} "
|
||||||
|
f"url: {self._remote_url}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _LogError(self, fmt, *inputs):
|
def _LogError(self, fmt, *inputs):
|
||||||
@@ -169,30 +224,63 @@ class Superproject(object):
|
|||||||
"""
|
"""
|
||||||
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|.
|
||||||
@@ -230,12 +318,20 @@ class Superproject(object):
|
|||||||
if rev_commit:
|
if rev_commit:
|
||||||
cmd.extend(["--negotiation-tip", rev_commit])
|
cmd.extend(["--negotiation-tip", rev_commit])
|
||||||
|
|
||||||
if self._branch:
|
if self.revision:
|
||||||
cmd += [self._branch + ":" + self._branch]
|
# If revision is a commit hash, fetch it directly to avoid
|
||||||
|
# creating a local branch of the same name.
|
||||||
|
refspec = (
|
||||||
|
self.revision
|
||||||
|
if IsId(self.revision)
|
||||||
|
else f"{self.revision}:{self.revision}"
|
||||||
|
)
|
||||||
|
cmd.append(refspec)
|
||||||
p = GitCommand(
|
p = GitCommand(
|
||||||
None,
|
None,
|
||||||
cmd,
|
cmd,
|
||||||
cwd=self._work_git,
|
gitdir=self._work_git,
|
||||||
|
bare=True,
|
||||||
capture_stdout=True,
|
capture_stdout=True,
|
||||||
capture_stderr=True,
|
capture_stderr=True,
|
||||||
)
|
)
|
||||||
@@ -257,7 +353,7 @@ class Superproject(object):
|
|||||||
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(
|
||||||
@@ -265,13 +361,14 @@ class Superproject(object):
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
data = None
|
data = None
|
||||||
branch = "HEAD" if not self._branch else self._branch
|
branch = "HEAD" if not self.revision else self.revision
|
||||||
cmd = ["ls-tree", "-z", "-r", branch]
|
cmd = ["ls-tree", "-z", "-r", branch]
|
||||||
|
|
||||||
p = GitCommand(
|
p = GitCommand(
|
||||||
None,
|
None,
|
||||||
cmd,
|
cmd,
|
||||||
cwd=self._work_git,
|
gitdir=self._work_git,
|
||||||
|
bare=True,
|
||||||
capture_stdout=True,
|
capture_stdout=True,
|
||||||
capture_stderr=True,
|
capture_stderr=True,
|
||||||
)
|
)
|
||||||
@@ -286,6 +383,7 @@ class Superproject(object):
|
|||||||
retval,
|
retval,
|
||||||
p.stderr,
|
p.stderr,
|
||||||
)
|
)
|
||||||
|
return None
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def Sync(self, git_event_log):
|
def Sync(self, git_event_log):
|
||||||
@@ -305,8 +403,6 @@ class Superproject(object):
|
|||||||
)
|
)
|
||||||
return SyncResult(False, False)
|
return SyncResult(False, False)
|
||||||
|
|
||||||
_PrintBetaNotice()
|
|
||||||
|
|
||||||
should_exit = True
|
should_exit = True
|
||||||
if not self._remote_url:
|
if not self._remote_url:
|
||||||
self._LogWarning(
|
self._LogWarning(
|
||||||
@@ -317,6 +413,8 @@ class Superproject(object):
|
|||||||
|
|
||||||
if not self._Init():
|
if not self._Init():
|
||||||
return SyncResult(False, should_exit)
|
return SyncResult(False, should_exit)
|
||||||
|
if IsId(self.revision) and self.commit_id:
|
||||||
|
return SyncResult(True, False)
|
||||||
if not self._Fetch():
|
if not self._Fetch():
|
||||||
return SyncResult(False, should_exit)
|
return SyncResult(False, should_exit)
|
||||||
if not self._quiet:
|
if not self._quiet:
|
||||||
@@ -375,13 +473,14 @@ class Superproject(object):
|
|||||||
)
|
)
|
||||||
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:
|
||||||
with open(manifest_path, "w", encoding="utf-8") as fp:
|
with open(manifest_path, "w", encoding="utf-8") as fp:
|
||||||
fp.write(manifest_str)
|
fp.write(manifest_str)
|
||||||
except IOError as e:
|
except OSError as e:
|
||||||
self._LogError("cannot write manifest to : {} {}", manifest_path, e)
|
self._LogError("cannot write manifest to : {} {}", manifest_path, e)
|
||||||
return None
|
return None
|
||||||
return manifest_path
|
return manifest_path
|
||||||
@@ -450,16 +549,6 @@ class Superproject(object):
|
|||||||
return UpdateProjectsResult(manifest_path, False)
|
return UpdateProjectsResult(manifest_path, False)
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache(maxsize=10)
|
|
||||||
def _PrintBetaNotice():
|
|
||||||
"""Print the notice of beta status."""
|
|
||||||
print(
|
|
||||||
"NOTICE: --use-superproject is in beta; report any issues to the "
|
|
||||||
"address described in `repo version`",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache(maxsize=None)
|
@functools.lru_cache(maxsize=None)
|
||||||
def _UseSuperprojectFromConfiguration():
|
def _UseSuperprojectFromConfiguration():
|
||||||
"""Returns the user choice of whether to use superproject."""
|
"""Returns the user choice of whether to use superproject."""
|
||||||
|
|||||||
+10
-337
@@ -12,36 +12,14 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
"""Provide event logging in the git trace2 EVENT format.
|
"""Event logging in the git trace2 EVENT format."""
|
||||||
|
|
||||||
The git trace2 EVENT format is defined at:
|
from git_command import GetEventTargetPath
|
||||||
https://www.kernel.org/pub/software/scm/git/docs/technical/api-trace2.html#_event_format
|
|
||||||
https://git-scm.com/docs/api-trace2#_the_event_format_target
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
git_trace_log = EventLog()
|
|
||||||
git_trace_log.StartEvent()
|
|
||||||
...
|
|
||||||
git_trace_log.ExitEvent()
|
|
||||||
git_trace_log.Write()
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import errno
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from git_command import GitCommand
|
|
||||||
from git_command import RepoSourceVersion
|
from git_command import RepoSourceVersion
|
||||||
|
from git_trace2_event_log_base import BaseEventLog
|
||||||
|
|
||||||
|
|
||||||
class EventLog(object):
|
class EventLog(BaseEventLog):
|
||||||
"""Event log that records events that occurred during a repo invocation.
|
"""Event log that records events that occurred during a repo invocation.
|
||||||
|
|
||||||
Events are written to the log as a consecutive JSON entries, one per line.
|
Events are written to the log as a consecutive JSON entries, one per line.
|
||||||
@@ -58,318 +36,13 @@ class EventLog(object):
|
|||||||
https://git-scm.com/docs/api-trace2#_event_format
|
https://git-scm.com/docs/api-trace2#_event_format
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, env=None):
|
def __init__(self, **kwargs):
|
||||||
"""Initializes the event log."""
|
super().__init__(repo_source_version=RepoSourceVersion(), **kwargs)
|
||||||
self._log = []
|
|
||||||
# Try to get session-id (sid) from environment (setup in repo launcher).
|
|
||||||
KEY = "GIT_TRACE2_PARENT_SID"
|
|
||||||
if env is None:
|
|
||||||
env = os.environ
|
|
||||||
|
|
||||||
self.start = datetime.datetime.utcnow()
|
def Write(self, path=None, **kwargs):
|
||||||
|
|
||||||
# Save both our sid component and the complete sid.
|
|
||||||
# We use our sid component (self._sid) as the unique filename prefix and
|
|
||||||
# the full sid (self._full_sid) in the log itself.
|
|
||||||
self._sid = "repo-%s-P%08x" % (
|
|
||||||
self.start.strftime("%Y%m%dT%H%M%SZ"),
|
|
||||||
os.getpid(),
|
|
||||||
)
|
|
||||||
parent_sid = env.get(KEY)
|
|
||||||
# Append our sid component to the parent sid (if it exists).
|
|
||||||
if parent_sid is not None:
|
|
||||||
self._full_sid = parent_sid + "/" + self._sid
|
|
||||||
else:
|
|
||||||
self._full_sid = self._sid
|
|
||||||
|
|
||||||
# Set/update the environment variable.
|
|
||||||
# Environment handling across systems is messy.
|
|
||||||
try:
|
|
||||||
env[KEY] = self._full_sid
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
env[KEY] = self._full_sid.encode()
|
|
||||||
|
|
||||||
# Add a version event to front of the log.
|
|
||||||
self._AddVersionEvent()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def full_sid(self):
|
|
||||||
return self._full_sid
|
|
||||||
|
|
||||||
def _AddVersionEvent(self):
|
|
||||||
"""Adds a 'version' event at the beginning of current log."""
|
|
||||||
version_event = self._CreateEventDict("version")
|
|
||||||
version_event["evt"] = "2"
|
|
||||||
version_event["exe"] = RepoSourceVersion()
|
|
||||||
self._log.insert(0, version_event)
|
|
||||||
|
|
||||||
def _CreateEventDict(self, event_name):
|
|
||||||
"""Returns a dictionary with common keys/values for git trace2 events.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
event_name: The event name.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary with the common event fields populated.
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
"event": event_name,
|
|
||||||
"sid": self._full_sid,
|
|
||||||
"thread": threading.current_thread().name,
|
|
||||||
"time": datetime.datetime.utcnow().isoformat() + "Z",
|
|
||||||
}
|
|
||||||
|
|
||||||
def StartEvent(self):
|
|
||||||
"""Append a 'start' event to the current log."""
|
|
||||||
start_event = self._CreateEventDict("start")
|
|
||||||
start_event["argv"] = sys.argv
|
|
||||||
self._log.append(start_event)
|
|
||||||
|
|
||||||
def ExitEvent(self, result):
|
|
||||||
"""Append an 'exit' event to the current log.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
result: Exit code of the event
|
|
||||||
"""
|
|
||||||
exit_event = self._CreateEventDict("exit")
|
|
||||||
|
|
||||||
# Consider 'None' success (consistent with event_log result handling).
|
|
||||||
if result is None:
|
|
||||||
result = 0
|
|
||||||
exit_event["code"] = result
|
|
||||||
time_delta = datetime.datetime.utcnow() - self.start
|
|
||||||
exit_event["t_abs"] = time_delta.total_seconds()
|
|
||||||
self._log.append(exit_event)
|
|
||||||
|
|
||||||
def CommandEvent(self, name, subcommands):
|
|
||||||
"""Append a 'command' event to the current log.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: Name of the primary command (ex: repo, git)
|
|
||||||
subcommands: List of the sub-commands (ex: version, init, sync)
|
|
||||||
"""
|
|
||||||
command_event = self._CreateEventDict("command")
|
|
||||||
command_event["name"] = name
|
|
||||||
command_event["subcommands"] = subcommands
|
|
||||||
self._log.append(command_event)
|
|
||||||
|
|
||||||
def LogConfigEvents(self, config, event_dict_name):
|
|
||||||
"""Append a |event_dict_name| event for each config key in |config|.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config: Configuration dictionary.
|
|
||||||
event_dict_name: Name of the event dictionary for items to be logged
|
|
||||||
under.
|
|
||||||
"""
|
|
||||||
for param, value in config.items():
|
|
||||||
event = self._CreateEventDict(event_dict_name)
|
|
||||||
event["param"] = param
|
|
||||||
event["value"] = value
|
|
||||||
self._log.append(event)
|
|
||||||
|
|
||||||
def DefParamRepoEvents(self, config):
|
|
||||||
"""Append 'def_param' events for repo config keys to the current log.
|
|
||||||
|
|
||||||
This appends one event for each repo.* config key.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config: Repo configuration dictionary
|
|
||||||
"""
|
|
||||||
# Only output the repo.* config parameters.
|
|
||||||
repo_config = {k: v for k, v in config.items() if k.startswith("repo.")}
|
|
||||||
self.LogConfigEvents(repo_config, "def_param")
|
|
||||||
|
|
||||||
def GetDataEventName(self, value):
|
|
||||||
"""Returns 'data-json' if the value is an array else returns 'data'."""
|
|
||||||
return "data-json" if value[0] == "[" and value[-1] == "]" else "data"
|
|
||||||
|
|
||||||
def LogDataConfigEvents(self, config, prefix):
|
|
||||||
"""Append a 'data' event for each entry in |config| to the current log.
|
|
||||||
|
|
||||||
For each keyX and valueX of the config, "key" field of the event is
|
|
||||||
'|prefix|/keyX' and the "value" of the "key" field is valueX.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config: Configuration dictionary.
|
|
||||||
prefix: Prefix for each key that is logged.
|
|
||||||
"""
|
|
||||||
for key, value in config.items():
|
|
||||||
event = self._CreateEventDict(self.GetDataEventName(value))
|
|
||||||
event["key"] = f"{prefix}/{key}"
|
|
||||||
event["value"] = value
|
|
||||||
self._log.append(event)
|
|
||||||
|
|
||||||
def ErrorEvent(self, msg, fmt=None):
|
|
||||||
"""Append a 'error' event to the current log."""
|
|
||||||
error_event = self._CreateEventDict("error")
|
|
||||||
if fmt is None:
|
|
||||||
fmt = msg
|
|
||||||
error_event["msg"] = f"RepoErrorEvent:{msg}"
|
|
||||||
error_event["fmt"] = f"RepoErrorEvent:{fmt}"
|
|
||||||
self._log.append(error_event)
|
|
||||||
|
|
||||||
def _GetEventTargetPath(self):
|
|
||||||
"""Get the 'trace2.eventtarget' path from git configuration.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
path: git config's 'trace2.eventtarget' path if it exists, or None
|
|
||||||
"""
|
|
||||||
path = None
|
|
||||||
cmd = ["config", "--get", "trace2.eventtarget"]
|
|
||||||
# TODO(https://crbug.com/gerrit/13706): Use GitConfig when it supports
|
|
||||||
# system git config variables.
|
|
||||||
p = GitCommand(
|
|
||||||
None, cmd, capture_stdout=True, capture_stderr=True, bare=True
|
|
||||||
)
|
|
||||||
retval = p.Wait()
|
|
||||||
if retval == 0:
|
|
||||||
# Strip trailing carriage-return in path.
|
|
||||||
path = p.stdout.rstrip("\n")
|
|
||||||
elif retval != 1:
|
|
||||||
# `git config --get` is documented to produce an exit status of `1`
|
|
||||||
# if the requested variable is not present in the configuration.
|
|
||||||
# Report any other return value as an error.
|
|
||||||
print(
|
|
||||||
"repo: error: 'git config --get' call failed with return code: "
|
|
||||||
"%r, stderr: %r" % (retval, p.stderr),
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return path
|
|
||||||
|
|
||||||
def _WriteLog(self, write_fn):
|
|
||||||
"""Writes the log out using a provided writer function.
|
|
||||||
|
|
||||||
Generate compact JSON output for each item in the log, and write it
|
|
||||||
using write_fn.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
write_fn: A function that accepts byts and writes them to a
|
|
||||||
destination.
|
|
||||||
"""
|
|
||||||
|
|
||||||
for e in self._log:
|
|
||||||
# Dump in compact encoding mode.
|
|
||||||
# See 'Compact encoding' in Python docs:
|
|
||||||
# https://docs.python.org/3/library/json.html#module-json
|
|
||||||
write_fn(
|
|
||||||
json.dumps(e, indent=None, separators=(",", ":")).encode(
|
|
||||||
"utf-8"
|
|
||||||
)
|
|
||||||
+ b"\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
def Write(self, path=None):
|
|
||||||
"""Writes the log out to a file or socket.
|
|
||||||
|
|
||||||
Log is only written if 'path' or 'git config --get trace2.eventtarget'
|
|
||||||
provide a valid path (or socket) to write logs to.
|
|
||||||
|
|
||||||
Logging filename format follows the git trace2 style of being a unique
|
|
||||||
(exclusive writable) file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path: Path to where logs should be written. The path may have a
|
|
||||||
prefix of the form "af_unix:[{stream|dgram}:]", in which case
|
|
||||||
the path is treated as a Unix domain socket. See
|
|
||||||
https://git-scm.com/docs/api-trace2#_enabling_a_target for
|
|
||||||
details.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
log_path: Path to the log file or socket if log is written,
|
|
||||||
otherwise None
|
|
||||||
"""
|
|
||||||
log_path = None
|
|
||||||
# If no logging path is specified, get the path from
|
|
||||||
# 'trace2.eventtarget'.
|
|
||||||
if path is None:
|
if path is None:
|
||||||
path = self._GetEventTargetPath()
|
path = self._GetEventTargetPath()
|
||||||
|
return super().Write(path=path, **kwargs)
|
||||||
|
|
||||||
# If no logging path is specified, exit.
|
def _GetEventTargetPath(self):
|
||||||
if path is None:
|
return GetEventTargetPath()
|
||||||
return None
|
|
||||||
|
|
||||||
path_is_socket = False
|
|
||||||
socket_type = None
|
|
||||||
if isinstance(path, str):
|
|
||||||
parts = path.split(":", 1)
|
|
||||||
if parts[0] == "af_unix" and len(parts) == 2:
|
|
||||||
path_is_socket = True
|
|
||||||
path = parts[1]
|
|
||||||
parts = path.split(":", 1)
|
|
||||||
if parts[0] == "stream" and len(parts) == 2:
|
|
||||||
socket_type = socket.SOCK_STREAM
|
|
||||||
path = parts[1]
|
|
||||||
elif parts[0] == "dgram" and len(parts) == 2:
|
|
||||||
socket_type = socket.SOCK_DGRAM
|
|
||||||
path = parts[1]
|
|
||||||
else:
|
|
||||||
# Get absolute path.
|
|
||||||
path = os.path.abspath(os.path.expanduser(path))
|
|
||||||
else:
|
|
||||||
raise TypeError("path: str required but got %s." % type(path))
|
|
||||||
|
|
||||||
# Git trace2 requires a directory to write log to.
|
|
||||||
|
|
||||||
# TODO(https://crbug.com/gerrit/13706): Support file (append) mode also.
|
|
||||||
if not (path_is_socket or os.path.isdir(path)):
|
|
||||||
return None
|
|
||||||
|
|
||||||
if path_is_socket:
|
|
||||||
if socket_type == socket.SOCK_STREAM or socket_type is None:
|
|
||||||
try:
|
|
||||||
with socket.socket(
|
|
||||||
socket.AF_UNIX, socket.SOCK_STREAM
|
|
||||||
) as sock:
|
|
||||||
sock.connect(path)
|
|
||||||
self._WriteLog(sock.sendall)
|
|
||||||
return f"af_unix:stream:{path}"
|
|
||||||
except OSError as err:
|
|
||||||
# If we tried to connect to a DGRAM socket using STREAM,
|
|
||||||
# ignore the attempt and continue to DGRAM below. Otherwise,
|
|
||||||
# issue a warning.
|
|
||||||
if err.errno != errno.EPROTOTYPE:
|
|
||||||
print(
|
|
||||||
f"repo: warning: git trace2 logging failed: {err}",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
if socket_type == socket.SOCK_DGRAM or socket_type is None:
|
|
||||||
try:
|
|
||||||
with socket.socket(
|
|
||||||
socket.AF_UNIX, socket.SOCK_DGRAM
|
|
||||||
) as sock:
|
|
||||||
self._WriteLog(lambda bs: sock.sendto(bs, path))
|
|
||||||
return f"af_unix:dgram:{path}"
|
|
||||||
except OSError as err:
|
|
||||||
print(
|
|
||||||
f"repo: warning: git trace2 logging failed: {err}",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
# Tried to open a socket but couldn't connect (SOCK_STREAM) or write
|
|
||||||
# (SOCK_DGRAM).
|
|
||||||
print(
|
|
||||||
"repo: warning: git trace2 logging failed: could not write to "
|
|
||||||
"socket",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Path is an absolute path
|
|
||||||
# Use NamedTemporaryFile to generate a unique filename as required by
|
|
||||||
# git trace2.
|
|
||||||
try:
|
|
||||||
with tempfile.NamedTemporaryFile(
|
|
||||||
mode="xb", prefix=self._sid, dir=path, delete=False
|
|
||||||
) as f:
|
|
||||||
# TODO(https://crbug.com/gerrit/13706): Support writing events
|
|
||||||
# as they occur.
|
|
||||||
self._WriteLog(f.write)
|
|
||||||
log_path = f.name
|
|
||||||
except FileExistsError as err:
|
|
||||||
print(
|
|
||||||
"repo: warning: git trace2 logging failed: %r" % err,
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
return log_path
|
|
||||||
|
|||||||
@@ -0,0 +1,362 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Provide event logging in the git trace2 EVENT format.
|
||||||
|
|
||||||
|
The git trace2 EVENT format is defined at:
|
||||||
|
https://www.kernel.org/pub/software/scm/git/docs/technical/api-trace2.html#_event_format
|
||||||
|
https://git-scm.com/docs/api-trace2#_the_event_format_target
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
git_trace_log = EventLog()
|
||||||
|
git_trace_log.StartEvent()
|
||||||
|
...
|
||||||
|
git_trace_log.ExitEvent()
|
||||||
|
git_trace_log.Write()
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import errno
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
# Timeout when sending events via socket (applies to connect, send)
|
||||||
|
SOCK_TIMEOUT = 0.5 # in seconds
|
||||||
|
# BaseEventLog __init__ Counter that is consistent within the same process
|
||||||
|
p_init_count = 0
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEventLog:
|
||||||
|
"""Event log that records events that occurred during a repo invocation.
|
||||||
|
|
||||||
|
Events are written to the log as a consecutive JSON entries, one per line.
|
||||||
|
Entries follow the git trace2 EVENT format.
|
||||||
|
|
||||||
|
Each entry contains the following common keys:
|
||||||
|
- event: The event name
|
||||||
|
- sid: session-id - Unique string to allow process instance to be
|
||||||
|
identified.
|
||||||
|
- thread: The thread name.
|
||||||
|
- time: is the UTC time of the event.
|
||||||
|
|
||||||
|
Valid 'event' names and event specific fields are documented here:
|
||||||
|
https://git-scm.com/docs/api-trace2#_event_format
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, env=None, repo_source_version=None, add_init_count=False
|
||||||
|
):
|
||||||
|
"""Initializes the event log."""
|
||||||
|
global p_init_count
|
||||||
|
p_init_count += 1
|
||||||
|
self._log = []
|
||||||
|
self.verbose = False
|
||||||
|
# Try to get session-id (sid) from environment (setup in repo launcher).
|
||||||
|
KEY = "GIT_TRACE2_PARENT_SID"
|
||||||
|
if env is None:
|
||||||
|
env = os.environ
|
||||||
|
|
||||||
|
self.start = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
|
||||||
|
# Save both our sid component and the complete sid.
|
||||||
|
# We use our sid component (self._sid) as the unique filename prefix and
|
||||||
|
# the full sid (self._full_sid) in the log itself.
|
||||||
|
self._sid = (
|
||||||
|
f"repo-{self.start.strftime('%Y%m%dT%H%M%SZ')}-P{os.getpid():08x}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if add_init_count:
|
||||||
|
self._sid = f"{self._sid}-{p_init_count}"
|
||||||
|
|
||||||
|
parent_sid = env.get(KEY)
|
||||||
|
# Append our sid component to the parent sid (if it exists).
|
||||||
|
if parent_sid is not None:
|
||||||
|
self._full_sid = parent_sid + "/" + self._sid
|
||||||
|
else:
|
||||||
|
self._full_sid = self._sid
|
||||||
|
|
||||||
|
# Set/update the environment variable.
|
||||||
|
# Environment handling across systems is messy.
|
||||||
|
try:
|
||||||
|
env[KEY] = self._full_sid
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
env[KEY] = self._full_sid.encode()
|
||||||
|
|
||||||
|
if repo_source_version is not None:
|
||||||
|
# Add a version event to front of the log.
|
||||||
|
self._AddVersionEvent(repo_source_version)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_sid(self):
|
||||||
|
return self._full_sid
|
||||||
|
|
||||||
|
def _AddVersionEvent(self, repo_source_version):
|
||||||
|
"""Adds a 'version' event at the beginning of current log."""
|
||||||
|
version_event = self._CreateEventDict("version")
|
||||||
|
version_event["evt"] = "2"
|
||||||
|
version_event["exe"] = repo_source_version
|
||||||
|
self._log.insert(0, version_event)
|
||||||
|
|
||||||
|
def _CreateEventDict(self, event_name):
|
||||||
|
"""Returns a dictionary with common keys/values for git trace2 events.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_name: The event name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with the common event fields populated.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"event": event_name,
|
||||||
|
"sid": self._full_sid,
|
||||||
|
"thread": threading.current_thread().name,
|
||||||
|
"time": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def StartEvent(self, argv):
|
||||||
|
"""Append a 'start' event to the current log."""
|
||||||
|
start_event = self._CreateEventDict("start")
|
||||||
|
start_event["argv"] = argv
|
||||||
|
self._log.append(start_event)
|
||||||
|
|
||||||
|
def ExitEvent(self, result):
|
||||||
|
"""Append an 'exit' event to the current log.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result: Exit code of the event
|
||||||
|
"""
|
||||||
|
exit_event = self._CreateEventDict("exit")
|
||||||
|
|
||||||
|
# Consider 'None' success (consistent with event_log result handling).
|
||||||
|
if result is None:
|
||||||
|
result = 0
|
||||||
|
exit_event["code"] = result
|
||||||
|
time_delta = datetime.datetime.now(datetime.timezone.utc) - self.start
|
||||||
|
exit_event["t_abs"] = time_delta.total_seconds()
|
||||||
|
self._log.append(exit_event)
|
||||||
|
|
||||||
|
def CommandEvent(self, name, subcommands):
|
||||||
|
"""Append a 'command' event to the current log.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Name of the primary command (ex: repo, git)
|
||||||
|
subcommands: List of the sub-commands (ex: version, init, sync)
|
||||||
|
"""
|
||||||
|
command_event = self._CreateEventDict("cmd_name")
|
||||||
|
name = f"{name}-"
|
||||||
|
name += "-".join(subcommands)
|
||||||
|
command_event["name"] = name
|
||||||
|
command_event["hierarchy"] = name
|
||||||
|
self._log.append(command_event)
|
||||||
|
|
||||||
|
def LogConfigEvents(self, config, event_dict_name):
|
||||||
|
"""Append a |event_dict_name| event for each config key in |config|.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Configuration dictionary.
|
||||||
|
event_dict_name: Name of the event dictionary for items to be logged
|
||||||
|
under.
|
||||||
|
"""
|
||||||
|
for param, value in config.items():
|
||||||
|
event = self._CreateEventDict(event_dict_name)
|
||||||
|
event["param"] = param
|
||||||
|
event["value"] = value
|
||||||
|
self._log.append(event)
|
||||||
|
|
||||||
|
def DefParamRepoEvents(self, config):
|
||||||
|
"""Append 'def_param' events for repo config keys to the current log.
|
||||||
|
|
||||||
|
This appends one event for each repo.* config key.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Repo configuration dictionary
|
||||||
|
"""
|
||||||
|
# Only output the repo.* config parameters.
|
||||||
|
repo_config = {k: v for k, v in config.items() if k.startswith("repo.")}
|
||||||
|
self.LogConfigEvents(repo_config, "def_param")
|
||||||
|
|
||||||
|
def GetDataEventName(self, value):
|
||||||
|
"""Returns 'data-json' if the value is an array else returns 'data'."""
|
||||||
|
return "data-json" if value[0] == "[" and value[-1] == "]" else "data"
|
||||||
|
|
||||||
|
def LogDataConfigEvents(self, config, prefix):
|
||||||
|
"""Append a 'data' event for each entry in |config| to the current log.
|
||||||
|
|
||||||
|
For each keyX and valueX of the config, "key" field of the event is
|
||||||
|
'|prefix|/keyX' and the "value" of the "key" field is valueX.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: Configuration dictionary.
|
||||||
|
prefix: Prefix for each key that is logged.
|
||||||
|
"""
|
||||||
|
for key, value in config.items():
|
||||||
|
event = self._CreateEventDict(self.GetDataEventName(value))
|
||||||
|
event["key"] = f"{prefix}/{key}"
|
||||||
|
event["value"] = value
|
||||||
|
self._log.append(event)
|
||||||
|
|
||||||
|
def ErrorEvent(self, msg, fmt=None):
|
||||||
|
"""Append a 'error' event to the current log."""
|
||||||
|
error_event = self._CreateEventDict("error")
|
||||||
|
if fmt is None:
|
||||||
|
fmt = msg
|
||||||
|
error_event["msg"] = f"RepoErrorEvent:{msg}"
|
||||||
|
error_event["fmt"] = f"RepoErrorEvent:{fmt}"
|
||||||
|
self._log.append(error_event)
|
||||||
|
|
||||||
|
def _WriteLog(self, write_fn):
|
||||||
|
"""Writes the log out using a provided writer function.
|
||||||
|
|
||||||
|
Generate compact JSON output for each item in the log, and write it
|
||||||
|
using write_fn.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
write_fn: A function that accepts byts and writes them to a
|
||||||
|
destination.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for e in self._log:
|
||||||
|
# Dump in compact encoding mode.
|
||||||
|
# See 'Compact encoding' in Python docs:
|
||||||
|
# https://docs.python.org/3/library/json.html#module-json
|
||||||
|
write_fn(
|
||||||
|
json.dumps(e, indent=None, separators=(",", ":")).encode(
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
|
+ b"\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
def Write(self, path=None):
|
||||||
|
"""Writes the log out to a file or socket.
|
||||||
|
|
||||||
|
Log is only written if 'path' or 'git config --get trace2.eventtarget'
|
||||||
|
provide a valid path (or socket) to write logs to.
|
||||||
|
|
||||||
|
Logging filename format follows the git trace2 style of being a unique
|
||||||
|
(exclusive writable) file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: Path to where logs should be written. The path may have a
|
||||||
|
prefix of the form "af_unix:[{stream|dgram}:]", in which case
|
||||||
|
the path is treated as a Unix domain socket. See
|
||||||
|
https://git-scm.com/docs/api-trace2#_enabling_a_target for
|
||||||
|
details.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
log_path: Path to the log file or socket if log is written,
|
||||||
|
otherwise None
|
||||||
|
"""
|
||||||
|
log_path = None
|
||||||
|
# If no logging path is specified, exit.
|
||||||
|
if path is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
path_is_socket = False
|
||||||
|
socket_type = None
|
||||||
|
if isinstance(path, str):
|
||||||
|
parts = path.split(":", 1)
|
||||||
|
if parts[0] == "af_unix" and len(parts) == 2:
|
||||||
|
path_is_socket = True
|
||||||
|
path = parts[1]
|
||||||
|
parts = path.split(":", 1)
|
||||||
|
if parts[0] == "stream" and len(parts) == 2:
|
||||||
|
socket_type = socket.SOCK_STREAM
|
||||||
|
path = parts[1]
|
||||||
|
elif parts[0] == "dgram" and len(parts) == 2:
|
||||||
|
socket_type = socket.SOCK_DGRAM
|
||||||
|
path = parts[1]
|
||||||
|
else:
|
||||||
|
# Get absolute path.
|
||||||
|
path = os.path.abspath(os.path.expanduser(path))
|
||||||
|
else:
|
||||||
|
raise TypeError("path: str required but got %s." % type(path))
|
||||||
|
|
||||||
|
# Git trace2 requires a directory to write log to.
|
||||||
|
|
||||||
|
# TODO(https://crbug.com/gerrit/13706): Support file (append) mode also.
|
||||||
|
if not (path_is_socket or os.path.isdir(path)):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if path_is_socket:
|
||||||
|
if socket_type == socket.SOCK_STREAM or socket_type is None:
|
||||||
|
try:
|
||||||
|
with socket.socket(
|
||||||
|
socket.AF_UNIX, socket.SOCK_STREAM
|
||||||
|
) as sock:
|
||||||
|
sock.settimeout(SOCK_TIMEOUT)
|
||||||
|
sock.connect(path)
|
||||||
|
self._WriteLog(sock.sendall)
|
||||||
|
return f"af_unix:stream:{path}"
|
||||||
|
except OSError as err:
|
||||||
|
# If we tried to connect to a DGRAM socket using STREAM,
|
||||||
|
# ignore the attempt and continue to DGRAM below. Otherwise,
|
||||||
|
# issue a warning.
|
||||||
|
if err.errno != errno.EPROTOTYPE:
|
||||||
|
if self.verbose:
|
||||||
|
print(
|
||||||
|
"repo: warning: git trace2 logging failed:",
|
||||||
|
f"{err}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
if socket_type == socket.SOCK_DGRAM or socket_type is None:
|
||||||
|
try:
|
||||||
|
with socket.socket(
|
||||||
|
socket.AF_UNIX, socket.SOCK_DGRAM
|
||||||
|
) as sock:
|
||||||
|
self._WriteLog(lambda bs: sock.sendto(bs, path))
|
||||||
|
return f"af_unix:dgram:{path}"
|
||||||
|
except OSError as err:
|
||||||
|
if self.verbose:
|
||||||
|
print(
|
||||||
|
f"repo: warning: git trace2 logging failed: {err}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
# Tried to open a socket but couldn't connect (SOCK_STREAM) or write
|
||||||
|
# (SOCK_DGRAM).
|
||||||
|
if self.verbose:
|
||||||
|
print(
|
||||||
|
"repo: warning: git trace2 logging failed: could not"
|
||||||
|
"write to socket",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Path is an absolute path
|
||||||
|
# Use NamedTemporaryFile to generate a unique filename as required by
|
||||||
|
# git trace2.
|
||||||
|
try:
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode="xb", prefix=self._sid, dir=path, delete=False
|
||||||
|
) as f:
|
||||||
|
# TODO(https://crbug.com/gerrit/13706): Support writing events
|
||||||
|
# as they occur.
|
||||||
|
self._WriteLog(f.write)
|
||||||
|
log_path = f.name
|
||||||
|
except FileExistsError as err:
|
||||||
|
if self.verbose:
|
||||||
|
print(
|
||||||
|
"repo: warning: git trace2 logging failed: %r" % err,
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
return log_path
|
||||||
@@ -12,11 +12,8 @@
|
|||||||
# 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 errno
|
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
@@ -25,7 +22,14 @@ from error import HookError
|
|||||||
from git_refs import HEAD
|
from git_refs import HEAD
|
||||||
|
|
||||||
|
|
||||||
class RepoHook(object):
|
# The API we've documented to hook authors. Keep in sync with repo-hooks.md.
|
||||||
|
_API_ARGS = {
|
||||||
|
"pre-upload": {"project_list", "worktree_list"},
|
||||||
|
"post-sync": {"repo_topdir", "sync_duration_seconds"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RepoHook:
|
||||||
"""A RepoHook contains information about a script to run as a hook.
|
"""A RepoHook contains information about a script to run as a hook.
|
||||||
|
|
||||||
Hooks are used to run a python script before running an upload (for
|
Hooks are used to run a python script before running an upload (for
|
||||||
@@ -59,6 +63,7 @@ class RepoHook(object):
|
|||||||
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,
|
||||||
@@ -78,6 +83,7 @@ class RepoHook(object):
|
|||||||
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'.
|
||||||
@@ -88,18 +94,18 @@ class RepoHook(object):
|
|||||||
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.
|
||||||
@@ -183,7 +189,7 @@ class RepoHook(object):
|
|||||||
abort_if_user_denies was passed to the consturctor.
|
abort_if_user_denies was passed to the consturctor.
|
||||||
"""
|
"""
|
||||||
hooks_config = self._hooks_project.config
|
hooks_config = self._hooks_project.config
|
||||||
git_approval_key = "repo.hooks.%s.%s" % (self._hook_type, subkey)
|
git_approval_key = f"repo.hooks.{self._hook_type}.{subkey}"
|
||||||
|
|
||||||
# Get the last value that the user approved for this hook; may be None.
|
# Get the last value that the user approved for this hook; may be None.
|
||||||
old_val = hooks_config.GetString(git_approval_key)
|
old_val = hooks_config.GetString(git_approval_key)
|
||||||
@@ -196,7 +202,7 @@ class RepoHook(object):
|
|||||||
else:
|
else:
|
||||||
# Give the user a reason why we're prompting, since they last
|
# Give the user a reason why we're prompting, since they last
|
||||||
# told us to "never ask again".
|
# told us to "never ask again".
|
||||||
prompt = "WARNING: %s\n\n" % (changed_prompt,)
|
prompt = f"WARNING: {changed_prompt}\n\n"
|
||||||
else:
|
else:
|
||||||
prompt = ""
|
prompt = ""
|
||||||
|
|
||||||
@@ -244,9 +250,8 @@ class RepoHook(object):
|
|||||||
return self._CheckForHookApprovalHelper(
|
return self._CheckForHookApprovalHelper(
|
||||||
"approvedmanifest",
|
"approvedmanifest",
|
||||||
self._manifest_url,
|
self._manifest_url,
|
||||||
"Run hook scripts from %s" % (self._manifest_url,),
|
f"Run hook scripts from {self._manifest_url}",
|
||||||
"Manifest URL has changed since %s was allowed."
|
f"Manifest URL has changed since {self._hook_type} was allowed.",
|
||||||
% (self._hook_type,),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _CheckForHookApprovalHash(self):
|
def _CheckForHookApprovalHash(self):
|
||||||
@@ -265,7 +270,7 @@ class RepoHook(object):
|
|||||||
"approvedhash",
|
"approvedhash",
|
||||||
self._GetHash(),
|
self._GetHash(),
|
||||||
prompt % (self._GetMustVerb(), self._script_fullpath),
|
prompt % (self._GetMustVerb(), self._script_fullpath),
|
||||||
"Scripts have changed since %s was allowed." % (self._hook_type,),
|
f"Scripts have changed since {self._hook_type} was allowed.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -298,43 +303,6 @@ class RepoHook(object):
|
|||||||
|
|
||||||
return interp
|
return interp
|
||||||
|
|
||||||
def _ExecuteHookViaReexec(self, interp, context, **kwargs):
|
|
||||||
"""Execute the hook script through |interp|.
|
|
||||||
|
|
||||||
Note: Support for this feature should be dropped ~Jun 2021.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
interp: The Python program to run.
|
|
||||||
context: Basic Python context to execute the hook inside.
|
|
||||||
kwargs: Arbitrary arguments to pass to the hook script.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HookError: When the hooks failed for any reason.
|
|
||||||
"""
|
|
||||||
# This logic needs to be kept in sync with _ExecuteHookViaImport below.
|
|
||||||
script = """
|
|
||||||
import json, os, sys
|
|
||||||
path = '''%(path)s'''
|
|
||||||
kwargs = json.loads('''%(kwargs)s''')
|
|
||||||
context = json.loads('''%(context)s''')
|
|
||||||
sys.path.insert(0, os.path.dirname(path))
|
|
||||||
data = open(path).read()
|
|
||||||
exec(compile(data, path, 'exec'), context)
|
|
||||||
context['main'](**kwargs)
|
|
||||||
""" % {
|
|
||||||
"path": self._script_fullpath,
|
|
||||||
"kwargs": json.dumps(kwargs),
|
|
||||||
"context": json.dumps(context),
|
|
||||||
}
|
|
||||||
|
|
||||||
# We pass the script via stdin to avoid OS argv limits. It also makes
|
|
||||||
# unhandled exception tracebacks less verbose/confusing for users.
|
|
||||||
cmd = [interp, "-c", "import sys; exec(sys.stdin.read())"]
|
|
||||||
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
|
|
||||||
proc.communicate(input=script.encode("utf-8"))
|
|
||||||
if proc.returncode:
|
|
||||||
raise HookError("Failed to run %s hook." % (self._hook_type,))
|
|
||||||
|
|
||||||
def _ExecuteHookViaImport(self, data, context, **kwargs):
|
def _ExecuteHookViaImport(self, data, context, **kwargs):
|
||||||
"""Execute the hook code in |data| directly.
|
"""Execute the hook code in |data| directly.
|
||||||
|
|
||||||
@@ -412,30 +380,13 @@ context['main'](**kwargs)
|
|||||||
# See what version of python the hook has been written against.
|
# See what version of python the hook has been written against.
|
||||||
data = open(self._script_fullpath).read()
|
data = open(self._script_fullpath).read()
|
||||||
interp = self._ExtractInterpFromShebang(data)
|
interp = self._ExtractInterpFromShebang(data)
|
||||||
reexec = False
|
|
||||||
if interp:
|
if interp:
|
||||||
prog = os.path.basename(interp)
|
prog = os.path.basename(interp)
|
||||||
if prog.startswith("python2") and sys.version_info.major != 2:
|
if prog.startswith("python2"):
|
||||||
reexec = True
|
raise HookError("Python 2 is not supported")
|
||||||
elif prog.startswith("python3") and sys.version_info.major == 2:
|
|
||||||
reexec = True
|
|
||||||
|
|
||||||
# Attempt to execute the hooks through the requested version of
|
|
||||||
# Python.
|
|
||||||
if reexec:
|
|
||||||
try:
|
|
||||||
self._ExecuteHookViaReexec(interp, context, **kwargs)
|
|
||||||
except OSError as e:
|
|
||||||
if e.errno == errno.ENOENT:
|
|
||||||
# We couldn't find the interpreter, so fallback to
|
|
||||||
# importing.
|
|
||||||
reexec = False
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Run the hook by importing directly.
|
# Run the hook by importing directly.
|
||||||
if not reexec:
|
self._ExecuteHookViaImport(data, context, **kwargs)
|
||||||
self._ExecuteHookViaImport(data, context, **kwargs)
|
|
||||||
finally:
|
finally:
|
||||||
# Restore sys.path and CWD.
|
# Restore sys.path and CWD.
|
||||||
sys.path = orig_syspath
|
sys.path = orig_syspath
|
||||||
@@ -472,11 +423,26 @@ context['main'](**kwargs)
|
|||||||
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
|
||||||
@@ -530,6 +496,7 @@ context['main'](**kwargs)
|
|||||||
"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)
|
||||||
|
|||||||
+20
-11
@@ -1,5 +1,8 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# From Gerrit Code Review 3.6.1 c67916dbdc07555c44e32a68f92ffc484b9b34f0
|
# DO NOT EDIT THIS FILE
|
||||||
|
# All updates should be sent upstream: https://gerrit.googlesource.com/gerrit/
|
||||||
|
# This is synced from commit: 62f5bbea67f6dafa6e22a601a0c298214c510caf
|
||||||
|
# DO NOT EDIT THIS FILE
|
||||||
#
|
#
|
||||||
# Part of Gerrit Code Review (https://www.gerritcodereview.com/)
|
# Part of Gerrit Code Review (https://www.gerritcodereview.com/)
|
||||||
#
|
#
|
||||||
@@ -31,14 +34,20 @@ if test ! -f "$1" ; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Do not create a change id if requested
|
# Do not create a change id if requested
|
||||||
if test "false" = "$(git config --bool --get gerrit.createChangeId)" ; then
|
case "$(git config --get gerrit.createChangeId)" in
|
||||||
exit 0
|
false)
|
||||||
fi
|
exit 0
|
||||||
|
;;
|
||||||
|
always)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Do not create a change id for squash/fixup commits.
|
||||||
|
if head -n1 "$1" | LC_ALL=C grep -q '^[a-z][a-z]*! '; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
# Do not create a change id for squash commits.
|
|
||||||
if head -n1 "$1" | grep -q '^squash! '; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if git rev-parse --verify HEAD >/dev/null 2>&1; then
|
if git rev-parse --verify HEAD >/dev/null 2>&1; then
|
||||||
refhash="$(git rev-parse HEAD)"
|
refhash="$(git rev-parse HEAD)"
|
||||||
@@ -51,7 +60,7 @@ dest="$1.tmp.${random}"
|
|||||||
|
|
||||||
trap 'rm -f "$dest" "$dest-2"' EXIT
|
trap 'rm -f "$dest" "$dest-2"' EXIT
|
||||||
|
|
||||||
if ! git stripspace --strip-comments < "$1" > "${dest}" ; then
|
if ! cat "$1" | sed -e '/>8/q' | git stripspace --strip-comments > "${dest}" ; then
|
||||||
echo "cannot strip comments from $1"
|
echo "cannot strip comments from $1"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -65,7 +74,7 @@ reviewurl="$(git config --get gerrit.reviewUrl)"
|
|||||||
if test -n "${reviewurl}" ; then
|
if test -n "${reviewurl}" ; then
|
||||||
token="Link"
|
token="Link"
|
||||||
value="${reviewurl%/}/id/I$random"
|
value="${reviewurl%/}/id/I$random"
|
||||||
pattern=".*/id/I[0-9a-f]\{40\}$"
|
pattern=".*/id/I[0-9a-f]\{40\}"
|
||||||
else
|
else
|
||||||
token="Change-Id"
|
token="Change-Id"
|
||||||
value="I$random"
|
value="I$random"
|
||||||
@@ -92,7 +101,7 @@ fi
|
|||||||
# Avoid the --where option which only appeared in Git 2.15
|
# Avoid the --where option which only appeared in Git 2.15
|
||||||
if ! git -c trailer.where=before interpret-trailers \
|
if ! git -c trailer.where=before interpret-trailers \
|
||||||
--trailer "Signed-off-by: $token: $value" < "$dest-2" |
|
--trailer "Signed-off-by: $token: $value" < "$dest-2" |
|
||||||
sed -re "s/^Signed-off-by: ($token: )/\1/" \
|
sed -e "s/^Signed-off-by: \($token: \)/\1/" \
|
||||||
-e "/^Signed-off-by: SENTINEL/d" > "$dest" ; then
|
-e "/^Signed-off-by: SENTINEL/d" > "$dest" ; then
|
||||||
echo "cannot insert $token line in $1"
|
echo "cannot insert $token line in $1"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
+17
-30
@@ -1,33 +1,25 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
# DO NOT EDIT THIS FILE
|
||||||
|
# All updates should be sent upstream: https://github.com/git/git
|
||||||
|
# This is synced from commit: 00e10ef10e161a913893b8cb33aa080d4ca5baa6
|
||||||
|
# DO NOT EDIT THIS FILE
|
||||||
#
|
#
|
||||||
# An example hook script to verify if you are on battery, in case you
|
# An example hook script to verify if you are on battery, in case you
|
||||||
# are running Windows, Linux or OS X. Called by git-gc --auto with no
|
# are running Linux or OS X. Called by git-gc --auto with no arguments.
|
||||||
# arguments. The hook should exit with non-zero status after issuing an
|
# The hook should exit with non-zero status after issuing an appropriate
|
||||||
# appropriate message if it wants to stop the auto repacking.
|
# message if it wants to stop the auto repacking.
|
||||||
|
|
||||||
# This program is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 2 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
#
|
||||||
# This program is distributed in the hope that it will be useful,
|
# This hook is stored in the contrib/hooks directory. Your distribution
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# may have put this somewhere else. If you want to use this hook, you
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# should make this script executable then link to it in the repository
|
||||||
# GNU General Public License for more details.
|
# you would like to use it in.
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU General Public License
|
# For example, if the hook is stored in
|
||||||
# along with this program; if not, write to the Free Software
|
# /usr/share/git-core/contrib/hooks/pre-auto-gc-battery:
|
||||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
#
|
||||||
|
# cd /path/to/your/repository.git
|
||||||
if uname -s | grep -q "_NT-"
|
# ln -sf /usr/share/git-core/contrib/hooks/pre-auto-gc-battery \
|
||||||
then
|
# hooks/pre-auto-gc
|
||||||
if test -x $SYSTEMROOT/System32/Wbem/wmic
|
|
||||||
then
|
|
||||||
STATUS=$(wmic path win32_battery get batterystatus /format:list | tr -d '\r\n')
|
|
||||||
[ "$STATUS" = "BatteryStatus=2" ] && exit 0 || exit 1
|
|
||||||
fi
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if test -x /sbin/on_ac_power && (/sbin/on_ac_power;test $? -ne 1)
|
if test -x /sbin/on_ac_power && (/sbin/on_ac_power;test $? -ne 1)
|
||||||
then
|
then
|
||||||
@@ -48,11 +40,6 @@ elif test -x /usr/bin/pmset && /usr/bin/pmset -g batt |
|
|||||||
grep -q "drawing from 'AC Power'"
|
grep -q "drawing from 'AC Power'"
|
||||||
then
|
then
|
||||||
exit 0
|
exit 0
|
||||||
elif test -d /sys/bus/acpi/drivers/battery && test 0 = \
|
|
||||||
"$(find /sys/bus/acpi/drivers/battery/ -type l | wc -l)";
|
|
||||||
then
|
|
||||||
# No battery exists.
|
|
||||||
exit 0
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Auto packing deferred; not on AC"
|
echo "Auto packing deferred; not on AC"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
#
|
|
||||||
# Copyright (C) 2008 The Android Open Source Project
|
# Copyright (C) 2008 The Android Open Source Project
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
@@ -20,6 +19,7 @@ People shouldn't run this directly; instead, they should use the `repo` wrapper
|
|||||||
which takes care of execing this entry point.
|
which takes care of execing this entry point.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import difflib
|
||||||
import getpass
|
import getpass
|
||||||
import json
|
import json
|
||||||
import netrc
|
import netrc
|
||||||
@@ -30,8 +30,11 @@ 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
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import kerberos
|
import kerberos
|
||||||
@@ -43,9 +46,9 @@ 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 NoManifestException
|
from error import NoManifestException
|
||||||
from error import NoSuchProjectError
|
from error import NoSuchProjectError
|
||||||
from error import RepoChangedException
|
from error import RepoChangedException
|
||||||
@@ -69,6 +72,9 @@ from wrapper import Wrapper
|
|||||||
from wrapper import WrapperPath
|
from wrapper import WrapperPath
|
||||||
|
|
||||||
|
|
||||||
|
logger = RepoLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
# NB: These do not need to be kept in sync with the repo launcher script.
|
# NB: These do not need to be kept in sync with the repo launcher script.
|
||||||
# These may be much newer as it allows the repo launcher to roll between
|
# These may be much newer as it allows the repo launcher to roll between
|
||||||
# different repo releases while source versions might require a newer python.
|
# different repo releases while source versions might require a newer python.
|
||||||
@@ -81,27 +87,19 @@ from wrapper import WrapperPath
|
|||||||
MIN_PYTHON_VERSION_SOFT = (3, 6)
|
MIN_PYTHON_VERSION_SOFT = (3, 6)
|
||||||
MIN_PYTHON_VERSION_HARD = (3, 6)
|
MIN_PYTHON_VERSION_HARD = (3, 6)
|
||||||
|
|
||||||
if sys.version_info.major < 3:
|
if sys.version_info < MIN_PYTHON_VERSION_HARD:
|
||||||
print(
|
logger.error(
|
||||||
"repo: error: Python 2 is no longer supported; "
|
"repo: error: Python version is too old; "
|
||||||
"Please upgrade to Python {}.{}+.".format(*MIN_PYTHON_VERSION_SOFT),
|
"Please upgrade to Python %d.%d+.",
|
||||||
file=sys.stderr,
|
*MIN_PYTHON_VERSION_SOFT,
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
elif sys.version_info < MIN_PYTHON_VERSION_SOFT:
|
||||||
if sys.version_info < MIN_PYTHON_VERSION_HARD:
|
logger.error(
|
||||||
print(
|
"repo: warning: your Python version is no longer supported; "
|
||||||
"repo: error: Python 3 version is too old; "
|
"Please upgrade to Python %d.%d+.",
|
||||||
"Please upgrade to Python {}.{}+.".format(*MIN_PYTHON_VERSION_SOFT),
|
*MIN_PYTHON_VERSION_SOFT,
|
||||||
file=sys.stderr,
|
)
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
elif sys.version_info < MIN_PYTHON_VERSION_SOFT:
|
|
||||||
print(
|
|
||||||
"repo: warning: your Python 3 version is no longer supported; "
|
|
||||||
"Please upgrade to Python {}.{}+.".format(*MIN_PYTHON_VERSION_SOFT),
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
|
|
||||||
KEYBOARD_INTERRUPT_EXIT = 128 + signal.SIGINT
|
KEYBOARD_INTERRUPT_EXIT = 128 + signal.SIGINT
|
||||||
MAX_PRINT_ERRORS = 5
|
MAX_PRINT_ERRORS = 5
|
||||||
@@ -189,7 +187,7 @@ global_options.add_option(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class _Repo(object):
|
class _Repo:
|
||||||
def __init__(self, repodir):
|
def __init__(self, repodir):
|
||||||
self.repodir = repodir
|
self.repodir = repodir
|
||||||
self.commands = all_commands
|
self.commands = all_commands
|
||||||
@@ -201,9 +199,8 @@ class _Repo(object):
|
|||||||
if short:
|
if short:
|
||||||
commands = " ".join(sorted(self.commands))
|
commands = " ".join(sorted(self.commands))
|
||||||
wrapped_commands = textwrap.wrap(commands, width=77)
|
wrapped_commands = textwrap.wrap(commands, width=77)
|
||||||
print(
|
help_commands = "".join(f"\n {x}" for x in wrapped_commands)
|
||||||
"Available commands:\n %s" % ("\n ".join(wrapped_commands),)
|
print(f"Available commands:{help_commands}")
|
||||||
)
|
|
||||||
print("\nRun `repo help <command>` for command-specific details.")
|
print("\nRun `repo help <command>` for command-specific details.")
|
||||||
print("Bug reports:", Wrapper().BUG_URL)
|
print("Bug reports:", Wrapper().BUG_URL)
|
||||||
else:
|
else:
|
||||||
@@ -239,7 +236,7 @@ class _Repo(object):
|
|||||||
if name in self.commands:
|
if name in self.commands:
|
||||||
return name, []
|
return name, []
|
||||||
|
|
||||||
key = "alias.%s" % (name,)
|
key = f"alias.{name}"
|
||||||
alias = RepoConfig.ForRepository(self.repodir).GetString(key)
|
alias = RepoConfig.ForRepository(self.repodir).GetString(key)
|
||||||
if alias is None:
|
if alias is None:
|
||||||
alias = RepoConfig.ForUser().GetString(key)
|
alias = RepoConfig.ForUser().GetString(key)
|
||||||
@@ -273,10 +270,14 @@ class _Repo(object):
|
|||||||
self._PrintHelp(short=True)
|
self._PrintHelp(short=True)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
run = lambda: self._RunLong(name, gopts, argv) or 0
|
git_trace2_event_log = EventLog()
|
||||||
|
run = (
|
||||||
|
lambda: self._RunLong(name, gopts, argv, git_trace2_event_log) or 0
|
||||||
|
)
|
||||||
with Trace(
|
with Trace(
|
||||||
"starting new command: %s",
|
"starting new command: %s [sid=%s]",
|
||||||
", ".join([name] + argv),
|
", ".join([name] + argv),
|
||||||
|
git_trace2_event_log.full_sid,
|
||||||
first_trace=True,
|
first_trace=True,
|
||||||
):
|
):
|
||||||
if gopts.trace_python:
|
if gopts.trace_python:
|
||||||
@@ -293,12 +294,107 @@ class _Repo(object):
|
|||||||
result = run()
|
result = run()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _RunLong(self, name, gopts, argv):
|
def _autocorrect_command_name(
|
||||||
|
self, name: str, config: RepoConfig
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""Autocorrect command name based on user's git config."""
|
||||||
|
|
||||||
|
close_commands = difflib.get_close_matches(
|
||||||
|
name, self.commands.keys(), n=5, cutoff=0.7
|
||||||
|
)
|
||||||
|
|
||||||
|
if not close_commands:
|
||||||
|
logger.error(
|
||||||
|
"repo: '%s' is not a repo command. See 'repo help'.", name
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
assumed = close_commands[0]
|
||||||
|
autocorrect = config.GetString("help.autocorrect")
|
||||||
|
|
||||||
|
# If there are multiple close matches, git won't automatically run one.
|
||||||
|
# We'll always prompt instead of guessing.
|
||||||
|
if len(close_commands) > 1:
|
||||||
|
autocorrect = "prompt"
|
||||||
|
|
||||||
|
# Handle git configuration boolean values:
|
||||||
|
# 0, "false", "off", "no", "show": show suggestion (default)
|
||||||
|
# 1, "true", "on", "yes", "immediate": run suggestion immediately
|
||||||
|
# "never": don't run or show any suggested command
|
||||||
|
# "prompt": show the suggestion and prompt for confirmation
|
||||||
|
# positive number > 1: run suggestion after specified deciseconds
|
||||||
|
if autocorrect is None:
|
||||||
|
autocorrect = "0"
|
||||||
|
|
||||||
|
autocorrect = autocorrect.lower()
|
||||||
|
|
||||||
|
if autocorrect in ("0", "false", "off", "no", "show"):
|
||||||
|
autocorrect = 0
|
||||||
|
elif autocorrect in ("true", "on", "yes", "immediate"):
|
||||||
|
autocorrect = -1 # immediate
|
||||||
|
elif autocorrect == "never":
|
||||||
|
return None
|
||||||
|
elif autocorrect == "prompt":
|
||||||
|
logger.warning(
|
||||||
|
"You called a repo command named "
|
||||||
|
"'%s', which does not exist.",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
resp = input(f"Run '{assumed}' instead [y/N]? ")
|
||||||
|
if resp.lower().startswith("y"):
|
||||||
|
return assumed
|
||||||
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
autocorrect = int(autocorrect)
|
||||||
|
except ValueError:
|
||||||
|
autocorrect = 0
|
||||||
|
|
||||||
|
if autocorrect != 0:
|
||||||
|
if autocorrect < 0:
|
||||||
|
logger.warning(
|
||||||
|
"You called a repo command named "
|
||||||
|
"'%s', which does not exist.\n"
|
||||||
|
"Continuing assuming that "
|
||||||
|
"you meant '%s'.",
|
||||||
|
name,
|
||||||
|
assumed,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
delay = autocorrect * 0.1
|
||||||
|
logger.warning(
|
||||||
|
"You called a repo command named "
|
||||||
|
"'%s', which does not exist.\n"
|
||||||
|
"Continuing in %.1f seconds, assuming "
|
||||||
|
"that you meant '%s'.",
|
||||||
|
name,
|
||||||
|
delay,
|
||||||
|
assumed,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
time.sleep(delay)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
return None
|
||||||
|
return assumed
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
"repo: '%s' is not a repo command. See 'repo help'.", name
|
||||||
|
)
|
||||||
|
logger.warning(
|
||||||
|
"The most similar command%s\n\t%s",
|
||||||
|
"s are" if len(close_commands) > 1 else " is",
|
||||||
|
"\n\t".join(close_commands),
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _RunLong(self, name, gopts, argv, git_trace2_event_log):
|
||||||
"""Execute the (longer running) requested subcommand."""
|
"""Execute the (longer running) requested subcommand."""
|
||||||
result = 0
|
result = 0
|
||||||
SetDefaultColoring(gopts.color)
|
SetDefaultColoring(gopts.color)
|
||||||
|
|
||||||
git_trace2_event_log = EventLog()
|
|
||||||
outer_client = RepoClient(self.repodir)
|
outer_client = RepoClient(self.repodir)
|
||||||
repo_client = outer_client
|
repo_client = outer_client
|
||||||
if gopts.submanifest_path:
|
if gopts.submanifest_path:
|
||||||
@@ -308,49 +404,42 @@ class _Repo(object):
|
|||||||
outer_client=outer_client,
|
outer_client=outer_client,
|
||||||
)
|
)
|
||||||
|
|
||||||
if Wrapper().gitc_parse_clientdir(os.getcwd()):
|
if name not in self.commands:
|
||||||
print("GITC is not supported.", file=sys.stderr)
|
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:
|
|
||||||
print(
|
|
||||||
"repo: '%s' is not a repo command. See 'repo help'." % name,
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
Editor.globalConfig = cmd.client.globalConfig
|
Editor.globalConfig = cmd.client.globalConfig
|
||||||
|
|
||||||
if not isinstance(cmd, MirrorSafeCommand) and cmd.manifest.IsMirror:
|
if not isinstance(cmd, MirrorSafeCommand) and cmd.manifest.IsMirror:
|
||||||
print(
|
logger.error("fatal: '%s' requires a working directory", name)
|
||||||
"fatal: '%s' requires a working directory" % name,
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
copts, cargs = cmd.OptionParser.parse_args(argv)
|
copts, cargs = cmd.OptionParser.parse_args(argv)
|
||||||
copts = cmd.ReadEnvironmentOptions(copts)
|
copts = cmd.ReadEnvironmentOptions(copts)
|
||||||
except NoManifestException as e:
|
except NoManifestException as e:
|
||||||
print(
|
logger.error("error: in `%s`: %s", " ".join([name] + argv), e)
|
||||||
"error: in `%s`: %s" % (" ".join([name] + argv), str(e)),
|
logger.error(
|
||||||
file=sys.stderr,
|
"error: manifest missing or unreadable -- please run init"
|
||||||
)
|
|
||||||
print(
|
|
||||||
"error: manifest missing or unreadable -- please run init",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
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:
|
||||||
@@ -365,7 +454,7 @@ class _Repo(object):
|
|||||||
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():
|
||||||
@@ -373,7 +462,6 @@ class _Repo(object):
|
|||||||
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
|
||||||
@@ -433,7 +521,7 @@ class _Repo(object):
|
|||||||
error_info = json.dumps(
|
error_info = json.dumps(
|
||||||
{
|
{
|
||||||
"ErrorType": type(error).__name__,
|
"ErrorType": type(error).__name__,
|
||||||
"Project": project,
|
"Project": str(project),
|
||||||
"Message": str(error),
|
"Message": str(error),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -451,36 +539,31 @@ class _Repo(object):
|
|||||||
except (
|
except (
|
||||||
DownloadError,
|
DownloadError,
|
||||||
ManifestInvalidRevisionError,
|
ManifestInvalidRevisionError,
|
||||||
|
ManifestParseError,
|
||||||
NoManifestException,
|
NoManifestException,
|
||||||
) as e:
|
) as e:
|
||||||
print(
|
logger.error("error: in `%s`: %s", " ".join([name] + argv), e)
|
||||||
"error: in `%s`: %s" % (" ".join([name] + argv), str(e)),
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
if isinstance(e, NoManifestException):
|
if isinstance(e, NoManifestException):
|
||||||
print(
|
logger.error(
|
||||||
"error: manifest missing or unreadable -- please run init",
|
"error: manifest missing or unreadable -- please run init"
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
result = e.exit_code
|
result = e.exit_code
|
||||||
except NoSuchProjectError as e:
|
except NoSuchProjectError as e:
|
||||||
if e.name:
|
if e.name:
|
||||||
print("error: project %s not found" % e.name, file=sys.stderr)
|
logger.error("error: project %s not found", e.name)
|
||||||
else:
|
else:
|
||||||
print("error: no project in current directory", file=sys.stderr)
|
logger.error("error: no project in current directory")
|
||||||
result = e.exit_code
|
result = e.exit_code
|
||||||
except InvalidProjectGroupsError as e:
|
except InvalidProjectGroupsError as e:
|
||||||
if e.name:
|
if e.name:
|
||||||
print(
|
logger.error(
|
||||||
"error: project group must be enabled for project %s"
|
"error: project group must be enabled for project %s",
|
||||||
% e.name,
|
e.name,
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(
|
logger.error(
|
||||||
"error: project group must be enabled for the project in "
|
"error: project group must be enabled for the project in "
|
||||||
"the current directory",
|
"the current directory"
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
result = e.exit_code
|
result = e.exit_code
|
||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
@@ -547,7 +630,7 @@ def _CheckWrapperVersion(ver_str, repo_path):
|
|||||||
repo_path = "~/bin/repo"
|
repo_path = "~/bin/repo"
|
||||||
|
|
||||||
if not ver_str:
|
if not ver_str:
|
||||||
print("no --wrapper-version argument", file=sys.stderr)
|
logger.error("no --wrapper-version argument")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Pull out the version of the repo launcher we know about to compare.
|
# Pull out the version of the repo launcher we know about to compare.
|
||||||
@@ -556,7 +639,7 @@ def _CheckWrapperVersion(ver_str, repo_path):
|
|||||||
|
|
||||||
exp_str = ".".join(map(str, exp))
|
exp_str = ".".join(map(str, exp))
|
||||||
if ver < MIN_REPO_VERSION:
|
if ver < MIN_REPO_VERSION:
|
||||||
print(
|
logger.error(
|
||||||
"""
|
"""
|
||||||
repo: error:
|
repo: error:
|
||||||
!!! Your version of repo %s is too old.
|
!!! Your version of repo %s is too old.
|
||||||
@@ -565,42 +648,44 @@ repo: error:
|
|||||||
!!! You must upgrade before you can continue:
|
!!! You must upgrade before you can continue:
|
||||||
|
|
||||||
cp %s %s
|
cp %s %s
|
||||||
"""
|
""",
|
||||||
% (ver_str, min_str, exp_str, WrapperPath(), repo_path),
|
ver_str,
|
||||||
file=sys.stderr,
|
min_str,
|
||||||
|
exp_str,
|
||||||
|
WrapperPath(),
|
||||||
|
repo_path,
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if exp > ver:
|
if exp > ver:
|
||||||
print(
|
logger.warning(
|
||||||
"\n... A new version of repo (%s) is available." % (exp_str,),
|
"\n... A new version of repo (%s) is available.", exp_str
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
if os.access(repo_path, os.W_OK):
|
if os.access(repo_path, os.W_OK):
|
||||||
print(
|
logger.warning(
|
||||||
"""\
|
"""\
|
||||||
... You should upgrade soon:
|
... You should upgrade soon:
|
||||||
cp %s %s
|
cp %s %s
|
||||||
"""
|
""",
|
||||||
% (WrapperPath(), repo_path),
|
WrapperPath(),
|
||||||
file=sys.stderr,
|
repo_path,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(
|
logger.warning(
|
||||||
"""\
|
"""\
|
||||||
... New version is available at: %s
|
... New version is available at: %s
|
||||||
... The launcher is run from: %s
|
... The launcher is run from: %s
|
||||||
!!! The launcher is not writable. Please talk to your sysadmin or distro
|
!!! The launcher is not writable. Please talk to your sysadmin or distro
|
||||||
!!! to get an update installed.
|
!!! to get an update installed.
|
||||||
"""
|
""",
|
||||||
% (WrapperPath(), repo_path),
|
WrapperPath(),
|
||||||
file=sys.stderr,
|
repo_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _CheckRepoDir(repo_dir):
|
def _CheckRepoDir(repo_dir):
|
||||||
if not repo_dir:
|
if not repo_dir:
|
||||||
print("no --repo-dir argument", file=sys.stderr)
|
logger.error("no --repo-dir argument")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -804,7 +889,7 @@ def init_http():
|
|||||||
mgr.add_password(p[1], "https://%s/" % host, p[0], p[2])
|
mgr.add_password(p[1], "https://%s/" % host, p[0], p[2])
|
||||||
except netrc.NetrcParseError:
|
except netrc.NetrcParseError:
|
||||||
pass
|
pass
|
||||||
except IOError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
handlers.append(_BasicAuthHandler(mgr))
|
handlers.append(_BasicAuthHandler(mgr))
|
||||||
handlers.append(_DigestAuthHandler(mgr))
|
handlers.append(_DigestAuthHandler(mgr))
|
||||||
@@ -861,18 +946,7 @@ def _Main(argv):
|
|||||||
result = repo._Run(name, gopts, argv) or 0
|
result = repo._Run(name, gopts, argv) or 0
|
||||||
except RepoExitError as e:
|
except RepoExitError as e:
|
||||||
if not isinstance(e, SilentRepoExitError):
|
if not isinstance(e, SilentRepoExitError):
|
||||||
exception_name = type(e).__name__
|
logger.log_aggregated_errors(e)
|
||||||
print("fatal: %s" % e, file=sys.stderr)
|
|
||||||
if e.aggregate_errors:
|
|
||||||
print(f"{exception_name} Aggregate Errors")
|
|
||||||
for err in e.aggregate_errors[:MAX_PRINT_ERRORS]:
|
|
||||||
print(err)
|
|
||||||
if (
|
|
||||||
e.aggregate_errors
|
|
||||||
and len(e.aggregate_errors) > MAX_PRINT_ERRORS
|
|
||||||
):
|
|
||||||
diff = len(e.aggregate_errors) - MAX_PRINT_ERRORS
|
|
||||||
print(f"+{diff} additional errors ...")
|
|
||||||
result = e.exit_code
|
result = e.exit_code
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("aborted by user", file=sys.stderr)
|
print("aborted by user", file=sys.stderr)
|
||||||
|
|||||||
+6
-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" "July 2022" "repo forall" "Repo Manual"
|
.TH REPO "1" "May 2026" "repo forall" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo forall - manual page for repo forall
|
repo \- repo forall - manual page for repo forall
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -120,6 +120,11 @@ git command, use REPO_LREV.
|
|||||||
REPO_RREV is the name of the revision from the manifest, exactly as written in
|
REPO_RREV is the name of the revision from the manifest, exactly as written in
|
||||||
the manifest.
|
the manifest.
|
||||||
.PP
|
.PP
|
||||||
|
REPO_UPSTREAM is the name of the upstream branch as specified in the manifest.
|
||||||
|
.PP
|
||||||
|
REPO_DEST_BRANCH is the name of the destination branch for code review, as
|
||||||
|
specified in the manifest.
|
||||||
|
.PP
|
||||||
REPO_COUNT is the total number of projects being iterated.
|
REPO_COUNT is the total number of projects being iterated.
|
||||||
.PP
|
.PP
|
||||||
REPO_I is the current (1\-based) iteration count. Can be used in conjunction with
|
REPO_I is the current (1\-based) iteration count. Can be used in conjunction with
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "July 2022" "repo gitc-delete" "Repo Manual"
|
.TH REPO "1" "April 2025" "repo gc" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo gitc-delete - manual page for repo gitc-delete
|
repo \- repo gc - manual page for repo gc
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
.B repo
|
.B repo
|
||||||
\fI\,gitc-delete\/\fR
|
\fI\,gc\/\fR
|
||||||
.SH DESCRIPTION
|
.SH DESCRIPTION
|
||||||
Summary
|
Summary
|
||||||
.PP
|
.PP
|
||||||
Delete a GITC Client.
|
Cleaning up internal repo and Git state.
|
||||||
.SH OPTIONS
|
.SH OPTIONS
|
||||||
.TP
|
.TP
|
||||||
\fB\-h\fR, \fB\-\-help\fR
|
\fB\-h\fR, \fB\-\-help\fR
|
||||||
show this help message and exit
|
show this help message and exit
|
||||||
.TP
|
.TP
|
||||||
\fB\-f\fR, \fB\-\-force\fR
|
\fB\-n\fR, \fB\-\-dry\-run\fR
|
||||||
force the deletion (no prompt)
|
do everything except actually delete
|
||||||
|
.TP
|
||||||
|
\fB\-y\fR, \fB\-\-yes\fR
|
||||||
|
answer yes to all safe prompts
|
||||||
|
.TP
|
||||||
|
\fB\-\-repack\fR
|
||||||
|
repack all projects that use partial clone with
|
||||||
|
filter=blob:none
|
||||||
.SS Logging options:
|
.SS Logging options:
|
||||||
.TP
|
.TP
|
||||||
\fB\-v\fR, \fB\-\-verbose\fR
|
\fB\-v\fR, \fB\-\-verbose\fR
|
||||||
@@ -37,8 +44,4 @@ only operate on this (sub)manifest
|
|||||||
\fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
|
\fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
|
||||||
operate on this manifest and its submanifests
|
operate on this manifest and its submanifests
|
||||||
.PP
|
.PP
|
||||||
Run `repo help gitc\-delete` to view the detailed manual.
|
Run `repo help gc` to view the detailed manual.
|
||||||
.SH DETAILS
|
|
||||||
.PP
|
|
||||||
This subcommand deletes the current GITC client, deleting the GITC manifest and
|
|
||||||
all locally downloaded sources.
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
|
||||||
.TH REPO "1" "October 2022" "repo gitc-init" "Repo Manual"
|
|
||||||
.SH NAME
|
|
||||||
repo \- repo gitc-init - manual page for repo gitc-init
|
|
||||||
.SH SYNOPSIS
|
|
||||||
.B repo
|
|
||||||
\fI\,gitc-init \/\fR[\fI\,options\/\fR] [\fI\,client name\/\fR]
|
|
||||||
.SH DESCRIPTION
|
|
||||||
Summary
|
|
||||||
.PP
|
|
||||||
Initialize a GITC Client.
|
|
||||||
.SH OPTIONS
|
|
||||||
.TP
|
|
||||||
\fB\-h\fR, \fB\-\-help\fR
|
|
||||||
show this help message and exit
|
|
||||||
.SS Logging options:
|
|
||||||
.TP
|
|
||||||
\fB\-v\fR, \fB\-\-verbose\fR
|
|
||||||
show all output
|
|
||||||
.TP
|
|
||||||
\fB\-q\fR, \fB\-\-quiet\fR
|
|
||||||
only show errors
|
|
||||||
.SS Manifest options:
|
|
||||||
.TP
|
|
||||||
\fB\-u\fR URL, \fB\-\-manifest\-url\fR=\fI\,URL\/\fR
|
|
||||||
manifest repository location
|
|
||||||
.TP
|
|
||||||
\fB\-b\fR REVISION, \fB\-\-manifest\-branch\fR=\fI\,REVISION\/\fR
|
|
||||||
manifest branch or revision (use HEAD for default)
|
|
||||||
.TP
|
|
||||||
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
|
|
||||||
initial manifest file
|
|
||||||
.TP
|
|
||||||
\fB\-g\fR GROUP, \fB\-\-groups\fR=\fI\,GROUP\/\fR
|
|
||||||
restrict manifest projects to ones with specified
|
|
||||||
group(s) [default|all|G1,G2,G3|G4,\-G5,\-G6]
|
|
||||||
.TP
|
|
||||||
\fB\-p\fR PLATFORM, \fB\-\-platform\fR=\fI\,PLATFORM\/\fR
|
|
||||||
restrict manifest projects to ones with a specified
|
|
||||||
platform group [auto|all|none|linux|darwin|...]
|
|
||||||
.TP
|
|
||||||
\fB\-\-submodules\fR
|
|
||||||
sync any submodules associated with the manifest repo
|
|
||||||
.TP
|
|
||||||
\fB\-\-standalone\-manifest\fR
|
|
||||||
download the manifest as a static file rather then
|
|
||||||
create a git checkout of the manifest repo
|
|
||||||
.TP
|
|
||||||
\fB\-\-manifest\-depth\fR=\fI\,DEPTH\/\fR
|
|
||||||
create a shallow clone of the manifest repo with given
|
|
||||||
depth (0 for full clone); see git clone (default: 0)
|
|
||||||
.SS Manifest (only) checkout options:
|
|
||||||
.TP
|
|
||||||
\fB\-\-current\-branch\fR
|
|
||||||
fetch only current manifest branch from server
|
|
||||||
(default)
|
|
||||||
.TP
|
|
||||||
\fB\-\-no\-current\-branch\fR
|
|
||||||
fetch all manifest branches from server
|
|
||||||
.TP
|
|
||||||
\fB\-\-tags\fR
|
|
||||||
fetch tags in the manifest
|
|
||||||
.TP
|
|
||||||
\fB\-\-no\-tags\fR
|
|
||||||
don't fetch tags in the manifest
|
|
||||||
.SS Checkout modes:
|
|
||||||
.TP
|
|
||||||
\fB\-\-mirror\fR
|
|
||||||
create a replica of the remote repositories rather
|
|
||||||
than a client working directory
|
|
||||||
.TP
|
|
||||||
\fB\-\-archive\fR
|
|
||||||
checkout an archive instead of a git repository for
|
|
||||||
each project. See git archive.
|
|
||||||
.TP
|
|
||||||
\fB\-\-worktree\fR
|
|
||||||
use git\-worktree to manage projects
|
|
||||||
.SS Project checkout optimizations:
|
|
||||||
.TP
|
|
||||||
\fB\-\-reference\fR=\fI\,DIR\/\fR
|
|
||||||
location of mirror directory
|
|
||||||
.TP
|
|
||||||
\fB\-\-dissociate\fR
|
|
||||||
dissociate from reference mirrors after clone
|
|
||||||
.TP
|
|
||||||
\fB\-\-depth\fR=\fI\,DEPTH\/\fR
|
|
||||||
create a shallow clone with given depth; see git clone
|
|
||||||
.TP
|
|
||||||
\fB\-\-partial\-clone\fR
|
|
||||||
perform partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
|
|
||||||
.TP
|
|
||||||
\fB\-\-no\-partial\-clone\fR
|
|
||||||
disable use of partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
|
|
||||||
.TP
|
|
||||||
\fB\-\-partial\-clone\-exclude\fR=\fI\,PARTIAL_CLONE_EXCLUDE\/\fR
|
|
||||||
exclude the specified projects (a comma\-delimited
|
|
||||||
project names) from partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
|
|
||||||
.TP
|
|
||||||
\fB\-\-clone\-filter\fR=\fI\,CLONE_FILTER\/\fR
|
|
||||||
filter for use with \fB\-\-partial\-clone\fR [default:
|
|
||||||
blob:none]
|
|
||||||
.TP
|
|
||||||
\fB\-\-use\-superproject\fR
|
|
||||||
use the manifest superproject to sync projects;
|
|
||||||
implies \fB\-c\fR
|
|
||||||
.TP
|
|
||||||
\fB\-\-no\-use\-superproject\fR
|
|
||||||
disable use of manifest superprojects
|
|
||||||
.TP
|
|
||||||
\fB\-\-clone\-bundle\fR
|
|
||||||
enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if
|
|
||||||
not \fB\-\-partial\-clone\fR)
|
|
||||||
.TP
|
|
||||||
\fB\-\-no\-clone\-bundle\fR
|
|
||||||
disable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if
|
|
||||||
\fB\-\-partial\-clone\fR)
|
|
||||||
.TP
|
|
||||||
\fB\-\-git\-lfs\fR
|
|
||||||
enable Git LFS support
|
|
||||||
.TP
|
|
||||||
\fB\-\-no\-git\-lfs\fR
|
|
||||||
disable Git LFS support
|
|
||||||
.SS repo Version options:
|
|
||||||
.TP
|
|
||||||
\fB\-\-repo\-url\fR=\fI\,URL\/\fR
|
|
||||||
repo repository location ($REPO_URL)
|
|
||||||
.TP
|
|
||||||
\fB\-\-repo\-rev\fR=\fI\,REV\/\fR
|
|
||||||
repo branch or revision ($REPO_REV)
|
|
||||||
.TP
|
|
||||||
\fB\-\-no\-repo\-verify\fR
|
|
||||||
do not verify repo source code
|
|
||||||
.SS Other options:
|
|
||||||
.TP
|
|
||||||
\fB\-\-config\-name\fR
|
|
||||||
Always prompt for name/e\-mail
|
|
||||||
.SS GITC options:
|
|
||||||
.TP
|
|
||||||
\fB\-f\fR MANIFEST_FILE, \fB\-\-manifest\-file\fR=\fI\,MANIFEST_FILE\/\fR
|
|
||||||
Optional manifest file to use for this GITC client.
|
|
||||||
.TP
|
|
||||||
\fB\-c\fR GITC_CLIENT, \fB\-\-gitc\-client\fR=\fI\,GITC_CLIENT\/\fR
|
|
||||||
Name of the gitc_client instance to create or modify.
|
|
||||||
.SS Multi\-manifest:
|
|
||||||
.TP
|
|
||||||
\fB\-\-outer\-manifest\fR
|
|
||||||
operate starting at the outermost manifest
|
|
||||||
.TP
|
|
||||||
\fB\-\-no\-outer\-manifest\fR
|
|
||||||
do not operate on outer manifests
|
|
||||||
.TP
|
|
||||||
\fB\-\-this\-manifest\-only\fR
|
|
||||||
only operate on this (sub)manifest
|
|
||||||
.TP
|
|
||||||
\fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
|
|
||||||
operate on this manifest and its submanifests
|
|
||||||
.PP
|
|
||||||
Run `repo help gitc\-init` to view the detailed manual.
|
|
||||||
.SH DETAILS
|
|
||||||
.PP
|
|
||||||
The 'repo gitc\-init' command is ran to initialize a new GITC client for use with
|
|
||||||
the GITC file system.
|
|
||||||
.PP
|
|
||||||
This command will setup the client directory, initialize repo, just like repo
|
|
||||||
init does, and then downloads the manifest collection and installs it in the
|
|
||||||
\&.repo/directory of the GITC client.
|
|
||||||
.PP
|
|
||||||
Once this is done, a GITC manifest is generated by pulling the HEAD SHA for each
|
|
||||||
project and generates the properly formatted XML file and installs it as
|
|
||||||
\&.manifest in the GITC client directory.
|
|
||||||
.PP
|
|
||||||
The \fB\-c\fR argument is required to specify the GITC client name.
|
|
||||||
.PP
|
|
||||||
The optional \fB\-f\fR argument can be used to specify the manifest file to use for
|
|
||||||
this GITC client.
|
|
||||||
+21
-2
@@ -1,10 +1,10 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "July 2022" "repo info" "Repo Manual"
|
.TH REPO "1" "May 2026" "repo info" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo info - manual page for repo info
|
repo \- repo info - manual page for repo info
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
.B repo
|
.B repo
|
||||||
\fI\,info \/\fR[\fI\,-dl\/\fR] [\fI\,-o \/\fR[\fI\,-c\/\fR]] [\fI\,<project>\/\fR...]
|
\fI\,info \/\fR[\fI\,-dl\/\fR] [\fI\,-o \/\fR[\fI\,-c\/\fR]] [\fI\,--format=<format>\/\fR] [\fI\,<project>\/\fR...]
|
||||||
.SH DESCRIPTION
|
.SH DESCRIPTION
|
||||||
Summary
|
Summary
|
||||||
.PP
|
.PP
|
||||||
@@ -14,6 +14,10 @@ Get info on the manifest branch, current branch or unmerged branches
|
|||||||
\fB\-h\fR, \fB\-\-help\fR
|
\fB\-h\fR, \fB\-\-help\fR
|
||||||
show this help message and exit
|
show this help message and exit
|
||||||
.TP
|
.TP
|
||||||
|
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||||
|
number of jobs to run in parallel (default: based on
|
||||||
|
number of CPU cores)
|
||||||
|
.TP
|
||||||
\fB\-d\fR, \fB\-\-diff\fR
|
\fB\-d\fR, \fB\-\-diff\fR
|
||||||
show full info and commit diff including remote
|
show full info and commit diff including remote
|
||||||
branches
|
branches
|
||||||
@@ -21,6 +25,18 @@ branches
|
|||||||
\fB\-o\fR, \fB\-\-overview\fR
|
\fB\-o\fR, \fB\-\-overview\fR
|
||||||
show overview of all local commits
|
show overview of all local commits
|
||||||
.TP
|
.TP
|
||||||
|
\fB\-\-include\-summary\fR
|
||||||
|
include manifest summary (default: true)
|
||||||
|
.TP
|
||||||
|
\fB\-\-no\-include\-summary\fR
|
||||||
|
exclude manifest summary
|
||||||
|
.TP
|
||||||
|
\fB\-\-include\-projects\fR
|
||||||
|
include project details (default: true)
|
||||||
|
.TP
|
||||||
|
\fB\-\-no\-include\-projects\fR
|
||||||
|
exclude project details
|
||||||
|
.TP
|
||||||
\fB\-c\fR, \fB\-\-current\-branch\fR
|
\fB\-c\fR, \fB\-\-current\-branch\fR
|
||||||
consider only checked out branches
|
consider only checked out branches
|
||||||
.TP
|
.TP
|
||||||
@@ -29,6 +45,9 @@ consider all local branches
|
|||||||
.TP
|
.TP
|
||||||
\fB\-l\fR, \fB\-\-local\-only\fR
|
\fB\-l\fR, \fB\-\-local\-only\fR
|
||||||
disable all remote operations
|
disable all remote operations
|
||||||
|
.TP
|
||||||
|
\fB\-\-format\fR=\fI\,FORMAT\/\fR
|
||||||
|
output format: text, json (default: text)
|
||||||
.SS Logging options:
|
.SS Logging options:
|
||||||
.TP
|
.TP
|
||||||
\fB\-v\fR, \fB\-\-verbose\fR
|
\fB\-v\fR, \fB\-\-verbose\fR
|
||||||
|
|||||||
+10
-1
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "October 2022" "repo init" "Repo Manual"
|
.TH REPO "1" "September 2024" "repo init" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo init - manual page for repo init
|
repo \- repo init - manual page for repo init
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -28,6 +28,11 @@ manifest repository location
|
|||||||
\fB\-b\fR REVISION, \fB\-\-manifest\-branch\fR=\fI\,REVISION\/\fR
|
\fB\-b\fR REVISION, \fB\-\-manifest\-branch\fR=\fI\,REVISION\/\fR
|
||||||
manifest branch or revision (use HEAD for default)
|
manifest branch or revision (use HEAD for default)
|
||||||
.TP
|
.TP
|
||||||
|
\fB\-\-manifest\-upstream\-branch\fR=\fI\,BRANCH\/\fR
|
||||||
|
when a commit is provided to \fB\-\-manifest\-branch\fR, this
|
||||||
|
is the name of the git ref in which the commit can be
|
||||||
|
found
|
||||||
|
.TP
|
||||||
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
|
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
|
||||||
initial manifest file
|
initial manifest file
|
||||||
.TP
|
.TP
|
||||||
@@ -163,6 +168,10 @@ The optional \fB\-b\fR argument can be used to select the manifest branch to che
|
|||||||
and use. If no branch is specified, the remote's default branch is used. This is
|
and use. If no branch is specified, the remote's default branch is used. This is
|
||||||
equivalent to using \fB\-b\fR HEAD.
|
equivalent to using \fB\-b\fR HEAD.
|
||||||
.PP
|
.PP
|
||||||
|
The optional \fB\-\-manifest\-upstream\-branch\fR argument can be used when a commit is
|
||||||
|
provided to \fB\-\-manifest\-branch\fR (or \fB\-b\fR), to specify the name of the git ref in
|
||||||
|
which the commit can be found.
|
||||||
|
.PP
|
||||||
The optional \fB\-m\fR argument can be used to specify an alternate manifest to be
|
The optional \fB\-m\fR argument can be used to specify an alternate manifest to be
|
||||||
used. If no manifest is specified, the manifest default.xml will be used.
|
used. If no manifest is specified, the manifest default.xml will be used.
|
||||||
.PP
|
.PP
|
||||||
|
|||||||
+155
-66
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "October 2022" "repo manifest" "Repo Manual"
|
.TH REPO "1" "April 2026" "repo manifest" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo manifest - manual page for repo manifest
|
repo \- repo manifest - manual page for repo manifest
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -30,8 +30,8 @@ if in \fB\-r\fR mode, do not write the dest\-branch field
|
|||||||
(only of use if the branch names for a sha1 manifest
|
(only of use if the branch names for a sha1 manifest
|
||||||
are sensitive)
|
are sensitive)
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-json\fR
|
\fB\-\-format\fR=\fI\,FORMAT\/\fR
|
||||||
output manifest in JSON format (experimental)
|
output format: xml, json (default: xml)
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-pretty\fR
|
\fB\-\-pretty\fR
|
||||||
format output for humans to read
|
format output for humans to read
|
||||||
@@ -78,6 +78,10 @@ set to the ref we were on when the manifest was generated. The 'dest\-branch'
|
|||||||
attribute is set to indicate the remote ref to push changes to via 'repo
|
attribute is set to indicate the remote ref to push changes to via 'repo
|
||||||
upload'.
|
upload'.
|
||||||
.PP
|
.PP
|
||||||
|
Multiple output formats are supported via \fB\-\-format\fR. The default output is XML,
|
||||||
|
and formats are generally "condensed". Use \fB\-\-pretty\fR for more human\-readable
|
||||||
|
variations.
|
||||||
|
.PP
|
||||||
repo Manifest Format
|
repo Manifest Format
|
||||||
.PP
|
.PP
|
||||||
A repo manifest describes the structure of a repo client; that is the
|
A repo manifest describes the structure of a repo client; that is the
|
||||||
@@ -127,6 +131,7 @@ include*)>
|
|||||||
<!ATTLIST default dest\-branch CDATA #IMPLIED>
|
<!ATTLIST default dest\-branch CDATA #IMPLIED>
|
||||||
<!ATTLIST default upstream CDATA #IMPLIED>
|
<!ATTLIST default upstream CDATA #IMPLIED>
|
||||||
<!ATTLIST default sync\-j CDATA #IMPLIED>
|
<!ATTLIST default sync\-j CDATA #IMPLIED>
|
||||||
|
<!ATTLIST default sync\-j\-max CDATA #IMPLIED>
|
||||||
<!ATTLIST default sync\-c CDATA #IMPLIED>
|
<!ATTLIST default sync\-c CDATA #IMPLIED>
|
||||||
<!ATTLIST default sync\-s CDATA #IMPLIED>
|
<!ATTLIST default sync\-s CDATA #IMPLIED>
|
||||||
<!ATTLIST default sync\-tags CDATA #IMPLIED>
|
<!ATTLIST default sync\-tags CDATA #IMPLIED>
|
||||||
@@ -135,7 +140,7 @@ include*)>
|
|||||||
<!ATTLIST manifest\-server url CDATA #REQUIRED>
|
<!ATTLIST manifest\-server url CDATA #REQUIRED>
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT submanifest EMPTY>
|
<!ELEMENT submanifest EMPTY>
|
||||||
<!ATTLIST submanifest name ID #REQUIRED>
|
<!ATTLIST submanifest name ID #REQUIRED>
|
||||||
<!ATTLIST submanifest remote IDREF #IMPLIED>
|
<!ATTLIST submanifest remote IDREF #IMPLIED>
|
||||||
<!ATTLIST submanifest project CDATA #IMPLIED>
|
<!ATTLIST submanifest project CDATA #IMPLIED>
|
||||||
<!ATTLIST submanifest manifest\-name CDATA #IMPLIED>
|
<!ATTLIST submanifest manifest\-name CDATA #IMPLIED>
|
||||||
@@ -160,15 +165,32 @@ IDREF #IMPLIED>
|
|||||||
.TP
|
.TP
|
||||||
<!ATTLIST project revision
|
<!ATTLIST project revision
|
||||||
CDATA #IMPLIED>
|
CDATA #IMPLIED>
|
||||||
|
.TP
|
||||||
|
<!ATTLIST project dest\-branch
|
||||||
|
CDATA #IMPLIED>
|
||||||
|
.TP
|
||||||
|
<!ATTLIST project groups
|
||||||
|
CDATA #IMPLIED>
|
||||||
|
.TP
|
||||||
|
<!ATTLIST project sync\-c
|
||||||
|
CDATA #IMPLIED>
|
||||||
|
.TP
|
||||||
|
<!ATTLIST project sync\-s
|
||||||
|
CDATA #IMPLIED>
|
||||||
|
.TP
|
||||||
|
<!ATTLIST project sync\-tags
|
||||||
|
CDATA #IMPLIED>
|
||||||
|
.TP
|
||||||
|
<!ATTLIST project upstream
|
||||||
|
CDATA #IMPLIED>
|
||||||
|
.TP
|
||||||
|
<!ATTLIST project clone\-depth
|
||||||
|
CDATA #IMPLIED>
|
||||||
|
.TP
|
||||||
|
<!ATTLIST project force\-path
|
||||||
|
CDATA #IMPLIED>
|
||||||
.IP
|
.IP
|
||||||
<!ATTLIST project dest\-branch CDATA #IMPLIED>
|
<!ATTLIST project sync\-strategy CDATA #IMPLIED>
|
||||||
<!ATTLIST project groups CDATA #IMPLIED>
|
|
||||||
<!ATTLIST project sync\-c CDATA #IMPLIED>
|
|
||||||
<!ATTLIST project sync\-s CDATA #IMPLIED>
|
|
||||||
<!ATTLIST project sync\-tags CDATA #IMPLIED>
|
|
||||||
<!ATTLIST project upstream CDATA #IMPLIED>
|
|
||||||
<!ATTLIST project clone\-depth CDATA #IMPLIED>
|
|
||||||
<!ATTLIST project force\-path CDATA #IMPLIED>
|
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT annotation EMPTY>
|
<!ELEMENT annotation EMPTY>
|
||||||
<!ATTLIST annotation name CDATA #REQUIRED>
|
<!ATTLIST annotation name CDATA #REQUIRED>
|
||||||
@@ -180,25 +202,43 @@ CDATA #IMPLIED>
|
|||||||
<!ATTLIST copyfile dest CDATA #REQUIRED>
|
<!ATTLIST copyfile dest CDATA #REQUIRED>
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT linkfile EMPTY>
|
<!ELEMENT linkfile EMPTY>
|
||||||
<!ATTLIST linkfile src CDATA #REQUIRED>
|
<!ATTLIST linkfile src CDATA #REQUIRED>
|
||||||
<!ATTLIST linkfile dest CDATA #REQUIRED>
|
<!ATTLIST linkfile dest CDATA #REQUIRED>
|
||||||
|
.TP
|
||||||
|
<!ELEMENT extend\-project (annotation*,
|
||||||
|
copyfile*,
|
||||||
|
linkfile*)>
|
||||||
|
.TP
|
||||||
|
<!ATTLIST extend\-project name
|
||||||
|
CDATA #REQUIRED>
|
||||||
|
.TP
|
||||||
|
<!ATTLIST extend\-project path
|
||||||
|
CDATA #IMPLIED>
|
||||||
|
.TP
|
||||||
|
<!ATTLIST extend\-project dest\-path
|
||||||
|
CDATA #IMPLIED>
|
||||||
|
.TP
|
||||||
|
<!ATTLIST extend\-project groups
|
||||||
|
CDATA #IMPLIED>
|
||||||
|
.TP
|
||||||
|
<!ATTLIST extend\-project revision
|
||||||
|
CDATA #IMPLIED>
|
||||||
|
.TP
|
||||||
|
<!ATTLIST extend\-project remote
|
||||||
|
CDATA #IMPLIED>
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT extend\-project EMPTY>
|
|
||||||
<!ATTLIST extend\-project name CDATA #REQUIRED>
|
|
||||||
<!ATTLIST extend\-project path CDATA #IMPLIED>
|
|
||||||
<!ATTLIST extend\-project dest\-path CDATA #IMPLIED>
|
|
||||||
<!ATTLIST extend\-project groups CDATA #IMPLIED>
|
|
||||||
<!ATTLIST extend\-project revision CDATA #IMPLIED>
|
|
||||||
<!ATTLIST extend\-project remote CDATA #IMPLIED>
|
|
||||||
<!ATTLIST extend\-project dest\-branch CDATA #IMPLIED>
|
<!ATTLIST extend\-project dest\-branch CDATA #IMPLIED>
|
||||||
<!ATTLIST extend\-project upstream CDATA #IMPLIED>
|
<!ATTLIST extend\-project upstream CDATA #IMPLIED>
|
||||||
|
<!ATTLIST extend\-project base\-rev CDATA #IMPLIED>
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT remove\-project EMPTY>
|
<!ELEMENT remove\-project EMPTY>
|
||||||
<!ATTLIST remove\-project name CDATA #REQUIRED>
|
<!ATTLIST remove\-project name CDATA #IMPLIED>
|
||||||
<!ATTLIST remove\-project optional CDATA #IMPLIED>
|
<!ATTLIST remove\-project path CDATA #IMPLIED>
|
||||||
|
<!ATTLIST remove\-project optional CDATA #IMPLIED>
|
||||||
|
<!ATTLIST remove\-project base\-rev CDATA #IMPLIED>
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT repo\-hooks EMPTY>
|
<!ELEMENT repo\-hooks EMPTY>
|
||||||
<!ATTLIST repo\-hooks in\-project CDATA #REQUIRED>
|
<!ATTLIST repo\-hooks in\-project CDATA #REQUIRED>
|
||||||
<!ATTLIST repo\-hooks enabled\-list CDATA #REQUIRED>
|
<!ATTLIST repo\-hooks enabled\-list CDATA #REQUIRED>
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT superproject EMPTY>
|
<!ELEMENT superproject EMPTY>
|
||||||
@@ -207,11 +247,12 @@ CDATA #IMPLIED>
|
|||||||
<!ATTLIST superproject revision CDATA #IMPLIED>
|
<!ATTLIST superproject revision CDATA #IMPLIED>
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT contactinfo EMPTY>
|
<!ELEMENT contactinfo EMPTY>
|
||||||
<!ATTLIST contactinfo bugurl CDATA #REQUIRED>
|
<!ATTLIST contactinfo bugurl CDATA #REQUIRED>
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT include EMPTY>
|
<!ELEMENT include EMPTY>
|
||||||
<!ATTLIST include name CDATA #REQUIRED>
|
<!ATTLIST include name CDATA #REQUIRED>
|
||||||
<!ATTLIST include groups CDATA #IMPLIED>
|
<!ATTLIST include groups CDATA #IMPLIED>
|
||||||
|
<!ATTLIST include revision CDATA #IMPLIED>
|
||||||
.PP
|
.PP
|
||||||
]>
|
]>
|
||||||
```
|
```
|
||||||
@@ -286,7 +327,9 @@ when syncing a revision locked manifest in \fB\-c\fR mode to avoid having to syn
|
|||||||
entire ref space. Project elements not setting their own `upstream` will inherit
|
entire ref space. Project elements not setting their own `upstream` will inherit
|
||||||
this value.
|
this value.
|
||||||
.PP
|
.PP
|
||||||
Attribute `sync\-j`: Number of parallel jobs to use when synching.
|
Attribute `sync\-j`: Number of parallel jobs to use when syncing.
|
||||||
|
.PP
|
||||||
|
Attribute `sync\-j\-max`: Maximum number of parallel jobs to use when syncing.
|
||||||
.PP
|
.PP
|
||||||
Attribute `sync\-c`: Set to true to only sync the given Git branch (specified in
|
Attribute `sync\-c`: Set to true to only sync the given Git branch (specified in
|
||||||
the `revision` attribute) rather than the whole ref space. Project elements
|
the `revision` attribute) rather than the whole ref space. Project elements
|
||||||
@@ -302,25 +345,7 @@ Element manifest\-server
|
|||||||
At most one manifest\-server may be specified. The url attribute is used to
|
At most one manifest\-server may be specified. The url attribute is used to
|
||||||
specify the URL of a manifest server, which is an XML RPC service.
|
specify the URL of a manifest server, which is an XML RPC service.
|
||||||
.PP
|
.PP
|
||||||
The manifest server should implement the following RPC methods:
|
See the [smart sync documentation](./smart\-sync.md) for more details.
|
||||||
.IP
|
|
||||||
GetApprovedManifest(branch, target)
|
|
||||||
.PP
|
|
||||||
Return a manifest in which each project is pegged to a known good revision for
|
|
||||||
the current branch and target. This is used by repo sync when the \fB\-\-smart\-sync\fR
|
|
||||||
option is given.
|
|
||||||
.PP
|
|
||||||
The target to use is defined by environment variables TARGET_PRODUCT and
|
|
||||||
TARGET_BUILD_VARIANT. These variables are used to create a string of the form
|
|
||||||
$TARGET_PRODUCT\-$TARGET_BUILD_VARIANT, e.g. passion\-userdebug. If one of those
|
|
||||||
variables or both are not present, the program will call GetApprovedManifest
|
|
||||||
without the target parameter and the manifest server should choose a reasonable
|
|
||||||
default target.
|
|
||||||
.IP
|
|
||||||
GetManifest(tag)
|
|
||||||
.PP
|
|
||||||
Return a manifest in which each project is pegged to the revision at the
|
|
||||||
specified tag. This is used by repo sync when the \fB\-\-smart\-tag\fR option is given.
|
|
||||||
.PP
|
.PP
|
||||||
Element submanifest
|
Element submanifest
|
||||||
.PP
|
.PP
|
||||||
@@ -372,7 +397,7 @@ supplied, `revision` is used.
|
|||||||
.PP
|
.PP
|
||||||
`path` may not be an absolute path or use "." or ".." path components.
|
`path` may not be an absolute path or use "." or ".." path components.
|
||||||
.PP
|
.PP
|
||||||
Attribute `groups`: List of additional groups to which all projects in the
|
Attribute `groups`: Set of additional groups to which all projects in the
|
||||||
included submanifest belong. This appends and recurses, meaning all projects in
|
included submanifest belong. This appends and recurses, meaning all projects in
|
||||||
submanifests carry all parent submanifest groups. Same syntax as the
|
submanifests carry all parent submanifest groups. Same syntax as the
|
||||||
corresponding element of `project`.
|
corresponding element of `project`.
|
||||||
@@ -434,7 +459,7 @@ Attribute `dest\-branch`: Name of a Git branch (e.g. `main`). When using `repo
|
|||||||
upload`, changes will be submitted for code review on this branch. If
|
upload`, changes will be submitted for code review on this branch. If
|
||||||
unspecified both here and in the default element, `revision` is used instead.
|
unspecified both here and in the default element, `revision` is used instead.
|
||||||
.PP
|
.PP
|
||||||
Attribute `groups`: List of groups to which this project belongs, whitespace or
|
Attribute `groups`: Set of groups to which this project belongs, whitespace or
|
||||||
comma separated. All projects belong to the group "all", and each project
|
comma separated. All projects belong to the group "all", and each project
|
||||||
automatically belongs to a group of its name:`name` and path:`path`. E.g. for
|
automatically belongs to a group of its name:`name` and path:`path`. E.g. for
|
||||||
`<project name="monkeys" path="barrel\-of"/>`, that project definition is
|
`<project name="monkeys" path="barrel\-of"/>`, that project definition is
|
||||||
@@ -461,6 +486,21 @@ mirror repository according to its `path` attribute (if supplied) rather than
|
|||||||
the `name` attribute. This attribute only applies to the local mirrors syncing,
|
the `name` attribute. This attribute only applies to the local mirrors syncing,
|
||||||
it will be ignored when syncing the projects in a client working directory.
|
it will be ignored when syncing the projects in a client working directory.
|
||||||
.PP
|
.PP
|
||||||
|
Attribute `sync\-strategy`: Set the sync strategy used when fetching this
|
||||||
|
project. Currently the only supported value is `stateless`. When set to
|
||||||
|
`stateless`, repo will run a reflog expiration and aggressive garbage collection
|
||||||
|
at the end of the sync process. This is useful for projects that contain large
|
||||||
|
binary files and use `clone\-depth="1"`, where garbage can accumulate as binaries
|
||||||
|
are added, deleted, or modified across successive syncs.
|
||||||
|
.PP
|
||||||
|
During a stateless sync, repo checks the following before cleaning up: 1. The
|
||||||
|
project does not share an object directory with other projects. 2. The working
|
||||||
|
tree is clean (no uncommitted changes, no untracked files). 3. There are no
|
||||||
|
unpushed local commits. 4. There is no Git stash.
|
||||||
|
.PP
|
||||||
|
If any of these conditions are not met, repo falls back to a standard sync
|
||||||
|
without garbage collection.
|
||||||
|
.PP
|
||||||
Element extend\-project
|
Element extend\-project
|
||||||
.PP
|
.PP
|
||||||
Modify the attributes of the named project.
|
Modify the attributes of the named project.
|
||||||
@@ -470,6 +510,11 @@ of an existing project without completely replacing the existing project
|
|||||||
definition. This makes the local manifest more robust against changes to the
|
definition. This makes the local manifest more robust against changes to the
|
||||||
original manifest.
|
original manifest.
|
||||||
.PP
|
.PP
|
||||||
|
The `extend\-project` element can also contain `annotation`, `copyfile`, and
|
||||||
|
`linkfile` child elements. These are added to the project's definition. A
|
||||||
|
`copyfile` or `linkfile` with a `dest` that already exists in the project will
|
||||||
|
overwrite the original.
|
||||||
|
.PP
|
||||||
Attribute `path`: If specified, limit the change to projects checked out at the
|
Attribute `path`: If specified, limit the change to projects checked out at the
|
||||||
specified path, rather than all projects with the given name.
|
specified path, rather than all projects with the given name.
|
||||||
.PP
|
.PP
|
||||||
@@ -478,8 +523,8 @@ repo client where the Git working directory for this project should be placed.
|
|||||||
This is used to move a project in the checkout by overriding the existing `path`
|
This is used to move a project in the checkout by overriding the existing `path`
|
||||||
setting.
|
setting.
|
||||||
.PP
|
.PP
|
||||||
Attribute `groups`: List of additional groups to which this project belongs.
|
Attribute `groups`: Set of additional groups to which this project belongs. Same
|
||||||
Same syntax as the corresponding element of `project`.
|
syntax as the corresponding element of `project`.
|
||||||
.PP
|
.PP
|
||||||
Attribute `revision`: If specified, overrides the revision of the original
|
Attribute `revision`: If specified, overrides the revision of the original
|
||||||
project. Same syntax as the corresponding element of `project`.
|
project. Same syntax as the corresponding element of `project`.
|
||||||
@@ -493,21 +538,31 @@ project. Same syntax as the corresponding element of `project`.
|
|||||||
Attribute `upstream`: If specified, overrides the upstream of the original
|
Attribute `upstream`: If specified, overrides the upstream of the original
|
||||||
project. Same syntax as the corresponding element of `project`.
|
project. Same syntax as the corresponding element of `project`.
|
||||||
.PP
|
.PP
|
||||||
|
Attribute `base\-rev`: If specified, adds a check against the revision to be
|
||||||
|
extended. Manifest parse will fail and give a list of mismatch extends if the
|
||||||
|
revisions being extended have changed since base\-rev was set. Intended for use
|
||||||
|
with layered manifests using hash revisions to prevent patch branches hiding
|
||||||
|
newer upstream revisions. Also compares named refs like branches or tags but is
|
||||||
|
misleading if branches are used as base\-rev. Same syntax as the corresponding
|
||||||
|
element of `project`.
|
||||||
|
.PP
|
||||||
Element annotation
|
Element annotation
|
||||||
.PP
|
.PP
|
||||||
Zero or more annotation elements may be specified as children of a project or
|
Zero or more annotation elements may be specified as children of a project
|
||||||
remote element. Each element describes a name\-value pair. For projects, this
|
element, an extend\-project element, or a remote element. Each element describes
|
||||||
name\-value pair will be exported into each project's environment during a
|
a name\-value pair. For projects, this name\-value pair will be exported into each
|
||||||
\&'forall' command, prefixed with `REPO__`. In addition, there is an optional
|
project's environment during a 'forall' command, prefixed with `REPO__`. In
|
||||||
attribute "keep" which accepts the case insensitive values "true" (default) or
|
addition, there is an optional attribute "keep" which accepts the case
|
||||||
"false". This attribute determines whether or not the annotation will be kept
|
insensitive values "true" (default) or "false". This attribute determines
|
||||||
when exported with the manifest subcommand.
|
whether or not the annotation will be kept when exported with the manifest
|
||||||
|
subcommand.
|
||||||
.PP
|
.PP
|
||||||
Element copyfile
|
Element copyfile
|
||||||
.PP
|
.PP
|
||||||
Zero or more copyfile elements may be specified as children of a project
|
Zero or more copyfile elements may be specified as children of a project
|
||||||
element. Each element describes a src\-dest pair of files; the "src" file will be
|
element, or an extend\-project element. Each element describes a src\-dest pair of
|
||||||
copied to the "dest" place during `repo sync` command.
|
files; the "src" file will be copied to the "dest" place during `repo sync`
|
||||||
|
command.
|
||||||
.PP
|
.PP
|
||||||
"src" is project relative, "dest" is relative to the top of the tree. Copying
|
"src" is project relative, "dest" is relative to the top of the tree. Copying
|
||||||
from paths outside of the project or to paths outside of the repo client is not
|
from paths outside of the project or to paths outside of the repo client is not
|
||||||
@@ -518,10 +573,14 @@ Intermediate paths must not be symlinks either.
|
|||||||
.PP
|
.PP
|
||||||
Parent directories of "dest" will be automatically created if missing.
|
Parent directories of "dest" will be automatically created if missing.
|
||||||
.PP
|
.PP
|
||||||
|
The files are copied in the order they are specified in the manifests. If
|
||||||
|
multiple elements specify the same source and destination, they will only be
|
||||||
|
applied as one, based on the first occurence. Files are copied before any links
|
||||||
|
specified via linkfile elements are created.
|
||||||
|
.PP
|
||||||
Element linkfile
|
Element linkfile
|
||||||
.PP
|
.PP
|
||||||
It's just like copyfile and runs at the same time as copyfile but instead of
|
It's just like copyfile, but instead of copying it creates a symlink.
|
||||||
copying it creates a symlink.
|
|
||||||
.PP
|
.PP
|
||||||
The symlink is created at "dest" (relative to the top of the tree) and points to
|
The symlink is created at "dest" (relative to the top of the tree) and points to
|
||||||
the path specified by "src" which is a path in the project.
|
the path specified by "src" which is a path in the project.
|
||||||
@@ -531,18 +590,42 @@ Parent directories of "dest" will be automatically created if missing.
|
|||||||
The symlink target may be a file or directory, but it may not point outside of
|
The symlink target may be a file or directory, but it may not point outside of
|
||||||
the repo client.
|
the repo client.
|
||||||
.PP
|
.PP
|
||||||
|
The links are created in the order they are specified in the manifests. If
|
||||||
|
multiple elements specify the same source and destination, they will only be
|
||||||
|
applied as one, based on the first occurence. Links are created after any files
|
||||||
|
specified via copyfile elements are copied.
|
||||||
|
.PP
|
||||||
Element remove\-project
|
Element remove\-project
|
||||||
.PP
|
.PP
|
||||||
Deletes the named project from the internal manifest table, possibly allowing a
|
Deletes a project from the internal manifest table, possibly allowing a
|
||||||
subsequent project element in the same manifest file to replace the project with
|
subsequent project element in the same manifest file to replace the project with
|
||||||
a different source.
|
a different source.
|
||||||
.PP
|
.PP
|
||||||
This element is mostly useful in a local manifest file, where the user can
|
This element is mostly useful in a local manifest file, where the user can
|
||||||
remove a project, and possibly replace it with their own definition.
|
remove a project, and possibly replace it with their own definition.
|
||||||
.PP
|
.PP
|
||||||
|
The project `name` or project `path` can be used to specify the remove target
|
||||||
|
meaning one of them is required. If only name is specified, all projects with
|
||||||
|
that name are removed.
|
||||||
|
.PP
|
||||||
|
If both name and path are specified, only projects with the same name and path
|
||||||
|
are removed, meaning projects with the same name but in other locations are
|
||||||
|
kept.
|
||||||
|
.PP
|
||||||
|
If only path is specified, a matching project is removed regardless of its name.
|
||||||
|
Logic otherwise behaves like both are specified.
|
||||||
|
.PP
|
||||||
Attribute `optional`: Set to true to ignore remove\-project elements with no
|
Attribute `optional`: Set to true to ignore remove\-project elements with no
|
||||||
matching `project` element.
|
matching `project` element.
|
||||||
.PP
|
.PP
|
||||||
|
Attribute `base\-rev`: If specified, adds a check against the revision to be
|
||||||
|
removed. Manifest parse will fail and give a list of mismatch removes if the
|
||||||
|
revisions being removed have changed since base\-rev was set. Intended for use
|
||||||
|
with layered manifests using hash revisions to prevent patch branches hiding
|
||||||
|
newer upstream revisions. Also compares named refs like branches or tags but is
|
||||||
|
misleading if branches are used as base\-rev. Same syntax as the corresponding
|
||||||
|
element of `project`.
|
||||||
|
.PP
|
||||||
Element repo\-hooks
|
Element repo\-hooks
|
||||||
.PP
|
.PP
|
||||||
NB: See the [practical documentation](./repo\-hooks.md) for using repo hooks.
|
NB: See the [practical documentation](./repo\-hooks.md) for using repo hooks.
|
||||||
@@ -603,12 +686,18 @@ repository's root.
|
|||||||
"name" may not be an absolute path or use "." or ".." path components. These
|
"name" may not be an absolute path or use "." or ".." path components. These
|
||||||
restrictions are not enforced for [Local Manifests].
|
restrictions are not enforced for [Local Manifests].
|
||||||
.PP
|
.PP
|
||||||
Attribute `groups`: List of additional groups to which all projects in the
|
Attribute `groups`: Set of additional groups to which all projects in the
|
||||||
included manifest belong. This appends and recurses, meaning all projects in
|
included manifest belong. This appends and recurses, meaning all projects in
|
||||||
included manifests carry all parent include groups. Same syntax as the
|
included manifests carry all parent include groups. This also applies to all
|
||||||
|
extend\-project elements in the included manifests. Same syntax as the
|
||||||
corresponding element of `project`.
|
corresponding element of `project`.
|
||||||
.PP
|
.PP
|
||||||
Local Manifests
|
Attribute `revision`: Name of a Git branch (e.g. `main` or `refs/heads/main`)
|
||||||
|
default to which all projects in the included manifest belong. This recurses,
|
||||||
|
meaning it will apply to all projects in all manifests included as a result of
|
||||||
|
this element.
|
||||||
|
.PP
|
||||||
|
Local Manifests
|
||||||
.PP
|
.PP
|
||||||
Additional remotes and projects may be added through local manifest files stored
|
Additional remotes and projects may be added through local manifest files stored
|
||||||
in `$TOP_DIR/.repo/local_manifests/*.xml`.
|
in `$TOP_DIR/.repo/local_manifests/*.xml`.
|
||||||
|
|||||||
+32
-3
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "November 2022" "repo smartsync" "Repo Manual"
|
.TH REPO "1" "May 2026" "repo smartsync" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo smartsync - manual page for repo smartsync
|
repo \- repo smartsync - manual page for repo smartsync
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -20,11 +20,11 @@ number of CPU cores)
|
|||||||
.TP
|
.TP
|
||||||
\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
|
\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
|
||||||
number of network jobs to run in parallel (defaults to
|
number of network jobs to run in parallel (defaults to
|
||||||
\fB\-\-jobs\fR or 1)
|
\fB\-\-jobs\fR or 1). Ignored unless \fB\-\-no\-interleaved\fR is set
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
|
\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
|
||||||
number of local checkout jobs to run in parallel
|
number of local checkout jobs to run in parallel
|
||||||
(defaults to \fB\-\-jobs\fR or 8)
|
(defaults to \fB\-\-jobs\fR or 8). Ignored unless \fB\-\-nointerleaved\fR is set
|
||||||
.TP
|
.TP
|
||||||
\fB\-f\fR, \fB\-\-force\-broken\fR
|
\fB\-f\fR, \fB\-\-force\-broken\fR
|
||||||
obsolete option (to be deleted in the future)
|
obsolete option (to be deleted in the future)
|
||||||
@@ -37,11 +37,20 @@ overwrite an existing git directory if it needs to
|
|||||||
point to a different object directory. WARNING: this
|
point to a different object directory. WARNING: this
|
||||||
may cause loss of data
|
may cause loss of data
|
||||||
.TP
|
.TP
|
||||||
|
\fB\-\-force\-checkout\fR
|
||||||
|
force checkout even if it results in throwing away
|
||||||
|
uncommitted modifications. WARNING: this may cause
|
||||||
|
loss of data
|
||||||
|
.TP
|
||||||
\fB\-\-force\-remove\-dirty\fR
|
\fB\-\-force\-remove\-dirty\fR
|
||||||
force remove projects with uncommitted modifications
|
force remove projects with uncommitted modifications
|
||||||
if projects no longer exist in the manifest. WARNING:
|
if projects no longer exist in the manifest. WARNING:
|
||||||
this may cause loss of data
|
this may cause loss of data
|
||||||
.TP
|
.TP
|
||||||
|
\fB\-\-rebase\fR
|
||||||
|
rebase local commits regardless of whether they are
|
||||||
|
published
|
||||||
|
.TP
|
||||||
\fB\-l\fR, \fB\-\-local\-only\fR
|
\fB\-l\fR, \fB\-\-local\-only\fR
|
||||||
only update working tree, don't fetch
|
only update working tree, don't fetch
|
||||||
.TP
|
.TP
|
||||||
@@ -49,6 +58,12 @@ only update working tree, don't fetch
|
|||||||
use the existing manifest checkout as\-is. (do not
|
use the existing manifest checkout as\-is. (do not
|
||||||
update to the latest revision)
|
update to the latest revision)
|
||||||
.TP
|
.TP
|
||||||
|
\fB\-\-interleaved\fR
|
||||||
|
fetch and checkout projects in parallel (default)
|
||||||
|
.TP
|
||||||
|
\fB\-\-no\-interleaved\fR
|
||||||
|
fetch and checkout projects in phases
|
||||||
|
.TP
|
||||||
\fB\-n\fR, \fB\-\-network\-only\fR
|
\fB\-n\fR, \fB\-\-network\-only\fR
|
||||||
fetch only, don't update working tree
|
fetch only, don't update working tree
|
||||||
.TP
|
.TP
|
||||||
@@ -86,6 +101,10 @@ implies \fB\-c\fR
|
|||||||
\fB\-\-no\-use\-superproject\fR
|
\fB\-\-no\-use\-superproject\fR
|
||||||
disable use of manifest superprojects
|
disable use of manifest superprojects
|
||||||
.TP
|
.TP
|
||||||
|
\fB\-\-superproject\-revision\fR=\fI\,SUPERPROJECT_REVISION\/\fR
|
||||||
|
sync to superproject revision (applies to outer
|
||||||
|
manifest)
|
||||||
|
.TP
|
||||||
\fB\-\-tags\fR
|
\fB\-\-tags\fR
|
||||||
fetch tags
|
fetch tags
|
||||||
.TP
|
.TP
|
||||||
@@ -136,6 +155,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
|
||||||
|
|||||||
+37
-3
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "November 2022" "repo sync" "Repo Manual"
|
.TH REPO "1" "May 2026" "repo sync" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo sync - manual page for repo sync
|
repo \- repo sync - manual page for repo sync
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -20,11 +20,11 @@ number of CPU cores)
|
|||||||
.TP
|
.TP
|
||||||
\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
|
\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
|
||||||
number of network jobs to run in parallel (defaults to
|
number of network jobs to run in parallel (defaults to
|
||||||
\fB\-\-jobs\fR or 1)
|
\fB\-\-jobs\fR or 1). Ignored unless \fB\-\-no\-interleaved\fR is set
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
|
\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
|
||||||
number of local checkout jobs to run in parallel
|
number of local checkout jobs to run in parallel
|
||||||
(defaults to \fB\-\-jobs\fR or 8)
|
(defaults to \fB\-\-jobs\fR or 8). Ignored unless \fB\-\-nointerleaved\fR is set
|
||||||
.TP
|
.TP
|
||||||
\fB\-f\fR, \fB\-\-force\-broken\fR
|
\fB\-f\fR, \fB\-\-force\-broken\fR
|
||||||
obsolete option (to be deleted in the future)
|
obsolete option (to be deleted in the future)
|
||||||
@@ -37,11 +37,20 @@ overwrite an existing git directory if it needs to
|
|||||||
point to a different object directory. WARNING: this
|
point to a different object directory. WARNING: this
|
||||||
may cause loss of data
|
may cause loss of data
|
||||||
.TP
|
.TP
|
||||||
|
\fB\-\-force\-checkout\fR
|
||||||
|
force checkout even if it results in throwing away
|
||||||
|
uncommitted modifications. WARNING: this may cause
|
||||||
|
loss of data
|
||||||
|
.TP
|
||||||
\fB\-\-force\-remove\-dirty\fR
|
\fB\-\-force\-remove\-dirty\fR
|
||||||
force remove projects with uncommitted modifications
|
force remove projects with uncommitted modifications
|
||||||
if projects no longer exist in the manifest. WARNING:
|
if projects no longer exist in the manifest. WARNING:
|
||||||
this may cause loss of data
|
this may cause loss of data
|
||||||
.TP
|
.TP
|
||||||
|
\fB\-\-rebase\fR
|
||||||
|
rebase local commits regardless of whether they are
|
||||||
|
published
|
||||||
|
.TP
|
||||||
\fB\-l\fR, \fB\-\-local\-only\fR
|
\fB\-l\fR, \fB\-\-local\-only\fR
|
||||||
only update working tree, don't fetch
|
only update working tree, don't fetch
|
||||||
.TP
|
.TP
|
||||||
@@ -49,6 +58,12 @@ only update working tree, don't fetch
|
|||||||
use the existing manifest checkout as\-is. (do not
|
use the existing manifest checkout as\-is. (do not
|
||||||
update to the latest revision)
|
update to the latest revision)
|
||||||
.TP
|
.TP
|
||||||
|
\fB\-\-interleaved\fR
|
||||||
|
fetch and checkout projects in parallel (default)
|
||||||
|
.TP
|
||||||
|
\fB\-\-no\-interleaved\fR
|
||||||
|
fetch and checkout projects in phases
|
||||||
|
.TP
|
||||||
\fB\-n\fR, \fB\-\-network\-only\fR
|
\fB\-n\fR, \fB\-\-network\-only\fR
|
||||||
fetch only, don't update working tree
|
fetch only, don't update working tree
|
||||||
.TP
|
.TP
|
||||||
@@ -86,6 +101,10 @@ implies \fB\-c\fR
|
|||||||
\fB\-\-no\-use\-superproject\fR
|
\fB\-\-no\-use\-superproject\fR
|
||||||
disable use of manifest superprojects
|
disable use of manifest superprojects
|
||||||
.TP
|
.TP
|
||||||
|
\fB\-\-superproject\-revision\fR=\fI\,SUPERPROJECT_REVISION\/\fR
|
||||||
|
sync to superproject revision (applies to outer
|
||||||
|
manifest)
|
||||||
|
.TP
|
||||||
\fB\-\-tags\fR
|
\fB\-\-tags\fR
|
||||||
fetch tags
|
fetch tags
|
||||||
.TP
|
.TP
|
||||||
@@ -143,6 +162,16 @@ operate on this manifest and its submanifests
|
|||||||
.TP
|
.TP
|
||||||
\fB\-\-no\-repo\-verify\fR
|
\fB\-\-no\-repo\-verify\fR
|
||||||
do not verify repo source code
|
do not verify repo source code
|
||||||
|
.SS post\-sync hooks:
|
||||||
|
.TP
|
||||||
|
\fB\-\-no\-verify\fR
|
||||||
|
Do not run the post\-sync hook.
|
||||||
|
.TP
|
||||||
|
\fB\-\-verify\fR
|
||||||
|
Run the post\-sync hook without prompting.
|
||||||
|
.TP
|
||||||
|
\fB\-\-ignore\-hooks\fR
|
||||||
|
Do not abort if post\-sync hooks fail.
|
||||||
.PP
|
.PP
|
||||||
Run `repo help sync` to view the detailed manual.
|
Run `repo help sync` to view the detailed manual.
|
||||||
.SH DETAILS
|
.SH DETAILS
|
||||||
@@ -185,6 +214,11 @@ The \fB\-\-force\-sync\fR option can be used to overwrite existing git directori
|
|||||||
they have previously been linked to a different object directory. WARNING: This
|
they have previously been linked to a different object directory. WARNING: This
|
||||||
may cause data to be lost since refs may be removed when overwriting.
|
may cause data to be lost since refs may be removed when overwriting.
|
||||||
.PP
|
.PP
|
||||||
|
The \fB\-\-force\-checkout\fR option can be used to force git to switch revs even if the
|
||||||
|
index or the working tree differs from HEAD, and if there are untracked files.
|
||||||
|
WARNING: This may cause data to be lost since uncommitted changes may be
|
||||||
|
removed.
|
||||||
|
.PP
|
||||||
The \fB\-\-force\-remove\-dirty\fR option can be used to remove previously used projects
|
The \fB\-\-force\-remove\-dirty\fR option can be used to remove previously used projects
|
||||||
with uncommitted changes. WARNING: This may cause data to be lost since
|
with uncommitted changes. WARNING: This may cause data to be lost since
|
||||||
uncommitted changes may be removed with projects that no longer exist in the
|
uncommitted changes may be removed with projects that no longer exist in the
|
||||||
|
|||||||
+15
-3
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "August 2022" "repo upload" "Repo Manual"
|
.TH REPO "1" "June 2024" "repo upload" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo upload - manual page for repo upload
|
repo \- repo upload - manual page for repo upload
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -18,8 +18,11 @@ show this help message and exit
|
|||||||
number of jobs to run in parallel (default: based on
|
number of jobs to run in parallel (default: based on
|
||||||
number of CPU cores)
|
number of CPU cores)
|
||||||
.TP
|
.TP
|
||||||
\fB\-t\fR
|
\fB\-t\fR, \fB\-\-topic\-branch\fR
|
||||||
send local branch name to Gerrit Code Review
|
set the topic to the local branch name
|
||||||
|
.TP
|
||||||
|
\fB\-\-topic\fR=\fI\,TOPIC\/\fR
|
||||||
|
set topic for the change
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-hashtag\fR=\fI\,HASHTAGS\/\fR, \fB\-\-ht\fR=\fI\,HASHTAGS\/\fR
|
\fB\-\-hashtag\fR=\fI\,HASHTAGS\/\fR, \fB\-\-ht\fR=\fI\,HASHTAGS\/\fR
|
||||||
add hashtags (comma delimited) to the review
|
add hashtags (comma delimited) to the review
|
||||||
@@ -30,6 +33,9 @@ add local branch name as a hashtag
|
|||||||
\fB\-l\fR LABELS, \fB\-\-label\fR=\fI\,LABELS\/\fR
|
\fB\-l\fR LABELS, \fB\-\-label\fR=\fI\,LABELS\/\fR
|
||||||
add a label when uploading
|
add a label when uploading
|
||||||
.TP
|
.TP
|
||||||
|
\fB\-\-pd\fR=\fI\,PATCHSET_DESCRIPTION\/\fR, \fB\-\-patchset\-description\fR=\fI\,PATCHSET_DESCRIPTION\/\fR
|
||||||
|
description for patchset
|
||||||
|
.TP
|
||||||
\fB\-\-re\fR=\fI\,REVIEWERS\/\fR, \fB\-\-reviewers\fR=\fI\,REVIEWERS\/\fR
|
\fB\-\-re\fR=\fI\,REVIEWERS\/\fR, \fB\-\-reviewers\fR=\fI\,REVIEWERS\/\fR
|
||||||
request reviews from these people
|
request reviews from these people
|
||||||
.TP
|
.TP
|
||||||
@@ -198,6 +204,12 @@ review.URL.uploadnotify:
|
|||||||
Control e\-mail notifications when uploading.
|
Control e\-mail notifications when uploading.
|
||||||
https://gerrit\-review.googlesource.com/Documentation/user\-upload.html#notify
|
https://gerrit\-review.googlesource.com/Documentation/user\-upload.html#notify
|
||||||
.PP
|
.PP
|
||||||
|
review.URL.uploadwarningthreshold:
|
||||||
|
.PP
|
||||||
|
Repo will warn you if you are attempting to upload a large number of commits in
|
||||||
|
one or more branches. By default, the threshold is five commits. This option
|
||||||
|
allows you to override the warning threshold to a different value.
|
||||||
|
.PP
|
||||||
References
|
References
|
||||||
.PP
|
.PP
|
||||||
Gerrit Code Review: https://www.gerritcodereview.com/
|
Gerrit Code Review: https://www.gerritcodereview.com/
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
|
.TH REPO "1" "November 2025" "repo wipe" "Repo Manual"
|
||||||
|
.SH NAME
|
||||||
|
repo \- repo wipe - manual page for repo wipe
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B repo
|
||||||
|
\fI\,wipe <project>\/\fR...
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Summary
|
||||||
|
.PP
|
||||||
|
Wipe projects from the worktree
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
\fB\-h\fR, \fB\-\-help\fR
|
||||||
|
show this help message and exit
|
||||||
|
.TP
|
||||||
|
\fB\-f\fR, \fB\-\-force\fR
|
||||||
|
force wipe shared projects and uncommitted changes
|
||||||
|
.TP
|
||||||
|
\fB\-\-force\-uncommitted\fR
|
||||||
|
force wipe even if there are uncommitted changes
|
||||||
|
.TP
|
||||||
|
\fB\-\-force\-shared\fR
|
||||||
|
force wipe even if the project shares an object
|
||||||
|
directory
|
||||||
|
.SS Logging options:
|
||||||
|
.TP
|
||||||
|
\fB\-v\fR, \fB\-\-verbose\fR
|
||||||
|
show all output
|
||||||
|
.TP
|
||||||
|
\fB\-q\fR, \fB\-\-quiet\fR
|
||||||
|
only show errors
|
||||||
|
.SS Multi\-manifest options:
|
||||||
|
.TP
|
||||||
|
\fB\-\-outer\-manifest\fR
|
||||||
|
operate starting at the outermost manifest
|
||||||
|
.TP
|
||||||
|
\fB\-\-no\-outer\-manifest\fR
|
||||||
|
do not operate on outer manifests
|
||||||
|
.TP
|
||||||
|
\fB\-\-this\-manifest\-only\fR
|
||||||
|
only operate on this (sub)manifest
|
||||||
|
.TP
|
||||||
|
\fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
|
||||||
|
operate on this manifest and its submanifests
|
||||||
|
.PP
|
||||||
|
Run `repo help wipe` to view the detailed manual.
|
||||||
|
.SH DETAILS
|
||||||
|
.PP
|
||||||
|
The 'repo wipe' command removes the specified projects from the worktree (the
|
||||||
|
checked out source code) and deletes the project's git data from `.repo`.
|
||||||
|
.PP
|
||||||
|
This is a destructive operation and cannot be undone.
|
||||||
|
.PP
|
||||||
|
Projects can be specified either by name, or by a relative or absolute path to
|
||||||
|
the project's local directory.
|
||||||
|
.SH EXAMPLES
|
||||||
|
.SS # Wipe the project "platform/build" by name:
|
||||||
|
$ repo wipe platform/build
|
||||||
|
.SS # Wipe the project at the path "build/make":
|
||||||
|
$ repo wipe build/make
|
||||||
+6
-6
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "June 2023" "repo" "Repo Manual"
|
.TH REPO "1" "November 2025" "repo" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repository management tool built on top of git
|
repo \- repository management tool built on top of git
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -79,11 +79,8 @@ Download and checkout a change
|
|||||||
forall
|
forall
|
||||||
Run a shell command in each project
|
Run a shell command in each project
|
||||||
.TP
|
.TP
|
||||||
gitc\-delete
|
gc
|
||||||
Delete a GITC Client.
|
Cleaning up internal repo and Git state.
|
||||||
.TP
|
|
||||||
gitc\-init
|
|
||||||
Initialize a GITC Client.
|
|
||||||
.TP
|
.TP
|
||||||
grep
|
grep
|
||||||
Print lines matching a pattern
|
Print lines matching a pattern
|
||||||
@@ -135,6 +132,9 @@ Upload changes for code review
|
|||||||
.TP
|
.TP
|
||||||
version
|
version
|
||||||
Display the version of repo
|
Display the version of repo
|
||||||
|
.TP
|
||||||
|
wipe
|
||||||
|
Wipe projects from the worktree
|
||||||
.PP
|
.PP
|
||||||
See 'repo help <command>' for more information on a specific command.
|
See 'repo help <command>' for more information on a specific command.
|
||||||
Bug reports: https://issues.gerritcodereview.com/issues/new?component=1370071
|
Bug reports: https://issues.gerritcodereview.com/issues/new?component=1370071
|
||||||
|
|||||||
+247
-153
@@ -114,12 +114,40 @@ def XmlInt(node, attr, default=None):
|
|||||||
try:
|
try:
|
||||||
return int(value)
|
return int(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ManifestParseError(
|
raise ManifestParseError(f'manifest: invalid {attr}="{value}" integer')
|
||||||
'manifest: invalid %s="%s" integer' % (attr, value)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class _Default(object):
|
def normalize_url(url: str) -> str:
|
||||||
|
"""Mutate input 'url' into normalized form:
|
||||||
|
|
||||||
|
* remove trailing slashes
|
||||||
|
* convert SCP-like syntax to SSH URL
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: URL to modify
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The normalized URL.
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = url.rstrip("/")
|
||||||
|
parsed_url = urllib.parse.urlparse(url)
|
||||||
|
|
||||||
|
# This matches patterns like "git@github.com:foo".
|
||||||
|
scp_like_url_re = r"^[^/:]+@[^/:]+:[^/]+"
|
||||||
|
|
||||||
|
# If our URL is missing a schema and matches git's
|
||||||
|
# SCP-like syntax we should convert it to a proper
|
||||||
|
# SSH URL instead to make urljoin() happier.
|
||||||
|
#
|
||||||
|
# See: https://git-scm.com/docs/git-clone#URLS
|
||||||
|
if not parsed_url.scheme and re.match(scp_like_url_re, url):
|
||||||
|
return "ssh://" + url.replace(":", "/", 1)
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
class _Default:
|
||||||
"""Project defaults within the manifest."""
|
"""Project defaults within the manifest."""
|
||||||
|
|
||||||
revisionExpr = None
|
revisionExpr = None
|
||||||
@@ -127,6 +155,7 @@ class _Default(object):
|
|||||||
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
|
||||||
@@ -142,7 +171,7 @@ class _Default(object):
|
|||||||
return self.__dict__ != other.__dict__
|
return self.__dict__ != other.__dict__
|
||||||
|
|
||||||
|
|
||||||
class _XmlRemote(object):
|
class _XmlRemote:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name,
|
name,
|
||||||
@@ -182,20 +211,22 @@ class _XmlRemote(object):
|
|||||||
def _resolveFetchUrl(self):
|
def _resolveFetchUrl(self):
|
||||||
if self.fetchUrl is None:
|
if self.fetchUrl is None:
|
||||||
return ""
|
return ""
|
||||||
url = self.fetchUrl.rstrip("/")
|
|
||||||
manifestUrl = self.manifestUrl.rstrip("/")
|
|
||||||
# urljoin will gets confused over quite a few things. The ones we care
|
|
||||||
# about here are:
|
|
||||||
# * no scheme in the base url, like <hostname:port>
|
|
||||||
# We handle no scheme by replacing it with an obscure protocol, gopher
|
|
||||||
# and then replacing it with the original when we are done.
|
|
||||||
|
|
||||||
if manifestUrl.find(":") != manifestUrl.find("/") - 1:
|
fetch_url = normalize_url(self.fetchUrl)
|
||||||
url = urllib.parse.urljoin("gopher://" + manifestUrl, url)
|
manifest_url = normalize_url(self.manifestUrl)
|
||||||
url = re.sub(r"^gopher://", "", url)
|
|
||||||
|
# urljoin doesn't like URLs with no scheme in the base URL
|
||||||
|
# such as file paths. We handle this by prefixing it with
|
||||||
|
# an obscure protocol, gopher, and replacing it with the
|
||||||
|
# original after urljoin
|
||||||
|
if manifest_url.find(":") != manifest_url.find("/") - 1:
|
||||||
|
fetch_url = urllib.parse.urljoin(
|
||||||
|
"gopher://" + manifest_url, fetch_url
|
||||||
|
)
|
||||||
|
fetch_url = re.sub(r"^gopher://", "", fetch_url)
|
||||||
else:
|
else:
|
||||||
url = urllib.parse.urljoin(manifestUrl, url)
|
fetch_url = urllib.parse.urljoin(manifest_url, fetch_url)
|
||||||
return url
|
return fetch_url
|
||||||
|
|
||||||
def ToRemoteSpec(self, projectName):
|
def ToRemoteSpec(self, projectName):
|
||||||
fetchUrl = self.resolvedFetchUrl.rstrip("/")
|
fetchUrl = self.resolvedFetchUrl.rstrip("/")
|
||||||
@@ -225,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.
|
||||||
@@ -251,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
|
||||||
@@ -274,8 +305,8 @@ 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=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,
|
||||||
)
|
)
|
||||||
@@ -315,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
|
||||||
@@ -329,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."""
|
||||||
@@ -351,10 +380,10 @@ 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(object):
|
class XmlManifest:
|
||||||
"""manages the repo configuration file"""
|
"""manages the repo configuration file"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -363,7 +392,7 @@ class XmlManifest(object):
|
|||||||
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,
|
||||||
):
|
):
|
||||||
@@ -379,7 +408,8 @@ class XmlManifest(object):
|
|||||||
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.
|
||||||
"""
|
"""
|
||||||
@@ -402,14 +432,9 @@ class XmlManifest(object):
|
|||||||
self.manifestFileOverrides = {}
|
self.manifestFileOverrides = {}
|
||||||
self.local_manifests = local_manifests
|
self.local_manifests = local_manifests
|
||||||
self._load_local_manifests = True
|
self._load_local_manifests = True
|
||||||
self.parent_groups = parent_groups
|
self.parent_groups = parent_groups or set()
|
||||||
self.default_groups = default_groups
|
self.default_groups = default_groups
|
||||||
|
|
||||||
if outer_client and self.isGitcClient:
|
|
||||||
raise ManifestParseError(
|
|
||||||
"Multi-manifest is incompatible with `gitc-init`"
|
|
||||||
)
|
|
||||||
|
|
||||||
if submanifest_path and not outer_client:
|
if submanifest_path and not outer_client:
|
||||||
# If passing a submanifest_path, there must be an outer_client.
|
# If passing a submanifest_path, there must be an outer_client.
|
||||||
raise ManifestParseError(f"Bad call to {self.__class__.__name__}")
|
raise ManifestParseError(f"Bad call to {self.__class__.__name__}")
|
||||||
@@ -542,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")
|
||||||
@@ -599,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")
|
||||||
@@ -629,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):
|
||||||
@@ -700,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":
|
||||||
@@ -724,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 = set(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 = set(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(""))
|
||||||
@@ -788,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",
|
||||||
}
|
}
|
||||||
@@ -800,17 +837,17 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
for child in node.childNodes:
|
for child in node.childNodes:
|
||||||
if child.nodeType == xml.dom.Node.ELEMENT_NODE:
|
if child.nodeType == xml.dom.Node.ELEMENT_NODE:
|
||||||
attrs = child.attributes
|
attrs = child.attributes
|
||||||
element = dict(
|
element = {
|
||||||
(attrs.item(i).localName, attrs.item(i).value)
|
attrs.item(i).localName: attrs.item(i).value
|
||||||
for i in range(attrs.length)
|
for i in range(attrs.length)
|
||||||
)
|
}
|
||||||
if child.nodeName in SINGLE_ELEMENTS:
|
if child.nodeName in SINGLE_ELEMENTS:
|
||||||
ret[child.nodeName] = element
|
ret[child.nodeName] = element
|
||||||
elif child.nodeName in MULTI_ELEMENTS:
|
elif child.nodeName in MULTI_ELEMENTS:
|
||||||
ret.setdefault(child.nodeName, []).append(element)
|
ret.setdefault(child.nodeName, []).append(element)
|
||||||
else:
|
else:
|
||||||
raise ManifestParseError(
|
raise ManifestParseError(
|
||||||
'Unhandled element "%s"' % (child.nodeName,)
|
f'Unhandled element "{child.nodeName}"'
|
||||||
)
|
)
|
||||||
|
|
||||||
append_children(element, child)
|
append_children(element, child)
|
||||||
@@ -857,8 +894,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
self._Load()
|
self._Load()
|
||||||
outer = self._outer_client
|
outer = self._outer_client
|
||||||
yield outer
|
yield outer
|
||||||
for tree in outer.all_children:
|
yield from outer.all_children
|
||||||
yield tree
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def all_children(self):
|
def all_children(self):
|
||||||
@@ -867,8 +903,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
for child in self._submanifests.values():
|
for child in self._submanifests.values():
|
||||||
if child.repo_client:
|
if child.repo_client:
|
||||||
yield child.repo_client
|
yield child.repo_client
|
||||||
for tree in child.repo_client.all_children:
|
yield from child.repo_client.all_children
|
||||||
yield tree
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_prefix(self):
|
def path_prefix(self):
|
||||||
@@ -987,13 +1022,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
@property
|
@property
|
||||||
def PartialCloneExclude(self):
|
def PartialCloneExclude(self):
|
||||||
exclude = self.manifest.manifestProject.partial_clone_exclude or ""
|
exclude = self.manifest.manifestProject.partial_clone_exclude or ""
|
||||||
return set(x.strip() for x in exclude.split(","))
|
return {x.strip() for x in exclude.split(",")}
|
||||||
|
|
||||||
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):
|
||||||
@@ -1093,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()
|
||||||
@@ -1148,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.
|
||||||
@@ -1179,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,
|
||||||
)
|
)
|
||||||
@@ -1239,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,
|
||||||
):
|
):
|
||||||
@@ -1248,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.
|
||||||
@@ -1260,35 +1295,42 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
try:
|
try:
|
||||||
root = xml.dom.minidom.parse(path)
|
root = xml.dom.minidom.parse(path)
|
||||||
except (OSError, xml.parsers.expat.ExpatError) as e:
|
except (OSError, xml.parsers.expat.ExpatError) as e:
|
||||||
raise ManifestParseError(
|
raise ManifestParseError(f"error parsing manifest {path}: {e}")
|
||||||
"error parsing manifest %s: %s" % (path, e)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not root or not root.childNodes:
|
if not root or not root.childNodes:
|
||||||
raise ManifestParseError("no root node in %s" % (path,))
|
raise ManifestParseError(f"no root node in {path}")
|
||||||
|
|
||||||
for manifest in root.childNodes:
|
for manifest in root.childNodes:
|
||||||
if manifest.nodeName == "manifest":
|
if (
|
||||||
|
manifest.nodeType == manifest.ELEMENT_NODE
|
||||||
|
and manifest.nodeName == "manifest"
|
||||||
|
):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise ManifestParseError("no <manifest> in %s" % (path,))
|
raise ManifestParseError(f"no <manifest> in {path}")
|
||||||
|
|
||||||
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:
|
||||||
msg = self._CheckLocalPath(name)
|
msg = self._CheckLocalPath(name)
|
||||||
if msg:
|
if msg:
|
||||||
raise ManifestInvalidPathError(
|
raise ManifestInvalidPathError(
|
||||||
'<include> invalid "name": %s: %s' % (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):
|
||||||
@@ -1304,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(
|
||||||
"failed parsing included manifest %s: %s" % (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
|
||||||
|
|
||||||
@@ -1421,6 +1453,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
|
|
||||||
repo_hooks_project = None
|
repo_hooks_project = None
|
||||||
enabled_repo_hooks = None
|
enabled_repo_hooks = None
|
||||||
|
failed_revision_changes = []
|
||||||
for node in itertools.chain(*node_list):
|
for node in itertools.chain(*node_list):
|
||||||
if node.nodeName == "project":
|
if node.nodeName == "project":
|
||||||
project = self._ParseProject(node)
|
project = self._ParseProject(node)
|
||||||
@@ -1438,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:
|
||||||
@@ -1447,6 +1480,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
remote = self._get_remote(node)
|
remote = self._get_remote(node)
|
||||||
dest_branch = node.getAttribute("dest-branch")
|
dest_branch = node.getAttribute("dest-branch")
|
||||||
upstream = node.getAttribute("upstream")
|
upstream = node.getAttribute("upstream")
|
||||||
|
base_revision = node.getAttribute("base-rev")
|
||||||
|
|
||||||
named_projects = self._projects[name]
|
named_projects = self._projects[name]
|
||||||
if dest_path and not path and len(named_projects) > 1:
|
if dest_path and not path and len(named_projects) > 1:
|
||||||
@@ -1458,8 +1492,23 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
if path and p.relpath != path:
|
if path and p.relpath != path:
|
||||||
continue
|
continue
|
||||||
if groups:
|
if groups:
|
||||||
p.groups.extend(groups)
|
p.groups |= groups
|
||||||
|
# Drop local groups so we don't mistakenly omit this
|
||||||
|
# project from the superproject override manifest.
|
||||||
|
p.groups = {
|
||||||
|
g
|
||||||
|
for g in p.groups
|
||||||
|
if not g.startswith(LOCAL_MANIFEST_GROUP_PREFIX)
|
||||||
|
}
|
||||||
|
|
||||||
if revision:
|
if revision:
|
||||||
|
if base_revision:
|
||||||
|
if p.revisionExpr != base_revision:
|
||||||
|
failed_revision_changes.append(
|
||||||
|
"extend-project name %s mismatch base "
|
||||||
|
"%s vs revision %s"
|
||||||
|
% (name, base_revision, p.revisionExpr)
|
||||||
|
)
|
||||||
p.SetRevision(revision)
|
p.SetRevision(revision)
|
||||||
|
|
||||||
if remote_name:
|
if remote_name:
|
||||||
@@ -1481,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:
|
||||||
@@ -1534,6 +1591,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
if node.nodeName == "remove-project":
|
if node.nodeName == "remove-project":
|
||||||
name = node.getAttribute("name")
|
name = node.getAttribute("name")
|
||||||
path = node.getAttribute("path")
|
path = node.getAttribute("path")
|
||||||
|
base_revision = node.getAttribute("base-rev")
|
||||||
|
|
||||||
# Name or path needed.
|
# Name or path needed.
|
||||||
if not name and not path:
|
if not name and not path:
|
||||||
@@ -1547,6 +1605,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
for projname, projects in list(self._projects.items()):
|
for projname, projects in list(self._projects.items()):
|
||||||
for p in projects:
|
for p in projects:
|
||||||
if name == projname and not path:
|
if name == projname and not path:
|
||||||
|
if base_revision:
|
||||||
|
if p.revisionExpr != base_revision:
|
||||||
|
failed_revision_changes.append(
|
||||||
|
"remove-project name %s mismatch base "
|
||||||
|
"%s vs revision %s"
|
||||||
|
% (name, base_revision, p.revisionExpr)
|
||||||
|
)
|
||||||
del self._paths[p.relpath]
|
del self._paths[p.relpath]
|
||||||
if not removed_project:
|
if not removed_project:
|
||||||
del self._projects[name]
|
del self._projects[name]
|
||||||
@@ -1554,6 +1619,17 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
elif path == p.relpath and (
|
elif path == p.relpath and (
|
||||||
name == projname or not name
|
name == projname or not name
|
||||||
):
|
):
|
||||||
|
if base_revision:
|
||||||
|
if p.revisionExpr != base_revision:
|
||||||
|
failed_revision_changes.append(
|
||||||
|
"remove-project path %s mismatch base "
|
||||||
|
"%s vs revision %s"
|
||||||
|
% (
|
||||||
|
p.relpath,
|
||||||
|
base_revision,
|
||||||
|
p.revisionExpr,
|
||||||
|
)
|
||||||
|
)
|
||||||
self._projects[projname].remove(p)
|
self._projects[projname].remove(p)
|
||||||
del self._paths[p.relpath]
|
del self._paths[p.relpath]
|
||||||
removed_project = p.name
|
removed_project = p.name
|
||||||
@@ -1573,6 +1649,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
"project: %s" % node.toxml()
|
"project: %s" % node.toxml()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if failed_revision_changes:
|
||||||
|
raise ManifestParseError(
|
||||||
|
"revision base check failed, rebase patches and update "
|
||||||
|
"base revs for: ",
|
||||||
|
failed_revision_changes,
|
||||||
|
)
|
||||||
|
|
||||||
# Store repo hooks project information.
|
# Store repo hooks project information.
|
||||||
if repo_hooks_project:
|
if repo_hooks_project:
|
||||||
# Store a reference to the Project.
|
# Store a reference to the Project.
|
||||||
@@ -1686,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)
|
||||||
@@ -1748,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 == "":
|
||||||
@@ -1764,13 +1854,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
msg = self._CheckLocalPath(name)
|
msg = self._CheckLocalPath(name)
|
||||||
if msg:
|
if msg:
|
||||||
raise ManifestInvalidPathError(
|
raise ManifestInvalidPathError(
|
||||||
'<submanifest> invalid "name": %s: %s' % (name, msg)
|
f'<submanifest> invalid "name": {name}: {msg}'
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
msg = self._CheckLocalPath(path)
|
msg = self._CheckLocalPath(path)
|
||||||
if msg:
|
if msg:
|
||||||
raise ManifestInvalidPathError(
|
raise ManifestInvalidPathError(
|
||||||
'<submanifest> invalid "path": %s: %s' % (path, msg)
|
f'<submanifest> invalid "path": {path}: {msg}'
|
||||||
)
|
)
|
||||||
|
|
||||||
submanifest = _XmlSubmanifest(
|
submanifest = _XmlSubmanifest(
|
||||||
@@ -1805,7 +1895,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
msg = self._CheckLocalPath(name, dir_ok=True)
|
msg = self._CheckLocalPath(name, dir_ok=True)
|
||||||
if msg:
|
if msg:
|
||||||
raise ManifestInvalidPathError(
|
raise ManifestInvalidPathError(
|
||||||
'<project> invalid "name": %s: %s' % (name, msg)
|
f'<project> invalid "name": {name}: {msg}'
|
||||||
)
|
)
|
||||||
if parent:
|
if parent:
|
||||||
name = self._JoinName(parent.name, name)
|
name = self._JoinName(parent.name, name)
|
||||||
@@ -1815,7 +1905,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
remote = self._default.remote
|
remote = self._default.remote
|
||||||
if remote is None:
|
if remote is None:
|
||||||
raise ManifestParseError(
|
raise ManifestParseError(
|
||||||
"no remote for project %s within %s" % (name, self.manifestFile)
|
f"no remote for project {name} within {self.manifestFile}"
|
||||||
)
|
)
|
||||||
|
|
||||||
revisionExpr = node.getAttribute("revision") or remote.revision
|
revisionExpr = node.getAttribute("revision") or remote.revision
|
||||||
@@ -1836,7 +1926,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
msg = self._CheckLocalPath(path, dir_ok=True, cwd_dot_ok=True)
|
msg = self._CheckLocalPath(path, dir_ok=True, cwd_dot_ok=True)
|
||||||
if msg:
|
if msg:
|
||||||
raise ManifestInvalidPathError(
|
raise ManifestInvalidPathError(
|
||||||
'<project> invalid "path": %s: %s' % (path, msg)
|
f'<project> invalid "path": {path}: {msg}'
|
||||||
)
|
)
|
||||||
|
|
||||||
rebase = XmlBool(node, "rebase", True)
|
rebase = XmlBool(node, "rebase", True)
|
||||||
@@ -1851,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,
|
||||||
@@ -1876,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):
|
||||||
@@ -1899,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,
|
||||||
@@ -1909,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)
|
||||||
)
|
)
|
||||||
@@ -1997,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
|
||||||
)
|
)
|
||||||
@@ -2048,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.
|
||||||
@@ -2093,7 +2189,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
if not cwd_dot_ok or parts != ["."]:
|
if not cwd_dot_ok or parts != ["."]:
|
||||||
for part in set(parts):
|
for part in set(parts):
|
||||||
if part in {".", "..", ".git"} or part.startswith(".repo"):
|
if part in {".", "..", ".git"} or part.startswith(".repo"):
|
||||||
return "bad component: %s" % (part,)
|
return f"bad component: {part}"
|
||||||
|
|
||||||
if not dir_ok and resep.match(path[-1]):
|
if not dir_ok and resep.match(path[-1]):
|
||||||
return "dirs not allowed"
|
return "dirs not allowed"
|
||||||
@@ -2129,7 +2225,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
msg = cls._CheckLocalPath(dest)
|
msg = cls._CheckLocalPath(dest)
|
||||||
if msg:
|
if msg:
|
||||||
raise ManifestInvalidPathError(
|
raise ManifestInvalidPathError(
|
||||||
'<%s> invalid "dest": %s: %s' % (element, dest, msg)
|
f'<{element}> invalid "dest": {dest}: {msg}'
|
||||||
)
|
)
|
||||||
|
|
||||||
# |src| is the file we read from or path we point to for symlinks.
|
# |src| is the file we read from or path we point to for symlinks.
|
||||||
@@ -2140,7 +2236,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
)
|
)
|
||||||
if msg:
|
if msg:
|
||||||
raise ManifestInvalidPathError(
|
raise ManifestInvalidPathError(
|
||||||
'<%s> invalid "src": %s: %s' % (element, src, msg)
|
f'<{element}> invalid "src": {src}: {msg}'
|
||||||
)
|
)
|
||||||
|
|
||||||
def _ParseCopyFile(self, project, node):
|
def _ParseCopyFile(self, project, node):
|
||||||
@@ -2184,7 +2280,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
v = self._remotes.get(name)
|
v = self._remotes.get(name)
|
||||||
if not v:
|
if not v:
|
||||||
raise ManifestParseError(
|
raise ManifestParseError(
|
||||||
"remote %s not defined in %s" % (name, self.manifestFile)
|
f"remote {name} not defined in {self.manifestFile}"
|
||||||
)
|
)
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@@ -2210,7 +2306,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
toProjects = manifest.paths
|
toProjects = manifest.paths
|
||||||
|
|
||||||
fromKeys = sorted(fromProjects.keys())
|
fromKeys = sorted(fromProjects.keys())
|
||||||
toKeys = sorted(toProjects.keys())
|
toKeys = set(toProjects.keys())
|
||||||
|
|
||||||
diff = {
|
diff = {
|
||||||
"added": [],
|
"added": [],
|
||||||
@@ -2221,13 +2317,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
}
|
}
|
||||||
|
|
||||||
for proj in fromKeys:
|
for proj in fromKeys:
|
||||||
|
fromProj = fromProjects[proj]
|
||||||
if proj not in toKeys:
|
if proj not in toKeys:
|
||||||
diff["removed"].append(fromProjects[proj])
|
diff["removed"].append(fromProj)
|
||||||
elif not fromProjects[proj].Exists:
|
elif not fromProj.Exists:
|
||||||
diff["missing"].append(toProjects[proj])
|
diff["missing"].append(toProjects[proj])
|
||||||
toKeys.remove(proj)
|
toKeys.remove(proj)
|
||||||
else:
|
else:
|
||||||
fromProj = fromProjects[proj]
|
|
||||||
toProj = toProjects[proj]
|
toProj = toProjects[proj]
|
||||||
try:
|
try:
|
||||||
fromRevId = fromProj.GetCommitRevisionId()
|
fromRevId = fromProj.GetCommitRevisionId()
|
||||||
@@ -2239,8 +2335,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
diff["changed"].append((fromProj, toProj))
|
diff["changed"].append((fromProj, toProj))
|
||||||
toKeys.remove(proj)
|
toKeys.remove(proj)
|
||||||
|
|
||||||
for proj in toKeys:
|
diff["added"].extend(toProjects[proj] for proj in sorted(toKeys))
|
||||||
diff["added"].append(toProjects[proj])
|
|
||||||
|
|
||||||
return diff
|
return diff
|
||||||
|
|
||||||
@@ -2262,7 +2357,6 @@ class RepoClient(XmlManifest):
|
|||||||
submanifest_path: The submanifest root relative to the repo root.
|
submanifest_path: The submanifest root relative to the repo root.
|
||||||
**kwargs: Additional keyword arguments, passed to XmlManifest.
|
**kwargs: Additional keyword arguments, passed to XmlManifest.
|
||||||
"""
|
"""
|
||||||
self.isGitcClient = False
|
|
||||||
submanifest_path = submanifest_path or ""
|
submanifest_path = submanifest_path or ""
|
||||||
if submanifest_path:
|
if submanifest_path:
|
||||||
self._CheckLocalPath(submanifest_path)
|
self._CheckLocalPath(submanifest_path)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ def RunPager(globalConfig):
|
|||||||
|
|
||||||
|
|
||||||
def TerminatePager():
|
def TerminatePager():
|
||||||
global pager_process, old_stdout, old_stderr
|
global pager_process
|
||||||
if pager_process:
|
if pager_process:
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
|||||||
+47
-34
@@ -57,8 +57,8 @@ def _validate_winpath(path):
|
|||||||
if _winpath_is_valid(path):
|
if _winpath_is_valid(path):
|
||||||
return path
|
return path
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Path "{}" must be a relative path or an absolute '
|
f'Path "{path}" must be a relative path or an absolute '
|
||||||
"path starting with a drive letter".format(path)
|
"path starting with a drive letter"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -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:
|
||||||
@@ -193,10 +199,9 @@ def _walk_windows_impl(top, topdown, onerror, followlinks):
|
|||||||
for name in dirs:
|
for name in dirs:
|
||||||
new_path = os.path.join(top, name)
|
new_path = os.path.join(top, name)
|
||||||
if followlinks or not islink(new_path):
|
if followlinks or not islink(new_path):
|
||||||
for x in _walk_windows_impl(
|
yield from _walk_windows_impl(
|
||||||
new_path, topdown, onerror, followlinks
|
new_path, topdown, onerror, followlinks
|
||||||
):
|
)
|
||||||
yield x
|
|
||||||
if not topdown:
|
if not topdown:
|
||||||
yield top, dirs, nondirs
|
yield top, dirs, nondirs
|
||||||
|
|
||||||
@@ -217,6 +222,43 @@ def rmdir(path):
|
|||||||
os.rmdir(_makelongpath(path))
|
os.rmdir(_makelongpath(path))
|
||||||
|
|
||||||
|
|
||||||
|
def removedirs(path: str) -> None:
|
||||||
|
"""Remove a directory tree of symlinks and empty directories.
|
||||||
|
|
||||||
|
Walks |path| bottom-up without following symlinks. Removes symlinks
|
||||||
|
and empty directories. Stops at (and preserves) regular files and
|
||||||
|
non-empty directories. Silently succeeds if |path| does not exist.
|
||||||
|
|
||||||
|
Availability: Unix, Windows.
|
||||||
|
"""
|
||||||
|
if islink(path):
|
||||||
|
remove(path)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not isdir(path):
|
||||||
|
return
|
||||||
|
|
||||||
|
for dirpath, dirnames, filenames in walk(path, topdown=False):
|
||||||
|
for name in dirnames:
|
||||||
|
entry = os.path.join(dirpath, name)
|
||||||
|
if islink(entry):
|
||||||
|
remove(entry)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
rmdir(entry)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
for name in filenames:
|
||||||
|
entry = os.path.join(dirpath, name)
|
||||||
|
if islink(entry):
|
||||||
|
remove(entry)
|
||||||
|
|
||||||
|
try:
|
||||||
|
rmdir(path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def isdir(path):
|
def isdir(path):
|
||||||
"""os.path.isdir(path) wrapper with support for long paths on Windows.
|
"""os.path.isdir(path) wrapper with support for long paths on Windows.
|
||||||
|
|
||||||
@@ -252,32 +294,3 @@ def readlink(path):
|
|||||||
return platform_utils_win32.readlink(_makelongpath(path))
|
return platform_utils_win32.readlink(_makelongpath(path))
|
||||||
else:
|
else:
|
||||||
return os.readlink(path)
|
return os.readlink(path)
|
||||||
|
|
||||||
|
|
||||||
def realpath(path):
|
|
||||||
"""Return the canonical path of the specified filename, eliminating
|
|
||||||
any symbolic links encountered in the path.
|
|
||||||
|
|
||||||
Availability: Windows, Unix.
|
|
||||||
"""
|
|
||||||
if isWindows():
|
|
||||||
current_path = os.path.abspath(path)
|
|
||||||
path_tail = []
|
|
||||||
for c in range(0, 100): # Avoid cycles
|
|
||||||
if islink(current_path):
|
|
||||||
target = readlink(current_path)
|
|
||||||
current_path = os.path.join(
|
|
||||||
os.path.dirname(current_path), target
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
basename = os.path.basename(current_path)
|
|
||||||
if basename == "":
|
|
||||||
path_tail.append(current_path)
|
|
||||||
break
|
|
||||||
path_tail.append(basename)
|
|
||||||
current_path = os.path.dirname(current_path)
|
|
||||||
path_tail.reverse()
|
|
||||||
result = os.path.normpath(os.path.join(*path_tail))
|
|
||||||
return result
|
|
||||||
else:
|
|
||||||
return os.path.realpath(path)
|
|
||||||
|
|||||||
@@ -186,9 +186,7 @@ def _create_symlink(source, link_name, dwFlags):
|
|||||||
error_desc = FormatError(code).strip()
|
error_desc = FormatError(code).strip()
|
||||||
if code == ERROR_PRIVILEGE_NOT_HELD:
|
if code == ERROR_PRIVILEGE_NOT_HELD:
|
||||||
raise OSError(errno.EPERM, error_desc, link_name)
|
raise OSError(errno.EPERM, error_desc, link_name)
|
||||||
_raise_winerror(
|
_raise_winerror(code, f'Error creating symbolic link "{link_name}"')
|
||||||
code, 'Error creating symbolic link "{}"'.format(link_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def islink(path):
|
def islink(path):
|
||||||
@@ -210,7 +208,7 @@ def readlink(path):
|
|||||||
)
|
)
|
||||||
if reparse_point_handle == INVALID_HANDLE_VALUE:
|
if reparse_point_handle == INVALID_HANDLE_VALUE:
|
||||||
_raise_winerror(
|
_raise_winerror(
|
||||||
get_last_error(), 'Error opening symbolic link "{}"'.format(path)
|
get_last_error(), f'Error opening symbolic link "{path}"'
|
||||||
)
|
)
|
||||||
target_buffer = c_buffer(MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
|
target_buffer = c_buffer(MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
|
||||||
n_bytes_returned = DWORD()
|
n_bytes_returned = DWORD()
|
||||||
@@ -227,7 +225,7 @@ def readlink(path):
|
|||||||
CloseHandle(reparse_point_handle)
|
CloseHandle(reparse_point_handle)
|
||||||
if not io_result:
|
if not io_result:
|
||||||
_raise_winerror(
|
_raise_winerror(
|
||||||
get_last_error(), 'Error reading symbolic link "{}"'.format(path)
|
get_last_error(), f'Error reading symbolic link "{path}"'
|
||||||
)
|
)
|
||||||
rdb = REPARSE_DATA_BUFFER.from_buffer(target_buffer)
|
rdb = REPARSE_DATA_BUFFER.from_buffer(target_buffer)
|
||||||
if rdb.ReparseTag == IO_REPARSE_TAG_SYMLINK:
|
if rdb.ReparseTag == IO_REPARSE_TAG_SYMLINK:
|
||||||
@@ -236,11 +234,11 @@ def readlink(path):
|
|||||||
return rdb.MountPointReparseBuffer.PrintName
|
return rdb.MountPointReparseBuffer.PrintName
|
||||||
# Unsupported reparse point type.
|
# Unsupported reparse point type.
|
||||||
_raise_winerror(
|
_raise_winerror(
|
||||||
ERROR_NOT_SUPPORTED, 'Error reading symbolic link "{}"'.format(path)
|
ERROR_NOT_SUPPORTED, f'Error reading symbolic link "{path}"'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _raise_winerror(code, error_desc):
|
def _raise_winerror(code, error_desc):
|
||||||
win_error_desc = FormatError(code).strip()
|
win_error_desc = FormatError(code).strip()
|
||||||
error_desc = "{0}: {1}".format(error_desc, win_error_desc)
|
error_desc = f"{error_desc}: {win_error_desc}"
|
||||||
raise WinError(code, error_desc)
|
raise WinError(code, error_desc)
|
||||||
|
|||||||
+42
-17
@@ -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
|
||||||
@@ -52,11 +55,11 @@ def duration_str(total):
|
|||||||
uses microsecond resolution. This makes for noisy output.
|
uses microsecond resolution. This makes for noisy output.
|
||||||
"""
|
"""
|
||||||
hours, mins, secs = convert_to_hms(total)
|
hours, mins, secs = convert_to_hms(total)
|
||||||
ret = "%.3fs" % (secs,)
|
ret = f"{secs:.3f}s"
|
||||||
if mins:
|
if mins:
|
||||||
ret = "%im%s" % (mins, ret)
|
ret = f"{mins}m{ret}"
|
||||||
if hours:
|
if hours:
|
||||||
ret = "%ih%s" % (hours, ret)
|
ret = f"{hours}h{ret}"
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
@@ -82,7 +85,7 @@ def jobs_str(total):
|
|||||||
return f"{total} job{'s' if total > 1 else ''}"
|
return f"{total} job{'s' if total > 1 else ''}"
|
||||||
|
|
||||||
|
|
||||||
class Progress(object):
|
class Progress:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
title,
|
title,
|
||||||
@@ -100,6 +103,8 @@ class Progress(object):
|
|||||||
self._show = not delay
|
self._show = not delay
|
||||||
self._units = units
|
self._units = units
|
||||||
self._elide = elide and _TTY
|
self._elide = elide and _TTY
|
||||||
|
self._quiet = quiet
|
||||||
|
self._ended = False
|
||||||
|
|
||||||
# Only show the active jobs section if we run more than one in parallel.
|
# Only show the active jobs section if we run more than one in parallel.
|
||||||
self._show_jobs = False
|
self._show_jobs = False
|
||||||
@@ -114,15 +119,14 @@ class Progress(object):
|
|||||||
)
|
)
|
||||||
self._update_thread.daemon = True
|
self._update_thread.daemon = True
|
||||||
|
|
||||||
# When quiet, never show any output. It's a bit hacky, but reusing the
|
if not quiet and show_elapsed:
|
||||||
# existing logic that delays initial output keeps the rest of the class
|
|
||||||
# clean. Basically we set the start time to years in the future.
|
|
||||||
if quiet:
|
|
||||||
self._show = False
|
|
||||||
self._start += 2**32
|
|
||||||
elif show_elapsed:
|
|
||||||
self._update_thread.start()
|
self._update_thread.start()
|
||||||
|
|
||||||
|
def update_total(self, new_total):
|
||||||
|
"""Updates the total if the new total is larger."""
|
||||||
|
if new_total > self._total:
|
||||||
|
self._total = new_total
|
||||||
|
|
||||||
def _update_loop(self):
|
def _update_loop(self):
|
||||||
while True:
|
while True:
|
||||||
self.update(inc=0)
|
self.update(inc=0)
|
||||||
@@ -132,11 +136,11 @@ class Progress(object):
|
|||||||
def _write(self, s):
|
def _write(self, s):
|
||||||
s = "\r" + s
|
s = "\r" + s
|
||||||
if self._elide:
|
if self._elide:
|
||||||
col = os.get_terminal_size(sys.stderr.fileno()).columns
|
col = os.get_terminal_size(_STDERR.fileno()).columns
|
||||||
if len(s) > col:
|
if len(s) > col:
|
||||||
s = s[: col - 1] + ".."
|
s = s[: col - 1] + ".."
|
||||||
sys.stderr.write(s)
|
_STDERR.write(s)
|
||||||
sys.stderr.flush()
|
_STDERR.flush()
|
||||||
|
|
||||||
def start(self, name):
|
def start(self, name):
|
||||||
self._active += 1
|
self._active += 1
|
||||||
@@ -155,12 +159,14 @@ class Progress(object):
|
|||||||
inc: The number of items completed.
|
inc: The number of items completed.
|
||||||
msg: The message to display. If None, use the last message.
|
msg: The message to display. If None, use the last message.
|
||||||
"""
|
"""
|
||||||
|
if self._ended:
|
||||||
|
return
|
||||||
self._done += inc
|
self._done += inc
|
||||||
if msg is None:
|
if msg is None:
|
||||||
msg = self._last_msg
|
msg = self._last_msg
|
||||||
self._last_msg = msg
|
self._last_msg = msg
|
||||||
|
|
||||||
if not _TTY or IsTraceToStderr():
|
if not _TTY or IsTraceToStderr() or self._quiet:
|
||||||
return
|
return
|
||||||
|
|
||||||
elapsed_sec = time.time() - self._start
|
elapsed_sec = time.time() - self._start
|
||||||
@@ -200,9 +206,28 @@ class Progress(object):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def display_message(self, msg):
|
||||||
|
"""Clears the current progress line and prints a message above it.
|
||||||
|
|
||||||
|
The progress bar is then redrawn on the next line.
|
||||||
|
"""
|
||||||
|
if not _TTY or IsTraceToStderr() or self._quiet:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Erase the current line, print the message with a newline,
|
||||||
|
# and then immediately redraw the progress bar on the new line.
|
||||||
|
_STDERR.write("\r" + CSI_ERASE_LINE)
|
||||||
|
_STDERR.write(msg + "\n")
|
||||||
|
_STDERR.flush()
|
||||||
|
self.update(inc=0)
|
||||||
|
|
||||||
def end(self):
|
def end(self):
|
||||||
|
if self._ended:
|
||||||
|
return
|
||||||
|
self._ended = True
|
||||||
|
|
||||||
self._update_event.set()
|
self._update_event.set()
|
||||||
if not _TTY or IsTraceToStderr() or not self._show:
|
if not _TTY or IsTraceToStderr() or self._quiet:
|
||||||
return
|
return
|
||||||
|
|
||||||
duration = duration_str(time.time() - self._start)
|
duration = duration_str(time.time() - self._start)
|
||||||
|
|||||||
+1009
-497
File diff suppressed because it is too large
Load Diff
+31
-3
@@ -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']
|
|
||||||
|
# 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 = ""
|
||||||
@@ -82,18 +85,18 @@ Repo launcher bucket:
|
|||||||
gs://git-repo-downloads/
|
gs://git-repo-downloads/
|
||||||
|
|
||||||
You should first upload it with a specific version:
|
You should first upload it with a specific version:
|
||||||
gsutil cp -a public-read {opts.launcher} gs://git-repo-downloads/repo-{version}
|
gcloud storage cp --predefined-acl=publicRead {opts.launcher} gs://git-repo-downloads/repo-{version}
|
||||||
gsutil cp -a public-read {opts.launcher}.asc gs://git-repo-downloads/repo-{version}.asc
|
gcloud storage cp --predefined-acl=publicRead {opts.launcher}.asc gs://git-repo-downloads/repo-{version}.asc
|
||||||
|
|
||||||
Then to make it the public default:
|
Then to make it the public default:
|
||||||
gsutil cp -a public-read gs://git-repo-downloads/repo-{version} gs://git-repo-downloads/repo
|
gcloud storage cp --predefined-acl=publicRead gs://git-repo-downloads/repo-{version} gs://git-repo-downloads/repo
|
||||||
gsutil cp -a public-read gs://git-repo-downloads/repo-{version}.asc gs://git-repo-downloads/repo.asc
|
gcloud storage cp --predefined-acl=publicRead gs://git-repo-downloads/repo-{version}.asc gs://git-repo-downloads/repo.asc
|
||||||
|
|
||||||
NB: If a rollback is necessary, the GS bucket archives old versions, and may be
|
NB: If a rollback is necessary, the GS bucket archives old versions, and may be
|
||||||
accessed by specifying their unique id number.
|
accessed by specifying their unique id number.
|
||||||
gsutil ls -la gs://git-repo-downloads/repo gs://git-repo-downloads/repo.asc
|
gcloud storage ls --long --all-versions gs://git-repo-downloads/repo gs://git-repo-downloads/repo.asc
|
||||||
gsutil cp -a public-read gs://git-repo-downloads/repo#<unique id> gs://git-repo-downloads/repo
|
gcloud storage cp --predefined-acl=publicRead gs://git-repo-downloads/repo#<unique id> gs://git-repo-downloads/repo
|
||||||
gsutil cp -a public-read gs://git-repo-downloads/repo.asc#<unique id> gs://git-repo-downloads/repo.asc
|
gcloud storage cp --predefined-acl=publicRead gs://git-repo-downloads/repo.asc#<unique id> gs://git-repo-downloads/repo.asc
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ import sys
|
|||||||
import util
|
import util
|
||||||
|
|
||||||
|
|
||||||
|
assert sys.version_info >= (3, 9), "Release framework requires Python 3.9+"
|
||||||
|
|
||||||
|
|
||||||
# We currently sign with the old DSA key as it's been around the longest.
|
# We currently sign with the old DSA key as it's been around the longest.
|
||||||
# We should transition to RSA by Jun 2020, and ECC by Jun 2021.
|
# We should transition to RSA by Jun 2020, and ECC by Jun 2021.
|
||||||
KEYID = util.KEYID_DSA
|
KEYID = util.KEYID_DSA
|
||||||
|
|||||||
Executable
+143
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (C) 2024 The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Helper tool for updating hooks from their various upstreams."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
from typing import List, Optional
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
|
assert sys.version_info >= (3, 9), "Release framework requires Python 3.9+"
|
||||||
|
|
||||||
|
|
||||||
|
TOPDIR = Path(__file__).resolve().parent.parent
|
||||||
|
HOOKS_DIR = TOPDIR / "hooks"
|
||||||
|
|
||||||
|
|
||||||
|
def update_hook_commit_msg() -> None:
|
||||||
|
"""Update commit-msg hook from Gerrit."""
|
||||||
|
hook = HOOKS_DIR / "commit-msg"
|
||||||
|
print(
|
||||||
|
f"{hook.name}: Updating from https://gerrit.googlesource.com/gerrit/"
|
||||||
|
"+/HEAD/resources/com/google/gerrit/server/tools/root/hooks/commit-msg"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the current commit.
|
||||||
|
url = "https://gerrit.googlesource.com/gerrit/+/HEAD?format=JSON"
|
||||||
|
with urllib.request.urlopen(url) as fp:
|
||||||
|
data = fp.read()
|
||||||
|
# Discard the xss protection.
|
||||||
|
data = data.split(b"\n", 1)[1]
|
||||||
|
data = json.loads(data)
|
||||||
|
commit = data["commit"]
|
||||||
|
|
||||||
|
# Fetch the data for that commit.
|
||||||
|
url = (
|
||||||
|
f"https://gerrit.googlesource.com/gerrit/+/{commit}/"
|
||||||
|
"resources/com/google/gerrit/server/tools/root/hooks/commit-msg"
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(f"{url}?format=TEXT") as fp:
|
||||||
|
data = fp.read()
|
||||||
|
|
||||||
|
# gitiles base64 encodes text data.
|
||||||
|
data = base64.b64decode(data)
|
||||||
|
|
||||||
|
# Inject header into the hook.
|
||||||
|
lines = data.split(b"\n")
|
||||||
|
lines = (
|
||||||
|
lines[:1]
|
||||||
|
+ [
|
||||||
|
b"# DO NOT EDIT THIS FILE",
|
||||||
|
(
|
||||||
|
b"# All updates should be sent upstream: "
|
||||||
|
b"https://gerrit.googlesource.com/gerrit/"
|
||||||
|
),
|
||||||
|
f"# This is synced from commit: {commit}".encode("utf-8"),
|
||||||
|
b"# DO NOT EDIT THIS FILE",
|
||||||
|
]
|
||||||
|
+ lines[1:]
|
||||||
|
)
|
||||||
|
data = b"\n".join(lines)
|
||||||
|
|
||||||
|
# Update the hook.
|
||||||
|
hook.write_bytes(data)
|
||||||
|
hook.chmod(0o755)
|
||||||
|
|
||||||
|
|
||||||
|
def update_hook_pre_auto_gc() -> None:
|
||||||
|
"""Update pre-auto-gc hook from git."""
|
||||||
|
hook = HOOKS_DIR / "pre-auto-gc"
|
||||||
|
print(
|
||||||
|
f"{hook.name}: Updating from https://github.com/git/git/"
|
||||||
|
"HEAD/contrib/hooks/pre-auto-gc-battery"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the current commit.
|
||||||
|
headers = {
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
}
|
||||||
|
url = "https://api.github.com/repos/git/git/git/refs/heads/master"
|
||||||
|
req = urllib.request.Request(url, headers=headers)
|
||||||
|
with urllib.request.urlopen(req) as fp:
|
||||||
|
data = fp.read()
|
||||||
|
data = json.loads(data)
|
||||||
|
|
||||||
|
# Fetch the data for that commit.
|
||||||
|
commit = data["object"]["sha"]
|
||||||
|
url = (
|
||||||
|
f"https://raw.githubusercontent.com/git/git/{commit}/"
|
||||||
|
"contrib/hooks/pre-auto-gc-battery"
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(url) as fp:
|
||||||
|
data = fp.read()
|
||||||
|
|
||||||
|
# Inject header into the hook.
|
||||||
|
lines = data.split(b"\n")
|
||||||
|
lines = (
|
||||||
|
lines[:1]
|
||||||
|
+ [
|
||||||
|
b"# DO NOT EDIT THIS FILE",
|
||||||
|
(
|
||||||
|
b"# All updates should be sent upstream: "
|
||||||
|
b"https://github.com/git/git/"
|
||||||
|
),
|
||||||
|
f"# This is synced from commit: {commit}".encode("utf-8"),
|
||||||
|
b"# DO NOT EDIT THIS FILE",
|
||||||
|
]
|
||||||
|
+ lines[1:]
|
||||||
|
)
|
||||||
|
data = b"\n".join(lines)
|
||||||
|
|
||||||
|
# Update the hook.
|
||||||
|
hook.write_bytes(data)
|
||||||
|
hook.chmod(0o755)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Optional[List[str]] = None) -> Optional[int]:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.parse_args(argv)
|
||||||
|
|
||||||
|
update_hook_commit_msg()
|
||||||
|
update_hook_pre_auto_gc()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv[1:]))
|
||||||
@@ -27,9 +27,14 @@ import shutil
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
TOPDIR = Path(__file__).resolve().parent.parent
|
assert sys.version_info >= (3, 9), "Release framework requires Python 3.9+"
|
||||||
|
|
||||||
|
|
||||||
|
THIS_FILE = Path(__file__).resolve()
|
||||||
|
TOPDIR = THIS_FILE.parent.parent
|
||||||
MANDIR = TOPDIR.joinpath("man")
|
MANDIR = TOPDIR.joinpath("man")
|
||||||
|
|
||||||
# Load repo local modules.
|
# Load repo local modules.
|
||||||
@@ -42,11 +47,29 @@ 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
|
||||||
|
|
||||||
if not shutil.which("help2man"):
|
|
||||||
|
def main(argv: List[str]) -> int:
|
||||||
|
parser = get_parser()
|
||||||
|
opts = parser.parse_args(argv)
|
||||||
|
|
||||||
|
help2man = ["help2man"]
|
||||||
|
cipd_help2man = TOPDIR / ".cipd_bin/bin/help2man"
|
||||||
|
if cipd_help2man.exists():
|
||||||
|
help2man = [cipd_help2man]
|
||||||
|
elif not shutil.which("help2man"):
|
||||||
sys.exit("Please install help2man to continue.")
|
sys.exit("Please install help2man to continue.")
|
||||||
|
|
||||||
# Let repo know we're generating man pages so it can avoid some dynamic
|
# Let repo know we're generating man pages so it can avoid some dynamic
|
||||||
@@ -61,7 +84,7 @@ def main(argv):
|
|||||||
version = RepoSourceVersion()
|
version = RepoSourceVersion()
|
||||||
cmdlist = [
|
cmdlist = [
|
||||||
[
|
[
|
||||||
"help2man",
|
*help2man,
|
||||||
"-N",
|
"-N",
|
||||||
"-n",
|
"-n",
|
||||||
f"repo {cmd} - manual page for repo {cmd}",
|
f"repo {cmd} - manual page for repo {cmd}",
|
||||||
@@ -80,7 +103,7 @@ def main(argv):
|
|||||||
]
|
]
|
||||||
cmdlist.append(
|
cmdlist.append(
|
||||||
[
|
[
|
||||||
"help2man",
|
*help2man,
|
||||||
"-N",
|
"-N",
|
||||||
"-n",
|
"-n",
|
||||||
"repository management tool built on top of git",
|
"repository management tool built on top of git",
|
||||||
@@ -117,6 +140,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 +157,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):
|
||||||
@@ -147,7 +181,6 @@ def replace_regex(data):
|
|||||||
"""
|
"""
|
||||||
regex = (
|
regex = (
|
||||||
(r"(It was generated by help2man) [0-9.]+", r"\g<1>."),
|
(r"(It was generated by help2man) [0-9.]+", r"\g<1>."),
|
||||||
(r"^\033\[[0-9;]*m([^\033]*)\033\[m", r"\g<1>"),
|
|
||||||
(r"^\.IP\n(.*:)\n", r".SS \g<1>\n"),
|
(r"^\.IP\n(.*:)\n", r".SS \g<1>\n"),
|
||||||
(r"^\.PP\nDescription", r".SH DETAILS"),
|
(r"^\.PP\nDescription", r".SH DETAILS"),
|
||||||
)
|
)
|
||||||
|
|||||||
+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()
|
||||||
|
|||||||
+53
-35
@@ -15,24 +15,16 @@
|
|||||||
"""Logic for printing user-friendly logs in repo."""
|
"""Logic for printing user-friendly logs in repo."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
|
||||||
|
|
||||||
from color import Coloring
|
from color import Coloring
|
||||||
|
from error import RepoExitError
|
||||||
|
|
||||||
|
|
||||||
SEPARATOR = "=" * 80
|
SEPARATOR = "=" * 80
|
||||||
|
MAX_PRINT_ERRORS = 5
|
||||||
|
|
||||||
|
|
||||||
class LogColoring(Coloring):
|
class _ConfigMock:
|
||||||
"""Coloring outstream for logging."""
|
|
||||||
|
|
||||||
def __init__(self, config):
|
|
||||||
super().__init__(config, "logs")
|
|
||||||
self.error = self.colorer("error", fg="red")
|
|
||||||
self.warning = self.colorer("warn", fg="yellow")
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigMock:
|
|
||||||
"""Default coloring config to use when Logging.config is not set."""
|
"""Default coloring config to use when Logging.config is not set."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -42,34 +34,60 @@ class ConfigMock:
|
|||||||
return self.default_values.get(x, None)
|
return self.default_values.get(x, None)
|
||||||
|
|
||||||
|
|
||||||
|
class _LogColoring(Coloring):
|
||||||
|
"""Coloring outstream for logging."""
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
super().__init__(config, "logs")
|
||||||
|
self.error = self.nofmt_colorer("error", fg="red")
|
||||||
|
self.warning = self.nofmt_colorer("warn", fg="yellow")
|
||||||
|
self.levelMap = {
|
||||||
|
"WARNING": self.warning,
|
||||||
|
"ERROR": self.error,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _LogColoringFormatter(logging.Formatter):
|
||||||
|
"""Coloring formatter for logging."""
|
||||||
|
|
||||||
|
def __init__(self, config=None, *args, **kwargs):
|
||||||
|
self.config = config if config else _ConfigMock()
|
||||||
|
self.colorer = _LogColoring(self.config)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def format(self, record):
|
||||||
|
"""Formats |record| with color."""
|
||||||
|
msg = super().format(record)
|
||||||
|
colorer = self.colorer.levelMap.get(record.levelname)
|
||||||
|
return msg if not colorer else colorer(msg)
|
||||||
|
|
||||||
|
|
||||||
class RepoLogger(logging.Logger):
|
class RepoLogger(logging.Logger):
|
||||||
"""Repo Logging Module."""
|
"""Repo Logging Module."""
|
||||||
|
|
||||||
# Aggregates error-level logs. This is used to generate an error summary
|
def __init__(self, name: str, config=None, **kwargs):
|
||||||
# section at the end of a command execution.
|
|
||||||
errors = multiprocessing.Manager().list()
|
|
||||||
|
|
||||||
def __init__(self, name, config=None, **kwargs):
|
|
||||||
super().__init__(name, **kwargs)
|
super().__init__(name, **kwargs)
|
||||||
self.config = config if config else ConfigMock()
|
handler = logging.StreamHandler()
|
||||||
self.colorer = LogColoring(self.config)
|
handler.setFormatter(_LogColoringFormatter(config))
|
||||||
|
self.addHandler(handler)
|
||||||
|
|
||||||
def error(self, msg, *args, **kwargs):
|
def log_aggregated_errors(self, err: RepoExitError):
|
||||||
"""Print and aggregate error-level logs."""
|
|
||||||
colored_error = self.colorer.error(msg, *args)
|
|
||||||
RepoLogger.errors.append(colored_error)
|
|
||||||
|
|
||||||
super().error(colored_error, **kwargs)
|
|
||||||
|
|
||||||
def warning(self, msg, *args, **kwargs):
|
|
||||||
"""Print warning-level logs with coloring."""
|
|
||||||
colored_warning = self.colorer.warning(msg, *args)
|
|
||||||
super().warning(colored_warning, **kwargs)
|
|
||||||
|
|
||||||
def log_aggregated_errors(self):
|
|
||||||
"""Print all aggregated logs."""
|
"""Print all aggregated logs."""
|
||||||
super().error(self.colorer.error(SEPARATOR))
|
self.error(SEPARATOR)
|
||||||
super().error(
|
|
||||||
self.colorer.error("Repo command failed due to following errors:")
|
if not err.aggregate_errors:
|
||||||
|
self.error("Repo command failed: %s", type(err).__name__)
|
||||||
|
self.error("\t%s", str(err))
|
||||||
|
return
|
||||||
|
|
||||||
|
self.error(
|
||||||
|
"Repo command failed due to the following `%s` errors:",
|
||||||
|
type(err).__name__,
|
||||||
)
|
)
|
||||||
super().error("\n".join(RepoLogger.errors))
|
self.error(
|
||||||
|
"\n".join(str(e) for e in err.aggregate_errors[:MAX_PRINT_ERRORS])
|
||||||
|
)
|
||||||
|
|
||||||
|
diff = len(err.aggregate_errors) - MAX_PRINT_ERRORS
|
||||||
|
if diff > 0:
|
||||||
|
self.error("+%d additional errors...", diff)
|
||||||
|
|||||||
+1
-1
@@ -142,7 +142,7 @@ def _GetTraceFile(quiet):
|
|||||||
def _ClearOldTraces():
|
def _ClearOldTraces():
|
||||||
"""Clear the oldest commands if trace file is too big."""
|
"""Clear the oldest commands if trace file is too big."""
|
||||||
try:
|
try:
|
||||||
with open(_TRACE_FILE, "r", errors="ignore") as f:
|
with open(_TRACE_FILE, errors="ignore") as f:
|
||||||
if os.path.getsize(f.name) / (1024 * 1024) <= _MAX_SIZE:
|
if os.path.getsize(f.name) / (1024 * 1024) <= _MAX_SIZE:
|
||||||
return
|
return
|
||||||
trace_lines = f.readlines()
|
trace_lines = f.readlines()
|
||||||
|
|||||||
+7
-5
@@ -46,12 +46,14 @@
|
|||||||
|
|
||||||
# Supported git versions.
|
# Supported git versions.
|
||||||
#
|
#
|
||||||
# git-1.7.2 is in Debian Squeeze.
|
|
||||||
# git-1.7.9 is in Ubuntu Precise.
|
|
||||||
# git-1.9.1 is in Ubuntu Trusty.
|
# git-1.9.1 is in Ubuntu Trusty.
|
||||||
# git-1.7.10 is in Debian Wheezy.
|
# git-2.1.4 is in Debian Jessie.
|
||||||
|
# git-2.7.4 is in Ubuntu Xenial.
|
||||||
|
# git-2.11.0 is in Debian Stretch.
|
||||||
|
# git-2.17.0 is in Ubuntu Bionic.
|
||||||
|
# git-2.20.1 is in Debian Buster.
|
||||||
"git": {
|
"git": {
|
||||||
"hard": [1, 7, 2],
|
"hard": [1, 9, 1],
|
||||||
"soft": [1, 9, 1]
|
"soft": [2, 7, 4]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# Copyright 2019 The Android Open Source Project
|
# Copyright (C) 2019 The Android Open Source Project
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -15,46 +15,187 @@
|
|||||||
|
|
||||||
"""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
|
||||||
|
# scripts manually.
|
||||||
|
extra_programs = [
|
||||||
|
"repo",
|
||||||
|
"run_tests",
|
||||||
|
"release/update-hooks",
|
||||||
|
"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], check=False
|
[sys.executable, "-m", "black"] + argv,
|
||||||
|
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
|
).returncode
|
||||||
|
|
||||||
|
|
||||||
|
def run_check_metadata():
|
||||||
|
"""Returns the exit code from check-metadata."""
|
||||||
|
argv = []
|
||||||
|
log_cmd("release/check-metadata.py", argv)
|
||||||
|
return subprocess.run(
|
||||||
|
[sys.executable, "release/check-metadata.py"] + argv,
|
||||||
|
check=False,
|
||||||
|
cwd=ROOT_DIR,
|
||||||
|
).returncode
|
||||||
|
|
||||||
|
|
||||||
|
def run_update_manpages() -> int:
|
||||||
|
"""Returns the exit code from release/update-manpages."""
|
||||||
|
argv = ["--check"]
|
||||||
|
log_cmd("release/update-manpages", argv)
|
||||||
|
return subprocess.run(
|
||||||
|
[sys.executable, "release/update-manpages"] + argv,
|
||||||
|
check=False,
|
||||||
|
cwd=ROOT_DIR,
|
||||||
|
).returncode
|
||||||
|
|
||||||
|
|
||||||
|
def cipd_bootstrap() -> None:
|
||||||
|
"""Install packages via cipd if available."""
|
||||||
|
argv = ["ensure", "-root", ".cipd_bin", "-ensure-file", "cipd_manifest.txt"]
|
||||||
|
log_cmd("cipd", argv)
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["cipd"] + argv,
|
||||||
|
check=True,
|
||||||
|
cwd=ROOT_DIR,
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
# Skip if the user doesn't have cipd from depot_tools.
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def main(argv):
|
def main(argv):
|
||||||
"""The main entry."""
|
"""The main entry."""
|
||||||
|
cipd_bootstrap()
|
||||||
checks = (
|
checks = (
|
||||||
lambda: pytest.main(argv),
|
functools.partial(run_pytest, argv),
|
||||||
|
functools.partial(run_pytest_py38, argv),
|
||||||
run_black,
|
run_black,
|
||||||
run_flake8,
|
run_flake8,
|
||||||
run_isort,
|
run_isort,
|
||||||
|
run_check_metadata,
|
||||||
|
run_update_manpages,
|
||||||
)
|
)
|
||||||
return 0 if all(not c() for c in checks) else 1
|
# Run all the tests all the time to get full feedback. Don't exit on the
|
||||||
|
# first error as that makes it more difficult to iterate in the CQ.
|
||||||
|
return 1 if sum(c() for c in checks) else 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+31
-26
@@ -5,97 +5,92 @@
|
|||||||
# List of available wheels:
|
# List of available wheels:
|
||||||
# https://chromium.googlesource.com/infra/infra/+/main/infra/tools/dockerbuild/wheels.md
|
# https://chromium.googlesource.com/infra/infra/+/main/infra/tools/dockerbuild/wheels.md
|
||||||
|
|
||||||
python_version: "3.8"
|
python_version: "3.11"
|
||||||
|
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/pytest-py3"
|
name: "infra/python/wheels/pytest-py3"
|
||||||
version: "version:6.2.2"
|
version: "version:8.3.4"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by pytest==6.2.2
|
# Required by pytest==8.3.4
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/py-py2_py3"
|
name: "infra/python/wheels/py-py2_py3"
|
||||||
version: "version:1.10.0"
|
version: "version:1.11.0"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by pytest==6.2.2
|
# Required by pytest==8.3.4
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/iniconfig-py3"
|
name: "infra/python/wheels/iniconfig-py3"
|
||||||
version: "version:1.1.1"
|
version: "version:1.1.1"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by pytest==6.2.2
|
# Required by pytest==8.3.4
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/packaging-py3"
|
name: "infra/python/wheels/packaging-py3"
|
||||||
version: "version:23.0"
|
version: "version:23.0"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by pytest==6.2.2
|
# Required by pytest==8.3.4
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/pluggy-py3"
|
name: "infra/python/wheels/pluggy-py3"
|
||||||
version: "version:0.13.1"
|
version: "version:1.5.0"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by pytest==6.2.2
|
# Required by pytest==8.3.4
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/toml-py3"
|
name: "infra/python/wheels/toml-py3"
|
||||||
version: "version:0.10.1"
|
version: "version:0.10.1"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by pytest==6.2.2
|
# Required by pytest==8.3.4
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/pyparsing-py3"
|
name: "infra/python/wheels/pyparsing-py3"
|
||||||
version: "version:3.0.7"
|
version: "version:3.0.7"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by pytest==6.2.2
|
# Required by pytest==8.3.4 and flake8-bugbear==24.12.12
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/attrs-py2_py3"
|
name: "infra/python/wheels/attrs-py3"
|
||||||
version: "version:21.4.0"
|
version: "version:24.2.0"
|
||||||
>
|
|
||||||
|
|
||||||
# Required by packaging==16.8
|
|
||||||
wheel: <
|
|
||||||
name: "infra/python/wheels/six-py2_py3"
|
|
||||||
version: "version:1.16.0"
|
|
||||||
>
|
>
|
||||||
|
|
||||||
|
# NB: Keep in sync with constraints.txt.
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/black-py3"
|
name: "infra/python/wheels/black-py3"
|
||||||
version: "version:23.1.0"
|
version: "version:25.1.0"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by black==23.1.0
|
# Required by black==25.1.0
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/mypy-extensions-py3"
|
name: "infra/python/wheels/mypy-extensions-py3"
|
||||||
version: "version:0.4.3"
|
version: "version:0.4.3"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by black==23.1.0
|
# Required by black==25.1.0
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/tomli-py3"
|
name: "infra/python/wheels/tomli-py3"
|
||||||
version: "version:2.0.1"
|
version: "version:2.0.1"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by black==23.1.0
|
# Required by black==25.1.0
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/platformdirs-py3"
|
name: "infra/python/wheels/platformdirs-py3"
|
||||||
version: "version:2.5.2"
|
version: "version:2.5.2"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by black==23.1.0
|
# Required by black==25.1.0
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/pathspec-py3"
|
name: "infra/python/wheels/pathspec-py3"
|
||||||
version: "version:0.9.0"
|
version: "version:0.9.0"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by black==23.1.0
|
# Required by black==25.1.0
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/typing-extensions-py3"
|
name: "infra/python/wheels/typing-extensions-py3"
|
||||||
version: "version:4.3.0"
|
version: "version:4.3.0"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by black==23.1.0
|
# Required by black==25.1.0
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/click-py3"
|
name: "infra/python/wheels/click-py3"
|
||||||
version: "version:8.0.3"
|
version: "version:8.0.3"
|
||||||
@@ -124,6 +119,16 @@ wheel: <
|
|||||||
version: "version:2.10.0"
|
version: "version:2.10.0"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
wheel: <
|
||||||
|
name: "infra/python/wheels/flake8-bugbear-py3"
|
||||||
|
version: "version:24.12.12"
|
||||||
|
>
|
||||||
|
|
||||||
|
wheel: <
|
||||||
|
name: "infra/python/wheels/flake8-comprehensions-py3"
|
||||||
|
version: "version:3.16.0"
|
||||||
|
>
|
||||||
|
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/isort-py3"
|
name: "infra/python/wheels/isort-py3"
|
||||||
version: "version:5.10.1"
|
version: "version:5.10.1"
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# This is a vpython "spec" file.
|
||||||
|
#
|
||||||
|
# Read more about `vpython` and how to modify this file here:
|
||||||
|
# https://chromium.googlesource.com/infra/infra/+/main/doc/users/vpython.md
|
||||||
|
# List of available wheels:
|
||||||
|
# https://chromium.googlesource.com/infra/infra/+/main/infra/tools/dockerbuild/wheels.md
|
||||||
|
|
||||||
|
python_version: "3.8"
|
||||||
|
|
||||||
|
wheel: <
|
||||||
|
name: "infra/python/wheels/pytest-py3"
|
||||||
|
version: "version:8.3.4"
|
||||||
|
>
|
||||||
|
|
||||||
|
# Required by pytest==8.3.4
|
||||||
|
wheel: <
|
||||||
|
name: "infra/python/wheels/py-py2_py3"
|
||||||
|
version: "version:1.11.0"
|
||||||
|
>
|
||||||
|
|
||||||
|
# Required by pytest==8.3.4
|
||||||
|
wheel: <
|
||||||
|
name: "infra/python/wheels/iniconfig-py3"
|
||||||
|
version: "version:1.1.1"
|
||||||
|
>
|
||||||
|
|
||||||
|
# Required by pytest==8.3.4
|
||||||
|
wheel: <
|
||||||
|
name: "infra/python/wheels/packaging-py3"
|
||||||
|
version: "version:23.0"
|
||||||
|
>
|
||||||
|
|
||||||
|
# Required by pytest==8.3.4
|
||||||
|
wheel: <
|
||||||
|
name: "infra/python/wheels/pluggy-py3"
|
||||||
|
version: "version:1.5.0"
|
||||||
|
>
|
||||||
|
|
||||||
|
# Required by pytest==8.3.4
|
||||||
|
wheel: <
|
||||||
|
name: "infra/python/wheels/toml-py3"
|
||||||
|
version: "version:0.10.1"
|
||||||
|
>
|
||||||
|
|
||||||
|
# Required by pytest==8.3.4
|
||||||
|
wheel: <
|
||||||
|
name: "infra/python/wheels/tomli-py3"
|
||||||
|
version: "version:2.1.0"
|
||||||
|
>
|
||||||
|
|
||||||
|
# Required by pytest==8.3.4
|
||||||
|
wheel: <
|
||||||
|
name: "infra/python/wheels/pyparsing-py3"
|
||||||
|
version: "version:3.0.7"
|
||||||
|
>
|
||||||
|
|
||||||
|
# Required by pytest==8.3.4
|
||||||
|
wheel: <
|
||||||
|
name: "infra/python/wheels/attrs-py2_py3"
|
||||||
|
version: "version:21.4.0"
|
||||||
|
>
|
||||||
|
|
||||||
|
# Required by pytest==8.3.4
|
||||||
|
wheel: <
|
||||||
|
name: "infra/python/wheels/exceptiongroup-py3"
|
||||||
|
version: "version:1.1.2"
|
||||||
|
>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# Copyright 2019 The Android Open Source Project
|
# Copyright (C) 2019 The Android Open Source Project
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the 'License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
# You may obtain a copy of the License at
|
# You may obtain a copy of the License at
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from git_command import git
|
||||||
import platform_utils
|
import platform_utils
|
||||||
from repo_trace import Trace
|
from repo_trace import Trace
|
||||||
|
|
||||||
@@ -51,14 +52,18 @@ def _parse_ssh_version(ver_str=None):
|
|||||||
|
|
||||||
@functools.lru_cache(maxsize=None)
|
@functools.lru_cache(maxsize=None)
|
||||||
def version():
|
def version():
|
||||||
"""return ssh version as a tuple"""
|
"""Return ssh version as a tuple.
|
||||||
|
|
||||||
|
If ssh is not available, a FileNotFoundError will be raised.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return _parse_ssh_version()
|
return _parse_ssh_version()
|
||||||
except FileNotFoundError:
|
except subprocess.CalledProcessError as e:
|
||||||
print("fatal: ssh not installed", file=sys.stderr)
|
print(
|
||||||
sys.exit(1)
|
"fatal: unable to detect ssh version"
|
||||||
except subprocess.CalledProcessError:
|
f" (code={e.returncode}, output={e.stdout})",
|
||||||
print("fatal: unable to detect ssh version", file=sys.stderr)
|
file=sys.stderr,
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -97,9 +102,18 @@ class ProxyManager:
|
|||||||
self._clients = manager.list()
|
self._clients = manager.list()
|
||||||
# Path to directory for holding master sockets.
|
# Path to directory for holding master sockets.
|
||||||
self._sock_path = None
|
self._sock_path = None
|
||||||
|
# See if ssh is usable.
|
||||||
|
self._ssh_installed = False
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
"""Enter a new context."""
|
"""Enter a new context."""
|
||||||
|
# Check which version of ssh is available.
|
||||||
|
try:
|
||||||
|
version()
|
||||||
|
self._ssh_installed = True
|
||||||
|
except FileNotFoundError:
|
||||||
|
self._ssh_installed = False
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback):
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
@@ -135,7 +149,7 @@ class ProxyManager:
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
procs.pop(0)
|
procs.pop(0)
|
||||||
except: # noqa: E722
|
except IndexError:
|
||||||
break
|
break
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
@@ -165,7 +179,7 @@ class ProxyManager:
|
|||||||
# Check to see whether we already think that the master is running; if
|
# Check to see whether we already think that the master is running; if
|
||||||
# we think it's already running, return right away.
|
# we think it's already running, return right away.
|
||||||
if port is not None:
|
if port is not None:
|
||||||
key = "%s:%s" % (host, port)
|
key = f"{host}:{port}"
|
||||||
else:
|
else:
|
||||||
key = host
|
key = host
|
||||||
|
|
||||||
@@ -207,7 +221,33 @@ class ProxyManager:
|
|||||||
# and print to the log there.
|
# and print to the log there.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
command = command_base[:1] + ["-M", "-N"] + command_base[1:]
|
# Git protocol V2 is a new feature in git 2.18.0, made default in
|
||||||
|
# git 2.26.0
|
||||||
|
# It is faster and more efficient than V1.
|
||||||
|
# To enable it when using SSH, the environment variable GIT_PROTOCOL
|
||||||
|
# must be set in the SSH side channel when establishing the connection
|
||||||
|
# to the git server.
|
||||||
|
# See https://git-scm.com/docs/protocol-v2#_ssh_and_file_transport
|
||||||
|
# Normally git does this by itself. But here, where the SSH connection
|
||||||
|
# is established manually over ControlMaster via the repo-tool, it must
|
||||||
|
# be passed in explicitly instead.
|
||||||
|
# Based on https://git-scm.com/docs/gitprotocol-pack#_extra_parameters,
|
||||||
|
# GIT_PROTOCOL is considered an "Extra Parameter" and must be ignored
|
||||||
|
# by servers that do not understand it. This means that it is safe to
|
||||||
|
# set it even when connecting to older servers.
|
||||||
|
# It should also be safe to set the environment variable for older
|
||||||
|
# local git versions, since it is only part of the ssh side channel.
|
||||||
|
git_protocol_version = _get_git_protocol_version()
|
||||||
|
ssh_git_protocol_args = [
|
||||||
|
"-o",
|
||||||
|
f"SetEnv GIT_PROTOCOL=version={git_protocol_version}",
|
||||||
|
]
|
||||||
|
|
||||||
|
command = (
|
||||||
|
command_base[:1]
|
||||||
|
+ ["-M", "-N", *ssh_git_protocol_args]
|
||||||
|
+ command_base[1:]
|
||||||
|
)
|
||||||
p = None
|
p = None
|
||||||
try:
|
try:
|
||||||
with Trace("Call to ssh: %s", " ".join(command)):
|
with Trace("Call to ssh: %s", " ".join(command)):
|
||||||
@@ -251,6 +291,9 @@ class ProxyManager:
|
|||||||
|
|
||||||
def preconnect(self, url):
|
def preconnect(self, url):
|
||||||
"""If |uri| will create a ssh connection, setup the ssh master for it.""" # noqa: E501
|
"""If |uri| will create a ssh connection, setup the ssh master for it.""" # noqa: E501
|
||||||
|
if not self._ssh_installed:
|
||||||
|
return False
|
||||||
|
|
||||||
m = URI_ALL.match(url)
|
m = URI_ALL.match(url)
|
||||||
if m:
|
if m:
|
||||||
scheme = m.group(1)
|
scheme = m.group(1)
|
||||||
@@ -275,6 +318,9 @@ class ProxyManager:
|
|||||||
|
|
||||||
This has all the master sockets so clients can talk to them.
|
This has all the master sockets so clients can talk to them.
|
||||||
"""
|
"""
|
||||||
|
if not self._ssh_installed:
|
||||||
|
return None
|
||||||
|
|
||||||
if self._sock_path is None:
|
if self._sock_path is None:
|
||||||
if not create:
|
if not create:
|
||||||
return None
|
return None
|
||||||
@@ -289,3 +335,32 @@ class ProxyManager:
|
|||||||
tempfile.mkdtemp("", "ssh-", tmp_dir), "master-" + tokens
|
tempfile.mkdtemp("", "ssh-", tmp_dir), "master-" + tokens
|
||||||
)
|
)
|
||||||
return self._sock_path
|
return self._sock_path
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=1)
|
||||||
|
def _get_git_protocol_version() -> str:
|
||||||
|
"""Return the git protocol version.
|
||||||
|
|
||||||
|
The version is found by first reading the global git config.
|
||||||
|
If no git config for protocol version exists, try to deduce the default
|
||||||
|
protocol version based on the git version.
|
||||||
|
|
||||||
|
See https://git-scm.com/docs/gitprotocol-v2 for details.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return subprocess.check_output(
|
||||||
|
["git", "config", "--get", "--global", "protocol.version"],
|
||||||
|
encoding="utf-8",
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
).strip()
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
if e.returncode == 1:
|
||||||
|
# Exit code 1 means that the git config key was not found.
|
||||||
|
# Try to imitate the defaults that git would have used.
|
||||||
|
git_version = git.version_tuple()
|
||||||
|
if git_version >= (2, 26, 0):
|
||||||
|
# Since git version 2.26, protocol v2 is the default.
|
||||||
|
return "2"
|
||||||
|
return "1"
|
||||||
|
# Other exit codes indicate error with reading the config.
|
||||||
|
raise
|
||||||
|
|||||||
+1
-3
@@ -37,9 +37,7 @@ for py in os.listdir(my_dir):
|
|||||||
try:
|
try:
|
||||||
cmd = getattr(mod, clsn)
|
cmd = getattr(mod, clsn)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise SyntaxError(
|
raise SyntaxError(f"{__name__}/{py} does not define class {clsn}")
|
||||||
"%s/%s does not define class %s" % (__name__, py, clsn)
|
|
||||||
)
|
|
||||||
|
|
||||||
name = name.replace("_", "-")
|
name = name.replace("_", "-")
|
||||||
cmd.NAME = name
|
cmd.NAME = name
|
||||||
|
|||||||
+27
-24
@@ -15,7 +15,6 @@
|
|||||||
import collections
|
import collections
|
||||||
import functools
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
import sys
|
|
||||||
|
|
||||||
from command import Command
|
from command import Command
|
||||||
from command import DEFAULT_LOCAL_JOBS
|
from command import DEFAULT_LOCAL_JOBS
|
||||||
@@ -23,6 +22,10 @@ from error import RepoError
|
|||||||
from error import RepoExitError
|
from error import RepoExitError
|
||||||
from git_command import git
|
from git_command import git
|
||||||
from progress import Progress
|
from progress import Progress
|
||||||
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
|
|
||||||
|
logger = RepoLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class AbandonError(RepoExitError):
|
class AbandonError(RepoExitError):
|
||||||
@@ -45,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",
|
||||||
)
|
)
|
||||||
@@ -67,8 +69,10 @@ It is equivalent to "git branch -D <branchname>".
|
|||||||
else:
|
else:
|
||||||
args.insert(0, "'All local branches'")
|
args.insert(0, "'All local branches'")
|
||||||
|
|
||||||
def _ExecuteOne(self, all_branches, nb, project):
|
@classmethod
|
||||||
|
def _ExecuteOne(cls, all_branches, nb, project_idx):
|
||||||
"""Abandon one project."""
|
"""Abandon one project."""
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
if all_branches:
|
if all_branches:
|
||||||
branches = project.GetBranches()
|
branches = project.GetBranches()
|
||||||
else:
|
else:
|
||||||
@@ -86,10 +90,11 @@ It is equivalent to "git branch -D <branchname>".
|
|||||||
if status is not None:
|
if status is not None:
|
||||||
ret[name] = status
|
ret[name] = status
|
||||||
|
|
||||||
return (ret, project, errors)
|
return (ret, project_idx, errors)
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
nb = args[0].split()
|
nb = args[0].split()
|
||||||
|
self.TryOverrideManifestWithSmartSync()
|
||||||
err = collections.defaultdict(list)
|
err = collections.defaultdict(list)
|
||||||
success = collections.defaultdict(list)
|
success = collections.defaultdict(list)
|
||||||
aggregate_errors = []
|
aggregate_errors = []
|
||||||
@@ -99,7 +104,8 @@ It is equivalent to "git branch -D <branchname>".
|
|||||||
_RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
|
_RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
|
||||||
|
|
||||||
def _ProcessResults(_pool, pm, states):
|
def _ProcessResults(_pool, pm, states):
|
||||||
for results, project, errors in states:
|
for results, project_idx, errors in states:
|
||||||
|
project = all_projects[project_idx]
|
||||||
for branch, status in results.items():
|
for branch, status in results.items():
|
||||||
if status:
|
if status:
|
||||||
success[branch].append(project)
|
success[branch].append(project)
|
||||||
@@ -108,15 +114,18 @@ It is equivalent to "git branch -D <branchname>".
|
|||||||
aggregate_errors.extend(errors)
|
aggregate_errors.extend(errors)
|
||||||
pm.update(msg="")
|
pm.update(msg="")
|
||||||
|
|
||||||
self.ExecuteInParallel(
|
with self.ParallelContext():
|
||||||
opt.jobs,
|
self.get_parallel_context()["projects"] = all_projects
|
||||||
functools.partial(self._ExecuteOne, opt.all, nb),
|
self.ExecuteInParallel(
|
||||||
all_projects,
|
opt.jobs,
|
||||||
callback=_ProcessResults,
|
functools.partial(self._ExecuteOne, opt.all, nb),
|
||||||
output=Progress(
|
range(len(all_projects)),
|
||||||
"Abandon %s" % (nb,), len(all_projects), quiet=opt.quiet
|
callback=_ProcessResults,
|
||||||
),
|
output=Progress(
|
||||||
)
|
f"Abandon {nb}", len(all_projects), quiet=opt.quiet
|
||||||
|
),
|
||||||
|
chunksize=1,
|
||||||
|
)
|
||||||
|
|
||||||
width = max(
|
width = max(
|
||||||
itertools.chain(
|
itertools.chain(
|
||||||
@@ -126,18 +135,12 @@ It is equivalent to "git branch -D <branchname>".
|
|||||||
if err:
|
if err:
|
||||||
for br in err.keys():
|
for br in err.keys():
|
||||||
err_msg = "error: cannot abandon %s" % br
|
err_msg = "error: cannot abandon %s" % br
|
||||||
print(err_msg, file=sys.stderr)
|
logger.error(err_msg)
|
||||||
for proj in err[br]:
|
for proj in err[br]:
|
||||||
print(
|
logger.error(" " * len(err_msg) + " | %s", _RelPath(proj))
|
||||||
" " * len(err_msg) + " | %s" % _RelPath(proj),
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
raise AbandonError(aggregate_errors=aggregate_errors)
|
raise AbandonError(aggregate_errors=aggregate_errors)
|
||||||
elif not success:
|
elif not success:
|
||||||
print(
|
logger.error("error: no project has local branch(es) : %s", nb)
|
||||||
"error: no project has local branch(es) : %s" % nb,
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
raise AbandonError(aggregate_errors=aggregate_errors)
|
raise AbandonError(aggregate_errors=aggregate_errors)
|
||||||
else:
|
else:
|
||||||
# Everything below here is displaying status.
|
# Everything below here is displaying status.
|
||||||
@@ -155,4 +158,4 @@ It is equivalent to "git branch -D <branchname>".
|
|||||||
_RelPath(p) for p in success[br]
|
_RelPath(p) for p in success[br]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
print("%s%s| %s\n" % (br, " " * (width - len(br)), result))
|
print(f"{br}{' ' * (width - len(br))}| {result}\n")
|
||||||
|
|||||||
+32
-26
@@ -28,7 +28,7 @@ class BranchColoring(Coloring):
|
|||||||
self.notinproject = self.printer("notinproject", fg="red")
|
self.notinproject = self.printer("notinproject", fg="red")
|
||||||
|
|
||||||
|
|
||||||
class BranchInfo(object):
|
class BranchInfo:
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.current = 0
|
self.current = 0
|
||||||
@@ -98,6 +98,22 @@ is shown, then the branch appears in all projects.
|
|||||||
"""
|
"""
|
||||||
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
|
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _ExpandProjectToBranches(cls, project_idx):
|
||||||
|
"""Expands a project into a list of branch names & associated info.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_idx: project.Project index
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Tuple[str, git_config.Branch, int]]
|
||||||
|
"""
|
||||||
|
branches = []
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
|
for name, b in project.GetBranches().items():
|
||||||
|
branches.append((name, b, project_idx))
|
||||||
|
return branches
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
projects = self.GetProjects(
|
projects = self.GetProjects(
|
||||||
args, all_manifests=not opt.this_manifest_only
|
args, all_manifests=not opt.this_manifest_only
|
||||||
@@ -107,17 +123,20 @@ is shown, then the branch appears in all projects.
|
|||||||
project_cnt = len(projects)
|
project_cnt = len(projects)
|
||||||
|
|
||||||
def _ProcessResults(_pool, _output, results):
|
def _ProcessResults(_pool, _output, results):
|
||||||
for name, b in itertools.chain.from_iterable(results):
|
for name, b, project_idx in itertools.chain.from_iterable(results):
|
||||||
|
b.project = projects[project_idx]
|
||||||
if name not in all_branches:
|
if name not in all_branches:
|
||||||
all_branches[name] = BranchInfo(name)
|
all_branches[name] = BranchInfo(name)
|
||||||
all_branches[name].add(b)
|
all_branches[name].add(b)
|
||||||
|
|
||||||
self.ExecuteInParallel(
|
with self.ParallelContext():
|
||||||
opt.jobs,
|
self.get_parallel_context()["projects"] = projects
|
||||||
expand_project_to_branches,
|
self.ExecuteInParallel(
|
||||||
projects,
|
opt.jobs,
|
||||||
callback=_ProcessResults,
|
self._ExpandProjectToBranches,
|
||||||
)
|
range(len(projects)),
|
||||||
|
callback=_ProcessResults,
|
||||||
|
)
|
||||||
|
|
||||||
names = sorted(all_branches)
|
names = sorted(all_branches)
|
||||||
|
|
||||||
@@ -148,7 +167,10 @@ is shown, then the branch appears in all projects.
|
|||||||
else:
|
else:
|
||||||
published = " "
|
published = " "
|
||||||
|
|
||||||
hdr("%c%c %-*s" % (current, published, width, name))
|
# A branch name can contain a percent sign, so we need to escape it.
|
||||||
|
# Escape after f-string formatting to properly account for leading
|
||||||
|
# spaces.
|
||||||
|
hdr(f"{current}{published} {name:{width}}".replace("%", "%%"))
|
||||||
out.write(" |")
|
out.write(" |")
|
||||||
|
|
||||||
_RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
|
_RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
|
||||||
@@ -174,7 +196,7 @@ is shown, then the branch appears in all projects.
|
|||||||
if _RelPath(p) not in have:
|
if _RelPath(p) not in have:
|
||||||
paths.append(_RelPath(p))
|
paths.append(_RelPath(p))
|
||||||
|
|
||||||
s = " %s %s" % (in_type, ", ".join(paths))
|
s = f" {in_type} {', '.join(paths)}"
|
||||||
if not i.IsSplitCurrent and (width + 7 + len(s) < 80):
|
if not i.IsSplitCurrent and (width + 7 + len(s) < 80):
|
||||||
fmt = out.current if i.IsCurrent else fmt
|
fmt = out.current if i.IsCurrent else fmt
|
||||||
fmt(s)
|
fmt(s)
|
||||||
@@ -191,19 +213,3 @@ is shown, then the branch appears in all projects.
|
|||||||
else:
|
else:
|
||||||
out.write(" in all projects")
|
out.write(" in all projects")
|
||||||
out.nl()
|
out.nl()
|
||||||
|
|
||||||
|
|
||||||
def expand_project_to_branches(project):
|
|
||||||
"""Expands a project into a list of branch names & associated information.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
project: project.Project
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[Tuple[str, git_config.Branch]]
|
|
||||||
"""
|
|
||||||
branches = []
|
|
||||||
for name, b in project.GetBranches().items():
|
|
||||||
b.project = project
|
|
||||||
branches.append((name, b))
|
|
||||||
return branches
|
|
||||||
|
|||||||
+25
-21
@@ -13,7 +13,6 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import sys
|
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
from command import Command
|
from command import Command
|
||||||
@@ -21,13 +20,16 @@ from command import DEFAULT_LOCAL_JOBS
|
|||||||
from error import GitError
|
from error import GitError
|
||||||
from error import RepoExitError
|
from error import RepoExitError
|
||||||
from progress import Progress
|
from progress import Progress
|
||||||
from project import Project
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
|
|
||||||
|
logger = RepoLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class CheckoutBranchResult(NamedTuple):
|
class CheckoutBranchResult(NamedTuple):
|
||||||
# Whether the Project is on the branch (i.e. branch exists and no errors)
|
# Whether the Project is on the branch (i.e. branch exists and no errors)
|
||||||
result: bool
|
result: bool
|
||||||
project: Project
|
project_idx: int
|
||||||
error: Exception
|
error: Exception
|
||||||
|
|
||||||
|
|
||||||
@@ -59,15 +61,17 @@ The command is equivalent to:
|
|||||||
if not args:
|
if not args:
|
||||||
self.Usage()
|
self.Usage()
|
||||||
|
|
||||||
def _ExecuteOne(self, nb, project):
|
@classmethod
|
||||||
|
def _ExecuteOne(cls, nb, project_idx):
|
||||||
"""Checkout one project."""
|
"""Checkout one project."""
|
||||||
error = None
|
error = None
|
||||||
result = None
|
result = None
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
try:
|
try:
|
||||||
result = project.CheckoutBranch(nb)
|
result = project.CheckoutBranch(nb)
|
||||||
except GitError as e:
|
except GitError as e:
|
||||||
error = e
|
error = e
|
||||||
return CheckoutBranchResult(result, project, error)
|
return CheckoutBranchResult(result, project_idx, error)
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
nb = args[0]
|
nb = args[0]
|
||||||
@@ -80,31 +84,31 @@ The command is equivalent to:
|
|||||||
|
|
||||||
def _ProcessResults(_pool, pm, results):
|
def _ProcessResults(_pool, pm, results):
|
||||||
for result in results:
|
for result in results:
|
||||||
|
project = all_projects[result.project_idx]
|
||||||
if result.error is not None:
|
if result.error is not None:
|
||||||
err.append(result.error)
|
err.append(result.error)
|
||||||
err_projects.append(result.project)
|
err_projects.append(project)
|
||||||
elif result.result:
|
elif result.result:
|
||||||
success.append(result.project)
|
success.append(project)
|
||||||
pm.update(msg="")
|
pm.update(msg="")
|
||||||
|
|
||||||
self.ExecuteInParallel(
|
with self.ParallelContext():
|
||||||
opt.jobs,
|
self.get_parallel_context()["projects"] = all_projects
|
||||||
functools.partial(self._ExecuteOne, nb),
|
self.ExecuteInParallel(
|
||||||
all_projects,
|
opt.jobs,
|
||||||
callback=_ProcessResults,
|
functools.partial(self._ExecuteOne, nb),
|
||||||
output=Progress(
|
range(len(all_projects)),
|
||||||
"Checkout %s" % (nb,), len(all_projects), quiet=opt.quiet
|
callback=_ProcessResults,
|
||||||
),
|
output=Progress(
|
||||||
)
|
f"Checkout {nb}", len(all_projects), quiet=opt.quiet
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if err_projects:
|
if err_projects:
|
||||||
for p in err_projects:
|
for p in err_projects:
|
||||||
print(
|
logger.error("error: %s/: cannot checkout %s", p.relpath, nb)
|
||||||
"error: %s/: cannot checkout %s" % (p.relpath, nb),
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
raise CheckoutCommandError(aggregate_errors=err)
|
raise CheckoutCommandError(aggregate_errors=err)
|
||||||
elif not success:
|
elif not success:
|
||||||
msg = f"error: no project has branch {nb}"
|
msg = f"error: no project has branch {nb}"
|
||||||
print(msg, file=sys.stderr)
|
logger.error(msg)
|
||||||
raise MissingBranchError(msg)
|
raise MissingBranchError(msg)
|
||||||
|
|||||||
+9
-13
@@ -18,9 +18,11 @@ import sys
|
|||||||
from command import Command
|
from command import Command
|
||||||
from error import GitError
|
from error import GitError
|
||||||
from git_command import GitCommand
|
from git_command import GitCommand
|
||||||
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
|
|
||||||
CHANGE_ID_RE = re.compile(r"^\s*Change-Id: I([0-9a-f]{40})\s*$")
|
CHANGE_ID_RE = re.compile(r"^\s*Change-Id: I([0-9a-f]{40})\s*$")
|
||||||
|
logger = RepoLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class CherryPick(Command):
|
class CherryPick(Command):
|
||||||
@@ -52,7 +54,7 @@ change id will be added.
|
|||||||
try:
|
try:
|
||||||
p.Wait()
|
p.Wait()
|
||||||
except GitError:
|
except GitError:
|
||||||
print(p.stderr, file=sys.stderr)
|
logger.error(p.stderr)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
sha1 = p.stdout.strip()
|
sha1 = p.stdout.strip()
|
||||||
@@ -67,9 +69,7 @@ change id will be added.
|
|||||||
try:
|
try:
|
||||||
p.Wait()
|
p.Wait()
|
||||||
except GitError:
|
except GitError:
|
||||||
print(
|
logger.error("error: Failed to retrieve old commit message")
|
||||||
"error: Failed to retrieve old commit message", file=sys.stderr
|
|
||||||
)
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
old_msg = self._StripHeader(p.stdout)
|
old_msg = self._StripHeader(p.stdout)
|
||||||
@@ -85,14 +85,13 @@ change id will be added.
|
|||||||
try:
|
try:
|
||||||
p.Wait()
|
p.Wait()
|
||||||
except GitError as e:
|
except GitError as e:
|
||||||
print(str(e))
|
logger.error(e)
|
||||||
print(
|
logger.warning(
|
||||||
"NOTE: When committing (please see above) and editing the "
|
"NOTE: When committing (please see above) and editing the "
|
||||||
"commit message, please remove the old Change-Id-line and "
|
"commit message, please remove the old Change-Id-line and "
|
||||||
"add:"
|
"add:\n%s",
|
||||||
|
self._GetReference(sha1),
|
||||||
)
|
)
|
||||||
print(self._GetReference(sha1), file=sys.stderr)
|
|
||||||
print(file=sys.stderr)
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if p.stdout:
|
if p.stdout:
|
||||||
@@ -115,10 +114,7 @@ change id will be added.
|
|||||||
try:
|
try:
|
||||||
p.Wait()
|
p.Wait()
|
||||||
except GitError:
|
except GitError:
|
||||||
print(
|
logger.error("error: Failed to update commit message")
|
||||||
"error: Failed to update commit message",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _IsChangeId(self, line):
|
def _IsChangeId(self, line):
|
||||||
|
|||||||
+16
-12
@@ -35,12 +35,12 @@ to the Unix 'patch' command.
|
|||||||
p.add_option(
|
p.add_option(
|
||||||
"-u",
|
"-u",
|
||||||
"--absolute",
|
"--absolute",
|
||||||
dest="absolute",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="paths are relative to the repository root",
|
help="paths are relative to the repository root",
|
||||||
)
|
)
|
||||||
|
|
||||||
def _ExecuteOne(self, absolute, local, project):
|
@classmethod
|
||||||
|
def _ExecuteOne(cls, absolute, local, project_idx):
|
||||||
"""Obtains the diff for a specific project.
|
"""Obtains the diff for a specific project.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -48,12 +48,13 @@ to the Unix 'patch' command.
|
|||||||
local: a boolean, if True, the path is relative to the local
|
local: a boolean, if True, the path is relative to the local
|
||||||
(sub)manifest. If false, the path is relative to the outermost
|
(sub)manifest. If false, the path is relative to the outermost
|
||||||
manifest.
|
manifest.
|
||||||
project: Project to get status of.
|
project_idx: Project index to get status of.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The status of the project.
|
The status of the project.
|
||||||
"""
|
"""
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
ret = project.PrintWorkTreeDiff(absolute, output_redir=buf, local=local)
|
ret = project.PrintWorkTreeDiff(absolute, output_redir=buf, local=local)
|
||||||
return (ret, buf.getvalue())
|
return (ret, buf.getvalue())
|
||||||
|
|
||||||
@@ -71,12 +72,15 @@ to the Unix 'patch' command.
|
|||||||
ret = 1
|
ret = 1
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
return self.ExecuteInParallel(
|
with self.ParallelContext():
|
||||||
opt.jobs,
|
self.get_parallel_context()["projects"] = all_projects
|
||||||
functools.partial(
|
return self.ExecuteInParallel(
|
||||||
self._ExecuteOne, opt.absolute, opt.this_manifest_only
|
opt.jobs,
|
||||||
),
|
functools.partial(
|
||||||
all_projects,
|
self._ExecuteOne, opt.absolute, opt.this_manifest_only
|
||||||
callback=_ProcessResults,
|
),
|
||||||
ordered=True,
|
range(len(all_projects)),
|
||||||
)
|
callback=_ProcessResults,
|
||||||
|
ordered=True,
|
||||||
|
chunksize=1,
|
||||||
|
)
|
||||||
|
|||||||
+12
-23
@@ -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",
|
||||||
@@ -87,25 +88,17 @@ synced and their revisions won't be found.
|
|||||||
def _printRawDiff(self, diff, pretty_format=None, local=False):
|
def _printRawDiff(self, diff, pretty_format=None, local=False):
|
||||||
_RelPath = lambda p: p.RelPath(local=local)
|
_RelPath = lambda p: p.RelPath(local=local)
|
||||||
for project in diff["added"]:
|
for project in diff["added"]:
|
||||||
self.printText(
|
self.printText(f"A {_RelPath(project)} {project.revisionExpr}")
|
||||||
"A %s %s" % (_RelPath(project), project.revisionExpr)
|
|
||||||
)
|
|
||||||
self.out.nl()
|
self.out.nl()
|
||||||
|
|
||||||
for project in diff["removed"]:
|
for project in diff["removed"]:
|
||||||
self.printText(
|
self.printText(f"R {_RelPath(project)} {project.revisionExpr}")
|
||||||
"R %s %s" % (_RelPath(project), project.revisionExpr)
|
|
||||||
)
|
|
||||||
self.out.nl()
|
self.out.nl()
|
||||||
|
|
||||||
for project, otherProject in diff["changed"]:
|
for project, otherProject in diff["changed"]:
|
||||||
self.printText(
|
self.printText(
|
||||||
"C %s %s %s"
|
f"C {_RelPath(project)} {project.revisionExpr} "
|
||||||
% (
|
f"{otherProject.revisionExpr}"
|
||||||
_RelPath(project),
|
|
||||||
project.revisionExpr,
|
|
||||||
otherProject.revisionExpr,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.out.nl()
|
self.out.nl()
|
||||||
self._printLogs(
|
self._printLogs(
|
||||||
@@ -118,12 +111,8 @@ synced and their revisions won't be found.
|
|||||||
|
|
||||||
for project, otherProject in diff["unreachable"]:
|
for project, otherProject in diff["unreachable"]:
|
||||||
self.printText(
|
self.printText(
|
||||||
"U %s %s %s"
|
f"U {_RelPath(project)} {project.revisionExpr} "
|
||||||
% (
|
f"{otherProject.revisionExpr}"
|
||||||
_RelPath(project),
|
|
||||||
project.revisionExpr,
|
|
||||||
otherProject.revisionExpr,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.out.nl()
|
self.out.nl()
|
||||||
|
|
||||||
@@ -245,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)
|
||||||
|
|||||||
+25
-25
@@ -19,9 +19,11 @@ from command import Command
|
|||||||
from error import GitError
|
from error import GitError
|
||||||
from error import NoSuchProjectError
|
from error import NoSuchProjectError
|
||||||
from error import RepoExitError
|
from error import RepoExitError
|
||||||
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
|
|
||||||
CHANGE_RE = re.compile(r"^([1-9][0-9]*)(?:[/\.-]([1-9][0-9]*))?$")
|
CHANGE_RE = re.compile(r"^([1-9][0-9]*)(?:[/\.-]([1-9][0-9]*))?$")
|
||||||
|
logger = RepoLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class DownloadCommandError(RepoExitError):
|
class DownloadCommandError(RepoExitError):
|
||||||
@@ -58,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",
|
||||||
)
|
)
|
||||||
@@ -109,21 +110,16 @@ If no project is specified try to use current directory as a project.
|
|||||||
except NoSuchProjectError:
|
except NoSuchProjectError:
|
||||||
project = None
|
project = None
|
||||||
if project not in projects:
|
if project not in projects:
|
||||||
print(
|
logger.error(
|
||||||
"error: %s matches too many projects; please "
|
"error: %s matches too many projects; please "
|
||||||
"re-run inside the project checkout." % (a,),
|
"re-run inside the project checkout.",
|
||||||
file=sys.stderr,
|
a,
|
||||||
)
|
)
|
||||||
for project in projects:
|
for project in projects:
|
||||||
print(
|
logger.error(
|
||||||
" %s/ @ %s"
|
" %s/ @ %s",
|
||||||
% (
|
project.RelPath(local=opt.this_manifest_only),
|
||||||
project.RelPath(
|
project.revisionExpr,
|
||||||
local=opt.this_manifest_only
|
|
||||||
),
|
|
||||||
project.revisionExpr,
|
|
||||||
),
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
raise NoSuchProjectError()
|
raise NoSuchProjectError()
|
||||||
else:
|
else:
|
||||||
@@ -156,18 +152,21 @@ If no project is specified try to use current directory as a project.
|
|||||||
dl = project.DownloadPatchSet(change_id, ps_id)
|
dl = project.DownloadPatchSet(change_id, ps_id)
|
||||||
|
|
||||||
if not opt.revert and not dl.commits:
|
if not opt.revert and not dl.commits:
|
||||||
print(
|
logger.error(
|
||||||
"[%s] change %d/%d has already been merged"
|
"[%s] change %d/%d has already been merged",
|
||||||
% (project.name, change_id, ps_id),
|
project.name,
|
||||||
file=sys.stderr,
|
change_id,
|
||||||
|
ps_id,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if len(dl.commits) > 1:
|
if len(dl.commits) > 1:
|
||||||
print(
|
logger.error(
|
||||||
"[%s] %d/%d depends on %d unmerged changes:"
|
"[%s] %d/%d depends on %d unmerged changes:",
|
||||||
% (project.name, change_id, ps_id, len(dl.commits)),
|
project.name,
|
||||||
file=sys.stderr,
|
change_id,
|
||||||
|
ps_id,
|
||||||
|
len(dl.commits),
|
||||||
)
|
)
|
||||||
for c in dl.commits:
|
for c in dl.commits:
|
||||||
print(" %s" % (c), file=sys.stderr)
|
print(" %s" % (c), file=sys.stderr)
|
||||||
@@ -204,9 +203,10 @@ If no project is specified try to use current directory as a project.
|
|||||||
project._Checkout(dl.commit)
|
project._Checkout(dl.commit)
|
||||||
|
|
||||||
except GitError:
|
except GitError:
|
||||||
print(
|
logger.error(
|
||||||
"[%s] Could not complete the %s of %s"
|
"[%s] Could not complete the %s of %s",
|
||||||
% (project.name, mode, dl.commit),
|
project.name,
|
||||||
file=sys.stderr,
|
mode,
|
||||||
|
dl.commit,
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|||||||
+66
-62
@@ -15,7 +15,6 @@
|
|||||||
import errno
|
import errno
|
||||||
import functools
|
import functools
|
||||||
import io
|
import io
|
||||||
import multiprocessing
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
@@ -26,10 +25,11 @@ from color import Coloring
|
|||||||
from command import Command
|
from command import Command
|
||||||
from command import DEFAULT_LOCAL_JOBS
|
from command import DEFAULT_LOCAL_JOBS
|
||||||
from command import MirrorSafeCommand
|
from command import MirrorSafeCommand
|
||||||
from command import WORKER_BATCH_SIZE
|
|
||||||
from error import ManifestInvalidRevisionError
|
from error import ManifestInvalidRevisionError
|
||||||
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
|
|
||||||
|
logger = RepoLogger(__file__)
|
||||||
_CAN_COLOR = [
|
_CAN_COLOR = [
|
||||||
"branch",
|
"branch",
|
||||||
"diff",
|
"diff",
|
||||||
@@ -102,6 +102,12 @@ revision to a locally executed git command, use REPO_LREV.
|
|||||||
REPO_RREV is the name of the revision from the manifest, exactly
|
REPO_RREV is the name of the revision from the manifest, exactly
|
||||||
as written in the manifest.
|
as written in the manifest.
|
||||||
|
|
||||||
|
REPO_UPSTREAM is the name of the upstream branch as specified in the
|
||||||
|
manifest.
|
||||||
|
|
||||||
|
REPO_DEST_BRANCH is the name of the destination branch for code review,
|
||||||
|
as specified in the manifest.
|
||||||
|
|
||||||
REPO_COUNT is the total number of projects being iterated.
|
REPO_COUNT is the total number of projects being iterated.
|
||||||
|
|
||||||
REPO_I is the current (1-based) iteration count. Can be used in
|
REPO_I is the current (1-based) iteration count. Can be used in
|
||||||
@@ -141,7 +147,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 +154,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 +161,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",
|
||||||
)
|
)
|
||||||
@@ -239,15 +241,8 @@ without iterating through the remaining projects.
|
|||||||
cmd.insert(cmd.index(cn) + 1, "--color")
|
cmd.insert(cmd.index(cn) + 1, "--color")
|
||||||
|
|
||||||
mirror = self.manifest.IsMirror
|
mirror = self.manifest.IsMirror
|
||||||
rc = 0
|
|
||||||
|
|
||||||
smart_sync_manifest_name = "smart_sync_override.xml"
|
self.TryOverrideManifestWithSmartSync()
|
||||||
smart_sync_manifest_path = os.path.join(
|
|
||||||
self.manifest.manifestProject.worktree, smart_sync_manifest_name
|
|
||||||
)
|
|
||||||
|
|
||||||
if os.path.isfile(smart_sync_manifest_path):
|
|
||||||
self.manifest.Override(smart_sync_manifest_path)
|
|
||||||
|
|
||||||
if opt.regex:
|
if opt.regex:
|
||||||
projects = self.FindProjects(args, all_manifests=all_trees)
|
projects = self.FindProjects(args, all_manifests=all_trees)
|
||||||
@@ -262,71 +257,80 @@ without iterating through the remaining projects.
|
|||||||
|
|
||||||
os.environ["REPO_COUNT"] = str(len(projects))
|
os.environ["REPO_COUNT"] = str(len(projects))
|
||||||
|
|
||||||
|
def _ProcessResults(_pool, _output, results):
|
||||||
|
rc = 0
|
||||||
|
first = True
|
||||||
|
for r, output in results:
|
||||||
|
if output:
|
||||||
|
if first:
|
||||||
|
first = False
|
||||||
|
elif opt.project_header:
|
||||||
|
print()
|
||||||
|
# To simplify the DoWorkWrapper, take care of automatic
|
||||||
|
# newlines.
|
||||||
|
end = "\n"
|
||||||
|
if output[-1] == "\n":
|
||||||
|
end = ""
|
||||||
|
print(output, end=end)
|
||||||
|
rc = rc or r
|
||||||
|
if r != 0 and opt.abort_on_errors:
|
||||||
|
raise Exception("Aborting due to previous error")
|
||||||
|
return rc
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = self.manifest.manifestProject.config
|
config = self.manifest.manifestProject.config
|
||||||
with multiprocessing.Pool(opt.jobs, InitWorker) as pool:
|
with self.ParallelContext():
|
||||||
results_it = pool.imap(
|
self.get_parallel_context()["projects"] = projects
|
||||||
|
rc = self.ExecuteInParallel(
|
||||||
|
opt.jobs,
|
||||||
functools.partial(
|
functools.partial(
|
||||||
DoWorkWrapper, mirror, opt, cmd, shell, config
|
self.DoWorkWrapper, mirror, opt, cmd, shell, config
|
||||||
),
|
),
|
||||||
enumerate(projects),
|
range(len(projects)),
|
||||||
chunksize=WORKER_BATCH_SIZE,
|
callback=_ProcessResults,
|
||||||
|
ordered=True,
|
||||||
|
initializer=self.InitWorker,
|
||||||
|
chunksize=1,
|
||||||
)
|
)
|
||||||
first = True
|
|
||||||
for r, output in results_it:
|
|
||||||
if output:
|
|
||||||
if first:
|
|
||||||
first = False
|
|
||||||
elif opt.project_header:
|
|
||||||
print()
|
|
||||||
# To simplify the DoWorkWrapper, take care of automatic
|
|
||||||
# newlines.
|
|
||||||
end = "\n"
|
|
||||||
if output[-1] == "\n":
|
|
||||||
end = ""
|
|
||||||
print(output, end=end)
|
|
||||||
rc = rc or r
|
|
||||||
if r != 0 and opt.abort_on_errors:
|
|
||||||
raise Exception("Aborting due to previous error")
|
|
||||||
except (KeyboardInterrupt, WorkerKeyboardInterrupt):
|
except (KeyboardInterrupt, WorkerKeyboardInterrupt):
|
||||||
# Catch KeyboardInterrupt raised inside and outside of workers
|
# Catch KeyboardInterrupt raised inside and outside of workers
|
||||||
rc = rc or errno.EINTR
|
rc = errno.EINTR
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Catch any other exceptions raised
|
# Catch any other exceptions raised
|
||||||
print(
|
logger.error(
|
||||||
"forall: unhandled error, terminating the pool: %s: %s"
|
"forall: unhandled error, terminating the pool: %s: %s",
|
||||||
% (type(e).__name__, e),
|
type(e).__name__,
|
||||||
file=sys.stderr,
|
e,
|
||||||
)
|
)
|
||||||
rc = rc or getattr(e, "errno", 1)
|
rc = getattr(e, "errno", 1)
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
sys.exit(rc)
|
sys.exit(rc)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def InitWorker(cls):
|
||||||
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def DoWorkWrapper(cls, mirror, opt, cmd, shell, config, project_idx):
|
||||||
|
"""A wrapper around the DoWork() method.
|
||||||
|
|
||||||
|
Catch the KeyboardInterrupt exceptions here and re-raise them as a
|
||||||
|
different, ``Exception``-based exception to stop it flooding the console
|
||||||
|
with stacktraces and making the parent hang indefinitely.
|
||||||
|
|
||||||
|
"""
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
|
try:
|
||||||
|
return DoWork(project, mirror, opt, cmd, shell, project_idx, config)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("%s: Worker interrupted" % project.name)
|
||||||
|
raise WorkerKeyboardInterrupt()
|
||||||
|
|
||||||
|
|
||||||
class WorkerKeyboardInterrupt(Exception):
|
class WorkerKeyboardInterrupt(Exception):
|
||||||
"""Keyboard interrupt exception for worker processes."""
|
"""Keyboard interrupt exception for worker processes."""
|
||||||
|
|
||||||
|
|
||||||
def InitWorker():
|
|
||||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
||||||
|
|
||||||
|
|
||||||
def DoWorkWrapper(mirror, opt, cmd, shell, config, args):
|
|
||||||
"""A wrapper around the DoWork() method.
|
|
||||||
|
|
||||||
Catch the KeyboardInterrupt exceptions here and re-raise them as a
|
|
||||||
different, ``Exception``-based exception to stop it flooding the console
|
|
||||||
with stacktraces and making the parent hang indefinitely.
|
|
||||||
|
|
||||||
"""
|
|
||||||
cnt, project = args
|
|
||||||
try:
|
|
||||||
return DoWork(project, mirror, opt, cmd, shell, cnt, config)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("%s: Worker interrupted" % project.name)
|
|
||||||
raise WorkerKeyboardInterrupt()
|
|
||||||
|
|
||||||
|
|
||||||
def DoWork(project, mirror, opt, cmd, shell, cnt, config):
|
def DoWork(project, mirror, opt, cmd, shell, cnt, config):
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
|
|
||||||
|
|||||||
+312
@@ -0,0 +1,312 @@
|
|||||||
|
# Copyright (C) 2024 The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import List, Set
|
||||||
|
|
||||||
|
from command import Command
|
||||||
|
from git_command import git_require
|
||||||
|
from git_command import GitCommand
|
||||||
|
import platform_utils
|
||||||
|
from progress import Progress
|
||||||
|
from project import Project
|
||||||
|
|
||||||
|
|
||||||
|
class Gc(Command):
|
||||||
|
COMMON = True
|
||||||
|
helpSummary = "Cleaning up internal repo and Git state."
|
||||||
|
helpUsage = """
|
||||||
|
%prog
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _Options(self, p):
|
||||||
|
p.add_option(
|
||||||
|
"-n",
|
||||||
|
"--dry-run",
|
||||||
|
dest="dryrun",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="do everything except actually delete",
|
||||||
|
)
|
||||||
|
p.add_option(
|
||||||
|
"-y",
|
||||||
|
"--yes",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="answer yes to all safe prompts",
|
||||||
|
)
|
||||||
|
p.add_option(
|
||||||
|
"--repack",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="repack all projects that use partial clone with "
|
||||||
|
"filter=blob:none",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _find_git_to_delete(
|
||||||
|
self, to_keep: Set[str], start_dir: str
|
||||||
|
) -> Set[str]:
|
||||||
|
"""Searches no longer needed ".git" directories.
|
||||||
|
|
||||||
|
Scans the file system starting from `start_dir` and removes all
|
||||||
|
directories that end with ".git" that are not in the `to_keep` set.
|
||||||
|
"""
|
||||||
|
to_delete = set()
|
||||||
|
for root, dirs, _ in platform_utils.walk(start_dir):
|
||||||
|
for directory in dirs:
|
||||||
|
if not directory.endswith(".git"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
path = os.path.join(root, directory)
|
||||||
|
if path not in to_keep:
|
||||||
|
to_delete.add(path)
|
||||||
|
|
||||||
|
return to_delete
|
||||||
|
|
||||||
|
def delete_unused_projects(self, projects: List[Project], opt):
|
||||||
|
print(f"Scanning filesystem under {self.repodir}...")
|
||||||
|
|
||||||
|
project_paths = set()
|
||||||
|
project_object_paths = set()
|
||||||
|
|
||||||
|
for project in projects:
|
||||||
|
project_paths.add(project.gitdir)
|
||||||
|
project_object_paths.add(project.objdir)
|
||||||
|
|
||||||
|
to_delete = self._find_git_to_delete(
|
||||||
|
project_paths, os.path.join(self.repodir, "projects")
|
||||||
|
)
|
||||||
|
|
||||||
|
to_delete.update(
|
||||||
|
self._find_git_to_delete(
|
||||||
|
project_object_paths,
|
||||||
|
os.path.join(self.repodir, "project-objects"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not to_delete:
|
||||||
|
print("Nothing to clean up.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print("Identified the following projects are no longer used:")
|
||||||
|
print("\n".join(to_delete))
|
||||||
|
print("")
|
||||||
|
if not opt.yes:
|
||||||
|
print(
|
||||||
|
"If you proceed, any local commits in those projects will be "
|
||||||
|
"destroyed!"
|
||||||
|
)
|
||||||
|
ask = input("Proceed? [y/N] ")
|
||||||
|
if ask.lower() != "y":
|
||||||
|
return 1
|
||||||
|
|
||||||
|
pm = Progress(
|
||||||
|
"Deleting",
|
||||||
|
len(to_delete),
|
||||||
|
delay=False,
|
||||||
|
quiet=opt.quiet,
|
||||||
|
show_elapsed=True,
|
||||||
|
elide=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
for path in to_delete:
|
||||||
|
if opt.dryrun:
|
||||||
|
print(f"\nWould have deleted ${path}")
|
||||||
|
else:
|
||||||
|
tmp_path = os.path.join(
|
||||||
|
os.path.dirname(path),
|
||||||
|
f"to_be_deleted_{os.path.basename(path)}",
|
||||||
|
)
|
||||||
|
platform_utils.rename(path, tmp_path)
|
||||||
|
platform_utils.rmtree(tmp_path)
|
||||||
|
pm.update(msg=path)
|
||||||
|
pm.end()
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _generate_promisor_files(self, pack_dir: str):
|
||||||
|
"""Generates promisor files for all pack files in the given directory.
|
||||||
|
|
||||||
|
Promisor files are empty files with the same name as the corresponding
|
||||||
|
pack file but with the ".promisor" extension. They are used by Git.
|
||||||
|
"""
|
||||||
|
for root, _, files in platform_utils.walk(pack_dir):
|
||||||
|
for file in files:
|
||||||
|
if not file.endswith(".pack"):
|
||||||
|
continue
|
||||||
|
with open(os.path.join(root, f"{file[:-4]}promisor"), "w"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def repack_projects(self, projects: List[Project], opt):
|
||||||
|
repack_projects = []
|
||||||
|
# Find all projects eligible for repacking:
|
||||||
|
# - can't be shared
|
||||||
|
# - have a specific fetch filter
|
||||||
|
for project in projects:
|
||||||
|
if project.config.GetBoolean("extensions.preciousObjects"):
|
||||||
|
continue
|
||||||
|
if not project.clone_depth:
|
||||||
|
continue
|
||||||
|
if project.manifest.CloneFilterForDepth != "blob:none":
|
||||||
|
continue
|
||||||
|
|
||||||
|
repack_projects.append(project)
|
||||||
|
|
||||||
|
if opt.dryrun:
|
||||||
|
print(f"Would have repacked {len(repack_projects)} projects.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
pm = Progress(
|
||||||
|
"Repacking (this will take a while)",
|
||||||
|
len(repack_projects),
|
||||||
|
delay=False,
|
||||||
|
quiet=opt.quiet,
|
||||||
|
show_elapsed=True,
|
||||||
|
elide=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
for project in repack_projects:
|
||||||
|
pm.update(msg=f"{project.name}")
|
||||||
|
|
||||||
|
pack_dir = os.path.join(project.gitdir, "tmp_repo_repack")
|
||||||
|
if os.path.isdir(pack_dir):
|
||||||
|
platform_utils.rmtree(pack_dir)
|
||||||
|
os.mkdir(pack_dir)
|
||||||
|
|
||||||
|
# Prepare workspace for repacking - remove all unreachable refs and
|
||||||
|
# their objects.
|
||||||
|
GitCommand(
|
||||||
|
project,
|
||||||
|
["reflog", "expire", "--expire-unreachable=all"],
|
||||||
|
verify_command=True,
|
||||||
|
).Wait()
|
||||||
|
pm.update(msg=f"{project.name} | gc", inc=0)
|
||||||
|
GitCommand(
|
||||||
|
project,
|
||||||
|
["gc"],
|
||||||
|
verify_command=True,
|
||||||
|
).Wait()
|
||||||
|
|
||||||
|
# Get all objects that are reachable from the remote, and pack them.
|
||||||
|
pm.update(msg=f"{project.name} | generating list of objects", inc=0)
|
||||||
|
remote_objects_cmd = GitCommand(
|
||||||
|
project,
|
||||||
|
[
|
||||||
|
"rev-list",
|
||||||
|
"--objects",
|
||||||
|
"--missing=allow-promisor",
|
||||||
|
f"--remotes={project.remote.name}",
|
||||||
|
"--filter=blob:none",
|
||||||
|
"--tags",
|
||||||
|
],
|
||||||
|
capture_stdout=True,
|
||||||
|
verify_command=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get all local objects and pack them.
|
||||||
|
local_head_objects_cmd = GitCommand(
|
||||||
|
project,
|
||||||
|
[
|
||||||
|
"rev-list",
|
||||||
|
"--objects",
|
||||||
|
"--missing=allow-promisor",
|
||||||
|
"HEAD^{tree}",
|
||||||
|
],
|
||||||
|
capture_stdout=True,
|
||||||
|
verify_command=True,
|
||||||
|
)
|
||||||
|
local_objects_cmd = GitCommand(
|
||||||
|
project,
|
||||||
|
[
|
||||||
|
"rev-list",
|
||||||
|
"--objects",
|
||||||
|
"--missing=allow-promisor",
|
||||||
|
"--all",
|
||||||
|
"--reflog",
|
||||||
|
"--indexed-objects",
|
||||||
|
"--not",
|
||||||
|
f"--remotes={project.remote.name}",
|
||||||
|
"--tags",
|
||||||
|
],
|
||||||
|
capture_stdout=True,
|
||||||
|
verify_command=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
remote_objects_cmd.Wait()
|
||||||
|
|
||||||
|
pm.update(msg=f"{project.name} | remote repack", inc=0)
|
||||||
|
GitCommand(
|
||||||
|
project,
|
||||||
|
["pack-objects", os.path.join(pack_dir, "pack")],
|
||||||
|
input=remote_objects_cmd.stdout,
|
||||||
|
capture_stderr=True,
|
||||||
|
capture_stdout=True,
|
||||||
|
verify_command=True,
|
||||||
|
).Wait()
|
||||||
|
|
||||||
|
# create promisor file for each pack file
|
||||||
|
self._generate_promisor_files(pack_dir)
|
||||||
|
|
||||||
|
local_head_objects_cmd.Wait()
|
||||||
|
local_objects_cmd.Wait()
|
||||||
|
|
||||||
|
pm.update(msg=f"{project.name} | local repack", inc=0)
|
||||||
|
GitCommand(
|
||||||
|
project,
|
||||||
|
["pack-objects", os.path.join(pack_dir, "pack")],
|
||||||
|
input=local_head_objects_cmd.stdout + local_objects_cmd.stdout,
|
||||||
|
capture_stderr=True,
|
||||||
|
capture_stdout=True,
|
||||||
|
verify_command=True,
|
||||||
|
).Wait()
|
||||||
|
|
||||||
|
# Swap the old pack directory with the new one.
|
||||||
|
platform_utils.rename(
|
||||||
|
os.path.join(project.objdir, "objects", "pack"),
|
||||||
|
os.path.join(project.objdir, "objects", "pack_old"),
|
||||||
|
)
|
||||||
|
platform_utils.rename(
|
||||||
|
pack_dir,
|
||||||
|
os.path.join(project.objdir, "objects", "pack"),
|
||||||
|
)
|
||||||
|
platform_utils.rmtree(
|
||||||
|
os.path.join(project.objdir, "objects", "pack_old")
|
||||||
|
)
|
||||||
|
|
||||||
|
pm.end()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def Execute(self, opt, args):
|
||||||
|
projects: List[Project] = self.GetProjects(
|
||||||
|
args, all_manifests=not opt.this_manifest_only
|
||||||
|
)
|
||||||
|
|
||||||
|
# If the user specified projects, fetch the global list separately
|
||||||
|
# to avoid deleting untargeted projects.
|
||||||
|
if args:
|
||||||
|
all_projects = self.GetProjects(
|
||||||
|
[], all_manifests=not opt.this_manifest_only
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
all_projects = projects
|
||||||
|
|
||||||
|
ret = self.delete_unused_projects(all_projects, opt)
|
||||||
|
if ret != 0:
|
||||||
|
return ret
|
||||||
|
|
||||||
|
if opt.repack:
|
||||||
|
git_require((2, 17, 0), fail=True, msg="--repack")
|
||||||
|
ret = self.repack_projects(projects, opt)
|
||||||
|
|
||||||
|
return ret
|
||||||
+34
-26
@@ -23,7 +23,10 @@ from error import GitError
|
|||||||
from error import InvalidArgumentsError
|
from error import InvalidArgumentsError
|
||||||
from error import SilentRepoExitError
|
from error import SilentRepoExitError
|
||||||
from git_command import GitCommand
|
from git_command import GitCommand
|
||||||
from project import Project
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
|
|
||||||
|
logger = RepoLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class GrepColoring(Coloring):
|
class GrepColoring(Coloring):
|
||||||
@@ -36,7 +39,7 @@ class GrepColoring(Coloring):
|
|||||||
class ExecuteOneResult(NamedTuple):
|
class ExecuteOneResult(NamedTuple):
|
||||||
"""Result from an execute instance."""
|
"""Result from an execute instance."""
|
||||||
|
|
||||||
project: Project
|
project_idx: int
|
||||||
rc: int
|
rc: int
|
||||||
stdout: str
|
stdout: str
|
||||||
stderr: str
|
stderr: str
|
||||||
@@ -90,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("(")
|
||||||
@@ -117,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",
|
||||||
@@ -258,8 +260,10 @@ contain a line that matches both expressions:
|
|||||||
help="Show only file names not containing matching lines",
|
help="Show only file names not containing matching lines",
|
||||||
)
|
)
|
||||||
|
|
||||||
def _ExecuteOne(self, cmd_argv, project):
|
@classmethod
|
||||||
|
def _ExecuteOne(cls, cmd_argv, project_idx):
|
||||||
"""Process one project."""
|
"""Process one project."""
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
try:
|
try:
|
||||||
p = GitCommand(
|
p = GitCommand(
|
||||||
project,
|
project,
|
||||||
@@ -270,7 +274,7 @@ contain a line that matches both expressions:
|
|||||||
verify_command=True,
|
verify_command=True,
|
||||||
)
|
)
|
||||||
except GitError as e:
|
except GitError as e:
|
||||||
return ExecuteOneResult(project, -1, None, str(e), e)
|
return ExecuteOneResult(project_idx, -1, None, str(e), e)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
error = None
|
error = None
|
||||||
@@ -278,10 +282,12 @@ contain a line that matches both expressions:
|
|||||||
except GitError as e:
|
except GitError as e:
|
||||||
rc = 1
|
rc = 1
|
||||||
error = e
|
error = e
|
||||||
return ExecuteOneResult(project, rc, p.stdout, p.stderr, error)
|
return ExecuteOneResult(project_idx, rc, p.stdout, p.stderr, error)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _ProcessResults(full_name, have_rev, opt, _pool, out, results):
|
def _ProcessResults(
|
||||||
|
full_name, have_rev, opt, projects, _pool, out, results
|
||||||
|
):
|
||||||
git_failed = False
|
git_failed = False
|
||||||
bad_rev = False
|
bad_rev = False
|
||||||
have_match = False
|
have_match = False
|
||||||
@@ -289,9 +295,10 @@ contain a line that matches both expressions:
|
|||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
|
project = projects[result.project_idx]
|
||||||
if result.rc < 0:
|
if result.rc < 0:
|
||||||
git_failed = True
|
git_failed = True
|
||||||
out.project("--- project %s ---" % _RelPath(result.project))
|
out.project("--- project %s ---" % _RelPath(project))
|
||||||
out.nl()
|
out.nl()
|
||||||
out.fail("%s", result.stderr)
|
out.fail("%s", result.stderr)
|
||||||
out.nl()
|
out.nl()
|
||||||
@@ -307,9 +314,7 @@ contain a line that matches both expressions:
|
|||||||
):
|
):
|
||||||
bad_rev = True
|
bad_rev = True
|
||||||
else:
|
else:
|
||||||
out.project(
|
out.project("--- project %s ---" % _RelPath(project))
|
||||||
"--- project %s ---" % _RelPath(result.project)
|
|
||||||
)
|
|
||||||
out.nl()
|
out.nl()
|
||||||
out.fail("%s", result.stderr.strip())
|
out.fail("%s", result.stderr.strip())
|
||||||
out.nl()
|
out.nl()
|
||||||
@@ -327,13 +332,13 @@ contain a line that matches both expressions:
|
|||||||
rev, line = line.split(":", 1)
|
rev, line = line.split(":", 1)
|
||||||
out.write("%s", rev)
|
out.write("%s", rev)
|
||||||
out.write(":")
|
out.write(":")
|
||||||
out.project(_RelPath(result.project))
|
out.project(_RelPath(project))
|
||||||
out.write("/")
|
out.write("/")
|
||||||
out.write("%s", line)
|
out.write("%s", line)
|
||||||
out.nl()
|
out.nl()
|
||||||
elif full_name:
|
elif full_name:
|
||||||
for line in r:
|
for line in r:
|
||||||
out.project(_RelPath(result.project))
|
out.project(_RelPath(project))
|
||||||
out.write("/")
|
out.write("/")
|
||||||
out.write("%s", line)
|
out.write("%s", line)
|
||||||
out.nl()
|
out.nl()
|
||||||
@@ -371,22 +376,25 @@ contain a line that matches both expressions:
|
|||||||
if opt.revision:
|
if opt.revision:
|
||||||
if "--cached" in cmd_argv:
|
if "--cached" in cmd_argv:
|
||||||
msg = "fatal: cannot combine --cached and --revision"
|
msg = "fatal: cannot combine --cached and --revision"
|
||||||
print(msg, file=sys.stderr)
|
logger.error(msg)
|
||||||
raise InvalidArgumentsError(msg)
|
raise InvalidArgumentsError(msg)
|
||||||
have_rev = True
|
have_rev = True
|
||||||
cmd_argv.extend(opt.revision)
|
cmd_argv.extend(opt.revision)
|
||||||
cmd_argv.append("--")
|
cmd_argv.append("--")
|
||||||
|
|
||||||
git_failed, bad_rev, have_match, errors = self.ExecuteInParallel(
|
with self.ParallelContext():
|
||||||
opt.jobs,
|
self.get_parallel_context()["projects"] = projects
|
||||||
functools.partial(self._ExecuteOne, cmd_argv),
|
git_failed, bad_rev, have_match, errors = self.ExecuteInParallel(
|
||||||
projects,
|
opt.jobs,
|
||||||
callback=functools.partial(
|
functools.partial(self._ExecuteOne, cmd_argv),
|
||||||
self._ProcessResults, full_name, have_rev, opt
|
range(len(projects)),
|
||||||
),
|
callback=functools.partial(
|
||||||
output=out,
|
self._ProcessResults, full_name, have_rev, opt, projects
|
||||||
ordered=True,
|
),
|
||||||
)
|
output=out,
|
||||||
|
ordered=True,
|
||||||
|
chunksize=1,
|
||||||
|
)
|
||||||
|
|
||||||
if git_failed:
|
if git_failed:
|
||||||
raise GrepCommandError(
|
raise GrepCommandError(
|
||||||
@@ -396,5 +404,5 @@ contain a line that matches both expressions:
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
elif have_rev and bad_rev:
|
elif have_rev and bad_rev:
|
||||||
for r in opt.revision:
|
for r in opt.revision:
|
||||||
print("error: can't search revision %s" % r, file=sys.stderr)
|
logger.error("error: can't search revision %s", r)
|
||||||
raise GrepCommandError(aggregate_errors=errors)
|
raise GrepCommandError(aggregate_errors=errors)
|
||||||
|
|||||||
+4
-6
@@ -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)
|
||||||
|
|
||||||
@@ -150,7 +148,7 @@ Displays detailed usage information about a command.
|
|||||||
def _PrintAllCommandHelp(self):
|
def _PrintAllCommandHelp(self):
|
||||||
for name in sorted(all_commands):
|
for name in sorted(all_commands):
|
||||||
cmd = all_commands[name](manifest=self.manifest)
|
cmd = all_commands[name](manifest=self.manifest)
|
||||||
self._PrintCommandHelp(cmd, header_prefix="[%s] " % (name,))
|
self._PrintCommandHelp(cmd, header_prefix=f"[{name}] ")
|
||||||
|
|
||||||
def _Options(self, p):
|
def _Options(self, p):
|
||||||
p.add_option(
|
p.add_option(
|
||||||
|
|||||||
+323
-120
@@ -12,14 +12,41 @@
|
|||||||
# 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 functools
|
||||||
|
import io
|
||||||
|
import json
|
||||||
import optparse
|
import optparse
|
||||||
|
import sys
|
||||||
|
from typing import Any, Dict, List, NamedTuple
|
||||||
|
|
||||||
from color import Coloring
|
from color import Coloring
|
||||||
|
from command import DEFAULT_LOCAL_JOBS
|
||||||
from command import PagedCommand
|
from command import PagedCommand
|
||||||
from git_refs import R_HEADS
|
from git_refs import R_HEADS
|
||||||
from git_refs import R_M
|
from git_refs import R_M
|
||||||
|
|
||||||
|
|
||||||
|
class BranchInfo(NamedTuple):
|
||||||
|
"""Holds information about a branch in a project."""
|
||||||
|
|
||||||
|
relpath: str
|
||||||
|
name: str
|
||||||
|
commits: Any
|
||||||
|
date: str
|
||||||
|
is_current: bool
|
||||||
|
|
||||||
|
|
||||||
|
class OutputFormat(enum.Enum):
|
||||||
|
"""Type for the requested output format."""
|
||||||
|
|
||||||
|
# Human-readable text output.
|
||||||
|
TEXT = enum.auto()
|
||||||
|
|
||||||
|
# Machine-readable JSON output.
|
||||||
|
JSON = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
class _Coloring(Coloring):
|
class _Coloring(Coloring):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
Coloring.__init__(self, config, "status")
|
Coloring.__init__(self, config, "status")
|
||||||
@@ -27,10 +54,11 @@ class _Coloring(Coloring):
|
|||||||
|
|
||||||
class Info(PagedCommand):
|
class Info(PagedCommand):
|
||||||
COMMON = True
|
COMMON = True
|
||||||
|
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
|
||||||
helpSummary = (
|
helpSummary = (
|
||||||
"Get info on the manifest branch, current branch or unmerged branches"
|
"Get info on the manifest branch, current branch or unmerged branches"
|
||||||
)
|
)
|
||||||
helpUsage = "%prog [-dl] [-o [-c]] [<project>...]"
|
helpUsage = "%prog [-dl] [-o [-c]] [--format=<format>] [<project>...]"
|
||||||
|
|
||||||
def _Options(self, p):
|
def _Options(self, p):
|
||||||
p.add_option(
|
p.add_option(
|
||||||
@@ -43,14 +71,36 @@ 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(
|
||||||
|
"--include-summary",
|
||||||
|
action="store_true",
|
||||||
|
default=True,
|
||||||
|
help="include manifest summary (default: true)",
|
||||||
|
)
|
||||||
|
p.add_option(
|
||||||
|
"--no-include-summary",
|
||||||
|
dest="include_summary",
|
||||||
|
action="store_false",
|
||||||
|
help="exclude manifest summary",
|
||||||
|
)
|
||||||
|
p.add_option(
|
||||||
|
"--include-projects",
|
||||||
|
action="store_true",
|
||||||
|
default=True,
|
||||||
|
help="include project details (default: true)",
|
||||||
|
)
|
||||||
|
p.add_option(
|
||||||
|
"--no-include-projects",
|
||||||
|
dest="include_projects",
|
||||||
|
action="store_false",
|
||||||
|
help="exclude project details",
|
||||||
|
)
|
||||||
p.add_option(
|
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",
|
||||||
)
|
)
|
||||||
@@ -74,8 +124,39 @@ class Info(PagedCommand):
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="disable all remote operations",
|
help="disable all remote operations",
|
||||||
)
|
)
|
||||||
|
formats = tuple(x.lower() for x in OutputFormat.__members__.keys())
|
||||||
|
p.add_option(
|
||||||
|
"--format",
|
||||||
|
default=OutputFormat.TEXT.name.lower(),
|
||||||
|
choices=formats,
|
||||||
|
help=f"output format: {', '.join(formats)} (default: %default)",
|
||||||
|
)
|
||||||
|
|
||||||
|
def WantPager(self, opt):
|
||||||
|
return OutputFormat[opt.format.upper()] == OutputFormat.TEXT
|
||||||
|
|
||||||
|
def ValidateOptions(self, opt, args):
|
||||||
|
output_format = OutputFormat[opt.format.upper()]
|
||||||
|
if output_format == OutputFormat.JSON:
|
||||||
|
if opt.all:
|
||||||
|
self.OptionParser.error("--diff is not supported with JSON")
|
||||||
|
if opt.overview:
|
||||||
|
self.OptionParser.error("--overview is not supported with JSON")
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
|
if not opt.this_manifest_only:
|
||||||
|
self.manifest = self.manifest.outer_client
|
||||||
|
|
||||||
|
self.TryOverrideManifestWithSmartSync()
|
||||||
|
|
||||||
|
output_format = OutputFormat[opt.format.upper()]
|
||||||
|
if output_format == OutputFormat.JSON:
|
||||||
|
self._ExecuteJson(opt, args)
|
||||||
|
else:
|
||||||
|
self._ExecuteText(opt, args)
|
||||||
|
|
||||||
|
def _ExecuteText(self, opt, args) -> None:
|
||||||
|
"""Output info as human-readable text."""
|
||||||
self.out = _Coloring(self.client.globalConfig)
|
self.out = _Coloring(self.client.globalConfig)
|
||||||
self.heading = self.out.printer("heading", attr="bold")
|
self.heading = self.out.printer("heading", attr="bold")
|
||||||
self.headtext = self.out.nofmt_printer("headtext", fg="yellow")
|
self.headtext = self.out.nofmt_printer("headtext", fg="yellow")
|
||||||
@@ -86,150 +167,272 @@ class Info(PagedCommand):
|
|||||||
|
|
||||||
self.opt = opt
|
self.opt = opt
|
||||||
|
|
||||||
if not opt.this_manifest_only:
|
if opt.include_summary:
|
||||||
self.manifest = self.manifest.outer_client
|
self._printSummary()
|
||||||
manifestConfig = self.manifest.manifestProject.config
|
|
||||||
mergeBranch = manifestConfig.GetBranch("default").merge
|
|
||||||
manifestGroups = self.manifest.GetGroupsStr()
|
|
||||||
|
|
||||||
self.heading("Manifest branch: ")
|
if not opt.include_projects:
|
||||||
if self.manifest.default.revisionExpr:
|
return
|
||||||
self.headtext(self.manifest.default.revisionExpr)
|
elif not opt.overview:
|
||||||
self.out.nl()
|
|
||||||
self.heading("Manifest merge branch: ")
|
|
||||||
self.headtext(mergeBranch)
|
|
||||||
self.out.nl()
|
|
||||||
self.heading("Manifest groups: ")
|
|
||||||
self.headtext(manifestGroups)
|
|
||||||
self.out.nl()
|
|
||||||
|
|
||||||
self.printSeparator()
|
|
||||||
|
|
||||||
if not opt.overview:
|
|
||||||
self._printDiffInfo(opt, args)
|
self._printDiffInfo(opt, args)
|
||||||
else:
|
else:
|
||||||
self._printCommitOverview(opt, args)
|
self._printCommitOverview(opt, args)
|
||||||
|
|
||||||
|
def _getSummaryData(self) -> Dict[str, Any]:
|
||||||
|
"""Gather manifest summary data as a dict."""
|
||||||
|
manifestConfig = self.manifest.manifestProject.config
|
||||||
|
mergeBranch = manifestConfig.GetBranch("default").merge
|
||||||
|
manifestGroups = self.manifest.GetManifestGroupsStr()
|
||||||
|
sp = self.manifest.superproject
|
||||||
|
srev = sp.commit_id if sp and sp.commit_id else None
|
||||||
|
return {
|
||||||
|
"manifest_branch": self.manifest.default.revisionExpr or "",
|
||||||
|
"manifest_merge_branch": mergeBranch or "",
|
||||||
|
"manifest_groups": manifestGroups,
|
||||||
|
"superproject_revision": srev,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _getProjectData(self, project) -> Dict[str, Any]:
|
||||||
|
"""Gather project data as a dict."""
|
||||||
|
data = {
|
||||||
|
"name": project.name,
|
||||||
|
"mount_path": project.worktree,
|
||||||
|
"current_revision": project.GetRevisionId(),
|
||||||
|
"manifest_revision": project.revisionExpr,
|
||||||
|
"local_branches": list(project.GetBranches()),
|
||||||
|
}
|
||||||
|
currentBranch = project.CurrentBranch
|
||||||
|
if currentBranch:
|
||||||
|
data["current_branch"] = currentBranch
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _ExecuteJson(self, opt, args) -> None:
|
||||||
|
"""Output info as JSON."""
|
||||||
|
result = {}
|
||||||
|
if opt.include_summary:
|
||||||
|
result["summary"] = self._getSummaryData()
|
||||||
|
if opt.include_projects:
|
||||||
|
projs = self.GetProjects(
|
||||||
|
args, all_manifests=not opt.this_manifest_only
|
||||||
|
)
|
||||||
|
result["projects"] = [self._getProjectData(p) for p in projs]
|
||||||
|
|
||||||
|
json_settings = {
|
||||||
|
# JSON style guide says Unicode characters are fully allowed.
|
||||||
|
"ensure_ascii": False,
|
||||||
|
# We use 2 space indent to match JSON style guide.
|
||||||
|
"indent": 2,
|
||||||
|
"separators": (",", ": "),
|
||||||
|
"sort_keys": True,
|
||||||
|
}
|
||||||
|
sys.stdout.write(json.dumps(result, **json_settings) + "\n")
|
||||||
|
|
||||||
|
def _printSummary(self) -> None:
|
||||||
|
"""Print manifest summary in text format."""
|
||||||
|
data = self._getSummaryData()
|
||||||
|
self.heading("Manifest branch: ")
|
||||||
|
self.headtext(data["manifest_branch"])
|
||||||
|
self.out.nl()
|
||||||
|
self.heading("Manifest merge branch: ")
|
||||||
|
self.headtext(data["manifest_merge_branch"])
|
||||||
|
self.out.nl()
|
||||||
|
self.heading("Manifest groups: ")
|
||||||
|
self.headtext(data["manifest_groups"])
|
||||||
|
self.out.nl()
|
||||||
|
self.heading("Superproject revision: ")
|
||||||
|
self.headtext(data["superproject_revision"] or "None")
|
||||||
|
self.out.nl()
|
||||||
|
self.printSeparator()
|
||||||
|
|
||||||
def printSeparator(self):
|
def printSeparator(self):
|
||||||
self.text("----------------------------")
|
self.text("----------------------------")
|
||||||
self.out.nl()
|
self.out.nl()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _DiffHelper(cls, project_idx: int, opt: Any) -> str:
|
||||||
|
"""Helper for ParallelContext to get diff info for a project."""
|
||||||
|
buf = io.StringIO()
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
|
config = cls.get_parallel_context()["config"]
|
||||||
|
|
||||||
|
out = _Coloring(config)
|
||||||
|
out.redirect(buf)
|
||||||
|
|
||||||
|
heading = out.printer("heading", attr="bold")
|
||||||
|
headtext = out.nofmt_printer("headtext", fg="yellow")
|
||||||
|
redtext = out.printer("redtext", fg="red")
|
||||||
|
sha = out.printer("sha", fg="yellow")
|
||||||
|
text = out.nofmt_printer("text")
|
||||||
|
dimtext = out.printer("dimtext", attr="dim")
|
||||||
|
|
||||||
|
heading("Project: ")
|
||||||
|
headtext(project.name)
|
||||||
|
out.nl()
|
||||||
|
|
||||||
|
heading("Mount path: ")
|
||||||
|
headtext(project.worktree)
|
||||||
|
out.nl()
|
||||||
|
|
||||||
|
heading("Current revision: ")
|
||||||
|
headtext(project.GetRevisionId())
|
||||||
|
out.nl()
|
||||||
|
|
||||||
|
currentBranch = project.CurrentBranch
|
||||||
|
if currentBranch:
|
||||||
|
heading("Current branch: ")
|
||||||
|
headtext(currentBranch)
|
||||||
|
out.nl()
|
||||||
|
|
||||||
|
heading("Manifest revision: ")
|
||||||
|
headtext(project.revisionExpr)
|
||||||
|
out.nl()
|
||||||
|
|
||||||
|
localBranches = list(project.GetBranches().keys())
|
||||||
|
heading("Local Branches: ")
|
||||||
|
redtext(str(len(localBranches)))
|
||||||
|
if localBranches:
|
||||||
|
text(" [")
|
||||||
|
text(", ".join(localBranches))
|
||||||
|
text("]")
|
||||||
|
out.nl()
|
||||||
|
|
||||||
|
if opt.all:
|
||||||
|
if not opt.local:
|
||||||
|
project.Sync_NetworkHalf(quiet=True, current_branch_only=True)
|
||||||
|
|
||||||
|
branch = project.manifest.manifestProject.config.GetBranch(
|
||||||
|
"default"
|
||||||
|
).merge
|
||||||
|
if branch.startswith(R_HEADS):
|
||||||
|
branch = branch[len(R_HEADS) :]
|
||||||
|
logTarget = R_M + branch
|
||||||
|
|
||||||
|
bareTmp = project.bare_git._bare
|
||||||
|
project.bare_git._bare = False
|
||||||
|
localCommits = project.bare_git.rev_list(
|
||||||
|
"--abbrev=8",
|
||||||
|
"--abbrev-commit",
|
||||||
|
"--pretty=oneline",
|
||||||
|
logTarget + "..",
|
||||||
|
"--",
|
||||||
|
)
|
||||||
|
|
||||||
|
originCommits = project.bare_git.rev_list(
|
||||||
|
"--abbrev=8",
|
||||||
|
"--abbrev-commit",
|
||||||
|
"--pretty=oneline",
|
||||||
|
".." + logTarget,
|
||||||
|
"--",
|
||||||
|
)
|
||||||
|
project.bare_git._bare = bareTmp
|
||||||
|
|
||||||
|
heading("Local Commits: ")
|
||||||
|
redtext(str(len(localCommits)))
|
||||||
|
dimtext(" (on current branch)")
|
||||||
|
out.nl()
|
||||||
|
|
||||||
|
for c in localCommits:
|
||||||
|
split = c.split()
|
||||||
|
sha(split[0] + " ")
|
||||||
|
text(" ".join(split[1:]))
|
||||||
|
out.nl()
|
||||||
|
|
||||||
|
text("----------------------------")
|
||||||
|
out.nl()
|
||||||
|
|
||||||
|
heading("Remote Commits: ")
|
||||||
|
redtext(str(len(originCommits)))
|
||||||
|
out.nl()
|
||||||
|
|
||||||
|
for c in originCommits:
|
||||||
|
split = c.split()
|
||||||
|
sha(split[0] + " ")
|
||||||
|
text(" ".join(split[1:]))
|
||||||
|
out.nl()
|
||||||
|
|
||||||
|
text("----------------------------")
|
||||||
|
out.nl()
|
||||||
|
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
def _printDiffInfo(self, opt, args):
|
def _printDiffInfo(self, opt, args):
|
||||||
# We let exceptions bubble up to main as they'll be well structured.
|
|
||||||
projs = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
|
projs = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
|
||||||
|
|
||||||
for p in projs:
|
def _ProcessResults(_pool, _output, results):
|
||||||
self.heading("Project: ")
|
for output in results:
|
||||||
self.headtext(p.name)
|
if output:
|
||||||
self.out.nl()
|
print(output, end="")
|
||||||
|
|
||||||
self.heading("Mount path: ")
|
with self.ParallelContext():
|
||||||
self.headtext(p.worktree)
|
self.get_parallel_context()["projects"] = projs
|
||||||
self.out.nl()
|
self.get_parallel_context()[
|
||||||
|
"config"
|
||||||
|
] = self.manifest.manifestProject.config
|
||||||
|
|
||||||
self.heading("Current revision: ")
|
self.ExecuteInParallel(
|
||||||
self.headtext(p.GetRevisionId())
|
opt.jobs,
|
||||||
self.out.nl()
|
functools.partial(self._DiffHelper, opt=opt),
|
||||||
|
range(len(projs)),
|
||||||
|
callback=_ProcessResults,
|
||||||
|
ordered=True,
|
||||||
|
chunksize=1,
|
||||||
|
)
|
||||||
|
|
||||||
currentBranch = p.CurrentBranch
|
@classmethod
|
||||||
if currentBranch:
|
def _OverviewHelper(cls, project_idx: int, opt: Any) -> List[BranchInfo]:
|
||||||
self.heading("Current branch: ")
|
"""Helper to get overview of uploadable branches."""
|
||||||
self.headtext(currentBranch)
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
self.out.nl()
|
|
||||||
|
|
||||||
self.heading("Manifest revision: ")
|
branches = []
|
||||||
self.headtext(p.revisionExpr)
|
br = [project.GetUploadableBranch(x) for x in project.GetBranches()]
|
||||||
self.out.nl()
|
br = [x for x in br if x]
|
||||||
|
if opt.current_branch:
|
||||||
|
br = [x for x in br if x.name == project.CurrentBranch]
|
||||||
|
|
||||||
localBranches = list(p.GetBranches().keys())
|
for b in br:
|
||||||
self.heading("Local Branches: ")
|
branches.append(
|
||||||
self.redtext(str(len(localBranches)))
|
BranchInfo(
|
||||||
if localBranches:
|
relpath=project.RelPath(local=opt.this_manifest_only),
|
||||||
self.text(" [")
|
name=b.name,
|
||||||
self.text(", ".join(localBranches))
|
commits=b.commits,
|
||||||
self.text("]")
|
date=b.date,
|
||||||
self.out.nl()
|
is_current=b.name == project.CurrentBranch,
|
||||||
|
)
|
||||||
if self.opt.all:
|
)
|
||||||
self.findRemoteLocalDiff(p)
|
return branches
|
||||||
|
|
||||||
self.printSeparator()
|
|
||||||
|
|
||||||
def findRemoteLocalDiff(self, project):
|
|
||||||
# Fetch all the latest commits.
|
|
||||||
if not self.opt.local:
|
|
||||||
project.Sync_NetworkHalf(quiet=True, current_branch_only=True)
|
|
||||||
|
|
||||||
branch = self.manifest.manifestProject.config.GetBranch("default").merge
|
|
||||||
if branch.startswith(R_HEADS):
|
|
||||||
branch = branch[len(R_HEADS) :]
|
|
||||||
logTarget = R_M + branch
|
|
||||||
|
|
||||||
bareTmp = project.bare_git._bare
|
|
||||||
project.bare_git._bare = False
|
|
||||||
localCommits = project.bare_git.rev_list(
|
|
||||||
"--abbrev=8",
|
|
||||||
"--abbrev-commit",
|
|
||||||
"--pretty=oneline",
|
|
||||||
logTarget + "..",
|
|
||||||
"--",
|
|
||||||
)
|
|
||||||
|
|
||||||
originCommits = project.bare_git.rev_list(
|
|
||||||
"--abbrev=8",
|
|
||||||
"--abbrev-commit",
|
|
||||||
"--pretty=oneline",
|
|
||||||
".." + logTarget,
|
|
||||||
"--",
|
|
||||||
)
|
|
||||||
project.bare_git._bare = bareTmp
|
|
||||||
|
|
||||||
self.heading("Local Commits: ")
|
|
||||||
self.redtext(str(len(localCommits)))
|
|
||||||
self.dimtext(" (on current branch)")
|
|
||||||
self.out.nl()
|
|
||||||
|
|
||||||
for c in localCommits:
|
|
||||||
split = c.split()
|
|
||||||
self.sha(split[0] + " ")
|
|
||||||
self.text(" ".join(split[1:]))
|
|
||||||
self.out.nl()
|
|
||||||
|
|
||||||
self.printSeparator()
|
|
||||||
|
|
||||||
self.heading("Remote Commits: ")
|
|
||||||
self.redtext(str(len(originCommits)))
|
|
||||||
self.out.nl()
|
|
||||||
|
|
||||||
for c in originCommits:
|
|
||||||
split = c.split()
|
|
||||||
self.sha(split[0] + " ")
|
|
||||||
self.text(" ".join(split[1:]))
|
|
||||||
self.out.nl()
|
|
||||||
|
|
||||||
def _printCommitOverview(self, opt, args):
|
def _printCommitOverview(self, opt, args):
|
||||||
|
projs = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
|
||||||
|
|
||||||
all_branches = []
|
all_branches = []
|
||||||
for project in self.GetProjects(
|
|
||||||
args, all_manifests=not opt.this_manifest_only
|
def _ProcessResults(_pool, _output, results):
|
||||||
):
|
for branches in results:
|
||||||
br = [project.GetUploadableBranch(x) for x in project.GetBranches()]
|
all_branches.extend(branches)
|
||||||
br = [x for x in br if x]
|
|
||||||
if self.opt.current_branch:
|
with self.ParallelContext():
|
||||||
br = [x for x in br if x.name == project.CurrentBranch]
|
self.get_parallel_context()["projects"] = projs
|
||||||
all_branches.extend(br)
|
|
||||||
|
self.ExecuteInParallel(
|
||||||
|
opt.jobs,
|
||||||
|
functools.partial(self._OverviewHelper, opt=opt),
|
||||||
|
range(len(projs)),
|
||||||
|
callback=_ProcessResults,
|
||||||
|
ordered=True,
|
||||||
|
chunksize=1,
|
||||||
|
)
|
||||||
|
|
||||||
if not all_branches:
|
if not all_branches:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.out.nl()
|
self.out.nl()
|
||||||
self.heading("Projects Overview")
|
self.heading("Projects Overview")
|
||||||
project = None
|
current_relpath = None
|
||||||
|
|
||||||
for branch in all_branches:
|
for branch in all_branches:
|
||||||
if project != branch.project:
|
if current_relpath != branch.relpath:
|
||||||
project = branch.project
|
current_relpath = branch.relpath
|
||||||
self.out.nl()
|
self.out.nl()
|
||||||
self.headtext(project.RelPath(local=opt.this_manifest_only))
|
self.headtext(current_relpath)
|
||||||
self.out.nl()
|
self.out.nl()
|
||||||
|
|
||||||
commits = branch.commits
|
commits = branch.commits
|
||||||
@@ -237,7 +440,7 @@ class Info(PagedCommand):
|
|||||||
self.text(
|
self.text(
|
||||||
"%s %-33s (%2d commit%s, %s)"
|
"%s %-33s (%2d commit%s, %s)"
|
||||||
% (
|
% (
|
||||||
branch.name == project.CurrentBranch and "*" or " ",
|
branch.is_current and "*" or " ",
|
||||||
branch.name,
|
branch.name,
|
||||||
len(commits),
|
len(commits),
|
||||||
len(commits) != 1 and "s" or "",
|
len(commits) != 1 and "s" or "",
|
||||||
@@ -248,7 +451,7 @@ class Info(PagedCommand):
|
|||||||
|
|
||||||
for commit in commits:
|
for commit in commits:
|
||||||
split = commit.split()
|
split = commit.split()
|
||||||
self.text("{0:38}{1} ".format("", "-"))
|
self.text(f"{'':38}{'-'} ")
|
||||||
self.sha(split[0] + " ")
|
self.sha(split[0] + " ")
|
||||||
self.text(" ".join(split[1:]))
|
self.text(" ".join(split[1:]))
|
||||||
self.out.nl()
|
self.out.nl()
|
||||||
|
|||||||
+30
-16
@@ -21,11 +21,13 @@ from command import MirrorSafeCommand
|
|||||||
from error import RepoUnhandledExceptionError
|
from error import RepoUnhandledExceptionError
|
||||||
from error import UpdateManifestError
|
from error import UpdateManifestError
|
||||||
from git_command import git_require
|
from git_command import git_require
|
||||||
from git_command import MIN_GIT_VERSION_HARD
|
from repo_logging import RepoLogger
|
||||||
from git_command import MIN_GIT_VERSION_SOFT
|
|
||||||
from wrapper import Wrapper
|
from wrapper import Wrapper
|
||||||
|
from wrapper import WrapperDir
|
||||||
|
|
||||||
|
|
||||||
|
logger = RepoLogger(__file__)
|
||||||
|
|
||||||
_REPO_ALLOW_SHALLOW = os.environ.get("REPO_ALLOW_SHALLOW")
|
_REPO_ALLOW_SHALLOW = os.environ.get("REPO_ALLOW_SHALLOW")
|
||||||
|
|
||||||
|
|
||||||
@@ -50,6 +52,10 @@ The optional -b argument can be used to select the manifest branch
|
|||||||
to checkout and use. If no branch is specified, the remote's default
|
to checkout and use. If no branch is specified, the remote's default
|
||||||
branch is used. This is equivalent to using -b HEAD.
|
branch is used. This is equivalent to using -b HEAD.
|
||||||
|
|
||||||
|
The optional --manifest-upstream-branch argument can be used when a commit is
|
||||||
|
provided to --manifest-branch (or -b), to specify the name of the git ref in
|
||||||
|
which the commit can be found.
|
||||||
|
|
||||||
The optional -m argument can be used to specify an alternate manifest
|
The optional -m argument can be used to specify an alternate manifest
|
||||||
to be used. If no manifest is specified, the manifest default.xml
|
to be used. If no manifest is specified, the manifest default.xml
|
||||||
will be used.
|
will be used.
|
||||||
@@ -121,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):
|
||||||
@@ -133,6 +140,7 @@ to update the working directory files.
|
|||||||
# manifest project is special and is created when instantiating the
|
# manifest project is special and is created when instantiating the
|
||||||
# manifest which happens before we parse options.
|
# manifest which happens before we parse options.
|
||||||
self.manifest.manifestProject.clone_depth = opt.manifest_depth
|
self.manifest.manifestProject.clone_depth = opt.manifest_depth
|
||||||
|
self.manifest.manifestProject.upstream = opt.manifest_upstream_branch
|
||||||
clone_filter_for_depth = (
|
clone_filter_for_depth = (
|
||||||
"blob:none" if (_REPO_ALLOW_SHALLOW == "0") else None
|
"blob:none" if (_REPO_ALLOW_SHALLOW == "0") else None
|
||||||
)
|
)
|
||||||
@@ -212,7 +220,7 @@ to update the working directory files.
|
|||||||
|
|
||||||
if not opt.quiet:
|
if not opt.quiet:
|
||||||
print()
|
print()
|
||||||
print("Your identity is: %s <%s>" % (name, email))
|
print(f"Your identity is: {name} <{email}>")
|
||||||
print("is this correct [y/N]? ", end="", flush=True)
|
print("is this correct [y/N]? ", end="", flush=True)
|
||||||
a = sys.stdin.readline().strip().lower()
|
a = sys.stdin.readline().strip().lower()
|
||||||
if a in ("yes", "y", "t", "true"):
|
if a in ("yes", "y", "t", "true"):
|
||||||
@@ -315,6 +323,12 @@ to update the working directory files.
|
|||||||
" be used with --standalone-manifest."
|
" be used with --standalone-manifest."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if opt.manifest_upstream_branch and opt.manifest_branch is None:
|
||||||
|
self.OptionParser.error(
|
||||||
|
"--manifest-upstream-branch cannot be used without "
|
||||||
|
"--manifest-branch."
|
||||||
|
)
|
||||||
|
|
||||||
if args:
|
if args:
|
||||||
if opt.manifest_url:
|
if opt.manifest_url:
|
||||||
self.OptionParser.error(
|
self.OptionParser.error(
|
||||||
@@ -328,13 +342,17 @@ to update the working directory files.
|
|||||||
self.OptionParser.error("too many arguments to init")
|
self.OptionParser.error("too many arguments to init")
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
git_require(MIN_GIT_VERSION_HARD, fail=True)
|
wrapper = Wrapper()
|
||||||
if not git_require(MIN_GIT_VERSION_SOFT):
|
|
||||||
print(
|
reqs = wrapper.Requirements.from_dir(WrapperDir())
|
||||||
"repo: warning: git-%s+ will soon be required; please upgrade "
|
git_require(reqs.get_hard_ver("git"), fail=True)
|
||||||
"your version of git to maintain support."
|
min_git_version_soft = reqs.get_soft_ver("git")
|
||||||
% (".".join(str(x) for x in MIN_GIT_VERSION_SOFT),),
|
if not git_require(min_git_version_soft):
|
||||||
file=sys.stderr,
|
logger.warning(
|
||||||
|
"repo: warning: git-%s+ will soon be required; "
|
||||||
|
"please upgrade your version of git to maintain "
|
||||||
|
"support.",
|
||||||
|
".".join(str(x) for x in min_git_version_soft),
|
||||||
)
|
)
|
||||||
|
|
||||||
rp = self.manifest.repoProject
|
rp = self.manifest.repoProject
|
||||||
@@ -347,20 +365,16 @@ to update the working directory files.
|
|||||||
|
|
||||||
# Handle new --repo-rev requests.
|
# Handle new --repo-rev requests.
|
||||||
if opt.repo_rev:
|
if opt.repo_rev:
|
||||||
wrapper = Wrapper()
|
|
||||||
try:
|
try:
|
||||||
remote_ref, rev = wrapper.check_repo_rev(
|
remote_ref, rev = wrapper.check_repo_rev(
|
||||||
rp.gitdir,
|
rp.worktree,
|
||||||
opt.repo_rev,
|
opt.repo_rev,
|
||||||
repo_verify=opt.repo_verify,
|
repo_verify=opt.repo_verify,
|
||||||
quiet=opt.quiet,
|
quiet=opt.quiet,
|
||||||
)
|
)
|
||||||
except wrapper.CloneFailure as e:
|
except wrapper.CloneFailure as e:
|
||||||
err_msg = "fatal: double check your --repo-rev setting."
|
err_msg = "fatal: double check your --repo-rev setting."
|
||||||
print(
|
logger.error(err_msg)
|
||||||
err_msg,
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
self.git_event_log.ErrorEvent(err_msg)
|
self.git_event_log.ErrorEvent(err_msg)
|
||||||
raise RepoUnhandledExceptionError(e)
|
raise RepoUnhandledExceptionError(e)
|
||||||
|
|
||||||
|
|||||||
+1
-6
@@ -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",
|
||||||
)
|
)
|
||||||
@@ -131,7 +126,7 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
|
|||||||
elif opt.path_only and not opt.name_only:
|
elif opt.path_only and not opt.name_only:
|
||||||
lines.append("%s" % (_getpath(project)))
|
lines.append("%s" % (_getpath(project)))
|
||||||
else:
|
else:
|
||||||
lines.append("%s : %s" % (_getpath(project), project.name))
|
lines.append(f"{_getpath(project)} : {project.name}")
|
||||||
|
|
||||||
if lines:
|
if lines:
|
||||||
lines.sort()
|
lines.sort()
|
||||||
|
|||||||
+42
-12
@@ -12,11 +12,27 @@
|
|||||||
# 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
|
||||||
|
|
||||||
from command import PagedCommand
|
from command import PagedCommand
|
||||||
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
|
|
||||||
|
logger = RepoLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
class OutputFormat(enum.Enum):
|
||||||
|
"""Type for the requested output format."""
|
||||||
|
|
||||||
|
# Canonicalized manifest in XML format.
|
||||||
|
XML = enum.auto()
|
||||||
|
|
||||||
|
# Canonicalized manifest in JSON format.
|
||||||
|
JSON = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
class Manifest(PagedCommand):
|
class Manifest(PagedCommand):
|
||||||
@@ -38,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
|
||||||
@@ -82,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",
|
||||||
@@ -104,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.)",
|
||||||
@@ -117,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 == "-":
|
||||||
@@ -131,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:
|
||||||
print("warning: --json is experimental!", file=sys.stderr)
|
|
||||||
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,
|
||||||
@@ -148,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,
|
||||||
@@ -159,13 +189,13 @@ to indicate the remote ref to push changes to via 'repo upload'.
|
|||||||
if output_file != "-":
|
if output_file != "-":
|
||||||
fd.close()
|
fd.close()
|
||||||
if manifest.path_prefix:
|
if manifest.path_prefix:
|
||||||
print(
|
logger.warning(
|
||||||
f"Saved {manifest.path_prefix} submanifest to "
|
"Saved %s submanifest to %s",
|
||||||
f"{output_file}",
|
manifest.path_prefix,
|
||||||
file=sys.stderr,
|
output_file,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(f"Saved manifest to {output_file}", file=sys.stderr)
|
logger.warning("Saved manifest to %s", output_file)
|
||||||
|
|
||||||
def ValidateOptions(self, opt, args):
|
def ValidateOptions(self, opt, args):
|
||||||
if args:
|
if args:
|
||||||
|
|||||||
@@ -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",
|
||||||
)
|
)
|
||||||
|
|||||||
+13
-11
@@ -27,8 +27,10 @@ class Prune(PagedCommand):
|
|||||||
"""
|
"""
|
||||||
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
|
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
|
||||||
|
|
||||||
def _ExecuteOne(self, project):
|
@classmethod
|
||||||
|
def _ExecuteOne(cls, project_idx):
|
||||||
"""Process one project."""
|
"""Process one project."""
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
return project.PruneHeads()
|
return project.PruneHeads()
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
@@ -41,13 +43,15 @@ class Prune(PagedCommand):
|
|||||||
def _ProcessResults(_pool, _output, results):
|
def _ProcessResults(_pool, _output, results):
|
||||||
return list(itertools.chain.from_iterable(results))
|
return list(itertools.chain.from_iterable(results))
|
||||||
|
|
||||||
all_branches = self.ExecuteInParallel(
|
with self.ParallelContext():
|
||||||
opt.jobs,
|
self.get_parallel_context()["projects"] = projects
|
||||||
self._ExecuteOne,
|
all_branches = self.ExecuteInParallel(
|
||||||
projects,
|
opt.jobs,
|
||||||
callback=_ProcessResults,
|
self._ExecuteOne,
|
||||||
ordered=True,
|
range(len(projects)),
|
||||||
)
|
callback=_ProcessResults,
|
||||||
|
ordered=True,
|
||||||
|
)
|
||||||
|
|
||||||
if not all_branches:
|
if not all_branches:
|
||||||
return
|
return
|
||||||
@@ -83,9 +87,7 @@ class Prune(PagedCommand):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not branch.base_exists:
|
if not branch.base_exists:
|
||||||
print(
|
print(f"(ignoring: tracking branch is gone: {branch.base})")
|
||||||
"(ignoring: tracking branch is gone: %s)" % (branch.base,)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
commits = branch.commits
|
commits = branch.commits
|
||||||
date = branch.date
|
date = branch.date
|
||||||
|
|||||||
+16
-23
@@ -17,6 +17,10 @@ import sys
|
|||||||
from color import Coloring
|
from color import Coloring
|
||||||
from command import Command
|
from command import Command
|
||||||
from git_command import GitCommand
|
from git_command import GitCommand
|
||||||
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
|
|
||||||
|
logger = RepoLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class RebaseColoring(Coloring):
|
class RebaseColoring(Coloring):
|
||||||
@@ -43,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",
|
||||||
)
|
)
|
||||||
@@ -70,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 "
|
||||||
@@ -104,17 +101,15 @@ branch but need to incorporate new upstream changes "underneath" them.
|
|||||||
one_project = len(all_projects) == 1
|
one_project = len(all_projects) == 1
|
||||||
|
|
||||||
if opt.interactive and not one_project:
|
if opt.interactive and not one_project:
|
||||||
print(
|
logger.error(
|
||||||
"error: interactive rebase not supported with multiple "
|
"error: interactive rebase not supported with multiple projects"
|
||||||
"projects",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(args) == 1:
|
if len(args) == 1:
|
||||||
print(
|
logger.warning(
|
||||||
"note: project %s is mapped to more than one path"
|
"note: project %s is mapped to more than one path", args[0]
|
||||||
% (args[0],),
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# Setup the common git rebase args that we use for all projects.
|
# Setup the common git rebase args that we use for all projects.
|
||||||
@@ -145,10 +140,9 @@ branch but need to incorporate new upstream changes "underneath" them.
|
|||||||
cb = project.CurrentBranch
|
cb = project.CurrentBranch
|
||||||
if not cb:
|
if not cb:
|
||||||
if one_project:
|
if one_project:
|
||||||
print(
|
logger.error(
|
||||||
"error: project %s has a detached HEAD"
|
"error: project %s has a detached HEAD",
|
||||||
% _RelPath(project),
|
_RelPath(project),
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
return 1
|
return 1
|
||||||
# Ignore branches with detached HEADs.
|
# Ignore branches with detached HEADs.
|
||||||
@@ -157,10 +151,9 @@ branch but need to incorporate new upstream changes "underneath" them.
|
|||||||
upbranch = project.GetBranch(cb)
|
upbranch = project.GetBranch(cb)
|
||||||
if not upbranch.LocalMerge:
|
if not upbranch.LocalMerge:
|
||||||
if one_project:
|
if one_project:
|
||||||
print(
|
logger.error(
|
||||||
"error: project %s does not track any remote branches"
|
"error: project %s does not track any remote branches",
|
||||||
% _RelPath(project),
|
_RelPath(project),
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
return 1
|
return 1
|
||||||
# Ignore branches without remotes.
|
# Ignore branches without remotes.
|
||||||
|
|||||||
@@ -13,15 +13,18 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import optparse
|
import optparse
|
||||||
import sys
|
|
||||||
|
|
||||||
from command import Command
|
from command import Command
|
||||||
from command import MirrorSafeCommand
|
from command import MirrorSafeCommand
|
||||||
from error import RepoExitError
|
from error import RepoExitError
|
||||||
|
from repo_logging import RepoLogger
|
||||||
from subcmds.sync import _PostRepoFetch
|
from subcmds.sync import _PostRepoFetch
|
||||||
from subcmds.sync import _PostRepoUpgrade
|
from subcmds.sync import _PostRepoUpgrade
|
||||||
|
|
||||||
|
|
||||||
|
logger = RepoLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class SelfupdateError(RepoExitError):
|
class SelfupdateError(RepoExitError):
|
||||||
"""Exit error for failed selfupdate command."""
|
"""Exit error for failed selfupdate command."""
|
||||||
|
|
||||||
@@ -51,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,
|
||||||
)
|
)
|
||||||
@@ -66,7 +68,7 @@ need to be performed by an end-user.
|
|||||||
else:
|
else:
|
||||||
result = rp.Sync_NetworkHalf()
|
result = rp.Sync_NetworkHalf()
|
||||||
if result.error:
|
if result.error:
|
||||||
print("error: can't update repo", file=sys.stderr)
|
logger.error("error: can't update repo")
|
||||||
raise SelfupdateError(aggregate_errors=[result.error])
|
raise SelfupdateError(aggregate_errors=[result.error])
|
||||||
|
|
||||||
rp.bare_git.gc("--auto")
|
rp.bare_git.gc("--auto")
|
||||||
|
|||||||
+5
-2
@@ -17,6 +17,10 @@ import sys
|
|||||||
from color import Coloring
|
from color import Coloring
|
||||||
from command import InteractiveCommand
|
from command import InteractiveCommand
|
||||||
from git_command import GitCommand
|
from git_command import GitCommand
|
||||||
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
|
|
||||||
|
logger = RepoLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class _ProjectList(Coloring):
|
class _ProjectList(Coloring):
|
||||||
@@ -42,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",
|
||||||
)
|
)
|
||||||
@@ -62,7 +65,7 @@ The '%prog' command stages files to prepare the next commit.
|
|||||||
if p.IsDirty()
|
if p.IsDirty()
|
||||||
]
|
]
|
||||||
if not all_projects:
|
if not all_projects:
|
||||||
print("no projects have uncommitted modifications", file=sys.stderr)
|
logger.error("no projects have uncommitted modifications")
|
||||||
return
|
return
|
||||||
|
|
||||||
out = _ProjectList(self.manifest.manifestProject.config)
|
out = _ProjectList(self.manifest.manifestProject.config)
|
||||||
|
|||||||
+35
-25
@@ -13,7 +13,6 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import sys
|
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
from command import Command
|
from command import Command
|
||||||
@@ -22,11 +21,14 @@ from error import RepoExitError
|
|||||||
from git_command import git
|
from git_command import git
|
||||||
from git_config import IsImmutable
|
from git_config import IsImmutable
|
||||||
from progress import Progress
|
from progress import Progress
|
||||||
from project import Project
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
|
|
||||||
|
logger = RepoLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class ExecuteOneResult(NamedTuple):
|
class ExecuteOneResult(NamedTuple):
|
||||||
project: Project
|
project_idx: int
|
||||||
error: Exception
|
error: Exception
|
||||||
|
|
||||||
|
|
||||||
@@ -49,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",
|
||||||
)
|
)
|
||||||
@@ -77,33 +78,33 @@ revision specified in the manifest.
|
|||||||
if not git.check_ref_format("heads/%s" % nb):
|
if not git.check_ref_format("heads/%s" % nb):
|
||||||
self.OptionParser.error("'%s' is not a valid name" % nb)
|
self.OptionParser.error("'%s' is not a valid name" % nb)
|
||||||
|
|
||||||
def _ExecuteOne(self, revision, nb, project):
|
@classmethod
|
||||||
|
def _ExecuteOne(cls, revision, nb, default_revisionExpr, project_idx):
|
||||||
"""Start one project."""
|
"""Start one project."""
|
||||||
# If the current revision is immutable, such as a SHA1, a tag or
|
# If the current revision is immutable, such as a SHA1, a tag or
|
||||||
# a change, then we can't push back to it. Substitute with
|
# a change, then we can't push back to it. Substitute with
|
||||||
# dest_branch, if defined; or with manifest default revision instead.
|
# dest_branch, if defined; or with manifest default revision instead.
|
||||||
branch_merge = ""
|
branch_merge = ""
|
||||||
error = None
|
error = None
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
if IsImmutable(project.revisionExpr):
|
if IsImmutable(project.revisionExpr):
|
||||||
if project.dest_branch:
|
if project.dest_branch:
|
||||||
branch_merge = project.dest_branch
|
branch_merge = project.dest_branch
|
||||||
else:
|
else:
|
||||||
branch_merge = self.manifest.default.revisionExpr
|
branch_merge = default_revisionExpr
|
||||||
|
|
||||||
try:
|
try:
|
||||||
project.StartBranch(
|
project.StartBranch(
|
||||||
nb, branch_merge=branch_merge, revision=revision
|
nb, branch_merge=branch_merge, revision=revision
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(
|
logger.error("error: unable to checkout %s: %s", project.name, e)
|
||||||
"error: unable to checkout %s: %s" % (project.name, e),
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
error = e
|
error = e
|
||||||
return ExecuteOneResult(project, error)
|
return ExecuteOneResult(project_idx, error)
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
nb = args[0]
|
nb = args[0]
|
||||||
|
self.TryOverrideManifestWithSmartSync()
|
||||||
err_projects = []
|
err_projects = []
|
||||||
err = []
|
err = []
|
||||||
projects = []
|
projects = []
|
||||||
@@ -120,26 +121,35 @@ revision specified in the manifest.
|
|||||||
def _ProcessResults(_pool, pm, results):
|
def _ProcessResults(_pool, pm, results):
|
||||||
for result in results:
|
for result in results:
|
||||||
if result.error:
|
if result.error:
|
||||||
err_projects.append(result.project)
|
project = all_projects[result.project_idx]
|
||||||
|
err_projects.append(project)
|
||||||
err.append(result.error)
|
err.append(result.error)
|
||||||
pm.update(msg="")
|
pm.update(msg="")
|
||||||
|
|
||||||
self.ExecuteInParallel(
|
with self.ParallelContext():
|
||||||
opt.jobs,
|
self.get_parallel_context()["projects"] = all_projects
|
||||||
functools.partial(self._ExecuteOne, opt.revision, nb),
|
self.ExecuteInParallel(
|
||||||
all_projects,
|
opt.jobs,
|
||||||
callback=_ProcessResults,
|
functools.partial(
|
||||||
output=Progress(
|
self._ExecuteOne,
|
||||||
"Starting %s" % (nb,), len(all_projects), quiet=opt.quiet
|
opt.revision,
|
||||||
),
|
nb,
|
||||||
)
|
self.manifest.default.revisionExpr,
|
||||||
|
),
|
||||||
|
range(len(all_projects)),
|
||||||
|
callback=_ProcessResults,
|
||||||
|
output=Progress(
|
||||||
|
f"Starting {nb}", len(all_projects), quiet=opt.quiet
|
||||||
|
),
|
||||||
|
chunksize=1,
|
||||||
|
)
|
||||||
|
|
||||||
if err_projects:
|
if err_projects:
|
||||||
for p in err_projects:
|
for p in err_projects:
|
||||||
print(
|
logger.error(
|
||||||
"error: %s/: cannot start %s"
|
"error: %s/: cannot start %s",
|
||||||
% (p.RelPath(local=opt.this_manifest_only), nb),
|
p.RelPath(local=opt.this_manifest_only),
|
||||||
file=sys.stderr,
|
nb,
|
||||||
)
|
)
|
||||||
msg_fmt = "cannot start %d project(s)"
|
msg_fmt = "cannot start %d project(s)"
|
||||||
self.git_event_log.ErrorEvent(
|
self.git_event_log.ErrorEvent(
|
||||||
|
|||||||
+16
-12
@@ -82,13 +82,13 @@ the following meanings:
|
|||||||
p.add_option(
|
p.add_option(
|
||||||
"-o",
|
"-o",
|
||||||
"--orphans",
|
"--orphans",
|
||||||
dest="orphans",
|
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="include objects in working directory outside of repo "
|
help="include objects in working directory outside of repo "
|
||||||
"projects",
|
"projects",
|
||||||
)
|
)
|
||||||
|
|
||||||
def _StatusHelper(self, quiet, local, project):
|
@classmethod
|
||||||
|
def _StatusHelper(cls, quiet, local, project_idx):
|
||||||
"""Obtains the status for a specific project.
|
"""Obtains the status for a specific project.
|
||||||
|
|
||||||
Obtains the status for a project, redirecting the output to
|
Obtains the status for a project, redirecting the output to
|
||||||
@@ -99,12 +99,13 @@ the following meanings:
|
|||||||
local: a boolean, if True, the path is relative to the local
|
local: a boolean, if True, the path is relative to the local
|
||||||
(sub)manifest. If false, the path is relative to the outermost
|
(sub)manifest. If false, the path is relative to the outermost
|
||||||
manifest.
|
manifest.
|
||||||
project: Project to get status of.
|
project_idx: Project index to get status of.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The status of the project.
|
The status of the project.
|
||||||
"""
|
"""
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
ret = project.PrintWorkTreeStatus(
|
ret = project.PrintWorkTreeStatus(
|
||||||
quiet=quiet, output_redir=buf, local=local
|
quiet=quiet, output_redir=buf, local=local
|
||||||
)
|
)
|
||||||
@@ -143,15 +144,18 @@ the following meanings:
|
|||||||
ret += 1
|
ret += 1
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
counter = self.ExecuteInParallel(
|
with self.ParallelContext():
|
||||||
opt.jobs,
|
self.get_parallel_context()["projects"] = all_projects
|
||||||
functools.partial(
|
counter = self.ExecuteInParallel(
|
||||||
self._StatusHelper, opt.quiet, opt.this_manifest_only
|
opt.jobs,
|
||||||
),
|
functools.partial(
|
||||||
all_projects,
|
self._StatusHelper, opt.quiet, opt.this_manifest_only
|
||||||
callback=_ProcessResults,
|
),
|
||||||
ordered=True,
|
range(len(all_projects)),
|
||||||
)
|
callback=_ProcessResults,
|
||||||
|
ordered=True,
|
||||||
|
chunksize=1,
|
||||||
|
)
|
||||||
|
|
||||||
if not opt.quiet and len(all_projects) == counter:
|
if not opt.quiet and len(all_projects) == counter:
|
||||||
print("nothing to commit (working directory clean)")
|
print("nothing to commit (working directory clean)")
|
||||||
|
|||||||
+1497
-446
File diff suppressed because it is too large
Load Diff
+71
-77
@@ -27,12 +27,15 @@ 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 subcmds.sync import LocalSyncState
|
from subcmds.sync import LocalSyncState
|
||||||
|
|
||||||
|
|
||||||
_DEFAULT_UNUSUAL_COMMIT_THRESHOLD = 5
|
_DEFAULT_UNUSUAL_COMMIT_THRESHOLD = 5
|
||||||
|
logger = RepoLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class UploadExitError(SilentRepoExitError):
|
class UploadExitError(SilentRepoExitError):
|
||||||
@@ -70,16 +73,16 @@ def _VerifyPendingCommits(branches: List[ReviewableBranch]) -> bool:
|
|||||||
# If any branch has many commits, prompt the user.
|
# If any branch has many commits, prompt the user.
|
||||||
if many_commits:
|
if many_commits:
|
||||||
if len(branches) > 1:
|
if len(branches) > 1:
|
||||||
print(
|
logger.warning(
|
||||||
"ATTENTION: One or more branches has an unusually high number "
|
"ATTENTION: One or more branches has an unusually high number "
|
||||||
"of commits."
|
"of commits."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print(
|
logger.warning(
|
||||||
"ATTENTION: You are uploading an unusually high number of "
|
"ATTENTION: You are uploading an unusually high number of "
|
||||||
"commits."
|
"commits."
|
||||||
)
|
)
|
||||||
print(
|
logger.warning(
|
||||||
"YOU PROBABLY DO NOT MEAN TO DO THIS. (Did you rebase across "
|
"YOU PROBABLY DO NOT MEAN TO DO THIS. (Did you rebase across "
|
||||||
"branches?)"
|
"branches?)"
|
||||||
)
|
)
|
||||||
@@ -93,7 +96,7 @@ def _VerifyPendingCommits(branches: List[ReviewableBranch]) -> bool:
|
|||||||
|
|
||||||
def _die(fmt, *args):
|
def _die(fmt, *args):
|
||||||
msg = fmt % args
|
msg = fmt % args
|
||||||
print("error: %s" % msg, file=sys.stderr)
|
logger.error("error: %s", msg)
|
||||||
raise UploadExitError(msg)
|
raise UploadExitError(msg)
|
||||||
|
|
||||||
|
|
||||||
@@ -216,9 +219,14 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
def _Options(self, p):
|
def _Options(self, p):
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"-t",
|
"-t",
|
||||||
|
"--topic-branch",
|
||||||
dest="auto_topic",
|
dest="auto_topic",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="send local branch name to Gerrit Code Review",
|
help="set the topic to the local branch name",
|
||||||
|
)
|
||||||
|
p.add_option(
|
||||||
|
"--topic",
|
||||||
|
help="set topic for the change",
|
||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--hashtag",
|
"--hashtag",
|
||||||
@@ -242,6 +250,12 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
default=[],
|
default=[],
|
||||||
help="add a label when uploading",
|
help="add a label when uploading",
|
||||||
)
|
)
|
||||||
|
p.add_option(
|
||||||
|
"--pd",
|
||||||
|
"--patchset-description",
|
||||||
|
dest="patchset_description",
|
||||||
|
help="description for patchset",
|
||||||
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--re",
|
"--re",
|
||||||
"--reviewers",
|
"--reviewers",
|
||||||
@@ -254,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(
|
||||||
@@ -268,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",
|
||||||
)
|
)
|
||||||
@@ -297,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)",
|
||||||
)
|
)
|
||||||
@@ -305,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",
|
||||||
)
|
)
|
||||||
@@ -541,42 +551,14 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
people = copy.deepcopy(original_people)
|
people = copy.deepcopy(original_people)
|
||||||
self._AppendAutoList(branch, people)
|
self._AppendAutoList(branch, people)
|
||||||
|
|
||||||
# Check if there are local changes that may have been forgotten.
|
|
||||||
changes = branch.project.UncommitedFiles()
|
|
||||||
if opt.ignore_untracked_files:
|
|
||||||
untracked = set(branch.project.UntrackedFiles())
|
|
||||||
changes = [x for x in changes if x not in untracked]
|
|
||||||
|
|
||||||
if changes:
|
|
||||||
key = "review.%s.autoupload" % branch.project.remote.review
|
|
||||||
answer = branch.project.config.GetBoolean(key)
|
|
||||||
|
|
||||||
# If they want to auto upload, let's not ask because it
|
|
||||||
# could be automated.
|
|
||||||
if answer is None:
|
|
||||||
print()
|
|
||||||
print(
|
|
||||||
"Uncommitted changes in %s (did you forget to "
|
|
||||||
"amend?):" % branch.project.name
|
|
||||||
)
|
|
||||||
print("\n".join(changes))
|
|
||||||
print("Continue uploading? (y/N) ", end="", flush=True)
|
|
||||||
if opt.yes:
|
|
||||||
print("<--yes>")
|
|
||||||
a = "yes"
|
|
||||||
else:
|
|
||||||
a = sys.stdin.readline().strip().lower()
|
|
||||||
if a not in ("y", "yes", "t", "true", "on"):
|
|
||||||
print("skipping upload", file=sys.stderr)
|
|
||||||
branch.uploaded = False
|
|
||||||
branch.error = "User aborted"
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check if topic branches should be sent to the server during
|
# Check if topic branches should be sent to the server during
|
||||||
# upload.
|
# upload.
|
||||||
if opt.auto_topic is not True:
|
if opt.topic is None:
|
||||||
key = "review.%s.uploadtopic" % branch.project.remote.review
|
if opt.auto_topic is not True:
|
||||||
opt.auto_topic = branch.project.config.GetBoolean(key)
|
key = "review.%s.uploadtopic" % branch.project.remote.review
|
||||||
|
opt.auto_topic = branch.project.config.GetBoolean(key)
|
||||||
|
if opt.auto_topic:
|
||||||
|
opt.topic = branch.name
|
||||||
|
|
||||||
def _ExpandCommaList(value):
|
def _ExpandCommaList(value):
|
||||||
"""Split |value| up into comma delimited entries."""
|
"""Split |value| up into comma delimited entries."""
|
||||||
@@ -618,19 +600,22 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
full_dest = destination
|
full_dest = destination
|
||||||
if not full_dest.startswith(R_HEADS):
|
if not full_dest.startswith(R_HEADS):
|
||||||
full_dest = R_HEADS + full_dest
|
full_dest = R_HEADS + full_dest
|
||||||
|
full_revision = branch.project.revisionExpr
|
||||||
|
if not full_revision.startswith(R_HEADS):
|
||||||
|
full_revision = R_HEADS + full_revision
|
||||||
|
|
||||||
# If the merge branch of the local branch is different from
|
# If the merge branch of the local branch is different from
|
||||||
# the project's revision AND destination, this might not be
|
# the project's revision AND destination, this might not be
|
||||||
# intentional.
|
# intentional.
|
||||||
if (
|
if (
|
||||||
merge_branch
|
merge_branch
|
||||||
and merge_branch != branch.project.revisionExpr
|
and merge_branch != full_revision
|
||||||
and merge_branch != full_dest
|
and merge_branch != full_dest
|
||||||
):
|
):
|
||||||
print(
|
print(
|
||||||
f"For local branch {branch.name}: merge branch "
|
f"For local branch {branch.name}: merge branch "
|
||||||
f"{merge_branch} does not match destination branch "
|
f"{merge_branch} does not match destination branch "
|
||||||
f"{destination}"
|
f"{destination} and revision {branch.project.revisionExpr}"
|
||||||
)
|
)
|
||||||
print("skipping upload.")
|
print("skipping upload.")
|
||||||
print(
|
print(
|
||||||
@@ -640,10 +625,20 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
branch.uploaded = False
|
branch.uploaded = False
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# If using superproject, add the root repo as a push option.
|
||||||
|
manifest = branch.project.manifest
|
||||||
|
push_options = list(opt.push_options)
|
||||||
|
if git_superproject.UseSuperproject(None, manifest):
|
||||||
|
sp = manifest.superproject
|
||||||
|
if sp:
|
||||||
|
r_id = sp.repo_id
|
||||||
|
if r_id:
|
||||||
|
push_options.append(f"custom-keyed-value=rootRepo:{r_id}")
|
||||||
|
|
||||||
branch.UploadForReview(
|
branch.UploadForReview(
|
||||||
people,
|
people,
|
||||||
dryrun=opt.dryrun,
|
dryrun=opt.dryrun,
|
||||||
auto_topic=opt.auto_topic,
|
topic=opt.topic,
|
||||||
hashtags=hashtags,
|
hashtags=hashtags,
|
||||||
labels=labels,
|
labels=labels,
|
||||||
private=opt.private,
|
private=opt.private,
|
||||||
@@ -652,7 +647,8 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
ready=opt.ready,
|
ready=opt.ready,
|
||||||
dest_branch=destination,
|
dest_branch=destination,
|
||||||
validate_certs=opt.validate_certs,
|
validate_certs=opt.validate_certs,
|
||||||
push_options=opt.push_options,
|
push_options=push_options,
|
||||||
|
patchset_description=opt.patchset_description,
|
||||||
)
|
)
|
||||||
|
|
||||||
branch.uploaded = True
|
branch.uploaded = True
|
||||||
@@ -727,16 +723,17 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
merge_branch = p.stdout.strip()
|
merge_branch = p.stdout.strip()
|
||||||
return merge_branch
|
return merge_branch
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def _GatherOne(opt, project):
|
def _GatherOne(cls, opt, project_idx):
|
||||||
"""Figure out the upload status for |project|."""
|
"""Figure out the upload status for |project|."""
|
||||||
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
if opt.current_branch:
|
if opt.current_branch:
|
||||||
cbr = project.CurrentBranch
|
cbr = project.CurrentBranch
|
||||||
up_branch = project.GetUploadableBranch(cbr)
|
up_branch = project.GetUploadableBranch(cbr)
|
||||||
avail = [up_branch] if up_branch else None
|
avail = [up_branch] if up_branch else None
|
||||||
else:
|
else:
|
||||||
avail = project.GetUploadableBranches(opt.branch)
|
avail = project.GetUploadableBranches(opt.branch)
|
||||||
return (project, avail)
|
return (project_idx, avail)
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
projects = self.GetProjects(
|
projects = self.GetProjects(
|
||||||
@@ -746,40 +743,37 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
def _ProcessResults(_pool, _out, results):
|
def _ProcessResults(_pool, _out, results):
|
||||||
pending = []
|
pending = []
|
||||||
for result in results:
|
for result in results:
|
||||||
project, avail = result
|
project_idx, avail = result
|
||||||
|
project = projects[project_idx]
|
||||||
if avail is None:
|
if avail is None:
|
||||||
print(
|
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"
|
||||||
" git branch --set-upstream-to m/%s"
|
" git branch --set-upstream-to m/%s",
|
||||||
% (
|
project.RelPath(local=opt.this_manifest_only),
|
||||||
project.RelPath(local=opt.this_manifest_only),
|
project.CurrentBranch,
|
||||||
project.CurrentBranch,
|
project.manifest.branch,
|
||||||
project.manifest.branch,
|
|
||||||
),
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
elif avail:
|
elif avail:
|
||||||
pending.append(result)
|
pending.append((project, avail))
|
||||||
return pending
|
return pending
|
||||||
|
|
||||||
pending = self.ExecuteInParallel(
|
with self.ParallelContext():
|
||||||
opt.jobs,
|
self.get_parallel_context()["projects"] = projects
|
||||||
functools.partial(self._GatherOne, opt),
|
pending = self.ExecuteInParallel(
|
||||||
projects,
|
opt.jobs,
|
||||||
callback=_ProcessResults,
|
functools.partial(self._GatherOne, opt),
|
||||||
)
|
range(len(projects)),
|
||||||
|
callback=_ProcessResults,
|
||||||
|
)
|
||||||
|
|
||||||
if not pending:
|
if not pending:
|
||||||
if opt.branch is None:
|
if opt.branch is None:
|
||||||
print(
|
logger.error("repo: error: no branches ready for upload")
|
||||||
"repo: error: no branches ready for upload", file=sys.stderr
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
print(
|
logger.error(
|
||||||
'repo: error: no branches named "%s" ready for upload'
|
'repo: error: no branches named "%s" ready for upload',
|
||||||
% (opt.branch,),
|
opt.branch,
|
||||||
file=sys.stderr,
|
|
||||||
)
|
)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
@@ -809,10 +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():
|
||||||
print(
|
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, "
|
||||||
file=sys.stderr,
|
"a full `repo sync` might help."
|
||||||
)
|
)
|
||||||
ret = 1
|
ret = 1
|
||||||
if ret:
|
if ret:
|
||||||
|
|||||||
+14
-21
@@ -42,35 +42,28 @@ class Version(Command, MirrorSafeCommand):
|
|||||||
# These might not be the same. Report them both.
|
# These might not be the same. Report them both.
|
||||||
src_ver = RepoSourceVersion()
|
src_ver = RepoSourceVersion()
|
||||||
rp_ver = rp.bare_git.describe(HEAD)
|
rp_ver = rp.bare_git.describe(HEAD)
|
||||||
print("repo version %s" % rp_ver)
|
print(f"repo version {rp_ver}")
|
||||||
print(" (from %s)" % rem.url)
|
print(f" (from {rem.url})")
|
||||||
print(" (tracking %s)" % branch.merge)
|
print(f" (tracking {branch.merge})")
|
||||||
print(" (%s)" % rp.bare_git.log("-1", "--format=%cD", HEAD))
|
print(f" ({rp.bare_git.log('-1', '--format=%cD', HEAD)})")
|
||||||
|
|
||||||
if self.wrapper_path is not None:
|
if self.wrapper_path is not None:
|
||||||
print("repo launcher version %s" % self.wrapper_version)
|
print(f"repo launcher version {self.wrapper_version}")
|
||||||
print(" (from %s)" % self.wrapper_path)
|
print(f" (from {self.wrapper_path})")
|
||||||
|
|
||||||
if src_ver != rp_ver:
|
if src_ver != rp_ver:
|
||||||
print(" (currently at %s)" % src_ver)
|
print(f" (currently at {src_ver})")
|
||||||
|
|
||||||
print("repo User-Agent %s" % user_agent.repo)
|
print(f"repo User-Agent {user_agent.repo}")
|
||||||
print("git %s" % git.version_tuple().full)
|
print(f"git {git.version_tuple().full}")
|
||||||
print("git User-Agent %s" % user_agent.git)
|
print(f"git User-Agent {user_agent.git}")
|
||||||
print("Python %s" % sys.version)
|
print(f"Python {sys.version}")
|
||||||
uname = platform.uname()
|
uname = platform.uname()
|
||||||
if sys.version_info.major < 3:
|
if sys.version_info.major < 3:
|
||||||
# Python 3 returns a named tuple, but Python 2 is simpler.
|
# Python 3 returns a named tuple, but Python 2 is simpler.
|
||||||
print(uname)
|
print(uname)
|
||||||
else:
|
else:
|
||||||
print(
|
print(f"OS {uname.system} {uname.release} ({uname.version})")
|
||||||
"OS %s %s (%s)" % (uname.system, uname.release, uname.version)
|
processor = uname.processor if uname.processor else "unknown"
|
||||||
)
|
print(f"CPU {uname.machine} ({processor})")
|
||||||
print(
|
|
||||||
"CPU %s (%s)"
|
|
||||||
% (
|
|
||||||
uname.machine,
|
|
||||||
uname.processor if uname.processor else "unknown",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
print("Bug reports:", Wrapper().BUG_URL)
|
print("Bug reports:", Wrapper().BUG_URL)
|
||||||
|
|||||||
+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
|
||||||
+70
-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.
|
||||||
@@ -14,8 +14,12 @@
|
|||||||
|
|
||||||
"""Common fixtures for pytests."""
|
"""Common fixtures for pytests."""
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import color
|
||||||
|
import platform_utils
|
||||||
import repo_trace
|
import repo_trace
|
||||||
|
|
||||||
|
|
||||||
@@ -23,3 +27,68 @@ import repo_trace
|
|||||||
def disable_repo_trace(tmp_path):
|
def disable_repo_trace(tmp_path):
|
||||||
"""Set an environment marker to relax certain strict checks for test code.""" # noqa: E501
|
"""Set an environment marker to relax certain strict checks for test code.""" # noqa: E501
|
||||||
repo_trace._TRACE_FILE = str(tmp_path / "TRACE_FILE_from_test")
|
repo_trace._TRACE_FILE = str(tmp_path / "TRACE_FILE_from_test")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def restore_default_coloring() -> None:
|
||||||
|
"""Force disable color for reproducible test behavior.
|
||||||
|
|
||||||
|
The few tests that cover color behavior can still reset/change the color as
|
||||||
|
needed.
|
||||||
|
"""
|
||||||
|
color.SetDefaultColoring("never")
|
||||||
|
|
||||||
|
|
||||||
|
# adapted from pytest-home 0.5.1
|
||||||
|
def _set_home(monkeypatch, path: pathlib.Path):
|
||||||
|
"""
|
||||||
|
Set the home dir using a pytest monkeypatch context.
|
||||||
|
"""
|
||||||
|
win = platform_utils.isWindows()
|
||||||
|
vars = ["HOME"] + win * ["USERPROFILE"]
|
||||||
|
for var in vars:
|
||||||
|
monkeypatch.setenv(var, str(path))
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
# copied from
|
||||||
|
# https://github.com/pytest-dev/pytest/issues/363#issuecomment-1335631998
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def monkeysession():
|
||||||
|
with pytest.MonkeyPatch.context() as mp:
|
||||||
|
yield mp
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True, scope="session")
|
||||||
|
def session_tmp_home_dir(tmp_path_factory, monkeysession):
|
||||||
|
"""Set HOME to a temporary directory, avoiding user's .gitconfig.
|
||||||
|
|
||||||
|
b/302797407
|
||||||
|
|
||||||
|
Set home at session scope to take effect prior to
|
||||||
|
``test_wrapper.GitCheckoutTestCase.setUpClass``.
|
||||||
|
"""
|
||||||
|
return _set_home(monkeysession, tmp_path_factory.mktemp("home"))
|
||||||
|
|
||||||
|
|
||||||
|
# adapted from pytest-home 0.5.1
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def tmp_home_dir(monkeypatch, tmp_path_factory):
|
||||||
|
"""Set HOME to a temporary directory.
|
||||||
|
|
||||||
|
Ensures that state doesn't accumulate in $HOME across tests.
|
||||||
|
|
||||||
|
Note that in conjunction with session_tmp_homedir, the HOME
|
||||||
|
dir is patched twice, once at session scope, and then again at
|
||||||
|
the function scope.
|
||||||
|
"""
|
||||||
|
return _set_home(monkeypatch, tmp_path_factory.mktemp("home"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def setup_user_identity(monkeysession, scope="session"):
|
||||||
|
"""Set env variables for author and committer name and email."""
|
||||||
|
monkeysession.setenv("GIT_AUTHOR_NAME", "Foo Bar")
|
||||||
|
monkeysession.setenv("GIT_COMMITTER_NAME", "Foo Bar")
|
||||||
|
monkeysession.setenv("GIT_AUTHOR_EMAIL", "foo@bar.baz")
|
||||||
|
monkeysession.setenv("GIT_COMMITTER_EMAIL", "foo@bar.baz")
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user