mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-09-09 16:30:12 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d27d6829a8 | ||
|
|
0ea57e2eed | ||
|
|
ba8ddf396c | ||
|
|
d88ce8d952 | ||
|
|
5e8d2a6e3a | ||
|
|
e59c9cde99 | ||
|
|
83428a9b26 | ||
|
|
948abc85bc | ||
|
|
c63a2f92fa | ||
|
|
0039e39000 | ||
|
|
5f378458d2 | ||
|
|
d27034bf62 | ||
|
|
6541729a18 | ||
|
|
b85e76a86a | ||
|
|
e5bbb5c9e6 | ||
|
|
4fe87617ff | ||
|
|
d7299422ae | ||
|
|
3f087a8dd9 | ||
|
|
3a6e25af75 | ||
|
|
41c2597509 | ||
|
|
e6ad708009 | ||
|
|
09914bcab7 | ||
|
|
3f1775607f | ||
|
|
b85886fa9f | ||
|
|
d9da609d8c | ||
|
|
4bec297eb6 | ||
|
|
54fa31cd84 | ||
|
|
29b6630a65 | ||
|
|
0b82311632 | ||
|
|
06c4f9e1cc | ||
|
|
dd1130352b | ||
|
|
eeba6f268d | ||
|
|
1729aaebae | ||
|
|
978adb7ea5 | ||
|
|
0398c6718e | ||
|
|
3bb4871c44 | ||
|
|
35bbf701d0 | ||
|
|
881af15cdc | ||
|
|
fbc9c79192 | ||
|
|
ead4b2d7aa | ||
|
|
e7cac4bca6 | ||
|
|
91986011b0 | ||
|
|
a27dbcdb7b | ||
|
|
547dc9985c | ||
|
|
88a7e88e54 | ||
|
|
c21a41c7cc | ||
|
|
6586efe79a | ||
|
|
3af9e2f146 | ||
|
|
d32b70275c | ||
|
|
7f58543703 | ||
|
|
39c0b60900 | ||
|
|
cd307a6089 | ||
|
|
4b462634e0 | ||
|
|
d9d86fb595 | ||
|
|
f7a24df00c | ||
|
|
e0bd39c691 | ||
|
|
c883613e31 | ||
|
|
384c059f9e | ||
|
|
b8531133de | ||
|
|
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 |
@@ -12,5 +12,6 @@ extend-ignore =
|
||||
# E731: do not assign a lambda expression, use a def
|
||||
E731,
|
||||
exclude =
|
||||
.venv,
|
||||
venv,
|
||||
.tox,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# GitHub actions workflow.
|
||||
# https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
|
||||
# https://black.readthedocs.io/en/stable/integrations/github_actions.html
|
||||
|
||||
name: Format
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: psf/black@stable
|
||||
@@ -18,5 +18,5 @@ jobs:
|
||||
Thanks for your contribution!
|
||||
Unfortunately, we don't use GitHub pull requests to manage code
|
||||
contributions to this repository.
|
||||
Instead, please see [README.md](../blob/HEAD/SUBMITTING_PATCHES.md)
|
||||
Instead, please see [README.md](../blob/HEAD/CONTRIBUTING.md)
|
||||
which provides full instructions on how to get involved.
|
||||
|
||||
@@ -27,6 +27,6 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install tox tox-gh-actions
|
||||
- name: Test with tox
|
||||
run: tox
|
||||
python -m pip install pytest
|
||||
- name: Run tests
|
||||
run: python -m pytest
|
||||
|
||||
@@ -6,8 +6,14 @@ __pycache__
|
||||
/dist
|
||||
.repopickle_*
|
||||
/repoc
|
||||
/.cipd_bin
|
||||
/.tox
|
||||
/.venv
|
||||
|
||||
# PyCharm related
|
||||
/.idea/
|
||||
|
||||
# AI tool related.
|
||||
/AGENTS.md
|
||||
/CLAUDE.md
|
||||
/GEMINI.md
|
||||
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
# Copyright 2023 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Config file for the isort python module.
|
||||
# This is used to enforce import sorting standards.
|
||||
#
|
||||
# https://pycqa.github.io/isort/docs/configuration/options.html
|
||||
|
||||
[settings]
|
||||
# Be compatible with `black` since it also matches what we want.
|
||||
profile = black
|
||||
|
||||
line_length = 80
|
||||
length_sort = false
|
||||
force_single_line = true
|
||||
lines_after_imports = 2
|
||||
from_first = false
|
||||
case_sensitive = false
|
||||
force_sort_within_sections = true
|
||||
order_by_type = false
|
||||
|
||||
# Ignore generated files.
|
||||
extend_skip_glob = *_pb2.py
|
||||
|
||||
# Allow importing multiple classes on a single line from these modules.
|
||||
# https://google.github.io/styleguide/pyguide#s2.2-imports
|
||||
single_line_exclusions =
|
||||
abc,
|
||||
collections.abc,
|
||||
typing,
|
||||
@@ -5,6 +5,5 @@
|
||||
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
|
||||
<path>/git-repo</path>
|
||||
</pydev_pathproperty>
|
||||
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
|
||||
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
|
||||
</pydev_project>
|
||||
|
||||
@@ -24,7 +24,7 @@ However there are some differences, so please review and familiarize
|
||||
yourself with the following relevant bits.
|
||||
|
||||
|
||||
## Make separate commits for logically separate changes.
|
||||
## Make separate commits for logically separate changes
|
||||
|
||||
Unless your patch is really trivial, you should not be sending out a patch that
|
||||
was generated between your working tree and your commit head.
|
||||
@@ -43,17 +43,12 @@ probably need to split up your commit to finer grained pieces.
|
||||
|
||||
Lint any changes by running:
|
||||
```sh
|
||||
$ tox -e lint -- file.py
|
||||
$ flake8
|
||||
```
|
||||
|
||||
And format with:
|
||||
```sh
|
||||
$ tox -e format -- file.py
|
||||
```
|
||||
|
||||
Or format everything:
|
||||
```sh
|
||||
$ tox -e format
|
||||
$ black file.py
|
||||
```
|
||||
|
||||
Repo uses [black](https://black.readthedocs.io/) with line length of 80 as its
|
||||
@@ -73,15 +68,11 @@ the entire project in the included `.flake8` file.
|
||||
[PEP 8]: https://www.python.org/dev/peps/pep-0008/
|
||||
[flake8 documentation]: https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html#in-line-ignoring-errors
|
||||
|
||||
|
||||
## Running tests
|
||||
|
||||
We use [pytest](https://pytest.org/) and [tox](https://tox.readthedocs.io/) for
|
||||
running tests. You should make sure to install those first.
|
||||
|
||||
To run the full suite against all supported Python versions, simply execute:
|
||||
```sh
|
||||
$ tox -p auto
|
||||
```
|
||||
We use [pytest](https://pytest.org/) for running tests. You should make sure to
|
||||
install that first.
|
||||
|
||||
We have [`./run_tests`](./run_tests) which is a simple wrapper around `pytest`:
|
||||
```sh
|
||||
@@ -127,7 +118,7 @@ your patch. It is virtually impossible to remove a patch once it
|
||||
has been applied and pushed out.
|
||||
|
||||
|
||||
## Sending your patches.
|
||||
## Sending your patches
|
||||
|
||||
Do not email your patches to anyone.
|
||||
|
||||
@@ -143,7 +134,7 @@ they have right to redistribute your work under the Apache License:
|
||||
|
||||
Ensure you have obtained an HTTP password to authenticate:
|
||||
|
||||
https://gerrit-review.googlesource.com/new-password
|
||||
https://www.googlesource.com/new-password
|
||||
|
||||
Ensure that you have the local commit hook installed to automatically
|
||||
add a ChangeId to your commits:
|
||||
@@ -14,7 +14,7 @@ that you can put anywhere in your path.
|
||||
* Docs: <https://source.android.com/source/using-repo.html>
|
||||
* [repo Manifest Format](./docs/manifest-format.md)
|
||||
* [repo Hooks](./docs/repo-hooks.md)
|
||||
* [Submitting patches](./SUBMITTING_PATCHES.md)
|
||||
* [Contributing](./CONTRIBUTING.md)
|
||||
* Running Repo in [Microsoft Windows](./docs/windows.md)
|
||||
* GitHub mirror: <https://github.com/GerritCodeReview/git-repo>
|
||||
* Postsubmit tests: <https://github.com/GerritCodeReview/git-repo/actions>
|
||||
@@ -49,6 +49,43 @@ $ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.bin/repo
|
||||
$ chmod a+rx ~/.bin/repo
|
||||
```
|
||||
|
||||
## Shell Completion
|
||||
|
||||
Repo includes completion scripts for Bash and Zsh.
|
||||
|
||||
### Bash
|
||||
|
||||
To enable completion in Bash, source `completion.bash` in your `~/.bashrc`:
|
||||
|
||||
```sh
|
||||
source /path/to/git-repo/completion.bash
|
||||
```
|
||||
|
||||
### Zsh
|
||||
|
||||
To enable completion in Zsh, you can either:
|
||||
|
||||
1. Copy or symlink `completion.zsh` to a file named `_repo` in a directory in your `$fpath`:
|
||||
```sh
|
||||
mkdir -p ~/.zsh/completion
|
||||
# You can copy the file:
|
||||
cp /path/to/git-repo/completion.zsh ~/.zsh/completion/_repo
|
||||
# Or symlink it:
|
||||
ln -s /path/to/git-repo/completion.zsh ~/.zsh/completion/_repo
|
||||
```
|
||||
Then add that directory to your `fpath` in `~/.zshrc` before `compinit`:
|
||||
```zsh
|
||||
fpath=(~/.zsh/completion $fpath)
|
||||
autoload -Uz compinit
|
||||
compinit
|
||||
```
|
||||
|
||||
2. Or source the file directly and call `compdef` in your `~/.zshrc`:
|
||||
```zsh
|
||||
source /path/to/git-repo/completion.zsh
|
||||
compdef _repo repo
|
||||
```
|
||||
|
||||
|
||||
[new-bug]: https://issues.gerritcodereview.com/issues/new?component=1370071
|
||||
[issue tracker]: https://issues.gerritcodereview.com/issues?q=is:open%20componentid:1370071
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# git-repo AI Review Agents (WIP)
|
||||
|
||||
**Note:** This project is a work in progress and is subject to change.
|
||||
|
||||
This directory contains configurations and skills for AI review agents that
|
||||
automatically analyze changes in the `git-repo` codebase.
|
||||
|
||||
These agents help maintain code quality, enforce style guidelines, and catch
|
||||
common pitfalls before code is merged.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
* [`agent_configs.txtpb`](agent_configs.txtpb): Defines the active AI
|
||||
agents, their configurations, and which skills they are equipped with.
|
||||
* [`skills/`](skills/): Contains the "skills" (rules, guidelines, and traps)
|
||||
used by the agents.
|
||||
* [`code_review_workflow/`](skills/code_review_workflow/SKILL.md):
|
||||
Guidelines for code review processes, commit messages, and testing.
|
||||
* [`core_internals/`](skills/core_internals/SKILL.md): Technical
|
||||
guidelines for `git-repo` core logic (sync, manifest, git integration,
|
||||
etc.).
|
||||
|
||||
## How It Works
|
||||
|
||||
The agents defined in `agent_configs.txtpb` are configured to run
|
||||
automatically on new changes. They analyze the diffs against the rules defined
|
||||
in their respective skills and provide feedback in the code review interface
|
||||
(e.g., Gerrit).
|
||||
|
||||
## Contributing
|
||||
|
||||
To improve the agent's review quality or add new rules:
|
||||
|
||||
1. **Update existing skills**: Modify the `SKILL.md` files under `skills/` to
|
||||
add new rules, "What" explanations, "Why" rationales, and "Traps"
|
||||
(Don't/Do code examples).
|
||||
2. **Add new skills**:
|
||||
* Create a new directory under `skills/`.
|
||||
* Add a `SKILL.md` following the established format (see existing skills
|
||||
for reference).
|
||||
* Register the new skill in `agent_configs.txtpb` by adding it to the
|
||||
`skills` field of an agent configuration.
|
||||
@@ -0,0 +1,26 @@
|
||||
# proto-file: google/corp/android/engprod/codereviewagentconfiguration/v1/agent.proto
|
||||
# proto-message: HostAgents
|
||||
|
||||
# Code Review Workflow Agent
|
||||
configs {
|
||||
id: "code-review-workflow"
|
||||
display_name: "Code Review Workflow"
|
||||
description: "Analyzes git-repo Gerrit submission labeling, commit metadata, Python linting, and testing strategy."
|
||||
skills: "code_review_workflow"
|
||||
include_filters {
|
||||
project: "git-repo"
|
||||
}
|
||||
automatic: false
|
||||
}
|
||||
|
||||
# Core Internals Agent
|
||||
configs {
|
||||
id: "core-internals"
|
||||
display_name: "Core Internals"
|
||||
description: "Analyzes git-repo synchronization, multiprocessing, manifest parsing, git integration, worktree layouts, and CLI commands."
|
||||
skills: "core_internals"
|
||||
include_filters {
|
||||
project: "git-repo"
|
||||
}
|
||||
automatic: false
|
||||
}
|
||||
@@ -0,0 +1,968 @@
|
||||
---
|
||||
name: code-review-workflow
|
||||
description: Provides guidance and best practices on Gerrit submission labeling, CI builder execution, Python code formatting/linting, commit metadata standardization, and testing strategy in git-repo.
|
||||
---
|
||||
|
||||
# Code Review Workflow Engineering Guide
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Welcome to the authoritative engineering guide for the Code Review Workflow.
|
||||
This living repository exists to capture critical folk knowledge, prevent the
|
||||
recurrence of historical failure modes, and enforce strict architectural and
|
||||
procedural boundaries across our integration pipeline. By standardizing these
|
||||
protocols, we ensure high development velocity while maintaining rock-solid
|
||||
codebase stability and traceability.
|
||||
|
||||
This guide covers the complete lifecycle of a change list (CL) from local
|
||||
development to automated submission. It defines the strict Gerrit labeling
|
||||
mechanisms required to trigger the Commit-Queue, mandates comprehensive CI
|
||||
builder environment checks, and enforces centralized Python static analysis.
|
||||
Furthermore, it outlines uncompromising standards for atomic commit metadata and
|
||||
pragmatic testing state isolation to guarantee that every integration is fully
|
||||
bisectable and verifiable.
|
||||
|
||||
For incoming engineers, adherence to these mandates eliminates the friction of
|
||||
stalled pipelines, unreviewable monolithic changes, and silent CI regressions.
|
||||
Treat this guide as your primary roadmap for navigating the repository's strict
|
||||
submission requirements, enabling seamless transitions from peer approval to
|
||||
successfully integrated code.
|
||||
|
||||
## Summary
|
||||
|
||||
| Chapter Theme / Title | Scope & Objective |
|
||||
| :------------------------------- | :---------------------------------------- |
|
||||
| **Gerrit Submission and Labeling | Dictates strict access controls, review |
|
||||
: Workflow** : enforcement protocols, and Gerrit :
|
||||
: : labeling mechanisms required to advance :
|
||||
: : changes through the CI pipeline, ensuring :
|
||||
: : seamless transitions to automated :
|
||||
: : integration via the Commit-Queue. :
|
||||
| **CI Builder Environment and | Defines guidelines for ensuring build |
|
||||
: Execution Integrity** : script resilience against missing :
|
||||
: : dependencies and managing process :
|
||||
: : execution contexts within LUCI and local :
|
||||
: : testing environments to prevent silent :
|
||||
: : builder failures. :
|
||||
| **Python Code Formatting and | Governs the automated enforcement of |
|
||||
: Linting** : Python style guidelines, mandating strict :
|
||||
: : PEP-8 compliance, import sorting, and :
|
||||
: : consistent string quoting to ensure :
|
||||
: : codebase uniformity and prevent CI :
|
||||
: : regressions. :
|
||||
| **Commit Metadata and History | Establishes the structural composition |
|
||||
: Standardization** : and metadata formatting of change lists :
|
||||
: : (CLs) to ensure precise issue tracker :
|
||||
: : integration, reliable CI/CD parsing, and :
|
||||
: : an atomic, bisectable repository history. :
|
||||
| **Testing Strategy and State | Outlines test implementation boundaries, |
|
||||
: Isolation** : emphasizing pragmatic mocking limits to :
|
||||
: : prevent false positives and detailing :
|
||||
: : acceptable workflows for deferred test :
|
||||
: : coverage while maintaining verification :
|
||||
: : integrity. :
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
## Chapter: Gerrit Submission and Labeling Workflow
|
||||
|
||||
**Context:** This domain dictates the strict access controls, review enforcement
|
||||
protocols, and specific Gerrit labeling mechanisms required to advance changes
|
||||
through the CI pipeline. Adherence ensures seamless transitions from peer
|
||||
approval to automated integration via the Commit-Queue.
|
||||
|
||||
### Summary
|
||||
|
||||
| Rule ID | Principle / Constraint | Priority | Primary Symptom / |
|
||||
: : : : Trap :
|
||||
| :-------- | :------------------------------ | :------- | :----------------- |
|
||||
| **T1-01** | Explicit Labeling for Gerrit | High | Leaving a change |
|
||||
: : Automated Submission : : idle after :
|
||||
: : : : addressing :
|
||||
: : : : comments or :
|
||||
: : : : receiving a :
|
||||
: : : : reviewer's LGTM, :
|
||||
: : : : expecting the :
|
||||
: : : : reviewer to merge :
|
||||
: : : : it. :
|
||||
| **T1-02** | Automated Submission via | Medium | Requesting a |
|
||||
: : Commit-Queue (CQ) : : manual push or :
|
||||
: : : : direct submit from :
|
||||
: : : : repository :
|
||||
: : : : maintainers after :
|
||||
: : : : receiving code :
|
||||
: : : : review approval. :
|
||||
| **T1-03** | Gerrit Trusted Contributor | Medium | Relying on a |
|
||||
: : Review Enforcement Verification : : standard +2 vote :
|
||||
: : : : from a non-trusted :
|
||||
: : : : contributor to :
|
||||
: : : : fulfill strict :
|
||||
: : : : Review-Enforcement :
|
||||
: : : : requirements. :
|
||||
| **T1-04** | Mandatory Gerrit Labels for | High | Acknowledging an |
|
||||
: : Automated Submission : : approval but :
|
||||
: : : : failing to apply :
|
||||
: : : : the appropriate :
|
||||
: : : : Gerrit labels to :
|
||||
: : : : initiate the merge :
|
||||
: : : : pipeline. :
|
||||
| **T1-05** | Gerrit Automated Submission | Medium | Leaving an |
|
||||
: : Triggers : : approved patchset :
|
||||
: : : : idle and waiting :
|
||||
: : : : for maintainers to :
|
||||
: : : : manually merge it. :
|
||||
| **T1-06** | Active Reviewer Rerouting for | Medium | Waiting weeks or |
|
||||
: : Stalled Changes : : months for an :
|
||||
: : : : inactive or OOO :
|
||||
: : : : reviewer to :
|
||||
: : : : respond to a :
|
||||
: : : : patchset update. :
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
### Rules
|
||||
|
||||
#### T1-01: Explicit Labeling for Gerrit Automated Submission
|
||||
|
||||
> **Rule:** Always apply `Verified+1` and `Commit-Queue+2` explicitly to trigger
|
||||
> the final submission phase. Never assume a code approval automatically
|
||||
> initiates the pipeline.
|
||||
>
|
||||
> **What:** Changes are not merged automatically upon receiving approval;
|
||||
> contributors must explicitly set the `Verified+1` and `Commit-Queue+2` labels
|
||||
> to trigger the final submission phase.
|
||||
>
|
||||
> **Applies To:** Gerrit review UI and change submission pipeline as defined in
|
||||
> `CONTRIBUTING.md`.
|
||||
>
|
||||
> **Why:** Contributors often mistakenly assume an LGTM implies an immediate
|
||||
> merge, leading to stalled changes. The project relies on explicitly triggering
|
||||
> the Commit-Queue to finalize CI checks and perform the merge. Failing to
|
||||
> adhere to this typically results in **Stalled Submission Pipeline**.
|
||||
|
||||
**Trap 1: Leaving a change idle after addressing comments or receiving a
|
||||
reviewer's LGTM, expecting the reviewer to merge it.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
* Waiting indefinitely after reviewer posts 'LGTM'.
|
||||
|
||||
**Do:**
|
||||
|
||||
* Vote `Verified+1` and `Commit-Queue+2` manually to submit the change to the
|
||||
automated queue.
|
||||
|
||||
**Exceptions:** Contributors lacking trusted permissions must ping a repository
|
||||
maintainer to apply the final `Commit-Queue+2` vote.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
#### T1-02: Automated Submission via Commit-Queue (CQ)
|
||||
|
||||
> **Rule:** Must utilize the Gerrit Commit-Queue (CQ) labeling system to merge
|
||||
> code. Maintainers must never perform direct manual submissions.
|
||||
>
|
||||
> **What:** Merging code must be triggered via the Gerrit Commit-Queue (CQ)
|
||||
> labeling system rather than relying on direct manual submission by
|
||||
> maintainers.
|
||||
>
|
||||
> **Applies To:** Gerrit code review UI and CI/CD submission workflow.
|
||||
>
|
||||
> **Why:** Contributors would request maintainers to directly merge patches once
|
||||
> approved, bypassing the automated commit-queue pipeline, which guarantees that
|
||||
> final integration tests pass before pushing to the target branch. Failing to
|
||||
> adhere to this typically results in **Bypassed CI / Direct Submit**.
|
||||
|
||||
**Trap 1: Requesting a manual push or direct submit from repository maintainers
|
||||
after receiving code review approval.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
* Leaving a comment: "I believe everything is ready for integrating this. So
|
||||
if either of you can submit it, it would be appreciated."
|
||||
|
||||
**Do:**
|
||||
|
||||
* Applying the `Commit-Queue+2` (CQ+2) label in Gerrit, which delegates
|
||||
testing and the final merge operation to the automated bot.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
#### T1-03: Gerrit Trusted Contributor Review Enforcement Verification
|
||||
|
||||
> **Rule:** Verify review enforcement requirements are satisfied by contributors
|
||||
> within the explicitly configured trusted group. Never cast misleading +2 votes
|
||||
> if you lack valid trusted group privileges.
|
||||
>
|
||||
> **What:** Gerrit submission requirements may mandate specific approval levels
|
||||
> (e.g., two trusted contributors). Votes from users with +2 access who are not
|
||||
> in the designated 'trusted' group do not satisfy the 'Review-Enforcement'
|
||||
> submit requirement.
|
||||
>
|
||||
> **Applies To:** Gerrit repository administration and code review voting
|
||||
> workflows.
|
||||
>
|
||||
> **Why:** Non-trusted contributors with +2 rights were casting +2 votes on
|
||||
> changes. These votes did not fulfill the 'Two trusted contributors'
|
||||
> Review-Enforcement requirement, leading to stalled submissions and confusion
|
||||
> regarding why the UI showed a +2 but blocked submission. Failing to adhere to
|
||||
> this typically results in **Blocked Submission / Silent Requirement Failure**.
|
||||
|
||||
**Trap 1: Relying on a standard +2 vote from a non-trusted contributor to
|
||||
fulfill strict Review-Enforcement requirements.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
* Leaving a +2 vote on a change as a non-trusted contributor, creating the
|
||||
false appearance that the Review-Enforcement requirement has been partially
|
||||
or fully met.
|
||||
|
||||
**Do:**
|
||||
|
||||
* Verifying the reviewer is in the explicitly configured trusted group for the
|
||||
repository. If not, the reviewer should manually downgrade their invalid +2
|
||||
vote to a +1 to clearly indicate that their vote does not count toward the
|
||||
enforcement threshold.
|
||||
|
||||
**Exceptions:** Repositories where specific non-employee groups have been
|
||||
explicitly added to the trusted administrators list.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
#### T1-04: Mandatory Gerrit Labels for Automated Submission
|
||||
|
||||
> **Rule:** Always apply `Verified+1` and `Commit-Queue+2` labels to initiate
|
||||
> the CI merge process. Never leave an approved CL in a technically unlabeled
|
||||
> state.
|
||||
>
|
||||
> **What:** A code change must receive explicit `Verified+1` and
|
||||
> `Commit-Queue+2` labels by the author or reviewer to trigger the automated CI
|
||||
> merge process.
|
||||
>
|
||||
> **Applies To:** Gerrit workflow / Merge execution phase.
|
||||
>
|
||||
> **Why:** Historically, leaving a Change List (CL) in an approved but unlabeled
|
||||
> state causes the integration pipeline to stall indefinitely, requiring manual
|
||||
> intervention or reviewer pinging to trigger the CI queue. Failing to adhere to
|
||||
> this typically results in **Merge Pipeline Stall**.
|
||||
|
||||
**Trap 1: Acknowledging an approval but failing to apply the appropriate Gerrit
|
||||
labels to initiate the merge pipeline.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
* Leaving the CL in an approved state and waiting for auto-submission without
|
||||
applying the `Verified+1` or `Commit-Queue+2` labels.
|
||||
|
||||
**Do:**
|
||||
|
||||
* Explicitly applying `Verified+1` (and `Commit-Queue+2` if ready) once
|
||||
reviewers have approved the logic, to instruct the automation to merge the
|
||||
code.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
#### T1-05: Gerrit Automated Submission Triggers
|
||||
|
||||
> **Rule:** Must actively signal patch readiness to Gerrit systems using proper
|
||||
> label thresholds. Avoid leaving patchsets idle assuming upstream maintainer
|
||||
> action.
|
||||
>
|
||||
> **What:** A patchset requires specific label thresholds ('Verified+1' and
|
||||
> 'Commit-Queue+2') to trigger automated submission in the Gerrit workflow.
|
||||
>
|
||||
> **Applies To:** Gerrit review UI and automated CI/CD submission process for
|
||||
> the git-repo codebase.
|
||||
>
|
||||
> **Why:** Contributors frequently asked how to integrate changes after
|
||||
> receiving an approval, leading to stalled patches because the automated
|
||||
> pipeline was not explicitly triggered. Failing to adhere to this typically
|
||||
> results in **Stalled Patch Integration**.
|
||||
|
||||
**Trap 1: Leaving an approved patchset idle and waiting for maintainers to
|
||||
manually merge it.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
* Waiting indefinitely after receiving an 'LGTM' without setting workflow
|
||||
labels.
|
||||
|
||||
**Do:**
|
||||
|
||||
* The patch author manually sets the 'Verified' flag (if locally tested) and
|
||||
applies the 'Commit-Queue+2' vote to signal readiness for automated merge.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
#### T1-06: Active Reviewer Rerouting for Stalled Changes
|
||||
|
||||
> **Rule:** Actively reroute reviews stalled by unresponsive or out-of-office
|
||||
> (OOO) primary reviewers. Must explicitly tag alternate maintainers and
|
||||
> document the absence to prevent lifecycle stalls.
|
||||
>
|
||||
> **What:** If the primary reviewer is out-of-office (OOO) or unresponsive for
|
||||
> an extended period, contributors must actively CC and reroute the review to
|
||||
> another active maintainer.
|
||||
>
|
||||
> **Applies To:** Gerrit review cycle and reviewer assignment process.
|
||||
>
|
||||
> **Why:** Patchsets have historically stalled for over a month due to reviewers
|
||||
> taking extended leave without actively delegating their review queues. Failing
|
||||
> to adhere to this typically results in **Indefinite Review Stalls**.
|
||||
|
||||
**Trap 1: Waiting weeks or months for an inactive or OOO reviewer to respond to
|
||||
a patchset update.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
* Leaving a review assigned strictly to an unresponsive reviewer without
|
||||
notifying other maintainers or attempting to escalate.
|
||||
|
||||
**Do:**
|
||||
|
||||
* Tag a new reviewer with 'PTAL' (Please Take A Look) in the thread,
|
||||
explicitly noting the original reviewer's absence, and confirm alignment
|
||||
with the original author.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
### Cross-Domain Dependencies
|
||||
|
||||
* **Upstream:** T4 | Python Code Formatting and Linting - *Proper formatting
|
||||
and static analysis are enforced before changes become eligible for final
|
||||
Gerrit review and automated integration.*
|
||||
* **Upstream:** T5 | Commit Metadata and History Standardization - *Accurate
|
||||
commit messaging and isolated history must be validated by reviewers prior
|
||||
to receiving approval labels.*
|
||||
* **Downstream:** T3 | CI Builder Environment and Execution Integrity -
|
||||
*Triggering the Commit-Queue directly invokes downstream LUCI environments
|
||||
to guarantee execution integrity prior to branch merge.*
|
||||
|
||||
## Chapter: CI Builder Environment and Execution Integrity
|
||||
|
||||
**Context:** This section defines strict guidelines for ensuring the resilience
|
||||
of build scripts against missing dependencies and managing process execution
|
||||
contexts within LUCI and local testing environments. Adherence guarantees robust
|
||||
verification across diverse operating systems and CI pipelines while preventing
|
||||
silent builder failures.
|
||||
|
||||
### Summary
|
||||
|
||||
| Rule ID | Principle / Constraint | Priority | Primary Symptom / Trap |
|
||||
| :-------- | :------------------------ | :------- | :------------------------ |
|
||||
| **T3-01** | Verification Against | High | Running a standard local |
|
||||
: : Breaking Change Build : : `make` without testing :
|
||||
: : Configurations : : strict configurations or :
|
||||
: : : : breaking-change flags. :
|
||||
| **T3-02** | Windows Developer Mode | Medium | Attempting to run full |
|
||||
: : Requirements for Tool : : local verification on a :
|
||||
: : Verification : : standard Windows user :
|
||||
: : : : account. :
|
||||
| **T3-03** | Graceful Degradation for | Medium | Assuming all local |
|
||||
: : Missing Builder Utilities : : developer utilities exist :
|
||||
: : : : in the strict CI builder :
|
||||
: : : : environment and :
|
||||
: : : : unconditionally executing :
|
||||
: : : : them. :
|
||||
| **T3-04** | Contextual Diagnostic | High | Observing a generic CI |
|
||||
: : Logging for LUCI CI : : failure without isolating :
|
||||
: : Failures : : the specific process :
|
||||
: : : : execution context or :
|
||||
: : : : dependency resolution :
|
||||
: : : : step. :
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
### Rules
|
||||
|
||||
#### T3-01: Verification Against Breaking Change Build Configurations
|
||||
|
||||
> **Rule:** Always explicitly test core build structure modifications with
|
||||
> breaking changes enabled to ensure forward compatibility.
|
||||
>
|
||||
> **What:** When modifying core build structures, the build must be tested
|
||||
> explicitly with breaking changes enabled to ensure forward compatibility and
|
||||
> correct regeneration of generated files.
|
||||
>
|
||||
> **Applies To:** Local build environments and Makefile targets.
|
||||
>
|
||||
> **Why:** Changes might succeed in a standard default build but fail when
|
||||
> breaking change toggles are activated, hiding underlying dependency or
|
||||
> regeneration issues. Failing to adhere to this typically results in **Build
|
||||
> Breakage / Stale Artifacts**.
|
||||
|
||||
**Trap 1: Running a standard local `make` without testing strict configurations
|
||||
or breaking-change flags.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
```bash
|
||||
make -j
|
||||
```
|
||||
|
||||
**Do:**
|
||||
|
||||
```bash
|
||||
make -j WITH_BREAKING_CHANGES=1
|
||||
```
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
#### T3-02: Windows Developer Mode Requirements for Tool Verification
|
||||
|
||||
> **Rule:** Must execute local tool verification on Windows (gWindows) using an
|
||||
> Administrator account to enable Developer Mode.
|
||||
>
|
||||
> **What:** Local verification of git-repo tooling on Windows (gWindows)
|
||||
> explicitly requires the host environment to be running with Administrator
|
||||
> privileges to enable Developer Mode.
|
||||
>
|
||||
> **Applies To:** Windows (gWindows) test environments verifying file system
|
||||
> operations.
|
||||
>
|
||||
> **Why:** Without Developer Mode enabled (which necessitates Admin rights),
|
||||
> features relying on advanced OS-level file system operations (like symlinks)
|
||||
> cannot execute, permanently blocking full local test suite execution on
|
||||
> standard accounts. Failing to adhere to this typically results in
|
||||
> **Verification Blocked / OS Permission Error**.
|
||||
|
||||
**Trap 1: Attempting to run full local verification on a standard Windows user
|
||||
account.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
* Executing the test suite from a non-elevated command prompt on Windows
|
||||
without Developer Mode.
|
||||
|
||||
**Do:**
|
||||
|
||||
* Elevate to an Administrator account to enable Developer Mode before
|
||||
executing the test suite on gWindows.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
#### T3-03: Graceful Degradation for Missing Builder Utilities
|
||||
|
||||
> **Rule:** Always implement auto-skip logic for optional utilities in build
|
||||
> scripts rather than hard-failing when unavailable on the CI builder.
|
||||
>
|
||||
> **What:** Build scripts and test suites must implement auto-skip logic for
|
||||
> optional, environment-specific utilities rather than hard-failing when the
|
||||
> utility is unavailable on the CI builder.
|
||||
>
|
||||
> **Applies To:** CI Builder environment scripts and test suites, specifically
|
||||
> testing external CLI utilities (e.g., `help2man`).
|
||||
>
|
||||
> **Why:** When a required utility was not pre-installed on the CI builder
|
||||
> image, the build hard-failed. Adding auto-skip logic allows the CI pipeline to
|
||||
> remain unblocked while still providing local testing benefits for developers
|
||||
> who have the tool installed. Failing to adhere to this typically results in
|
||||
> **Build Failure / Blocked CI**.
|
||||
|
||||
**Trap 1: Assuming all local developer utilities exist in the strict CI builder
|
||||
environment and unconditionally executing them.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
```python
|
||||
# BAD: Hard failure if utility is missing
|
||||
subprocess.run(["help2man", "repo"], check=True)
|
||||
```
|
||||
|
||||
**Do:**
|
||||
|
||||
```python
|
||||
# GOOD: Auto-skip test if utility is missing in the environment
|
||||
if not shutil.which("help2man"):
|
||||
self.skipTest("help2man not installed")
|
||||
subprocess.run(["help2man", "repo"], check=True)
|
||||
```
|
||||
|
||||
**Exceptions:** Core dependencies required for fundamental build steps cannot be
|
||||
skipped and must be installed on the bot image.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
#### T3-04: Contextual Diagnostic Logging for LUCI CI Failures
|
||||
|
||||
> **Rule:** Must investigate CI builder failures by extracting and analyzing
|
||||
> full execution context logs to isolate environmental roadblocks.
|
||||
>
|
||||
> **What:** CI builder failures must be investigated using full execution
|
||||
> context logs (e.g., LUCI context, vpython3 resolution, and retcode outputs) to
|
||||
> isolate environmental roadblocks.
|
||||
>
|
||||
> **Applies To:** LUCI builder execution environment, vpython3 resolution, and
|
||||
> CI pipeline debugging.
|
||||
>
|
||||
> **Why:** CI commands failed with `retcode 1` due to external factors like
|
||||
> specific URLs being flagged as suspect by internal security tools, breaking
|
||||
> the build environment. Failing to adhere to this typically results in **Silent
|
||||
> Builder Failure**.
|
||||
|
||||
**Trap 1: Observing a generic CI failure without isolating the specific process
|
||||
execution context or dependency resolution step.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
* Restarting the CI pipeline blindly when a job fails with a generic retcode,
|
||||
ignoring potential external network or security blockers.
|
||||
|
||||
**Do:**
|
||||
|
||||
* Extract the step-by-step LUCI context log, verify path resolution (e.g.,
|
||||
CIPD packages), and explicitly document external blockers like security
|
||||
flags in the review.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
### Cross-Domain Dependencies
|
||||
|
||||
* **Upstream:** T6 | Testing Strategy and State Isolation - *Test
|
||||
implementation dictates how missing builder utilities are mocked or
|
||||
gracefully skipped during execution.*
|
||||
* **Downstream:** T1 | Gerrit Submission and Labeling Workflow - *Automated
|
||||
Verified+1 labels rely entirely on the stable, unblocked execution of CI
|
||||
builder pipelines.*
|
||||
|
||||
## Chapter: Python Code Formatting and Linting
|
||||
|
||||
**Context:** This domain governs the automated enforcement of Python style
|
||||
guidelines, mandating strict PEP-8 compliance, import sorting, and consistent
|
||||
string quoting. All Python modifications must pass centralized static analysis
|
||||
pipelines before integration to ensure codebase uniformity and prevent CI
|
||||
regressions.
|
||||
|
||||
### Summary
|
||||
|
||||
| Rule ID | Principle / Constraint | Priority | Primary Symptom / Trap |
|
||||
| :-------- | :----------------------- | :------- | :------------------------ |
|
||||
| **T4-01** | Automated Flake8 | Medium | Relying purely on manual |
|
||||
: : Post-Submit Verification : : code review or sporadic :
|
||||
: : : : local linting without a :
|
||||
: : : : continuous integration :
|
||||
: : : : check. :
|
||||
| **T4-02** | Mandatory Python | High | Using single quotes for |
|
||||
: : Formatting and Import : : strings and appending new :
|
||||
: : Sorting : : imports to the bottom of :
|
||||
: : : : the import block without :
|
||||
: : : : alphabetical or :
|
||||
: : : : categorical sorting. :
|
||||
| **T4-03** | Strict Python Import | High | Mixing local application |
|
||||
: : Ordering : : imports with standard :
|
||||
: : : : library imports, causing :
|
||||
: : : : linting tools to fail the :
|
||||
: : : : CQ job. :
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
### Rules
|
||||
|
||||
#### T4-01: Automated Flake8 Post-Submit Verification
|
||||
|
||||
> **Rule:** Always configure and maintain centralized CI workflows to
|
||||
> automatically run static analysis and validate Python code styling
|
||||
> post-submit.
|
||||
>
|
||||
> **What:** Static analysis and Python linting must be automated via a
|
||||
> centralized CI pipeline (e.g., Flake8 post-submit workflows) to enforce
|
||||
> consistent style and prevent basic errors.
|
||||
>
|
||||
> **Applies To:** All Python files in the git-repo codebase; specifically
|
||||
> validated via `.github/workflows/flake8-postsubmit.yml`.
|
||||
>
|
||||
> **Why:** Relying strictly on manual code review to catch styling and linting
|
||||
> violations is error-prone. Automation ensures a baseline of code quality on
|
||||
> every code push without consuming human review cycles. Failing to adhere to
|
||||
> this typically results in **Linting Regression / Style Violation**.
|
||||
|
||||
**Trap 1: Relying purely on manual code review or sporadic local linting without
|
||||
a continuous integration check.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
* Committing Python code without an active CI linting workflow configuration.
|
||||
|
||||
**Do:**
|
||||
|
||||
* Maintain `.github/workflows/flake8-postsubmit.yml` to automatically run
|
||||
flake8 on target branches.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
#### T4-02: Mandatory Python Formatting and Import Sorting
|
||||
|
||||
> **Rule:** Must format Python code to enforce double-quoted strings and
|
||||
> alphabetically sorted import blocks to satisfy automated formatting checks.
|
||||
>
|
||||
> **What:** Python code modifications must pass automated style and linting
|
||||
> checks ('Verify git-repo CL'), which strictly enforce string quote conventions
|
||||
> (preferring double quotes), import block sorting, and PEP-8 style formatting.
|
||||
>
|
||||
> **Applies To:** All Python source files modified in the git-repo codebase.
|
||||
>
|
||||
> **Why:** Developers submitting patches with single-quoted strings or unsorted
|
||||
> imports triggered automated CI failures in the `Verify git-repo CL` job,
|
||||
> completely blocking code submission until formatting tools were executed
|
||||
> locally. Failing to adhere to this typically results in **CI Pipeline
|
||||
> Failure**.
|
||||
|
||||
**Trap 1: Using single quotes for strings and appending new imports to the
|
||||
bottom of the import block without alphabetical or categorical sorting.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
```python
|
||||
import sys
|
||||
import os
|
||||
|
||||
msg = 'This is an error'
|
||||
```
|
||||
|
||||
**Do:**
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
|
||||
msg = "This is an error"
|
||||
```
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
#### T4-03: Strict Python Import Ordering
|
||||
|
||||
> **Rule:** Always segment and order Python imports strictly according to
|
||||
> project standards (standard library, third-party, local) to prevent CQ
|
||||
> pipeline failures.
|
||||
>
|
||||
> **What:** Python module imports must adhere strictly to the project's
|
||||
> formatting rules (e.g., standard library, third-party, local module ordering)
|
||||
> to pass automated Commit-Queue (CQ) checks.
|
||||
>
|
||||
> **Applies To:** Python source files.
|
||||
>
|
||||
> **Why:** Non-standard import blocks cause the automated CI/CQ linting pipeline
|
||||
> to fail, completely blocking submission even if the core functional logic of
|
||||
> the patch is flawless. Failing to adhere to this typically results in **CI
|
||||
> Linting Failure**.
|
||||
|
||||
**Trap 1: Mixing local application imports with standard library imports,
|
||||
causing linting tools to fail the CQ job.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
```python
|
||||
import sys
|
||||
import my_local_module
|
||||
import os
|
||||
```
|
||||
|
||||
**Do:**
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
|
||||
import my_local_module
|
||||
```
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
### Cross-Domain Dependencies
|
||||
|
||||
* **Upstream:** T3 | CI Builder Environment and Execution Integrity -
|
||||
*Reliable CI builder environments must be available to execute the static
|
||||
analysis and Python formatting verifications.*
|
||||
* **Downstream:** T1 | Gerrit Submission and Labeling Workflow - *Formatting
|
||||
and linting rules must be fully satisfied before automated mechanisms like
|
||||
the Commit-Queue (CQ+2) will merge code into the repository.*
|
||||
|
||||
## Chapter: Commit Metadata and History Standardization
|
||||
|
||||
**Context:** This domain governs the structural composition and metadata
|
||||
formatting of change lists (CLs) within the git-repo codebase. Strict adherence
|
||||
ensures precise issue tracker integration, reliable CI/CD parsing, and atomic,
|
||||
bisectable repository history.
|
||||
|
||||
### Summary
|
||||
|
||||
| Rule ID | Principle / Constraint | Priority | Primary Symptom / Trap |
|
||||
| :-------- | :------------------------ | :------- | :------------------------ |
|
||||
| **T5-01** | Strict Commit Message Bug | Medium | Providing free-text |
|
||||
: : Tag Formatting : : descriptions, arbitrary :
|
||||
: : : : prefixes, or non-standard :
|
||||
: : : : bug references in the :
|
||||
: : : : commit block. :
|
||||
| **T5-02** | Atomic and Bisectable | High | Waiting for an entire |
|
||||
: : Change Integration : : feature stack of multiple :
|
||||
: : : : interdependent CLs to be :
|
||||
: : : : approved before merging :
|
||||
: : : : the base commits. :
|
||||
| **T5-03** | Explicit Bug Tracker | Medium | Submitting a fix or |
|
||||
: : Linking for Context : : revert without :
|
||||
: : Restoration : : referencing the :
|
||||
: : : : corresponding bug tracker :
|
||||
: : : : issue detailing the :
|
||||
: : : : specific regression or :
|
||||
: : : : stack trace. :
|
||||
| **T5-04** | Atomic Change List | Medium | Submitting a single large |
|
||||
: : Decomposition : : CL that touches multiple :
|
||||
: : : : isolated components or :
|
||||
: : : : implements several :
|
||||
: : : : distinct features :
|
||||
: : : : simultaneously. :
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
### Rules
|
||||
|
||||
#### T5-01: Strict Commit Message Bug Tag Formatting
|
||||
|
||||
> **Rule:** Must use the exact `Bug: <number>` syntax in commit messages to
|
||||
> properly link issue trackers.
|
||||
>
|
||||
> **What:** Commit messages must link directly to issue trackers using the
|
||||
> explicit 'Bug: <number>' syntax to allow reliable parsing by CI/CD and history
|
||||
> tracking systems.
|
||||
>
|
||||
> **Applies To:** Commit messages across all git-repo changes.
|
||||
>
|
||||
> **Why:** Improperly formatted bug tags fail to link with the external issue
|
||||
> tracker, severing historical context and breaking automated post-submit
|
||||
> tracking workflows. Failing to adhere to this typically results in **Broken
|
||||
> Traceability / Pre-submit Failure**.
|
||||
|
||||
**Trap 1: Providing free-text descriptions, arbitrary prefixes, or non-standard
|
||||
bug references in the commit block.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
```text
|
||||
Fixes bug 486536908
|
||||
Closes issue 486536908
|
||||
```
|
||||
|
||||
**Do:**
|
||||
|
||||
```text
|
||||
Bug: 486536908
|
||||
```
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
#### T5-02: Atomic and Bisectable Change Integration
|
||||
|
||||
> **Rule:** Always submit code incrementally as isolated, functional units
|
||||
> rather than hoarding monolithic stacks.
|
||||
>
|
||||
> **What:** Code changes must be submitted incrementally as isolated, functional
|
||||
> units rather than waiting to merge a massive interdependent stack all at once.
|
||||
>
|
||||
> **Applies To:** Git commit history, PR structuring, and stack-based code
|
||||
> integration.
|
||||
>
|
||||
> **Why:** Contributors accustomed to integrating full monolithic stacks at once
|
||||
> held off on landing initial, stable changes. This practice hinders the ability
|
||||
> to isolate regressions via `git bisect` and prevents foundational code from
|
||||
> "baking" in production. Failing to adhere to this typically results in
|
||||
> **Bisection Breakage / Monolithic Rollbacks**.
|
||||
|
||||
**Trap 1: Waiting for an entire feature stack of multiple interdependent CLs to
|
||||
be approved before merging the base commits.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
* Holding all changes in a stack locally or in code review until the final
|
||||
feature patch is approved, then landing 10+ patches simultaneously.
|
||||
|
||||
**Do:**
|
||||
|
||||
* Landing initial, independent CLs one-by-one as soon as they are approved.
|
||||
Ensuring each commit is independently usable and does not break the build.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
#### T5-03: Explicit Bug Tracker Linking for Context Restoration
|
||||
|
||||
> **Rule:** Must include a direct URL to the relevant bug tracker issue
|
||||
> documenting the failure traceback when submitting a regression fix or revert.
|
||||
>
|
||||
> **What:** When submitting a change (especially a revert or bug fix) addressing
|
||||
> a specific runtime regression, the commit metadata or patchset-level comments
|
||||
> must include a direct link to the bug tracker issue documenting the failure
|
||||
> traceback.
|
||||
>
|
||||
> **Applies To:** Commit messages and patchset documentation during code
|
||||
> reviews, particularly for reverts.
|
||||
>
|
||||
> **Why:** A previous commit caused a runtime regression (e.g., an
|
||||
> AttributeError related to a missing object attribute). Without linking the
|
||||
> specific issue containing the traceback, reviewers lacked the necessary
|
||||
> context to justify restoring the previous codebase state. Failing to adhere to
|
||||
> this typically results in **Undocumented Regression / Context Loss**.
|
||||
|
||||
**Trap 1: Submitting a fix or revert without referencing the corresponding bug
|
||||
tracker issue detailing the specific regression or stack trace.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
* Reverting a change with a vague description like "Fixing previous breakage"
|
||||
or "Reverting due to pipeline failure" without providing the traceback
|
||||
source.
|
||||
|
||||
**Do:**
|
||||
|
||||
* Linking the specific issue tracker URL containing the exact failure mode.
|
||||
Example: "for more context, see
|
||||
https://g-issues.gerritcodereview.com/issues/[ISSUE_ID]#comment4"
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
#### T5-04: Atomic Change List Decomposition
|
||||
|
||||
> **Rule:** Never submit large, monolithic change lists; always decompose them
|
||||
> into logically independent patchsets.
|
||||
>
|
||||
> **What:** Large, monolithic change lists (CLs) must be broken down into
|
||||
> smaller, logically independent patchsets to ensure accurate review and
|
||||
> historical bisectability.
|
||||
>
|
||||
> **Applies To:** Version control history and code review scoping.
|
||||
>
|
||||
> **Why:** Massive CLs heavily increase reviewer cognitive load, making thorough
|
||||
> reviews impossible and complicating future `git bisect` operations when
|
||||
> tracking down the origin of a regression. Failing to adhere to this typically
|
||||
> results in **Unreviewable Monolithic Change**.
|
||||
|
||||
**Trap 1: Submitting a single large CL that touches multiple isolated components
|
||||
or implements several distinct features simultaneously.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
* A single CL containing sweeping refactoring, new feature implementation, and
|
||||
unrelated bug fixes.
|
||||
|
||||
**Do:**
|
||||
|
||||
* Breaking the monolithic change into smaller, logically dependent or
|
||||
independent CLs where each addresses one specific piece of the feature or
|
||||
refactor.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
### Cross-Domain Dependencies
|
||||
|
||||
* **Downstream:** T1 | Gerrit Submission and Labeling Workflow - *Gerrit and
|
||||
CI pipelines strictly rely on standardized commit metadata to link tracking
|
||||
issues and depend on atomic patchsets to execute automated review and
|
||||
verification correctly.*
|
||||
|
||||
## Chapter: Testing Strategy and State Isolation
|
||||
|
||||
**Context:** This chapter governs test implementation boundaries, emphasizing
|
||||
pragmatic mocking limits to prevent false positives and detailing acceptable
|
||||
workflows for deferred test coverage. Strict adherence ensures robust state
|
||||
isolation and maintains development velocity without compromising verification
|
||||
integrity.
|
||||
|
||||
### Summary
|
||||
|
||||
| Rule ID | Principle / Constraint | Priority | Primary Symptom / Trap |
|
||||
| :-------- | :----------------------- | :------- | :------------------------- |
|
||||
| **T6-01** | Pragmatic Mocking | Medium | Mocking the entire core |
|
||||
: : Boundaries in Unit Tests : : state or framework :
|
||||
: : : : dependencies just to force :
|
||||
: : : : a unit test for a highly :
|
||||
: : : : integrated function. :
|
||||
| **T6-02** | Deferred Test | Medium | Submitting functional code |
|
||||
: : Implementation via : : without matching test :
|
||||
: : Follow-up : : coverage and stalling the :
|
||||
: : : : merge while complex tests :
|
||||
: : : : are written. :
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
### Rules
|
||||
|
||||
#### T6-01: Pragmatic Mocking Boundaries in Unit Tests
|
||||
|
||||
> **Rule:** Always restrict unit tests to isolated methods and avoid aggressive
|
||||
> mocking of core functionality to prevent brittle, false-positive verification.
|
||||
>
|
||||
> **What:** Do not aggressively mock core functionality in unit tests; restrict
|
||||
> unit tests to isolated methods to avoid creating brittle tests based on false
|
||||
> assumptions when an integration framework is unavailable.
|
||||
>
|
||||
> **Applies To:** Test suite implementation (Unit vs. Integration testing
|
||||
> boundaries).
|
||||
>
|
||||
> **Why:** Over-mocking complex systems in unit tests leads to scenarios where
|
||||
> tests pass but the core integration fails in production because the unit test
|
||||
> mocks assumed incorrect behavior about the underlying environment. Failing to
|
||||
> adhere to this typically results in **False Positive Test Passage**.
|
||||
|
||||
**Trap 1: Mocking the entire core state or framework dependencies just to force
|
||||
a unit test for a highly integrated function.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
* Mocking file systems, external processes, and global state heavily to test a
|
||||
core workflow orchestrator in a unit test suite.
|
||||
|
||||
**Do:**
|
||||
|
||||
* Limiting unit tests strictly to isolated utility methods (e.g., adding
|
||||
promisor files) and explicitly documenting testing gaps that require
|
||||
integration test frameworks.
|
||||
|
||||
**Exceptions:** Isolated helper methods or purely functional data
|
||||
transformations should be fully unit tested with appropriate mocked inputs.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
#### T6-02: Deferred Test Implementation via Follow-up
|
||||
|
||||
> **Rule:** Never stall critical feature merges indefinitely for test
|
||||
> implementation if maintainers authorize formalized, immediate follow-up test
|
||||
> coverage.
|
||||
>
|
||||
> **What:** New logic requires automated tests; however, reviewers may permit
|
||||
> test coverage to be implemented in a subsequent follow-up CL to maintain
|
||||
> development velocity.
|
||||
>
|
||||
> **Applies To:** Feature development, regression testing, and code review
|
||||
> criteria.
|
||||
>
|
||||
> **Why:** Reviewers identified a lack of test coverage for new functionality
|
||||
> but opted not to block the immediate patchset, instead formalizing the test
|
||||
> requirement as a near-term follow-up task. Failing to adhere to this typically
|
||||
> results in **Missing Test Coverage**.
|
||||
|
||||
**Trap 1: Submitting functional code without matching test coverage and stalling
|
||||
the merge while complex tests are written.**
|
||||
|
||||
**Don't:**
|
||||
|
||||
* Blocking a necessary feature indefinitely due to missing unit tests when a
|
||||
follow-up CL is viable and acceptable to maintainers.
|
||||
|
||||
**Do:**
|
||||
|
||||
* Approve the feature with an explicit, documented 'TODO' for a follow-up CL
|
||||
dedicated strictly to adding the corresponding automated tests.
|
||||
|
||||
**Exceptions:** Critical path features or security fixes where a lack of
|
||||
immediate coverage introduces an unacceptable regression risk.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
### Cross-Domain Dependencies
|
||||
|
||||
* **Upstream:** T1 | Gerrit Submission and Labeling Workflow - *Reviewer
|
||||
approval mechanisms and label enforcement dictate when a feature can merge
|
||||
while deferring tests to a follow-up CL.*
|
||||
* **Downstream:** T3 | CI Builder Environment and Execution Integrity -
|
||||
*Pragmatically bounded unit and integration tests ensure reliable CI
|
||||
pipeline execution without false-positive success markers.*
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
# Copyright (C) 2026 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# This file contains version pins of a few tools used by run_tests.
|
||||
|
||||
# Pin resolved versions in the repo, to reduce trust in the CIPD backend.
|
||||
#
|
||||
# Most of these tools are generated via builders at
|
||||
# https://ci.chromium.org/p/infra/g/infra/console
|
||||
#
|
||||
# For these, the git revision is the one of
|
||||
# https://chromium.googlesource.com/infra/infra.git.
|
||||
#
|
||||
# To regenerate them (after modifying this file):
|
||||
# cipd ensure-file-resolve -ensure-file cipd_manifest.txt
|
||||
$ResolvedVersions cipd_manifest.versions
|
||||
|
||||
# Supported platforms. Feel free to add more as long as the tools work.
|
||||
$VerifiedPlatform linux-amd64 linux-arm64 mac-amd64 mac-arm64
|
||||
|
||||
infra/3pp/tools/help2man/${platform} version:3@1.49.3
|
||||
@@ -0,0 +1,18 @@
|
||||
# This file is auto-generated by 'cipd ensure-file-resolve'.
|
||||
# Do not modify manually. All changes will be overwritten.
|
||||
|
||||
infra/3pp/tools/help2man/linux-amd64
|
||||
version:3@1.49.3
|
||||
82ySLC4--mvjnmuwj_j_2odHLj5zFaNFpWvfCyMunY0C
|
||||
|
||||
infra/3pp/tools/help2man/linux-arm64
|
||||
version:3@1.49.3
|
||||
szfWfjI4RyCT6gIB48CeUOGG6_Un4wfbIb0Vvs5M9MUC
|
||||
|
||||
infra/3pp/tools/help2man/mac-amd64
|
||||
version:3@1.49.3
|
||||
t7iyQx5TnGbuJfj6zMTvjPdBl-Cb_w5papUh80NI2S8C
|
||||
|
||||
infra/3pp/tools/help2man/mac-arm64
|
||||
version:3@1.49.3
|
||||
t5eD-eLKRpnSifZdbi072U8bnPfGye1pIMmQ07nxJ7kC
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
import pager
|
||||
|
||||
@@ -84,28 +85,43 @@ def _Color(fg=None, bg=None, attr=None):
|
||||
DEFAULT = None
|
||||
|
||||
|
||||
def SetDefaultColoring(state):
|
||||
class _CheckConsoleSentinel:
|
||||
"""Sentinel for checking console coloring."""
|
||||
|
||||
|
||||
# Placholder value that indicates we need to check if the user is in an
|
||||
# interactive terminal session to determine if we turn on color or not.
|
||||
_CHECK_CONSOLE = _CheckConsoleSentinel()
|
||||
|
||||
# https://git-scm.com/docs/git-config#Documentation/git-config.txt-colorui
|
||||
_CONFIG_TO_COLOR_SETTING = {
|
||||
"false": False,
|
||||
"never": False,
|
||||
"no": False,
|
||||
"auto": _CHECK_CONSOLE,
|
||||
"true": _CHECK_CONSOLE,
|
||||
"yes": _CHECK_CONSOLE,
|
||||
"always": True,
|
||||
}
|
||||
|
||||
|
||||
def SetDefaultColoring(state: Optional[str]) -> None:
|
||||
"""Set coloring behavior to |state|.
|
||||
|
||||
This is useful for overriding config options via the command line.
|
||||
"""
|
||||
if state is None:
|
||||
# Leave it alone -- return quick!
|
||||
return
|
||||
|
||||
global DEFAULT
|
||||
state = state.lower()
|
||||
if state in ("auto",):
|
||||
|
||||
if isinstance(state, str):
|
||||
state = state.lower()
|
||||
if state in _CONFIG_TO_COLOR_SETTING:
|
||||
DEFAULT = state
|
||||
elif state in ("always", "yes", "true", True):
|
||||
DEFAULT = "always"
|
||||
elif state in ("never", "no", "false", False):
|
||||
DEFAULT = "never"
|
||||
|
||||
|
||||
class Coloring:
|
||||
def __init__(self, config, section_type):
|
||||
self._section = "color.%s" % section_type
|
||||
self._section = f"color.{section_type}"
|
||||
self._config = config
|
||||
self._out = sys.stdout
|
||||
|
||||
@@ -114,16 +130,12 @@ class Coloring:
|
||||
on = self._config.GetString(self._section)
|
||||
if on is None:
|
||||
on = self._config.GetString("color.ui")
|
||||
if isinstance(on, str):
|
||||
on = on.lower()
|
||||
|
||||
if on == "auto":
|
||||
if pager.active or os.isatty(1):
|
||||
self._on = True
|
||||
else:
|
||||
self._on = False
|
||||
elif on in ("true", "always"):
|
||||
self._on = True
|
||||
else:
|
||||
self._on = False
|
||||
self._on = _CONFIG_TO_COLOR_SETTING.get(on, _CHECK_CONSOLE)
|
||||
if self._on is _CHECK_CONSOLE:
|
||||
self._on = pager.active or os.isatty(1)
|
||||
|
||||
def redirect(self, out):
|
||||
self._out = out
|
||||
@@ -210,6 +222,7 @@ class Coloring:
|
||||
if have_fg:
|
||||
bg = a
|
||||
else:
|
||||
have_fg = True
|
||||
fg = a
|
||||
elif is_attr(a):
|
||||
attr = a
|
||||
|
||||
+100
-14
@@ -12,10 +12,12 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import contextlib
|
||||
import multiprocessing
|
||||
import optparse
|
||||
import os
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from error import InvalidProjectGroupsError
|
||||
from error import NoSuchProjectError
|
||||
@@ -24,6 +26,10 @@ from event_log import EventLog
|
||||
import progress
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from project import Project
|
||||
|
||||
|
||||
# Are we generating man-pages?
|
||||
GENERATE_MANPAGES = os.environ.get("_REPO_GENERATE_MANPAGES_") == " indeed! "
|
||||
|
||||
@@ -60,6 +66,10 @@ class Command:
|
||||
# command to show short-vs-full summaries.
|
||||
COMMON = False
|
||||
|
||||
# Whether this command should respect the smart sync override manifest if
|
||||
# it exists.
|
||||
RESPECT_SMART_SYNC_OVERRIDE = True
|
||||
|
||||
# Whether this command supports running in parallel. If greater than 0,
|
||||
# it is the number of parallel jobs to default to.
|
||||
PARALLEL_JOBS = None
|
||||
@@ -70,6 +80,14 @@ class Command:
|
||||
# migrated subcommands can set it to False.
|
||||
MULTI_MANIFEST_SUPPORT = True
|
||||
|
||||
# Shared data across parallel execution workers.
|
||||
_parallel_context = None
|
||||
|
||||
@classmethod
|
||||
def get_parallel_context(cls):
|
||||
assert cls._parallel_context is not None
|
||||
return cls._parallel_context
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
repodir=None,
|
||||
@@ -92,6 +110,11 @@ class Command:
|
||||
def WantPager(self, _opt):
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_multiprocessing_active() -> bool:
|
||||
"""Whether the current process is a worker in a pool."""
|
||||
return multiprocessing.current_process().name != "MainProcess"
|
||||
|
||||
def ReadEnvironmentOptions(self, opts):
|
||||
"""Set options from environment variables."""
|
||||
|
||||
@@ -228,6 +251,12 @@ class Command:
|
||||
# from the user's perspective.
|
||||
opt.outer_manifest = True
|
||||
|
||||
if self.RESPECT_SMART_SYNC_OVERRIDE:
|
||||
if self.manifest:
|
||||
self.TryOverrideManifestWithSmartSync(self.manifest)
|
||||
if self.outer_manifest and self.outer_manifest != self.manifest:
|
||||
self.TryOverrideManifestWithSmartSync(self.outer_manifest)
|
||||
|
||||
def ValidateOptions(self, opt, args):
|
||||
"""Validate the user options & arguments before executing.
|
||||
|
||||
@@ -242,9 +271,39 @@ class Command:
|
||||
"""Perform the action, after option parsing is complete."""
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
@classmethod
|
||||
@contextlib.contextmanager
|
||||
def ParallelContext(cls):
|
||||
"""Obtains the context, which is shared to ExecuteInParallel workers.
|
||||
|
||||
Callers can store data in the context dict before invocation of
|
||||
ExecuteInParallel. The dict will then be shared to child workers of
|
||||
ExecuteInParallel.
|
||||
"""
|
||||
assert cls._parallel_context is None
|
||||
cls._parallel_context = {}
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
cls._parallel_context = None
|
||||
|
||||
@classmethod
|
||||
def _InitParallelWorker(cls, context, initializer):
|
||||
cls._parallel_context = context
|
||||
if initializer:
|
||||
initializer()
|
||||
|
||||
@classmethod
|
||||
def ExecuteInParallel(
|
||||
jobs, func, inputs, callback, output=None, ordered=False
|
||||
cls,
|
||||
jobs,
|
||||
func,
|
||||
inputs,
|
||||
callback,
|
||||
output=None,
|
||||
ordered=False,
|
||||
chunksize=WORKER_BATCH_SIZE,
|
||||
initializer=None,
|
||||
):
|
||||
"""Helper for managing parallel execution boiler plate.
|
||||
|
||||
@@ -269,6 +328,9 @@ class Command:
|
||||
output: An output manager. May be progress.Progess or
|
||||
color.Coloring.
|
||||
ordered: Whether the jobs should be processed in order.
|
||||
chunksize: The number of jobs processed in batch by parallel
|
||||
workers.
|
||||
initializer: Worker initializer.
|
||||
|
||||
Returns:
|
||||
The |callback| function's results are returned.
|
||||
@@ -278,12 +340,16 @@ class Command:
|
||||
if len(inputs) == 1 or jobs == 1:
|
||||
return callback(None, output, (func(x) for x in inputs))
|
||||
else:
|
||||
with multiprocessing.Pool(jobs) as pool:
|
||||
with multiprocessing.Pool(
|
||||
jobs,
|
||||
initializer=cls._InitParallelWorker,
|
||||
initargs=(cls._parallel_context, initializer),
|
||||
) as pool:
|
||||
submit = pool.imap if ordered else pool.imap_unordered
|
||||
return callback(
|
||||
pool,
|
||||
output,
|
||||
submit(func, inputs, chunksize=WORKER_BATCH_SIZE),
|
||||
submit(func, inputs, chunksize=chunksize),
|
||||
)
|
||||
finally:
|
||||
if isinstance(output, progress.Progress):
|
||||
@@ -324,7 +390,7 @@ class Command:
|
||||
manifest=None,
|
||||
groups="",
|
||||
missing_ok=False,
|
||||
submodules_ok=False,
|
||||
submodules_ok=None,
|
||||
all_manifests=False,
|
||||
):
|
||||
"""A list of projects that match the arguments.
|
||||
@@ -334,7 +400,9 @@ class Command:
|
||||
manifest: an XmlManifest, the manifest to use, or None for default.
|
||||
groups: a string, the manifest groups in use.
|
||||
missing_ok: a boolean, whether to allow missing projects.
|
||||
submodules_ok: a boolean, whether to allow submodules.
|
||||
submodules_ok: whether to allow submodules. True allows them for
|
||||
all projects, False disallows them for all projects, and None
|
||||
defers to each project's sync-s setting.
|
||||
all_manifests: a boolean, if True then all manifests and
|
||||
submanifests are used. If False, then only the local
|
||||
(sub)manifest is used.
|
||||
@@ -352,16 +420,22 @@ class Command:
|
||||
all_projects_list = manifest.projects
|
||||
result = []
|
||||
|
||||
def should_include_submodules(project: "Project") -> bool:
|
||||
if submodules_ok is None:
|
||||
return project.sync_s
|
||||
return submodules_ok
|
||||
|
||||
if not groups:
|
||||
groups = manifest.GetGroupsStr()
|
||||
groups = manifest.GetManifestGroupsStr()
|
||||
groups = [x for x in re.split(r"[,\s]+", groups) if x]
|
||||
|
||||
if not args:
|
||||
derived_projects = {}
|
||||
for project in all_projects_list:
|
||||
if submodules_ok or project.sync_s:
|
||||
if should_include_submodules(project):
|
||||
derived_projects.update(
|
||||
(p.name, p) for p in project.GetDerivedSubprojects()
|
||||
(p.RelPath(local=False), p)
|
||||
for p in project.GetDerivedSubprojects()
|
||||
)
|
||||
all_projects_list.extend(derived_projects.values())
|
||||
for project in all_projects_list:
|
||||
@@ -400,7 +474,7 @@ class Command:
|
||||
if (
|
||||
project
|
||||
and not project.Derived
|
||||
and (submodules_ok or project.sync_s)
|
||||
and should_include_submodules(project)
|
||||
):
|
||||
search_again = False
|
||||
for subproject in project.GetDerivedSubprojects():
|
||||
@@ -465,6 +539,22 @@ class Command:
|
||||
)
|
||||
return result
|
||||
|
||||
def GetSmartSyncOverridePath(self, manifest=None) -> str:
|
||||
"""Return the path where smart sync writes its override manifest."""
|
||||
if manifest is None:
|
||||
manifest = self.manifest
|
||||
return os.path.join(
|
||||
manifest.manifestProject.worktree, "smart_sync_override.xml"
|
||||
)
|
||||
|
||||
def TryOverrideManifestWithSmartSync(self, manifest=None) -> None:
|
||||
"""Override manifest with smart_sync_override.xml if it exists."""
|
||||
if manifest is None:
|
||||
manifest = self.manifest
|
||||
smart_sync_manifest_path = self.GetSmartSyncOverridePath(manifest)
|
||||
if os.path.isfile(smart_sync_manifest_path):
|
||||
manifest.Override(smart_sync_manifest_path)
|
||||
|
||||
def ManifestList(self, opt):
|
||||
"""Yields all of the manifests to traverse.
|
||||
|
||||
@@ -501,7 +591,3 @@ class MirrorSafeCommand:
|
||||
"""Command permits itself to run within a mirror, and does not require a
|
||||
working directory.
|
||||
"""
|
||||
|
||||
|
||||
class GitcClientCommand:
|
||||
"""Command that requires the local client to be a GITC client."""
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Copyright 2021 The Android Open Source Project
|
||||
# Copyright (C) 2021 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
||||
+453
@@ -0,0 +1,453 @@
|
||||
#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:' \
|
||||
'--recurse-submodules[Sync submodules]' \
|
||||
'--no-recurse-submodules[Do not sync submodules]' \
|
||||
'--fetch-submodules[Deprecated alias for --recurse-submodules]' \
|
||||
'--no-fetch-submodules[Deprecated alias for --no-recurse-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]' \
|
||||
'--fix[Automatically fix]' \
|
||||
'*: :->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
|
||||
@@ -0,0 +1,63 @@
|
||||
# Fetch Command Contract
|
||||
|
||||
The `repo.fetchcmd` configuration allows specifying a custom command to be
|
||||
executed during `repo sync` to fetch objects, instead of using standard
|
||||
`git fetch`. This is particularly useful in environments with virtualized
|
||||
filesystems or lazy checkouts where fetching metadata and downloading file
|
||||
contents should be decoupled.
|
||||
|
||||
## Configuration
|
||||
|
||||
To use this feature, set the following in `.repo/manifests.git/config`:
|
||||
```ini
|
||||
[repo]
|
||||
fetchcmd = "your custom command here"
|
||||
uselocalgitdirs = true
|
||||
```
|
||||
Setting `repo.fetchcmd` **requires** `repo.uselocalgitdirs` to be set to `true`.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The custom command is executed in a subshell populated with standard
|
||||
project-context environment variables. For details on standard variables (such
|
||||
as `REPO_PROJECT`, `REPO_PATH`, `REPO_PROJECT_FETCH_URL`, etc.), see the
|
||||
Environment section in `repo help forall` or `subcmds/forall.py`.
|
||||
|
||||
The following environment variable is specific to `repo.fetchcmd`:
|
||||
|
||||
* `REPO_TREV`: The target revision resolved to a full commit hash.
|
||||
|
||||
## Contract
|
||||
|
||||
### Postconditions on exit 0
|
||||
|
||||
After the fetch command exits with status 0, `repo` expects the following
|
||||
postconditions to be met:
|
||||
|
||||
1. `git cat-file -e REPO_TREV` succeeds (the commit must exist in the object
|
||||
store).
|
||||
2. The mapped local tracking ref (e.g. `refs/remotes/REPO_REMOTE/<branch>`
|
||||
for a branch revision, or the tag ref itself for a tag) must point to
|
||||
`REPO_TREV`.
|
||||
3. `FETCH_HEAD` must point to `REPO_TREV`.
|
||||
4. The commit graph from `REPO_TREV` must be reachable far enough to compute
|
||||
merge bases with local branches.
|
||||
|
||||
### Invariants
|
||||
|
||||
* The command should be idempotent; fetching the same `REPO_TREV` twice should
|
||||
be a no-op.
|
||||
* Only `FETCH_HEAD` and `refs/remotes/*` should be modified to preserve
|
||||
`repo sync --network-only` semantics. `HEAD` and local branches must not be
|
||||
touched by the fetch command.
|
||||
* Dirty worktree state must be preserved.
|
||||
* The command is **not** executed for `MetaProject`s (i.e. the internal `repo`
|
||||
repository itself at `.repo/repo` and the `manifests` repository at
|
||||
`.repo/manifests`).
|
||||
|
||||
### Failure
|
||||
|
||||
* A non-zero exit status aborts the project's sync, and the command's stderr
|
||||
is surfaced to the user.
|
||||
* `repo` verifies the tracking ref and target reachability after exit 0. Any
|
||||
mismatch is treated as a failure.
|
||||
@@ -141,7 +141,7 @@ Instead, you should use standard Git workflows like [git worktree] or
|
||||
(e.g. a local mirror & a public review server) while avoiding duplicating
|
||||
the content. However, this can run into problems if different remotes use
|
||||
the same path on their respective servers. Best to avoid that.
|
||||
* `subprojects/`: Like `projects/`, but for git submodules.
|
||||
* `modules/`: Like `projects/`, but for git submodules.
|
||||
* `subproject-objects/`: Like `project-objects/`, but for git submodules.
|
||||
* `worktrees/`: Bare checkouts of every project synced by the manifest. The
|
||||
filesystem layout matches the `<project name=...` setting in the manifest
|
||||
@@ -182,7 +182,7 @@ User controlled settings are initialized when running `repo init`.
|
||||
| user.email | `--config-name` | User's e-mail address; Copied into `.git/config` when checking out a new project |
|
||||
| user.name | `--config-name` | User's name; Copied into `.git/config` when checking out a new project |
|
||||
|
||||
[partial git clones]: https://git-scm.com/docs/gitrepository-layout#_code_partialclone_code
|
||||
[partial git clones]: https://git-scm.com/docs/partial-clone
|
||||
[superproject]: https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects
|
||||
|
||||
### Repo hooks settings
|
||||
|
||||
+103
-58
@@ -51,15 +51,17 @@ following DTD:
|
||||
<!ATTLIST default dest-branch CDATA #IMPLIED>
|
||||
<!ATTLIST default upstream CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-j CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-j-max CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-c CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-s CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-tags CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT manifest-server EMPTY>
|
||||
<!ATTLIST manifest-server url CDATA #REQUIRED>
|
||||
<!ATTLIST manifest-server helper CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT submanifest EMPTY>
|
||||
<!ATTLIST submanifest name ID #REQUIRED>
|
||||
<!ATTLIST submanifest name ID #REQUIRED>
|
||||
<!ATTLIST submanifest remote IDREF #IMPLIED>
|
||||
<!ATTLIST submanifest project CDATA #IMPLIED>
|
||||
<!ATTLIST submanifest manifest-name CDATA #IMPLIED>
|
||||
@@ -72,18 +74,19 @@ following DTD:
|
||||
project*,
|
||||
copyfile*,
|
||||
linkfile*)>
|
||||
<!ATTLIST project name CDATA #REQUIRED>
|
||||
<!ATTLIST project path CDATA #IMPLIED>
|
||||
<!ATTLIST project remote IDREF #IMPLIED>
|
||||
<!ATTLIST project revision CDATA #IMPLIED>
|
||||
<!ATTLIST project dest-branch CDATA #IMPLIED>
|
||||
<!ATTLIST project groups CDATA #IMPLIED>
|
||||
<!ATTLIST project sync-c CDATA #IMPLIED>
|
||||
<!ATTLIST project sync-s CDATA #IMPLIED>
|
||||
<!ATTLIST project sync-tags CDATA #IMPLIED>
|
||||
<!ATTLIST project upstream CDATA #IMPLIED>
|
||||
<!ATTLIST project clone-depth CDATA #IMPLIED>
|
||||
<!ATTLIST project force-path CDATA #IMPLIED>
|
||||
<!ATTLIST project name CDATA #REQUIRED>
|
||||
<!ATTLIST project path CDATA #IMPLIED>
|
||||
<!ATTLIST project remote IDREF #IMPLIED>
|
||||
<!ATTLIST project revision CDATA #IMPLIED>
|
||||
<!ATTLIST project dest-branch CDATA #IMPLIED>
|
||||
<!ATTLIST project groups CDATA #IMPLIED>
|
||||
<!ATTLIST project sync-c CDATA #IMPLIED>
|
||||
<!ATTLIST project sync-s CDATA #IMPLIED>
|
||||
<!ATTLIST project sync-tags CDATA #IMPLIED>
|
||||
<!ATTLIST project upstream CDATA #IMPLIED>
|
||||
<!ATTLIST project clone-depth CDATA #IMPLIED>
|
||||
<!ATTLIST project force-path CDATA #IMPLIED>
|
||||
<!ATTLIST project sync-strategy CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT annotation EMPTY>
|
||||
<!ATTLIST annotation name CDATA #REQUIRED>
|
||||
@@ -95,26 +98,30 @@ following DTD:
|
||||
<!ATTLIST copyfile dest CDATA #REQUIRED>
|
||||
|
||||
<!ELEMENT linkfile EMPTY>
|
||||
<!ATTLIST linkfile src CDATA #REQUIRED>
|
||||
<!ATTLIST linkfile src CDATA #REQUIRED>
|
||||
<!ATTLIST linkfile dest CDATA #REQUIRED>
|
||||
|
||||
<!ELEMENT extend-project EMPTY>
|
||||
<!ATTLIST extend-project name CDATA #REQUIRED>
|
||||
<!ATTLIST extend-project path CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project dest-path CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project groups CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project revision CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project remote CDATA #IMPLIED>
|
||||
<!ELEMENT extend-project (annotation*,
|
||||
copyfile*,
|
||||
linkfile*)>
|
||||
<!ATTLIST extend-project name CDATA #REQUIRED>
|
||||
<!ATTLIST extend-project path CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project dest-path CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project groups CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project revision CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project remote CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project dest-branch CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project upstream CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project upstream CDATA #IMPLIED>
|
||||
<!ATTLIST extend-project base-rev CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT remove-project EMPTY>
|
||||
<!ATTLIST remove-project name CDATA #IMPLIED>
|
||||
<!ATTLIST remove-project path CDATA #IMPLIED>
|
||||
<!ATTLIST remove-project optional CDATA #IMPLIED>
|
||||
<!ATTLIST remove-project base-rev CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT repo-hooks EMPTY>
|
||||
<!ATTLIST repo-hooks in-project CDATA #REQUIRED>
|
||||
<!ATTLIST repo-hooks in-project CDATA #REQUIRED>
|
||||
<!ATTLIST repo-hooks enabled-list CDATA #REQUIRED>
|
||||
|
||||
<!ELEMENT superproject EMPTY>
|
||||
@@ -123,7 +130,7 @@ following DTD:
|
||||
<!ATTLIST superproject revision CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT contactinfo EMPTY>
|
||||
<!ATTLIST contactinfo bugurl CDATA #REQUIRED>
|
||||
<!ATTLIST contactinfo bugurl CDATA #REQUIRED>
|
||||
|
||||
<!ELEMENT include EMPTY>
|
||||
<!ATTLIST include name CDATA #REQUIRED>
|
||||
@@ -209,7 +216,9 @@ can be found. Used when syncing a revision locked manifest in
|
||||
-c mode to avoid having to sync the entire ref space. Project elements
|
||||
not setting their own `upstream` will inherit this value.
|
||||
|
||||
Attribute `sync-j`: Number of parallel jobs to use when synching.
|
||||
Attribute `sync-j`: Number of parallel jobs to use when syncing.
|
||||
|
||||
Attribute `sync-j-max`: Maximum number of parallel jobs to use when syncing.
|
||||
|
||||
Attribute `sync-c`: Set to true to only sync the given Git
|
||||
branch (specified in the `revision` attribute) rather than the
|
||||
@@ -229,26 +238,12 @@ At most one manifest-server may be specified. The url attribute
|
||||
is used to specify the URL of a manifest server, which is an
|
||||
XML RPC service.
|
||||
|
||||
The manifest server should implement the following RPC methods:
|
||||
See the [smart sync documentation](./smart-sync.md) for more details.
|
||||
|
||||
GetApprovedManifest(branch, target)
|
||||
Attribute `url`: The URL of the manifest server.
|
||||
|
||||
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.
|
||||
Attribute `helper`: Optional name of a remote helper binary to execute to
|
||||
resolve proxying or authentication for the manifest server.
|
||||
|
||||
|
||||
### Element submanifest
|
||||
@@ -302,7 +297,7 @@ should be placed. If not supplied, `revision` is used.
|
||||
|
||||
`path` may not be an absolute path or use "." or ".." path components.
|
||||
|
||||
Attribute `groups`: List of additional groups to which all projects
|
||||
Attribute `groups`: Set of additional groups to which all projects
|
||||
in the included submanifest belong. This appends and recurses, meaning
|
||||
all projects in submanifests carry all parent submanifest groups.
|
||||
Same syntax as the corresponding element of `project`.
|
||||
@@ -370,7 +365,7 @@ When using `repo upload`, changes will be submitted for code
|
||||
review on this branch. If unspecified both here and in the
|
||||
default element, `revision` is used instead.
|
||||
|
||||
Attribute `groups`: List of groups to which this project belongs,
|
||||
Attribute `groups`: Set of groups to which this project belongs,
|
||||
whitespace or comma separated. All projects belong to the group
|
||||
"all", and each project automatically belongs to a group of
|
||||
its name:`name` and path:`path`. E.g. for
|
||||
@@ -401,6 +396,22 @@ rather than the `name` attribute. This attribute only applies to the
|
||||
local mirrors syncing, it will be ignored when syncing the projects in a
|
||||
client working directory.
|
||||
|
||||
Attribute `sync-strategy`: Set the sync strategy used when fetching this
|
||||
project. Currently the only supported value is `stateless`. When set to
|
||||
`stateless`, repo will run a reflog expiration and aggressive garbage collection
|
||||
at the end of the sync process. This is useful for projects that contain
|
||||
large binary files and use `clone-depth="1"`, where garbage can accumulate
|
||||
as binaries are added, deleted, or modified across successive syncs.
|
||||
|
||||
During a stateless sync, repo checks the following before cleaning up:
|
||||
1. The project does not share an object directory with other projects.
|
||||
2. The working tree is clean (no uncommitted changes, no untracked files).
|
||||
3. There are no unpushed local commits.
|
||||
4. There is no Git stash.
|
||||
|
||||
If any of these conditions are not met, repo falls back to a standard
|
||||
sync without garbage collection.
|
||||
|
||||
### Element extend-project
|
||||
|
||||
Modify the attributes of the named project.
|
||||
@@ -410,6 +421,11 @@ attributes of an existing project without completely replacing the
|
||||
existing project definition. This makes the local manifest more robust
|
||||
against changes to the original manifest.
|
||||
|
||||
The `extend-project` element can also contain `annotation`, `copyfile`, and
|
||||
`linkfile` child elements. These are added to the project's definition. A
|
||||
`copyfile` or `linkfile` with a `dest` that already exists in the project
|
||||
will overwrite the original.
|
||||
|
||||
Attribute `path`: If specified, limit the change to projects checked out
|
||||
at the specified path, rather than all projects with the given name.
|
||||
|
||||
@@ -418,7 +434,7 @@ of the repo client where the Git working directory for this project
|
||||
should be placed. This is used to move a project in the checkout by
|
||||
overriding the existing `path` setting.
|
||||
|
||||
Attribute `groups`: List of additional groups to which this project
|
||||
Attribute `groups`: Set of additional groups to which this project
|
||||
belongs. Same syntax as the corresponding element of `project`.
|
||||
|
||||
Attribute `revision`: If specified, overrides the revision of the original
|
||||
@@ -433,22 +449,31 @@ project. Same syntax as the corresponding element of `project`.
|
||||
Attribute `upstream`: If specified, overrides the upstream of the original
|
||||
project. Same syntax as the corresponding element of `project`.
|
||||
|
||||
Attribute `base-rev`: If specified, adds a check against the revision
|
||||
to be extended. Manifest parse will fail and give a list of mismatch extends
|
||||
if the revisions being extended have changed since base-rev was set.
|
||||
Intended for use with layered manifests using hash revisions to prevent
|
||||
patch branches hiding newer upstream revisions. Also compares named refs
|
||||
like branches or tags but is misleading if branches are used as base-rev.
|
||||
Same syntax as the corresponding element of `project`.
|
||||
|
||||
### Element annotation
|
||||
|
||||
Zero or more annotation elements may be specified as children of a
|
||||
project or remote element. Each element describes a name-value pair.
|
||||
For projects, this name-value pair will be exported into each project's
|
||||
environment during a 'forall' command, prefixed with `REPO__`. In addition,
|
||||
there is an optional attribute "keep" which accepts the case insensitive values
|
||||
"true" (default) or "false". This attribute determines whether or not the
|
||||
project element, an extend-project element, or a remote element. Each
|
||||
element describes a name-value pair. For projects, this name-value pair
|
||||
will be exported into each project's environment during a 'forall'
|
||||
command, prefixed with `REPO__`. In addition, there is an optional
|
||||
attribute "keep" which accepts the case insensitive values "true"
|
||||
(default) or "false". This attribute determines whether or not the
|
||||
annotation will be kept when exported with the manifest subcommand.
|
||||
|
||||
### Element copyfile
|
||||
|
||||
Zero or more copyfile elements may be specified as children of a
|
||||
project element. Each element describes a src-dest pair of files;
|
||||
the "src" file will be copied to the "dest" place during `repo sync`
|
||||
command.
|
||||
project element, or an extend-project element. Each element describes a
|
||||
src-dest pair of files; the "src" file will be copied to the "dest"
|
||||
place during `repo sync` command.
|
||||
|
||||
"src" is project relative, "dest" is relative to the top of the tree.
|
||||
Copying from paths outside of the project or to paths outside of the repo
|
||||
@@ -459,10 +484,14 @@ Intermediate paths must not be symlinks either.
|
||||
|
||||
Parent directories of "dest" will be automatically created if missing.
|
||||
|
||||
The files are copied in the order they are specified in the manifests.
|
||||
If multiple elements specify the same source and destination, they will
|
||||
only be applied as one, based on the first occurence. Files are copied
|
||||
before any links specified via linkfile elements are created.
|
||||
|
||||
### Element linkfile
|
||||
|
||||
It's just like copyfile and runs at the same time as copyfile but
|
||||
instead of copying it creates a symlink.
|
||||
It's just like copyfile, but instead of copying it creates a symlink.
|
||||
|
||||
The symlink is created at "dest" (relative to the top of the tree) and
|
||||
points to the path specified by "src" which is a path in the project.
|
||||
@@ -472,6 +501,11 @@ Parent directories of "dest" will be automatically created if missing.
|
||||
The symlink target may be a file or directory, but it may not point outside
|
||||
of the repo client.
|
||||
|
||||
The links are created in the order they are specified in the manifests.
|
||||
If multiple elements specify the same source and destination, they will
|
||||
only be applied as one, based on the first occurence. Links are created
|
||||
after any files specified via copyfile elements are copied.
|
||||
|
||||
### Element remove-project
|
||||
|
||||
Deletes a project from the internal manifest table, possibly
|
||||
@@ -496,6 +530,14 @@ name. Logic otherwise behaves like both are specified.
|
||||
Attribute `optional`: Set to true to ignore remove-project elements with no
|
||||
matching `project` element.
|
||||
|
||||
Attribute `base-rev`: If specified, adds a check against the revision
|
||||
to be removed. Manifest parse will fail and give a list of mismatch removes
|
||||
if the revisions being removed have changed since base-rev was set.
|
||||
Intended for use with layered manifests using hash revisions to prevent
|
||||
patch branches hiding newer upstream revisions. Also compares named refs
|
||||
like branches or tags but is misleading if branches are used as base-rev.
|
||||
Same syntax as the corresponding element of `project`.
|
||||
|
||||
### Element repo-hooks
|
||||
|
||||
NB: See the [practical documentation](./repo-hooks.md) for using repo hooks.
|
||||
@@ -561,13 +603,16 @@ the manifest repository's root.
|
||||
"name" may not be an absolute path or use "." or ".." path components.
|
||||
These restrictions are not enforced for [Local Manifests].
|
||||
|
||||
Attribute `groups`: List of additional groups to which all projects
|
||||
Attribute `groups`: Set of additional groups to which all projects
|
||||
in the included manifest belong. This appends and recurses, meaning
|
||||
all projects in included manifests carry all parent include groups.
|
||||
This also applies to all extend-project elements in the included manifests.
|
||||
Same syntax as the corresponding element of `project`.
|
||||
|
||||
Attribute `revision`: Name of a Git branch (e.g. `main` or `refs/heads/main`)
|
||||
default to which all projects in the included manifest belong.
|
||||
default to which all projects in the included manifest belong. This recurses,
|
||||
meaning it will apply to all projects in all manifests included as a result of
|
||||
this element.
|
||||
|
||||
## Local Manifests {#local-manifests}
|
||||
|
||||
|
||||
@@ -96,6 +96,9 @@ If that tag is valid, then repo will warn and use that commit instead.
|
||||
|
||||
If that tag cannot be verified, it gives up and forces the user to resolve.
|
||||
|
||||
If env variable `REPO_SKIP_SELF_UPDATE` is defined, this will
|
||||
bypass the self update algorithm.
|
||||
|
||||
### Force an update
|
||||
|
||||
The `repo selfupdate` command can be used to force an immediate update.
|
||||
|
||||
+59
-1
@@ -83,6 +83,20 @@ then check it directly. Hooks should not normally modify the active git repo
|
||||
the user. Although user interaction is discouraged in the common case, it can
|
||||
be useful when deploying automatic fixes.
|
||||
|
||||
### Safe Prompts
|
||||
|
||||
If the repo command that triggered the hook supports a "yes" option (e.g.,
|
||||
`repo upload --yes`), this option is propagated to the hook's `main` function
|
||||
as `yes` parameter (defaulting to `False`). Hooks can use this to bypass
|
||||
interactive confirmation prompts for safe non-modifying operations.
|
||||
|
||||
### Automated Fixes
|
||||
|
||||
If the repo command that triggered the hook supports a "fix" option (e.g.,
|
||||
`repo upload --fix`), this option is propagated to the hook's `main` function
|
||||
as `fix` parameter (defaulting to `False`). Hooks can use this to automatically
|
||||
apply fixes without prompting the user.
|
||||
|
||||
### Shebang Handling
|
||||
|
||||
*** note
|
||||
@@ -119,7 +133,7 @@ This hook runs when people run `repo upload`.
|
||||
The `pre-upload.py` file should be defined like:
|
||||
|
||||
```py
|
||||
def main(project_list, worktree_list=None, **kwargs):
|
||||
def main(project_list, worktree_list=None, fix=False, yes=False, **kwargs):
|
||||
"""Main function invoked directly by repo.
|
||||
|
||||
We must use the name "main" as that is what repo requires.
|
||||
@@ -130,6 +144,50 @@ def main(project_list, worktree_list=None, **kwargs):
|
||||
project_list, so that each entry in project_list matches with a
|
||||
directory in worktree_list. If None, we will attempt to calculate
|
||||
the directories automatically.
|
||||
fix: Whether to automatically apply fixes without prompting.
|
||||
yes: Whether to answer yes to all safe prompts (see
|
||||
[Safe Prompts](#safe-prompts)).
|
||||
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,160 @@
|
||||
# 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"
|
||||
helper="repo-remote-helper-name" />
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### Pluggable Remote Helpers
|
||||
|
||||
For custom proxying or authentication, Repo supports pluggable remote helpers.
|
||||
You can declare a helper binary via the optional `helper` attribute on the
|
||||
`<manifest-server>` element.
|
||||
|
||||
If the `helper` attribute is present in `<manifest-server>`:
|
||||
1. Repo searches your system `PATH` for the specified helper binary (e.g.,
|
||||
`repo-remote-sso`). If the helper cannot be found in `PATH`, Repo will raise
|
||||
a `SmartSyncError` and abort the sync.
|
||||
2. The helper is executed with the manifest server URL as its first argument:
|
||||
```bash
|
||||
<helper-binary-name> <url>
|
||||
```
|
||||
3. The helper must output a single-line JSON object on stdout and exit:
|
||||
* On success, return `status: "ok"` and a loopback proxy URL (including
|
||||
scheme, e.g., `http://127.0.0.1:999`):
|
||||
```json
|
||||
{"status": "ok", "message": "http://127.0.0.1:999"}
|
||||
```
|
||||
* On failure, return `status: "error"` and a detailed error message:
|
||||
```json
|
||||
{"status": "error", "message": "unauthorized"}
|
||||
```
|
||||
4. Repo routes the XML-RPC request through the returned proxy address.
|
||||
|
||||
**Timeout Constraint**: The remote helper must complete and exit within 10
|
||||
seconds. If it hangs or exceeds this limit, Repo will force-terminate (`kill`)
|
||||
the process and abort the synchronization.
|
||||
|
||||
## Credentials
|
||||
|
||||
Credentials may be specified directly in typical `username:password`
|
||||
[URI syntax](https://en.wikipedia.org/wiki/URI#Syntax) in the
|
||||
`<manifest-server>` element directly in the manifest.
|
||||
|
||||
If they are not specified, `repo sync` has `--manifest-server-username=USERNAME`
|
||||
and `--manifest-server-password=PASSWORD` options.
|
||||
|
||||
If those are not used, then repo will look up the host in your
|
||||
[`~/.netrc`](https://docs.python.org/3/library/netrc.html) database.
|
||||
|
||||
When making the connection, cookies matching the host are automatically loaded
|
||||
from the cookiejar specified in
|
||||
[Git's `http.cookiefile` setting](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpcookieFile).
|
||||
|
||||
## Manifest Server
|
||||
|
||||
Unfortunately, there are no public reference implementations. Google has an
|
||||
internal one for Android, but it is written using Google's internal systems,
|
||||
so wouldn't be that helpful as a reference.
|
||||
|
||||
That said, the XML-RPC API is pretty simple, so any standard XML-RPC server
|
||||
example would do. Google's internal server uses Python's
|
||||
[xmlrpc.server.SimpleXMLRPCDispatcher](https://docs.python.org/3/library/xmlrpc.server.html).
|
||||
|
||||
## Network API
|
||||
|
||||
The manifest server should implement the following RPC methods.
|
||||
|
||||
### GetApprovedManifest
|
||||
|
||||
> `GetApprovedManifest(branch: str, target: Optional[str]) -> str`
|
||||
|
||||
The meaning of `branch` and `target` is not strictly defined. The server may
|
||||
interpret them however it wants. The recommended interpretation is that the
|
||||
`branch` matches the manifest branch, and `target` is an identifier for your
|
||||
project that matches something users would build.
|
||||
|
||||
See the client section below for how repo typically generates these values.
|
||||
|
||||
The server will return a manifest or an error. If it's an error, repo will
|
||||
show the output directly to the user to provide a limited feedback channel.
|
||||
|
||||
If the user's request is ambiguous and could match multiple manifests, the
|
||||
server has to decide whether to pick one automatically (and silently such that
|
||||
the user won't know there were multiple matches), or return an error and force
|
||||
the user to be more specific.
|
||||
|
||||
### GetManifest
|
||||
|
||||
> `GetManifest(tag: str) -> str`
|
||||
|
||||
The meaning of `tag` is not strictly defined. Projects are encouraged to use
|
||||
a system where the tag matches a unique source state.
|
||||
|
||||
See the client section below for how repo typically generates these values.
|
||||
|
||||
The server will return a manifest or an error. If it's an error, repo will
|
||||
show the output directly to the user to provide a limited feedback channel.
|
||||
|
||||
If the user's request is ambiguous and could match multiple manifests, the
|
||||
server has to decide whether to pick one automatically (and silently such that
|
||||
the user won't know there were multiple matches), or return an error and force
|
||||
the user to be more specific.
|
||||
|
||||
## Client Options
|
||||
|
||||
Once repo has successfully downloaded the manifest from the server, it saves a
|
||||
copy into `.repo/manifests/smart_sync_override.xml` so users can examine it.
|
||||
The next time `repo sync` is run, this file is automatically replaced or removed
|
||||
based on the current set of options.
|
||||
|
||||
### --smart-sync
|
||||
|
||||
Repo will call `GetApprovedManifest(branch[, target])`.
|
||||
|
||||
The `branch` is determined by the current manifest branch as specified by
|
||||
`--manifest-branch=BRANCH` when running `repo init`.
|
||||
|
||||
The `target` is defined by environment variables in the order below. If none
|
||||
of them match, then `target` is omitted. These variables were decided as they
|
||||
match the settings Android build environments automatically setup.
|
||||
|
||||
1. `${SYNC_TARGET}`: If defined, the value is used directly.
|
||||
2. `${TARGET_PRODUCT}-${TARGET_RELEASE}-${TARGET_BUILD_VARIANT}`: If these
|
||||
variables are all defined, then they are merged with `-` and used.
|
||||
3. `${TARGET_PRODUCT}-${TARGET_BUILD_VARIANT}`: If these variables are all
|
||||
defined, then they are merged with `-` and used.
|
||||
|
||||
### --smart-tag=TAG
|
||||
|
||||
Repo will call `GetManifest(TAG)`.
|
||||
+6
-3
@@ -50,8 +50,11 @@ Git worktrees (see the previous section for more info).
|
||||
Repo will use symlinks heavily internally.
|
||||
On *NIX platforms, this isn't an issue, but Windows makes it a bit difficult.
|
||||
|
||||
There are some documents out there for how to do this, but usually the easiest
|
||||
answer is to run your shell as an Administrator and invoke repo/git in that.
|
||||
The easiest method to allow users to create symlinks is by enabling
|
||||
[Windows Developer Mode](https://learn.microsoft.com/en-us/windows/advanced-settings/developer-mode).
|
||||
|
||||
The next easiest answer is to run your shell as an Administrator and invoke
|
||||
repo/git in that.
|
||||
|
||||
This isn't a great solution, but Windows doesn't make this easy, so here we are.
|
||||
|
||||
@@ -142,7 +145,7 @@ You will have to specify this flag every time you upload.
|
||||
|
||||
[mintty]: https://github.com/mintty/mintty/issues/56
|
||||
|
||||
### repohooks always fail with an close_fds error.
|
||||
### repohooks always fail with an close_fds error
|
||||
|
||||
When using the [reference repohooks project][repohooks] included in AOSP,
|
||||
you might see errors like this when running `repo upload`:
|
||||
|
||||
@@ -107,8 +107,8 @@ class GitError(RepoError):
|
||||
return self.message
|
||||
|
||||
|
||||
class GitcUnsupportedError(RepoExitError):
|
||||
"""Gitc no longer supported."""
|
||||
class GitAuthError(RepoExitError):
|
||||
"""Cannot talk to remote due to auth issue."""
|
||||
|
||||
|
||||
class UploadError(RepoError):
|
||||
|
||||
+10
-2
@@ -168,8 +168,10 @@ class EventLog:
|
||||
f.write("\n")
|
||||
|
||||
|
||||
# An integer id that is unique across this invocation of the program.
|
||||
_EVENT_ID = multiprocessing.Value("i", 1)
|
||||
# An integer id that is unique across this invocation of the program, to be set
|
||||
# by the first Add event. We can't set it here since it results in leaked
|
||||
# resources (see: https://issues.gerritcodereview.com/353656374).
|
||||
_EVENT_ID = None
|
||||
|
||||
|
||||
def _NextEventId():
|
||||
@@ -178,6 +180,12 @@ def _NextEventId():
|
||||
Returns:
|
||||
A unique, to this invocation of the program, integer id.
|
||||
"""
|
||||
global _EVENT_ID
|
||||
if _EVENT_ID is None:
|
||||
# There is a small chance of race condition - two parallel processes
|
||||
# setting up _EVENT_ID. However, we expect TASK_COMMAND to happen before
|
||||
# mp kicks in.
|
||||
_EVENT_ID = multiprocessing.Value("i", 1)
|
||||
with _EVENT_ID.get_lock():
|
||||
val = _EVENT_ID.value
|
||||
_EVENT_ID.value += 1
|
||||
|
||||
@@ -34,7 +34,7 @@ def fetch_file(url, verbose=False):
|
||||
"""
|
||||
scheme = urlparse(url).scheme
|
||||
if scheme == "gs":
|
||||
cmd = ["gsutil", "cat", url]
|
||||
cmd = ["gcloud", "storage", "cat", url]
|
||||
errors = []
|
||||
try:
|
||||
result = subprocess.run(
|
||||
@@ -42,7 +42,7 @@ def fetch_file(url, verbose=False):
|
||||
)
|
||||
if result.stderr and verbose:
|
||||
print(
|
||||
'warning: non-fatal error running "gsutil": %s'
|
||||
'warning: non-fatal error running "gcloud storage": %s'
|
||||
% result.stderr,
|
||||
file=sys.stderr,
|
||||
)
|
||||
@@ -50,7 +50,8 @@ def fetch_file(url, verbose=False):
|
||||
except subprocess.CalledProcessError as e:
|
||||
errors.append(e)
|
||||
print(
|
||||
'fatal: error running "gsutil": %s' % e.stderr, file=sys.stderr
|
||||
'fatal: error running "gcloud storage": %s' % e.stderr,
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise FetchFileError(aggregate_errors=errors)
|
||||
with urlopen(url) as f:
|
||||
|
||||
+32
-17
@@ -22,7 +22,6 @@ from typing import Any, Optional
|
||||
|
||||
from error import GitError
|
||||
from error import RepoExitError
|
||||
from git_refs import HEAD
|
||||
from git_trace2_event_log_base import BaseEventLog
|
||||
import platform_utils
|
||||
from repo_logging import RepoLogger
|
||||
@@ -48,7 +47,7 @@ logger = RepoLogger(__file__)
|
||||
|
||||
|
||||
class _GitCall:
|
||||
@functools.lru_cache(maxsize=None)
|
||||
@functools.lru_cache(maxsize=None) # noqa: B019
|
||||
def version_tuple(self):
|
||||
ret = Wrapper().ParseGitVersion()
|
||||
if ret is None:
|
||||
@@ -71,6 +70,19 @@ class _GitCall:
|
||||
git = _GitCall()
|
||||
|
||||
|
||||
def IsValidBranchName(name: str) -> bool:
|
||||
"""Return whether |name| is valid where Git expects a branch name."""
|
||||
p = GitCommand(
|
||||
None,
|
||||
["check-ref-format", "--branch", name],
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
add_event_log=False,
|
||||
log_as_error=False,
|
||||
)
|
||||
return p.Wait() == 0
|
||||
|
||||
|
||||
def RepoSourceVersion():
|
||||
"""Return the version of the repo.git tree."""
|
||||
ver = getattr(RepoSourceVersion, "version", None)
|
||||
@@ -83,7 +95,7 @@ def RepoSourceVersion():
|
||||
proj = os.path.dirname(os.path.abspath(__file__))
|
||||
env[GIT_DIR] = os.path.join(proj, ".git")
|
||||
result = subprocess.run(
|
||||
[GIT, "describe", HEAD],
|
||||
[GIT, "describe", "HEAD"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
encoding="utf-8",
|
||||
@@ -96,7 +108,7 @@ def RepoSourceVersion():
|
||||
ver = ver[1:]
|
||||
else:
|
||||
ver = "unknown"
|
||||
setattr(RepoSourceVersion, "version", ver)
|
||||
RepoSourceVersion.version = ver
|
||||
|
||||
return ver
|
||||
|
||||
@@ -238,9 +250,9 @@ def _build_env(
|
||||
s = p + " " + s
|
||||
env["GIT_CONFIG_PARAMETERS"] = s
|
||||
if "GIT_ALLOW_PROTOCOL" not in env:
|
||||
env[
|
||||
"GIT_ALLOW_PROTOCOL"
|
||||
] = "file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc"
|
||||
env["GIT_ALLOW_PROTOCOL"] = (
|
||||
"file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc"
|
||||
)
|
||||
env["GIT_HTTP_USER_AGENT"] = user_agent.git
|
||||
|
||||
if objdir:
|
||||
@@ -313,12 +325,15 @@ class GitCommand:
|
||||
cwd = None
|
||||
command_name = cmdv[0]
|
||||
command.append(command_name)
|
||||
# Need to use the --progress flag for fetch/clone so output will be
|
||||
# displayed as by default git only does progress output if stderr is a
|
||||
# TTY.
|
||||
if sys.stderr.isatty() and command_name in ("fetch", "clone"):
|
||||
if "--progress" not in cmdv and "--quiet" not in cmdv:
|
||||
command.append("--progress")
|
||||
|
||||
if command_name in ("fetch", "clone"):
|
||||
env["GIT_TERMINAL_PROMPT"] = "0"
|
||||
# Need to use the --progress flag for fetch/clone so output will be
|
||||
# displayed as by default git only does progress output if stderr is
|
||||
# a TTY.
|
||||
if sys.stderr.isatty():
|
||||
if "--progress" not in cmdv and "--quiet" not in cmdv:
|
||||
command.append("--progress")
|
||||
command.extend(cmdv[1:])
|
||||
|
||||
event_log = (
|
||||
@@ -347,9 +362,9 @@ class GitCommand:
|
||||
"Project": e.project,
|
||||
"CommandName": command_name,
|
||||
"Message": str(e),
|
||||
"ReturnCode": str(e.git_rc)
|
||||
if e.git_rc is not None
|
||||
else None,
|
||||
"ReturnCode": (
|
||||
str(e.git_rc) if e.git_rc is not None else None
|
||||
),
|
||||
"IsError": log_as_error,
|
||||
}
|
||||
)
|
||||
@@ -609,7 +624,7 @@ class GitCommandError(GitError):
|
||||
self.git_stderr = git_stderr
|
||||
|
||||
@property
|
||||
@functools.lru_cache(maxsize=None)
|
||||
@functools.lru_cache(maxsize=None) # noqa: B019
|
||||
def suggestion(self):
|
||||
"""Returns helpful next steps for the given stderr."""
|
||||
if not self.git_stderr:
|
||||
|
||||
+30
-7
@@ -40,17 +40,17 @@ from repo_trace import Trace
|
||||
# that is saved in the config.
|
||||
SYNC_STATE_PREFIX = "repo.syncstate."
|
||||
|
||||
ID_RE = re.compile(r"^[0-9a-f]{40}$")
|
||||
ID_RE = re.compile(r"^(?:[0-9a-f]{40}|[0-9a-f]{64})$")
|
||||
|
||||
REVIEW_CACHE = dict()
|
||||
REVIEW_CACHE = {}
|
||||
|
||||
|
||||
def IsChange(rev):
|
||||
return rev.startswith(R_CHANGES)
|
||||
|
||||
|
||||
def IsId(rev):
|
||||
return ID_RE.match(rev)
|
||||
def IsId(rev: str) -> bool:
|
||||
return bool(ID_RE.match(rev))
|
||||
|
||||
|
||||
def IsTag(rev):
|
||||
@@ -90,6 +90,20 @@ class GitConfig:
|
||||
|
||||
@staticmethod
|
||||
def _getUserConfig():
|
||||
"""Get the user-specific config file.
|
||||
|
||||
Prefers the XDG config location if available, with fallback to
|
||||
~/.gitconfig
|
||||
|
||||
This matches git behavior:
|
||||
https://git-scm.com/docs/git-config#FILES
|
||||
"""
|
||||
xdg_config_home = os.getenv(
|
||||
"XDG_CONFIG_HOME", os.path.expanduser("~/.config")
|
||||
)
|
||||
xdg_config_file = os.path.join(xdg_config_home, "git", "config")
|
||||
if os.path.exists(xdg_config_file):
|
||||
return xdg_config_file
|
||||
return os.path.expanduser("~/.gitconfig")
|
||||
|
||||
@classmethod
|
||||
@@ -97,7 +111,7 @@ class GitConfig:
|
||||
return cls(configfile=os.path.join(gitdir, "config"), defaults=defaults)
|
||||
|
||||
def __init__(self, configfile, defaults=None, jsonFile=None):
|
||||
self.file = configfile
|
||||
self.file = str(configfile)
|
||||
self.defaults = defaults
|
||||
self._cache_dict = None
|
||||
self._section_dict = None
|
||||
@@ -208,6 +222,12 @@ class GitConfig:
|
||||
value = "true" if value else "false"
|
||||
self.SetString(name, value)
|
||||
|
||||
def SetInt(self, name: str, value: int) -> None:
|
||||
"""Set an integer value for a key."""
|
||||
if value is not None:
|
||||
value = str(value)
|
||||
self.SetString(name, value)
|
||||
|
||||
def GetString(self, name: str, all_keys: bool = False) -> Union[str, None]:
|
||||
"""Get the first value for a key, or None if it is not defined.
|
||||
|
||||
@@ -418,7 +438,7 @@ class GitConfig:
|
||||
if p.Wait() == 0:
|
||||
return p.stdout
|
||||
else:
|
||||
raise GitError(f"git config {str(args)}: {p.stderr}")
|
||||
raise GitError(f"git {' '.join(command)}: {p.stderr}")
|
||||
|
||||
|
||||
class RepoConfig(GitConfig):
|
||||
@@ -704,7 +724,10 @@ class Remote:
|
||||
def Save(self):
|
||||
"""Save this remote to the configuration."""
|
||||
self._Set("url", self.url)
|
||||
if self.pushUrl is not None:
|
||||
# projectname is initialized for projects listed in the manifest, but
|
||||
# not for others (e.g. the manifest project). This class is used for
|
||||
# all of them.
|
||||
if self.pushUrl is not None and self.projectname is not None:
|
||||
self._Set("pushurl", self.pushUrl + "/" + self.projectname)
|
||||
else:
|
||||
self._Set("pushurl", self.pushUrl)
|
||||
|
||||
+119
-57
@@ -14,6 +14,8 @@
|
||||
|
||||
import os
|
||||
|
||||
from git_command import git_require
|
||||
from git_command import GitCommand
|
||||
import platform_utils
|
||||
from repo_trace import Trace
|
||||
|
||||
@@ -40,6 +42,17 @@ class GitRefs:
|
||||
self._EnsureLoaded()
|
||||
return self._phyref
|
||||
|
||||
@property
|
||||
def head(self) -> str:
|
||||
"""Return HEAD's symbolic target or detached object ID."""
|
||||
self._EnsureLoaded()
|
||||
return self._symref.get(HEAD) or self._phyref.get(HEAD, "")
|
||||
|
||||
@property
|
||||
def is_loaded(self) -> bool:
|
||||
"""Whether a ref snapshot has already been loaded."""
|
||||
return self._phyref is not None
|
||||
|
||||
def get(self, name):
|
||||
try:
|
||||
return self.all[name]
|
||||
@@ -86,9 +99,12 @@ class GitRefs:
|
||||
self._symref = {}
|
||||
self._mtime = {}
|
||||
|
||||
self._ReadPackedRefs()
|
||||
self._ReadLoose("refs/")
|
||||
self._ReadLoose1(os.path.join(self._gitdir, HEAD), HEAD)
|
||||
root_refs_loaded = self._ReadRefs()
|
||||
if not root_refs_loaded or (
|
||||
HEAD not in self._phyref and HEAD not in self._symref
|
||||
):
|
||||
# --include-root-refs does not report an unborn HEAD.
|
||||
self._ReadSymbolicRef(HEAD)
|
||||
|
||||
scan = self._symref
|
||||
attempts = 0
|
||||
@@ -102,64 +118,110 @@ class GitRefs:
|
||||
scan = scan_next
|
||||
attempts += 1
|
||||
|
||||
def _ReadPackedRefs(self):
|
||||
path = os.path.join(self._gitdir, "packed-refs")
|
||||
self._TrackMtime(HEAD)
|
||||
self._TrackMtime("config")
|
||||
self._TrackMtime("packed-refs")
|
||||
self._TrackTreeMtimes("refs")
|
||||
self._TrackTreeMtimes("reftable")
|
||||
|
||||
@staticmethod
|
||||
def _IsNullRef(ref_id: str) -> bool:
|
||||
"""Check if a ref_id is a null object ID."""
|
||||
return ref_id and all(ch == "0" for ch in ref_id)
|
||||
|
||||
def _ReadRefs(self) -> bool:
|
||||
"""Read all references using git for-each-ref.
|
||||
|
||||
Returns:
|
||||
Whether root refs, including HEAD when it exists, were loaded.
|
||||
"""
|
||||
include_root_refs = git_require((2, 45, 0))
|
||||
cmd = [
|
||||
"for-each-ref",
|
||||
"--format=%(objectname)%00%(refname)%00%(symref)",
|
||||
]
|
||||
if include_root_refs:
|
||||
cmd.insert(1, "--include-root-refs")
|
||||
# Avoid caching volatile root refs such as ORIG_HEAD. HEAD and
|
||||
# refs/* are the only namespaces GitRefs exposes to callers.
|
||||
cmd.extend([HEAD, "refs"])
|
||||
p = GitCommand(
|
||||
None,
|
||||
cmd,
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
bare=True,
|
||||
gitdir=self._gitdir,
|
||||
)
|
||||
if p.Wait() != 0:
|
||||
return False
|
||||
|
||||
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
|
||||
return include_root_refs
|
||||
|
||||
def _ReadSymbolicRef(self, name: str) -> None:
|
||||
"""Read a symbolic reference."""
|
||||
p = GitCommand(
|
||||
None,
|
||||
["symbolic-ref", "-q", name],
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
bare=True,
|
||||
gitdir=self._gitdir,
|
||||
)
|
||||
if p.Wait() == 0:
|
||||
ref = p.stdout.strip()
|
||||
if ref:
|
||||
self._symref[name] = ref
|
||||
return
|
||||
|
||||
p = GitCommand(
|
||||
None,
|
||||
["rev-parse", "--verify", "-q", name],
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
bare=True,
|
||||
gitdir=self._gitdir,
|
||||
)
|
||||
if p.Wait() == 0:
|
||||
ref_id = p.stdout.strip()
|
||||
if ref_id:
|
||||
self._phyref[name] = ref_id
|
||||
|
||||
def _TrackMtime(self, name: str) -> None:
|
||||
"""Track the modification time of a specific gitdir path."""
|
||||
path = os.path.join(self._gitdir, name)
|
||||
try:
|
||||
fd = open(path)
|
||||
mtime = os.path.getmtime(path)
|
||||
self._mtime[name] = os.path.getmtime(path)
|
||||
except OSError:
|
||||
return
|
||||
|
||||
def _TrackTreeMtimes(self, root: str) -> None:
|
||||
"""Recursively track modification times for a directory tree."""
|
||||
root_path = os.path.join(self._gitdir, root)
|
||||
try:
|
||||
for line in fd:
|
||||
line = str(line)
|
||||
if line[0] == "#":
|
||||
continue
|
||||
if line[0] == "^":
|
||||
continue
|
||||
|
||||
line = line[:-1]
|
||||
p = line.split(" ")
|
||||
ref_id = p[0]
|
||||
name = p[1]
|
||||
|
||||
self._phyref[name] = ref_id
|
||||
finally:
|
||||
fd.close()
|
||||
self._mtime["packed-refs"] = mtime
|
||||
|
||||
def _ReadLoose(self, prefix):
|
||||
base = os.path.join(self._gitdir, prefix)
|
||||
for name in platform_utils.listdir(base):
|
||||
p = os.path.join(base, name)
|
||||
# We don't implement the full ref validation algorithm, just the
|
||||
# simple rules that would show up in local filesystems.
|
||||
# https://git-scm.com/docs/git-check-ref-format
|
||||
if name.startswith(".") or name.endswith(".lock"):
|
||||
pass
|
||||
elif platform_utils.isdir(p):
|
||||
self._mtime[prefix] = os.path.getmtime(base)
|
||||
self._ReadLoose(prefix + name + "/")
|
||||
else:
|
||||
self._ReadLoose1(p, prefix + name)
|
||||
|
||||
def _ReadLoose1(self, path, name):
|
||||
try:
|
||||
with open(path) as fd:
|
||||
mtime = os.path.getmtime(path)
|
||||
ref_id = fd.readline()
|
||||
except (OSError, UnicodeError):
|
||||
if not platform_utils.isdir(root_path):
|
||||
return
|
||||
except OSError:
|
||||
return
|
||||
|
||||
try:
|
||||
ref_id = ref_id.decode()
|
||||
except AttributeError:
|
||||
pass
|
||||
if not ref_id:
|
||||
return
|
||||
ref_id = ref_id[:-1]
|
||||
to_scan = [root]
|
||||
while to_scan:
|
||||
name = to_scan.pop()
|
||||
self._TrackMtime(name)
|
||||
path = os.path.join(self._gitdir, name)
|
||||
if not platform_utils.isdir(path):
|
||||
continue
|
||||
|
||||
if ref_id.startswith("ref: "):
|
||||
self._symref[name] = ref_id[5:]
|
||||
else:
|
||||
self._phyref[name] = ref_id
|
||||
self._mtime[name] = mtime
|
||||
for child in platform_utils.listdir(path):
|
||||
child_name = os.path.join(name, child)
|
||||
child_path = os.path.join(self._gitdir, child_name)
|
||||
if platform_utils.isdir(child_path):
|
||||
to_scan.append(child_name)
|
||||
else:
|
||||
self._TrackMtime(child_name)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Copyright (C) 2009 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
+143
-39
@@ -23,16 +23,20 @@ Examples:
|
||||
"""
|
||||
|
||||
import functools
|
||||
import glob
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from typing import NamedTuple
|
||||
import urllib.parse
|
||||
|
||||
from git_command import git_require
|
||||
from git_command import GitCommand
|
||||
from git_config import IsId
|
||||
from git_config import RepoConfig
|
||||
from git_refs import GitRefs
|
||||
import platform_utils
|
||||
|
||||
|
||||
_SUPERPROJECT_GIT_NAME = "superproject.git"
|
||||
@@ -96,7 +100,7 @@ class Superproject:
|
||||
self._manifest = manifest
|
||||
self.name = name
|
||||
self.remote = remote
|
||||
self.revision = self._branch = revision
|
||||
self.revision = revision
|
||||
self._repodir = manifest.repodir
|
||||
self._superproject_dir = superproject_dir
|
||||
self._superproject_path = manifest.SubmanifestInfoDir(
|
||||
@@ -128,6 +132,34 @@ class Superproject:
|
||||
"""Set the _print_messages attribute."""
|
||||
self._print_messages = value
|
||||
|
||||
def SetRevisionId(self, revision_id: str) -> None:
|
||||
"""Set the revisionId of the superproject to sync to."""
|
||||
self.revision = revision_id
|
||||
|
||||
@property
|
||||
def commit_id(self):
|
||||
"""Returns the commit ID of the superproject checkout."""
|
||||
cmd = ["rev-parse", self.revision]
|
||||
p = GitCommand(
|
||||
None, # project
|
||||
cmd,
|
||||
gitdir=self._work_git,
|
||||
bare=True,
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
)
|
||||
retval = p.Wait()
|
||||
if retval != 0:
|
||||
self._LogWarning(
|
||||
"git rev-parse call failed, command: git {}, "
|
||||
"return code: {}, stderr: {}",
|
||||
cmd,
|
||||
retval,
|
||||
p.stderr,
|
||||
)
|
||||
return None
|
||||
return p.stdout
|
||||
|
||||
@property
|
||||
def project_commit_ids(self):
|
||||
"""Returns a dictionary of projects and their commit ids."""
|
||||
@@ -140,17 +172,39 @@ class Superproject:
|
||||
self._manifest_path if os.path.exists(self._manifest_path) else None
|
||||
)
|
||||
|
||||
@property
|
||||
def repo_id(self):
|
||||
"""Returns the repo ID for the superproject.
|
||||
|
||||
For example, if the superproject points to:
|
||||
https://android-review.googlesource.com/platform/superproject/
|
||||
Then the repo_id would be:
|
||||
android/platform/superproject
|
||||
"""
|
||||
review_url = self.remote.review
|
||||
if review_url:
|
||||
parsed_url = urllib.parse.urlparse(review_url)
|
||||
netloc = parsed_url.netloc
|
||||
if netloc:
|
||||
parts = netloc.split("-review", 1)
|
||||
host = parts[0]
|
||||
rev = self._GetRef("HEAD")
|
||||
return f"{host}/{self.name}@{rev}"
|
||||
return None
|
||||
|
||||
def _LogMessage(self, fmt, *inputs):
|
||||
"""Logs message to stderr and _git_event_log."""
|
||||
message = f"{self._LogMessagePrefix()} {fmt.format(*inputs)}"
|
||||
if self._print_messages:
|
||||
print(message, file=sys.stderr)
|
||||
self._git_event_log.ErrorEvent(message, fmt)
|
||||
if self._git_event_log:
|
||||
self._git_event_log.ErrorEvent(message, fmt)
|
||||
|
||||
def _LogMessagePrefix(self):
|
||||
"""Returns the prefix string to be logged in each log message"""
|
||||
return (
|
||||
f"repo superproject branch: {self._branch} url: {self._remote_url}"
|
||||
f"repo superproject revision: {self.revision} "
|
||||
f"url: {self._remote_url}"
|
||||
)
|
||||
|
||||
def _LogError(self, fmt, *inputs):
|
||||
@@ -169,30 +223,63 @@ class Superproject:
|
||||
"""
|
||||
if not os.path.exists(self._superproject_path):
|
||||
os.mkdir(self._superproject_path)
|
||||
if not self._quiet and not os.path.exists(self._work_git):
|
||||
|
||||
if os.path.exists(self._work_git):
|
||||
return True
|
||||
|
||||
if not self._quiet:
|
||||
print(
|
||||
"%s: Performing initial setup for superproject; this might "
|
||||
"take several minutes." % self._work_git
|
||||
)
|
||||
cmd = ["init", "--bare", self._work_git_name]
|
||||
p = GitCommand(
|
||||
None,
|
||||
cmd,
|
||||
cwd=self._superproject_path,
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
|
||||
tmp_gitdir_prefix = ".tmp-superproject-initgitdir-"
|
||||
tmp_gitdir = tempfile.mkdtemp(
|
||||
prefix=tmp_gitdir_prefix,
|
||||
dir=self._superproject_path,
|
||||
)
|
||||
retval = p.Wait()
|
||||
if retval:
|
||||
self._LogWarning(
|
||||
"git init call failed, command: git {}, "
|
||||
"return code: {}, stderr: {}",
|
||||
tmp_git_name = os.path.basename(tmp_gitdir)
|
||||
|
||||
try:
|
||||
cmd = ["init", "--bare", tmp_git_name]
|
||||
p = GitCommand(
|
||||
None,
|
||||
cmd,
|
||||
retval,
|
||||
p.stderr,
|
||||
cwd=self._superproject_path,
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
retval = p.Wait()
|
||||
if retval:
|
||||
self._LogWarning(
|
||||
"git init call failed, command: git {}, "
|
||||
"return code: {}, stderr: {}",
|
||||
cmd,
|
||||
retval,
|
||||
p.stderr,
|
||||
)
|
||||
return False
|
||||
|
||||
platform_utils.rename(tmp_gitdir, self._work_git)
|
||||
tmp_gitdir = None
|
||||
return True
|
||||
finally:
|
||||
# Clean up the temporary directory created during the process,
|
||||
# as well as any stale ones left over from previous attempts.
|
||||
if tmp_gitdir and os.path.exists(tmp_gitdir):
|
||||
platform_utils.rmtree(tmp_gitdir)
|
||||
|
||||
age_threshold = 60 * 60 * 24 # 1 day in seconds
|
||||
now = time.time()
|
||||
for tmp_dir in glob.glob(
|
||||
os.path.join(self._superproject_path, f"{tmp_gitdir_prefix}*")
|
||||
):
|
||||
try:
|
||||
mtime = os.path.getmtime(tmp_dir)
|
||||
if now - mtime > age_threshold:
|
||||
platform_utils.rmtree(tmp_dir)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _Fetch(self):
|
||||
"""Fetches a superproject for the manifest based on |_remote_url|.
|
||||
@@ -226,12 +313,22 @@ class Superproject:
|
||||
# We use --negotiation-tip to speed up the fetch. Superproject branches
|
||||
# do not share commits. So this lets git know it only needs to send
|
||||
# commits reachable from the specified local refs.
|
||||
rev_commit = GitRefs(self._work_git).get(f"refs/heads/{self.revision}")
|
||||
negotiation_ref = self.revision
|
||||
if negotiation_ref and not negotiation_ref.startswith("refs/"):
|
||||
negotiation_ref = f"refs/heads/{negotiation_ref}"
|
||||
rev_commit = self._GetRef(negotiation_ref) if negotiation_ref else ""
|
||||
if rev_commit:
|
||||
cmd.extend(["--negotiation-tip", rev_commit])
|
||||
|
||||
if self._branch:
|
||||
cmd += [self._branch + ":" + self._branch]
|
||||
if self.revision:
|
||||
# If revision is a commit hash, fetch it directly to avoid
|
||||
# creating a local branch of the same name.
|
||||
refspec = (
|
||||
self.revision
|
||||
if IsId(self.revision)
|
||||
else f"{self.revision}:{self.revision}"
|
||||
)
|
||||
cmd.append(refspec)
|
||||
p = GitCommand(
|
||||
None,
|
||||
cmd,
|
||||
@@ -252,13 +349,28 @@ class Superproject:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _GetRef(self, ref: str) -> str:
|
||||
"""Resolve one local ref without loading the entire ref namespace."""
|
||||
p = GitCommand(
|
||||
None,
|
||||
["rev-parse", "--verify", "--quiet", ref],
|
||||
gitdir=self._work_git,
|
||||
bare=True,
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
log_as_error=False,
|
||||
)
|
||||
if p.Wait() == 0:
|
||||
return p.stdout.strip()
|
||||
return ""
|
||||
|
||||
def _LsTree(self):
|
||||
"""Gets the commit ids for all projects.
|
||||
|
||||
Works only in git repositories.
|
||||
|
||||
Returns:
|
||||
data: data returned from 'git ls-tree ...' instead of None.
|
||||
data: data returned from 'git ls-tree ...'. None on error.
|
||||
"""
|
||||
if not os.path.exists(self._work_git):
|
||||
self._LogWarning(
|
||||
@@ -266,7 +378,7 @@ class Superproject:
|
||||
)
|
||||
return None
|
||||
data = None
|
||||
branch = "HEAD" if not self._branch else self._branch
|
||||
branch = "HEAD" if not self.revision else self.revision
|
||||
cmd = ["ls-tree", "-z", "-r", branch]
|
||||
|
||||
p = GitCommand(
|
||||
@@ -288,6 +400,7 @@ class Superproject:
|
||||
retval,
|
||||
p.stderr,
|
||||
)
|
||||
return None
|
||||
return data
|
||||
|
||||
def Sync(self, git_event_log):
|
||||
@@ -307,8 +420,6 @@ class Superproject:
|
||||
)
|
||||
return SyncResult(False, False)
|
||||
|
||||
_PrintBetaNotice()
|
||||
|
||||
should_exit = True
|
||||
if not self._remote_url:
|
||||
self._LogWarning(
|
||||
@@ -319,6 +430,8 @@ class Superproject:
|
||||
|
||||
if not self._Init():
|
||||
return SyncResult(False, should_exit)
|
||||
if IsId(self.revision) and self.commit_id:
|
||||
return SyncResult(True, False)
|
||||
if not self._Fetch():
|
||||
return SyncResult(False, should_exit)
|
||||
if not self._quiet:
|
||||
@@ -377,7 +490,8 @@ class Superproject:
|
||||
)
|
||||
return None
|
||||
manifest_str = self._manifest.ToXml(
|
||||
groups=self._manifest.GetGroupsStr(), omit_local=True
|
||||
filter_groups="all",
|
||||
omit_local=True,
|
||||
).toxml()
|
||||
manifest_path = self._manifest_path
|
||||
try:
|
||||
@@ -452,16 +566,6 @@ class Superproject:
|
||||
return UpdateProjectsResult(manifest_path, False)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=10)
|
||||
def _PrintBetaNotice():
|
||||
"""Print the notice of beta status."""
|
||||
print(
|
||||
"NOTICE: --use-superproject is in beta; report any issues to the "
|
||||
"address described in `repo version`",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def _UseSuperprojectFromConfiguration():
|
||||
"""Returns the user choice of whether to use superproject."""
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
# Copyright (C) 2020 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Event logging in the git trace2 EVENT format."""
|
||||
|
||||
from git_command import GetEventTargetPath
|
||||
from git_command import RepoSourceVersion
|
||||
from git_trace2_event_log_base import BaseEventLog
|
||||
|
||||
@@ -68,6 +68,7 @@ class BaseEventLog:
|
||||
global p_init_count
|
||||
p_init_count += 1
|
||||
self._log = []
|
||||
self.verbose = False
|
||||
# Try to get session-id (sid) from environment (setup in repo launcher).
|
||||
KEY = "GIT_TRACE2_PARENT_SID"
|
||||
if env is None:
|
||||
@@ -130,10 +131,10 @@ class BaseEventLog:
|
||||
"time": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
def StartEvent(self):
|
||||
def StartEvent(self, argv):
|
||||
"""Append a 'start' event to the current log."""
|
||||
start_event = self._CreateEventDict("start")
|
||||
start_event["argv"] = sys.argv
|
||||
start_event["argv"] = argv
|
||||
self._log.append(start_event)
|
||||
|
||||
def ExitEvent(self, result):
|
||||
@@ -159,9 +160,11 @@ class BaseEventLog:
|
||||
name: Name of the primary command (ex: repo, git)
|
||||
subcommands: List of the sub-commands (ex: version, init, sync)
|
||||
"""
|
||||
command_event = self._CreateEventDict("command")
|
||||
command_event = self._CreateEventDict("cmd_name")
|
||||
name = f"{name}-"
|
||||
name += "-".join(subcommands)
|
||||
command_event["name"] = name
|
||||
command_event["subcommands"] = subcommands
|
||||
command_event["hierarchy"] = name
|
||||
self._log.append(command_event)
|
||||
|
||||
def LogConfigEvents(self, config, event_dict_name):
|
||||
@@ -192,7 +195,13 @@ class BaseEventLog:
|
||||
|
||||
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"
|
||||
return (
|
||||
"data-json"
|
||||
if isinstance(value, str)
|
||||
and value.startswith("[")
|
||||
and value.endswith("]")
|
||||
else "data"
|
||||
)
|
||||
|
||||
def LogDataConfigEvents(self, config, prefix):
|
||||
"""Append a 'data' event for each entry in |config| to the current log.
|
||||
@@ -307,10 +316,12 @@ class BaseEventLog:
|
||||
# ignore the attempt and continue to DGRAM below. Otherwise,
|
||||
# issue a warning.
|
||||
if err.errno != errno.EPROTOTYPE:
|
||||
print(
|
||||
f"repo: warning: git trace2 logging failed: {err}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if self.verbose:
|
||||
print(
|
||||
"repo: warning: git trace2 logging failed:",
|
||||
f"{err}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return None
|
||||
if socket_type == socket.SOCK_DGRAM or socket_type is None:
|
||||
try:
|
||||
@@ -320,18 +331,20 @@ class BaseEventLog:
|
||||
self._WriteLog(lambda bs: sock.sendto(bs, path))
|
||||
return f"af_unix:dgram:{path}"
|
||||
except OSError as err:
|
||||
print(
|
||||
f"repo: warning: git trace2 logging failed: {err}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if self.verbose:
|
||||
print(
|
||||
f"repo: warning: git trace2 logging failed: {err}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return None
|
||||
# Tried to open a socket but couldn't connect (SOCK_STREAM) or write
|
||||
# (SOCK_DGRAM).
|
||||
print(
|
||||
"repo: warning: git trace2 logging failed: could not write to "
|
||||
"socket",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if self.verbose:
|
||||
print(
|
||||
"repo: warning: git trace2 logging failed: could not"
|
||||
"write to socket",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return None
|
||||
|
||||
# Path is an absolute path
|
||||
@@ -346,9 +359,10 @@ class BaseEventLog:
|
||||
self._WriteLog(f.write)
|
||||
log_path = f.name
|
||||
except FileExistsError as err:
|
||||
print(
|
||||
"repo: warning: git trace2 logging failed: %r" % err,
|
||||
file=sys.stderr,
|
||||
)
|
||||
if self.verbose:
|
||||
print(
|
||||
"repo: warning: git trace2 logging failed: %r" % err,
|
||||
file=sys.stderr,
|
||||
)
|
||||
return None
|
||||
return log_path
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
@@ -22,6 +23,13 @@ from error import HookError
|
||||
from git_refs import HEAD
|
||||
|
||||
|
||||
# The API we've documented to hook authors. Keep in sync with repo-hooks.md.
|
||||
_API_ARGS = {
|
||||
"pre-upload": {"project_list", "worktree_list"},
|
||||
"post-sync": {"repo_topdir", "sync_duration_seconds"},
|
||||
}
|
||||
|
||||
|
||||
class RepoHook:
|
||||
"""A RepoHook contains information about a script to run as a hook.
|
||||
|
||||
@@ -56,10 +64,13 @@ class RepoHook:
|
||||
hooks_project,
|
||||
repo_topdir,
|
||||
manifest_url,
|
||||
bug_url=None,
|
||||
bypass_hooks=False,
|
||||
allow_all_hooks=False,
|
||||
ignore_hooks=False,
|
||||
abort_if_user_denies=False,
|
||||
yes=False,
|
||||
fix=False,
|
||||
):
|
||||
"""RepoHook constructor.
|
||||
|
||||
@@ -75,28 +86,33 @@ class RepoHook:
|
||||
run with CWD as this directory.
|
||||
If you have a manifest, this is manifest.topdir.
|
||||
manifest_url: The URL to the manifest git repo.
|
||||
bug_url: The URL to report issues.
|
||||
bypass_hooks: If True, then 'Do not run the hook'.
|
||||
allow_all_hooks: If True, then 'Run the hook without prompting'.
|
||||
ignore_hooks: If True, then 'Do not abort action if hooks fail'.
|
||||
abort_if_user_denies: If True, we'll abort running the hook if the
|
||||
user doesn't allow us to run the hook.
|
||||
yes: If True, then 'Yes' is assumed for any prompts.
|
||||
fix: If True, then 'Fix' is assumed for any fixup prompts.
|
||||
"""
|
||||
self._hook_type = hook_type
|
||||
self._hooks_project = hooks_project
|
||||
self._repo_topdir = repo_topdir
|
||||
self._manifest_url = manifest_url
|
||||
self._bug_url = bug_url
|
||||
self._bypass_hooks = bypass_hooks
|
||||
self._allow_all_hooks = allow_all_hooks
|
||||
self._ignore_hooks = ignore_hooks
|
||||
self._abort_if_user_denies = abort_if_user_denies
|
||||
self._yes = yes
|
||||
self._fix = fix
|
||||
|
||||
# Store the full path to the script for convenience.
|
||||
if self._hooks_project:
|
||||
self._script_fullpath = None
|
||||
if self._hooks_project and self._hooks_project.worktree:
|
||||
self._script_fullpath = os.path.join(
|
||||
self._hooks_project.worktree, self._hook_type + ".py"
|
||||
)
|
||||
else:
|
||||
self._script_fullpath = None
|
||||
|
||||
def _GetHash(self):
|
||||
"""Return a hash of the contents of the hooks directory.
|
||||
@@ -365,8 +381,12 @@ class RepoHook:
|
||||
# def main(project_list, **kwargs):
|
||||
#
|
||||
# This allows us to later expand the API without breaking old hooks.
|
||||
kwargs = kwargs.copy()
|
||||
kwargs["hook_should_take_kwargs"] = True
|
||||
kwargs = {
|
||||
**kwargs,
|
||||
"hook_should_take_kwargs": True,
|
||||
"fix": self._fix,
|
||||
"yes": self._yes,
|
||||
}
|
||||
|
||||
# See what version of python the hook has been written against.
|
||||
data = open(self._script_fullpath).read()
|
||||
@@ -414,11 +434,26 @@ class RepoHook:
|
||||
ignore the result through the option combinations as listed in
|
||||
AddHookOptionGroup().
|
||||
"""
|
||||
# Make sure our own callers use the documented API.
|
||||
exp_kwargs = _API_ARGS.get(self._hook_type, set())
|
||||
got_kwargs = set(kwargs.keys())
|
||||
if exp_kwargs != got_kwargs:
|
||||
print(
|
||||
"repo internal error: "
|
||||
f"hook '{self._hook_type}' called incorrectly\n"
|
||||
f" got: {sorted(got_kwargs)}\n"
|
||||
f" expected: {sorted(exp_kwargs)}\n"
|
||||
f"Please file a bug: {self._bug_url}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return False
|
||||
|
||||
# Do not do anything in case bypass_hooks is set, or
|
||||
# no-op if there is no hooks project or if hook is disabled.
|
||||
if (
|
||||
self._bypass_hooks
|
||||
or not self._hooks_project
|
||||
or not self._script_fullpath
|
||||
or self._hook_type not in self._hooks_project.enabled_repo_hooks
|
||||
):
|
||||
return True
|
||||
@@ -472,12 +507,19 @@ class RepoHook:
|
||||
"manifest_url": manifest.manifestProject.GetRemote(
|
||||
"origin"
|
||||
).url,
|
||||
"bug_url": manifest.contactinfo.bugurl,
|
||||
"yes": getattr(opt, "yes", False),
|
||||
"fix": getattr(opt, "fix", False),
|
||||
}
|
||||
)
|
||||
return cls(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def AddOptionGroup(parser, name):
|
||||
def AddOptionGroup(
|
||||
parser: optparse.OptionParser,
|
||||
name: str,
|
||||
allow_fix: bool = False,
|
||||
) -> None:
|
||||
"""Help options relating to the various hooks."""
|
||||
|
||||
# Note that verify and no-verify are NOT opposites of each other, which
|
||||
@@ -501,3 +543,10 @@ class RepoHook:
|
||||
action="store_true",
|
||||
help="Do not abort if %s hooks fail." % name,
|
||||
)
|
||||
if allow_fix:
|
||||
group.add_option(
|
||||
"--fix",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Automatically apply %s fixes without prompting." % name,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (C) 2008 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -20,6 +19,7 @@ People shouldn't run this directly; instead, they should use the `repo` wrapper
|
||||
which takes care of execing this entry point.
|
||||
"""
|
||||
|
||||
import difflib
|
||||
import getpass
|
||||
import json
|
||||
import netrc
|
||||
@@ -30,6 +30,7 @@ import signal
|
||||
import sys
|
||||
import textwrap
|
||||
import time
|
||||
from typing import Optional
|
||||
import urllib.request
|
||||
|
||||
from repo_logging import RepoLogger
|
||||
@@ -45,7 +46,6 @@ from command import InteractiveCommand
|
||||
from command import MirrorSafeCommand
|
||||
from editor import Editor
|
||||
from error import DownloadError
|
||||
from error import GitcUnsupportedError
|
||||
from error import InvalidProjectGroupsError
|
||||
from error import ManifestInvalidRevisionError
|
||||
from error import ManifestParseError
|
||||
@@ -294,6 +294,102 @@ class _Repo:
|
||||
result = run()
|
||||
return result
|
||||
|
||||
def _autocorrect_command_name(
|
||||
self, name: str, config: RepoConfig
|
||||
) -> Optional[str]:
|
||||
"""Autocorrect command name based on user's git config."""
|
||||
|
||||
close_commands = difflib.get_close_matches(
|
||||
name, self.commands.keys(), n=5, cutoff=0.7
|
||||
)
|
||||
|
||||
if not close_commands:
|
||||
logger.error(
|
||||
"repo: '%s' is not a repo command. See 'repo help'.", name
|
||||
)
|
||||
return None
|
||||
|
||||
assumed = close_commands[0]
|
||||
autocorrect = config.GetString("help.autocorrect")
|
||||
|
||||
# If there are multiple close matches, git won't automatically run one.
|
||||
# We'll always prompt instead of guessing.
|
||||
if len(close_commands) > 1:
|
||||
autocorrect = "prompt"
|
||||
|
||||
# Handle git configuration boolean values:
|
||||
# 0, "false", "off", "no", "show": show suggestion (default)
|
||||
# 1, "true", "on", "yes", "immediate": run suggestion immediately
|
||||
# "never": don't run or show any suggested command
|
||||
# "prompt": show the suggestion and prompt for confirmation
|
||||
# positive number > 1: run suggestion after specified deciseconds
|
||||
if autocorrect is None:
|
||||
autocorrect = "0"
|
||||
|
||||
autocorrect = autocorrect.lower()
|
||||
|
||||
if autocorrect in ("0", "false", "off", "no", "show"):
|
||||
autocorrect = 0
|
||||
elif autocorrect in ("true", "on", "yes", "immediate"):
|
||||
autocorrect = -1 # immediate
|
||||
elif autocorrect == "never":
|
||||
return None
|
||||
elif autocorrect == "prompt":
|
||||
logger.warning(
|
||||
"You called a repo command named "
|
||||
"'%s', which does not exist.",
|
||||
name,
|
||||
)
|
||||
try:
|
||||
resp = input(f"Run '{assumed}' instead [y/N]? ")
|
||||
if resp.lower().startswith("y"):
|
||||
return assumed
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
pass
|
||||
return None
|
||||
else:
|
||||
try:
|
||||
autocorrect = int(autocorrect)
|
||||
except ValueError:
|
||||
autocorrect = 0
|
||||
|
||||
if autocorrect != 0:
|
||||
if autocorrect < 0:
|
||||
logger.warning(
|
||||
"You called a repo command named "
|
||||
"'%s', which does not exist.\n"
|
||||
"Continuing assuming that "
|
||||
"you meant '%s'.",
|
||||
name,
|
||||
assumed,
|
||||
)
|
||||
else:
|
||||
delay = autocorrect * 0.1
|
||||
logger.warning(
|
||||
"You called a repo command named "
|
||||
"'%s', which does not exist.\n"
|
||||
"Continuing in %.1f seconds, assuming "
|
||||
"that you meant '%s'.",
|
||||
name,
|
||||
delay,
|
||||
assumed,
|
||||
)
|
||||
try:
|
||||
time.sleep(delay)
|
||||
except KeyboardInterrupt:
|
||||
return None
|
||||
return assumed
|
||||
|
||||
logger.error(
|
||||
"repo: '%s' is not a repo command. See 'repo help'.", name
|
||||
)
|
||||
logger.warning(
|
||||
"The most similar command%s\n\t%s",
|
||||
"s are" if len(close_commands) > 1 else " is",
|
||||
"\n\t".join(close_commands),
|
||||
)
|
||||
return None
|
||||
|
||||
def _RunLong(self, name, gopts, argv, git_trace2_event_log):
|
||||
"""Execute the (longer running) requested subcommand."""
|
||||
result = 0
|
||||
@@ -308,24 +404,22 @@ class _Repo:
|
||||
outer_client=outer_client,
|
||||
)
|
||||
|
||||
if Wrapper().gitc_parse_clientdir(os.getcwd()):
|
||||
logger.error("GITC is not supported.")
|
||||
raise GitcUnsupportedError()
|
||||
if name not in self.commands:
|
||||
corrected_name = self._autocorrect_command_name(
|
||||
name, outer_client.globalConfig
|
||||
)
|
||||
if not corrected_name:
|
||||
return 1
|
||||
name = corrected_name
|
||||
|
||||
try:
|
||||
cmd = self.commands[name](
|
||||
repodir=self.repodir,
|
||||
client=repo_client,
|
||||
manifest=repo_client.manifest,
|
||||
outer_client=outer_client,
|
||||
outer_manifest=outer_client.manifest,
|
||||
git_event_log=git_trace2_event_log,
|
||||
)
|
||||
except KeyError:
|
||||
logger.error(
|
||||
"repo: '%s' is not a repo command. See 'repo help'.", name
|
||||
)
|
||||
return 1
|
||||
cmd = self.commands[name](
|
||||
repodir=self.repodir,
|
||||
client=repo_client,
|
||||
manifest=repo_client.manifest,
|
||||
outer_client=outer_client,
|
||||
outer_manifest=outer_client.manifest,
|
||||
git_event_log=git_trace2_event_log,
|
||||
)
|
||||
|
||||
Editor.globalConfig = cmd.client.globalConfig
|
||||
|
||||
@@ -343,6 +437,9 @@ class _Repo:
|
||||
)
|
||||
return 1
|
||||
|
||||
cmd.CommonValidateOptions(copts, cargs)
|
||||
git_trace2_event_log.verbose = copts.verbose
|
||||
|
||||
if gopts.pager is not False and not isinstance(cmd, InteractiveCommand):
|
||||
config = cmd.client.globalConfig
|
||||
if gopts.pager:
|
||||
@@ -357,7 +454,7 @@ class _Repo:
|
||||
start = time.time()
|
||||
cmd_event = cmd.event_log.Add(name, event_log.TASK_COMMAND, start)
|
||||
cmd.event_log.SetParent(cmd_event)
|
||||
git_trace2_event_log.StartEvent()
|
||||
git_trace2_event_log.StartEvent(["repo", name] + argv)
|
||||
git_trace2_event_log.CommandEvent(name="repo", subcommands=[name])
|
||||
|
||||
def execute_command_helper():
|
||||
@@ -365,7 +462,6 @@ class _Repo:
|
||||
Execute the subcommand.
|
||||
"""
|
||||
nonlocal result
|
||||
cmd.CommonValidateOptions(copts, cargs)
|
||||
cmd.ValidateOptions(copts, cargs)
|
||||
|
||||
this_manifest_only = copts.this_manifest_only
|
||||
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "July 2022" "repo abandon" "Repo Manual"
|
||||
.TH REPO "1" "June 2026" "repo abandon" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo abandon - manual page for repo abandon
|
||||
.SH SYNOPSIS
|
||||
@@ -20,8 +20,7 @@ It is equivalent to "git branch \fB\-D\fR <branchname>".
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||
number of jobs to run in parallel (default: based on
|
||||
number of CPU cores)
|
||||
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||
.TP
|
||||
\fB\-\-all\fR
|
||||
delete all branches in all projects
|
||||
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "July 2022" "repo branches" "Repo Manual"
|
||||
.TH REPO "1" "June 2026" "repo branches" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo branches - manual page for repo branches
|
||||
.SH SYNOPSIS
|
||||
@@ -46,8 +46,7 @@ is shown, then the branch appears in all projects.
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||
number of jobs to run in parallel (default: based on
|
||||
number of CPU cores)
|
||||
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||
.SS Logging options:
|
||||
.TP
|
||||
\fB\-v\fR, \fB\-\-verbose\fR
|
||||
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "July 2022" "repo checkout" "Repo Manual"
|
||||
.TH REPO "1" "June 2026" "repo checkout" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo checkout - manual page for repo checkout
|
||||
.SH SYNOPSIS
|
||||
@@ -15,8 +15,7 @@ Checkout a branch for development
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||
number of jobs to run in parallel (default: based on
|
||||
number of CPU cores)
|
||||
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||
.SS Logging options:
|
||||
.TP
|
||||
\fB\-v\fR, \fB\-\-verbose\fR
|
||||
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "July 2022" "repo diff" "Repo Manual"
|
||||
.TH REPO "1" "June 2026" "repo diff" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo diff - manual page for repo diff
|
||||
.SH SYNOPSIS
|
||||
@@ -19,8 +19,7 @@ to the Unix 'patch' command.
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||
number of jobs to run in parallel (default: based on
|
||||
number of CPU cores)
|
||||
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||
.TP
|
||||
\fB\-u\fR, \fB\-\-absolute\fR
|
||||
paths are relative to the repository root
|
||||
|
||||
+13
-11
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "July 2022" "repo forall" "Repo Manual"
|
||||
.TH REPO "1" "June 2026" "repo forall" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo forall - manual page for repo forall
|
||||
.SH SYNOPSIS
|
||||
@@ -17,20 +17,16 @@ repo forall \fB\-r\fR str1 [str2] ... \fB\-c\fR <command> [<arg>...]
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||
number of jobs to run in parallel (default: based on
|
||||
number of CPU cores)
|
||||
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||
.TP
|
||||
\fB\-r\fR, \fB\-\-regex\fR
|
||||
execute the command only on projects matching regex or
|
||||
wildcard expression
|
||||
execute the command only on projects matching regex or wildcard expression
|
||||
.TP
|
||||
\fB\-i\fR, \fB\-\-inverse\-regex\fR
|
||||
execute the command only on projects not matching
|
||||
regex or wildcard expression
|
||||
execute the command only on projects not matching regex or wildcard expression
|
||||
.TP
|
||||
\fB\-g\fR GROUPS, \fB\-\-groups\fR=\fI\,GROUPS\/\fR
|
||||
execute the command only on projects matching the
|
||||
specified groups
|
||||
execute the command only on projects matching the specified groups
|
||||
.TP
|
||||
\fB\-c\fR, \fB\-\-command\fR
|
||||
command (and arguments) to execute
|
||||
@@ -39,8 +35,7 @@ command (and arguments) to execute
|
||||
abort if a command exits unsuccessfully
|
||||
.TP
|
||||
\fB\-\-ignore\-missing\fR
|
||||
silently skip & do not exit non\-zero due missing
|
||||
checkouts
|
||||
silently skip & do not exit non\-zero due missing checkouts
|
||||
.TP
|
||||
\fB\-\-interactive\fR
|
||||
force interactive usage
|
||||
@@ -120,6 +115,13 @@ git command, use REPO_LREV.
|
||||
REPO_RREV is the name of the revision from the manifest, exactly as written in
|
||||
the manifest.
|
||||
.PP
|
||||
REPO_UPSTREAM is the name of the upstream branch as specified in the manifest.
|
||||
.PP
|
||||
REPO_DEST_BRANCH is the name of the destination branch for code review, as
|
||||
specified in the manifest.
|
||||
.PP
|
||||
REPO_PROJECT_FETCH_URL is the full resolved fetch URL for the project.
|
||||
.PP
|
||||
REPO_COUNT is the total number of projects being iterated.
|
||||
.PP
|
||||
REPO_I is the current (1\-based) iteration count. Can be used in conjunction with
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "June 2026" "repo gc" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo gc - manual page for repo gc
|
||||
.SH SYNOPSIS
|
||||
.B repo
|
||||
\fI\,gc\/\fR
|
||||
.SH DESCRIPTION
|
||||
Summary
|
||||
.PP
|
||||
Cleaning up internal repo and Git state.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
\fB\-h\fR, \fB\-\-help\fR
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-n\fR, \fB\-\-dry\-run\fR
|
||||
do everything except actually delete
|
||||
.TP
|
||||
\fB\-y\fR, \fB\-\-yes\fR
|
||||
answer yes to all safe prompts
|
||||
.TP
|
||||
\fB\-\-repack\fR
|
||||
repack all projects that use partial clone with filter=blob:none
|
||||
.SS Logging options:
|
||||
.TP
|
||||
\fB\-v\fR, \fB\-\-verbose\fR
|
||||
show all output
|
||||
.TP
|
||||
\fB\-q\fR, \fB\-\-quiet\fR
|
||||
only show errors
|
||||
.SS Multi\-manifest options:
|
||||
.TP
|
||||
\fB\-\-outer\-manifest\fR
|
||||
operate starting at the outermost manifest
|
||||
.TP
|
||||
\fB\-\-no\-outer\-manifest\fR
|
||||
do not operate on outer manifests
|
||||
.TP
|
||||
\fB\-\-this\-manifest\-only\fR
|
||||
only operate on this (sub)manifest
|
||||
.TP
|
||||
\fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
|
||||
operate on this manifest and its submanifests
|
||||
.PP
|
||||
Run `repo help gc` to view the detailed manual.
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "July 2022" "repo grep" "Repo Manual"
|
||||
.TH REPO "1" "June 2026" "repo grep" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo grep - manual page for repo grep
|
||||
.SH SYNOPSIS
|
||||
@@ -15,8 +15,7 @@ Print lines matching a pattern
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||
number of jobs to run in parallel (default: based on
|
||||
number of CPU cores)
|
||||
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||
.SS Logging options:
|
||||
.TP
|
||||
\fB\-\-verbose\fR
|
||||
|
||||
+21
-4
@@ -1,10 +1,10 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "July 2022" "repo info" "Repo Manual"
|
||||
.TH REPO "1" "June 2026" "repo info" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo info - manual page for repo info
|
||||
.SH SYNOPSIS
|
||||
.B repo
|
||||
\fI\,info \/\fR[\fI\,-dl\/\fR] [\fI\,-o \/\fR[\fI\,-c\/\fR]] [\fI\,<project>\/\fR...]
|
||||
\fI\,info \/\fR[\fI\,-dl\/\fR] [\fI\,-o \/\fR[\fI\,-c\/\fR]] [\fI\,--format=<format>\/\fR] [\fI\,<project>\/\fR...]
|
||||
.SH DESCRIPTION
|
||||
Summary
|
||||
.PP
|
||||
@@ -14,13 +14,27 @@ Get info on the manifest branch, current branch or unmerged branches
|
||||
\fB\-h\fR, \fB\-\-help\fR
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||
.TP
|
||||
\fB\-d\fR, \fB\-\-diff\fR
|
||||
show full info and commit diff including remote
|
||||
branches
|
||||
show full info and commit diff including remote branches
|
||||
.TP
|
||||
\fB\-o\fR, \fB\-\-overview\fR
|
||||
show overview of all local commits
|
||||
.TP
|
||||
\fB\-\-include\-summary\fR
|
||||
include manifest summary (default: true)
|
||||
.TP
|
||||
\fB\-\-no\-include\-summary\fR
|
||||
exclude manifest summary
|
||||
.TP
|
||||
\fB\-\-include\-projects\fR
|
||||
include project details (default: true)
|
||||
.TP
|
||||
\fB\-\-no\-include\-projects\fR
|
||||
exclude project details
|
||||
.TP
|
||||
\fB\-c\fR, \fB\-\-current\-branch\fR
|
||||
consider only checked out branches
|
||||
.TP
|
||||
@@ -29,6 +43,9 @@ consider all local branches
|
||||
.TP
|
||||
\fB\-l\fR, \fB\-\-local\-only\fR
|
||||
disable all remote operations
|
||||
.TP
|
||||
\fB\-\-format\fR=\fI\,FORMAT\/\fR
|
||||
output format: text, json (default: text)
|
||||
.SS Logging options:
|
||||
.TP
|
||||
\fB\-v\fR, \fB\-\-verbose\fR
|
||||
|
||||
+25
-27
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "October 2022" "repo init" "Repo Manual"
|
||||
.TH REPO "1" "June 2026" "repo init" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo init - manual page for repo init
|
||||
.SH SYNOPSIS
|
||||
@@ -28,32 +28,30 @@ manifest repository location
|
||||
\fB\-b\fR REVISION, \fB\-\-manifest\-branch\fR=\fI\,REVISION\/\fR
|
||||
manifest branch or revision (use HEAD for default)
|
||||
.TP
|
||||
\fB\-\-manifest\-upstream\-branch\fR=\fI\,BRANCH\/\fR
|
||||
when a commit is provided to \fB\-\-manifest\-branch\fR, this is the name of the git ref in which the commit can be found
|
||||
.TP
|
||||
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
|
||||
initial manifest file
|
||||
.TP
|
||||
\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]
|
||||
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|...]
|
||||
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
|
||||
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)
|
||||
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\-c\fR, \fB\-\-current\-branch\fR
|
||||
fetch only current manifest branch from server
|
||||
(default)
|
||||
fetch only current manifest branch from server (default)
|
||||
.TP
|
||||
\fB\-\-no\-current\-branch\fR
|
||||
fetch all manifest branches from server
|
||||
@@ -66,15 +64,16 @@ 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
|
||||
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.
|
||||
checkout an archive instead of a git repository for each project. See git archive.
|
||||
.TP
|
||||
\fB\-\-worktree\fR
|
||||
use git\-worktree to manage projects
|
||||
.TP
|
||||
\fB\-\-use\-local\-gitdirs\fR
|
||||
bypass .repo/projects/ and use standard Git layout in working tree
|
||||
.SS Project checkout optimizations:
|
||||
.TP
|
||||
\fB\-\-reference\fR=\fI\,DIR\/\fR
|
||||
@@ -87,33 +86,28 @@ dissociate from reference mirrors after clone
|
||||
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)
|
||||
perform partial clone (https://git\-scm.com/docs/partial\-clone)
|
||||
.TP
|
||||
\fB\-\-no\-partial\-clone\fR
|
||||
disable use of partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
|
||||
disable use of partial clone (https://git\-scm.com/docs/partial\-clone)
|
||||
.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)
|
||||
exclude the specified projects (a comma\-delimited project names) from partial clone (https://git\-scm.com/docs/partial\-clone)
|
||||
.TP
|
||||
\fB\-\-clone\-filter\fR=\fI\,CLONE_FILTER\/\fR
|
||||
filter for use with \fB\-\-partial\-clone\fR [default:
|
||||
blob:none]
|
||||
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
|
||||
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)
|
||||
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)
|
||||
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
|
||||
@@ -163,6 +157,10 @@ The optional \fB\-b\fR argument can be used to select the manifest branch to che
|
||||
and use. If no branch is specified, the remote's default branch is used. This is
|
||||
equivalent to using \fB\-b\fR HEAD.
|
||||
.PP
|
||||
The optional \fB\-\-manifest\-upstream\-branch\fR argument can be used when a commit is
|
||||
provided to \fB\-\-manifest\-branch\fR (or \fB\-b\fR), to specify the name of the git ref in
|
||||
which the commit can be found.
|
||||
.PP
|
||||
The optional \fB\-m\fR argument can be used to specify an alternate manifest to be
|
||||
used. If no manifest is specified, the manifest default.xml will be used.
|
||||
.PP
|
||||
|
||||
+5
-9
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "July 2022" "repo list" "Repo Manual"
|
||||
.TH REPO "1" "June 2026" "repo list" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo list - manual page for repo list
|
||||
.SH SYNOPSIS
|
||||
@@ -17,12 +17,10 @@ repo list [\-f] \fB\-r\fR str1 [str2]...
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-r\fR, \fB\-\-regex\fR
|
||||
filter the project list based on regex or wildcard
|
||||
matching of strings
|
||||
filter the project list based on regex or wildcard matching of strings
|
||||
.TP
|
||||
\fB\-g\fR GROUPS, \fB\-\-groups\fR=\fI\,GROUPS\/\fR
|
||||
filter the project list based on the groups the
|
||||
project is in
|
||||
filter the project list based on the groups the project is in
|
||||
.TP
|
||||
\fB\-a\fR, \fB\-\-all\fR
|
||||
show projects regardless of checkout state
|
||||
@@ -34,12 +32,10 @@ display only the name of the repository
|
||||
display only the path of the repository
|
||||
.TP
|
||||
\fB\-f\fR, \fB\-\-fullpath\fR
|
||||
display the full work tree path instead of the
|
||||
relative path
|
||||
display the full work tree path instead of the relative path
|
||||
.TP
|
||||
\fB\-\-relative\-to\fR=\fI\,PATH\/\fR
|
||||
display paths relative to this one (default: top of
|
||||
repo client checkout)
|
||||
display paths relative to this one (default: top of repo client checkout)
|
||||
.SS Logging options:
|
||||
.TP
|
||||
\fB\-v\fR, \fB\-\-verbose\fR
|
||||
|
||||
+141
-67
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "April 2024" "repo manifest" "Repo Manual"
|
||||
.TH REPO "1" "June 2026" "repo manifest" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo manifest - manual page for repo manifest
|
||||
.SH SYNOPSIS
|
||||
@@ -21,17 +21,13 @@ save revisions as current HEAD
|
||||
temporary manifest to use for this sync
|
||||
.TP
|
||||
\fB\-\-suppress\-upstream\-revision\fR
|
||||
if in \fB\-r\fR mode, do not write the upstream field (only
|
||||
of use if the branch names for a sha1 manifest are
|
||||
sensitive)
|
||||
if in \fB\-r\fR mode, do not write the upstream field (only of use if the branch names for a sha1 manifest are sensitive)
|
||||
.TP
|
||||
\fB\-\-suppress\-dest\-branch\fR
|
||||
if in \fB\-r\fR mode, do not write the dest\-branch field
|
||||
(only of use if the branch names for a sha1 manifest
|
||||
are sensitive)
|
||||
if in \fB\-r\fR mode, do not write the dest\-branch field (only of use if the branch names for a sha1 manifest are sensitive)
|
||||
.TP
|
||||
\fB\-\-json\fR
|
||||
output manifest in JSON format (experimental)
|
||||
\fB\-\-format\fR=\fI\,FORMAT\/\fR
|
||||
output format: xml, json (default: xml)
|
||||
.TP
|
||||
\fB\-\-pretty\fR
|
||||
format output for humans to read
|
||||
@@ -40,8 +36,7 @@ format output for humans to read
|
||||
ignore local manifests
|
||||
.TP
|
||||
\fB\-o\fR \-|NAME.xml, \fB\-\-output\-file\fR=\fI\,\-\/\fR|NAME.xml
|
||||
file to save the manifest to. (Filename prefix for
|
||||
multi\-tree.)
|
||||
file to save the manifest to. (Filename prefix for multi\-tree.)
|
||||
.SS Logging options:
|
||||
.TP
|
||||
\fB\-v\fR, \fB\-\-verbose\fR
|
||||
@@ -78,6 +73,10 @@ set to the ref we were on when the manifest was generated. The 'dest\-branch'
|
||||
attribute is set to indicate the remote ref to push changes to via 'repo
|
||||
upload'.
|
||||
.PP
|
||||
Multiple output formats are supported via \fB\-\-format\fR. The default output is XML,
|
||||
and formats are generally "condensed". Use \fB\-\-pretty\fR for more human\-readable
|
||||
variations.
|
||||
.PP
|
||||
repo Manifest Format
|
||||
.PP
|
||||
A repo manifest describes the structure of a repo client; that is the
|
||||
@@ -127,15 +126,17 @@ include*)>
|
||||
<!ATTLIST default dest\-branch CDATA #IMPLIED>
|
||||
<!ATTLIST default upstream CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-j CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-j\-max CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-c CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-s CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-tags CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT manifest\-server EMPTY>
|
||||
<!ATTLIST manifest\-server url CDATA #REQUIRED>
|
||||
<!ATTLIST manifest\-server helper CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT submanifest EMPTY>
|
||||
<!ATTLIST submanifest name ID #REQUIRED>
|
||||
<!ATTLIST submanifest name ID #REQUIRED>
|
||||
<!ATTLIST submanifest remote IDREF #IMPLIED>
|
||||
<!ATTLIST submanifest project CDATA #IMPLIED>
|
||||
<!ATTLIST submanifest manifest\-name CDATA #IMPLIED>
|
||||
@@ -160,15 +161,32 @@ IDREF #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST project revision
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST project dest\-branch
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST project groups
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST project sync\-c
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST project sync\-s
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST project sync\-tags
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST project upstream
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST project clone\-depth
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST project force\-path
|
||||
CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ATTLIST project dest\-branch CDATA #IMPLIED>
|
||||
<!ATTLIST project groups CDATA #IMPLIED>
|
||||
<!ATTLIST project sync\-c CDATA #IMPLIED>
|
||||
<!ATTLIST project sync\-s CDATA #IMPLIED>
|
||||
<!ATTLIST project sync\-tags CDATA #IMPLIED>
|
||||
<!ATTLIST project upstream CDATA #IMPLIED>
|
||||
<!ATTLIST project clone\-depth CDATA #IMPLIED>
|
||||
<!ATTLIST project force\-path CDATA #IMPLIED>
|
||||
<!ATTLIST project sync\-strategy CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT annotation EMPTY>
|
||||
<!ATTLIST annotation name CDATA #REQUIRED>
|
||||
@@ -180,26 +198,43 @@ CDATA #IMPLIED>
|
||||
<!ATTLIST copyfile dest CDATA #REQUIRED>
|
||||
.IP
|
||||
<!ELEMENT linkfile EMPTY>
|
||||
<!ATTLIST linkfile src CDATA #REQUIRED>
|
||||
<!ATTLIST linkfile src CDATA #REQUIRED>
|
||||
<!ATTLIST linkfile dest CDATA #REQUIRED>
|
||||
.TP
|
||||
<!ELEMENT extend\-project (annotation*,
|
||||
copyfile*,
|
||||
linkfile*)>
|
||||
.TP
|
||||
<!ATTLIST extend\-project name
|
||||
CDATA #REQUIRED>
|
||||
.TP
|
||||
<!ATTLIST extend\-project path
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST extend\-project dest\-path
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST extend\-project groups
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST extend\-project revision
|
||||
CDATA #IMPLIED>
|
||||
.TP
|
||||
<!ATTLIST extend\-project remote
|
||||
CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT extend\-project EMPTY>
|
||||
<!ATTLIST extend\-project name CDATA #REQUIRED>
|
||||
<!ATTLIST extend\-project path CDATA #IMPLIED>
|
||||
<!ATTLIST extend\-project dest\-path CDATA #IMPLIED>
|
||||
<!ATTLIST extend\-project groups CDATA #IMPLIED>
|
||||
<!ATTLIST extend\-project revision CDATA #IMPLIED>
|
||||
<!ATTLIST extend\-project remote CDATA #IMPLIED>
|
||||
<!ATTLIST extend\-project dest\-branch CDATA #IMPLIED>
|
||||
<!ATTLIST extend\-project upstream CDATA #IMPLIED>
|
||||
<!ATTLIST extend\-project upstream CDATA #IMPLIED>
|
||||
<!ATTLIST extend\-project base\-rev CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT remove\-project EMPTY>
|
||||
<!ATTLIST remove\-project name CDATA #IMPLIED>
|
||||
<!ATTLIST remove\-project path CDATA #IMPLIED>
|
||||
<!ATTLIST remove\-project optional CDATA #IMPLIED>
|
||||
<!ATTLIST remove\-project base\-rev CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT repo\-hooks EMPTY>
|
||||
<!ATTLIST repo\-hooks in\-project CDATA #REQUIRED>
|
||||
<!ATTLIST repo\-hooks in\-project CDATA #REQUIRED>
|
||||
<!ATTLIST repo\-hooks enabled\-list CDATA #REQUIRED>
|
||||
.IP
|
||||
<!ELEMENT superproject EMPTY>
|
||||
@@ -208,7 +243,7 @@ CDATA #IMPLIED>
|
||||
<!ATTLIST superproject revision CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT contactinfo EMPTY>
|
||||
<!ATTLIST contactinfo bugurl CDATA #REQUIRED>
|
||||
<!ATTLIST contactinfo bugurl CDATA #REQUIRED>
|
||||
.IP
|
||||
<!ELEMENT include EMPTY>
|
||||
<!ATTLIST include name CDATA #REQUIRED>
|
||||
@@ -288,7 +323,9 @@ when syncing a revision locked manifest in \fB\-c\fR mode to avoid having to syn
|
||||
entire ref space. Project elements not setting their own `upstream` will inherit
|
||||
this value.
|
||||
.PP
|
||||
Attribute `sync\-j`: Number of parallel jobs to use when synching.
|
||||
Attribute `sync\-j`: Number of parallel jobs to use when syncing.
|
||||
.PP
|
||||
Attribute `sync\-j\-max`: Maximum number of parallel jobs to use when syncing.
|
||||
.PP
|
||||
Attribute `sync\-c`: Set to true to only sync the given Git branch (specified in
|
||||
the `revision` attribute) rather than the whole ref space. Project elements
|
||||
@@ -304,25 +341,12 @@ Element manifest\-server
|
||||
At most one manifest\-server may be specified. The url attribute is used to
|
||||
specify the URL of a manifest server, which is an XML RPC service.
|
||||
.PP
|
||||
The manifest server should implement the following RPC methods:
|
||||
.IP
|
||||
GetApprovedManifest(branch, target)
|
||||
See the [smart sync documentation](./smart\-sync.md) for more details.
|
||||
.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.
|
||||
Attribute `url`: The URL of the manifest server.
|
||||
.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.
|
||||
Attribute `helper`: Optional name of a remote helper binary to execute to
|
||||
resolve proxying or authentication for the manifest server.
|
||||
.PP
|
||||
Element submanifest
|
||||
.PP
|
||||
@@ -374,7 +398,7 @@ supplied, `revision` is used.
|
||||
.PP
|
||||
`path` may not be an absolute path or use "." or ".." path components.
|
||||
.PP
|
||||
Attribute `groups`: List of additional groups to which all projects in the
|
||||
Attribute `groups`: Set of additional groups to which all projects in the
|
||||
included submanifest belong. This appends and recurses, meaning all projects in
|
||||
submanifests carry all parent submanifest groups. Same syntax as the
|
||||
corresponding element of `project`.
|
||||
@@ -436,7 +460,7 @@ Attribute `dest\-branch`: Name of a Git branch (e.g. `main`). When using `repo
|
||||
upload`, changes will be submitted for code review on this branch. If
|
||||
unspecified both here and in the default element, `revision` is used instead.
|
||||
.PP
|
||||
Attribute `groups`: List of groups to which this project belongs, whitespace or
|
||||
Attribute `groups`: Set of groups to which this project belongs, whitespace or
|
||||
comma separated. All projects belong to the group "all", and each project
|
||||
automatically belongs to a group of its name:`name` and path:`path`. E.g. for
|
||||
`<project name="monkeys" path="barrel\-of"/>`, that project definition is
|
||||
@@ -463,6 +487,21 @@ mirror repository according to its `path` attribute (if supplied) rather than
|
||||
the `name` attribute. This attribute only applies to the local mirrors syncing,
|
||||
it will be ignored when syncing the projects in a client working directory.
|
||||
.PP
|
||||
Attribute `sync\-strategy`: Set the sync strategy used when fetching this
|
||||
project. Currently the only supported value is `stateless`. When set to
|
||||
`stateless`, repo will run a reflog expiration and aggressive garbage collection
|
||||
at the end of the sync process. This is useful for projects that contain large
|
||||
binary files and use `clone\-depth="1"`, where garbage can accumulate as binaries
|
||||
are added, deleted, or modified across successive syncs.
|
||||
.PP
|
||||
During a stateless sync, repo checks the following before cleaning up: 1. The
|
||||
project does not share an object directory with other projects. 2. The working
|
||||
tree is clean (no uncommitted changes, no untracked files). 3. There are no
|
||||
unpushed local commits. 4. There is no Git stash.
|
||||
.PP
|
||||
If any of these conditions are not met, repo falls back to a standard sync
|
||||
without garbage collection.
|
||||
.PP
|
||||
Element extend\-project
|
||||
.PP
|
||||
Modify the attributes of the named project.
|
||||
@@ -472,6 +511,11 @@ of an existing project without completely replacing the existing project
|
||||
definition. This makes the local manifest more robust against changes to the
|
||||
original manifest.
|
||||
.PP
|
||||
The `extend\-project` element can also contain `annotation`, `copyfile`, and
|
||||
`linkfile` child elements. These are added to the project's definition. A
|
||||
`copyfile` or `linkfile` with a `dest` that already exists in the project will
|
||||
overwrite the original.
|
||||
.PP
|
||||
Attribute `path`: If specified, limit the change to projects checked out at the
|
||||
specified path, rather than all projects with the given name.
|
||||
.PP
|
||||
@@ -480,8 +524,8 @@ repo client where the Git working directory for this project should be placed.
|
||||
This is used to move a project in the checkout by overriding the existing `path`
|
||||
setting.
|
||||
.PP
|
||||
Attribute `groups`: List of additional groups to which this project belongs.
|
||||
Same syntax as the corresponding element of `project`.
|
||||
Attribute `groups`: Set of additional groups to which this project belongs. Same
|
||||
syntax as the corresponding element of `project`.
|
||||
.PP
|
||||
Attribute `revision`: If specified, overrides the revision of the original
|
||||
project. Same syntax as the corresponding element of `project`.
|
||||
@@ -495,21 +539,31 @@ project. Same syntax as the corresponding element of `project`.
|
||||
Attribute `upstream`: If specified, overrides the upstream of the original
|
||||
project. Same syntax as the corresponding element of `project`.
|
||||
.PP
|
||||
Attribute `base\-rev`: If specified, adds a check against the revision to be
|
||||
extended. Manifest parse will fail and give a list of mismatch extends if the
|
||||
revisions being extended have changed since base\-rev was set. Intended for use
|
||||
with layered manifests using hash revisions to prevent patch branches hiding
|
||||
newer upstream revisions. Also compares named refs like branches or tags but is
|
||||
misleading if branches are used as base\-rev. Same syntax as the corresponding
|
||||
element of `project`.
|
||||
.PP
|
||||
Element annotation
|
||||
.PP
|
||||
Zero or more annotation elements may be specified as children of a project or
|
||||
remote element. Each element describes a name\-value pair. For projects, this
|
||||
name\-value pair will be exported into each project's environment during a
|
||||
\&'forall' command, prefixed with `REPO__`. In addition, there is an optional
|
||||
attribute "keep" which accepts the case insensitive values "true" (default) or
|
||||
"false". This attribute determines whether or not the annotation will be kept
|
||||
when exported with the manifest subcommand.
|
||||
Zero or more annotation elements may be specified as children of a project
|
||||
element, an extend\-project element, or a remote element. Each element describes
|
||||
a name\-value pair. For projects, this name\-value pair will be exported into each
|
||||
project's environment during a 'forall' command, prefixed with `REPO__`. In
|
||||
addition, there is an optional attribute "keep" which accepts the case
|
||||
insensitive values "true" (default) or "false". This attribute determines
|
||||
whether or not the annotation will be kept when exported with the manifest
|
||||
subcommand.
|
||||
.PP
|
||||
Element copyfile
|
||||
.PP
|
||||
Zero or more copyfile elements may be specified as children of a project
|
||||
element. Each element describes a src\-dest pair of files; the "src" file will be
|
||||
copied to the "dest" place during `repo sync` command.
|
||||
element, or an extend\-project element. Each element describes a src\-dest pair of
|
||||
files; the "src" file will be copied to the "dest" place during `repo sync`
|
||||
command.
|
||||
.PP
|
||||
"src" is project relative, "dest" is relative to the top of the tree. Copying
|
||||
from paths outside of the project or to paths outside of the repo client is not
|
||||
@@ -520,10 +574,14 @@ Intermediate paths must not be symlinks either.
|
||||
.PP
|
||||
Parent directories of "dest" will be automatically created if missing.
|
||||
.PP
|
||||
The files are copied in the order they are specified in the manifests. If
|
||||
multiple elements specify the same source and destination, they will only be
|
||||
applied as one, based on the first occurence. Files are copied before any links
|
||||
specified via linkfile elements are created.
|
||||
.PP
|
||||
Element linkfile
|
||||
.PP
|
||||
It's just like copyfile and runs at the same time as copyfile but instead of
|
||||
copying it creates a symlink.
|
||||
It's just like copyfile, but instead of copying it creates a symlink.
|
||||
.PP
|
||||
The symlink is created at "dest" (relative to the top of the tree) and points to
|
||||
the path specified by "src" which is a path in the project.
|
||||
@@ -533,6 +591,11 @@ Parent directories of "dest" will be automatically created if missing.
|
||||
The symlink target may be a file or directory, but it may not point outside of
|
||||
the repo client.
|
||||
.PP
|
||||
The links are created in the order they are specified in the manifests. If
|
||||
multiple elements specify the same source and destination, they will only be
|
||||
applied as one, based on the first occurence. Links are created after any files
|
||||
specified via copyfile elements are copied.
|
||||
.PP
|
||||
Element remove\-project
|
||||
.PP
|
||||
Deletes a project from the internal manifest table, possibly allowing a
|
||||
@@ -556,6 +619,14 @@ Logic otherwise behaves like both are specified.
|
||||
Attribute `optional`: Set to true to ignore remove\-project elements with no
|
||||
matching `project` element.
|
||||
.PP
|
||||
Attribute `base\-rev`: If specified, adds a check against the revision to be
|
||||
removed. Manifest parse will fail and give a list of mismatch removes if the
|
||||
revisions being removed have changed since base\-rev was set. Intended for use
|
||||
with layered manifests using hash revisions to prevent patch branches hiding
|
||||
newer upstream revisions. Also compares named refs like branches or tags but is
|
||||
misleading if branches are used as base\-rev. Same syntax as the corresponding
|
||||
element of `project`.
|
||||
.PP
|
||||
Element repo\-hooks
|
||||
.PP
|
||||
NB: See the [practical documentation](./repo\-hooks.md) for using repo hooks.
|
||||
@@ -616,13 +687,16 @@ repository's root.
|
||||
"name" may not be an absolute path or use "." or ".." path components. These
|
||||
restrictions are not enforced for [Local Manifests].
|
||||
.PP
|
||||
Attribute `groups`: List of additional groups to which all projects in the
|
||||
Attribute `groups`: Set of additional groups to which all projects in the
|
||||
included manifest belong. This appends and recurses, meaning all projects in
|
||||
included manifests carry all parent include groups. Same syntax as the
|
||||
included manifests carry all parent include groups. This also applies to all
|
||||
extend\-project elements in the included manifests. Same syntax as the
|
||||
corresponding element of `project`.
|
||||
.PP
|
||||
Attribute `revision`: Name of a Git branch (e.g. `main` or `refs/heads/main`)
|
||||
default to which all projects in the included manifest belong.
|
||||
default to which all projects in the included manifest belong. This recurses,
|
||||
meaning it will apply to all projects in all manifests included as a result of
|
||||
this element.
|
||||
.PP
|
||||
Local Manifests
|
||||
.PP
|
||||
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "July 2022" "repo prune" "Repo Manual"
|
||||
.TH REPO "1" "June 2026" "repo prune" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo prune - manual page for repo prune
|
||||
.SH SYNOPSIS
|
||||
@@ -15,8 +15,7 @@ Prune (delete) already merged topics
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||
number of jobs to run in parallel (default: based on
|
||||
number of CPU cores)
|
||||
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||
.SS Logging options:
|
||||
.TP
|
||||
\fB\-v\fR, \fB\-\-verbose\fR
|
||||
|
||||
+2
-4
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "July 2022" "repo rebase" "Repo Manual"
|
||||
.TH REPO "1" "June 2026" "repo rebase" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo rebase - manual page for repo rebase
|
||||
.SH SYNOPSIS
|
||||
@@ -33,9 +33,7 @@ pass \fB\-\-whitespace\fR to git rebase
|
||||
stash local modifications before starting
|
||||
.TP
|
||||
\fB\-m\fR, \fB\-\-onto\-manifest\fR
|
||||
rebase onto the manifest version instead of upstream
|
||||
HEAD (this helps to make sure the local tree stays
|
||||
consistent if you previously synced to a manifest)
|
||||
rebase onto the manifest version instead of upstream HEAD (this helps to make sure the local tree stays consistent if you previously synced to a manifest)
|
||||
.SS Logging options:
|
||||
.TP
|
||||
\fB\-v\fR, \fB\-\-verbose\fR
|
||||
|
||||
+42
-28
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "April 2024" "repo smartsync" "Repo Manual"
|
||||
.TH REPO "1" "July 2026" "repo smartsync" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo smartsync - manual page for repo smartsync
|
||||
.SH SYNOPSIS
|
||||
@@ -15,16 +15,13 @@ Update working tree to the latest known good revision
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||
number of jobs to run in parallel (default: based on
|
||||
number of CPU cores)
|
||||
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||
.TP
|
||||
\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
|
||||
number of network jobs to run in parallel (defaults to
|
||||
\fB\-\-jobs\fR or 1)
|
||||
number of network jobs to run in parallel (defaults to \fB\-\-jobs\fR or 1). Ignored unless \fB\-\-no\-interleaved\fR is set
|
||||
.TP
|
||||
\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
|
||||
number of local checkout jobs to run in parallel
|
||||
(defaults to \fB\-\-jobs\fR or 8)
|
||||
number of local checkout jobs to run in parallel (defaults to \fB\-\-jobs\fR or 8). Ignored unless \fB\-\-no\-interleaved\fR is set
|
||||
.TP
|
||||
\fB\-f\fR, \fB\-\-force\-broken\fR
|
||||
obsolete option (to be deleted in the future)
|
||||
@@ -33,26 +30,28 @@ obsolete option (to be deleted in the future)
|
||||
stop syncing after first error is hit
|
||||
.TP
|
||||
\fB\-\-force\-sync\fR
|
||||
overwrite an existing git directory if it needs to
|
||||
point to a different object directory. WARNING: this
|
||||
may cause loss of data
|
||||
overwrite an existing git directory if it needs to point to a different object directory. WARNING: this may cause loss of data
|
||||
.TP
|
||||
\fB\-\-force\-checkout\fR
|
||||
force checkout even if it results in throwing away
|
||||
uncommitted modifications. WARNING: this may cause
|
||||
loss of data
|
||||
force checkout even if it results in throwing away uncommitted modifications. WARNING: this may cause loss of data
|
||||
.TP
|
||||
\fB\-\-force\-remove\-dirty\fR
|
||||
force remove projects with uncommitted modifications
|
||||
if projects no longer exist in the manifest. WARNING:
|
||||
this may cause loss of data
|
||||
force remove projects with uncommitted modifications if projects no longer exist in the manifest. WARNING: this may cause loss of data
|
||||
.TP
|
||||
\fB\-\-rebase\fR
|
||||
rebase local commits regardless of whether they are published
|
||||
.TP
|
||||
\fB\-l\fR, \fB\-\-local\-only\fR
|
||||
only update working tree, don't fetch
|
||||
.TP
|
||||
\fB\-\-no\-manifest\-update\fR, \fB\-\-nmu\fR
|
||||
use the existing manifest checkout as\-is. (do not
|
||||
update to the latest revision)
|
||||
use the existing manifest checkout as\-is. (do not update to the latest revision)
|
||||
.TP
|
||||
\fB\-\-interleaved\fR
|
||||
fetch and checkout projects in parallel (default)
|
||||
.TP
|
||||
\fB\-\-no\-interleaved\fR
|
||||
fetch and checkout projects in phases
|
||||
.TP
|
||||
\fB\-n\fR, \fB\-\-network\-only\fR
|
||||
fetch only, don't update working tree
|
||||
@@ -69,6 +68,9 @@ fetch all branches from server
|
||||
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
|
||||
temporary manifest to use for this sync
|
||||
.TP
|
||||
\fB\-g\fR GROUP, \fB\-\-groups\fR=\fI\,GROUP\/\fR
|
||||
sync projects matching the specific groups. Not persistent unlike when used on init
|
||||
.TP
|
||||
\fB\-\-clone\-bundle\fR
|
||||
enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
|
||||
.TP
|
||||
@@ -81,16 +83,21 @@ username to authenticate with the manifest server
|
||||
\fB\-p\fR MANIFEST_SERVER_PASSWORD, \fB\-\-manifest\-server\-password\fR=\fI\,MANIFEST_SERVER_PASSWORD\/\fR
|
||||
password to authenticate with the manifest server
|
||||
.TP
|
||||
\fB\-\-fetch\-submodules\fR
|
||||
fetch submodules from server
|
||||
\fB\-\-recurse\-submodules\fR
|
||||
sync submodules from server
|
||||
.TP
|
||||
\fB\-\-no\-recurse\-submodules\fR
|
||||
don't sync submodules from server
|
||||
.TP
|
||||
\fB\-\-use\-superproject\fR
|
||||
use the manifest superproject to sync projects;
|
||||
implies \fB\-c\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\-\-superproject\-revision\fR=\fI\,SUPERPROJECT_REVISION\/\fR
|
||||
sync to superproject revision (applies to outer manifest)
|
||||
.TP
|
||||
\fB\-\-tags\fR
|
||||
fetch tags
|
||||
.TP
|
||||
@@ -98,15 +105,13 @@ fetch tags
|
||||
don't fetch tags (default)
|
||||
.TP
|
||||
\fB\-\-optimized\-fetch\fR
|
||||
only fetch projects fixed to sha1 if revision does not
|
||||
exist locally
|
||||
only fetch projects fixed to sha1 if revision does not exist locally
|
||||
.TP
|
||||
\fB\-\-retry\-fetches\fR=\fI\,RETRY_FETCHES\/\fR
|
||||
number of times to retry fetches on transient errors
|
||||
.TP
|
||||
\fB\-\-prune\fR
|
||||
delete refs that no longer exist on the remote
|
||||
(default)
|
||||
delete refs that no longer exist on the remote (default)
|
||||
.TP
|
||||
\fB\-\-no\-prune\fR
|
||||
do not delete refs that no longer exist on the remote
|
||||
@@ -115,8 +120,7 @@ do not delete refs that no longer exist on the remote
|
||||
run garbage collection on all synced projects
|
||||
.TP
|
||||
\fB\-\-no\-auto\-gc\fR
|
||||
do not run garbage collection on any projects
|
||||
(default)
|
||||
do not run garbage collection on any projects (default)
|
||||
.SS Logging options:
|
||||
.TP
|
||||
\fB\-v\fR, \fB\-\-verbose\fR
|
||||
@@ -141,6 +145,16 @@ operate on this manifest and its submanifests
|
||||
.TP
|
||||
\fB\-\-no\-repo\-verify\fR
|
||||
do not verify repo source code
|
||||
.SS post\-sync hooks:
|
||||
.TP
|
||||
\fB\-\-no\-verify\fR
|
||||
Do not run the post\-sync hook.
|
||||
.TP
|
||||
\fB\-\-verify\fR
|
||||
Run the post\-sync hook without prompting.
|
||||
.TP
|
||||
\fB\-\-ignore\-hooks\fR
|
||||
Do not abort if post\-sync hooks fail.
|
||||
.PP
|
||||
Run `repo help smartsync` to view the detailed manual.
|
||||
.SH DETAILS
|
||||
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "July 2022" "repo start" "Repo Manual"
|
||||
.TH REPO "1" "June 2026" "repo start" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo start - manual page for repo start
|
||||
.SH SYNOPSIS
|
||||
@@ -15,8 +15,7 @@ Start a new branch for development
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||
number of jobs to run in parallel (default: based on
|
||||
number of CPU cores)
|
||||
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||
.TP
|
||||
\fB\-\-all\fR
|
||||
begin branch in all projects
|
||||
|
||||
+12
-5
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "July 2022" "repo status" "Repo Manual"
|
||||
.TH REPO "1" "June 2026" "repo status" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo status - manual page for repo status
|
||||
.SH SYNOPSIS
|
||||
@@ -15,12 +15,10 @@ Show the working tree status
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||
number of jobs to run in parallel (default: based on
|
||||
number of CPU cores)
|
||||
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||
.TP
|
||||
\fB\-o\fR, \fB\-\-orphans\fR
|
||||
include objects in working directory outside of repo
|
||||
projects
|
||||
include objects in working directory outside of repo projects
|
||||
.SS Logging options:
|
||||
.TP
|
||||
\fB\-v\fR, \fB\-\-verbose\fR
|
||||
@@ -70,6 +68,15 @@ branch devwork
|
||||
\fB\-m\fR
|
||||
subcmds/status.py
|
||||
.PP
|
||||
If the branch is tracking an upstream branch, the number of commits ahead and/or
|
||||
behind is also shown:
|
||||
.TP
|
||||
project repo/
|
||||
branch devwork [ahead 1, behind 2]
|
||||
.TP
|
||||
\fB\-m\fR
|
||||
subcmds/status.py
|
||||
.PP
|
||||
The first column explains how the staging area (index) differs from the last
|
||||
commit (HEAD). Its values are always displayed in upper case and have the
|
||||
following meanings:
|
||||
|
||||
+49
-32
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "April 2024" "repo sync" "Repo Manual"
|
||||
.TH REPO "1" "July 2026" "repo sync" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo sync - manual page for repo sync
|
||||
.SH SYNOPSIS
|
||||
@@ -15,16 +15,13 @@ Update working tree to the latest revision
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||
number of jobs to run in parallel (default: based on
|
||||
number of CPU cores)
|
||||
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||
.TP
|
||||
\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
|
||||
number of network jobs to run in parallel (defaults to
|
||||
\fB\-\-jobs\fR or 1)
|
||||
number of network jobs to run in parallel (defaults to \fB\-\-jobs\fR or 1). Ignored unless \fB\-\-no\-interleaved\fR is set
|
||||
.TP
|
||||
\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
|
||||
number of local checkout jobs to run in parallel
|
||||
(defaults to \fB\-\-jobs\fR or 8)
|
||||
number of local checkout jobs to run in parallel (defaults to \fB\-\-jobs\fR or 8). Ignored unless \fB\-\-no\-interleaved\fR is set
|
||||
.TP
|
||||
\fB\-f\fR, \fB\-\-force\-broken\fR
|
||||
obsolete option (to be deleted in the future)
|
||||
@@ -33,26 +30,28 @@ obsolete option (to be deleted in the future)
|
||||
stop syncing after first error is hit
|
||||
.TP
|
||||
\fB\-\-force\-sync\fR
|
||||
overwrite an existing git directory if it needs to
|
||||
point to a different object directory. WARNING: this
|
||||
may cause loss of data
|
||||
overwrite an existing git directory if it needs to point to a different object directory. WARNING: this may cause loss of data
|
||||
.TP
|
||||
\fB\-\-force\-checkout\fR
|
||||
force checkout even if it results in throwing away
|
||||
uncommitted modifications. WARNING: this may cause
|
||||
loss of data
|
||||
force checkout even if it results in throwing away uncommitted modifications. WARNING: this may cause loss of data
|
||||
.TP
|
||||
\fB\-\-force\-remove\-dirty\fR
|
||||
force remove projects with uncommitted modifications
|
||||
if projects no longer exist in the manifest. WARNING:
|
||||
this may cause loss of data
|
||||
force remove projects with uncommitted modifications if projects no longer exist in the manifest. WARNING: this may cause loss of data
|
||||
.TP
|
||||
\fB\-\-rebase\fR
|
||||
rebase local commits regardless of whether they are published
|
||||
.TP
|
||||
\fB\-l\fR, \fB\-\-local\-only\fR
|
||||
only update working tree, don't fetch
|
||||
.TP
|
||||
\fB\-\-no\-manifest\-update\fR, \fB\-\-nmu\fR
|
||||
use the existing manifest checkout as\-is. (do not
|
||||
update to the latest revision)
|
||||
use the existing manifest checkout as\-is. (do not update to the latest revision)
|
||||
.TP
|
||||
\fB\-\-interleaved\fR
|
||||
fetch and checkout projects in parallel (default)
|
||||
.TP
|
||||
\fB\-\-no\-interleaved\fR
|
||||
fetch and checkout projects in phases
|
||||
.TP
|
||||
\fB\-n\fR, \fB\-\-network\-only\fR
|
||||
fetch only, don't update working tree
|
||||
@@ -69,6 +68,9 @@ fetch all branches from server
|
||||
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
|
||||
temporary manifest to use for this sync
|
||||
.TP
|
||||
\fB\-g\fR GROUP, \fB\-\-groups\fR=\fI\,GROUP\/\fR
|
||||
sync projects matching the specific groups. Not persistent unlike when used on init
|
||||
.TP
|
||||
\fB\-\-clone\-bundle\fR
|
||||
enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
|
||||
.TP
|
||||
@@ -81,16 +83,21 @@ username to authenticate with the manifest server
|
||||
\fB\-p\fR MANIFEST_SERVER_PASSWORD, \fB\-\-manifest\-server\-password\fR=\fI\,MANIFEST_SERVER_PASSWORD\/\fR
|
||||
password to authenticate with the manifest server
|
||||
.TP
|
||||
\fB\-\-fetch\-submodules\fR
|
||||
fetch submodules from server
|
||||
\fB\-\-recurse\-submodules\fR
|
||||
sync submodules from server
|
||||
.TP
|
||||
\fB\-\-no\-recurse\-submodules\fR
|
||||
don't sync submodules from server
|
||||
.TP
|
||||
\fB\-\-use\-superproject\fR
|
||||
use the manifest superproject to sync projects;
|
||||
implies \fB\-c\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\-\-superproject\-revision\fR=\fI\,SUPERPROJECT_REVISION\/\fR
|
||||
sync to superproject revision (applies to outer manifest)
|
||||
.TP
|
||||
\fB\-\-tags\fR
|
||||
fetch tags
|
||||
.TP
|
||||
@@ -98,15 +105,13 @@ fetch tags
|
||||
don't fetch tags (default)
|
||||
.TP
|
||||
\fB\-\-optimized\-fetch\fR
|
||||
only fetch projects fixed to sha1 if revision does not
|
||||
exist locally
|
||||
only fetch projects fixed to sha1 if revision does not exist locally
|
||||
.TP
|
||||
\fB\-\-retry\-fetches\fR=\fI\,RETRY_FETCHES\/\fR
|
||||
number of times to retry fetches on transient errors
|
||||
.TP
|
||||
\fB\-\-prune\fR
|
||||
delete refs that no longer exist on the remote
|
||||
(default)
|
||||
delete refs that no longer exist on the remote (default)
|
||||
.TP
|
||||
\fB\-\-no\-prune\fR
|
||||
do not delete refs that no longer exist on the remote
|
||||
@@ -115,12 +120,10 @@ do not delete refs that no longer exist on the remote
|
||||
run garbage collection on all synced projects
|
||||
.TP
|
||||
\fB\-\-no\-auto\-gc\fR
|
||||
do not run garbage collection on any projects
|
||||
(default)
|
||||
do not run garbage collection on any projects (default)
|
||||
.TP
|
||||
\fB\-s\fR, \fB\-\-smart\-sync\fR
|
||||
smart sync using manifest from the latest known good
|
||||
build
|
||||
smart sync using manifest from the latest known good build
|
||||
.TP
|
||||
\fB\-t\fR SMART_TAG, \fB\-\-smart\-tag\fR=\fI\,SMART_TAG\/\fR
|
||||
smart sync using manifest from a known tag
|
||||
@@ -148,6 +151,16 @@ operate on this manifest and its submanifests
|
||||
.TP
|
||||
\fB\-\-no\-repo\-verify\fR
|
||||
do not verify repo source code
|
||||
.SS post\-sync hooks:
|
||||
.TP
|
||||
\fB\-\-no\-verify\fR
|
||||
Do not run the post\-sync hook.
|
||||
.TP
|
||||
\fB\-\-verify\fR
|
||||
Run the post\-sync hook without prompting.
|
||||
.TP
|
||||
\fB\-\-ignore\-hooks\fR
|
||||
Do not abort if post\-sync hooks fail.
|
||||
.PP
|
||||
Run `repo help sync` to view the detailed manual.
|
||||
.SH DETAILS
|
||||
@@ -205,8 +218,12 @@ bootstrap a new Git repository from a resumeable bundle file on a content
|
||||
delivery network. This may be necessary if there are problems with the local
|
||||
Python HTTP client or proxy configuration, but the Git binary works.
|
||||
.PP
|
||||
The \fB\-\-fetch\-submodules\fR option enables fetching Git submodules of a project from
|
||||
server.
|
||||
The \fB\-\-recurse\-submodules\fR option enables syncing Git submodules of all projects
|
||||
from the server. The \fB\-\-no\-recurse\-submodules\fR option disables syncing Git
|
||||
submodules, even when a project has sync\-s="true" in the manifest.
|
||||
.PP
|
||||
The \fB\-\-fetch\-submodules\fR and \fB\-\-no\-fetch\-submodules\fR options are deprecated aliases
|
||||
for \fB\-\-recurse\-submodules\fR and \fB\-\-no\-recurse\-submodules\fR, respectively.
|
||||
.PP
|
||||
The \fB\-c\fR/\-\-current\-branch option can be used to only fetch objects that are on the
|
||||
branch specified by a project's revision.
|
||||
|
||||
+5
-3
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "June 2024" "repo upload" "Repo Manual"
|
||||
.TH REPO "1" "August 2026" "repo upload" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo upload - manual page for repo upload
|
||||
.SH SYNOPSIS
|
||||
@@ -15,8 +15,7 @@ Upload changes for code review
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||
number of jobs to run in parallel (default: based on
|
||||
number of CPU cores)
|
||||
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||
.TP
|
||||
\fB\-t\fR, \fB\-\-topic\-branch\fR
|
||||
set the topic to the local branch name
|
||||
@@ -113,6 +112,9 @@ Run the pre\-upload hook without prompting.
|
||||
.TP
|
||||
\fB\-\-ignore\-hooks\fR
|
||||
Do not abort if pre\-upload hooks fail.
|
||||
.TP
|
||||
\fB\-\-fix\fR
|
||||
Automatically apply pre\-upload fixes without prompting.
|
||||
.PP
|
||||
Run `repo help upload` to view the detailed manual.
|
||||
.SH DETAILS
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "June 2026" "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
|
||||
+9
-5
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "April 2024" "repo" "Repo Manual"
|
||||
.TH REPO "1" "June 2026" "repo" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repository management tool built on top of git
|
||||
.SH SYNOPSIS
|
||||
@@ -26,8 +26,7 @@ control color usage: auto, always, never
|
||||
trace git command execution (REPO_TRACE=1)
|
||||
.TP
|
||||
\fB\-\-trace\-to\-stderr\fR
|
||||
trace outputs go to stderr in addition to
|
||||
\&.repo/TRACE_FILE
|
||||
trace outputs go to stderr in addition to .repo/TRACE_FILE
|
||||
.TP
|
||||
\fB\-\-trace\-python\fR
|
||||
trace python command execution
|
||||
@@ -39,8 +38,7 @@ time repo command execution
|
||||
display this version of repo
|
||||
.TP
|
||||
\fB\-\-show\-toplevel\fR
|
||||
display the path of the top\-level directory of the
|
||||
repo client checkout
|
||||
display the path of the top\-level directory of the repo client checkout
|
||||
.TP
|
||||
\fB\-\-event\-log\fR=\fI\,EVENT_LOG\/\fR
|
||||
filename of event log to append timeline to
|
||||
@@ -79,6 +77,9 @@ Download and checkout a change
|
||||
forall
|
||||
Run a shell command in each project
|
||||
.TP
|
||||
gc
|
||||
Cleaning up internal repo and Git state.
|
||||
.TP
|
||||
grep
|
||||
Print lines matching a pattern
|
||||
.TP
|
||||
@@ -129,6 +130,9 @@ Upload changes for code review
|
||||
.TP
|
||||
version
|
||||
Display the version of repo
|
||||
.TP
|
||||
wipe
|
||||
Wipe projects from the worktree
|
||||
.PP
|
||||
See 'repo help <command>' for more information on a specific command.
|
||||
Bug reports: https://issues.gerritcodereview.com/issues/new?component=1370071
|
||||
|
||||
+196
-104
@@ -155,6 +155,7 @@ class _Default:
|
||||
upstreamExpr = None
|
||||
remote = None
|
||||
sync_j = None
|
||||
sync_j_max = None
|
||||
sync_c = False
|
||||
sync_s = False
|
||||
sync_tags = True
|
||||
@@ -255,7 +256,7 @@ class _XmlSubmanifest:
|
||||
project: a string, the name of the manifest project.
|
||||
revision: a string, the commitish.
|
||||
manifestName: a string, the submanifest file name.
|
||||
groups: a list of strings, the groups to add to all projects in the
|
||||
groups: a set of strings, the groups to add to all projects in the
|
||||
submanifest.
|
||||
default_groups: a list of strings, the default groups to sync.
|
||||
path: a string, the relative path for the submanifest checkout.
|
||||
@@ -281,7 +282,7 @@ class _XmlSubmanifest:
|
||||
self.project = project
|
||||
self.revision = revision
|
||||
self.manifestName = manifestName
|
||||
self.groups = groups
|
||||
self.groups = groups or set()
|
||||
self.default_groups = default_groups
|
||||
self.path = path
|
||||
self.parent = parent
|
||||
@@ -304,7 +305,7 @@ class _XmlSubmanifest:
|
||||
self.repo_client = RepoClient(
|
||||
parent.repodir,
|
||||
linkFile,
|
||||
parent_groups=",".join(groups) or "",
|
||||
parent_groups=groups,
|
||||
submanifest_path=os.path.join(parent.path_prefix, self.relpath),
|
||||
outer_client=outer_client,
|
||||
default_groups=default_groups,
|
||||
@@ -345,7 +346,7 @@ class _XmlSubmanifest:
|
||||
manifestName = self.manifestName or "default.xml"
|
||||
revision = self.revision or self.name
|
||||
path = self.path or revision.split("/")[-1]
|
||||
groups = self.groups or []
|
||||
groups = self.groups
|
||||
|
||||
return SubmanifestSpec(
|
||||
self.name, manifestUrl, manifestName, revision, path, groups
|
||||
@@ -359,9 +360,7 @@ class _XmlSubmanifest:
|
||||
|
||||
def GetGroupsStr(self):
|
||||
"""Returns the `groups` given for this submanifest."""
|
||||
if self.groups:
|
||||
return ",".join(self.groups)
|
||||
return ""
|
||||
return ",".join(sorted(self.groups))
|
||||
|
||||
def GetDefaultGroupsStr(self):
|
||||
"""Returns the `default-groups` given for this submanifest."""
|
||||
@@ -381,7 +380,7 @@ class SubmanifestSpec:
|
||||
self.manifestName = manifestName
|
||||
self.revision = revision
|
||||
self.path = path
|
||||
self.groups = groups or []
|
||||
self.groups = groups
|
||||
|
||||
|
||||
class XmlManifest:
|
||||
@@ -393,7 +392,7 @@ class XmlManifest:
|
||||
manifest_file,
|
||||
local_manifests=None,
|
||||
outer_client=None,
|
||||
parent_groups="",
|
||||
parent_groups=None,
|
||||
submanifest_path="",
|
||||
default_groups=None,
|
||||
):
|
||||
@@ -409,7 +408,8 @@ class XmlManifest:
|
||||
manifests. This will usually be
|
||||
|repodir|/|LOCAL_MANIFESTS_DIR_NAME|.
|
||||
outer_client: RepoClient of the outer manifest.
|
||||
parent_groups: a string, the groups to apply to this projects.
|
||||
parent_groups: a set of strings, the groups to apply to this
|
||||
manifest.
|
||||
submanifest_path: The submanifest root relative to the repo root.
|
||||
default_groups: a string, the default manifest groups to use.
|
||||
"""
|
||||
@@ -432,7 +432,7 @@ class XmlManifest:
|
||||
self.manifestFileOverrides = {}
|
||||
self.local_manifests = local_manifests
|
||||
self._load_local_manifests = True
|
||||
self.parent_groups = parent_groups
|
||||
self.parent_groups = parent_groups or set()
|
||||
self.default_groups = default_groups
|
||||
|
||||
if submanifest_path and not outer_client:
|
||||
@@ -567,21 +567,29 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
"""
|
||||
return [x for x in re.split(r"[,\s]+", field) if x]
|
||||
|
||||
def _ParseSet(self, field):
|
||||
"""Parse fields that contain flattened sets.
|
||||
|
||||
These are whitespace & comma separated. Empty elements will be
|
||||
discarded.
|
||||
"""
|
||||
return set(self._ParseList(field))
|
||||
|
||||
def ToXml(
|
||||
self,
|
||||
peg_rev=False,
|
||||
peg_rev_upstream=True,
|
||||
peg_rev_dest_branch=True,
|
||||
groups=None,
|
||||
filter_groups=None,
|
||||
omit_local=False,
|
||||
):
|
||||
"""Return the current manifest XML."""
|
||||
mp = self.manifestProject
|
||||
|
||||
if groups is None:
|
||||
groups = mp.manifest_groups
|
||||
if groups:
|
||||
groups = self._ParseList(groups)
|
||||
if filter_groups is None:
|
||||
filter_groups = mp.manifest_groups
|
||||
if filter_groups:
|
||||
filter_groups = self._ParseList(filter_groups)
|
||||
|
||||
doc = xml.dom.minidom.Document()
|
||||
root = doc.createElement("manifest")
|
||||
@@ -624,6 +632,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if d.sync_j is not None:
|
||||
have_default = True
|
||||
e.setAttribute("sync-j", "%d" % d.sync_j)
|
||||
if d.sync_j_max is not None:
|
||||
have_default = True
|
||||
e.setAttribute("sync-j-max", "%d" % d.sync_j_max)
|
||||
if d.sync_c:
|
||||
have_default = True
|
||||
e.setAttribute("sync-c", "true")
|
||||
@@ -640,6 +651,8 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if self._manifest_server:
|
||||
e = doc.createElement("manifest-server")
|
||||
e.setAttribute("url", self._manifest_server)
|
||||
if self._manifest_server_helper:
|
||||
e.setAttribute("helper", self._manifest_server_helper)
|
||||
root.appendChild(e)
|
||||
root.appendChild(doc.createTextNode(""))
|
||||
|
||||
@@ -654,7 +667,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
output_project(parent, parent_node, project)
|
||||
|
||||
def output_project(parent, parent_node, p):
|
||||
if not p.MatchesGroups(groups):
|
||||
if not p.MatchesGroups(filter_groups):
|
||||
return
|
||||
|
||||
if omit_local and self.IsFromLocalManifest(p):
|
||||
@@ -679,9 +692,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
e.setAttribute("remote", remoteName)
|
||||
if peg_rev:
|
||||
if self.IsMirror:
|
||||
value = p.bare_git.rev_parse(p.revisionExpr + "^0")
|
||||
value = p.bare_git.ResolveCommit(p.revisionExpr)
|
||||
else:
|
||||
value = p.work_git.rev_parse(HEAD + "^0")
|
||||
value = p.work_git.ResolveCommit(HEAD)
|
||||
e.setAttribute("revision", value)
|
||||
if peg_rev_upstream:
|
||||
if p.upstream:
|
||||
@@ -725,10 +738,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
le.setAttribute("dest", lf.dest)
|
||||
e.appendChild(le)
|
||||
|
||||
default_groups = ["all", "name:%s" % p.name, "path:%s" % p.relpath]
|
||||
egroups = [g for g in p.groups if g not in default_groups]
|
||||
if egroups:
|
||||
e.setAttribute("groups", ",".join(egroups))
|
||||
groups = p.groups - {"all", f"name:{p.name}", f"path:{p.relpath}"}
|
||||
if groups:
|
||||
e.setAttribute("groups", ",".join(sorted(groups)))
|
||||
|
||||
for a in p.annotations:
|
||||
if a.keep == "true":
|
||||
@@ -749,14 +761,17 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if p.clone_depth:
|
||||
e.setAttribute("clone-depth", str(p.clone_depth))
|
||||
|
||||
if p.sync_strategy:
|
||||
e.setAttribute("sync-strategy", str(p.sync_strategy))
|
||||
|
||||
self._output_manifest_project_extras(p, e)
|
||||
|
||||
if p.subprojects:
|
||||
subprojects = {subp.name for subp in p.subprojects}
|
||||
output_projects(p, e, list(sorted(subprojects)))
|
||||
output_projects(p, e, sorted(subprojects))
|
||||
|
||||
projects = {p.name for p in self._paths.values() if not p.parent}
|
||||
output_projects(None, root, list(sorted(projects)))
|
||||
output_projects(None, root, sorted(projects))
|
||||
|
||||
if self._repo_hooks_project:
|
||||
root.appendChild(doc.createTextNode(""))
|
||||
@@ -813,7 +828,6 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
"submanifest",
|
||||
# These are children of 'project' nodes.
|
||||
"annotation",
|
||||
"project",
|
||||
"copyfile",
|
||||
"linkfile",
|
||||
}
|
||||
@@ -987,6 +1001,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
self._Load()
|
||||
return self._manifest_server
|
||||
|
||||
@property
|
||||
def manifest_server_helper(self):
|
||||
self._Load()
|
||||
return self._manifest_server_helper
|
||||
|
||||
@property
|
||||
def CloneBundle(self):
|
||||
clone_bundle = self.manifestProject.clone_bundle
|
||||
@@ -1014,9 +1033,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
|
||||
def SetManifestOverride(self, path):
|
||||
"""Override manifestFile. The caller must call Unload()"""
|
||||
self._outer_client.manifest.manifestFileOverrides[
|
||||
self.path_prefix
|
||||
] = path
|
||||
self._outer_client.manifest.manifestFileOverrides[self.path_prefix] = (
|
||||
path
|
||||
)
|
||||
|
||||
@property
|
||||
def UseLocalManifests(self):
|
||||
@@ -1043,6 +1062,10 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
def UseGitWorktrees(self):
|
||||
return self.manifestProject.use_worktree
|
||||
|
||||
@property
|
||||
def UseLocalGitDirs(self):
|
||||
return self.manifestProject.use_local_gitdirs
|
||||
|
||||
@property
|
||||
def IsArchive(self):
|
||||
return self.manifestProject.archive
|
||||
@@ -1116,7 +1139,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
groups += f",platform-{platform.system().lower()}"
|
||||
return groups
|
||||
|
||||
def GetGroupsStr(self):
|
||||
def GetManifestGroupsStr(self):
|
||||
"""Returns the manifest group string that should be synced."""
|
||||
return (
|
||||
self.manifestProject.manifest_groups or self.GetDefaultGroupsStr()
|
||||
@@ -1141,6 +1164,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
self._notice = None
|
||||
self.branch = None
|
||||
self._manifest_server = None
|
||||
self._manifest_server_helper = None
|
||||
|
||||
def Load(self):
|
||||
"""Read the manifest into memory."""
|
||||
@@ -1171,12 +1195,12 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
b = b[len(R_HEADS) :]
|
||||
self.branch = b
|
||||
|
||||
parent_groups = self.parent_groups
|
||||
parent_groups = self.parent_groups.copy()
|
||||
if self.path_prefix:
|
||||
parent_groups = (
|
||||
parent_groups |= {
|
||||
f"{SUBMANIFEST_GROUP_PREFIX}:path:"
|
||||
f"{self.path_prefix},{parent_groups}"
|
||||
)
|
||||
f"{self.path_prefix}"
|
||||
}
|
||||
|
||||
# The manifestFile was specified by the user which is why we
|
||||
# allow include paths to point anywhere.
|
||||
@@ -1202,16 +1226,16 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
# Since local manifests are entirely managed by
|
||||
# the user, allow them to point anywhere the
|
||||
# user wants.
|
||||
local_group = (
|
||||
local_group = {
|
||||
f"{LOCAL_MANIFEST_GROUP_PREFIX}:"
|
||||
f"{local_file[:-4]}"
|
||||
)
|
||||
}
|
||||
nodes.append(
|
||||
self._ParseManifestXml(
|
||||
local,
|
||||
self.subdir,
|
||||
parent_groups=(
|
||||
f"{local_group},{parent_groups}"
|
||||
local_group | parent_groups
|
||||
),
|
||||
restrict_includes=False,
|
||||
)
|
||||
@@ -1262,7 +1286,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
self,
|
||||
path,
|
||||
include_root,
|
||||
parent_groups="",
|
||||
parent_groups=None,
|
||||
restrict_includes=True,
|
||||
parent_node=None,
|
||||
):
|
||||
@@ -1271,11 +1295,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
Args:
|
||||
path: The XML file to read & parse.
|
||||
include_root: The path to interpret include "name"s relative to.
|
||||
parent_groups: The groups to apply to this projects.
|
||||
parent_groups: The set of groups to apply to this manifest.
|
||||
restrict_includes: Whether to constrain the "name" attribute of
|
||||
includes.
|
||||
parent_node: The parent include node, to apply attribute to this
|
||||
projects.
|
||||
parent_node: The parent include node, to apply attributes to this
|
||||
manifest.
|
||||
|
||||
Returns:
|
||||
List of XML nodes.
|
||||
@@ -1299,6 +1323,14 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
|
||||
nodes = []
|
||||
for node in manifest.childNodes:
|
||||
if (
|
||||
parent_node
|
||||
and node.nodeName in ("include", "project")
|
||||
and not node.hasAttribute("revision")
|
||||
):
|
||||
node.setAttribute(
|
||||
"revision", parent_node.getAttribute("revision")
|
||||
)
|
||||
if node.nodeName == "include":
|
||||
name = self._reqatt(node, "name")
|
||||
if restrict_includes:
|
||||
@@ -1307,12 +1339,10 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
raise ManifestInvalidPathError(
|
||||
f'<include> invalid "name": {name}: {msg}'
|
||||
)
|
||||
include_groups = ""
|
||||
if parent_groups:
|
||||
include_groups = parent_groups
|
||||
include_groups = (parent_groups or set()).copy()
|
||||
if node.hasAttribute("groups"):
|
||||
include_groups = (
|
||||
node.getAttribute("groups") + "," + include_groups
|
||||
include_groups |= self._ParseSet(
|
||||
node.getAttribute("groups")
|
||||
)
|
||||
fp = os.path.join(include_root, name)
|
||||
if not os.path.isfile(fp):
|
||||
@@ -1328,33 +1358,23 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
)
|
||||
# should isolate this to the exact exception, but that's
|
||||
# tricky. actual parsing implementation may vary.
|
||||
except (
|
||||
KeyboardInterrupt,
|
||||
RuntimeError,
|
||||
SystemExit,
|
||||
ManifestParseError,
|
||||
):
|
||||
except (RuntimeError, ManifestParseError):
|
||||
raise
|
||||
except Exception as e:
|
||||
raise ManifestParseError(
|
||||
f"failed parsing included manifest {name}: {e}"
|
||||
)
|
||||
else:
|
||||
if parent_groups and node.nodeName == "project":
|
||||
nodeGroups = parent_groups
|
||||
if node.hasAttribute("groups"):
|
||||
nodeGroups = (
|
||||
node.getAttribute("groups") + "," + nodeGroups
|
||||
)
|
||||
node.setAttribute("groups", nodeGroups)
|
||||
if (
|
||||
parent_node
|
||||
and node.nodeName == "project"
|
||||
and not node.hasAttribute("revision")
|
||||
if parent_groups and node.nodeName in (
|
||||
"project",
|
||||
"extend-project",
|
||||
):
|
||||
node.setAttribute(
|
||||
"revision", parent_node.getAttribute("revision")
|
||||
)
|
||||
nodeGroups = parent_groups.copy()
|
||||
if node.hasAttribute("groups"):
|
||||
nodeGroups |= self._ParseSet(
|
||||
node.getAttribute("groups")
|
||||
)
|
||||
node.setAttribute("groups", ",".join(sorted(nodeGroups)))
|
||||
nodes.append(node)
|
||||
return nodes
|
||||
|
||||
@@ -1414,11 +1434,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
for node in itertools.chain(*node_list):
|
||||
if node.nodeName == "manifest-server":
|
||||
url = self._reqatt(node, "url")
|
||||
helper = node.getAttribute("helper") or None
|
||||
if self._manifest_server is not None:
|
||||
raise ManifestParseError(
|
||||
"duplicate manifest-server in %s" % (self.manifestFile)
|
||||
)
|
||||
self._manifest_server = url
|
||||
self._manifest_server_helper = helper
|
||||
|
||||
def recursively_add_projects(project):
|
||||
projects = self._projects.setdefault(project.name, [])
|
||||
@@ -1445,6 +1467,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
|
||||
repo_hooks_project = None
|
||||
enabled_repo_hooks = None
|
||||
failed_revision_changes = []
|
||||
for node in itertools.chain(*node_list):
|
||||
if node.nodeName == "project":
|
||||
project = self._ParseProject(node)
|
||||
@@ -1462,7 +1485,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
dest_path = node.getAttribute("dest-path")
|
||||
groups = node.getAttribute("groups")
|
||||
if groups:
|
||||
groups = self._ParseList(groups)
|
||||
groups = self._ParseSet(groups or "")
|
||||
revision = node.getAttribute("revision")
|
||||
remote_name = node.getAttribute("remote")
|
||||
if not remote_name:
|
||||
@@ -1471,6 +1494,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
remote = self._get_remote(node)
|
||||
dest_branch = node.getAttribute("dest-branch")
|
||||
upstream = node.getAttribute("upstream")
|
||||
base_revision = node.getAttribute("base-rev")
|
||||
|
||||
named_projects = self._projects[name]
|
||||
if dest_path and not path and len(named_projects) > 1:
|
||||
@@ -1482,8 +1506,23 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if path and p.relpath != path:
|
||||
continue
|
||||
if groups:
|
||||
p.groups.extend(groups)
|
||||
p.groups |= groups
|
||||
# Drop local groups so we don't mistakenly omit this
|
||||
# project from the superproject override manifest.
|
||||
p.groups = {
|
||||
g
|
||||
for g in p.groups
|
||||
if not g.startswith(LOCAL_MANIFEST_GROUP_PREFIX)
|
||||
}
|
||||
|
||||
if revision:
|
||||
if base_revision:
|
||||
if p.revisionExpr != base_revision:
|
||||
failed_revision_changes.append(
|
||||
"extend-project name %s mismatch base "
|
||||
"%s vs revision %s"
|
||||
% (name, base_revision, p.revisionExpr)
|
||||
)
|
||||
p.SetRevision(revision)
|
||||
|
||||
if remote_name:
|
||||
@@ -1505,6 +1544,14 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
p.UpdatePaths(relpath, worktree, gitdir, objdir)
|
||||
self._paths[p.relpath] = p
|
||||
|
||||
for n in node.childNodes:
|
||||
if n.nodeName == "copyfile":
|
||||
self._ParseCopyFile(p, n)
|
||||
elif n.nodeName == "linkfile":
|
||||
self._ParseLinkFile(p, n)
|
||||
elif n.nodeName == "annotation":
|
||||
self._ParseAnnotation(p, n)
|
||||
|
||||
if node.nodeName == "repo-hooks":
|
||||
# Only one project can be the hooks project
|
||||
if repo_hooks_project is not None:
|
||||
@@ -1558,6 +1605,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if node.nodeName == "remove-project":
|
||||
name = node.getAttribute("name")
|
||||
path = node.getAttribute("path")
|
||||
base_revision = node.getAttribute("base-rev")
|
||||
|
||||
# Name or path needed.
|
||||
if not name and not path:
|
||||
@@ -1571,6 +1619,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
for projname, projects in list(self._projects.items()):
|
||||
for p in projects:
|
||||
if name == projname and not path:
|
||||
if base_revision:
|
||||
if p.revisionExpr != base_revision:
|
||||
failed_revision_changes.append(
|
||||
"remove-project name %s mismatch base "
|
||||
"%s vs revision %s"
|
||||
% (name, base_revision, p.revisionExpr)
|
||||
)
|
||||
del self._paths[p.relpath]
|
||||
if not removed_project:
|
||||
del self._projects[name]
|
||||
@@ -1578,6 +1633,17 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
elif path == p.relpath and (
|
||||
name == projname or not name
|
||||
):
|
||||
if base_revision:
|
||||
if p.revisionExpr != base_revision:
|
||||
failed_revision_changes.append(
|
||||
"remove-project path %s mismatch base "
|
||||
"%s vs revision %s"
|
||||
% (
|
||||
p.relpath,
|
||||
base_revision,
|
||||
p.revisionExpr,
|
||||
)
|
||||
)
|
||||
self._projects[projname].remove(p)
|
||||
del self._paths[p.relpath]
|
||||
removed_project = p.name
|
||||
@@ -1597,6 +1663,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
"project: %s" % node.toxml()
|
||||
)
|
||||
|
||||
if failed_revision_changes:
|
||||
raise ManifestParseError(
|
||||
"revision base check failed, rebase patches and update "
|
||||
"base revs for: ",
|
||||
failed_revision_changes,
|
||||
)
|
||||
|
||||
# Store repo hooks project information.
|
||||
if repo_hooks_project:
|
||||
# Store a reference to the Project.
|
||||
@@ -1710,6 +1783,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
% (self.manifestFile, d.sync_j)
|
||||
)
|
||||
|
||||
d.sync_j_max = XmlInt(node, "sync-j-max", None)
|
||||
if d.sync_j_max is not None and d.sync_j_max <= 0:
|
||||
raise ManifestParseError(
|
||||
'%s: sync-j-max must be greater than 0, not "%s"'
|
||||
% (self.manifestFile, d.sync_j_max)
|
||||
)
|
||||
|
||||
d.sync_c = XmlBool(node, "sync-c", False)
|
||||
d.sync_s = XmlBool(node, "sync-s", False)
|
||||
d.sync_tags = XmlBool(node, "sync-tags", True)
|
||||
@@ -1772,7 +1852,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
groups = ""
|
||||
if node.hasAttribute("groups"):
|
||||
groups = node.getAttribute("groups")
|
||||
groups = self._ParseList(groups)
|
||||
groups = self._ParseSet(groups)
|
||||
default_groups = self._ParseList(node.getAttribute("default-groups"))
|
||||
path = node.getAttribute("path")
|
||||
if path == "":
|
||||
@@ -1875,17 +1955,14 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
% (self.manifestFile, clone_depth)
|
||||
)
|
||||
|
||||
sync_strategy = node.getAttribute("sync-strategy") or None
|
||||
|
||||
dest_branch = (
|
||||
node.getAttribute("dest-branch") or self._default.destBranchExpr
|
||||
)
|
||||
|
||||
upstream = node.getAttribute("upstream") or self._default.upstreamExpr
|
||||
|
||||
groups = ""
|
||||
if node.hasAttribute("groups"):
|
||||
groups = node.getAttribute("groups")
|
||||
groups = self._ParseList(groups)
|
||||
|
||||
if parent is None:
|
||||
(
|
||||
relpath,
|
||||
@@ -1900,8 +1977,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
parent, name, path
|
||||
)
|
||||
|
||||
default_groups = ["all", "name:%s" % name, "path:%s" % relpath]
|
||||
groups.extend(set(default_groups).difference(groups))
|
||||
groups = ""
|
||||
if node.hasAttribute("groups"):
|
||||
groups = node.getAttribute("groups")
|
||||
groups = self._ParseSet(groups)
|
||||
groups |= {"all", f"name:{name}", f"path:{relpath}"}
|
||||
|
||||
if self.IsMirror and node.hasAttribute("force-path"):
|
||||
if XmlBool(node, "force-path", False):
|
||||
@@ -1923,6 +2003,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
sync_s=sync_s,
|
||||
sync_tags=sync_tags,
|
||||
clone_depth=clone_depth,
|
||||
sync_strategy=sync_strategy,
|
||||
upstream=upstream,
|
||||
parent=parent,
|
||||
dest_branch=dest_branch,
|
||||
@@ -1933,11 +2014,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
for n in node.childNodes:
|
||||
if n.nodeName == "copyfile":
|
||||
self._ParseCopyFile(project, n)
|
||||
if n.nodeName == "linkfile":
|
||||
elif n.nodeName == "linkfile":
|
||||
self._ParseLinkFile(project, n)
|
||||
if n.nodeName == "annotation":
|
||||
elif n.nodeName == "annotation":
|
||||
self._ParseAnnotation(project, n)
|
||||
if n.nodeName == "project":
|
||||
elif n.nodeName == "project":
|
||||
project.subprojects.append(
|
||||
self._ParseProject(n, parent=project)
|
||||
)
|
||||
@@ -1975,15 +2056,21 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
else:
|
||||
namepath = f"{name}.git"
|
||||
worktree = os.path.join(self.topdir, path).replace("\\", "/")
|
||||
gitdir = os.path.join(self.subdir, "projects", "%s.git" % path)
|
||||
# We allow people to mix git worktrees & non-git worktrees for now.
|
||||
# This allows for in situ migration of repo clients.
|
||||
if os.path.exists(gitdir) or not self.UseGitWorktrees:
|
||||
objdir = os.path.join(self.repodir, "project-objects", namepath)
|
||||
else:
|
||||
use_git_worktrees = True
|
||||
gitdir = os.path.join(self.repodir, "worktrees", namepath)
|
||||
if self.UseLocalGitDirs:
|
||||
gitdir = os.path.join(worktree, ".git")
|
||||
objdir = gitdir
|
||||
else:
|
||||
gitdir = os.path.join(self.subdir, "projects", "%s.git" % path)
|
||||
# We allow people to mix git worktrees & non-git worktrees for
|
||||
# now. This allows for in situ migration of repo clients.
|
||||
if os.path.exists(gitdir) or not self.UseGitWorktrees:
|
||||
objdir = os.path.join(
|
||||
self.repodir, "project-objects", namepath
|
||||
)
|
||||
else:
|
||||
use_git_worktrees = True
|
||||
gitdir = os.path.join(self.repodir, "worktrees", namepath)
|
||||
objdir = gitdir
|
||||
return relpath, worktree, gitdir, objdir, use_git_worktrees
|
||||
|
||||
def GetProjectsWithName(self, name, all_manifests=False):
|
||||
@@ -2021,7 +2108,12 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
path = path.rstrip("/")
|
||||
name = name.rstrip("/")
|
||||
relpath = self._JoinRelpath(parent.relpath, path)
|
||||
gitdir = os.path.join(parent.gitdir, "subprojects", "%s.git" % path)
|
||||
subprojects = os.path.join(parent.gitdir, "subprojects", f"{path}.git")
|
||||
modules = os.path.join(parent.gitdir, "modules", path)
|
||||
if platform_utils.isdir(subprojects):
|
||||
gitdir = subprojects
|
||||
else:
|
||||
gitdir = modules
|
||||
objdir = os.path.join(
|
||||
parent.gitdir, "subproject-objects", "%s.git" % name
|
||||
)
|
||||
@@ -2072,22 +2164,22 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
# implementation:
|
||||
# https://eclipse.googlesource.com/jgit/jgit/+/9110037e3e9461ff4dac22fee84ef3694ed57648/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java#884
|
||||
BAD_CODEPOINTS = {
|
||||
"\u200C", # ZERO WIDTH NON-JOINER
|
||||
"\u200D", # ZERO WIDTH JOINER
|
||||
"\u200E", # LEFT-TO-RIGHT MARK
|
||||
"\u200F", # RIGHT-TO-LEFT MARK
|
||||
"\u202A", # LEFT-TO-RIGHT EMBEDDING
|
||||
"\u202B", # RIGHT-TO-LEFT EMBEDDING
|
||||
"\u202C", # POP DIRECTIONAL FORMATTING
|
||||
"\u202D", # LEFT-TO-RIGHT OVERRIDE
|
||||
"\u202E", # RIGHT-TO-LEFT OVERRIDE
|
||||
"\u206A", # INHIBIT SYMMETRIC SWAPPING
|
||||
"\u206B", # ACTIVATE SYMMETRIC SWAPPING
|
||||
"\u206C", # INHIBIT ARABIC FORM SHAPING
|
||||
"\u206D", # ACTIVATE ARABIC FORM SHAPING
|
||||
"\u206E", # NATIONAL DIGIT SHAPES
|
||||
"\u206F", # NOMINAL DIGIT SHAPES
|
||||
"\uFEFF", # ZERO WIDTH NO-BREAK SPACE
|
||||
"\u200c", # ZERO WIDTH NON-JOINER
|
||||
"\u200d", # ZERO WIDTH JOINER
|
||||
"\u200e", # LEFT-TO-RIGHT MARK
|
||||
"\u200f", # RIGHT-TO-LEFT MARK
|
||||
"\u202a", # LEFT-TO-RIGHT EMBEDDING
|
||||
"\u202b", # RIGHT-TO-LEFT EMBEDDING
|
||||
"\u202c", # POP DIRECTIONAL FORMATTING
|
||||
"\u202d", # LEFT-TO-RIGHT OVERRIDE
|
||||
"\u202e", # RIGHT-TO-LEFT OVERRIDE
|
||||
"\u206a", # INHIBIT SYMMETRIC SWAPPING
|
||||
"\u206b", # ACTIVATE SYMMETRIC SWAPPING
|
||||
"\u206c", # INHIBIT ARABIC FORM SHAPING
|
||||
"\u206d", # ACTIVATE ARABIC FORM SHAPING
|
||||
"\u206e", # NATIONAL DIGIT SHAPES
|
||||
"\u206f", # NOMINAL DIGIT SHAPES
|
||||
"\ufeff", # ZERO WIDTH NO-BREAK SPACE
|
||||
}
|
||||
if BAD_CODEPOINTS & path_codepoints:
|
||||
# This message is more expansive than reality, but should be fine.
|
||||
|
||||
@@ -40,7 +40,7 @@ def RunPager(globalConfig):
|
||||
|
||||
|
||||
def TerminatePager():
|
||||
global pager_process, old_stdout, old_stderr
|
||||
global pager_process
|
||||
if pager_process:
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
|
||||
@@ -156,6 +156,12 @@ def remove(path, missing_ok=False):
|
||||
os.rmdir(longpath)
|
||||
else:
|
||||
os.remove(longpath)
|
||||
elif (
|
||||
e.errno == errno.EROFS
|
||||
and missing_ok
|
||||
and not os.path.exists(longpath)
|
||||
):
|
||||
pass
|
||||
elif missing_ok and e.errno == errno.ENOENT:
|
||||
pass
|
||||
else:
|
||||
@@ -216,6 +222,43 @@ def rmdir(path):
|
||||
os.rmdir(_makelongpath(path))
|
||||
|
||||
|
||||
def removedirs(path: str) -> None:
|
||||
"""Remove a directory tree of symlinks and empty directories.
|
||||
|
||||
Walks |path| bottom-up without following symlinks. Removes symlinks
|
||||
and empty directories. Stops at (and preserves) regular files and
|
||||
non-empty directories. Silently succeeds if |path| does not exist.
|
||||
|
||||
Availability: Unix, Windows.
|
||||
"""
|
||||
if islink(path):
|
||||
remove(path)
|
||||
return
|
||||
|
||||
if not isdir(path):
|
||||
return
|
||||
|
||||
for dirpath, dirnames, filenames in walk(path, topdown=False):
|
||||
for name in dirnames:
|
||||
entry = os.path.join(dirpath, name)
|
||||
if islink(entry):
|
||||
remove(entry)
|
||||
else:
|
||||
try:
|
||||
rmdir(entry)
|
||||
except OSError:
|
||||
pass
|
||||
for name in filenames:
|
||||
entry = os.path.join(dirpath, name)
|
||||
if islink(entry):
|
||||
remove(entry)
|
||||
|
||||
try:
|
||||
rmdir(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def isdir(path):
|
||||
"""os.path.isdir(path) wrapper with support for long paths on Windows.
|
||||
|
||||
|
||||
+38
-13
@@ -25,7 +25,10 @@ except ImportError:
|
||||
from repo_trace import IsTraceToStderr
|
||||
|
||||
|
||||
_TTY = sys.stderr.isatty()
|
||||
# Capture the original stderr stream. We use this exclusively for progress
|
||||
# updates to ensure we talk to the terminal even if stderr is redirected.
|
||||
_STDERR = sys.stderr
|
||||
_TTY = _STDERR.isatty()
|
||||
|
||||
# This will erase all content in the current line (wherever the cursor is).
|
||||
# It does not move the cursor, so this is usually followed by \r to move to
|
||||
@@ -100,6 +103,8 @@ class Progress:
|
||||
self._show = not delay
|
||||
self._units = units
|
||||
self._elide = elide and _TTY
|
||||
self._quiet = quiet
|
||||
self._ended = False
|
||||
|
||||
# Only show the active jobs section if we run more than one in parallel.
|
||||
self._show_jobs = False
|
||||
@@ -114,15 +119,14 @@ class Progress:
|
||||
)
|
||||
self._update_thread.daemon = True
|
||||
|
||||
# When quiet, never show any output. It's a bit hacky, but reusing the
|
||||
# existing logic that delays initial output keeps the rest of the class
|
||||
# clean. Basically we set the start time to years in the future.
|
||||
if quiet:
|
||||
self._show = False
|
||||
self._start += 2**32
|
||||
elif show_elapsed:
|
||||
if not quiet and show_elapsed:
|
||||
self._update_thread.start()
|
||||
|
||||
def update_total(self, new_total):
|
||||
"""Updates the total if the new total is larger."""
|
||||
if new_total > self._total:
|
||||
self._total = new_total
|
||||
|
||||
def _update_loop(self):
|
||||
while True:
|
||||
self.update(inc=0)
|
||||
@@ -132,11 +136,11 @@ class Progress:
|
||||
def _write(self, s):
|
||||
s = "\r" + s
|
||||
if self._elide:
|
||||
col = os.get_terminal_size(sys.stderr.fileno()).columns
|
||||
col = os.get_terminal_size(_STDERR.fileno()).columns
|
||||
if len(s) > col:
|
||||
s = s[: col - 1] + ".."
|
||||
sys.stderr.write(s)
|
||||
sys.stderr.flush()
|
||||
_STDERR.write(s)
|
||||
_STDERR.flush()
|
||||
|
||||
def start(self, name):
|
||||
self._active += 1
|
||||
@@ -155,12 +159,14 @@ class Progress:
|
||||
inc: The number of items completed.
|
||||
msg: The message to display. If None, use the last message.
|
||||
"""
|
||||
if self._ended:
|
||||
return
|
||||
self._done += inc
|
||||
if msg is None:
|
||||
msg = self._last_msg
|
||||
self._last_msg = msg
|
||||
|
||||
if not _TTY or IsTraceToStderr():
|
||||
if not _TTY or IsTraceToStderr() or self._quiet:
|
||||
return
|
||||
|
||||
elapsed_sec = time.time() - self._start
|
||||
@@ -200,9 +206,28 @@ class Progress:
|
||||
)
|
||||
)
|
||||
|
||||
def display_message(self, msg):
|
||||
"""Clears the current progress line and prints a message above it.
|
||||
|
||||
The progress bar is then redrawn on the next line.
|
||||
"""
|
||||
if not _TTY or IsTraceToStderr() or self._quiet:
|
||||
return
|
||||
|
||||
# Erase the current line, print the message with a newline,
|
||||
# and then immediately redraw the progress bar on the new line.
|
||||
_STDERR.write("\r" + CSI_ERASE_LINE)
|
||||
_STDERR.write(msg + "\n")
|
||||
_STDERR.flush()
|
||||
self.update(inc=0)
|
||||
|
||||
def end(self):
|
||||
if self._ended:
|
||||
return
|
||||
self._ended = True
|
||||
|
||||
self._update_event.set()
|
||||
if not _TTY or IsTraceToStderr() or not self._show:
|
||||
if not _TTY or IsTraceToStderr() or self._quiet:
|
||||
return
|
||||
|
||||
duration = duration_str(time.time() - self._start)
|
||||
|
||||
+1434
-381
File diff suppressed because it is too large
Load Diff
+30
-2
@@ -1,4 +1,4 @@
|
||||
# Copyright 2023 The Android Open Source Project
|
||||
# Copyright (C) 2023 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -14,5 +14,33 @@
|
||||
|
||||
[tool.black]
|
||||
line-length = 80
|
||||
# NB: Keep in sync with tox.ini.
|
||||
target-version = ['py36', 'py37', 'py38', 'py39', 'py310', 'py311'] #, 'py312'
|
||||
|
||||
# Config file for the isort python module.
|
||||
# This is used to enforce import sorting standards.
|
||||
#
|
||||
# https://pycqa.github.io/isort/docs/configuration/options.html
|
||||
[tool.isort]
|
||||
# Be compatible with `black` since it also matches what we want.
|
||||
profile = 'black'
|
||||
|
||||
line_length = 80
|
||||
length_sort = false
|
||||
force_single_line = true
|
||||
lines_after_imports = 2
|
||||
from_first = false
|
||||
case_sensitive = false
|
||||
force_sort_within_sections = true
|
||||
order_by_type = false
|
||||
|
||||
# Ignore generated files.
|
||||
extend_skip_glob = '*_pb2.py'
|
||||
|
||||
# Allow importing multiple classes on a single line from these modules.
|
||||
# https://google.github.io/styleguide/pyguide#s2.2-imports
|
||||
single_line_exclusions = ['abc', 'collections.abc', 'typing']
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
markers = """
|
||||
skip_cq: Skip tests in the CQ. Should be rarely used!
|
||||
"""
|
||||
|
||||
Executable
+155
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (C) 2025 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Helper tool to check various metadata (e.g. licensing) in source files."""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
import util
|
||||
|
||||
|
||||
_FILE_HEADER_RE = re.compile(
|
||||
r"""# Copyright \(C\) 20[0-9]{2} The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2\.0 \(the "License"\);
|
||||
# you may not use this file except in compliance with the License\.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www\.apache\.org/licenses/LICENSE-2\.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License\.
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def check_license(path: Path, lines: list[str]) -> bool:
|
||||
"""Check license header."""
|
||||
# Enforce licensing on configs & scripts.
|
||||
if not (
|
||||
path.suffix in (".bash", ".cfg", ".ini", ".py", ".toml")
|
||||
or lines[0] in ("#!/bin/bash", "#!/bin/sh", "#!/usr/bin/env python3")
|
||||
):
|
||||
return True
|
||||
|
||||
# Extract the file header.
|
||||
header_lines = []
|
||||
for line in lines:
|
||||
if line.startswith("#"):
|
||||
header_lines.append(line)
|
||||
else:
|
||||
break
|
||||
if not header_lines:
|
||||
print(
|
||||
f"error: {path.relative_to(util.TOPDIR)}: "
|
||||
"missing file header (copyright+licensing)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return False
|
||||
|
||||
# Skip the shebang.
|
||||
if header_lines[0].startswith("#!"):
|
||||
header_lines.pop(0)
|
||||
|
||||
# If this file is imported into the tree, then leave it be.
|
||||
if header_lines[0] == "# DO NOT EDIT THIS FILE":
|
||||
return True
|
||||
|
||||
header = "".join(f"{x}\n" for x in header_lines)
|
||||
if not _FILE_HEADER_RE.match(header):
|
||||
print(
|
||||
f"error: {path.relative_to(util.TOPDIR)}: "
|
||||
"file header incorrectly formatted",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
"".join(f"> {x}\n" for x in header_lines), end="", file=sys.stderr
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_path(opts: argparse.Namespace, path: Path) -> bool:
|
||||
"""Check a single path."""
|
||||
try:
|
||||
data = path.read_text(encoding="utf-8")
|
||||
except FileNotFoundError:
|
||||
return True
|
||||
lines = data.splitlines()
|
||||
# NB: Use list comprehension and not a generator so we run all the checks.
|
||||
return all(
|
||||
[
|
||||
check_license(path, lines),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def check_paths(opts: argparse.Namespace, paths: list[Path]) -> bool:
|
||||
"""Check all the paths."""
|
||||
# NB: Use list comprehension and not a generator so we check all paths.
|
||||
return all([check_path(opts, x) for x in paths]) # noqa: C419
|
||||
|
||||
|
||||
def find_files(opts: argparse.Namespace) -> list[Path]:
|
||||
"""Find all the files in the source tree."""
|
||||
result = util.run(
|
||||
opts,
|
||||
["git", "ls-tree", "-r", "-z", "--name-only", "HEAD"],
|
||||
cwd=util.TOPDIR,
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
return [util.TOPDIR / x for x in result.stdout.split("\0")[:-1]]
|
||||
|
||||
|
||||
def get_parser() -> argparse.ArgumentParser:
|
||||
"""Get a CLI parser."""
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--dry-run",
|
||||
dest="dryrun",
|
||||
action="store_true",
|
||||
help="show everything that would be done",
|
||||
)
|
||||
parser.add_argument(
|
||||
"paths",
|
||||
nargs="*",
|
||||
help="the paths to scan",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
"""The main func!"""
|
||||
parser = get_parser()
|
||||
opts = parser.parse_args(argv)
|
||||
|
||||
paths = opts.paths
|
||||
if not opts.paths:
|
||||
paths = find_files(opts)
|
||||
|
||||
return 0 if check_paths(opts, paths) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
@@ -27,6 +27,9 @@ import sys
|
||||
import util
|
||||
|
||||
|
||||
assert sys.version_info >= (3, 9), "Release framework requires Python 3.9+"
|
||||
|
||||
|
||||
def sign(opts):
|
||||
"""Sign the launcher!"""
|
||||
output = ""
|
||||
@@ -82,18 +85,18 @@ Repo launcher bucket:
|
||||
gs://git-repo-downloads/
|
||||
|
||||
You should first upload it with a specific version:
|
||||
gsutil cp -a public-read {opts.launcher} gs://git-repo-downloads/repo-{version}
|
||||
gsutil cp -a public-read {opts.launcher}.asc gs://git-repo-downloads/repo-{version}.asc
|
||||
gcloud storage cp --predefined-acl=publicRead {opts.launcher} gs://git-repo-downloads/repo-{version}
|
||||
gcloud storage cp --predefined-acl=publicRead {opts.launcher}.asc gs://git-repo-downloads/repo-{version}.asc
|
||||
|
||||
Then to make it the public default:
|
||||
gsutil cp -a public-read gs://git-repo-downloads/repo-{version} gs://git-repo-downloads/repo
|
||||
gsutil cp -a public-read gs://git-repo-downloads/repo-{version}.asc gs://git-repo-downloads/repo.asc
|
||||
gcloud storage cp --predefined-acl=publicRead gs://git-repo-downloads/repo-{version} gs://git-repo-downloads/repo
|
||||
gcloud storage cp --predefined-acl=publicRead gs://git-repo-downloads/repo-{version}.asc gs://git-repo-downloads/repo.asc
|
||||
|
||||
NB: If a rollback is necessary, the GS bucket archives old versions, and may be
|
||||
accessed by specifying their unique id number.
|
||||
gsutil ls -la gs://git-repo-downloads/repo gs://git-repo-downloads/repo.asc
|
||||
gsutil cp -a public-read gs://git-repo-downloads/repo#<unique id> gs://git-repo-downloads/repo
|
||||
gsutil cp -a public-read gs://git-repo-downloads/repo.asc#<unique id> gs://git-repo-downloads/repo.asc
|
||||
gcloud storage ls --long --all-versions gs://git-repo-downloads/repo gs://git-repo-downloads/repo.asc
|
||||
gcloud storage cp --predefined-acl=publicRead gs://git-repo-downloads/repo#<unique id> gs://git-repo-downloads/repo
|
||||
gcloud storage cp --predefined-acl=publicRead gs://git-repo-downloads/repo.asc#<unique id> gs://git-repo-downloads/repo.asc
|
||||
""" # noqa: E501
|
||||
)
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ import sys
|
||||
import util
|
||||
|
||||
|
||||
assert sys.version_info >= (3, 9), "Release framework requires Python 3.9+"
|
||||
|
||||
|
||||
# We currently sign with the old DSA key as it's been around the longest.
|
||||
# We should transition to RSA by Jun 2020, and ECC by Jun 2021.
|
||||
KEYID = util.KEYID_DSA
|
||||
|
||||
@@ -24,7 +24,7 @@ from typing import List, Optional
|
||||
import urllib.request
|
||||
|
||||
|
||||
assert sys.version_info >= (3, 8), "Python 3.8+ required"
|
||||
assert sys.version_info >= (3, 9), "Release framework requires Python 3.9+"
|
||||
|
||||
|
||||
TOPDIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
@@ -27,9 +27,14 @@ import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import List
|
||||
|
||||
|
||||
TOPDIR = Path(__file__).resolve().parent.parent
|
||||
assert sys.version_info >= (3, 9), "Release framework requires Python 3.9+"
|
||||
|
||||
|
||||
THIS_FILE = Path(__file__).resolve()
|
||||
TOPDIR = THIS_FILE.parent.parent
|
||||
MANDIR = TOPDIR.joinpath("man")
|
||||
|
||||
# Load repo local modules.
|
||||
@@ -42,17 +47,36 @@ def worker(cmd, **kwargs):
|
||||
subprocess.run(cmd, **kwargs)
|
||||
|
||||
|
||||
def main(argv):
|
||||
def get_parser() -> argparse.ArgumentParser:
|
||||
"""Get argument parser."""
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.parse_args(argv)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--check",
|
||||
"--dry-run",
|
||||
action="store_const",
|
||||
const=True,
|
||||
help="Check if changes are necessary; don't actually change files",
|
||||
)
|
||||
return parser
|
||||
|
||||
if not shutil.which("help2man"):
|
||||
|
||||
def main(argv: List[str]) -> int:
|
||||
parser = get_parser()
|
||||
opts = parser.parse_args(argv)
|
||||
|
||||
help2man = ["help2man"]
|
||||
cipd_help2man = TOPDIR / ".cipd_bin/bin/help2man"
|
||||
if cipd_help2man.exists():
|
||||
help2man = [cipd_help2man]
|
||||
elif not shutil.which("help2man"):
|
||||
sys.exit("Please install help2man to continue.")
|
||||
|
||||
# Let repo know we're generating man pages so it can avoid some dynamic
|
||||
# behavior (like probing active number of CPUs). We use a weird name &
|
||||
# value to make it less likely for users to set this var themselves.
|
||||
os.environ["_REPO_GENERATE_MANPAGES_"] = " indeed! "
|
||||
os.environ["COLUMNS"] = "10000"
|
||||
|
||||
# "repo branch" is an alias for "repo branches".
|
||||
del subcmds.all_commands["branch"]
|
||||
@@ -61,7 +85,7 @@ def main(argv):
|
||||
version = RepoSourceVersion()
|
||||
cmdlist = [
|
||||
[
|
||||
"help2man",
|
||||
*help2man,
|
||||
"-N",
|
||||
"-n",
|
||||
f"repo {cmd} - manual page for repo {cmd}",
|
||||
@@ -80,7 +104,7 @@ def main(argv):
|
||||
]
|
||||
cmdlist.append(
|
||||
[
|
||||
"help2man",
|
||||
*help2man,
|
||||
"-N",
|
||||
"-n",
|
||||
"repository management tool built on top of git",
|
||||
@@ -117,6 +141,7 @@ def main(argv):
|
||||
functools.partial(worker, cwd=tempdir, check=True), cmdlist
|
||||
)
|
||||
|
||||
ret = 0
|
||||
for tmp_path in MANDIR.glob("*.1.tmp"):
|
||||
path = tmp_path.parent / tmp_path.stem
|
||||
old_data = path.read_text() if path.exists() else ""
|
||||
@@ -133,7 +158,17 @@ def main(argv):
|
||||
)
|
||||
new_data = re.sub(r'^(\.TH REPO "1" ")([^"]+)', r"\1", data, flags=re.M)
|
||||
if old_data != new_data:
|
||||
path.write_text(data)
|
||||
if opts.check:
|
||||
ret = 1
|
||||
print(
|
||||
f"{THIS_FILE.name}: {path.name}: "
|
||||
"man page needs regenerating",
|
||||
file=sys.stderr,
|
||||
)
|
||||
else:
|
||||
path.write_text(data)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def replace_regex(data):
|
||||
@@ -147,7 +182,6 @@ def replace_regex(data):
|
||||
"""
|
||||
regex = (
|
||||
(r"(It was generated by help2man) [0-9.]+", r"\g<1>."),
|
||||
(r"^\033\[[0-9;]*m([^\033]*)\033\[m", r"\g<1>"),
|
||||
(r"^\.IP\n(.*:)\n", r".SS \g<1>\n"),
|
||||
(r"^\.PP\nDescription", r".SH DETAILS"),
|
||||
)
|
||||
|
||||
+7
-10
@@ -14,8 +14,9 @@
|
||||
|
||||
"""Random utility code for release tools."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
@@ -23,8 +24,9 @@ import sys
|
||||
assert sys.version_info >= (3, 6), "This module requires Python 3.6+"
|
||||
|
||||
|
||||
TOPDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
HOMEDIR = os.path.expanduser("~")
|
||||
THIS_FILE = Path(__file__).resolve()
|
||||
TOPDIR = THIS_FILE.parent.parent
|
||||
HOMEDIR = Path("~").expanduser()
|
||||
|
||||
|
||||
# These are the release keys we sign with.
|
||||
@@ -35,12 +37,7 @@ KEYID_ECC = "E1F9040D7A3F6DAFAC897CD3D3B95DA243E48A39"
|
||||
|
||||
def cmdstr(cmd):
|
||||
"""Get a nicely quoted shell command."""
|
||||
ret = []
|
||||
for arg in cmd:
|
||||
if not re.match(r"^[a-zA-Z0-9/_.=-]+$", arg):
|
||||
arg = f'"{arg}"'
|
||||
ret.append(arg)
|
||||
return " ".join(ret)
|
||||
return " ".join(shlex.quote(x) for x in cmd)
|
||||
|
||||
|
||||
def run(opts, cmd, check=True, **kwargs):
|
||||
@@ -58,7 +55,7 @@ def run(opts, cmd, check=True, **kwargs):
|
||||
def import_release_key(opts):
|
||||
"""Import the public key of the official release repo signing key."""
|
||||
# Extract the key from our repo launcher.
|
||||
launcher = getattr(opts, "launcher", os.path.join(TOPDIR, "repo"))
|
||||
launcher = getattr(opts, "launcher", TOPDIR / "repo")
|
||||
print(f'Importing keys from "{launcher}" launcher script')
|
||||
with open(launcher, encoding="utf-8") as fp:
|
||||
data = fp.read()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (C) 2008 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -27,6 +26,7 @@ import platform
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
# These should never be newer than the main.py version since this needs to be a
|
||||
@@ -56,9 +56,14 @@ class Trace:
|
||||
trace = Trace()
|
||||
|
||||
|
||||
def cmdstr(cmd):
|
||||
"""Get a nicely quoted shell command."""
|
||||
return " ".join(shlex.quote(x) for x in cmd)
|
||||
|
||||
|
||||
def exec_command(cmd):
|
||||
"""Execute |cmd| or return None on failure."""
|
||||
trace.print(":", " ".join(cmd))
|
||||
trace.print(":", cmdstr(cmd))
|
||||
try:
|
||||
if platform.system() == "Windows":
|
||||
ret = subprocess.call(cmd)
|
||||
@@ -124,7 +129,7 @@ if not REPO_REV:
|
||||
BUG_URL = "https://issues.gerritcodereview.com/issues/new?component=1370071"
|
||||
|
||||
# increment this whenever we make important changes to this script
|
||||
VERSION = (2, 45)
|
||||
VERSION = (2, 65)
|
||||
|
||||
# increment this if the MAINTAINER_KEYS block is modified
|
||||
KEYRING_VERSION = (2, 3)
|
||||
@@ -215,11 +220,8 @@ repodir = ".repo" # name of repo's private directory
|
||||
S_repo = "repo" # special repo repository
|
||||
S_manifests = "manifests" # special manifest repository
|
||||
REPO_MAIN = S_repo + "/main.py" # main script
|
||||
GITC_CONFIG_FILE = "/gitc/.config"
|
||||
GITC_FS_ROOT_DIR = "/gitc/manifest-rw/"
|
||||
|
||||
|
||||
import collections
|
||||
import errno
|
||||
import json
|
||||
import optparse
|
||||
@@ -235,12 +237,9 @@ home_dot_repo = os.path.join(repo_config_dir, ".repoconfig")
|
||||
gpg_dir = os.path.join(home_dot_repo, "gnupg")
|
||||
|
||||
|
||||
def GetParser(gitc_init=False):
|
||||
def GetParser():
|
||||
"""Setup the CLI parser."""
|
||||
if gitc_init:
|
||||
sys.exit("repo: fatal: GITC not supported.")
|
||||
else:
|
||||
usage = "repo init [options] [-u] url"
|
||||
usage = "repo init [options] [-u] url"
|
||||
|
||||
parser = optparse.OptionParser(usage=usage)
|
||||
InitParser(parser)
|
||||
@@ -282,6 +281,12 @@ def InitParser(parser):
|
||||
metavar="REVISION",
|
||||
help="manifest branch or revision (use HEAD for default)",
|
||||
)
|
||||
group.add_option(
|
||||
"--manifest-upstream-branch",
|
||||
help="when a commit is provided to --manifest-branch, this "
|
||||
"is the name of the git ref in which the commit can be found",
|
||||
metavar="BRANCH",
|
||||
)
|
||||
group.add_option(
|
||||
"-m",
|
||||
"--manifest-name",
|
||||
@@ -374,6 +379,11 @@ def InitParser(parser):
|
||||
action="store_true",
|
||||
help="use git-worktree to manage projects",
|
||||
)
|
||||
group.add_option(
|
||||
"--use-local-gitdirs",
|
||||
action="store_true",
|
||||
help="bypass .repo/projects/ and use standard Git layout in working tree",
|
||||
)
|
||||
|
||||
# These are fundamentally different ways of structuring the checkout.
|
||||
group = parser.add_option_group("Project checkout optimizations")
|
||||
@@ -395,20 +405,20 @@ def InitParser(parser):
|
||||
"--partial-clone",
|
||||
action="store_true",
|
||||
help="perform partial clone (https://git-scm.com/"
|
||||
"docs/gitrepository-layout#_code_partialclone_code)",
|
||||
"docs/partial-clone)",
|
||||
)
|
||||
group.add_option(
|
||||
"--no-partial-clone",
|
||||
action="store_false",
|
||||
help="disable use of partial clone (https://git-scm.com/"
|
||||
"docs/gitrepository-layout#_code_partialclone_code)",
|
||||
"docs/partial-clone)",
|
||||
)
|
||||
group.add_option(
|
||||
"--partial-clone-exclude",
|
||||
action="store",
|
||||
help="exclude the specified projects (a comma-delimited "
|
||||
"project names) from partial clone (https://git-scm.com"
|
||||
"/docs/gitrepository-layout#_code_partialclone_code)",
|
||||
"/docs/partial-clone)",
|
||||
)
|
||||
group.add_option(
|
||||
"--clone-filter",
|
||||
@@ -481,16 +491,6 @@ def InitParser(parser):
|
||||
return parser
|
||||
|
||||
|
||||
# This is a poor replacement for subprocess.run until we require Python 3.6+.
|
||||
RunResult = collections.namedtuple(
|
||||
"RunResult", ("returncode", "stdout", "stderr")
|
||||
)
|
||||
|
||||
|
||||
class RunError(Exception):
|
||||
"""Error when running a command failed."""
|
||||
|
||||
|
||||
def run_command(cmd, **kwargs):
|
||||
"""Run |cmd| and return its output."""
|
||||
check = kwargs.pop("check", False)
|
||||
@@ -515,7 +515,7 @@ def run_command(cmd, **kwargs):
|
||||
# Run & package the results.
|
||||
proc = subprocess.Popen(cmd, **kwargs)
|
||||
(stdout, stderr) = proc.communicate(input=cmd_input)
|
||||
dbg = ": " + " ".join(cmd)
|
||||
dbg = ": " + cmdstr(cmd)
|
||||
if cmd_input is not None:
|
||||
dbg += " 0<|"
|
||||
if stdout == subprocess.PIPE:
|
||||
@@ -525,7 +525,9 @@ def run_command(cmd, **kwargs):
|
||||
elif stderr == subprocess.STDOUT:
|
||||
dbg += " 2>&1"
|
||||
trace.print(dbg)
|
||||
ret = RunResult(proc.returncode, decode(stdout), decode(stderr))
|
||||
ret = subprocess.CompletedProcess(
|
||||
cmd, proc.returncode, decode(stdout), decode(stderr)
|
||||
)
|
||||
|
||||
# If things failed, print useful debugging output.
|
||||
if check and ret.returncode:
|
||||
@@ -546,56 +548,13 @@ def run_command(cmd, **kwargs):
|
||||
|
||||
_print_output("stdout", ret.stdout)
|
||||
_print_output("stderr", ret.stderr)
|
||||
raise RunError(ret)
|
||||
# This will raise subprocess.CalledProcessError for us.
|
||||
ret.check_returncode()
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
_gitc_manifest_dir = None
|
||||
|
||||
|
||||
def get_gitc_manifest_dir():
|
||||
global _gitc_manifest_dir
|
||||
if _gitc_manifest_dir is None:
|
||||
_gitc_manifest_dir = ""
|
||||
try:
|
||||
with open(GITC_CONFIG_FILE) as gitc_config:
|
||||
for line in gitc_config:
|
||||
match = re.match("gitc_dir=(?P<gitc_manifest_dir>.*)", line)
|
||||
if match:
|
||||
_gitc_manifest_dir = match.group("gitc_manifest_dir")
|
||||
except OSError:
|
||||
pass
|
||||
return _gitc_manifest_dir
|
||||
|
||||
|
||||
def gitc_parse_clientdir(gitc_fs_path):
|
||||
"""Parse a path in the GITC FS and return its client name.
|
||||
|
||||
Args:
|
||||
gitc_fs_path: A subdirectory path within the GITC_FS_ROOT_DIR.
|
||||
|
||||
Returns:
|
||||
The GITC client name.
|
||||
"""
|
||||
if gitc_fs_path == GITC_FS_ROOT_DIR:
|
||||
return None
|
||||
if not gitc_fs_path.startswith(GITC_FS_ROOT_DIR):
|
||||
manifest_dir = get_gitc_manifest_dir()
|
||||
if manifest_dir == "":
|
||||
return None
|
||||
if manifest_dir[-1] != "/":
|
||||
manifest_dir += "/"
|
||||
if gitc_fs_path == manifest_dir:
|
||||
return None
|
||||
if not gitc_fs_path.startswith(manifest_dir):
|
||||
return None
|
||||
return gitc_fs_path.split(manifest_dir)[1].split("/")[0]
|
||||
return gitc_fs_path.split(GITC_FS_ROOT_DIR)[1].split("/")[0]
|
||||
|
||||
|
||||
class CloneFailure(Exception):
|
||||
|
||||
"""Indicate the remote clone of repo itself failed."""
|
||||
|
||||
|
||||
@@ -632,9 +591,9 @@ def check_repo_rev(dst, rev, repo_verify=True, quiet=False):
|
||||
return (remote_ref, rev)
|
||||
|
||||
|
||||
def _Init(args, gitc_init=False):
|
||||
def _Init(args):
|
||||
"""Installs repo by cloning it over the network."""
|
||||
parser = GetParser(gitc_init=gitc_init)
|
||||
parser = GetParser()
|
||||
opt, args = parser.parse_args(args)
|
||||
if args:
|
||||
if not opt.manifest_url:
|
||||
@@ -714,15 +673,20 @@ def run_git(*args, **kwargs):
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
except RunError:
|
||||
except subprocess.CalledProcessError:
|
||||
raise CloneFailure()
|
||||
|
||||
|
||||
# The git version info broken down into components for easy analysis.
|
||||
# Similar to Python's sys.version_info.
|
||||
GitVersion = collections.namedtuple(
|
||||
"GitVersion", ("major", "minor", "micro", "full")
|
||||
)
|
||||
class GitVersion(NamedTuple):
|
||||
"""The git version info broken down into components for easy analysis.
|
||||
|
||||
Similar to Python's sys.version_info.
|
||||
"""
|
||||
|
||||
major: int
|
||||
minor: int
|
||||
micro: int
|
||||
full: int
|
||||
|
||||
|
||||
def ParseGitVersion(ver_str=None):
|
||||
@@ -783,6 +747,28 @@ def SetGitTrace2ParentSid(env=None):
|
||||
_setenv(KEY, value, env=env)
|
||||
|
||||
|
||||
def _NormalizeGpgHomePath(path: str) -> str:
|
||||
"""Convert path for GPG on Windows if running in MSYS/MinGW.
|
||||
|
||||
If GPG is MSYS-based (common in Git Bash/Cygwin), it expects POSIX-like paths
|
||||
(e.g. /c/Users/... instead of C:\\Users\\...). Native Windows Python uses
|
||||
Windows-style paths, which MSYS GPG will incorrectly treat as relative paths.
|
||||
We use cygpath to query the active MSYS/Cygwin mount configuration and resolve
|
||||
the correct POSIX path.
|
||||
"""
|
||||
if platform.system() == "Windows":
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
["cygpath", "-u", path],
|
||||
stderr=subprocess.DEVNULL,
|
||||
universal_newlines=True,
|
||||
)
|
||||
return out.strip()
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
pass
|
||||
return path
|
||||
|
||||
|
||||
def _setenv(key, value, env=None):
|
||||
"""Set |key| in the OS environment |env| to |value|."""
|
||||
if env is None:
|
||||
@@ -888,10 +874,11 @@ def _GetRepoConfig(name):
|
||||
return None
|
||||
else:
|
||||
print(
|
||||
f"repo: error: git {' '.join(cmd)} failed:\n{ret.stderr}",
|
||||
f"repo: error: git {cmdstr(cmd)} failed:\n{ret.stderr}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise RunError()
|
||||
# This will raise subprocess.CalledProcessError for us.
|
||||
ret.check_returncode()
|
||||
|
||||
|
||||
def _InitHttp():
|
||||
@@ -1114,7 +1101,7 @@ def verify_rev(cwd, remote_ref, rev, quiet):
|
||||
print(file=sys.stderr)
|
||||
|
||||
env = os.environ.copy()
|
||||
_setenv("GNUPGHOME", gpg_dir, env)
|
||||
_setenv("GNUPGHOME", _NormalizeGpgHomePath(gpg_dir), env)
|
||||
run_git("tag", "-v", cur, cwd=cwd, env=env)
|
||||
return "%s^0" % cur
|
||||
|
||||
@@ -1158,7 +1145,7 @@ class _Options:
|
||||
def _ExpandAlias(name):
|
||||
"""Look up user registered aliases."""
|
||||
# We don't resolve aliases for existing subcommands. This matches git.
|
||||
if name in {"gitc-init", "help", "init"}:
|
||||
if name in {"help", "init"}:
|
||||
return name, []
|
||||
|
||||
alias = _GetRepoConfig(f"alias.{name}")
|
||||
@@ -1286,10 +1273,6 @@ class Requirements:
|
||||
|
||||
|
||||
def _Usage():
|
||||
gitc_usage = ""
|
||||
if get_gitc_manifest_dir():
|
||||
gitc_usage = " gitc-init Initialize a GITC Client.\n"
|
||||
|
||||
print(
|
||||
"""usage: repo COMMAND [ARGS]
|
||||
|
||||
@@ -1298,9 +1281,7 @@ repo is not yet installed. Use "repo init" to install it here.
|
||||
The most commonly used repo commands are:
|
||||
|
||||
init Install repo in the current working directory
|
||||
"""
|
||||
+ gitc_usage
|
||||
+ """ help Display detailed help on a command
|
||||
help Display detailed help on a command
|
||||
|
||||
For access to the full online help, install repo ("repo init").
|
||||
"""
|
||||
@@ -1311,8 +1292,8 @@ For access to the full online help, install repo ("repo init").
|
||||
|
||||
def _Help(args):
|
||||
if args:
|
||||
if args[0] in {"init", "gitc-init"}:
|
||||
parser = GetParser(gitc_init=args[0] == "gitc-init")
|
||||
if args[0] in {"init"}:
|
||||
parser = GetParser()
|
||||
parser.print_help()
|
||||
sys.exit(0)
|
||||
else:
|
||||
@@ -1329,10 +1310,11 @@ def _Help(args):
|
||||
|
||||
def _Version():
|
||||
"""Show version information."""
|
||||
git_version = ParseGitVersion()
|
||||
print("<repo not installed>")
|
||||
print(f"repo launcher version {'.'.join(str(x) for x in VERSION)}")
|
||||
print(f" (from {__file__})")
|
||||
print(f"git {ParseGitVersion().full}")
|
||||
print(f"git {git_version.full}" if git_version else "git not installed")
|
||||
print(f"Python {sys.version}")
|
||||
uname = platform.uname()
|
||||
print(f"OS {uname.system} {uname.release} ({uname.version})")
|
||||
@@ -1365,11 +1347,11 @@ def _RunSelf(wrapper_path):
|
||||
my_main = os.path.join(my_dir, "main.py")
|
||||
my_git = os.path.join(my_dir, ".git")
|
||||
|
||||
if os.path.isfile(my_main) and os.path.isdir(my_git):
|
||||
if os.path.isfile(my_main):
|
||||
for name in ["git_config.py", "project.py", "subcmds"]:
|
||||
if not os.path.exists(os.path.join(my_dir, name)):
|
||||
return None, None
|
||||
return my_main, my_git
|
||||
return my_main, my_git if os.path.isdir(my_git) else None
|
||||
return None, None
|
||||
|
||||
|
||||
@@ -1400,23 +1382,11 @@ def main(orig_args):
|
||||
# We run this early as we run some git commands ourselves.
|
||||
SetGitTrace2ParentSid()
|
||||
|
||||
repo_main, rel_repo_dir = None, None
|
||||
# Don't use the local repo copy, make sure to switch to the gitc client first.
|
||||
if cmd != "gitc-init":
|
||||
repo_main, rel_repo_dir = _FindRepo()
|
||||
repo_main, rel_repo_dir = _FindRepo()
|
||||
|
||||
wrapper_path = os.path.abspath(__file__)
|
||||
my_main, my_git = _RunSelf(wrapper_path)
|
||||
|
||||
cwd = os.getcwd()
|
||||
if get_gitc_manifest_dir() and cwd.startswith(get_gitc_manifest_dir()):
|
||||
print(
|
||||
"error: repo cannot be used in the GITC local manifest directory."
|
||||
"\nIf you want to work on this GITC client please rerun this "
|
||||
"command from the corresponding client under /gitc/",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if not repo_main:
|
||||
# Only expand aliases here since we'll be parsing the CLI ourselves.
|
||||
# If we had repo_main, alias expansion would happen in main.py.
|
||||
@@ -1431,11 +1401,11 @@ def main(orig_args):
|
||||
_Version()
|
||||
if not cmd:
|
||||
_NotInstalled()
|
||||
if cmd == "init" or cmd == "gitc-init":
|
||||
if cmd == "init":
|
||||
if my_git:
|
||||
_SetDefaultsTo(my_git)
|
||||
try:
|
||||
_Init(args, gitc_init=(cmd == "gitc-init"))
|
||||
_Init(args)
|
||||
except CloneFailure:
|
||||
path = os.path.join(repodir, S_repo)
|
||||
print(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2019 The Android Open Source Project
|
||||
# Copyright (C) 2019 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,18 +15,80 @@
|
||||
|
||||
"""Wrapper to run linters and pytest with the right settings."""
|
||||
|
||||
import functools
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
# NB: While tests/* support Python >=3.6 to match requirements.json for `repo`,
|
||||
# the higher level runner logic does not need to be held back.
|
||||
assert sys.version_info >= (3, 9), "Test/release framework requires Python 3.9+"
|
||||
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
|
||||
def log_cmd(cmd: str, argv: list[str]) -> None:
|
||||
"""Log a debug message to make history easier to track."""
|
||||
print("+", cmd, shlex.join(argv), file=sys.stderr)
|
||||
|
||||
|
||||
@functools.lru_cache()
|
||||
def is_ci() -> bool:
|
||||
"""Whether we're running in our CI system."""
|
||||
return os.getenv("LUCI_CQ") == "yes"
|
||||
|
||||
|
||||
def run_pytest(argv: list[str]) -> int:
|
||||
"""Returns the exit code from pytest."""
|
||||
if is_ci():
|
||||
argv = ["-m", "not skip_cq"] + argv
|
||||
|
||||
log_cmd("pytest", argv)
|
||||
return subprocess.run(
|
||||
[sys.executable, "-m", "pytest"] + argv,
|
||||
check=False,
|
||||
cwd=ROOT_DIR,
|
||||
).returncode
|
||||
|
||||
|
||||
def run_pytest_py38(argv: list[str]) -> int:
|
||||
"""Returns the exit code from pytest under Python 3.8."""
|
||||
if is_ci():
|
||||
argv = ["-m", "not skip_cq"] + argv
|
||||
|
||||
log_cmd("[vpython 3.8] pytest", argv)
|
||||
try:
|
||||
return subprocess.run(
|
||||
[
|
||||
"vpython3",
|
||||
"-vpython-spec",
|
||||
"run_tests.vpython3.8",
|
||||
"-m",
|
||||
"pytest",
|
||||
]
|
||||
+ argv,
|
||||
check=False,
|
||||
cwd=ROOT_DIR,
|
||||
).returncode
|
||||
except FileNotFoundError:
|
||||
# Skip if the user doesn't have vpython from depot_tools.
|
||||
return 0
|
||||
|
||||
|
||||
def run_black():
|
||||
"""Returns the exit code from black."""
|
||||
argv = ["--version"]
|
||||
log_cmd("black", argv)
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "black"] + argv,
|
||||
check=True,
|
||||
cwd=ROOT_DIR,
|
||||
)
|
||||
|
||||
# Black by default only matches .py files. We have to list standalone
|
||||
# scripts manually.
|
||||
extra_programs = [
|
||||
@@ -35,35 +97,105 @@ def run_black():
|
||||
"release/update-hooks",
|
||||
"release/update-manpages",
|
||||
]
|
||||
argv = ["--diff", "--check", ROOT_DIR] + extra_programs
|
||||
log_cmd("black", argv)
|
||||
return subprocess.run(
|
||||
[sys.executable, "-m", "black", "--check", ROOT_DIR] + extra_programs,
|
||||
[sys.executable, "-m", "black"] + argv,
|
||||
check=False,
|
||||
cwd=ROOT_DIR,
|
||||
).returncode
|
||||
|
||||
|
||||
def run_flake8():
|
||||
"""Returns the exit code from flake8."""
|
||||
argv = ["--version"]
|
||||
log_cmd("flake8", argv)
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "flake8"] + argv,
|
||||
check=True,
|
||||
cwd=ROOT_DIR,
|
||||
)
|
||||
|
||||
argv = [ROOT_DIR]
|
||||
log_cmd("flake8", argv)
|
||||
return subprocess.run(
|
||||
[sys.executable, "-m", "flake8", ROOT_DIR], check=False
|
||||
[sys.executable, "-m", "flake8"] + argv,
|
||||
check=False,
|
||||
cwd=ROOT_DIR,
|
||||
).returncode
|
||||
|
||||
|
||||
def run_isort():
|
||||
"""Returns the exit code from isort."""
|
||||
argv = ["--version-number"]
|
||||
log_cmd("isort", argv)
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "isort"] + argv,
|
||||
check=True,
|
||||
cwd=ROOT_DIR,
|
||||
)
|
||||
|
||||
argv = ["--check", ROOT_DIR]
|
||||
log_cmd("isort", argv)
|
||||
return subprocess.run(
|
||||
[sys.executable, "-m", "isort", "--check", ROOT_DIR], check=False
|
||||
[sys.executable, "-m", "isort"] + argv,
|
||||
check=False,
|
||||
cwd=ROOT_DIR,
|
||||
).returncode
|
||||
|
||||
|
||||
def run_check_metadata():
|
||||
"""Returns the exit code from check-metadata."""
|
||||
argv = []
|
||||
log_cmd("release/check-metadata.py", argv)
|
||||
return subprocess.run(
|
||||
[sys.executable, "release/check-metadata.py"] + argv,
|
||||
check=False,
|
||||
cwd=ROOT_DIR,
|
||||
).returncode
|
||||
|
||||
|
||||
def run_update_manpages() -> int:
|
||||
"""Returns the exit code from release/update-manpages."""
|
||||
argv = ["--check"]
|
||||
log_cmd("release/update-manpages", argv)
|
||||
return subprocess.run(
|
||||
[sys.executable, "release/update-manpages"] + argv,
|
||||
check=False,
|
||||
cwd=ROOT_DIR,
|
||||
).returncode
|
||||
|
||||
|
||||
def cipd_bootstrap() -> None:
|
||||
"""Install packages via cipd if available."""
|
||||
argv = ["ensure", "-root", ".cipd_bin", "-ensure-file", "cipd_manifest.txt"]
|
||||
log_cmd("cipd", argv)
|
||||
try:
|
||||
subprocess.run(
|
||||
["cipd"] + argv,
|
||||
check=True,
|
||||
cwd=ROOT_DIR,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
# Skip if the user doesn't have cipd from depot_tools.
|
||||
return
|
||||
|
||||
|
||||
def main(argv):
|
||||
"""The main entry."""
|
||||
cipd_bootstrap()
|
||||
checks = (
|
||||
lambda: pytest.main(argv),
|
||||
functools.partial(run_pytest, argv),
|
||||
functools.partial(run_pytest_py38, argv),
|
||||
run_black,
|
||||
run_flake8,
|
||||
run_isort,
|
||||
run_check_metadata,
|
||||
run_update_manpages,
|
||||
)
|
||||
return 0 if all(not c() for c in checks) else 1
|
||||
# Run all the tests all the time to get full feedback. Don't exit on the
|
||||
# first error as that makes it more difficult to iterate in the CQ.
|
||||
return 1 if sum(c() for c in checks) else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+31
-26
@@ -5,97 +5,92 @@
|
||||
# List of available wheels:
|
||||
# https://chromium.googlesource.com/infra/infra/+/main/infra/tools/dockerbuild/wheels.md
|
||||
|
||||
python_version: "3.8"
|
||||
python_version: "3.11"
|
||||
|
||||
wheel: <
|
||||
name: "infra/python/wheels/pytest-py3"
|
||||
version: "version:6.2.2"
|
||||
version: "version:8.3.4"
|
||||
>
|
||||
|
||||
# Required by pytest==6.2.2
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/py-py2_py3"
|
||||
version: "version:1.10.0"
|
||||
version: "version:1.11.0"
|
||||
>
|
||||
|
||||
# Required by pytest==6.2.2
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/iniconfig-py3"
|
||||
version: "version:1.1.1"
|
||||
>
|
||||
|
||||
# Required by pytest==6.2.2
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/packaging-py3"
|
||||
version: "version:23.0"
|
||||
>
|
||||
|
||||
# Required by pytest==6.2.2
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/pluggy-py3"
|
||||
version: "version:0.13.1"
|
||||
version: "version:1.5.0"
|
||||
>
|
||||
|
||||
# Required by pytest==6.2.2
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/toml-py3"
|
||||
version: "version:0.10.1"
|
||||
>
|
||||
|
||||
# Required by pytest==6.2.2
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/pyparsing-py3"
|
||||
version: "version:3.0.7"
|
||||
>
|
||||
|
||||
# Required by pytest==6.2.2
|
||||
# Required by pytest==8.3.4 and flake8-bugbear==24.12.12
|
||||
wheel: <
|
||||
name: "infra/python/wheels/attrs-py2_py3"
|
||||
version: "version:21.4.0"
|
||||
>
|
||||
|
||||
# Required by packaging==16.8
|
||||
wheel: <
|
||||
name: "infra/python/wheels/six-py2_py3"
|
||||
version: "version:1.16.0"
|
||||
name: "infra/python/wheels/attrs-py3"
|
||||
version: "version:24.2.0"
|
||||
>
|
||||
|
||||
# NB: Keep in sync with constraints.txt.
|
||||
wheel: <
|
||||
name: "infra/python/wheels/black-py3"
|
||||
version: "version:23.1.0"
|
||||
version: "version:25.1.0"
|
||||
>
|
||||
|
||||
# Required by black==23.1.0
|
||||
# Required by black==25.1.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/mypy-extensions-py3"
|
||||
version: "version:0.4.3"
|
||||
>
|
||||
|
||||
# Required by black==23.1.0
|
||||
# Required by black==25.1.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/tomli-py3"
|
||||
version: "version:2.0.1"
|
||||
>
|
||||
|
||||
# Required by black==23.1.0
|
||||
# Required by black==25.1.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/platformdirs-py3"
|
||||
version: "version:2.5.2"
|
||||
>
|
||||
|
||||
# Required by black==23.1.0
|
||||
# Required by black==25.1.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/pathspec-py3"
|
||||
version: "version:0.9.0"
|
||||
>
|
||||
|
||||
# Required by black==23.1.0
|
||||
# Required by black==25.1.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/typing-extensions-py3"
|
||||
version: "version:4.3.0"
|
||||
>
|
||||
|
||||
# Required by black==23.1.0
|
||||
# Required by black==25.1.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/click-py3"
|
||||
version: "version:8.0.3"
|
||||
@@ -124,6 +119,16 @@ wheel: <
|
||||
version: "version:2.10.0"
|
||||
>
|
||||
|
||||
wheel: <
|
||||
name: "infra/python/wheels/flake8-bugbear-py3"
|
||||
version: "version:24.12.12"
|
||||
>
|
||||
|
||||
wheel: <
|
||||
name: "infra/python/wheels/flake8-comprehensions-py3"
|
||||
version: "version:3.16.0"
|
||||
>
|
||||
|
||||
wheel: <
|
||||
name: "infra/python/wheels/isort-py3"
|
||||
version: "version:5.10.1"
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# This is a vpython "spec" file.
|
||||
#
|
||||
# Read more about `vpython` and how to modify this file here:
|
||||
# https://chromium.googlesource.com/infra/infra/+/main/doc/users/vpython.md
|
||||
# List of available wheels:
|
||||
# https://chromium.googlesource.com/infra/infra/+/main/infra/tools/dockerbuild/wheels.md
|
||||
|
||||
python_version: "3.8"
|
||||
|
||||
wheel: <
|
||||
name: "infra/python/wheels/pytest-py3"
|
||||
version: "version:8.3.4"
|
||||
>
|
||||
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/py-py2_py3"
|
||||
version: "version:1.11.0"
|
||||
>
|
||||
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/iniconfig-py3"
|
||||
version: "version:1.1.1"
|
||||
>
|
||||
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/packaging-py3"
|
||||
version: "version:23.0"
|
||||
>
|
||||
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/pluggy-py3"
|
||||
version: "version:1.5.0"
|
||||
>
|
||||
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/toml-py3"
|
||||
version: "version:0.10.1"
|
||||
>
|
||||
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/tomli-py3"
|
||||
version: "version:2.1.0"
|
||||
>
|
||||
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/pyparsing-py3"
|
||||
version: "version:3.0.7"
|
||||
>
|
||||
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/attrs-py2_py3"
|
||||
version: "version:21.4.0"
|
||||
>
|
||||
|
||||
# Required by pytest==8.3.4
|
||||
wheel: <
|
||||
name: "infra/python/wheels/exceptiongroup-py3"
|
||||
version: "version:1.1.2"
|
||||
>
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2019 The Android Open Source Project
|
||||
# Copyright (C) 2019 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the 'License");
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
|
||||
@@ -52,12 +52,12 @@ def _parse_ssh_version(ver_str=None):
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def version():
|
||||
"""return ssh version as a tuple"""
|
||||
"""Return ssh version as a tuple.
|
||||
|
||||
If ssh is not available, a FileNotFoundError will be raised.
|
||||
"""
|
||||
try:
|
||||
return _parse_ssh_version()
|
||||
except FileNotFoundError:
|
||||
print("fatal: ssh not installed", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(
|
||||
"fatal: unable to detect ssh version"
|
||||
@@ -102,9 +102,18 @@ class ProxyManager:
|
||||
self._clients = manager.list()
|
||||
# Path to directory for holding master sockets.
|
||||
self._sock_path = None
|
||||
# See if ssh is usable.
|
||||
self._ssh_installed = False
|
||||
|
||||
def __enter__(self):
|
||||
"""Enter a new context."""
|
||||
# Check which version of ssh is available.
|
||||
try:
|
||||
version()
|
||||
self._ssh_installed = True
|
||||
except FileNotFoundError:
|
||||
self._ssh_installed = False
|
||||
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
@@ -140,7 +149,7 @@ class ProxyManager:
|
||||
while True:
|
||||
try:
|
||||
procs.pop(0)
|
||||
except: # noqa: E722
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
def close(self):
|
||||
@@ -282,6 +291,9 @@ class ProxyManager:
|
||||
|
||||
def preconnect(self, url):
|
||||
"""If |uri| will create a ssh connection, setup the ssh master for it.""" # noqa: E501
|
||||
if not self._ssh_installed:
|
||||
return False
|
||||
|
||||
m = URI_ALL.match(url)
|
||||
if m:
|
||||
scheme = m.group(1)
|
||||
@@ -306,6 +318,9 @@ class ProxyManager:
|
||||
|
||||
This has all the master sockets so clients can talk to them.
|
||||
"""
|
||||
if not self._ssh_installed:
|
||||
return None
|
||||
|
||||
if self._sock_path is None:
|
||||
if not create:
|
||||
return None
|
||||
|
||||
+20
-17
@@ -20,7 +20,7 @@ from command import Command
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
from error import RepoError
|
||||
from error import RepoExitError
|
||||
from git_command import git
|
||||
from git_command import IsValidBranchName
|
||||
from progress import Progress
|
||||
from repo_logging import RepoLogger
|
||||
|
||||
@@ -48,7 +48,6 @@ It is equivalent to "git branch -D <branchname>".
|
||||
def _Options(self, p):
|
||||
p.add_option(
|
||||
"--all",
|
||||
dest="all",
|
||||
action="store_true",
|
||||
help="delete all branches in all projects",
|
||||
)
|
||||
@@ -59,9 +58,7 @@ It is equivalent to "git branch -D <branchname>".
|
||||
|
||||
if not opt.all:
|
||||
branches = args[0].split()
|
||||
invalid_branches = [
|
||||
x for x in branches if not git.check_ref_format(f"heads/{x}")
|
||||
]
|
||||
invalid_branches = [x for x in branches if not IsValidBranchName(x)]
|
||||
|
||||
if invalid_branches:
|
||||
self.OptionParser.error(
|
||||
@@ -70,8 +67,10 @@ It is equivalent to "git branch -D <branchname>".
|
||||
else:
|
||||
args.insert(0, "'All local branches'")
|
||||
|
||||
def _ExecuteOne(self, all_branches, nb, project):
|
||||
@classmethod
|
||||
def _ExecuteOne(cls, all_branches, nb, project_idx):
|
||||
"""Abandon one project."""
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
if all_branches:
|
||||
branches = project.GetBranches()
|
||||
else:
|
||||
@@ -89,7 +88,7 @@ It is equivalent to "git branch -D <branchname>".
|
||||
if status is not None:
|
||||
ret[name] = status
|
||||
|
||||
return (ret, project, errors)
|
||||
return (ret, project_idx, errors)
|
||||
|
||||
def Execute(self, opt, args):
|
||||
nb = args[0].split()
|
||||
@@ -102,7 +101,8 @@ It is equivalent to "git branch -D <branchname>".
|
||||
_RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
|
||||
|
||||
def _ProcessResults(_pool, pm, states):
|
||||
for results, project, errors in states:
|
||||
for results, project_idx, errors in states:
|
||||
project = all_projects[project_idx]
|
||||
for branch, status in results.items():
|
||||
if status:
|
||||
success[branch].append(project)
|
||||
@@ -111,15 +111,18 @@ It is equivalent to "git branch -D <branchname>".
|
||||
aggregate_errors.extend(errors)
|
||||
pm.update(msg="")
|
||||
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._ExecuteOne, opt.all, nb),
|
||||
all_projects,
|
||||
callback=_ProcessResults,
|
||||
output=Progress(
|
||||
f"Abandon {nb}", len(all_projects), quiet=opt.quiet
|
||||
),
|
||||
)
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = all_projects
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._ExecuteOne, opt.all, nb),
|
||||
range(len(all_projects)),
|
||||
callback=_ProcessResults,
|
||||
output=Progress(
|
||||
f"Abandon {nb}", len(all_projects), quiet=opt.quiet
|
||||
),
|
||||
chunksize=1,
|
||||
)
|
||||
|
||||
width = max(
|
||||
itertools.chain(
|
||||
|
||||
+30
-24
@@ -98,6 +98,22 @@ is shown, then the branch appears in all projects.
|
||||
"""
|
||||
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
|
||||
|
||||
@classmethod
|
||||
def _ExpandProjectToBranches(cls, project_idx):
|
||||
"""Expands a project into a list of branch names & associated info.
|
||||
|
||||
Args:
|
||||
project_idx: project.Project index
|
||||
|
||||
Returns:
|
||||
List[Tuple[str, git_config.Branch, int]]
|
||||
"""
|
||||
branches = []
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
for name, b in project.GetBranches().items():
|
||||
branches.append((name, b, project_idx))
|
||||
return branches
|
||||
|
||||
def Execute(self, opt, args):
|
||||
projects = self.GetProjects(
|
||||
args, all_manifests=not opt.this_manifest_only
|
||||
@@ -107,17 +123,20 @@ is shown, then the branch appears in all projects.
|
||||
project_cnt = len(projects)
|
||||
|
||||
def _ProcessResults(_pool, _output, results):
|
||||
for name, b in itertools.chain.from_iterable(results):
|
||||
for name, b, project_idx in itertools.chain.from_iterable(results):
|
||||
b.project = projects[project_idx]
|
||||
if name not in all_branches:
|
||||
all_branches[name] = BranchInfo(name)
|
||||
all_branches[name].add(b)
|
||||
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
expand_project_to_branches,
|
||||
projects,
|
||||
callback=_ProcessResults,
|
||||
)
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = projects
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
self._ExpandProjectToBranches,
|
||||
range(len(projects)),
|
||||
callback=_ProcessResults,
|
||||
)
|
||||
|
||||
names = sorted(all_branches)
|
||||
|
||||
@@ -148,7 +167,10 @@ is shown, then the branch appears in all projects.
|
||||
else:
|
||||
published = " "
|
||||
|
||||
hdr("%c%c %-*s" % (current, published, width, name))
|
||||
# A branch name can contain a percent sign, so we need to escape it.
|
||||
# Escape after f-string formatting to properly account for leading
|
||||
# spaces.
|
||||
hdr(f"{current}{published} {name:{width}}".replace("%", "%%"))
|
||||
out.write(" |")
|
||||
|
||||
_RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
|
||||
@@ -191,19 +213,3 @@ is shown, then the branch appears in all projects.
|
||||
else:
|
||||
out.write(" in all projects")
|
||||
out.nl()
|
||||
|
||||
|
||||
def expand_project_to_branches(project):
|
||||
"""Expands a project into a list of branch names & associated information.
|
||||
|
||||
Args:
|
||||
project: project.Project
|
||||
|
||||
Returns:
|
||||
List[Tuple[str, git_config.Branch]]
|
||||
"""
|
||||
branches = []
|
||||
for name, b in project.GetBranches().items():
|
||||
b.project = project
|
||||
branches.append((name, b))
|
||||
return branches
|
||||
|
||||
+19
-15
@@ -20,7 +20,6 @@ from command import DEFAULT_LOCAL_JOBS
|
||||
from error import GitError
|
||||
from error import RepoExitError
|
||||
from progress import Progress
|
||||
from project import Project
|
||||
from repo_logging import RepoLogger
|
||||
|
||||
|
||||
@@ -30,7 +29,7 @@ logger = RepoLogger(__file__)
|
||||
class CheckoutBranchResult(NamedTuple):
|
||||
# Whether the Project is on the branch (i.e. branch exists and no errors)
|
||||
result: bool
|
||||
project: Project
|
||||
project_idx: int
|
||||
error: Exception
|
||||
|
||||
|
||||
@@ -62,15 +61,17 @@ The command is equivalent to:
|
||||
if not args:
|
||||
self.Usage()
|
||||
|
||||
def _ExecuteOne(self, nb, project):
|
||||
@classmethod
|
||||
def _ExecuteOne(cls, nb, project_idx):
|
||||
"""Checkout one project."""
|
||||
error = None
|
||||
result = None
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
try:
|
||||
result = project.CheckoutBranch(nb)
|
||||
except GitError as e:
|
||||
error = e
|
||||
return CheckoutBranchResult(result, project, error)
|
||||
return CheckoutBranchResult(result, project_idx, error)
|
||||
|
||||
def Execute(self, opt, args):
|
||||
nb = args[0]
|
||||
@@ -83,22 +84,25 @@ The command is equivalent to:
|
||||
|
||||
def _ProcessResults(_pool, pm, results):
|
||||
for result in results:
|
||||
project = all_projects[result.project_idx]
|
||||
if result.error is not None:
|
||||
err.append(result.error)
|
||||
err_projects.append(result.project)
|
||||
err_projects.append(project)
|
||||
elif result.result:
|
||||
success.append(result.project)
|
||||
success.append(project)
|
||||
pm.update(msg="")
|
||||
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._ExecuteOne, nb),
|
||||
all_projects,
|
||||
callback=_ProcessResults,
|
||||
output=Progress(
|
||||
f"Checkout {nb}", len(all_projects), quiet=opt.quiet
|
||||
),
|
||||
)
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = all_projects
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._ExecuteOne, nb),
|
||||
range(len(all_projects)),
|
||||
callback=_ProcessResults,
|
||||
output=Progress(
|
||||
f"Checkout {nb}", len(all_projects), quiet=opt.quiet
|
||||
),
|
||||
)
|
||||
|
||||
if err_projects:
|
||||
for p in err_projects:
|
||||
|
||||
+45
-30
@@ -14,6 +14,7 @@
|
||||
|
||||
import re
|
||||
import sys
|
||||
from typing import Tuple
|
||||
|
||||
from command import Command
|
||||
from error import GitError
|
||||
@@ -43,36 +44,8 @@ change id will be added.
|
||||
|
||||
def Execute(self, opt, args):
|
||||
reference = args[0]
|
||||
|
||||
p = GitCommand(
|
||||
None,
|
||||
["rev-parse", "--verify", reference],
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
verify_command=True,
|
||||
)
|
||||
try:
|
||||
p.Wait()
|
||||
except GitError:
|
||||
logger.error(p.stderr)
|
||||
raise
|
||||
|
||||
sha1 = p.stdout.strip()
|
||||
|
||||
p = GitCommand(
|
||||
None,
|
||||
["cat-file", "commit", sha1],
|
||||
capture_stdout=True,
|
||||
verify_command=True,
|
||||
)
|
||||
|
||||
try:
|
||||
p.Wait()
|
||||
except GitError:
|
||||
logger.error("error: Failed to retrieve old commit message")
|
||||
raise
|
||||
|
||||
old_msg = self._StripHeader(p.stdout)
|
||||
sha1, commit = self._ResolveReference(reference)
|
||||
old_msg = self._StripHeader(commit)
|
||||
|
||||
p = GitCommand(
|
||||
None,
|
||||
@@ -117,6 +90,48 @@ change id will be added.
|
||||
logger.error("error: Failed to update commit message")
|
||||
raise
|
||||
|
||||
def _ResolveReference(self, reference: str) -> Tuple[str, str]:
|
||||
"""Resolve a commit and read it through one cat-file batch request."""
|
||||
expression = f"{reference}^{{commit}}"
|
||||
p = GitCommand(
|
||||
None,
|
||||
["cat-file", "--batch"],
|
||||
input=expression + "\n",
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
verify_command=True,
|
||||
)
|
||||
try:
|
||||
p.Wait()
|
||||
header, separator, output = p.stdout.partition("\n")
|
||||
if not separator:
|
||||
raise ValueError("missing cat-file header")
|
||||
if header.endswith(" missing") or header.endswith(" ambiguous"):
|
||||
raise GitError(f"commit {reference} not found")
|
||||
|
||||
parts = header.split(" ", 2)
|
||||
if len(parts) != 3 or parts[1] != "commit":
|
||||
raise ValueError(
|
||||
f"unexpected object type {parts[1]!r}"
|
||||
if len(parts) >= 2
|
||||
else "invalid header"
|
||||
)
|
||||
sha1, _object_type, _size = parts
|
||||
|
||||
if not output.endswith("\n"):
|
||||
raise ValueError("truncated cat-file object")
|
||||
|
||||
commit = output[:-1]
|
||||
except (GitError, ValueError) as e:
|
||||
logger.error(
|
||||
"error: Failed to resolve or read commit %s", reference
|
||||
)
|
||||
if isinstance(e, GitError):
|
||||
raise
|
||||
raise GitError(str(e)) from e
|
||||
|
||||
return sha1, commit
|
||||
|
||||
def _IsChangeId(self, line):
|
||||
return CHANGE_ID_RE.match(line)
|
||||
|
||||
|
||||
+16
-12
@@ -35,12 +35,12 @@ to the Unix 'patch' command.
|
||||
p.add_option(
|
||||
"-u",
|
||||
"--absolute",
|
||||
dest="absolute",
|
||||
action="store_true",
|
||||
help="paths are relative to the repository root",
|
||||
)
|
||||
|
||||
def _ExecuteOne(self, absolute, local, project):
|
||||
@classmethod
|
||||
def _ExecuteOne(cls, absolute, local, project_idx):
|
||||
"""Obtains the diff for a specific project.
|
||||
|
||||
Args:
|
||||
@@ -48,12 +48,13 @@ to the Unix 'patch' command.
|
||||
local: a boolean, if True, the path is relative to the local
|
||||
(sub)manifest. If false, the path is relative to the outermost
|
||||
manifest.
|
||||
project: Project to get status of.
|
||||
project_idx: Project index to get status of.
|
||||
|
||||
Returns:
|
||||
The status of the project.
|
||||
"""
|
||||
buf = io.StringIO()
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
ret = project.PrintWorkTreeDiff(absolute, output_redir=buf, local=local)
|
||||
return (ret, buf.getvalue())
|
||||
|
||||
@@ -71,12 +72,15 @@ to the Unix 'patch' command.
|
||||
ret = 1
|
||||
return ret
|
||||
|
||||
return self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(
|
||||
self._ExecuteOne, opt.absolute, opt.this_manifest_only
|
||||
),
|
||||
all_projects,
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
)
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = all_projects
|
||||
return self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(
|
||||
self._ExecuteOne, opt.absolute, opt.this_manifest_only
|
||||
),
|
||||
range(len(all_projects)),
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
chunksize=1,
|
||||
)
|
||||
|
||||
@@ -67,7 +67,9 @@ synced and their revisions won't be found.
|
||||
|
||||
def _Options(self, p):
|
||||
p.add_option(
|
||||
"--raw", dest="raw", action="store_true", help="display raw diff"
|
||||
"--raw",
|
||||
action="store_true",
|
||||
help="display raw diff",
|
||||
)
|
||||
p.add_option(
|
||||
"--no-color",
|
||||
@@ -78,7 +80,6 @@ synced and their revisions won't be found.
|
||||
)
|
||||
p.add_option(
|
||||
"--pretty-format",
|
||||
dest="pretty_format",
|
||||
action="store",
|
||||
metavar="<FORMAT>",
|
||||
help="print the log using a custom git pretty format string",
|
||||
@@ -233,9 +234,9 @@ synced and their revisions won't be found.
|
||||
)
|
||||
self.printRevision = self.out.nofmt_printer("revision", fg="yellow")
|
||||
else:
|
||||
self.printProject = (
|
||||
self.printAdded
|
||||
) = self.printRemoved = self.printRevision = self.printText
|
||||
self.printProject = self.printAdded = self.printRemoved = (
|
||||
self.printRevision
|
||||
) = self.printText
|
||||
|
||||
manifest1 = RepoClient(self.repodir)
|
||||
manifest1.Override(args[0], load_local_manifests=False)
|
||||
|
||||
@@ -60,7 +60,6 @@ If no project is specified try to use current directory as a project.
|
||||
p.add_option(
|
||||
"-r",
|
||||
"--revert",
|
||||
dest="revert",
|
||||
action="store_true",
|
||||
help="revert instead of checkout",
|
||||
)
|
||||
|
||||
+63
-79
@@ -15,7 +15,6 @@
|
||||
import errno
|
||||
import functools
|
||||
import io
|
||||
import multiprocessing
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
@@ -26,8 +25,6 @@ from color import Coloring
|
||||
from command import Command
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
from command import MirrorSafeCommand
|
||||
from command import WORKER_BATCH_SIZE
|
||||
from error import ManifestInvalidRevisionError
|
||||
from repo_logging import RepoLogger
|
||||
|
||||
|
||||
@@ -104,6 +101,14 @@ revision to a locally executed git command, use REPO_LREV.
|
||||
REPO_RREV is the name of the revision from the manifest, exactly
|
||||
as written in the manifest.
|
||||
|
||||
REPO_UPSTREAM is the name of the upstream branch as specified in the
|
||||
manifest.
|
||||
|
||||
REPO_DEST_BRANCH is the name of the destination branch for code review,
|
||||
as specified in the manifest.
|
||||
|
||||
REPO_PROJECT_FETCH_URL is the full resolved fetch URL for the project.
|
||||
|
||||
REPO_COUNT is the total number of projects being iterated.
|
||||
|
||||
REPO_I is the current (1-based) iteration count. Can be used in
|
||||
@@ -143,7 +148,6 @@ without iterating through the remaining projects.
|
||||
p.add_option(
|
||||
"-r",
|
||||
"--regex",
|
||||
dest="regex",
|
||||
action="store_true",
|
||||
help="execute the command only on projects matching regex or "
|
||||
"wildcard expression",
|
||||
@@ -151,7 +155,6 @@ without iterating through the remaining projects.
|
||||
p.add_option(
|
||||
"-i",
|
||||
"--inverse-regex",
|
||||
dest="inverse_regex",
|
||||
action="store_true",
|
||||
help="execute the command only on projects not matching regex or "
|
||||
"wildcard expression",
|
||||
@@ -159,22 +162,20 @@ without iterating through the remaining projects.
|
||||
p.add_option(
|
||||
"-g",
|
||||
"--groups",
|
||||
dest="groups",
|
||||
help="execute the command only on projects matching the specified "
|
||||
"groups",
|
||||
)
|
||||
p.add_option(
|
||||
"-c",
|
||||
"--command",
|
||||
help="command (and arguments) to execute",
|
||||
dest="command",
|
||||
help="command (and arguments) to execute",
|
||||
action="callback",
|
||||
callback=self._cmd_option,
|
||||
)
|
||||
p.add_option(
|
||||
"-e",
|
||||
"--abort-on-errors",
|
||||
dest="abort_on_errors",
|
||||
action="store_true",
|
||||
help="abort if a command exits unsuccessfully",
|
||||
)
|
||||
@@ -241,15 +242,6 @@ without iterating through the remaining projects.
|
||||
cmd.insert(cmd.index(cn) + 1, "--color")
|
||||
|
||||
mirror = self.manifest.IsMirror
|
||||
rc = 0
|
||||
|
||||
smart_sync_manifest_name = "smart_sync_override.xml"
|
||||
smart_sync_manifest_path = os.path.join(
|
||||
self.manifest.manifestProject.worktree, smart_sync_manifest_name
|
||||
)
|
||||
|
||||
if os.path.isfile(smart_sync_manifest_path):
|
||||
self.manifest.Override(smart_sync_manifest_path)
|
||||
|
||||
if opt.regex:
|
||||
projects = self.FindProjects(args, all_manifests=all_trees)
|
||||
@@ -264,35 +256,44 @@ without iterating through the remaining projects.
|
||||
|
||||
os.environ["REPO_COUNT"] = str(len(projects))
|
||||
|
||||
def _ProcessResults(_pool, _output, results):
|
||||
rc = 0
|
||||
first = True
|
||||
for r, output in results:
|
||||
if output:
|
||||
if first:
|
||||
first = False
|
||||
elif opt.project_header:
|
||||
print()
|
||||
# To simplify the DoWorkWrapper, take care of automatic
|
||||
# newlines.
|
||||
end = "\n"
|
||||
if output[-1] == "\n":
|
||||
end = ""
|
||||
print(output, end=end)
|
||||
rc = rc or r
|
||||
if r != 0 and opt.abort_on_errors:
|
||||
raise Exception("Aborting due to previous error")
|
||||
return rc
|
||||
|
||||
try:
|
||||
config = self.manifest.manifestProject.config
|
||||
with multiprocessing.Pool(opt.jobs, InitWorker) as pool:
|
||||
results_it = pool.imap(
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = projects
|
||||
rc = self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(
|
||||
DoWorkWrapper, mirror, opt, cmd, shell, config
|
||||
self.DoWorkWrapper, mirror, opt, cmd, shell, config
|
||||
),
|
||||
enumerate(projects),
|
||||
chunksize=WORKER_BATCH_SIZE,
|
||||
range(len(projects)),
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
initializer=self.InitWorker,
|
||||
chunksize=1,
|
||||
)
|
||||
first = True
|
||||
for r, output in results_it:
|
||||
if output:
|
||||
if first:
|
||||
first = False
|
||||
elif opt.project_header:
|
||||
print()
|
||||
# To simplify the DoWorkWrapper, take care of automatic
|
||||
# newlines.
|
||||
end = "\n"
|
||||
if output[-1] == "\n":
|
||||
end = ""
|
||||
print(output, end=end)
|
||||
rc = rc or r
|
||||
if r != 0 and opt.abort_on_errors:
|
||||
raise Exception("Aborting due to previous error")
|
||||
except (KeyboardInterrupt, WorkerKeyboardInterrupt):
|
||||
# Catch KeyboardInterrupt raised inside and outside of workers
|
||||
rc = rc or errno.EINTR
|
||||
rc = errno.EINTR
|
||||
except Exception as e:
|
||||
# Catch any other exceptions raised
|
||||
logger.error(
|
||||
@@ -300,35 +301,35 @@ without iterating through the remaining projects.
|
||||
type(e).__name__,
|
||||
e,
|
||||
)
|
||||
rc = rc or getattr(e, "errno", 1)
|
||||
rc = getattr(e, "errno", 1)
|
||||
if rc != 0:
|
||||
sys.exit(rc)
|
||||
|
||||
@classmethod
|
||||
def InitWorker(cls):
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
@classmethod
|
||||
def DoWorkWrapper(cls, mirror, opt, cmd, shell, config, project_idx):
|
||||
"""A wrapper around the DoWork() method.
|
||||
|
||||
Catch the KeyboardInterrupt exceptions here and re-raise them as a
|
||||
different, ``Exception``-based exception to stop it flooding the console
|
||||
with stacktraces and making the parent hang indefinitely.
|
||||
|
||||
"""
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
try:
|
||||
return DoWork(project, mirror, opt, cmd, shell, project_idx, config)
|
||||
except KeyboardInterrupt:
|
||||
print("%s: Worker interrupted" % project.name)
|
||||
raise WorkerKeyboardInterrupt()
|
||||
|
||||
|
||||
class WorkerKeyboardInterrupt(Exception):
|
||||
"""Keyboard interrupt exception for worker processes."""
|
||||
|
||||
|
||||
def InitWorker():
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
|
||||
def DoWorkWrapper(mirror, opt, cmd, shell, config, args):
|
||||
"""A wrapper around the DoWork() method.
|
||||
|
||||
Catch the KeyboardInterrupt exceptions here and re-raise them as a
|
||||
different, ``Exception``-based exception to stop it flooding the console
|
||||
with stacktraces and making the parent hang indefinitely.
|
||||
|
||||
"""
|
||||
cnt, project = args
|
||||
try:
|
||||
return DoWork(project, mirror, opt, cmd, shell, cnt, config)
|
||||
except KeyboardInterrupt:
|
||||
print("%s: Worker interrupted" % project.name)
|
||||
raise WorkerKeyboardInterrupt()
|
||||
|
||||
|
||||
def DoWork(project, mirror, opt, cmd, shell, cnt, config):
|
||||
env = os.environ.copy()
|
||||
|
||||
@@ -337,25 +338,8 @@ def DoWork(project, mirror, opt, cmd, shell, cnt, config):
|
||||
val = ""
|
||||
env[name] = val
|
||||
|
||||
setenv("REPO_PROJECT", project.name)
|
||||
setenv("REPO_OUTERPATH", project.manifest.path_prefix)
|
||||
setenv("REPO_INNERPATH", project.relpath)
|
||||
setenv("REPO_PATH", project.RelPath(local=opt.this_manifest_only))
|
||||
setenv("REPO_REMOTE", project.remote.name)
|
||||
try:
|
||||
# If we aren't in a fully synced state and we don't have the ref the
|
||||
# manifest wants, then this will fail. Ignore it for the purposes of
|
||||
# this code.
|
||||
lrev = "" if mirror else project.GetRevisionId()
|
||||
except ManifestInvalidRevisionError:
|
||||
lrev = ""
|
||||
setenv("REPO_LREV", lrev)
|
||||
setenv("REPO_RREV", project.revisionExpr)
|
||||
setenv("REPO_UPSTREAM", project.upstream)
|
||||
setenv("REPO_DEST_BRANCH", project.dest_branch)
|
||||
setenv("REPO_I", str(cnt + 1))
|
||||
for annotation in project.annotations:
|
||||
setenv("REPO__%s" % (annotation.name), annotation.value)
|
||||
env.update(project.GetEnvVars(local=opt.this_manifest_only))
|
||||
env["REPO_I"] = str(cnt + 1)
|
||||
|
||||
if mirror:
|
||||
setenv("GIT_DIR", project.gitdir)
|
||||
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
# Copyright (C) 2024 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
from typing import List, Set
|
||||
|
||||
from command import Command
|
||||
from git_command import git_require
|
||||
from git_command import GitCommand
|
||||
import platform_utils
|
||||
from progress import Progress
|
||||
from project import Project
|
||||
|
||||
|
||||
class Gc(Command):
|
||||
COMMON = True
|
||||
helpSummary = "Cleaning up internal repo and Git state."
|
||||
helpUsage = """
|
||||
%prog
|
||||
"""
|
||||
|
||||
def _Options(self, p):
|
||||
p.add_option(
|
||||
"-n",
|
||||
"--dry-run",
|
||||
dest="dryrun",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="do everything except actually delete",
|
||||
)
|
||||
p.add_option(
|
||||
"-y",
|
||||
"--yes",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="answer yes to all safe prompts",
|
||||
)
|
||||
p.add_option(
|
||||
"--repack",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="repack all projects that use partial clone with "
|
||||
"filter=blob:none",
|
||||
)
|
||||
|
||||
def _find_git_to_delete(
|
||||
self, to_keep: Set[str], start_dir: str
|
||||
) -> Set[str]:
|
||||
"""Searches no longer needed ".git" directories.
|
||||
|
||||
Scans the file system starting from `start_dir` and removes all
|
||||
directories that end with ".git" that are not in the `to_keep` set.
|
||||
"""
|
||||
to_delete = set()
|
||||
for root, dirs, _ in platform_utils.walk(start_dir):
|
||||
for directory in dirs:
|
||||
if not directory.endswith(".git"):
|
||||
continue
|
||||
|
||||
path = os.path.join(root, directory)
|
||||
if path not in to_keep:
|
||||
to_delete.add(path)
|
||||
|
||||
return to_delete
|
||||
|
||||
def delete_unused_projects(self, projects: List[Project], opt):
|
||||
print(f"Scanning filesystem under {self.repodir}...")
|
||||
|
||||
project_paths = set()
|
||||
project_object_paths = set()
|
||||
|
||||
for project in projects:
|
||||
project_paths.add(project.gitdir)
|
||||
project_object_paths.add(project.objdir)
|
||||
|
||||
to_delete = self._find_git_to_delete(
|
||||
project_paths, os.path.join(self.repodir, "projects")
|
||||
)
|
||||
|
||||
to_delete.update(
|
||||
self._find_git_to_delete(
|
||||
project_object_paths,
|
||||
os.path.join(self.repodir, "project-objects"),
|
||||
)
|
||||
)
|
||||
|
||||
if not to_delete:
|
||||
print("Nothing to clean up.")
|
||||
return 0
|
||||
|
||||
print("Identified the following projects are no longer used:")
|
||||
print("\n".join(to_delete))
|
||||
print("")
|
||||
if not opt.yes:
|
||||
print(
|
||||
"If you proceed, any local commits in those projects will be "
|
||||
"destroyed!"
|
||||
)
|
||||
ask = input("Proceed? [y/N] ")
|
||||
if ask.lower() != "y":
|
||||
return 1
|
||||
|
||||
pm = Progress(
|
||||
"Deleting",
|
||||
len(to_delete),
|
||||
delay=False,
|
||||
quiet=opt.quiet,
|
||||
show_elapsed=True,
|
||||
elide=True,
|
||||
)
|
||||
|
||||
for path in to_delete:
|
||||
if opt.dryrun:
|
||||
print(f"\nWould have deleted ${path}")
|
||||
else:
|
||||
tmp_path = os.path.join(
|
||||
os.path.dirname(path),
|
||||
f"to_be_deleted_{os.path.basename(path)}",
|
||||
)
|
||||
platform_utils.rename(path, tmp_path)
|
||||
platform_utils.rmtree(tmp_path)
|
||||
pm.update(msg=path)
|
||||
pm.end()
|
||||
|
||||
return 0
|
||||
|
||||
def _generate_promisor_files(self, pack_dir: str):
|
||||
"""Generates promisor files for all pack files in the given directory.
|
||||
|
||||
Promisor files are empty files with the same name as the corresponding
|
||||
pack file but with the ".promisor" extension. They are used by Git.
|
||||
"""
|
||||
for root, _, files in platform_utils.walk(pack_dir):
|
||||
for file in files:
|
||||
if not file.endswith(".pack"):
|
||||
continue
|
||||
with open(os.path.join(root, f"{file[:-4]}promisor"), "w"):
|
||||
pass
|
||||
|
||||
def repack_projects(self, projects: List[Project], opt):
|
||||
repack_projects = []
|
||||
# Find all projects eligible for repacking:
|
||||
# - can't be shared
|
||||
# - have a specific fetch filter
|
||||
for project in projects:
|
||||
if project.config.GetBoolean("extensions.preciousObjects"):
|
||||
continue
|
||||
if not project.clone_depth:
|
||||
continue
|
||||
if project.manifest.CloneFilterForDepth != "blob:none":
|
||||
continue
|
||||
|
||||
repack_projects.append(project)
|
||||
|
||||
if opt.dryrun:
|
||||
print(f"Would have repacked {len(repack_projects)} projects.")
|
||||
return 0
|
||||
|
||||
pm = Progress(
|
||||
"Repacking (this will take a while)",
|
||||
len(repack_projects),
|
||||
delay=False,
|
||||
quiet=opt.quiet,
|
||||
show_elapsed=True,
|
||||
elide=True,
|
||||
)
|
||||
|
||||
for project in repack_projects:
|
||||
pm.update(msg=f"{project.name}")
|
||||
|
||||
pack_dir = os.path.join(project.gitdir, "tmp_repo_repack")
|
||||
if os.path.isdir(pack_dir):
|
||||
platform_utils.rmtree(pack_dir)
|
||||
os.mkdir(pack_dir)
|
||||
|
||||
# Prepare workspace for repacking - remove all unreachable refs and
|
||||
# their objects.
|
||||
GitCommand(
|
||||
project,
|
||||
["reflog", "expire", "--expire-unreachable=all"],
|
||||
verify_command=True,
|
||||
).Wait()
|
||||
pm.update(msg=f"{project.name} | gc", inc=0)
|
||||
GitCommand(
|
||||
project,
|
||||
["gc"],
|
||||
verify_command=True,
|
||||
).Wait()
|
||||
|
||||
# Get all objects that are reachable from the remote, and pack them.
|
||||
pm.update(msg=f"{project.name} | generating list of objects", inc=0)
|
||||
remote_objects_cmd = GitCommand(
|
||||
project,
|
||||
[
|
||||
"rev-list",
|
||||
"--objects",
|
||||
"--missing=allow-promisor",
|
||||
f"--remotes={project.remote.name}",
|
||||
"--filter=blob:none",
|
||||
"--tags",
|
||||
],
|
||||
capture_stdout=True,
|
||||
verify_command=True,
|
||||
)
|
||||
|
||||
# Get all local objects and pack them.
|
||||
local_head_objects_cmd = GitCommand(
|
||||
project,
|
||||
[
|
||||
"rev-list",
|
||||
"--objects",
|
||||
"--missing=allow-promisor",
|
||||
"HEAD^{tree}",
|
||||
],
|
||||
capture_stdout=True,
|
||||
verify_command=True,
|
||||
)
|
||||
local_objects_cmd = GitCommand(
|
||||
project,
|
||||
[
|
||||
"rev-list",
|
||||
"--objects",
|
||||
"--missing=allow-promisor",
|
||||
"--all",
|
||||
"--reflog",
|
||||
"--indexed-objects",
|
||||
"--not",
|
||||
f"--remotes={project.remote.name}",
|
||||
"--tags",
|
||||
],
|
||||
capture_stdout=True,
|
||||
verify_command=True,
|
||||
)
|
||||
|
||||
remote_objects_cmd.Wait()
|
||||
|
||||
pm.update(msg=f"{project.name} | remote repack", inc=0)
|
||||
GitCommand(
|
||||
project,
|
||||
["pack-objects", os.path.join(pack_dir, "pack")],
|
||||
input=remote_objects_cmd.stdout,
|
||||
capture_stderr=True,
|
||||
capture_stdout=True,
|
||||
verify_command=True,
|
||||
).Wait()
|
||||
|
||||
# create promisor file for each pack file
|
||||
self._generate_promisor_files(pack_dir)
|
||||
|
||||
local_head_objects_cmd.Wait()
|
||||
local_objects_cmd.Wait()
|
||||
|
||||
pm.update(msg=f"{project.name} | local repack", inc=0)
|
||||
GitCommand(
|
||||
project,
|
||||
["pack-objects", os.path.join(pack_dir, "pack")],
|
||||
input=local_head_objects_cmd.stdout + local_objects_cmd.stdout,
|
||||
capture_stderr=True,
|
||||
capture_stdout=True,
|
||||
verify_command=True,
|
||||
).Wait()
|
||||
|
||||
# Swap the old pack directory with the new one.
|
||||
platform_utils.rename(
|
||||
os.path.join(project.objdir, "objects", "pack"),
|
||||
os.path.join(project.objdir, "objects", "pack_old"),
|
||||
)
|
||||
platform_utils.rename(
|
||||
pack_dir,
|
||||
os.path.join(project.objdir, "objects", "pack"),
|
||||
)
|
||||
platform_utils.rmtree(
|
||||
os.path.join(project.objdir, "objects", "pack_old")
|
||||
)
|
||||
|
||||
pm.end()
|
||||
return 0
|
||||
|
||||
def Execute(self, opt, args):
|
||||
projects: List[Project] = self.GetProjects(
|
||||
args, all_manifests=not opt.this_manifest_only
|
||||
)
|
||||
|
||||
# If the user specified projects, fetch the global list separately
|
||||
# to avoid deleting untargeted projects.
|
||||
if args:
|
||||
all_projects = self.GetProjects(
|
||||
[], all_manifests=not opt.this_manifest_only
|
||||
)
|
||||
else:
|
||||
all_projects = projects
|
||||
|
||||
ret = self.delete_unused_projects(all_projects, opt)
|
||||
if ret != 0:
|
||||
return ret
|
||||
|
||||
if opt.repack:
|
||||
git_require((2, 17, 0), fail=True, msg="--repack")
|
||||
ret = self.repack_projects(projects, opt)
|
||||
|
||||
return ret
|
||||
+28
-24
@@ -23,7 +23,6 @@ from error import GitError
|
||||
from error import InvalidArgumentsError
|
||||
from error import SilentRepoExitError
|
||||
from git_command import GitCommand
|
||||
from project import Project
|
||||
from repo_logging import RepoLogger
|
||||
|
||||
|
||||
@@ -40,7 +39,7 @@ class GrepColoring(Coloring):
|
||||
class ExecuteOneResult(NamedTuple):
|
||||
"""Result from an execute instance."""
|
||||
|
||||
project: Project
|
||||
project_idx: int
|
||||
rc: int
|
||||
stdout: str
|
||||
stderr: str
|
||||
@@ -94,7 +93,7 @@ contain a line that matches both expressions:
|
||||
pt = getattr(parser.values, "cmd_argv", None)
|
||||
if pt is None:
|
||||
pt = []
|
||||
setattr(parser.values, "cmd_argv", pt)
|
||||
parser.values.cmd_argv = pt
|
||||
|
||||
if opt_str == "-(":
|
||||
pt.append("(")
|
||||
@@ -121,7 +120,6 @@ contain a line that matches both expressions:
|
||||
g.add_option(
|
||||
"-r",
|
||||
"--revision",
|
||||
dest="revision",
|
||||
action="append",
|
||||
metavar="TREEish",
|
||||
help="Search TREEish, instead of the work tree",
|
||||
@@ -262,8 +260,10 @@ contain a line that matches both expressions:
|
||||
help="Show only file names not containing matching lines",
|
||||
)
|
||||
|
||||
def _ExecuteOne(self, cmd_argv, project):
|
||||
@classmethod
|
||||
def _ExecuteOne(cls, cmd_argv, project_idx):
|
||||
"""Process one project."""
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
try:
|
||||
p = GitCommand(
|
||||
project,
|
||||
@@ -274,7 +274,7 @@ contain a line that matches both expressions:
|
||||
verify_command=True,
|
||||
)
|
||||
except GitError as e:
|
||||
return ExecuteOneResult(project, -1, None, str(e), e)
|
||||
return ExecuteOneResult(project_idx, -1, None, str(e), e)
|
||||
|
||||
try:
|
||||
error = None
|
||||
@@ -282,10 +282,12 @@ contain a line that matches both expressions:
|
||||
except GitError as e:
|
||||
rc = 1
|
||||
error = e
|
||||
return ExecuteOneResult(project, rc, p.stdout, p.stderr, error)
|
||||
return ExecuteOneResult(project_idx, rc, p.stdout, p.stderr, error)
|
||||
|
||||
@staticmethod
|
||||
def _ProcessResults(full_name, have_rev, opt, _pool, out, results):
|
||||
def _ProcessResults(
|
||||
full_name, have_rev, opt, projects, _pool, out, results
|
||||
):
|
||||
git_failed = False
|
||||
bad_rev = False
|
||||
have_match = False
|
||||
@@ -293,9 +295,10 @@ contain a line that matches both expressions:
|
||||
errors = []
|
||||
|
||||
for result in results:
|
||||
project = projects[result.project_idx]
|
||||
if result.rc < 0:
|
||||
git_failed = True
|
||||
out.project("--- project %s ---" % _RelPath(result.project))
|
||||
out.project("--- project %s ---" % _RelPath(project))
|
||||
out.nl()
|
||||
out.fail("%s", result.stderr)
|
||||
out.nl()
|
||||
@@ -311,9 +314,7 @@ contain a line that matches both expressions:
|
||||
):
|
||||
bad_rev = True
|
||||
else:
|
||||
out.project(
|
||||
"--- project %s ---" % _RelPath(result.project)
|
||||
)
|
||||
out.project("--- project %s ---" % _RelPath(project))
|
||||
out.nl()
|
||||
out.fail("%s", result.stderr.strip())
|
||||
out.nl()
|
||||
@@ -331,13 +332,13 @@ contain a line that matches both expressions:
|
||||
rev, line = line.split(":", 1)
|
||||
out.write("%s", rev)
|
||||
out.write(":")
|
||||
out.project(_RelPath(result.project))
|
||||
out.project(_RelPath(project))
|
||||
out.write("/")
|
||||
out.write("%s", line)
|
||||
out.nl()
|
||||
elif full_name:
|
||||
for line in r:
|
||||
out.project(_RelPath(result.project))
|
||||
out.project(_RelPath(project))
|
||||
out.write("/")
|
||||
out.write("%s", line)
|
||||
out.nl()
|
||||
@@ -381,16 +382,19 @@ contain a line that matches both expressions:
|
||||
cmd_argv.extend(opt.revision)
|
||||
cmd_argv.append("--")
|
||||
|
||||
git_failed, bad_rev, have_match, errors = self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._ExecuteOne, cmd_argv),
|
||||
projects,
|
||||
callback=functools.partial(
|
||||
self._ProcessResults, full_name, have_rev, opt
|
||||
),
|
||||
output=out,
|
||||
ordered=True,
|
||||
)
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = projects
|
||||
git_failed, bad_rev, have_match, errors = self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._ExecuteOne, cmd_argv),
|
||||
range(len(projects)),
|
||||
callback=functools.partial(
|
||||
self._ProcessResults, full_name, have_rev, opt, projects
|
||||
),
|
||||
output=out,
|
||||
ordered=True,
|
||||
chunksize=1,
|
||||
)
|
||||
|
||||
if git_failed:
|
||||
raise GrepCommandError(
|
||||
|
||||
+3
-5
@@ -59,7 +59,7 @@ Displays detailed usage information about a command.
|
||||
|
||||
def PrintAllCommandsBody(self):
|
||||
print("The complete list of recognized repo commands is:")
|
||||
commandNames = list(sorted(all_commands))
|
||||
commandNames = sorted(all_commands)
|
||||
self._PrintCommands(commandNames)
|
||||
print(
|
||||
"See 'repo help <command>' for more information on a "
|
||||
@@ -74,10 +74,8 @@ Displays detailed usage information about a command.
|
||||
def PrintCommonCommandsBody(self):
|
||||
print("The most commonly used repo commands are:")
|
||||
|
||||
commandNames = list(
|
||||
sorted(
|
||||
name for name, command in all_commands.items() if command.COMMON
|
||||
)
|
||||
commandNames = sorted(
|
||||
name for name, command in all_commands.items() if command.COMMON
|
||||
)
|
||||
self._PrintCommands(commandNames)
|
||||
|
||||
|
||||
+343
-121
@@ -12,14 +12,41 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import enum
|
||||
import functools
|
||||
import io
|
||||
import json
|
||||
import optparse
|
||||
import sys
|
||||
from typing import Any, Dict, List, NamedTuple
|
||||
|
||||
from color import Coloring
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
from command import PagedCommand
|
||||
from git_refs import R_HEADS
|
||||
from git_refs import R_M
|
||||
|
||||
|
||||
class BranchInfo(NamedTuple):
|
||||
"""Holds information about a branch in a project."""
|
||||
|
||||
relpath: str
|
||||
name: str
|
||||
commits: Any
|
||||
date: str
|
||||
is_current: bool
|
||||
|
||||
|
||||
class OutputFormat(enum.Enum):
|
||||
"""Type for the requested output format."""
|
||||
|
||||
# Human-readable text output.
|
||||
TEXT = enum.auto()
|
||||
|
||||
# Machine-readable JSON output.
|
||||
JSON = enum.auto()
|
||||
|
||||
|
||||
class _Coloring(Coloring):
|
||||
def __init__(self, config):
|
||||
Coloring.__init__(self, config, "status")
|
||||
@@ -27,10 +54,11 @@ class _Coloring(Coloring):
|
||||
|
||||
class Info(PagedCommand):
|
||||
COMMON = True
|
||||
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
|
||||
helpSummary = (
|
||||
"Get info on the manifest branch, current branch or unmerged branches"
|
||||
)
|
||||
helpUsage = "%prog [-dl] [-o [-c]] [<project>...]"
|
||||
helpUsage = "%prog [-dl] [-o [-c]] [--format=<format>] [<project>...]"
|
||||
|
||||
def _Options(self, p):
|
||||
p.add_option(
|
||||
@@ -43,14 +71,36 @@ class Info(PagedCommand):
|
||||
p.add_option(
|
||||
"-o",
|
||||
"--overview",
|
||||
dest="overview",
|
||||
action="store_true",
|
||||
help="show overview of all local commits",
|
||||
)
|
||||
p.add_option(
|
||||
"--include-summary",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="include manifest summary (default: true)",
|
||||
)
|
||||
p.add_option(
|
||||
"--no-include-summary",
|
||||
dest="include_summary",
|
||||
action="store_false",
|
||||
help="exclude manifest summary",
|
||||
)
|
||||
p.add_option(
|
||||
"--include-projects",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="include project details (default: true)",
|
||||
)
|
||||
p.add_option(
|
||||
"--no-include-projects",
|
||||
dest="include_projects",
|
||||
action="store_false",
|
||||
help="exclude project details",
|
||||
)
|
||||
p.add_option(
|
||||
"-c",
|
||||
"--current-branch",
|
||||
dest="current_branch",
|
||||
action="store_true",
|
||||
help="consider only checked out branches",
|
||||
)
|
||||
@@ -74,8 +124,37 @@ class Info(PagedCommand):
|
||||
action="store_true",
|
||||
help="disable all remote operations",
|
||||
)
|
||||
formats = tuple(x.lower() for x in OutputFormat.__members__.keys())
|
||||
p.add_option(
|
||||
"--format",
|
||||
default=OutputFormat.TEXT.name.lower(),
|
||||
choices=formats,
|
||||
help=f"output format: {', '.join(formats)} (default: %default)",
|
||||
)
|
||||
|
||||
def WantPager(self, opt):
|
||||
return OutputFormat[opt.format.upper()] == OutputFormat.TEXT
|
||||
|
||||
def ValidateOptions(self, opt, args):
|
||||
output_format = OutputFormat[opt.format.upper()]
|
||||
if output_format == OutputFormat.JSON:
|
||||
if opt.all:
|
||||
self.OptionParser.error("--diff is not supported with JSON")
|
||||
if opt.overview:
|
||||
self.OptionParser.error("--overview is not supported with JSON")
|
||||
|
||||
def Execute(self, opt, args):
|
||||
if not opt.this_manifest_only:
|
||||
self.manifest = self.manifest.outer_client
|
||||
|
||||
output_format = OutputFormat[opt.format.upper()]
|
||||
if output_format == OutputFormat.JSON:
|
||||
self._ExecuteJson(opt, args)
|
||||
else:
|
||||
self._ExecuteText(opt, args)
|
||||
|
||||
def _ExecuteText(self, opt, args) -> None:
|
||||
"""Output info as human-readable text."""
|
||||
self.out = _Coloring(self.client.globalConfig)
|
||||
self.heading = self.out.printer("heading", attr="bold")
|
||||
self.headtext = self.out.nofmt_printer("headtext", fg="yellow")
|
||||
@@ -86,152 +165,295 @@ class Info(PagedCommand):
|
||||
|
||||
self.opt = opt
|
||||
|
||||
if not opt.this_manifest_only:
|
||||
self.manifest = self.manifest.outer_client
|
||||
manifestConfig = self.manifest.manifestProject.config
|
||||
mergeBranch = manifestConfig.GetBranch("default").merge
|
||||
manifestGroups = self.manifest.GetGroupsStr()
|
||||
if opt.include_summary:
|
||||
self._printSummary()
|
||||
|
||||
self.heading("Manifest branch: ")
|
||||
if self.manifest.default.revisionExpr:
|
||||
self.headtext(self.manifest.default.revisionExpr)
|
||||
self.out.nl()
|
||||
self.heading("Manifest merge branch: ")
|
||||
# The manifest might not have a merge branch if it isn't in a git repo,
|
||||
# e.g. if `repo init --standalone-manifest` is used.
|
||||
self.headtext(mergeBranch or "")
|
||||
self.out.nl()
|
||||
self.heading("Manifest groups: ")
|
||||
self.headtext(manifestGroups)
|
||||
self.out.nl()
|
||||
|
||||
self.printSeparator()
|
||||
|
||||
if not opt.overview:
|
||||
if not opt.include_projects:
|
||||
return
|
||||
elif not opt.overview:
|
||||
self._printDiffInfo(opt, args)
|
||||
else:
|
||||
self._printCommitOverview(opt, args)
|
||||
|
||||
def _getSummaryData(self) -> Dict[str, Any]:
|
||||
"""Gather manifest summary data as a dict."""
|
||||
manifestConfig = self.manifest.manifestProject.config
|
||||
mergeBranch = manifestConfig.GetBranch("default").merge
|
||||
manifestGroups = self.manifest.GetManifestGroupsStr()
|
||||
sp = self.manifest.superproject
|
||||
srev = sp.commit_id if sp and sp.commit_id else None
|
||||
return {
|
||||
"manifest_branch": self.manifest.default.revisionExpr or "",
|
||||
"manifest_merge_branch": mergeBranch or "",
|
||||
"manifest_groups": manifestGroups,
|
||||
"superproject_revision": srev,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _getProjectData(cls, project) -> Dict[str, Any]:
|
||||
"""Gather project data as a dict."""
|
||||
data = {
|
||||
"name": project.name,
|
||||
"mount_path": project.worktree,
|
||||
"current_revision": project.GetHeadRevisionId()
|
||||
or project.GetRevisionId(),
|
||||
"manifest_revision": project.revisionExpr,
|
||||
"local_branches": list(project.GetBranches()),
|
||||
}
|
||||
currentBranch = project.CurrentBranch
|
||||
if currentBranch:
|
||||
data["current_branch"] = currentBranch
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def _ProjectDataHelper(cls, project_idx: int) -> Dict[str, Any]:
|
||||
"""Helper to get project data in parallel."""
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
return cls._getProjectData(project)
|
||||
|
||||
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
|
||||
)
|
||||
project_data = []
|
||||
|
||||
def _ProcessResults(_pool, _output, results):
|
||||
project_data.extend(results)
|
||||
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = projs
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
self._ProjectDataHelper,
|
||||
range(len(projs)),
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
chunksize=1,
|
||||
)
|
||||
result["projects"] = project_data
|
||||
|
||||
json_settings = {
|
||||
# JSON style guide says Unicode characters are fully allowed.
|
||||
"ensure_ascii": False,
|
||||
# We use 2 space indent to match JSON style guide.
|
||||
"indent": 2,
|
||||
"separators": (",", ": "),
|
||||
"sort_keys": True,
|
||||
}
|
||||
sys.stdout.write(json.dumps(result, **json_settings) + "\n")
|
||||
|
||||
def _printSummary(self) -> None:
|
||||
"""Print manifest summary in text format."""
|
||||
data = self._getSummaryData()
|
||||
self.heading("Manifest branch: ")
|
||||
self.headtext(data["manifest_branch"])
|
||||
self.out.nl()
|
||||
self.heading("Manifest merge branch: ")
|
||||
self.headtext(data["manifest_merge_branch"])
|
||||
self.out.nl()
|
||||
self.heading("Manifest groups: ")
|
||||
self.headtext(data["manifest_groups"])
|
||||
self.out.nl()
|
||||
self.heading("Superproject revision: ")
|
||||
self.headtext(data["superproject_revision"] or "None")
|
||||
self.out.nl()
|
||||
self.printSeparator()
|
||||
|
||||
def printSeparator(self):
|
||||
self.text("----------------------------")
|
||||
self.out.nl()
|
||||
|
||||
@classmethod
|
||||
def _DiffHelper(cls, project_idx: int, opt: Any) -> str:
|
||||
"""Helper for ParallelContext to get diff info for a project."""
|
||||
buf = io.StringIO()
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
config = cls.get_parallel_context()["config"]
|
||||
|
||||
out = _Coloring(config)
|
||||
out.redirect(buf)
|
||||
|
||||
heading = out.printer("heading", attr="bold")
|
||||
headtext = out.nofmt_printer("headtext", fg="yellow")
|
||||
redtext = out.printer("redtext", fg="red")
|
||||
sha = out.printer("sha", fg="yellow")
|
||||
text = out.nofmt_printer("text")
|
||||
dimtext = out.printer("dimtext", attr="dim")
|
||||
|
||||
heading("Project: ")
|
||||
headtext(project.name)
|
||||
out.nl()
|
||||
|
||||
heading("Mount path: ")
|
||||
headtext(project.worktree)
|
||||
out.nl()
|
||||
|
||||
heading("Current revision: ")
|
||||
headtext(project.GetHeadRevisionId() or project.GetRevisionId())
|
||||
out.nl()
|
||||
|
||||
currentBranch = project.CurrentBranch
|
||||
if currentBranch:
|
||||
heading("Current branch: ")
|
||||
headtext(currentBranch)
|
||||
out.nl()
|
||||
|
||||
heading("Manifest revision: ")
|
||||
headtext(project.revisionExpr)
|
||||
out.nl()
|
||||
|
||||
localBranches = list(project.GetBranches().keys())
|
||||
heading("Local Branches: ")
|
||||
redtext(str(len(localBranches)))
|
||||
if localBranches:
|
||||
text(" [")
|
||||
text(", ".join(localBranches))
|
||||
text("]")
|
||||
out.nl()
|
||||
|
||||
if opt.all:
|
||||
if not opt.local:
|
||||
project.Sync_NetworkHalf(quiet=True, current_branch_only=True)
|
||||
|
||||
branch = project.manifest.manifestProject.config.GetBranch(
|
||||
"default"
|
||||
).merge
|
||||
if branch.startswith(R_HEADS):
|
||||
branch = branch[len(R_HEADS) :]
|
||||
logTarget = R_M + branch
|
||||
|
||||
bareTmp = project.bare_git._bare
|
||||
project.bare_git._bare = False
|
||||
localCommits = project.bare_git.rev_list(
|
||||
"--abbrev=8",
|
||||
"--abbrev-commit",
|
||||
"--pretty=oneline",
|
||||
logTarget + "..",
|
||||
"--",
|
||||
)
|
||||
|
||||
originCommits = project.bare_git.rev_list(
|
||||
"--abbrev=8",
|
||||
"--abbrev-commit",
|
||||
"--pretty=oneline",
|
||||
".." + logTarget,
|
||||
"--",
|
||||
)
|
||||
project.bare_git._bare = bareTmp
|
||||
|
||||
heading("Local Commits: ")
|
||||
redtext(str(len(localCommits)))
|
||||
dimtext(" (on current branch)")
|
||||
out.nl()
|
||||
|
||||
for c in localCommits:
|
||||
split = c.split()
|
||||
sha(split[0] + " ")
|
||||
text(" ".join(split[1:]))
|
||||
out.nl()
|
||||
|
||||
text("----------------------------")
|
||||
out.nl()
|
||||
|
||||
heading("Remote Commits: ")
|
||||
redtext(str(len(originCommits)))
|
||||
out.nl()
|
||||
|
||||
for c in originCommits:
|
||||
split = c.split()
|
||||
sha(split[0] + " ")
|
||||
text(" ".join(split[1:]))
|
||||
out.nl()
|
||||
|
||||
text("----------------------------")
|
||||
out.nl()
|
||||
|
||||
return buf.getvalue()
|
||||
|
||||
def _printDiffInfo(self, opt, args):
|
||||
# We let exceptions bubble up to main as they'll be well structured.
|
||||
projs = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
|
||||
|
||||
for p in projs:
|
||||
self.heading("Project: ")
|
||||
self.headtext(p.name)
|
||||
self.out.nl()
|
||||
def _ProcessResults(_pool, _output, results):
|
||||
for output in results:
|
||||
if output:
|
||||
print(output, end="")
|
||||
|
||||
self.heading("Mount path: ")
|
||||
self.headtext(p.worktree)
|
||||
self.out.nl()
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = projs
|
||||
self.get_parallel_context()[
|
||||
"config"
|
||||
] = self.manifest.manifestProject.config
|
||||
|
||||
self.heading("Current revision: ")
|
||||
self.headtext(p.GetRevisionId())
|
||||
self.out.nl()
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._DiffHelper, opt=opt),
|
||||
range(len(projs)),
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
chunksize=1,
|
||||
)
|
||||
|
||||
currentBranch = p.CurrentBranch
|
||||
if currentBranch:
|
||||
self.heading("Current branch: ")
|
||||
self.headtext(currentBranch)
|
||||
self.out.nl()
|
||||
@classmethod
|
||||
def _OverviewHelper(cls, project_idx: int, opt: Any) -> List[BranchInfo]:
|
||||
"""Helper to get overview of uploadable branches."""
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
|
||||
self.heading("Manifest revision: ")
|
||||
self.headtext(p.revisionExpr)
|
||||
self.out.nl()
|
||||
branches = []
|
||||
br = [project.GetUploadableBranch(x) for x in project.GetBranches()]
|
||||
br = [x for x in br if x]
|
||||
if opt.current_branch:
|
||||
br = [x for x in br if x.name == project.CurrentBranch]
|
||||
|
||||
localBranches = list(p.GetBranches().keys())
|
||||
self.heading("Local Branches: ")
|
||||
self.redtext(str(len(localBranches)))
|
||||
if localBranches:
|
||||
self.text(" [")
|
||||
self.text(", ".join(localBranches))
|
||||
self.text("]")
|
||||
self.out.nl()
|
||||
|
||||
if self.opt.all:
|
||||
self.findRemoteLocalDiff(p)
|
||||
|
||||
self.printSeparator()
|
||||
|
||||
def findRemoteLocalDiff(self, project):
|
||||
# Fetch all the latest commits.
|
||||
if not self.opt.local:
|
||||
project.Sync_NetworkHalf(quiet=True, current_branch_only=True)
|
||||
|
||||
branch = self.manifest.manifestProject.config.GetBranch("default").merge
|
||||
if branch.startswith(R_HEADS):
|
||||
branch = branch[len(R_HEADS) :]
|
||||
logTarget = R_M + branch
|
||||
|
||||
bareTmp = project.bare_git._bare
|
||||
project.bare_git._bare = False
|
||||
localCommits = project.bare_git.rev_list(
|
||||
"--abbrev=8",
|
||||
"--abbrev-commit",
|
||||
"--pretty=oneline",
|
||||
logTarget + "..",
|
||||
"--",
|
||||
)
|
||||
|
||||
originCommits = project.bare_git.rev_list(
|
||||
"--abbrev=8",
|
||||
"--abbrev-commit",
|
||||
"--pretty=oneline",
|
||||
".." + logTarget,
|
||||
"--",
|
||||
)
|
||||
project.bare_git._bare = bareTmp
|
||||
|
||||
self.heading("Local Commits: ")
|
||||
self.redtext(str(len(localCommits)))
|
||||
self.dimtext(" (on current branch)")
|
||||
self.out.nl()
|
||||
|
||||
for c in localCommits:
|
||||
split = c.split()
|
||||
self.sha(split[0] + " ")
|
||||
self.text(" ".join(split[1:]))
|
||||
self.out.nl()
|
||||
|
||||
self.printSeparator()
|
||||
|
||||
self.heading("Remote Commits: ")
|
||||
self.redtext(str(len(originCommits)))
|
||||
self.out.nl()
|
||||
|
||||
for c in originCommits:
|
||||
split = c.split()
|
||||
self.sha(split[0] + " ")
|
||||
self.text(" ".join(split[1:]))
|
||||
self.out.nl()
|
||||
for b in br:
|
||||
branches.append(
|
||||
BranchInfo(
|
||||
relpath=project.RelPath(local=opt.this_manifest_only),
|
||||
name=b.name,
|
||||
commits=b.commits,
|
||||
date=b.date,
|
||||
is_current=b.name == project.CurrentBranch,
|
||||
)
|
||||
)
|
||||
return branches
|
||||
|
||||
def _printCommitOverview(self, opt, args):
|
||||
projs = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
|
||||
|
||||
all_branches = []
|
||||
for project in self.GetProjects(
|
||||
args, all_manifests=not opt.this_manifest_only
|
||||
):
|
||||
br = [project.GetUploadableBranch(x) for x in project.GetBranches()]
|
||||
br = [x for x in br if x]
|
||||
if self.opt.current_branch:
|
||||
br = [x for x in br if x.name == project.CurrentBranch]
|
||||
all_branches.extend(br)
|
||||
|
||||
def _ProcessResults(_pool, _output, results):
|
||||
for branches in results:
|
||||
all_branches.extend(branches)
|
||||
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = projs
|
||||
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._OverviewHelper, opt=opt),
|
||||
range(len(projs)),
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
chunksize=1,
|
||||
)
|
||||
|
||||
if not all_branches:
|
||||
return
|
||||
|
||||
self.out.nl()
|
||||
self.heading("Projects Overview")
|
||||
project = None
|
||||
current_relpath = None
|
||||
|
||||
for branch in all_branches:
|
||||
if project != branch.project:
|
||||
project = branch.project
|
||||
if current_relpath != branch.relpath:
|
||||
current_relpath = branch.relpath
|
||||
self.out.nl()
|
||||
self.headtext(project.RelPath(local=opt.this_manifest_only))
|
||||
self.headtext(current_relpath)
|
||||
self.out.nl()
|
||||
|
||||
commits = branch.commits
|
||||
@@ -239,7 +461,7 @@ class Info(PagedCommand):
|
||||
self.text(
|
||||
"%s %-33s (%2d commit%s, %s)"
|
||||
% (
|
||||
branch.name == project.CurrentBranch and "*" or " ",
|
||||
branch.is_current and "*" or " ",
|
||||
branch.name,
|
||||
len(commits),
|
||||
len(commits) != 1 and "s" or "",
|
||||
|
||||
@@ -33,6 +33,7 @@ _REPO_ALLOW_SHALLOW = os.environ.get("REPO_ALLOW_SHALLOW")
|
||||
|
||||
class Init(InteractiveCommand, MirrorSafeCommand):
|
||||
COMMON = True
|
||||
RESPECT_SMART_SYNC_OVERRIDE = False
|
||||
MULTI_MANIFEST_SUPPORT = True
|
||||
helpSummary = "Initialize a repo client checkout in the current directory"
|
||||
helpUsage = """
|
||||
@@ -52,6 +53,10 @@ The optional -b argument can be used to select the manifest branch
|
||||
to checkout and use. If no branch is specified, the remote's default
|
||||
branch is used. This is equivalent to using -b HEAD.
|
||||
|
||||
The optional --manifest-upstream-branch argument can be used when a commit is
|
||||
provided to --manifest-branch (or -b), to specify the name of the git ref in
|
||||
which the commit can be found.
|
||||
|
||||
The optional -m argument can be used to specify an alternate manifest
|
||||
to be used. If no manifest is specified, the manifest default.xml
|
||||
will be used.
|
||||
@@ -123,6 +128,7 @@ to update the working directory files.
|
||||
return {
|
||||
"REPO_MANIFEST_URL": "manifest_url",
|
||||
"REPO_MIRROR_LOCATION": "reference",
|
||||
"REPO_GIT_LFS": "git_lfs",
|
||||
}
|
||||
|
||||
def _SyncManifest(self, opt):
|
||||
@@ -135,6 +141,7 @@ to update the working directory files.
|
||||
# manifest project is special and is created when instantiating the
|
||||
# manifest which happens before we parse options.
|
||||
self.manifest.manifestProject.clone_depth = opt.manifest_depth
|
||||
self.manifest.manifestProject.upstream = opt.manifest_upstream_branch
|
||||
clone_filter_for_depth = (
|
||||
"blob:none" if (_REPO_ALLOW_SHALLOW == "0") else None
|
||||
)
|
||||
@@ -163,6 +170,7 @@ to update the working directory files.
|
||||
depth=opt.depth,
|
||||
git_event_log=self.git_event_log,
|
||||
manifest_name=opt.manifest_name,
|
||||
use_local_gitdirs=opt.use_local_gitdirs,
|
||||
):
|
||||
manifest_name = opt.manifest_name
|
||||
raise UpdateManifestError(
|
||||
@@ -317,6 +325,12 @@ to update the working directory files.
|
||||
" be used with --standalone-manifest."
|
||||
)
|
||||
|
||||
if opt.manifest_upstream_branch and opt.manifest_branch is None:
|
||||
self.OptionParser.error(
|
||||
"--manifest-upstream-branch cannot be used without "
|
||||
"--manifest-branch."
|
||||
)
|
||||
|
||||
if args:
|
||||
if opt.manifest_url:
|
||||
self.OptionParser.error(
|
||||
|
||||
@@ -40,7 +40,6 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
|
||||
p.add_option(
|
||||
"-r",
|
||||
"--regex",
|
||||
dest="regex",
|
||||
action="store_true",
|
||||
help="filter the project list based on regex or wildcard matching "
|
||||
"of strings",
|
||||
@@ -48,7 +47,6 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
|
||||
p.add_option(
|
||||
"-g",
|
||||
"--groups",
|
||||
dest="groups",
|
||||
help="filter the project list based on the groups the project is "
|
||||
"in",
|
||||
)
|
||||
@@ -61,21 +59,18 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
|
||||
p.add_option(
|
||||
"-n",
|
||||
"--name-only",
|
||||
dest="name_only",
|
||||
action="store_true",
|
||||
help="display only the name of the repository",
|
||||
)
|
||||
p.add_option(
|
||||
"-p",
|
||||
"--path-only",
|
||||
dest="path_only",
|
||||
action="store_true",
|
||||
help="display only the path of the repository",
|
||||
)
|
||||
p.add_option(
|
||||
"-f",
|
||||
"--fullpath",
|
||||
dest="fullpath",
|
||||
action="store_true",
|
||||
help="display the full work tree path instead of the relative path",
|
||||
)
|
||||
|
||||
+33
-7
@@ -12,7 +12,9 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import enum
|
||||
import json
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -23,6 +25,16 @@ from repo_logging import RepoLogger
|
||||
logger = RepoLogger(__file__)
|
||||
|
||||
|
||||
class OutputFormat(enum.Enum):
|
||||
"""Type for the requested output format."""
|
||||
|
||||
# Canonicalized manifest in XML format.
|
||||
XML = enum.auto()
|
||||
|
||||
# Canonicalized manifest in JSON format.
|
||||
JSON = enum.auto()
|
||||
|
||||
|
||||
class Manifest(PagedCommand):
|
||||
COMMON = False
|
||||
helpSummary = "Manifest inspection utility"
|
||||
@@ -42,6 +54,10 @@ revisions set to the current commit hash. These are known as
|
||||
In this case, the 'upstream' attribute is set to the ref we were on
|
||||
when the manifest was generated. The 'dest-branch' attribute is set
|
||||
to indicate the remote ref to push changes to via 'repo upload'.
|
||||
|
||||
Multiple output formats are supported via --format. The default output
|
||||
is XML, and formats are generally "condensed". Use --pretty for more
|
||||
human-readable variations.
|
||||
"""
|
||||
|
||||
@property
|
||||
@@ -86,11 +102,21 @@ to indicate the remote ref to push changes to via 'repo upload'.
|
||||
"(only of use if the branch names for a sha1 manifest are "
|
||||
"sensitive)",
|
||||
)
|
||||
# Replaced with --format=json. Kept for backwards compatibility.
|
||||
# Can delete in Jun 2026 or later.
|
||||
p.add_option(
|
||||
"--json",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="output manifest in JSON format (experimental)",
|
||||
action="store_const",
|
||||
dest="format",
|
||||
const=OutputFormat.JSON.name.lower(),
|
||||
help=optparse.SUPPRESS_HELP,
|
||||
)
|
||||
formats = tuple(x.lower() for x in OutputFormat.__members__.keys())
|
||||
p.add_option(
|
||||
"--format",
|
||||
default=OutputFormat.XML.name.lower(),
|
||||
choices=formats,
|
||||
help=f"output format: {', '.join(formats)} (default: %default)",
|
||||
)
|
||||
p.add_option(
|
||||
"--pretty",
|
||||
@@ -108,7 +134,6 @@ to indicate the remote ref to push changes to via 'repo upload'.
|
||||
p.add_option(
|
||||
"-o",
|
||||
"--output-file",
|
||||
dest="output_file",
|
||||
default="-",
|
||||
help="file to save the manifest to. (Filename prefix for "
|
||||
"multi-tree.)",
|
||||
@@ -121,6 +146,8 @@ to indicate the remote ref to push changes to via 'repo upload'.
|
||||
if opt.manifest_name:
|
||||
self.manifest.Override(opt.manifest_name, False)
|
||||
|
||||
output_format = OutputFormat[opt.format.upper()]
|
||||
|
||||
for manifest in self.ManifestList(opt):
|
||||
output_file = opt.output_file
|
||||
if output_file == "-":
|
||||
@@ -135,8 +162,7 @@ to indicate the remote ref to push changes to via 'repo upload'.
|
||||
|
||||
manifest.SetUseLocalManifests(not opt.ignore_local_manifests)
|
||||
|
||||
if opt.json:
|
||||
logger.warning("warning: --json is experimental!")
|
||||
if output_format == OutputFormat.JSON:
|
||||
doc = manifest.ToDict(
|
||||
peg_rev=opt.peg_rev,
|
||||
peg_rev_upstream=opt.peg_rev_upstream,
|
||||
@@ -152,7 +178,7 @@ to indicate the remote ref to push changes to via 'repo upload'.
|
||||
"separators": (",", ": ") if opt.pretty else (",", ":"),
|
||||
"sort_keys": True,
|
||||
}
|
||||
fd.write(json.dumps(doc, **json_settings))
|
||||
fd.write(json.dumps(doc, **json_settings) + "\n")
|
||||
else:
|
||||
manifest.Save(
|
||||
fd,
|
||||
|
||||
+9
-5
@@ -37,7 +37,6 @@ are displayed.
|
||||
p.add_option(
|
||||
"-c",
|
||||
"--current-branch",
|
||||
dest="current_branch",
|
||||
action="store_true",
|
||||
help="consider only checked out branches",
|
||||
)
|
||||
@@ -60,10 +59,15 @@ are displayed.
|
||||
for project in self.GetProjects(
|
||||
args, all_manifests=not opt.this_manifest_only
|
||||
):
|
||||
br = [project.GetUploadableBranch(x) for x in project.GetBranches()]
|
||||
br = [x for x in br if x]
|
||||
local_branches = project.GetBranches()
|
||||
br = []
|
||||
for name, branch in local_branches.items():
|
||||
uploadable = project.GetUploadableBranch(name)
|
||||
if uploadable:
|
||||
uploadable.branch.current = branch.current
|
||||
br.append(uploadable)
|
||||
if opt.current_branch:
|
||||
br = [x for x in br if x.name == project.CurrentBranch]
|
||||
br = [x for x in br if x.current]
|
||||
all_branches.extend(br)
|
||||
|
||||
if not all_branches:
|
||||
@@ -98,7 +102,7 @@ are displayed.
|
||||
print(
|
||||
"%s %-33s (%2d commit%s, %s)"
|
||||
% (
|
||||
branch.name == project.CurrentBranch and "*" or " ",
|
||||
branch.current and "*" or " ",
|
||||
branch.name,
|
||||
len(commits),
|
||||
len(commits) != 1 and "s" or " ",
|
||||
|
||||
+13
-9
@@ -27,8 +27,10 @@ class Prune(PagedCommand):
|
||||
"""
|
||||
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
|
||||
|
||||
def _ExecuteOne(self, project):
|
||||
@classmethod
|
||||
def _ExecuteOne(cls, project_idx):
|
||||
"""Process one project."""
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
return project.PruneHeads()
|
||||
|
||||
def Execute(self, opt, args):
|
||||
@@ -41,13 +43,15 @@ class Prune(PagedCommand):
|
||||
def _ProcessResults(_pool, _output, results):
|
||||
return list(itertools.chain.from_iterable(results))
|
||||
|
||||
all_branches = self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
self._ExecuteOne,
|
||||
projects,
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
)
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = projects
|
||||
all_branches = self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
self._ExecuteOne,
|
||||
range(len(projects)),
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
)
|
||||
|
||||
if not all_branches:
|
||||
return
|
||||
@@ -76,7 +80,7 @@ class Prune(PagedCommand):
|
||||
print(
|
||||
"%s %-33s "
|
||||
% (
|
||||
branch.name == project.CurrentBranch and "*" or " ",
|
||||
branch.current and "*" or " ",
|
||||
branch.name,
|
||||
),
|
||||
end="",
|
||||
|
||||
+16
-27
@@ -16,7 +16,9 @@ import sys
|
||||
|
||||
from color import Coloring
|
||||
from command import Command
|
||||
from error import GitError
|
||||
from git_command import GitCommand
|
||||
from project import Project
|
||||
from repo_logging import RepoLogger
|
||||
|
||||
|
||||
@@ -30,6 +32,17 @@ class RebaseColoring(Coloring):
|
||||
self.fail = self.printer("fail", fg="red")
|
||||
|
||||
|
||||
def _ResolveOntoManifest(project: Project) -> str:
|
||||
"""Resolve project's revisionExpr to a local tracking branch.
|
||||
|
||||
Falls back to the raw revisionExpr if ToLocal fails or raises GitError.
|
||||
"""
|
||||
try:
|
||||
return project.GetRemote().ToLocal(project.revisionExpr)
|
||||
except GitError:
|
||||
return project.revisionExpr
|
||||
|
||||
|
||||
class Rebase(Command):
|
||||
COMMON = True
|
||||
helpSummary = "Rebase local branches on upstream branch"
|
||||
@@ -47,21 +60,18 @@ branch but need to incorporate new upstream changes "underneath" them.
|
||||
g.add_option(
|
||||
"-i",
|
||||
"--interactive",
|
||||
dest="interactive",
|
||||
action="store_true",
|
||||
help="interactive rebase (single project only)",
|
||||
)
|
||||
|
||||
p.add_option(
|
||||
"--fail-fast",
|
||||
dest="fail_fast",
|
||||
action="store_true",
|
||||
help="stop rebasing after first error is hit",
|
||||
)
|
||||
p.add_option(
|
||||
"-f",
|
||||
"--force-rebase",
|
||||
dest="force_rebase",
|
||||
action="store_true",
|
||||
help="pass --force-rebase to git rebase",
|
||||
)
|
||||
@@ -74,27 +84,23 @@ branch but need to incorporate new upstream changes "underneath" them.
|
||||
)
|
||||
p.add_option(
|
||||
"--autosquash",
|
||||
dest="autosquash",
|
||||
action="store_true",
|
||||
help="pass --autosquash to git rebase",
|
||||
)
|
||||
p.add_option(
|
||||
"--whitespace",
|
||||
dest="whitespace",
|
||||
action="store",
|
||||
metavar="WS",
|
||||
help="pass --whitespace to git rebase",
|
||||
)
|
||||
p.add_option(
|
||||
"--auto-stash",
|
||||
dest="auto_stash",
|
||||
action="store_true",
|
||||
help="stash local modifications before starting",
|
||||
)
|
||||
p.add_option(
|
||||
"-m",
|
||||
"--onto-manifest",
|
||||
dest="onto_manifest",
|
||||
action="store_true",
|
||||
help="rebase onto the manifest version instead of upstream "
|
||||
"HEAD (this helps to make sure the local tree stays "
|
||||
@@ -133,6 +139,8 @@ branch but need to incorporate new upstream changes "underneath" them.
|
||||
common_args.append("--autosquash")
|
||||
if opt.interactive:
|
||||
common_args.append("-i")
|
||||
if opt.auto_stash:
|
||||
common_args.append("--autostash")
|
||||
|
||||
config = self.manifest.manifestProject.config
|
||||
out = RebaseColoring(config)
|
||||
@@ -169,7 +177,7 @@ branch but need to incorporate new upstream changes "underneath" them.
|
||||
args = common_args[:]
|
||||
if opt.onto_manifest:
|
||||
args.append("--onto")
|
||||
args.append(project.revisionExpr)
|
||||
args.append(_ResolveOntoManifest(project))
|
||||
|
||||
args.append(upbranch.LocalMerge)
|
||||
|
||||
@@ -182,29 +190,10 @@ branch but need to incorporate new upstream changes "underneath" them.
|
||||
out.nl()
|
||||
out.flush()
|
||||
|
||||
needs_stash = False
|
||||
if opt.auto_stash:
|
||||
stash_args = ["update-index", "--refresh", "-q"]
|
||||
|
||||
if GitCommand(project, stash_args).Wait() != 0:
|
||||
needs_stash = True
|
||||
# Dirty index, requires stash...
|
||||
stash_args = ["stash"]
|
||||
|
||||
if GitCommand(project, stash_args).Wait() != 0:
|
||||
ret += 1
|
||||
continue
|
||||
|
||||
if GitCommand(project, args).Wait() != 0:
|
||||
ret += 1
|
||||
continue
|
||||
|
||||
if needs_stash:
|
||||
stash_args.append("pop")
|
||||
stash_args.append("--quiet")
|
||||
if GitCommand(project, stash_args).Wait() != 0:
|
||||
ret += 1
|
||||
|
||||
if ret:
|
||||
msg_fmt = "%d projects had errors"
|
||||
self.git_event_log.ErrorEvent(msg_fmt % (ret), msg_fmt)
|
||||
|
||||
@@ -54,7 +54,6 @@ need to be performed by an end-user.
|
||||
)
|
||||
g.add_option(
|
||||
"--repo-upgraded",
|
||||
dest="repo_upgraded",
|
||||
action="store_true",
|
||||
help=optparse.SUPPRESS_HELP,
|
||||
)
|
||||
|
||||
@@ -46,7 +46,6 @@ The '%prog' command stages files to prepare the next commit.
|
||||
g.add_option(
|
||||
"-i",
|
||||
"--interactive",
|
||||
dest="interactive",
|
||||
action="store_true",
|
||||
help="use interactive staging",
|
||||
)
|
||||
|
||||
+27
-18
@@ -18,10 +18,9 @@ from typing import NamedTuple
|
||||
from command import Command
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
from error import RepoExitError
|
||||
from git_command import git
|
||||
from git_command import IsValidBranchName
|
||||
from git_config import IsImmutable
|
||||
from progress import Progress
|
||||
from project import Project
|
||||
from repo_logging import RepoLogger
|
||||
|
||||
|
||||
@@ -29,7 +28,7 @@ logger = RepoLogger(__file__)
|
||||
|
||||
|
||||
class ExecuteOneResult(NamedTuple):
|
||||
project: Project
|
||||
project_idx: int
|
||||
error: Exception
|
||||
|
||||
|
||||
@@ -52,7 +51,6 @@ revision specified in the manifest.
|
||||
def _Options(self, p):
|
||||
p.add_option(
|
||||
"--all",
|
||||
dest="all",
|
||||
action="store_true",
|
||||
help="begin branch in all projects",
|
||||
)
|
||||
@@ -77,21 +75,23 @@ revision specified in the manifest.
|
||||
self.Usage()
|
||||
|
||||
nb = args[0]
|
||||
if not git.check_ref_format("heads/%s" % nb):
|
||||
if not IsValidBranchName(nb):
|
||||
self.OptionParser.error("'%s' is not a valid name" % nb)
|
||||
|
||||
def _ExecuteOne(self, revision, nb, project):
|
||||
@classmethod
|
||||
def _ExecuteOne(cls, revision, nb, default_revisionExpr, project_idx):
|
||||
"""Start one project."""
|
||||
# If the current revision is immutable, such as a SHA1, a tag or
|
||||
# a change, then we can't push back to it. Substitute with
|
||||
# dest_branch, if defined; or with manifest default revision instead.
|
||||
branch_merge = ""
|
||||
error = None
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
if IsImmutable(project.revisionExpr):
|
||||
if project.dest_branch:
|
||||
branch_merge = project.dest_branch
|
||||
else:
|
||||
branch_merge = self.manifest.default.revisionExpr
|
||||
branch_merge = default_revisionExpr
|
||||
|
||||
try:
|
||||
project.StartBranch(
|
||||
@@ -100,7 +100,7 @@ revision specified in the manifest.
|
||||
except Exception as e:
|
||||
logger.error("error: unable to checkout %s: %s", project.name, e)
|
||||
error = e
|
||||
return ExecuteOneResult(project, error)
|
||||
return ExecuteOneResult(project_idx, error)
|
||||
|
||||
def Execute(self, opt, args):
|
||||
nb = args[0]
|
||||
@@ -120,19 +120,28 @@ revision specified in the manifest.
|
||||
def _ProcessResults(_pool, pm, results):
|
||||
for result in results:
|
||||
if result.error:
|
||||
err_projects.append(result.project)
|
||||
project = all_projects[result.project_idx]
|
||||
err_projects.append(project)
|
||||
err.append(result.error)
|
||||
pm.update(msg="")
|
||||
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(self._ExecuteOne, opt.revision, nb),
|
||||
all_projects,
|
||||
callback=_ProcessResults,
|
||||
output=Progress(
|
||||
f"Starting {nb}", len(all_projects), quiet=opt.quiet
|
||||
),
|
||||
)
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = all_projects
|
||||
self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(
|
||||
self._ExecuteOne,
|
||||
opt.revision,
|
||||
nb,
|
||||
self.manifest.default.revisionExpr,
|
||||
),
|
||||
range(len(all_projects)),
|
||||
callback=_ProcessResults,
|
||||
output=Progress(
|
||||
f"Starting {nb}", len(all_projects), quiet=opt.quiet
|
||||
),
|
||||
chunksize=1,
|
||||
)
|
||||
|
||||
if err_projects:
|
||||
for p in err_projects:
|
||||
|
||||
+22
-12
@@ -54,6 +54,12 @@ project 'repo' on branch 'devwork':
|
||||
project repo/ branch devwork
|
||||
-m subcmds/status.py
|
||||
|
||||
If the branch is tracking an upstream branch, the number of commits
|
||||
ahead and/or behind is also shown:
|
||||
|
||||
project repo/ branch devwork [ahead 1, behind 2]
|
||||
-m subcmds/status.py
|
||||
|
||||
The first column explains how the staging area (index) differs from
|
||||
the last commit (HEAD). Its values are always displayed in upper
|
||||
case and have the following meanings:
|
||||
@@ -82,13 +88,13 @@ the following meanings:
|
||||
p.add_option(
|
||||
"-o",
|
||||
"--orphans",
|
||||
dest="orphans",
|
||||
action="store_true",
|
||||
help="include objects in working directory outside of repo "
|
||||
"projects",
|
||||
)
|
||||
|
||||
def _StatusHelper(self, quiet, local, project):
|
||||
@classmethod
|
||||
def _StatusHelper(cls, quiet, local, project_idx):
|
||||
"""Obtains the status for a specific project.
|
||||
|
||||
Obtains the status for a project, redirecting the output to
|
||||
@@ -99,12 +105,13 @@ the following meanings:
|
||||
local: a boolean, if True, the path is relative to the local
|
||||
(sub)manifest. If false, the path is relative to the outermost
|
||||
manifest.
|
||||
project: Project to get status of.
|
||||
project_idx: Project index to get status of.
|
||||
|
||||
Returns:
|
||||
The status of the project.
|
||||
"""
|
||||
buf = io.StringIO()
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
ret = project.PrintWorkTreeStatus(
|
||||
quiet=quiet, output_redir=buf, local=local
|
||||
)
|
||||
@@ -143,15 +150,18 @@ the following meanings:
|
||||
ret += 1
|
||||
return ret
|
||||
|
||||
counter = self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(
|
||||
self._StatusHelper, opt.quiet, opt.this_manifest_only
|
||||
),
|
||||
all_projects,
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
)
|
||||
with self.ParallelContext():
|
||||
self.get_parallel_context()["projects"] = all_projects
|
||||
counter = self.ExecuteInParallel(
|
||||
opt.jobs,
|
||||
functools.partial(
|
||||
self._StatusHelper, opt.quiet, opt.this_manifest_only
|
||||
),
|
||||
range(len(all_projects)),
|
||||
callback=_ProcessResults,
|
||||
ordered=True,
|
||||
chunksize=1,
|
||||
)
|
||||
|
||||
if not opt.quiet and len(all_projects) == counter:
|
||||
print("nothing to commit (working directory clean)")
|
||||
|
||||
+1614
-334
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user