mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-10-28 16:16:17 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
338ddb7d83 | ||
|
|
8c316d7c98 | ||
|
|
e3e8e8a3b0 | ||
|
|
cad9f591a4 | ||
|
|
24197b8ae1 | ||
|
|
bfb9869b89 | ||
|
|
887f9a9014 | ||
|
|
dc1811ba23 | ||
|
|
9aac80dafc | ||
|
|
f4ce9572d1 | ||
|
|
cc1f41d782 | ||
|
|
f7d252f5dc | ||
|
|
f95e7cd899 | ||
|
|
2c04b98c60 | ||
|
|
4212588c20 | ||
|
|
be06f8dd01 | ||
|
|
74d887ea86 | ||
|
|
29d644a5c0 | ||
|
|
a93741993b | ||
|
|
9991aa6177 | ||
|
|
d744eb15f3 | ||
|
|
f0124d0fd5 | ||
|
|
46e9866ca0 | ||
|
|
404aec7b53 | ||
|
|
a0bc2b14a3 | ||
|
|
3448df3954 | ||
|
|
a110680ff3 | ||
|
|
178b66ee3e | ||
|
|
beda65323e | ||
|
|
02e07525f9 | ||
|
|
2e8a3805bb | ||
|
|
277759b9e1 | ||
|
|
9593a76fd1 | ||
|
|
9293cda144 | ||
|
|
340b587e94 | ||
|
|
0a4e68f9d2 | ||
|
|
7bba4ee47e | ||
|
|
d53a4278f5 | ||
|
|
5d4149f32e | ||
|
|
cf499e312a | ||
|
|
cc0546bc71 | ||
|
|
ccf9ce39b8 | ||
|
|
9066409c2d | ||
|
|
2908f596c5 | ||
|
|
e1e215a14e | ||
|
|
175198b0aa | ||
|
|
310c99571f | ||
|
|
e7d4d97ffc | ||
|
|
8c7e0a683e | ||
|
|
f6f5946422 | ||
|
|
6321b26685 | ||
|
|
7bffc72b5b | ||
|
|
aaadd5da35 | ||
|
|
c9448f986e | ||
|
|
530258d08e | ||
|
|
c1566487a8 | ||
|
|
22f820de17 | ||
|
|
4c37f58806 | ||
|
|
c86ddc1628 | ||
|
|
c638b54e19 | ||
|
|
fb4a91060e | ||
|
|
4b3ada1781 | ||
|
|
a468ea7752 | ||
|
|
c2c330ba4a | ||
|
|
cc88be34d2 | ||
|
|
16cfb53e0a | ||
|
|
578b57c975 | ||
|
|
fe2c23f8e2 | ||
|
|
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 |
No files matched your search
@@ -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 (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.
|
||||
# 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>
|
||||
+2
-2
@@ -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.
|
||||
@@ -118,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.
|
||||
|
||||
|
||||
@@ -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,39 @@
|
||||
# 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
|
||||
}
|
||||
|
||||
# Default Multi-Perspective Code Review Agent
|
||||
configs {
|
||||
id: "git-repo-code-review-agent"
|
||||
display_name: "Code Review Agent"
|
||||
description: "Default multi-perspective review agent for git-repo."
|
||||
include_filters {
|
||||
project: "git-repo"
|
||||
path_regex: ".*"
|
||||
}
|
||||
use_default_subagents: true
|
||||
tags: "code-review-agent"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+121
-28
@@ -17,14 +17,17 @@ import multiprocessing
|
||||
import optparse
|
||||
import os
|
||||
import re
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
|
||||
from error import InvalidProjectGroupsError
|
||||
from error import NoSuchProjectError
|
||||
from error import RepoExitError
|
||||
import error
|
||||
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! "
|
||||
|
||||
@@ -43,7 +46,7 @@ WORKER_BATCH_SIZE = 32
|
||||
DEFAULT_LOCAL_JOBS = min(os.cpu_count(), 8)
|
||||
|
||||
|
||||
class UsageError(RepoExitError):
|
||||
class UsageError(error.RepoExitError):
|
||||
"""Exception thrown with invalid command usage."""
|
||||
|
||||
|
||||
@@ -61,6 +64,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
|
||||
@@ -101,6 +108,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."""
|
||||
|
||||
@@ -138,6 +150,20 @@ class Command:
|
||||
self._Options(self._optparse)
|
||||
return self._optparse
|
||||
|
||||
@staticmethod
|
||||
def _GetHelpForCpuJobCount(
|
||||
default_jobs: Optional[int] = None,
|
||||
) -> str:
|
||||
"""Return CPU-based job help, with an explicit default when needed.
|
||||
|
||||
Specify default_jobs when additional logic computes effective default.
|
||||
"""
|
||||
if GENERATE_MANPAGES:
|
||||
return "based on number of CPU cores"
|
||||
|
||||
default = "%default" if default_jobs is None else str(default_jobs)
|
||||
return f"{default}; based on number of CPU cores"
|
||||
|
||||
def _CommonOptions(self, p, opt_v=True):
|
||||
"""Initialize the option parser with common options.
|
||||
|
||||
@@ -162,11 +188,7 @@ class Command:
|
||||
)
|
||||
|
||||
if self.PARALLEL_JOBS is not None:
|
||||
default = "based on number of CPU cores"
|
||||
if not GENERATE_MANPAGES:
|
||||
# Only include active cpu count if we aren't generating man
|
||||
# pages.
|
||||
default = f"%default; {default}"
|
||||
default = self._GetHelpForCpuJobCount()
|
||||
p.add_option(
|
||||
"-j",
|
||||
"--jobs",
|
||||
@@ -237,6 +259,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.
|
||||
|
||||
@@ -370,7 +398,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.
|
||||
@@ -378,9 +406,15 @@ class Command:
|
||||
Args:
|
||||
args: a list of (case-insensitive) strings, projects to search for.
|
||||
manifest: an XmlManifest, the manifest to use, or None for default.
|
||||
groups: a string, the manifest groups in use.
|
||||
groups: a string, the manifest group selection to apply.
|
||||
Non-empty values apply to all candidate projects in this call.
|
||||
When empty or omitted, single-manifest calls use the selected
|
||||
manifest's effective groups; all-manifest calls use each
|
||||
candidate project's owning manifest's effective groups.
|
||||
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.
|
||||
@@ -398,22 +432,50 @@ class Command:
|
||||
all_projects_list = manifest.projects
|
||||
result = []
|
||||
|
||||
if not groups:
|
||||
groups = manifest.GetManifestGroupsStr()
|
||||
groups = [x for x in re.split(r"[,\s]+", groups) if x]
|
||||
def should_include_submodules(project: "Project") -> bool:
|
||||
if submodules_ok is None:
|
||||
return project.sync_s
|
||||
return submodules_ok
|
||||
|
||||
def parse_groups(value: str) -> List[str]:
|
||||
return [x for x in re.split(r"[,\s]+", value) if x]
|
||||
|
||||
if groups:
|
||||
groups_for_all_projects = parse_groups(groups)
|
||||
elif all_manifests:
|
||||
# In all-manifest mode, each project uses its owning
|
||||
# manifest's effective groups.
|
||||
groups_for_all_projects = None
|
||||
else:
|
||||
groups_for_all_projects = parse_groups(
|
||||
manifest.GetManifestGroupsStr()
|
||||
)
|
||||
|
||||
groups_by_manifest = {}
|
||||
|
||||
def matches_groups(project: "Project") -> bool:
|
||||
if groups_for_all_projects is not None:
|
||||
return project.MatchesGroups(groups_for_all_projects)
|
||||
|
||||
project_manifest = project.manifest
|
||||
if project_manifest not in groups_by_manifest:
|
||||
groups_by_manifest[project_manifest] = parse_groups(
|
||||
project_manifest.GetManifestGroupsStr()
|
||||
)
|
||||
|
||||
return project.MatchesGroups(groups_by_manifest[project_manifest])
|
||||
|
||||
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:
|
||||
if (missing_ok or project.Exists) and project.MatchesGroups(
|
||||
groups
|
||||
):
|
||||
if (missing_ok or project.Exists) and matches_groups(project):
|
||||
result.append(project)
|
||||
else:
|
||||
self._ResetPathToProjectMap(all_projects_list)
|
||||
@@ -427,7 +489,7 @@ class Command:
|
||||
for project in manifest.GetProjectsWithName(
|
||||
arg, all_manifests=all_manifests
|
||||
)
|
||||
if project.MatchesGroups(groups)
|
||||
if matches_groups(project)
|
||||
]
|
||||
|
||||
if not projects:
|
||||
@@ -446,7 +508,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():
|
||||
@@ -462,16 +524,16 @@ class Command:
|
||||
projects = [project]
|
||||
|
||||
if not projects:
|
||||
raise NoSuchProjectError(arg)
|
||||
raise error.NoSuchProjectError(arg)
|
||||
|
||||
for project in projects:
|
||||
if not missing_ok and not project.Exists:
|
||||
raise NoSuchProjectError(
|
||||
raise error.NoSuchProjectError(
|
||||
"%s (%s)"
|
||||
% (arg, project.RelPath(local=not all_manifests))
|
||||
)
|
||||
if not project.MatchesGroups(groups):
|
||||
raise InvalidProjectGroupsError(arg)
|
||||
if not matches_groups(project):
|
||||
raise error.InvalidProjectGroupsError(arg)
|
||||
|
||||
result.extend(projects)
|
||||
|
||||
@@ -481,7 +543,14 @@ class Command:
|
||||
result.sort(key=_getpath)
|
||||
return result
|
||||
|
||||
def FindProjects(self, args, inverse=False, all_manifests=False):
|
||||
def FindProjects(
|
||||
self,
|
||||
args: List[str],
|
||||
inverse: bool = False,
|
||||
all_manifests: bool = False,
|
||||
groups: Optional[str] = "",
|
||||
missing_ok: Optional[bool] = False,
|
||||
) -> List["Project"]:
|
||||
"""Find projects from command line arguments.
|
||||
|
||||
Args:
|
||||
@@ -491,10 +560,18 @@ class Command:
|
||||
all_manifests: a boolean, if True then all manifests and
|
||||
submanifests are used. If False, then only the local
|
||||
(sub)manifest is used.
|
||||
groups: a string specifying manifest groups. If empty or None, use
|
||||
each manifest's effective groups.
|
||||
missing_ok: a boolean, whether to allow missing projects.
|
||||
"""
|
||||
result = []
|
||||
patterns = [re.compile(r"%s" % a, re.IGNORECASE) for a in args]
|
||||
for project in self.GetProjects("", all_manifests=all_manifests):
|
||||
for project in self.GetProjects(
|
||||
"",
|
||||
groups=groups,
|
||||
missing_ok=missing_ok,
|
||||
all_manifests=all_manifests,
|
||||
):
|
||||
paths = [project.name, project.RelPath(local=not all_manifests)]
|
||||
for pattern in patterns:
|
||||
match = any(pattern.search(x) for x in paths)
|
||||
@@ -511,6 +588,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.
|
||||
|
||||
|
||||
+468
@@ -0,0 +1,468 @@
|
||||
#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'
|
||||
'config:Read and write per-user repo settings'
|
||||
'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"'
|
||||
;;
|
||||
config)
|
||||
_arguments \
|
||||
${common_opts} \
|
||||
'--type=[Validate and canonicalize the value as this type]:type:(bool int)' \
|
||||
'(-z --null)'{-z,--null}'[Terminate output with NUL]' \
|
||||
'1: :(get set unset list)' \
|
||||
'2: :_message "name"' \
|
||||
'3: :_message "value"'
|
||||
;;
|
||||
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]' \
|
||||
'(-s --smart-sync)'{-s,--smart-sync}'[Default to smart sync]' \
|
||||
'--no-smart-sync[Do not default to smart sync]' \
|
||||
'--head[Do not default to smart sync]' \
|
||||
'--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]' \
|
||||
'--no-smart-sync[Do not smart sync]' \
|
||||
'--head[Do not 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,70 @@
|
||||
# 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.
|
||||
|
||||
The checkout half of a sync has a counterpart, `repo.reprojectcmd`; see
|
||||
`docs/reproject-cmd.md`.
|
||||
|
||||
## 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 commit hash when the revision is already pinned to
|
||||
a commit (e.g. via a superproject or a commit SHA in the manifest), or
|
||||
empty when `REPO_RREV` is an unpinned branch or tag to be resolved by the
|
||||
fetch command.
|
||||
|
||||
## Contract
|
||||
|
||||
### Postconditions on exit 0
|
||||
|
||||
After the fetch command exits with status 0, `repo` expects the following
|
||||
postconditions to be met:
|
||||
|
||||
1. When the manifest revision names a branch or tag, the mapped local tracking
|
||||
ref (e.g. `refs/remotes/REPO_REMOTE/<branch>` for a branch, or
|
||||
`refs/tags/<tag>` for a tag) must resolve to a valid commit in the object
|
||||
store (`git rev-parse --verify <ref>^{commit}`), and must equal `REPO_TREV`
|
||||
if `REPO_TREV` was non-empty.
|
||||
2. `FETCH_HEAD` must resolve to the same target commit in the object store
|
||||
(`git rev-parse --verify FETCH_HEAD^{commit}`).
|
||||
3. The commit graph from the target commit must be reachable far enough to
|
||||
compute merge bases with local branches.
|
||||
|
||||
### Invariants
|
||||
|
||||
* The command should be idempotent; repeating the same fetch should be a
|
||||
no-op.
|
||||
* Only `FETCH_HEAD`, `refs/remotes/*`, and `refs/tags/*` 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.
|
||||
@@ -176,13 +176,14 @@ User controlled settings are initialized when running `repo init`.
|
||||
| repo.partialclone | `--partial-clone` | Create [partial git clones] |
|
||||
| repo.partialcloneexclude | `--partial-clone-exclude` | Comma-delimited list of project names (not paths) to exclude while using [partial git clones] |
|
||||
| repo.reference | `--reference` | Reference repo client checkout |
|
||||
| repo.smartsync | `--smart-sync` | Default to [Smart Sync](./smart-sync.md) when syncing |
|
||||
| repo.submodules | `--submodules` | Sync git submodules |
|
||||
| repo.superproject | `--use-superproject` | Sync [superproject] |
|
||||
| repo.worktree | `--worktree` | Use [git worktree] for checkouts |
|
||||
| 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
|
||||
@@ -253,6 +254,8 @@ setting `REPO_CONFIG_DIR`.
|
||||
|
||||
* `.repoconfig/`: Repo's per-user directory for all random config files/state.
|
||||
* `.repoconfig/config`: Per-user settings using [git-config] file format.
|
||||
Managed with `repo config`; see [repoconfig.md](./repoconfig.md) for the
|
||||
available settings.
|
||||
* `.repoconfig/keyring-version`: Cache file for checking if the gnupg subdir
|
||||
has all the same keys as the repo launcher. Used to avoid running gpg
|
||||
constantly as that can be quite slow.
|
||||
|
||||
+56
-22
@@ -46,17 +46,20 @@ following DTD:
|
||||
<!ATTLIST remote revision CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT default EMPTY>
|
||||
<!ATTLIST default remote IDREF #IMPLIED>
|
||||
<!ATTLIST default revision CDATA #IMPLIED>
|
||||
<!ATTLIST default dest-branch CDATA #IMPLIED>
|
||||
<!ATTLIST default upstream CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-j CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-c CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-s CDATA #IMPLIED>
|
||||
<!ATTLIST default sync-tags CDATA #IMPLIED>
|
||||
<!ATTLIST default remote IDREF #IMPLIED>
|
||||
<!ATTLIST default revision CDATA #IMPLIED>
|
||||
<!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>
|
||||
<!ATTLIST default sync-smartsync CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT manifest-server EMPTY>
|
||||
<!ATTLIST manifest-server url CDATA #REQUIRED>
|
||||
<!ATTLIST manifest-server url CDATA #REQUIRED>
|
||||
<!ATTLIST manifest-server helper CDATA #IMPLIED>
|
||||
|
||||
<!ELEMENT submanifest EMPTY>
|
||||
<!ATTLIST submanifest name ID #REQUIRED>
|
||||
@@ -72,18 +75,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>
|
||||
@@ -213,7 +217,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
|
||||
@@ -226,6 +232,8 @@ Attribute `sync-tags`: Set to false to only sync the given Git
|
||||
branch (specified in the `revision` attribute) rather than
|
||||
the other ref tags.
|
||||
|
||||
Attribute `sync-smartsync`: Set to true to enable smart sync by default.
|
||||
|
||||
|
||||
### Element manifest-server
|
||||
|
||||
@@ -235,6 +243,11 @@ XML RPC service.
|
||||
|
||||
See the [smart sync documentation](./smart-sync.md) for more details.
|
||||
|
||||
Attribute `url`: The URL of the manifest server.
|
||||
|
||||
Attribute `helper`: Optional name of a remote helper binary to execute to
|
||||
resolve proxying or authentication for the manifest server.
|
||||
|
||||
|
||||
### Element submanifest
|
||||
|
||||
@@ -386,6 +399,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.
|
||||
@@ -395,6 +424,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.
|
||||
|
||||
|
||||
+20
-2
@@ -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,9 @@ 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.
|
||||
"""
|
||||
```
|
||||
@@ -163,13 +180,14 @@ Example:
|
||||
The `post-sync.py` file should be defined like:
|
||||
|
||||
```py
|
||||
def main(repo_topdir=None, **kwargs):
|
||||
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,96 @@
|
||||
# repoconfig: User Configuration
|
||||
|
||||
Repo keeps a small number of settings that apply to the current user rather
|
||||
than to one checkout. They live in the per-user configuration file
|
||||
`~/.repoconfig/config`, which is separate from both git's own `~/.gitconfig`
|
||||
and the per-checkout settings stored in `.repo/manifests.git/config` (see
|
||||
[internal-fs-layout.md](./internal-fs-layout.md#Global-settings) for those).
|
||||
|
||||
[TOC]
|
||||
|
||||
## Location
|
||||
|
||||
The file is `.repoconfig/config` inside the user's home directory. The parent
|
||||
directory can be changed by setting the `REPO_CONFIG_DIR` environment variable,
|
||||
in which case repo reads `$REPO_CONFIG_DIR/.repoconfig/config` instead.
|
||||
|
||||
The file is created on demand; it's fine for it to be missing.
|
||||
|
||||
## Format
|
||||
|
||||
The file uses the [git-config] syntax. The easiest way to manage it is the
|
||||
`repo config` command:
|
||||
|
||||
```sh
|
||||
# Set a value.
|
||||
repo config set sync.smartsync true
|
||||
|
||||
# Read a value.
|
||||
repo config get sync.smartsync
|
||||
|
||||
# Remove a value.
|
||||
repo config unset sync.smartsync
|
||||
|
||||
# Show all settings.
|
||||
repo config list
|
||||
```
|
||||
|
||||
Like other repo subcommands, `repo config` must be run from inside a repo
|
||||
checkout. The file can also be edited by hand:
|
||||
|
||||
```ini
|
||||
[sync]
|
||||
smartsync = true
|
||||
```
|
||||
|
||||
Boolean settings accept `true`/`yes` and `false`/`no`.
|
||||
|
||||
## Settings
|
||||
|
||||
### alias.*
|
||||
|
||||
`alias.<name>` defines `repo <name>` as a shortcut for another subcommand,
|
||||
optionally with arguments, in the same way as git aliases:
|
||||
|
||||
```ini
|
||||
[alias]
|
||||
ss = sync -s
|
||||
st = status -o
|
||||
```
|
||||
|
||||
Aliases defined in a checkout's `.repo/config` take precedence over per-user
|
||||
ones. Existing subcommands cannot be overridden by an alias.
|
||||
|
||||
### sync
|
||||
|
||||
#### smartsync
|
||||
|
||||
`sync.smartsync` is a boolean that lets a user decide once, for all their
|
||||
checkouts, whether `repo sync` should [Smart Sync](./smart-sync.md) by
|
||||
default: `true` always Smart Syncs, as if `-s`/`--smart-sync` were passed;
|
||||
`false` never does by default, even if the manifest sets
|
||||
`sync-smartsync="true"`; unset follows the manifest.
|
||||
|
||||
The effective behavior is resolved in this order; the first match wins:
|
||||
|
||||
1. Command line: `-s`/`--smart-sync`, `--no-smart-sync`, or `-t`/`--smart-tag`.
|
||||
2. User config: `sync.smartsync` in `~/.repoconfig/config`.
|
||||
3. Manifest: `sync-smartsync="true"` on the `<default>` element.
|
||||
4. Otherwise, a regular ToT sync.
|
||||
|
||||
So `sync.smartsync=false` turns off the manifest-driven default, but an
|
||||
explicit `repo sync -s`, `repo sync -t TAG` or `repo smartsync` still works.
|
||||
Likewise `sync.smartsync=true` is overridden by `--no-smart-sync`.
|
||||
|
||||
A Smart Sync enabled by `sync.smartsync=true` is treated as an explicit
|
||||
request: it is subject to the same restrictions as `-s` (it is skipped when
|
||||
`-l`/`--local-only`, `-m`/`--manifest-name`, `-t`/`--smart-tag` or
|
||||
`--superproject-revision` is used), and a failure to obtain a manifest from
|
||||
the server fails the sync instead of silently falling back to ToT.
|
||||
|
||||
`sync.smartsync` does not affect `-l`/`--local-only`: a local-only sync keeps
|
||||
whatever `smart_sync_override.xml` the previous network sync left behind. See
|
||||
[smart-sync.md](./smart-sync.md#Client-Options) for details.
|
||||
|
||||
|
||||
[git-config]: https://git-scm.com/docs/git-config
|
||||
@@ -0,0 +1,126 @@
|
||||
# Reproject Command Contract
|
||||
|
||||
The `repo.reprojectcmd` configuration names a command that `repo sync` runs
|
||||
instead of Git to move a project's index and worktree to the tree of the
|
||||
target commit. It is the checkout-side counterpart of `repo.fetchcmd` (see
|
||||
`docs/fetch-cmd.md`): together they let an external tool take over both the
|
||||
network fetch and the materialization of a project. This is useful on
|
||||
virtualized filesystems that address content by hash, where a tree can be
|
||||
materialized far faster than `git checkout` can write every file.
|
||||
|
||||
The command only materializes the tree. `repo` then makes the ref write that
|
||||
Git would have made, using `git update-ref`.
|
||||
|
||||
## Configuration
|
||||
|
||||
To use this feature, set the following in `.repo/manifests.git/config`:
|
||||
```ini
|
||||
[repo]
|
||||
reprojectcmd = "your custom command here"
|
||||
uselocalgitdirs = true
|
||||
```
|
||||
Setting `repo.reprojectcmd` **requires** `repo.uselocalgitdirs` to be set to
|
||||
`true`.
|
||||
|
||||
For reference, this command does with Git what `repo` would otherwise do
|
||||
itself:
|
||||
```ini
|
||||
[repo]
|
||||
reprojectcmd = "git -C $REPO_PATH read-tree -m -u $REPO_TREV"
|
||||
uselocalgitdirs = true
|
||||
```
|
||||
The one-tree merge applies the change to the target, keeps local changes to
|
||||
every other path, and refuses to overwrite a modified or untracked file, so it
|
||||
enforces the preconditions below by itself. It also works for a project that
|
||||
has nothing checked out yet.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The command is executed in a subshell, from the root of the client, populated
|
||||
with standard project-context environment variables. For details on standard
|
||||
variables (such as `REPO_PROJECT`, `REPO_PATH`, `REPO_REMOTE`, etc.), see the
|
||||
Environment section in `repo help forall` or `subcmds/forall.py`.
|
||||
|
||||
The variables the command typically needs are:
|
||||
|
||||
* `REPO_PATH`: The project path relative to the root of the client.
|
||||
* `REPO_TREV`: The target revision resolved to a full commit hash. Match this
|
||||
commit's tree.
|
||||
|
||||
There is no force mode: a project that would need one never reaches the
|
||||
command (see the preconditions below).
|
||||
|
||||
## When the command runs
|
||||
|
||||
`repo sync` already classifies each project and picks a Git operation. The
|
||||
command replaces the three that are a materialization of a target tree:
|
||||
|
||||
1. The checkout that detaches HEAD at the target. This is the common case: a
|
||||
project on a detached HEAD, a project on a branch that does not track
|
||||
upstream, and `repo sync -d`.
|
||||
2. The fast-forward of the checked out branch to the target.
|
||||
3. The hard reset of the checked out branch to the target, when the commits
|
||||
it carried were dropped upstream.
|
||||
|
||||
After the command exits 0, `repo` writes the ref itself: it detaches `HEAD` at
|
||||
`REPO_TREV`, or moves the checked out branch to `REPO_TREV`.
|
||||
|
||||
The command is **not** run:
|
||||
|
||||
* When `HEAD` already names `REPO_TREV`.
|
||||
* At the fast-forward step when `HEAD` is ahead of `REPO_TREV`, where Git's
|
||||
merge would be a no-op.
|
||||
* For a rebase. A branch carrying local commits has them replayed onto the
|
||||
target by `git rebase`, which is not a materialization of a target tree.
|
||||
* For `MetaProject`s (i.e. the internal `repo` repository itself at
|
||||
`.repo/repo` and the `manifests` repository at `.repo/manifests`).
|
||||
|
||||
## Contract
|
||||
|
||||
### Preconditions
|
||||
|
||||
Before invoking the command, `repo` ensures that:
|
||||
|
||||
* The index has no staged changes (the index matches `HEAD`, or is empty on an unborn `HEAD`).
|
||||
* No rebase, cherry-pick, merge, or revert is in progress.
|
||||
|
||||
Detecting collisions with untracked files or unstaged working-tree modifications is the responsibility of the reproject command itself (e.g. via `git read-tree -m -u $REPO_TREV` or a custom virtual filesystem checkout tool). If local changes collide with the target tree, the command must abort with a non-zero exit code. Local modifications and untracked files outside the diff between `HEAD` and `REPO_TREV` must be preserved.
|
||||
|
||||
### Postconditions on exit 0
|
||||
|
||||
After the command exits with status 0, `repo` expects the following
|
||||
postconditions to be met:
|
||||
|
||||
1. `git diff-index --quiet --cached REPO_TREV^{tree}` exits 0 (the index
|
||||
matches the target tree).
|
||||
2. `HEAD` still names what it did before the command, and its resolved commit
|
||||
object ID has not changed.
|
||||
|
||||
### Invariants
|
||||
|
||||
The command may modify the worktree and the index, and may write project-local
|
||||
Git config. The command must:
|
||||
|
||||
* Apply the change from `HEAD`'s tree to `REPO_TREV`'s tree and leave every
|
||||
other path alone. Local modifications and untracked files outside that
|
||||
change must survive: the command applies a diff, it does not reset the
|
||||
tree.
|
||||
* Not write any ref, including `HEAD` and `ORIG_HEAD`. `repo` owns every ref
|
||||
write.
|
||||
* Not create or replace `.git/`, and not touch anything under `.repo/`.
|
||||
* Not require the Git remote, to preserve `repo sync --local-only`.
|
||||
* Be idempotent. Running it twice on the same target is a no-op.
|
||||
|
||||
### Failure
|
||||
|
||||
* A non-zero exit status, a failed precondition or a failed postcondition
|
||||
fails that project's sync, and the command's or Git's output is surfaced
|
||||
to the user.
|
||||
* Other projects continue, and `repo sync` exits non-zero.
|
||||
|
||||
## Limitations
|
||||
|
||||
Nested projects are out of scope: a project whose path lies inside another
|
||||
project's path, a `<project>` nested in another `<project>` in the manifest,
|
||||
and a submodule discovered with `sync-s` or `--recurse-submodules`. `repo sync`
|
||||
fails if the manifest has one while `repo.reprojectcmd` is set.
|
||||
+150
-5
@@ -27,13 +27,44 @@ 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" />
|
||||
<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`
|
||||
@@ -64,6 +95,36 @@ example would do. Google's internal server uses Python's
|
||||
|
||||
The manifest server should implement the following RPC methods.
|
||||
|
||||
### GetApprovedManifestV2
|
||||
|
||||
> `GetApprovedManifestV2(request: dict) -> dict`
|
||||
|
||||
Like [GetApprovedManifest](#GetApprovedManifest), but takes a single struct of
|
||||
named options so new options can be added without changing the method signature.
|
||||
Unset options are omitted from the struct. Servers should ignore options they do
|
||||
not understand.
|
||||
|
||||
Request options:
|
||||
|
||||
* `branch` (`str`, required): Same as in `GetApprovedManifest`.
|
||||
* `target` (`str`, optional): Same as in `GetApprovedManifest`.
|
||||
* `best_effort` (`bool`, optional, default `False`): The client can proceed
|
||||
without a manifest, so the server may decline the request (e.g. during a
|
||||
staged rollout or to shed load). Repo sets this for Smart Syncs enabled by
|
||||
the manifest's `sync-smartsync` default, and falls back to a regular ToT
|
||||
sync if the request is declined. Servers must not decline requests without
|
||||
this option for these reasons.
|
||||
|
||||
Response fields:
|
||||
|
||||
* `success` (`bool`): Whether a manifest was returned.
|
||||
* `manifest_xml` (`str`): The manifest. Required if `success` is true.
|
||||
* `error_message` (`str`): Why the request failed or was declined. Repo shows
|
||||
it to the user like `GetApprovedManifest` errors.
|
||||
|
||||
If the server does not implement this method, repo calls `GetApprovedManifest`
|
||||
instead and drops the options it cannot express.
|
||||
|
||||
### GetApprovedManifest
|
||||
|
||||
> `GetApprovedManifest(branch: str, target: Optional[str]) -> str`
|
||||
@@ -107,12 +168,79 @@ 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
|
||||
With `-l`/`--local-only` and no explicit `-s`/`--smart-sync` or
|
||||
`-t`/`--smart-tag`, repo does not contact the manifest server. Instead, it keeps
|
||||
using the existing `smart_sync_override.xml`, if there is one, whether it came
|
||||
from an explicit `-s`/`--smart-sync` or from the manifest's `sync-smartsync`
|
||||
default. This way, `repo sync -n` followed by `repo sync -l` ends up on the same
|
||||
revisions as the equivalent single `repo sync` (e.g. `repo sync -s -n` +
|
||||
`repo sync -l` matches `repo sync -s`). Pass `--no-smart-sync` to discard it and
|
||||
use the default manifest instead.
|
||||
|
||||
Repo will call `GetApprovedManifest(branch[, target])`.
|
||||
`--no-manifest-update` only skips updating the manifest checkout. It does not
|
||||
affect Smart Sync: the default Smart Sync from `sync-smartsync` (see below)
|
||||
still runs, and an existing `smart_sync_override.xml` is replaced or removed as
|
||||
in a regular sync.
|
||||
|
||||
### `repo init --[no-]smart-sync`
|
||||
|
||||
Users can configure Smart Sync as the default for a checkout by passing
|
||||
`-s`/`--smart-sync` (or `--no-smart-sync`/`--head`) to `repo init` (this can
|
||||
also be re-run in an existing checkout to change the setting).
|
||||
|
||||
When resolving whether to perform a Smart Sync by default, `repo sync` uses the
|
||||
following precedence order (highest to lowest):
|
||||
|
||||
1. Per-invocation CLI flags (`-s`/`--smart-sync`, `--no-smart-sync`/`--head`,
|
||||
or target selection flags like `-t`, `-m`, `--superproject-revision`, `-l`).
|
||||
2. Checkout `repo init` setting (`repo init --[no-]smart-sync`).
|
||||
3. [Per-user `sync.smartsync` setting](#Per_user-default-sync_smartsync).
|
||||
4. Manifest [`sync-smartsync`](#sync-smartsync-manifest-attribute) setting.
|
||||
|
||||
Running `repo init --no-smart-sync` (or `repo init --head`) disables default
|
||||
Smart Sync for the checkout even when enabled by the user setting or manifest.
|
||||
As with the [manifest setting](#sync-smartsync-manifest-attribute), a
|
||||
checkout-enabled default Smart Sync falls back to a regular ToT sync if the
|
||||
manifest does not define a `<manifest-server>` or the manifest server cannot
|
||||
provide a usable manifest.
|
||||
|
||||
### sync-smartsync (Manifest Attribute)
|
||||
|
||||
The manifest can enable Smart Sync by default for all `repo sync` invocations
|
||||
by setting `sync-smartsync="true"` on the `<default>` element:
|
||||
|
||||
```xml
|
||||
<default sync-smartsync="true" ... />
|
||||
```
|
||||
|
||||
When enabled in the manifest, `repo sync` will automatically perform a Smart
|
||||
Sync unless explicitly overridden on the command line. Since the user did not
|
||||
ask for a Smart Sync, any failure to obtain a manifest from the manifest server
|
||||
(e.g. the server is unreachable, or it returns a manifest that cannot be parsed)
|
||||
is silently ignored and `repo sync` falls back to a regular ToT sync; pass
|
||||
`--verbose` to see the reason. Repo marks these requests as `best_effort` (see
|
||||
[GetApprovedManifestV2](#GetApprovedManifestV2)), so the server may also decline
|
||||
them, e.g. during a staged rollout. This is only a fallback for the
|
||||
manifest-driven default: an explicit `-s`/`--smart-sync` or `-t`/`--smart-tag`
|
||||
still fails the sync, as before. Options that specify an explicit target or
|
||||
manifest source (such as `-t`/`--smart-tag`, `-m`/`--manifest-name` or
|
||||
`--superproject-revision`) will also disable the default Smart Sync behavior.
|
||||
|
||||
Users can override this manifest default on their workstation with the
|
||||
`sync.smartsync` user setting; see
|
||||
[Per-user default](#Per_user-default-sync_smartsync) below.
|
||||
|
||||
### --smart-sync / -s
|
||||
|
||||
Explicitly enables Smart Sync. Repo will call
|
||||
`GetApprovedManifestV2({branch[, target]})`, or
|
||||
`GetApprovedManifest(branch[, target])` if the server does not implement it.
|
||||
|
||||
The `branch` is determined by the current manifest branch as specified by
|
||||
`--manifest-branch=BRANCH` when running `repo init`.
|
||||
`--manifest-branch=BRANCH` when running `repo init`. If `${SYNC_BRANCH}` is
|
||||
defined and non-empty, its value is sent instead. This allows selecting a
|
||||
branch other than the one the checkout tracks, e.g. a server-side (CI) branch
|
||||
name that builds from the same manifest branch.
|
||||
|
||||
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
|
||||
@@ -124,6 +252,23 @@ match the settings Android build environments automatically setup.
|
||||
3. `${TARGET_PRODUCT}-${TARGET_BUILD_VARIANT}`: If these variables are all
|
||||
defined, then they are merged with `-` and used.
|
||||
|
||||
### --smart-tag=TAG
|
||||
### --no-smart-sync / --head
|
||||
|
||||
Explicitly disables Smart Sync, overriding any `repo.smartsync` git config or
|
||||
`sync-smartsync="true"` setting declared in the manifest `<default>` element.
|
||||
|
||||
### Per-user default (sync.smartsync)
|
||||
|
||||
A user can decide once, for all their checkouts, whether `repo sync` should
|
||||
Smart Sync by default by setting the boolean `sync.smartsync` key in their
|
||||
[per-user repo config](./repoconfig.md#smartsync).
|
||||
|
||||
Command line flags (`-s`, `--no-smart-sync`, `-t`) override the user setting,
|
||||
which in turn overrides the manifest's `sync-smartsync` default. A Smart Sync
|
||||
enabled this way is treated as an explicit request, so it is subject to the
|
||||
same restrictions as `-s` and fails the sync if no manifest can be obtained
|
||||
from the server instead of silently falling back to ToT.
|
||||
|
||||
### --smart-tag=TAG / -t
|
||||
|
||||
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`:
|
||||
|
||||
@@ -18,7 +18,7 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from error import EditorError
|
||||
import error
|
||||
import platform_utils
|
||||
|
||||
|
||||
@@ -104,9 +104,11 @@ least one of these before using this command.""", # noqa: E501
|
||||
try:
|
||||
rc = subprocess.Popen(args, shell=shell).wait()
|
||||
except OSError as e:
|
||||
raise EditorError(f"editor failed, {str(e)}: {editor} {path}")
|
||||
raise error.EditorError(
|
||||
f"editor failed, {str(e)}: {editor} {path}"
|
||||
)
|
||||
if rc != 0:
|
||||
raise EditorError(
|
||||
raise error.EditorError(
|
||||
"editor failed with exit status %d: %s %s"
|
||||
% (rc, editor, path)
|
||||
)
|
||||
|
||||
@@ -19,10 +19,10 @@ import sys
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import urlopen
|
||||
|
||||
from error import RepoExitError
|
||||
import error
|
||||
|
||||
|
||||
class FetchFileError(RepoExitError):
|
||||
class FetchFileError(error.RepoExitError):
|
||||
"""Exit error when fetch_file fails."""
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
+80
-30
@@ -20,15 +20,11 @@ import subprocess
|
||||
import sys
|
||||
from typing import Any, Optional
|
||||
|
||||
from error import GitError
|
||||
from error import RepoExitError
|
||||
from git_refs import HEAD
|
||||
import error
|
||||
from git_trace2_event_log_base import BaseEventLog
|
||||
import platform_utils
|
||||
from repo_logging import RepoLogger
|
||||
from repo_trace import IsTrace
|
||||
from repo_trace import REPO_TRACE
|
||||
from repo_trace import Trace
|
||||
import repo_logging
|
||||
import repo_trace
|
||||
from wrapper import Wrapper
|
||||
|
||||
|
||||
@@ -44,11 +40,11 @@ GIT_ERROR_STDOUT_LINES = 1
|
||||
GIT_ERROR_STDERR_LINES = 10
|
||||
INVALID_GIT_EXIT_CODE = 126
|
||||
|
||||
logger = RepoLogger(__file__)
|
||||
logger = repo_logging.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 +67,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 +92,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 +105,7 @@ def RepoSourceVersion():
|
||||
ver = ver[1:]
|
||||
else:
|
||||
ver = "unknown"
|
||||
setattr(RepoSourceVersion, "version", ver)
|
||||
RepoSourceVersion.version = ver
|
||||
|
||||
return ver
|
||||
|
||||
@@ -271,6 +280,7 @@ class GitCommand:
|
||||
bare=False,
|
||||
input=None,
|
||||
capture_stdout=False,
|
||||
capture_stdout_bytes: bool = False,
|
||||
capture_stderr=False,
|
||||
merge_output=False,
|
||||
disable_editor=False,
|
||||
@@ -292,6 +302,12 @@ class GitCommand:
|
||||
self.cmdv = cmdv
|
||||
self.verify_command = verify_command
|
||||
self.stdout, self.stderr = None, None
|
||||
if capture_stdout_bytes:
|
||||
if merge_output:
|
||||
raise ValueError(
|
||||
"capture_stdout_bytes cannot be combined with merge_output"
|
||||
)
|
||||
capture_stdout = True
|
||||
|
||||
# Git on Windows wants its paths only using / for reliability.
|
||||
if platform_utils.isWindows():
|
||||
@@ -316,11 +332,17 @@ class GitCommand:
|
||||
|
||||
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 command_name in ("fetch", "clone", "push"):
|
||||
# Need to use the --progress flag for fetch/clone/push 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:
|
||||
if (
|
||||
"--progress" not in cmdv
|
||||
and "--no-progress" not in cmdv
|
||||
and "--quiet" not in cmdv
|
||||
and "-q" not in cmdv
|
||||
):
|
||||
command.append("--progress")
|
||||
command.extend(cmdv[1:])
|
||||
|
||||
@@ -335,6 +357,7 @@ class GitCommand:
|
||||
command,
|
||||
env,
|
||||
capture_stdout=capture_stdout,
|
||||
capture_stdout_bytes=capture_stdout_bytes,
|
||||
capture_stderr=capture_stderr,
|
||||
merge_output=merge_output,
|
||||
ssh_proxy=ssh_proxy,
|
||||
@@ -368,6 +391,7 @@ class GitCommand:
|
||||
command,
|
||||
env,
|
||||
capture_stdout=False,
|
||||
capture_stdout_bytes: bool = False,
|
||||
capture_stderr=False,
|
||||
merge_output=False,
|
||||
ssh_proxy=None,
|
||||
@@ -400,6 +424,10 @@ class GitCommand:
|
||||
# See go/tee-repo-stderr for more context.
|
||||
tee_stderr = False
|
||||
kwargs = {"encoding": "utf-8", "errors": "backslashreplace"}
|
||||
if capture_stdout_bytes:
|
||||
kwargs = {}
|
||||
if isinstance(input, str):
|
||||
input = input.encode("utf-8", "surrogateescape")
|
||||
if not (stdin or stdout or stderr):
|
||||
tee_stderr = True
|
||||
# stderr will be written back to sys.stderr even though it is
|
||||
@@ -408,7 +436,7 @@ class GitCommand:
|
||||
kwargs = {}
|
||||
|
||||
dbg = ""
|
||||
if IsTrace():
|
||||
if repo_trace.IsTrace():
|
||||
global LAST_CWD
|
||||
global LAST_GITDIR
|
||||
|
||||
@@ -445,7 +473,7 @@ class GitCommand:
|
||||
elif stderr == subprocess.STDOUT:
|
||||
dbg += " 2>&1"
|
||||
|
||||
with Trace(
|
||||
with repo_trace.Trace(
|
||||
"git command %s %s with debug: %s", LAST_GITDIR, command, dbg
|
||||
):
|
||||
try:
|
||||
@@ -478,6 +506,10 @@ class GitCommand:
|
||||
self.stderr = self._Tee(p.stderr, sys.stderr)
|
||||
else:
|
||||
self.stdout, self.stderr = p.communicate(input=input)
|
||||
if capture_stdout_bytes and isinstance(self.stderr, bytes):
|
||||
self.stderr = self.stderr.decode(
|
||||
"utf-8", "backslashreplace"
|
||||
).replace("\r\n", "\n")
|
||||
finally:
|
||||
if ssh_proxy:
|
||||
ssh_proxy.remove_client(p)
|
||||
@@ -518,7 +550,7 @@ class GitCommand:
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
for key in (
|
||||
REPO_TRACE,
|
||||
repo_trace.REPO_TRACE,
|
||||
GIT_DIR,
|
||||
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
||||
"GIT_OBJECT_DIRECTORY",
|
||||
@@ -529,17 +561,35 @@ class GitCommand:
|
||||
env.pop(key, None)
|
||||
return env
|
||||
|
||||
def VerifyCommand(self):
|
||||
def VerifyCommand(self) -> None:
|
||||
if self.rc == 0:
|
||||
return None
|
||||
stdout = (
|
||||
"\n".join(self.stdout.split("\n")[:GIT_ERROR_STDOUT_LINES])
|
||||
if self.stdout
|
||||
else None
|
||||
)
|
||||
raw_stdout = self.stdout
|
||||
if isinstance(raw_stdout, bytes):
|
||||
first_records = re.split(
|
||||
rb"\r\n|[\r\n\0]", raw_stdout, maxsplit=GIT_ERROR_STDOUT_LINES
|
||||
)[:GIT_ERROR_STDOUT_LINES]
|
||||
stdout = (
|
||||
"\n".join(
|
||||
r.decode("utf-8", "backslashreplace") for r in first_records
|
||||
)
|
||||
if raw_stdout
|
||||
else None
|
||||
)
|
||||
elif raw_stdout:
|
||||
first_records = re.split(
|
||||
r"\r\n|[\r\n\0]", raw_stdout, maxsplit=GIT_ERROR_STDOUT_LINES
|
||||
)[:GIT_ERROR_STDOUT_LINES]
|
||||
stdout = "\n".join(first_records)
|
||||
else:
|
||||
stdout = None
|
||||
|
||||
raw_stderr = self.stderr
|
||||
if isinstance(raw_stderr, bytes):
|
||||
raw_stderr = raw_stderr.decode("utf-8", "backslashreplace")
|
||||
stderr = (
|
||||
"\n".join(self.stderr.split("\n")[:GIT_ERROR_STDERR_LINES])
|
||||
if self.stderr
|
||||
"\n".join(raw_stderr.split("\n")[:GIT_ERROR_STDERR_LINES])
|
||||
if raw_stderr
|
||||
else None
|
||||
)
|
||||
project = self.project.name if self.project else None
|
||||
@@ -557,14 +607,14 @@ class GitCommand:
|
||||
return self.rc
|
||||
|
||||
|
||||
class GitRequireError(RepoExitError):
|
||||
class GitRequireError(error.RepoExitError):
|
||||
"""Error raised when git version is unavailable or invalid."""
|
||||
|
||||
def __init__(self, message, exit_code: int = INVALID_GIT_EXIT_CODE):
|
||||
super().__init__(message, exit_code=exit_code)
|
||||
|
||||
|
||||
class GitCommandError(GitError):
|
||||
class GitCommandError(error.GitError):
|
||||
"""
|
||||
Error raised from a failed git command.
|
||||
Note that GitError can refer to any Git related error (e.g. branch not
|
||||
@@ -612,7 +662,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:
|
||||
@@ -644,7 +694,7 @@ class GitCommandError(GitError):
|
||||
return string
|
||||
|
||||
|
||||
class GitPopenCommandError(GitError):
|
||||
class GitPopenCommandError(error.GitError):
|
||||
"""
|
||||
Error raised when subprocess.Popen fails for a GitCommand
|
||||
"""
|
||||
+106
-56
@@ -26,41 +26,88 @@ from typing import Union
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
from error import GitError
|
||||
from error import UploadError
|
||||
from git_command import GitCommand
|
||||
from git_refs import R_CHANGES
|
||||
from git_refs import R_HEADS
|
||||
from git_refs import R_TAGS
|
||||
import error
|
||||
import git_command
|
||||
import git_refs
|
||||
import platform_utils
|
||||
from repo_trace import Trace
|
||||
import repo_trace
|
||||
|
||||
|
||||
# Prefix that is prepended to all the keys of SyncAnalysisState's data
|
||||
# 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)
|
||||
return rev.startswith(git_refs.R_CHANGES)
|
||||
|
||||
|
||||
def IsId(rev):
|
||||
return ID_RE.match(rev)
|
||||
def IsId(rev: str) -> bool:
|
||||
return bool(ID_RE.match(rev))
|
||||
|
||||
|
||||
def IsTag(rev):
|
||||
return rev.startswith(R_TAGS)
|
||||
return rev.startswith(git_refs.R_TAGS)
|
||||
|
||||
|
||||
def IsImmutable(rev):
|
||||
return IsChange(rev) or IsId(rev) or IsTag(rev)
|
||||
|
||||
|
||||
def ParseBoolean(value: str) -> Union[bool, None]:
|
||||
"""Parses a git config boolean value.
|
||||
|
||||
Accepts the same spellings as `git config --type=bool`.
|
||||
https://git-scm.com/docs/git-config#Documentation/git-config.txt-boolean
|
||||
|
||||
Returns:
|
||||
True/False, or None if |value| is not a valid boolean.
|
||||
"""
|
||||
v = value.strip().lower()
|
||||
if v in ("true", "yes", "on", "1"):
|
||||
return True
|
||||
if v in ("false", "no", "off", "0"):
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
def ParseInt(value: str) -> Union[int, None]:
|
||||
"""Parses a git config integer value.
|
||||
|
||||
Accepts the same syntax as `git config --type=int`: an optional k, m or
|
||||
g suffix (multiples of 1024) and hex with a 0x prefix.
|
||||
https://git-scm.com/docs/git-config#Documentation/git-config.txt-integer
|
||||
|
||||
Returns:
|
||||
The integer, or None if |value| is not a valid integer.
|
||||
"""
|
||||
v = value.strip().lower()
|
||||
|
||||
mult = 1
|
||||
if v.endswith("k"):
|
||||
v = v[:-1]
|
||||
mult = 1024
|
||||
elif v.endswith("m"):
|
||||
v = v[:-1]
|
||||
mult = 1024 * 1024
|
||||
elif v.endswith("g"):
|
||||
v = v[:-1]
|
||||
mult = 1024 * 1024 * 1024
|
||||
|
||||
base = 10
|
||||
if v.startswith("0x"):
|
||||
base = 16
|
||||
|
||||
try:
|
||||
return int(v, base=base) * mult
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _key(name):
|
||||
parts = name.split(".")
|
||||
if len(parts) < 2:
|
||||
@@ -111,7 +158,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
|
||||
@@ -152,32 +199,15 @@ class GitConfig:
|
||||
v = self.GetString(name)
|
||||
if v is None:
|
||||
return None
|
||||
v = v.strip()
|
||||
|
||||
mult = 1
|
||||
if v.endswith("k"):
|
||||
v = v[:-1]
|
||||
mult = 1024
|
||||
elif v.endswith("m"):
|
||||
v = v[:-1]
|
||||
mult = 1024 * 1024
|
||||
elif v.endswith("g"):
|
||||
v = v[:-1]
|
||||
mult = 1024 * 1024 * 1024
|
||||
|
||||
base = 10
|
||||
if v.startswith("0x"):
|
||||
base = 16
|
||||
|
||||
try:
|
||||
return int(v, base=base) * mult
|
||||
except ValueError:
|
||||
ret = ParseInt(v)
|
||||
if ret is None:
|
||||
print(
|
||||
f"warning: expected {name} to represent an integer, got {v} "
|
||||
"instead",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return None
|
||||
return ret
|
||||
|
||||
def DumpConfigDict(self):
|
||||
"""Returns the current configuration dict.
|
||||
@@ -197,24 +227,25 @@ class GitConfig:
|
||||
def GetBoolean(self, name: str) -> Union[bool, None]:
|
||||
"""Returns a boolean from the configuration file.
|
||||
|
||||
This follows the git config syntax.
|
||||
|
||||
Returns:
|
||||
None: The value was not defined, or is not a boolean.
|
||||
True: The value was set to true or yes.
|
||||
False: The value was set to false or no.
|
||||
True: The value was set to true, yes, on or 1.
|
||||
False: The value was set to false, no, off or 0.
|
||||
"""
|
||||
v = self.GetString(name)
|
||||
if v is None:
|
||||
return None
|
||||
v = v.lower()
|
||||
if v in ("true", "yes"):
|
||||
return True
|
||||
if v in ("false", "no"):
|
||||
return False
|
||||
print(
|
||||
f"warning: expected {name} to represent a boolean, got {v} instead",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return None
|
||||
|
||||
ret = ParseBoolean(v)
|
||||
if ret is None:
|
||||
print(
|
||||
f"warning: expected {name} to represent a boolean, got {v} "
|
||||
"instead",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return ret
|
||||
|
||||
def SetBoolean(self, name, value):
|
||||
"""Set the truthy value for a key."""
|
||||
@@ -266,6 +297,13 @@ class GitConfig:
|
||||
except KeyError:
|
||||
old = []
|
||||
|
||||
if self.file != self._SYSTEM_CONFIG:
|
||||
# `git config --file` does not create missing parent directories
|
||||
# (e.g. ~/.repoconfig/ on first use).
|
||||
parent_dir = os.path.dirname(self.file)
|
||||
if parent_dir:
|
||||
os.makedirs(parent_dir, exist_ok=True)
|
||||
|
||||
if value is None:
|
||||
if old:
|
||||
del self._cache[key]
|
||||
@@ -387,7 +425,7 @@ class GitConfig:
|
||||
except OSError:
|
||||
return None
|
||||
try:
|
||||
with Trace(": parsing %s", self.file):
|
||||
with repo_trace.Trace(": parsing %s", self.file):
|
||||
with open(self._json) as fd:
|
||||
return json.load(fd)
|
||||
except (OSError, ValueError):
|
||||
@@ -434,16 +472,23 @@ class GitConfig:
|
||||
command = ["config", "--file", self.file, "--includes"]
|
||||
command.extend(args)
|
||||
|
||||
p = GitCommand(None, command, capture_stdout=True, capture_stderr=True)
|
||||
p = git_command.GitCommand(
|
||||
None, command, capture_stdout=True, capture_stderr=True
|
||||
)
|
||||
if p.Wait() == 0:
|
||||
return p.stdout
|
||||
else:
|
||||
raise GitError(f"git config {str(args)}: {p.stderr}")
|
||||
raise error.GitError(f"git {' '.join(command)}: {p.stderr}")
|
||||
|
||||
|
||||
class RepoConfig(GitConfig):
|
||||
"""User settings for repo itself."""
|
||||
|
||||
# Keep separate caches from GitConfig, otherwise ForUser()/ForSystem()
|
||||
# would return the already-cached ~/.gitconfig instance of the parent.
|
||||
_ForUser = None
|
||||
_ForSystem = None
|
||||
|
||||
@staticmethod
|
||||
def _getUserConfig():
|
||||
repo_config_dir = os.getenv("REPO_CONFIG_DIR", os.path.expanduser("~"))
|
||||
@@ -671,11 +716,13 @@ class Remote:
|
||||
userEmail, host, port
|
||||
)
|
||||
except urllib.error.HTTPError as e:
|
||||
raise UploadError(f"{self.review}: {str(e)}")
|
||||
raise error.UploadError(f"{self.review}: {str(e)}")
|
||||
except urllib.error.URLError as e:
|
||||
raise UploadError(f"{self.review}: {str(e)}")
|
||||
raise error.UploadError(f"{self.review}: {str(e)}")
|
||||
except http.client.HTTPException as e:
|
||||
raise UploadError(f"{self.review}: {e.__class__.__name__}")
|
||||
raise error.UploadError(
|
||||
f"{self.review}: {e.__class__.__name__}"
|
||||
)
|
||||
|
||||
REVIEW_CACHE[u] = self._review_url
|
||||
return self._review_url + self.projectname
|
||||
@@ -692,16 +739,16 @@ class Remote:
|
||||
return rev
|
||||
|
||||
if not rev.startswith("refs/"):
|
||||
rev = R_HEADS + rev
|
||||
rev = git_refs.R_HEADS + rev
|
||||
|
||||
for spec in self.fetch:
|
||||
if spec.SourceMatches(rev):
|
||||
return spec.MapSource(rev)
|
||||
|
||||
if not rev.startswith(R_HEADS):
|
||||
if not rev.startswith(git_refs.R_HEADS):
|
||||
return rev
|
||||
|
||||
raise GitError(
|
||||
raise error.GitError(
|
||||
"%s: remote %s does not have %s"
|
||||
% (self.projectname, self.name, rev)
|
||||
)
|
||||
@@ -724,7 +771,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)
|
||||
|
||||
+121
-60
@@ -14,8 +14,9 @@
|
||||
|
||||
import os
|
||||
|
||||
import git_command
|
||||
import platform_utils
|
||||
from repo_trace import Trace
|
||||
import repo_trace
|
||||
|
||||
|
||||
HEAD = "HEAD"
|
||||
@@ -40,6 +41,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]
|
||||
@@ -69,7 +81,7 @@ class GitRefs:
|
||||
self._LoadAll()
|
||||
|
||||
def _NeedUpdate(self):
|
||||
with Trace(": scan refs %s", self._gitdir):
|
||||
with repo_trace.Trace(": scan refs %s", self._gitdir):
|
||||
for name, mtime in self._mtime.items():
|
||||
try:
|
||||
if mtime != os.path.getmtime(
|
||||
@@ -81,14 +93,17 @@ class GitRefs:
|
||||
return False
|
||||
|
||||
def _LoadAll(self):
|
||||
with Trace(": load refs %s", self._gitdir):
|
||||
with repo_trace.Trace(": load refs %s", self._gitdir):
|
||||
self._phyref = {}
|
||||
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 +117,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_command.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 = git_command.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 = git_command.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 = git_command.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)
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
# 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.
|
||||
|
||||
"""Read a worktree's state from one machine-readable git status snapshot."""
|
||||
|
||||
from collections import OrderedDict
|
||||
import os
|
||||
from typing import Iterator, List, Optional, TYPE_CHECKING
|
||||
|
||||
import git_command
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from project import Project
|
||||
|
||||
|
||||
class StatusEntry:
|
||||
"""The state of one path on one side of the index."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path: str,
|
||||
status: str,
|
||||
src_path: Optional[str] = None,
|
||||
level: Optional[str] = None,
|
||||
) -> None:
|
||||
self.path = path
|
||||
self.status = status
|
||||
self.src_path = src_path
|
||||
self.level = level
|
||||
|
||||
|
||||
class StatusSnapshot:
|
||||
"""A consistent view of worktree, index, and branch state."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.index_changes = OrderedDict()
|
||||
self.worktree_changes = OrderedDict()
|
||||
self.untracked = []
|
||||
self.branch_oid = None
|
||||
self.branch_head = None
|
||||
self.upstream = None
|
||||
self.ahead = 0
|
||||
self.behind = 0
|
||||
self.has_ahead_behind = False
|
||||
self.stash_count = 0
|
||||
|
||||
@property
|
||||
def current_branch(self) -> Optional[str]:
|
||||
if self.branch_head in (None, "(detached)", "(unknown)"):
|
||||
return None
|
||||
return self.branch_head
|
||||
|
||||
def is_dirty(self, consider_untracked: bool = True) -> bool:
|
||||
return bool(
|
||||
self.index_changes
|
||||
or self.worktree_changes
|
||||
or (consider_untracked and self.untracked)
|
||||
)
|
||||
|
||||
|
||||
def GetStatus(
|
||||
project: "Project",
|
||||
gitdir: str,
|
||||
untracked_files: str = "all",
|
||||
branch: bool = False,
|
||||
ahead_behind: bool = False,
|
||||
show_stash: bool = False,
|
||||
) -> StatusSnapshot:
|
||||
"""Return one machine-readable status snapshot for |project|."""
|
||||
if not git_command.git_require((2, 11, 0)):
|
||||
raise UnsupportedStatusError("porcelain v2 requires Git 2.11")
|
||||
cmd = [
|
||||
"status",
|
||||
"--porcelain=v2",
|
||||
"-z",
|
||||
"--ignore-submodules=all",
|
||||
f"--untracked-files={untracked_files}",
|
||||
]
|
||||
if branch:
|
||||
cmd.append("--branch")
|
||||
if git_command.git_require((2, 17, 0)):
|
||||
cmd.append(
|
||||
"--ahead-behind" if ahead_behind else "--no-ahead-behind"
|
||||
)
|
||||
if git_command.git_require((2, 18, 0)):
|
||||
# Match the existing staged diff's explicit rename detection even if
|
||||
# status.renames is disabled in the user's config.
|
||||
cmd.append("--renames")
|
||||
if show_stash and git_command.git_require((2, 35, 0)):
|
||||
cmd.append("--show-stash")
|
||||
|
||||
p = git_command.GitCommand(
|
||||
project,
|
||||
cmd,
|
||||
bare=False,
|
||||
gitdir=gitdir,
|
||||
capture_stdout=True,
|
||||
capture_stdout_bytes=True,
|
||||
capture_stderr=True,
|
||||
verify_command=True,
|
||||
)
|
||||
p.Wait()
|
||||
return ParsePorcelainV2(p.stdout)
|
||||
|
||||
|
||||
def _Path(value: bytes) -> str:
|
||||
"""Decode a Git pathname without losing undecodable bytes."""
|
||||
return os.fsdecode(value)
|
||||
|
||||
|
||||
def _Status(value: int) -> str:
|
||||
"""Normalize Git's unchanged markers for repo's status display."""
|
||||
char = chr(value)
|
||||
return "" if char == "." else char
|
||||
|
||||
|
||||
def _Records(output: bytes) -> Iterator[bytes]:
|
||||
if not output:
|
||||
return iter(())
|
||||
if not output.endswith(b"\0"):
|
||||
raise StatusParseError("porcelain v2 output is not NUL terminated")
|
||||
records = output.split(b"\0")
|
||||
if not records[-1]:
|
||||
records.pop()
|
||||
return iter(records)
|
||||
|
||||
|
||||
class StatusParseError(ValueError):
|
||||
"""Raised when machine-readable status output is malformed."""
|
||||
|
||||
|
||||
class UnsupportedStatusError(RuntimeError):
|
||||
"""Raised when the Git client cannot produce porcelain v2."""
|
||||
|
||||
|
||||
def _Fields(record: bytes, count: int) -> List[bytes]:
|
||||
fields = record.split(b" ", count - 1)
|
||||
if len(fields) != count:
|
||||
raise StatusParseError(f"malformed porcelain v2 record: {record!r}")
|
||||
return fields
|
||||
|
||||
|
||||
def _AddTracked(
|
||||
status: StatusSnapshot,
|
||||
path: str,
|
||||
xy: bytes,
|
||||
src_path: Optional[str] = None,
|
||||
level: Optional[str] = None,
|
||||
) -> None:
|
||||
index_status = _Status(xy[0])
|
||||
worktree_status = _Status(xy[1])
|
||||
if index_status:
|
||||
status.index_changes[path] = StatusEntry(
|
||||
path,
|
||||
index_status,
|
||||
src_path=src_path if index_status in ("R", "C") else None,
|
||||
level=level if index_status in ("R", "C") else None,
|
||||
)
|
||||
if worktree_status:
|
||||
status.worktree_changes[path] = StatusEntry(
|
||||
path,
|
||||
worktree_status,
|
||||
src_path=src_path if worktree_status in ("R", "C") else None,
|
||||
level=level if worktree_status in ("R", "C") else None,
|
||||
)
|
||||
|
||||
|
||||
def ParsePorcelainV2(output: bytes) -> StatusSnapshot:
|
||||
"""Parse ``git status --porcelain=v2 -z --branch`` output."""
|
||||
status = StatusSnapshot()
|
||||
records = _Records(output)
|
||||
for record in records:
|
||||
kind = record[:1]
|
||||
if kind == b"#":
|
||||
try:
|
||||
key, value = record[2:].split(b" ", 1)
|
||||
except ValueError as e:
|
||||
raise StatusParseError(
|
||||
f"malformed porcelain v2 header: {record!r}"
|
||||
) from e
|
||||
if key == b"branch.oid":
|
||||
value = value.decode("ascii")
|
||||
status.branch_oid = None if value == "(initial)" else value
|
||||
elif key == b"branch.head":
|
||||
status.branch_head = _Path(value)
|
||||
elif key == b"branch.upstream":
|
||||
status.upstream = _Path(value)
|
||||
elif key == b"branch.ab":
|
||||
try:
|
||||
value = value.decode("ascii")
|
||||
ahead, behind = value.split()
|
||||
if ahead != "+?" and behind != "-?":
|
||||
status.ahead = int(ahead)
|
||||
status.behind = -int(behind)
|
||||
status.has_ahead_behind = True
|
||||
except ValueError as e:
|
||||
raise StatusParseError(
|
||||
f"malformed porcelain v2 branch.ab record: {record!r}"
|
||||
) from e
|
||||
elif key == b"stash":
|
||||
status.stash_count = int(value.decode("ascii"))
|
||||
continue
|
||||
|
||||
if kind == b"1":
|
||||
fields = _Fields(record, 9)
|
||||
xy = fields[1]
|
||||
if len(xy) != 2:
|
||||
raise StatusParseError(f"invalid status pair: {xy!r}")
|
||||
path = _Path(fields[8])
|
||||
_AddTracked(status, path, xy)
|
||||
elif kind == b"2":
|
||||
fields = _Fields(record, 10)
|
||||
xy = fields[1]
|
||||
if len(xy) != 2:
|
||||
raise StatusParseError(f"invalid status pair: {xy!r}")
|
||||
score = fields[8][1:].lstrip(b"0") or b"0"
|
||||
try:
|
||||
src_path = _Path(next(records))
|
||||
except StopIteration as e:
|
||||
raise StatusParseError(
|
||||
"rename record has no source path"
|
||||
) from e
|
||||
path = _Path(fields[9])
|
||||
_AddTracked(
|
||||
status,
|
||||
path,
|
||||
xy,
|
||||
src_path=src_path,
|
||||
level=score.decode("ascii"),
|
||||
)
|
||||
elif kind == b"u":
|
||||
fields = _Fields(record, 11)
|
||||
path = _Path(fields[10])
|
||||
# The old diff-index/diff-files pair reported unmerged paths on
|
||||
# both sides, regardless of porcelain's more specific XY pair.
|
||||
status.index_changes[path] = StatusEntry(path, "U")
|
||||
status.worktree_changes[path] = StatusEntry(path, "U")
|
||||
elif kind == b"?":
|
||||
status.untracked.append(_Path(record[2:]))
|
||||
elif kind == b"!":
|
||||
continue
|
||||
else:
|
||||
raise StatusParseError(
|
||||
f"unknown porcelain v2 record type: {record!r}"
|
||||
)
|
||||
return status
|
||||
+99
-32
@@ -23,17 +23,19 @@ 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
|
||||
import git_command
|
||||
from git_config import IsId
|
||||
from git_config import RepoConfig
|
||||
from git_refs import GitRefs
|
||||
import platform_utils
|
||||
|
||||
|
||||
_SUPERPROJECT_GIT_NAME = "superproject.git"
|
||||
@@ -97,7 +99,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(
|
||||
@@ -129,11 +131,15 @@ 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(
|
||||
p = git_command.GitCommand(
|
||||
None, # project
|
||||
cmd,
|
||||
gitdir=self._work_git,
|
||||
@@ -181,7 +187,7 @@ class Superproject:
|
||||
if netloc:
|
||||
parts = netloc.split("-review", 1)
|
||||
host = parts[0]
|
||||
rev = GitRefs(self._work_git).get("HEAD")
|
||||
rev = self._GetRef("HEAD")
|
||||
return f"{host}/{self.name}@{rev}"
|
||||
return None
|
||||
|
||||
@@ -196,7 +202,8 @@ class Superproject:
|
||||
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):
|
||||
@@ -215,30 +222,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 = git_command.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|.
|
||||
@@ -251,7 +291,7 @@ class Superproject:
|
||||
if not os.path.exists(self._work_git):
|
||||
self._LogWarning("git fetch missing directory: {}", self._work_git)
|
||||
return False
|
||||
if not git_require((2, 28, 0)):
|
||||
if not git_command.git_require((2, 28, 0)):
|
||||
self._LogWarning(
|
||||
"superproject requires a git version 2.28 or later"
|
||||
)
|
||||
@@ -272,13 +312,23 @@ 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]
|
||||
p = GitCommand(
|
||||
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 = git_command.GitCommand(
|
||||
None,
|
||||
cmd,
|
||||
gitdir=self._work_git,
|
||||
@@ -298,6 +348,21 @@ class Superproject:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _GetRef(self, ref: str) -> str:
|
||||
"""Resolve one local ref without loading the entire ref namespace."""
|
||||
p = git_command.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.
|
||||
|
||||
@@ -312,10 +377,10 @@ 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(
|
||||
p = git_command.GitCommand(
|
||||
None,
|
||||
cmd,
|
||||
gitdir=self._work_git,
|
||||
@@ -364,6 +429,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:
|
||||
@@ -422,7 +489,7 @@ class Superproject:
|
||||
)
|
||||
return None
|
||||
manifest_str = self._manifest.ToXml(
|
||||
filter_groups=self._manifest.GetManifestGroupsStr(),
|
||||
filter_groups="all",
|
||||
omit_local=True,
|
||||
).toxml()
|
||||
manifest_path = self._manifest_path
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
|
||||
"""Event logging in the git trace2 EVENT format."""
|
||||
|
||||
from git_command import GetEventTargetPath
|
||||
from git_command import RepoSourceVersion
|
||||
import git_command
|
||||
from git_trace2_event_log_base import BaseEventLog
|
||||
|
||||
|
||||
@@ -37,7 +36,9 @@ class EventLog(BaseEventLog):
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(repo_source_version=RepoSourceVersion(), **kwargs)
|
||||
super().__init__(
|
||||
repo_source_version=git_command.RepoSourceVersion(), **kwargs
|
||||
)
|
||||
|
||||
def Write(self, path=None, **kwargs):
|
||||
if path is None:
|
||||
@@ -45,4 +46,4 @@ class EventLog(BaseEventLog):
|
||||
return super().Write(path=path, **kwargs)
|
||||
|
||||
def _GetEventTargetPath(self):
|
||||
return GetEventTargetPath()
|
||||
return git_command.GetEventTargetPath()
|
||||
@@ -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:
|
||||
@@ -194,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.
|
||||
@@ -309,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:
|
||||
@@ -322,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
|
||||
@@ -348,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,20 +12,21 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
import urllib.parse
|
||||
|
||||
from error import HookError
|
||||
from git_refs import HEAD
|
||||
import error
|
||||
import git_refs
|
||||
|
||||
|
||||
# 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"},
|
||||
"post-sync": {"repo_topdir", "sync_duration_seconds"},
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +69,8 @@ class RepoHook:
|
||||
allow_all_hooks=False,
|
||||
ignore_hooks=False,
|
||||
abort_if_user_denies=False,
|
||||
yes=False,
|
||||
fix=False,
|
||||
):
|
||||
"""RepoHook constructor.
|
||||
|
||||
@@ -89,6 +92,8 @@ class RepoHook:
|
||||
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
|
||||
@@ -99,6 +104,8 @@ class RepoHook:
|
||||
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.
|
||||
self._script_fullpath = None
|
||||
@@ -135,7 +142,7 @@ class RepoHook:
|
||||
# NOTE: Local (non-committed) changes will not be factored into this
|
||||
# hash. I think this is OK, since we're really only worried about
|
||||
# warning the user about upstream changes.
|
||||
return self._hooks_project.work_git.rev_parse(HEAD)
|
||||
return self._hooks_project.work_git.rev_parse(git_refs.HEAD)
|
||||
|
||||
def _GetMustVerb(self):
|
||||
"""Return 'must' if the hook is required; 'should' if not."""
|
||||
@@ -221,7 +228,7 @@ class RepoHook:
|
||||
|
||||
# For anything else, we'll assume no approval.
|
||||
if self._abort_if_user_denies:
|
||||
raise HookError(
|
||||
raise error.HookError(
|
||||
"You must allow the %s hook or use --no-verify."
|
||||
% self._hook_type
|
||||
)
|
||||
@@ -319,14 +326,16 @@ class RepoHook:
|
||||
try:
|
||||
exec(compile(data, self._script_fullpath, "exec"), context)
|
||||
except Exception:
|
||||
raise HookError(
|
||||
raise error.HookError(
|
||||
"%s\nFailed to import %s hook; see traceback above."
|
||||
% (traceback.format_exc(), self._hook_type)
|
||||
)
|
||||
|
||||
# Running the script should have defined a main() function.
|
||||
if "main" not in context:
|
||||
raise HookError('Missing main() in: "%s"' % self._script_fullpath)
|
||||
raise error.HookError(
|
||||
'Missing main() in: "%s"' % self._script_fullpath
|
||||
)
|
||||
|
||||
# Call the main function in the hook. If the hook should cause the
|
||||
# build to fail, it will raise an Exception. We'll catch that convert
|
||||
@@ -334,7 +343,7 @@ class RepoHook:
|
||||
try:
|
||||
context["main"](**kwargs)
|
||||
except Exception:
|
||||
raise HookError(
|
||||
raise error.HookError(
|
||||
"%s\nFailed to run main() for %s hook; see traceback "
|
||||
"above." % (traceback.format_exc(), self._hook_type)
|
||||
)
|
||||
@@ -374,8 +383,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()
|
||||
@@ -383,7 +396,7 @@ class RepoHook:
|
||||
if interp:
|
||||
prog = os.path.basename(interp)
|
||||
if prog.startswith("python2"):
|
||||
raise HookError("Python 2 is not supported")
|
||||
raise error.HookError("Python 2 is not supported")
|
||||
|
||||
# Run the hook by importing directly.
|
||||
self._ExecuteHookViaImport(data, context, **kwargs)
|
||||
@@ -395,7 +408,7 @@ class RepoHook:
|
||||
def _CheckHook(self):
|
||||
# Bail with a nice error if we can't find the hook.
|
||||
if not os.path.isfile(self._script_fullpath):
|
||||
raise HookError(
|
||||
raise error.HookError(
|
||||
"Couldn't find repo hook: %s" % self._script_fullpath
|
||||
)
|
||||
|
||||
@@ -462,7 +475,7 @@ class RepoHook:
|
||||
% (self._hook_type, str(e)),
|
||||
file=sys.stderr,
|
||||
)
|
||||
except HookError as e:
|
||||
except error.HookError as e:
|
||||
passed = False
|
||||
print("ERROR: %s" % str(e), file=sys.stderr)
|
||||
|
||||
@@ -497,12 +510,18 @@ class RepoHook:
|
||||
"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
|
||||
@@ -526,3 +545,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,
|
||||
)
|
||||
@@ -19,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
|
||||
@@ -29,9 +30,10 @@ import signal
|
||||
import sys
|
||||
import textwrap
|
||||
import time
|
||||
from typing import Optional
|
||||
import urllib.request
|
||||
|
||||
from repo_logging import RepoLogger
|
||||
import repo_logging
|
||||
|
||||
|
||||
try:
|
||||
@@ -43,34 +45,22 @@ from color import SetDefaultColoring
|
||||
from command import InteractiveCommand
|
||||
from command import MirrorSafeCommand
|
||||
from editor import Editor
|
||||
from error import DownloadError
|
||||
from error import InvalidProjectGroupsError
|
||||
from error import ManifestInvalidRevisionError
|
||||
from error import ManifestParseError
|
||||
from error import NoManifestException
|
||||
from error import NoSuchProjectError
|
||||
from error import RepoChangedException
|
||||
from error import RepoError
|
||||
from error import RepoExitError
|
||||
from error import RepoUnhandledExceptionError
|
||||
from error import SilentRepoExitError
|
||||
import error
|
||||
import event_log
|
||||
from git_command import user_agent
|
||||
import git_command
|
||||
from git_config import RepoConfig
|
||||
from git_trace2_event_log import EventLog
|
||||
from manifest_xml import RepoClient
|
||||
from pager import RunPager
|
||||
from pager import TerminatePager
|
||||
from repo_trace import SetTrace
|
||||
from repo_trace import SetTraceToStderr
|
||||
from repo_trace import Trace
|
||||
import repo_trace
|
||||
from subcmds import all_commands
|
||||
from subcmds.version import Version
|
||||
from wrapper import Wrapper
|
||||
from wrapper import WrapperPath
|
||||
|
||||
|
||||
logger = RepoLogger(__file__)
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
|
||||
|
||||
# NB: These do not need to be kept in sync with the repo launcher script.
|
||||
@@ -102,6 +92,11 @@ elif sys.version_info < MIN_PYTHON_VERSION_SOFT:
|
||||
KEYBOARD_INTERRUPT_EXIT = 128 + signal.SIGINT
|
||||
MAX_PRINT_ERRORS = 5
|
||||
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(errors="surrogateescape")
|
||||
if hasattr(sys.stderr, "reconfigure"):
|
||||
sys.stderr.reconfigure(errors="surrogateescape")
|
||||
|
||||
global_options = optparse.OptionParser(
|
||||
usage="repo [-p|--paginate|--no-pager] COMMAND [ARGS]",
|
||||
add_help_option=False,
|
||||
@@ -272,7 +267,7 @@ class _Repo:
|
||||
run = (
|
||||
lambda: self._RunLong(name, gopts, argv, git_trace2_event_log) or 0
|
||||
)
|
||||
with Trace(
|
||||
with repo_trace.Trace(
|
||||
"starting new command: %s [sid=%s]",
|
||||
", ".join([name] + argv),
|
||||
git_trace2_event_log.full_sid,
|
||||
@@ -292,6 +287,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
|
||||
@@ -306,20 +397,22 @@ class _Repo:
|
||||
outer_client=outer_client,
|
||||
)
|
||||
|
||||
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,
|
||||
if name not in self.commands:
|
||||
corrected_name = self._autocorrect_command_name(
|
||||
name, outer_client.globalConfig
|
||||
)
|
||||
except KeyError:
|
||||
logger.error(
|
||||
"repo: '%s' is not a repo command. See 'repo help'.", name
|
||||
)
|
||||
return 1
|
||||
if not corrected_name:
|
||||
return 1
|
||||
name = corrected_name
|
||||
|
||||
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
|
||||
|
||||
@@ -330,13 +423,16 @@ class _Repo:
|
||||
try:
|
||||
copts, cargs = cmd.OptionParser.parse_args(argv)
|
||||
copts = cmd.ReadEnvironmentOptions(copts)
|
||||
except NoManifestException as e:
|
||||
except error.NoManifestException as e:
|
||||
logger.error("error: in `%s`: %s", " ".join([name] + argv), e)
|
||||
logger.error(
|
||||
"error: manifest missing or unreadable -- please run init"
|
||||
)
|
||||
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:
|
||||
@@ -359,7 +455,6 @@ class _Repo:
|
||||
Execute the subcommand.
|
||||
"""
|
||||
nonlocal result
|
||||
cmd.CommonValidateOptions(copts, cargs)
|
||||
cmd.ValidateOptions(copts, cargs)
|
||||
|
||||
this_manifest_only = copts.this_manifest_only
|
||||
@@ -404,23 +499,23 @@ class _Repo:
|
||||
KeyboardInterrupt,
|
||||
SystemExit,
|
||||
Exception,
|
||||
RepoExitError,
|
||||
error.RepoExitError,
|
||||
) as e:
|
||||
ok = isinstance(e, SystemExit) and not e.code
|
||||
exception_name = type(e).__name__
|
||||
if isinstance(e, RepoUnhandledExceptionError):
|
||||
if isinstance(e, error.RepoUnhandledExceptionError):
|
||||
exception_name = type(e.error).__name__
|
||||
if isinstance(e, RepoExitError):
|
||||
if isinstance(e, error.RepoExitError):
|
||||
aggregated_errors = e.aggregate_errors or []
|
||||
for error in aggregated_errors:
|
||||
for err in aggregated_errors:
|
||||
project = None
|
||||
if isinstance(error, RepoError):
|
||||
project = error.project
|
||||
if isinstance(err, error.RepoError):
|
||||
project = err.project
|
||||
error_info = json.dumps(
|
||||
{
|
||||
"ErrorType": type(error).__name__,
|
||||
"ErrorType": type(err).__name__,
|
||||
"Project": str(project),
|
||||
"Message": str(error),
|
||||
"Message": str(err),
|
||||
}
|
||||
)
|
||||
git_trace2_event_log.ErrorEvent(
|
||||
@@ -435,24 +530,24 @@ class _Repo:
|
||||
try:
|
||||
execute_command()
|
||||
except (
|
||||
DownloadError,
|
||||
ManifestInvalidRevisionError,
|
||||
ManifestParseError,
|
||||
NoManifestException,
|
||||
error.DownloadError,
|
||||
error.ManifestInvalidRevisionError,
|
||||
error.ManifestParseError,
|
||||
error.NoManifestException,
|
||||
) as e:
|
||||
logger.error("error: in `%s`: %s", " ".join([name] + argv), e)
|
||||
if isinstance(e, NoManifestException):
|
||||
if isinstance(e, error.NoManifestException):
|
||||
logger.error(
|
||||
"error: manifest missing or unreadable -- please run init"
|
||||
)
|
||||
result = e.exit_code
|
||||
except NoSuchProjectError as e:
|
||||
except error.NoSuchProjectError as e:
|
||||
if e.name:
|
||||
logger.error("error: project %s not found", e.name)
|
||||
else:
|
||||
logger.error("error: no project in current directory")
|
||||
result = e.exit_code
|
||||
except InvalidProjectGroupsError as e:
|
||||
except error.InvalidProjectGroupsError as e:
|
||||
if e.name:
|
||||
logger.error(
|
||||
"error: project group must be enabled for project %s",
|
||||
@@ -471,7 +566,7 @@ class _Repo:
|
||||
except KeyboardInterrupt:
|
||||
result = KEYBOARD_INTERRUPT_EXIT
|
||||
raise
|
||||
except RepoExitError as e:
|
||||
except error.RepoExitError as e:
|
||||
result = e.exit_code
|
||||
raise
|
||||
except Exception:
|
||||
@@ -605,11 +700,11 @@ def _PruneOptions(argv, opt):
|
||||
|
||||
class _UserAgentHandler(urllib.request.BaseHandler):
|
||||
def http_request(self, req):
|
||||
req.add_header("User-Agent", user_agent.repo)
|
||||
req.add_header("User-Agent", git_command.user_agent.repo)
|
||||
return req
|
||||
|
||||
def https_request(self, req):
|
||||
req.add_header("User-Agent", user_agent.repo)
|
||||
req.add_header("User-Agent", git_command.user_agent.repo)
|
||||
return req
|
||||
|
||||
|
||||
@@ -836,20 +931,20 @@ def _Main(argv):
|
||||
name, gopts, argv = repo._ParseArgs(argv)
|
||||
|
||||
if gopts.trace:
|
||||
SetTrace()
|
||||
repo_trace.SetTrace()
|
||||
|
||||
if gopts.trace_to_stderr:
|
||||
SetTraceToStderr()
|
||||
repo_trace.SetTraceToStderr()
|
||||
|
||||
result = repo._Run(name, gopts, argv) or 0
|
||||
except RepoExitError as e:
|
||||
if not isinstance(e, SilentRepoExitError):
|
||||
except error.RepoExitError as e:
|
||||
if not isinstance(e, error.SilentRepoExitError):
|
||||
logger.log_aggregated_errors(e)
|
||||
result = e.exit_code
|
||||
except KeyboardInterrupt:
|
||||
print("aborted by user", file=sys.stderr)
|
||||
result = KEYBOARD_INTERRUPT_EXIT
|
||||
except RepoChangedException as rce:
|
||||
except error.RepoChangedException as rce:
|
||||
# If repo changed, re-exec ourselves.
|
||||
argv = list(sys.argv)
|
||||
argv.extend(rce.extra_args)
|
||||
|
||||
+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
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "October 2026" "repo config" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo config - manual page for repo config
|
||||
.SH SYNOPSIS
|
||||
.B repo
|
||||
\fI\,config \/\fR[\fI\,--type=<type>\/\fR] [\fI\,-z\/\fR] \fI\,get <name>\/\fR
|
||||
.SH DESCRIPTION
|
||||
Summary
|
||||
.PP
|
||||
Read and write per\-user repo settings
|
||||
.PP
|
||||
repo config [\-\-type=<type>] set <name> <value>
|
||||
repo config unset <name>
|
||||
repo config [\-z] list
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
\fB\-h\fR, \fB\-\-help\fR
|
||||
show this help message and exit
|
||||
.TP
|
||||
\fB\-\-type\fR=\fI\,TYPE\/\fR
|
||||
validate and canonicalize the value as this type for get and set (bool, int)
|
||||
.TP
|
||||
\fB\-z\fR, \fB\-\-null\fR
|
||||
terminate output of get and list with NUL instead of newline, and separate key and value with a newline
|
||||
.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 config` to view the detailed manual.
|
||||
.SH DETAILS
|
||||
.PP
|
||||
The 'repo config' command reads and writes repo's own per\-user settings. These
|
||||
are separate from git's config and from a checkout's settings; see
|
||||
docs/repoconfig.md for where they live and which settings repo understands.
|
||||
.PP
|
||||
Settings are named <section>.<name>, e.g. sync.smartsync. 'get' prints every
|
||||
value of the setting and exits 1 without output if it is not set, like `git
|
||||
config`.
|
||||
.PP
|
||||
Values are stored as strings. With \fB\-\-type\fR, 'set' validates the value and stores
|
||||
it in canonical form (e.g. "on" as "true", "1k" as "1024"), and 'get' prints the
|
||||
canonical form, like `git config \fB\-\-type\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
|
||||
|
||||
+13
-3
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "April 2025" "repo gc" "Repo Manual"
|
||||
.TH REPO "1" "September 2026" "repo gc" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo gc - manual page for repo gc
|
||||
.SH SYNOPSIS
|
||||
@@ -21,8 +21,10 @@ do everything except actually delete
|
||||
answer yes to all safe prompts
|
||||
.TP
|
||||
\fB\-\-repack\fR
|
||||
repack all projects that use partial clone with
|
||||
filter=blob:none
|
||||
repack all projects that use partial clone with filter=blob:none
|
||||
.TP
|
||||
\fB\-\-keep\-manifest\-projects\fR
|
||||
keep Git data for projects still in the manifests
|
||||
.SS Logging options:
|
||||
.TP
|
||||
\fB\-v\fR, \fB\-\-verbose\fR
|
||||
@@ -45,3 +47,11 @@ only operate on this (sub)manifest
|
||||
operate on this manifest and its submanifests
|
||||
.PP
|
||||
Run `repo help gc` to view the detailed manual.
|
||||
.SH DETAILS
|
||||
.PP
|
||||
The 'repo gc' command removes internal Git data for projects that are no longer
|
||||
active in the current configuration.
|
||||
.PP
|
||||
Projects excluded by the configured manifest groups are inactive by default. Use
|
||||
\fB\-\-keep\-manifest\-projects\fR to keep Git data for projects that are still in the
|
||||
manifests.
|
||||
+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
-30
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "September 2024" "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
|
||||
@@ -29,36 +29,29 @@ manifest repository location
|
||||
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
|
||||
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
|
||||
@@ -71,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
|
||||
@@ -92,33 +86,34 @@ 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\-s\fR, \fB\-\-smart\-sync\fR
|
||||
default to smart sync when syncing projects
|
||||
.TP
|
||||
\fB\-\-no\-smart\-sync\fR, \fB\-\-head\fR
|
||||
do not default to smart sync when syncing projects
|
||||
.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
|
||||
|
||||
+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
|
||||
|
||||
+71
-27
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "December 2025" "repo manifest" "Repo Manual"
|
||||
.TH REPO "1" "August 2026" "repo manifest" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo manifest - manual page for repo manifest
|
||||
.SH SYNOPSIS
|
||||
@@ -21,14 +21,10 @@ 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\-\-format\fR=\fI\,FORMAT\/\fR
|
||||
output format: xml, json (default: xml)
|
||||
@@ -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
|
||||
@@ -126,17 +121,20 @@ include*)>
|
||||
<!ATTLIST remote revision CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT default EMPTY>
|
||||
<!ATTLIST default remote IDREF #IMPLIED>
|
||||
<!ATTLIST default revision CDATA #IMPLIED>
|
||||
<!ATTLIST default dest\-branch CDATA #IMPLIED>
|
||||
<!ATTLIST default upstream CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-j CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-c CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-s CDATA #IMPLIED>
|
||||
<!ATTLIST default sync\-tags CDATA #IMPLIED>
|
||||
<!ATTLIST default remote IDREF #IMPLIED>
|
||||
<!ATTLIST default revision CDATA #IMPLIED>
|
||||
<!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>
|
||||
<!ATTLIST default sync\-smartsync CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT manifest\-server EMPTY>
|
||||
<!ATTLIST manifest\-server url CDATA #REQUIRED>
|
||||
<!ATTLIST manifest\-server url CDATA #REQUIRED>
|
||||
<!ATTLIST manifest\-server helper CDATA #IMPLIED>
|
||||
.IP
|
||||
<!ELEMENT submanifest EMPTY>
|
||||
<!ATTLIST submanifest name ID #REQUIRED>
|
||||
@@ -164,15 +162,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>
|
||||
@@ -309,7 +324,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
|
||||
@@ -320,6 +337,8 @@ Attribute `sync\-s`: Set to true to also sync sub\-projects.
|
||||
Attribute `sync\-tags`: Set to false to only sync the given Git branch (specified
|
||||
in the `revision` attribute) rather than the other ref tags.
|
||||
.PP
|
||||
Attribute `sync\-smartsync`: Set to true to enable smart sync by default.
|
||||
.PP
|
||||
Element manifest\-server
|
||||
.PP
|
||||
At most one manifest\-server may be specified. The url attribute is used to
|
||||
@@ -327,6 +346,11 @@ specify the URL of a manifest server, which is an XML RPC service.
|
||||
.PP
|
||||
See the [smart sync documentation](./smart\-sync.md) for more details.
|
||||
.PP
|
||||
Attribute `url`: The URL of the manifest server.
|
||||
.PP
|
||||
Attribute `helper`: Optional name of a remote helper binary to execute to
|
||||
resolve proxying or authentication for the manifest server.
|
||||
.PP
|
||||
Element submanifest
|
||||
.PP
|
||||
One or more submanifest elements may be specified. Each element describes a
|
||||
@@ -466,6 +490,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.
|
||||
@@ -475,6 +514,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
|
||||
|
||||
+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
|
||||
|
||||
+24
-30
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "August 2025" "repo smartsync" "Repo Manual"
|
||||
.TH REPO "1" "September 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). Ignored unless \fB\-\-no\-interleaved\fR is set
|
||||
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). Ignored unless \fB\-\-nointerleaved\fR is set
|
||||
number of local checkout jobs to run in parallel (defaults to \fB\-\-jobs\fR or based on number of CPU cores). 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,30 +30,22 @@ 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
|
||||
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)
|
||||
@@ -79,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
|
||||
@@ -91,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
|
||||
@@ -108,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
|
||||
@@ -125,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
|
||||
|
||||
+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:
|
||||
|
||||
+38
-34
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "August 2025" "repo sync" "Repo Manual"
|
||||
.TH REPO "1" "October 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). Ignored unless \fB\-\-no\-interleaved\fR is set
|
||||
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). Ignored unless \fB\-\-nointerleaved\fR is set
|
||||
number of local checkout jobs to run in parallel (defaults to \fB\-\-jobs\fR or based on number of CPU cores). 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,30 +30,22 @@ 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
|
||||
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)
|
||||
@@ -79,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
|
||||
@@ -91,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
|
||||
@@ -108,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
|
||||
@@ -125,12 +120,13 @@ 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\-\-no\-smart\-sync\fR, \fB\-\-head\fR
|
||||
disable smart sync and sync to ToT instead
|
||||
.TP
|
||||
\fB\-t\fR SMART_TAG, \fB\-\-smart\-tag\fR=\fI\,SMART_TAG\/\fR
|
||||
smart sync using manifest from a known tag
|
||||
@@ -192,6 +188,10 @@ The \fB\-s\fR/\-\-smart\-sync option can be used to sync to a known good build a
|
||||
specified by the manifest\-server element in the current manifest. The
|
||||
\fB\-t\fR/\-\-smart\-tag option is similar and allows you to specify a custom tag/label.
|
||||
.PP
|
||||
Whether 'repo sync' smart syncs by default can also be controlled with the
|
||||
sync.smartsync per\-user setting; see docs/repoconfig.md. Explicit
|
||||
\fB\-s\fR/\-\-no\-smart\-sync/\-t flags always override it.
|
||||
.PP
|
||||
The \fB\-u\fR/\-\-manifest\-server\-username and \fB\-p\fR/\-\-manifest\-server\-password options can
|
||||
be used to specify a username and password to authenticate with the manifest
|
||||
server when using the \fB\-s\fR or \fB\-t\fR option.
|
||||
@@ -225,8 +225,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
|
||||
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "November 2025" "repo wipe" "Repo Manual"
|
||||
.TH REPO "1" "June 2026" "repo wipe" "Repo Manual"
|
||||
.SH NAME
|
||||
repo \- repo wipe - manual page for repo wipe
|
||||
.SH SYNOPSIS
|
||||
@@ -21,8 +21,7 @@ force wipe shared projects and uncommitted changes
|
||||
force wipe even if there are uncommitted changes
|
||||
.TP
|
||||
\fB\-\-force\-shared\fR
|
||||
force wipe even if the project shares an object
|
||||
directory
|
||||
force wipe even if the project shares an object directory
|
||||
.SS Logging options:
|
||||
.TP
|
||||
\fB\-v\fR, \fB\-\-verbose\fR
|
||||
|
||||
+6
-5
@@ -1,5 +1,5 @@
|
||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||
.TH REPO "1" "November 2025" "repo" "Repo Manual"
|
||||
.TH REPO "1" "October 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
|
||||
@@ -67,6 +65,9 @@ Checkout a branch for development
|
||||
cherry\-pick
|
||||
Cherry\-pick a change.
|
||||
.TP
|
||||
config
|
||||
Read and write per\-user repo settings
|
||||
.TP
|
||||
diff
|
||||
Show changes between commit and working tree
|
||||
.TP
|
||||
|
||||
+138
-87
@@ -21,12 +21,9 @@ import sys
|
||||
import urllib.parse
|
||||
import xml.dom.minidom
|
||||
|
||||
from error import ManifestInvalidPathError
|
||||
from error import ManifestInvalidRevisionError
|
||||
from error import ManifestParseError
|
||||
import error
|
||||
from git_config import GitConfig
|
||||
from git_refs import HEAD
|
||||
from git_refs import R_HEADS
|
||||
import git_refs
|
||||
from git_superproject import Superproject
|
||||
import platform_utils
|
||||
from project import Annotation
|
||||
@@ -114,7 +111,9 @@ def XmlInt(node, attr, default=None):
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
raise ManifestParseError(f'manifest: invalid {attr}="{value}" integer')
|
||||
raise error.ManifestParseError(
|
||||
f'manifest: invalid {attr}="{value}" integer'
|
||||
)
|
||||
|
||||
|
||||
def normalize_url(url: str) -> str:
|
||||
@@ -155,9 +154,11 @@ class _Default:
|
||||
upstreamExpr = None
|
||||
remote = None
|
||||
sync_j = None
|
||||
sync_j_max = None
|
||||
sync_c = False
|
||||
sync_s = False
|
||||
sync_tags = True
|
||||
sync_smartsync = False
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, _Default):
|
||||
@@ -288,7 +289,7 @@ class _XmlSubmanifest:
|
||||
self.annotations = []
|
||||
outer_client = parent._outer_client or parent
|
||||
if self.remote and not self.project:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
f"Submanifest {name}: must specify project when remote is "
|
||||
"given."
|
||||
)
|
||||
@@ -422,7 +423,7 @@ class XmlManifest:
|
||||
# This avoids a trailing os.path.sep when submanifest_path is empty.
|
||||
self.topdir = os.path.join(self.topdir, submanifest_path)
|
||||
if manifest_file != os.path.abspath(manifest_file):
|
||||
raise ManifestParseError("manifest_file must be abspath")
|
||||
raise error.ManifestParseError("manifest_file must be abspath")
|
||||
self.manifestFile = manifest_file
|
||||
if not outer_client or outer_client == self:
|
||||
# manifestFileOverrides only exists in the outer_client's manifest,
|
||||
@@ -436,7 +437,9 @@ class XmlManifest:
|
||||
|
||||
if submanifest_path and not outer_client:
|
||||
# If passing a submanifest_path, there must be an outer_client.
|
||||
raise ManifestParseError(f"Bad call to {self.__class__.__name__}")
|
||||
raise error.ManifestParseError(
|
||||
f"Bad call to {self.__class__.__name__}"
|
||||
)
|
||||
|
||||
# If self._outer_client is None, this is not a checkout that supports
|
||||
# multi-tree.
|
||||
@@ -475,13 +478,24 @@ class XmlManifest:
|
||||
if path is None:
|
||||
path = os.path.join(self.manifestProject.worktree, name)
|
||||
if not os.path.isfile(path):
|
||||
raise ManifestParseError("manifest %s not found" % name)
|
||||
raise error.ManifestParseError("manifest %s not found" % name)
|
||||
|
||||
self._load_local_manifests = load_local_manifests
|
||||
self._outer_client.manifestFileOverrides[self.path_prefix] = path
|
||||
self.Unload()
|
||||
self._Load()
|
||||
|
||||
def ClearOverride(self) -> None:
|
||||
"""Stop overriding the manifest, reverting to the default one.
|
||||
|
||||
This is the inverse of Override(), and is a no-op if no override is in
|
||||
effect. It is useful to recover from a failed Override(), which
|
||||
registers the override before parsing it.
|
||||
"""
|
||||
self._outer_client.manifestFileOverrides.pop(self.path_prefix, None)
|
||||
self._load_local_manifests = True
|
||||
self.Unload()
|
||||
|
||||
def Link(self, name):
|
||||
"""Update the repo metadata to use a different manifest."""
|
||||
self.Override(name)
|
||||
@@ -631,6 +645,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 +657,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if not d.sync_tags:
|
||||
have_default = True
|
||||
e.setAttribute("sync-tags", "false")
|
||||
if d.sync_smartsync:
|
||||
have_default = True
|
||||
e.setAttribute("sync-smartsync", "true")
|
||||
if have_default:
|
||||
root.appendChild(e)
|
||||
root.appendChild(doc.createTextNode(""))
|
||||
@@ -647,6 +667,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(""))
|
||||
|
||||
@@ -686,9 +708,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(git_refs.HEAD)
|
||||
e.setAttribute("revision", value)
|
||||
if peg_rev_upstream:
|
||||
if p.upstream:
|
||||
@@ -755,14 +777,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(""))
|
||||
@@ -819,7 +844,6 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
"submanifest",
|
||||
# These are children of 'project' nodes.
|
||||
"annotation",
|
||||
"project",
|
||||
"copyfile",
|
||||
"linkfile",
|
||||
}
|
||||
@@ -840,7 +864,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
elif child.nodeName in MULTI_ELEMENTS:
|
||||
ret.setdefault(child.nodeName, []).append(element)
|
||||
else:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
f'Unhandled element "{child.nodeName}"'
|
||||
)
|
||||
|
||||
@@ -993,6 +1017,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
|
||||
@@ -1049,6 +1078,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
|
||||
@@ -1147,6 +1180,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."""
|
||||
@@ -1155,7 +1189,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
|
||||
def _Load(self, initial_client=None, submanifest_depth=0):
|
||||
if submanifest_depth > MAX_SUBMANIFEST_DEPTH:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"maximum submanifest depth %d exceeded." % MAX_SUBMANIFEST_DEPTH
|
||||
)
|
||||
if not self._loaded:
|
||||
@@ -1173,8 +1207,8 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
try:
|
||||
m = self.manifestProject
|
||||
b = m.GetBranch(m.CurrentBranch).merge
|
||||
if b is not None and b.startswith(R_HEADS):
|
||||
b = b[len(R_HEADS) :]
|
||||
if b is not None and b.startswith(git_refs.R_HEADS):
|
||||
b = b[len(git_refs.R_HEADS) :]
|
||||
self.branch = b
|
||||
|
||||
parent_groups = self.parent_groups.copy()
|
||||
@@ -1227,7 +1261,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
|
||||
try:
|
||||
self._ParseManifest(nodes)
|
||||
except ManifestParseError as e:
|
||||
except error.ManifestParseError as e:
|
||||
# There was a problem parsing, unload ourselves in case they
|
||||
# catch this error and try again later, we will show the
|
||||
# correct error
|
||||
@@ -1289,10 +1323,12 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
try:
|
||||
root = xml.dom.minidom.parse(path)
|
||||
except (OSError, xml.parsers.expat.ExpatError) as e:
|
||||
raise ManifestParseError(f"error parsing manifest {path}: {e}")
|
||||
raise error.ManifestParseError(
|
||||
f"error parsing manifest {path}: {e}"
|
||||
)
|
||||
|
||||
if not root or not root.childNodes:
|
||||
raise ManifestParseError(f"no root node in {path}")
|
||||
raise error.ManifestParseError(f"no root node in {path}")
|
||||
|
||||
for manifest in root.childNodes:
|
||||
if (
|
||||
@@ -1301,7 +1337,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
):
|
||||
break
|
||||
else:
|
||||
raise ManifestParseError(f"no <manifest> in {path}")
|
||||
raise error.ManifestParseError(f"no <manifest> in {path}")
|
||||
|
||||
nodes = []
|
||||
for node in manifest.childNodes:
|
||||
@@ -1318,7 +1354,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if restrict_includes:
|
||||
msg = self._CheckLocalPath(name)
|
||||
if msg:
|
||||
raise ManifestInvalidPathError(
|
||||
raise error.ManifestInvalidPathError(
|
||||
f'<include> invalid "name": {name}: {msg}'
|
||||
)
|
||||
include_groups = (parent_groups or set()).copy()
|
||||
@@ -1328,7 +1364,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
)
|
||||
fp = os.path.join(include_root, name)
|
||||
if not os.path.isfile(fp):
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"include [%s/]%s doesn't exist or isn't a file"
|
||||
% (include_root, name)
|
||||
)
|
||||
@@ -1340,10 +1376,10 @@ 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 (RuntimeError, ManifestParseError):
|
||||
except (RuntimeError, error.ManifestParseError):
|
||||
raise
|
||||
except Exception as e:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
f"failed parsing included manifest {name}: {e}"
|
||||
)
|
||||
else:
|
||||
@@ -1367,7 +1403,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if remote:
|
||||
if remote.name in self._remotes:
|
||||
if remote != self._remotes[remote.name]:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"remote %s already exists with different "
|
||||
"attributes" % (remote.name)
|
||||
)
|
||||
@@ -1383,7 +1419,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if self._default is None:
|
||||
self._default = new_default
|
||||
elif not emptyDefault and new_default != self._default:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"duplicate default in %s" % (self.manifestFile)
|
||||
)
|
||||
|
||||
@@ -1397,7 +1433,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if submanifest:
|
||||
if submanifest.name in self._submanifests:
|
||||
if submanifest != self._submanifests[submanifest.name]:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"submanifest %s already exists with different "
|
||||
"attributes" % (submanifest.name)
|
||||
)
|
||||
@@ -1408,7 +1444,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
for node in itertools.chain(*node_list):
|
||||
if node.nodeName == "notice":
|
||||
if self._notice is not None:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"duplicate notice in %s" % (self.manifestFile)
|
||||
)
|
||||
self._notice = self._ParseNotice(node)
|
||||
@@ -1416,27 +1452,29 @@ 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(
|
||||
raise error.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, [])
|
||||
if project.relpath is None:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"missing path for %s in %s"
|
||||
% (project.name, self.manifestFile)
|
||||
)
|
||||
if project.relpath in self._paths:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"duplicate path %s in %s"
|
||||
% (project.relpath, self.manifestFile)
|
||||
)
|
||||
for tree in submanifest_paths:
|
||||
if project.relpath.startswith(tree):
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"project %s conflicts with submanifest path %s"
|
||||
% (project.relpath, tree)
|
||||
)
|
||||
@@ -1456,7 +1494,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
name = self._reqatt(node, "name")
|
||||
|
||||
if name not in self._projects:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"extend-project element specifies non-existent "
|
||||
"project: %s" % name
|
||||
)
|
||||
@@ -1478,7 +1516,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
|
||||
named_projects = self._projects[name]
|
||||
if dest_path and not path and len(named_projects) > 1:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"extend-project cannot use dest-path when "
|
||||
"matching multiple projects: %s" % name
|
||||
)
|
||||
@@ -1499,9 +1537,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
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)
|
||||
f"extend-project name {name}:\n "
|
||||
f"base {base_revision} vs "
|
||||
f"revision {p.revisionExpr}"
|
||||
)
|
||||
p.SetRevision(revision)
|
||||
|
||||
@@ -1535,7 +1573,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if node.nodeName == "repo-hooks":
|
||||
# Only one project can be the hooks project
|
||||
if repo_hooks_project is not None:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"duplicate repo-hooks in %s" % (self.manifestFile)
|
||||
)
|
||||
|
||||
@@ -1549,7 +1587,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
name = self._reqatt(node, "name")
|
||||
# There can only be one superproject.
|
||||
if self._superproject:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"duplicate superproject in %s" % (self.manifestFile)
|
||||
)
|
||||
remote_name = node.getAttribute("remote")
|
||||
@@ -1558,7 +1596,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
else:
|
||||
remote = self._get_remote(node)
|
||||
if remote is None:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"no remote for superproject %s within %s"
|
||||
% (name, self.manifestFile)
|
||||
)
|
||||
@@ -1566,7 +1604,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if not revision:
|
||||
revision = self._default.revisionExpr
|
||||
if not revision:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"no revision for superproject %s within %s"
|
||||
% (name, self.manifestFile)
|
||||
)
|
||||
@@ -1589,7 +1627,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
|
||||
# Name or path needed.
|
||||
if not name and not path:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"remove-project must have name and/or path"
|
||||
)
|
||||
|
||||
@@ -1602,9 +1640,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
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)
|
||||
f"remove-project name {name}:\n "
|
||||
f"base {base_revision} vs "
|
||||
f"revision {p.revisionExpr}"
|
||||
)
|
||||
del self._paths[p.relpath]
|
||||
if not removed_project:
|
||||
@@ -1616,13 +1654,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
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,
|
||||
)
|
||||
f"remove-project path {p.relpath}:\n "
|
||||
f"base {base_revision} vs "
|
||||
f"revision {p.revisionExpr}"
|
||||
)
|
||||
self._projects[projname].remove(p)
|
||||
del self._paths[p.relpath]
|
||||
@@ -1638,16 +1672,16 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
repo_hooks_project = None
|
||||
|
||||
if not removed_project and not XmlBool(node, "optional", False):
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"remove-project element specifies non-existent "
|
||||
"project: %s" % node.toxml()
|
||||
)
|
||||
|
||||
if failed_revision_changes:
|
||||
raise ManifestParseError(
|
||||
"revision base check failed, rebase patches and update "
|
||||
"base revs for: ",
|
||||
failed_revision_changes,
|
||||
fail_string = "\n".join(failed_revision_changes)
|
||||
raise error.ManifestParseError(
|
||||
f"detected base-revision mismatch, updates needed:\n"
|
||||
f"{fail_string}",
|
||||
)
|
||||
|
||||
# Store repo hooks project information.
|
||||
@@ -1656,12 +1690,12 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
try:
|
||||
repo_hooks_projects = self._projects[repo_hooks_project]
|
||||
except KeyError:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"project %s not found for repo-hooks" % (repo_hooks_project)
|
||||
)
|
||||
|
||||
if len(repo_hooks_projects) != 1:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"internal error parsing repo-hooks in %s"
|
||||
% (self.manifestFile)
|
||||
)
|
||||
@@ -1673,7 +1707,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
name = None
|
||||
m_url = m.GetRemote().url
|
||||
if m_url.endswith("/.git"):
|
||||
raise ManifestParseError("refusing to mirror %s" % m_url)
|
||||
raise error.ManifestParseError("refusing to mirror %s" % m_url)
|
||||
|
||||
if self._default and self._default.remote:
|
||||
url = self._default.remote.resolvedFetchUrl
|
||||
@@ -1758,14 +1792,22 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
|
||||
d.sync_j = XmlInt(node, "sync-j", None)
|
||||
if d.sync_j is not None and d.sync_j <= 0:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
'%s: sync-j must be greater than 0, not "%s"'
|
||||
% (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 error.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)
|
||||
d.sync_smartsync = XmlBool(node, "sync-smartsync", False)
|
||||
return d
|
||||
|
||||
def _ParseNotice(self, node):
|
||||
@@ -1833,20 +1875,20 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if revision:
|
||||
msg = self._CheckLocalPath(revision.split("/")[-1])
|
||||
if msg:
|
||||
raise ManifestInvalidPathError(
|
||||
raise error.ManifestInvalidPathError(
|
||||
'<submanifest> invalid "revision": %s: %s'
|
||||
% (revision, msg)
|
||||
)
|
||||
else:
|
||||
msg = self._CheckLocalPath(name)
|
||||
if msg:
|
||||
raise ManifestInvalidPathError(
|
||||
raise error.ManifestInvalidPathError(
|
||||
f'<submanifest> invalid "name": {name}: {msg}'
|
||||
)
|
||||
else:
|
||||
msg = self._CheckLocalPath(path)
|
||||
if msg:
|
||||
raise ManifestInvalidPathError(
|
||||
raise error.ManifestInvalidPathError(
|
||||
f'<submanifest> invalid "path": {path}: {msg}'
|
||||
)
|
||||
|
||||
@@ -1881,7 +1923,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
name = self._reqatt(node, "name")
|
||||
msg = self._CheckLocalPath(name, dir_ok=True)
|
||||
if msg:
|
||||
raise ManifestInvalidPathError(
|
||||
raise error.ManifestInvalidPathError(
|
||||
f'<project> invalid "name": {name}: {msg}'
|
||||
)
|
||||
if parent:
|
||||
@@ -1891,7 +1933,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if remote is None:
|
||||
remote = self._default.remote
|
||||
if remote is None:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
f"no remote for project {name} within {self.manifestFile}"
|
||||
)
|
||||
|
||||
@@ -1899,7 +1941,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
if not revisionExpr:
|
||||
revisionExpr = self._default.revisionExpr
|
||||
if not revisionExpr:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"no revision for project %s within %s"
|
||||
% (name, self.manifestFile)
|
||||
)
|
||||
@@ -1912,7 +1954,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
# Project.Sync_LocalHalf.
|
||||
msg = self._CheckLocalPath(path, dir_ok=True, cwd_dot_ok=True)
|
||||
if msg:
|
||||
raise ManifestInvalidPathError(
|
||||
raise error.ManifestInvalidPathError(
|
||||
f'<project> invalid "path": {path}: {msg}'
|
||||
)
|
||||
|
||||
@@ -1923,11 +1965,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
|
||||
clone_depth = XmlInt(node, "clone-depth")
|
||||
if clone_depth is not None and clone_depth <= 0:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
'%s: clone-depth must be greater than 0, not "%s"'
|
||||
% (self.manifestFile, clone_depth)
|
||||
)
|
||||
|
||||
sync_strategy = node.getAttribute("sync-strategy") or None
|
||||
|
||||
dest_branch = (
|
||||
node.getAttribute("dest-branch") or self._default.destBranchExpr
|
||||
)
|
||||
@@ -1974,6 +2018,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,
|
||||
@@ -2026,15 +2071,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):
|
||||
@@ -2208,7 +2259,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
# It is relative to the top of the repo client checkout.
|
||||
msg = cls._CheckLocalPath(dest)
|
||||
if msg:
|
||||
raise ManifestInvalidPathError(
|
||||
raise error.ManifestInvalidPathError(
|
||||
f'<{element}> invalid "dest": {dest}: {msg}'
|
||||
)
|
||||
|
||||
@@ -2219,7 +2270,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
src, dir_ok=is_linkfile, cwd_dot_ok=is_linkfile
|
||||
)
|
||||
if msg:
|
||||
raise ManifestInvalidPathError(
|
||||
raise error.ManifestInvalidPathError(
|
||||
f'<{element}> invalid "src": {src}: {msg}'
|
||||
)
|
||||
|
||||
@@ -2248,10 +2299,10 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
value = self._reqatt(node, "value")
|
||||
try:
|
||||
keep = self._reqatt(node, "keep").lower()
|
||||
except ManifestParseError:
|
||||
except error.ManifestParseError:
|
||||
keep = "true"
|
||||
if keep != "true" and keep != "false":
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
'optional "keep" attribute must be ' '"true" or "false"'
|
||||
)
|
||||
element.AddAnnotation(name, value, keep)
|
||||
@@ -2263,7 +2314,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
|
||||
v = self._remotes.get(name)
|
||||
if not v:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
f"remote {name} not defined in {self.manifestFile}"
|
||||
)
|
||||
return v
|
||||
@@ -2274,7 +2325,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
"""
|
||||
v = node.getAttribute(attname)
|
||||
if not v:
|
||||
raise ManifestParseError(
|
||||
raise error.ManifestParseError(
|
||||
"no %s in <%s> within %s"
|
||||
% (attname, node.nodeName, self.manifestFile)
|
||||
)
|
||||
@@ -2312,7 +2363,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
||||
try:
|
||||
fromRevId = fromProj.GetCommitRevisionId()
|
||||
toRevId = toProj.GetCommitRevisionId()
|
||||
except ManifestInvalidRevisionError:
|
||||
except error.ManifestInvalidRevisionError:
|
||||
diff["unreachable"].append((fromProj, toProj))
|
||||
else:
|
||||
if fromRevId != toRevId:
|
||||
|
||||
@@ -222,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.
|
||||
|
||||
|
||||
+62
-80
@@ -12,32 +12,14 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from ctypes import addressof
|
||||
from ctypes import byref
|
||||
from ctypes import c_buffer
|
||||
from ctypes import c_ubyte
|
||||
from ctypes import FormatError
|
||||
from ctypes import get_last_error
|
||||
from ctypes import Structure
|
||||
from ctypes import Union
|
||||
from ctypes import WinDLL
|
||||
from ctypes import WinError
|
||||
from ctypes.wintypes import BOOL
|
||||
from ctypes.wintypes import BOOLEAN
|
||||
from ctypes.wintypes import DWORD
|
||||
from ctypes.wintypes import HANDLE
|
||||
from ctypes.wintypes import LPCWSTR
|
||||
from ctypes.wintypes import LPDWORD
|
||||
from ctypes.wintypes import LPVOID
|
||||
from ctypes.wintypes import ULONG
|
||||
from ctypes.wintypes import USHORT
|
||||
from ctypes.wintypes import WCHAR
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
import errno
|
||||
|
||||
|
||||
kernel32 = WinDLL("kernel32", use_last_error=True)
|
||||
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
|
||||
UCHAR = c_ubyte
|
||||
UCHAR = ctypes.c_ubyte
|
||||
|
||||
# Win32 error codes
|
||||
ERROR_SUCCESS = 0
|
||||
@@ -46,11 +28,11 @@ ERROR_PRIVILEGE_NOT_HELD = 1314
|
||||
|
||||
# Win32 API entry points
|
||||
CreateSymbolicLinkW = kernel32.CreateSymbolicLinkW
|
||||
CreateSymbolicLinkW.restype = BOOLEAN
|
||||
CreateSymbolicLinkW.restype = wintypes.BOOLEAN
|
||||
CreateSymbolicLinkW.argtypes = (
|
||||
LPCWSTR, # lpSymlinkFileName In
|
||||
LPCWSTR, # lpTargetFileName In
|
||||
DWORD, # dwFlags In
|
||||
wintypes.LPCWSTR, # lpSymlinkFileName In
|
||||
wintypes.LPCWSTR, # lpTargetFileName In
|
||||
wintypes.DWORD, # dwFlags In
|
||||
)
|
||||
|
||||
# Symbolic link creation flags
|
||||
@@ -61,44 +43,44 @@ SYMBOLIC_LINK_FLAG_DIRECTORY = 0x01
|
||||
SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE = 0x02
|
||||
|
||||
GetFileAttributesW = kernel32.GetFileAttributesW
|
||||
GetFileAttributesW.restype = DWORD
|
||||
GetFileAttributesW.argtypes = (LPCWSTR,) # lpFileName In
|
||||
GetFileAttributesW.restype = wintypes.DWORD
|
||||
GetFileAttributesW.argtypes = (wintypes.LPCWSTR,) # lpFileName In
|
||||
|
||||
INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF
|
||||
FILE_ATTRIBUTE_REPARSE_POINT = 0x00400
|
||||
|
||||
CreateFileW = kernel32.CreateFileW
|
||||
CreateFileW.restype = HANDLE
|
||||
CreateFileW.restype = wintypes.HANDLE
|
||||
CreateFileW.argtypes = (
|
||||
LPCWSTR, # lpFileName In
|
||||
DWORD, # dwDesiredAccess In
|
||||
DWORD, # dwShareMode In
|
||||
LPVOID, # lpSecurityAttributes In_opt
|
||||
DWORD, # dwCreationDisposition In
|
||||
DWORD, # dwFlagsAndAttributes In
|
||||
HANDLE, # hTemplateFile In_opt
|
||||
wintypes.LPCWSTR, # lpFileName In
|
||||
wintypes.DWORD, # dwDesiredAccess In
|
||||
wintypes.DWORD, # dwShareMode In
|
||||
wintypes.LPVOID, # lpSecurityAttributes In_opt
|
||||
wintypes.DWORD, # dwCreationDisposition In
|
||||
wintypes.DWORD, # dwFlagsAndAttributes In
|
||||
wintypes.HANDLE, # hTemplateFile In_opt
|
||||
)
|
||||
|
||||
CloseHandle = kernel32.CloseHandle
|
||||
CloseHandle.restype = BOOL
|
||||
CloseHandle.argtypes = (HANDLE,) # hObject In
|
||||
CloseHandle.restype = wintypes.BOOL
|
||||
CloseHandle.argtypes = (wintypes.HANDLE,) # hObject In
|
||||
|
||||
INVALID_HANDLE_VALUE = HANDLE(-1).value
|
||||
INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value
|
||||
OPEN_EXISTING = 3
|
||||
FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
|
||||
FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
|
||||
|
||||
DeviceIoControl = kernel32.DeviceIoControl
|
||||
DeviceIoControl.restype = BOOL
|
||||
DeviceIoControl.restype = wintypes.BOOL
|
||||
DeviceIoControl.argtypes = (
|
||||
HANDLE, # hDevice In
|
||||
DWORD, # dwIoControlCode In
|
||||
LPVOID, # lpInBuffer In_opt
|
||||
DWORD, # nInBufferSize In
|
||||
LPVOID, # lpOutBuffer Out_opt
|
||||
DWORD, # nOutBufferSize In
|
||||
LPDWORD, # lpBytesReturned Out_opt
|
||||
LPVOID, # lpOverlapped Inout_opt
|
||||
wintypes.HANDLE, # hDevice In
|
||||
wintypes.DWORD, # dwIoControlCode In
|
||||
wintypes.LPVOID, # lpInBuffer In_opt
|
||||
wintypes.DWORD, # nInBufferSize In
|
||||
wintypes.LPVOID, # lpOutBuffer Out_opt
|
||||
wintypes.DWORD, # nOutBufferSize In
|
||||
wintypes.LPDWORD, # lpBytesReturned Out_opt
|
||||
wintypes.LPVOID, # lpOverlapped Inout_opt
|
||||
)
|
||||
|
||||
# Device I/O control flags and options
|
||||
@@ -108,45 +90,45 @@ IO_REPARSE_TAG_SYMLINK = 0xA000000C
|
||||
MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 0x4000
|
||||
|
||||
|
||||
class GENERIC_REPARSE_BUFFER(Structure):
|
||||
class GENERIC_REPARSE_BUFFER(ctypes.Structure):
|
||||
_fields_ = (("DataBuffer", UCHAR * 1),)
|
||||
|
||||
|
||||
class SYMBOLIC_LINK_REPARSE_BUFFER(Structure):
|
||||
class SYMBOLIC_LINK_REPARSE_BUFFER(ctypes.Structure):
|
||||
_fields_ = (
|
||||
("SubstituteNameOffset", USHORT),
|
||||
("SubstituteNameLength", USHORT),
|
||||
("PrintNameOffset", USHORT),
|
||||
("PrintNameLength", USHORT),
|
||||
("Flags", ULONG),
|
||||
("PathBuffer", WCHAR * 1),
|
||||
("SubstituteNameOffset", wintypes.USHORT),
|
||||
("SubstituteNameLength", wintypes.USHORT),
|
||||
("PrintNameOffset", wintypes.USHORT),
|
||||
("PrintNameLength", wintypes.USHORT),
|
||||
("Flags", wintypes.ULONG),
|
||||
("PathBuffer", wintypes.WCHAR * 1),
|
||||
)
|
||||
|
||||
@property
|
||||
def PrintName(self):
|
||||
arrayt = WCHAR * (self.PrintNameLength // 2)
|
||||
arrayt = wintypes.WCHAR * (self.PrintNameLength // 2)
|
||||
offset = type(self).PathBuffer.offset + self.PrintNameOffset
|
||||
return arrayt.from_address(addressof(self) + offset).value
|
||||
return arrayt.from_address(ctypes.addressof(self) + offset).value
|
||||
|
||||
|
||||
class MOUNT_POINT_REPARSE_BUFFER(Structure):
|
||||
class MOUNT_POINT_REPARSE_BUFFER(ctypes.Structure):
|
||||
_fields_ = (
|
||||
("SubstituteNameOffset", USHORT),
|
||||
("SubstituteNameLength", USHORT),
|
||||
("PrintNameOffset", USHORT),
|
||||
("PrintNameLength", USHORT),
|
||||
("PathBuffer", WCHAR * 1),
|
||||
("SubstituteNameOffset", wintypes.USHORT),
|
||||
("SubstituteNameLength", wintypes.USHORT),
|
||||
("PrintNameOffset", wintypes.USHORT),
|
||||
("PrintNameLength", wintypes.USHORT),
|
||||
("PathBuffer", wintypes.WCHAR * 1),
|
||||
)
|
||||
|
||||
@property
|
||||
def PrintName(self):
|
||||
arrayt = WCHAR * (self.PrintNameLength // 2)
|
||||
arrayt = wintypes.WCHAR * (self.PrintNameLength // 2)
|
||||
offset = type(self).PathBuffer.offset + self.PrintNameOffset
|
||||
return arrayt.from_address(addressof(self) + offset).value
|
||||
return arrayt.from_address(ctypes.addressof(self) + offset).value
|
||||
|
||||
|
||||
class REPARSE_DATA_BUFFER(Structure):
|
||||
class REPARSE_BUFFER(Union):
|
||||
class REPARSE_DATA_BUFFER(ctypes.Structure):
|
||||
class REPARSE_BUFFER(ctypes.Union):
|
||||
_fields_ = (
|
||||
("SymbolicLinkReparseBuffer", SYMBOLIC_LINK_REPARSE_BUFFER),
|
||||
("MountPointReparseBuffer", MOUNT_POINT_REPARSE_BUFFER),
|
||||
@@ -154,9 +136,9 @@ class REPARSE_DATA_BUFFER(Structure):
|
||||
)
|
||||
|
||||
_fields_ = (
|
||||
("ReparseTag", ULONG),
|
||||
("ReparseDataLength", USHORT),
|
||||
("Reserved", USHORT),
|
||||
("ReparseTag", wintypes.ULONG),
|
||||
("ReparseDataLength", wintypes.USHORT),
|
||||
("Reserved", wintypes.USHORT),
|
||||
("ReparseBuffer", REPARSE_BUFFER),
|
||||
)
|
||||
_anonymous_ = ("ReparseBuffer",)
|
||||
@@ -182,8 +164,8 @@ def _create_symlink(source, link_name, dwFlags):
|
||||
# "the unprivileged create flag is unsupported below Windows 10 (1703,
|
||||
# v10.0.14972). retry without it."
|
||||
if not CreateSymbolicLinkW(link_name, source, dwFlags):
|
||||
code = get_last_error()
|
||||
error_desc = FormatError(code).strip()
|
||||
code = ctypes.get_last_error()
|
||||
error_desc = ctypes.FormatError(code).strip()
|
||||
if code == ERROR_PRIVILEGE_NOT_HELD:
|
||||
raise OSError(errno.EPERM, error_desc, link_name)
|
||||
_raise_winerror(code, f'Error creating symbolic link "{link_name}"')
|
||||
@@ -208,10 +190,10 @@ def readlink(path):
|
||||
)
|
||||
if reparse_point_handle == INVALID_HANDLE_VALUE:
|
||||
_raise_winerror(
|
||||
get_last_error(), f'Error opening symbolic link "{path}"'
|
||||
ctypes.get_last_error(), f'Error opening symbolic link "{path}"'
|
||||
)
|
||||
target_buffer = c_buffer(MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
|
||||
n_bytes_returned = DWORD()
|
||||
target_buffer = ctypes.c_buffer(MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
|
||||
n_bytes_returned = wintypes.DWORD()
|
||||
io_result = DeviceIoControl(
|
||||
reparse_point_handle,
|
||||
FSCTL_GET_REPARSE_POINT,
|
||||
@@ -219,13 +201,13 @@ def readlink(path):
|
||||
0,
|
||||
target_buffer,
|
||||
len(target_buffer),
|
||||
byref(n_bytes_returned),
|
||||
ctypes.byref(n_bytes_returned),
|
||||
None,
|
||||
)
|
||||
CloseHandle(reparse_point_handle)
|
||||
if not io_result:
|
||||
_raise_winerror(
|
||||
get_last_error(), f'Error reading symbolic link "{path}"'
|
||||
ctypes.get_last_error(), f'Error reading symbolic link "{path}"'
|
||||
)
|
||||
rdb = REPARSE_DATA_BUFFER.from_buffer(target_buffer)
|
||||
if rdb.ReparseTag == IO_REPARSE_TAG_SYMLINK:
|
||||
@@ -239,6 +221,6 @@ def readlink(path):
|
||||
|
||||
|
||||
def _raise_winerror(code, error_desc):
|
||||
win_error_desc = FormatError(code).strip()
|
||||
win_error_desc = ctypes.FormatError(code).strip()
|
||||
error_desc = f"{error_desc}: {win_error_desc}"
|
||||
raise WinError(code, error_desc)
|
||||
raise ctypes.WinError(code, error_desc)
|
||||
+6
-4
@@ -22,7 +22,7 @@ try:
|
||||
except ImportError:
|
||||
import dummy_threading as _threading
|
||||
|
||||
from repo_trace import IsTraceToStderr
|
||||
import repo_trace
|
||||
|
||||
|
||||
# Capture the original stderr stream. We use this exclusively for progress
|
||||
@@ -159,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() or self._quiet:
|
||||
if not _TTY or repo_trace.IsTraceToStderr() or self._quiet:
|
||||
return
|
||||
|
||||
elapsed_sec = time.time() - self._start
|
||||
@@ -209,7 +211,7 @@ class Progress:
|
||||
|
||||
The progress bar is then redrawn on the next line.
|
||||
"""
|
||||
if not _TTY or IsTraceToStderr() or self._quiet:
|
||||
if not _TTY or repo_trace.IsTraceToStderr() or self._quiet:
|
||||
return
|
||||
|
||||
# Erase the current line, print the message with a newline,
|
||||
@@ -225,7 +227,7 @@ class Progress:
|
||||
self._ended = True
|
||||
|
||||
self._update_event.set()
|
||||
if not _TTY or IsTraceToStderr() or self._quiet:
|
||||
if not _TTY or repo_trace.IsTraceToStderr() or self._quiet:
|
||||
return
|
||||
|
||||
duration = duration_str(time.time() - self._start)
|
||||
|
||||
+1890
-534
File diff suppressed because it is too large.
Load diff
@@ -16,7 +16,32 @@
|
||||
line-length = 80
|
||||
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!
|
||||
e2e: Hermetic end-to-end tests (tests/e2e/). Deselect with -m 'not e2e'.
|
||||
"""
|
||||
@@ -106,7 +106,7 @@ def check_path(opts: argparse.Namespace, path: Path) -> bool:
|
||||
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])
|
||||
return all([check_path(opts, x) for x in paths]) # noqa: C419
|
||||
|
||||
|
||||
def find_files(opts: argparse.Namespace) -> list[Path]:
|
||||
|
||||
@@ -85,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
|
||||
)
|
||||
|
||||
|
||||
@@ -23,4 +23,5 @@ import sys
|
||||
import update_manpages
|
||||
|
||||
|
||||
sys.exit(update_manpages.main(sys.argv[1:]))
|
||||
if __name__ == "__main__":
|
||||
sys.exit(update_manpages.main(sys.argv[1:]))
|
||||
@@ -30,8 +30,7 @@ import tempfile
|
||||
from typing import List
|
||||
|
||||
|
||||
# NB: This script is currently imported by tests/ to unittest some logic.
|
||||
assert sys.version_info >= (3, 6), "Python 3.6+ required"
|
||||
assert sys.version_info >= (3, 9), "Release framework requires Python 3.9+"
|
||||
|
||||
|
||||
THIS_FILE = Path(__file__).resolve()
|
||||
@@ -40,7 +39,7 @@ MANDIR = TOPDIR.joinpath("man")
|
||||
|
||||
# Load repo local modules.
|
||||
sys.path.insert(0, str(TOPDIR))
|
||||
from git_command import RepoSourceVersion
|
||||
import git_command
|
||||
import subcmds
|
||||
|
||||
|
||||
@@ -66,22 +65,27 @@ def main(argv: List[str]) -> int:
|
||||
parser = get_parser()
|
||||
opts = parser.parse_args(argv)
|
||||
|
||||
if not shutil.which("help2man"):
|
||||
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"]
|
||||
(MANDIR / "repo-branch.1").write_text(".so man1/repo-branches.1")
|
||||
|
||||
version = RepoSourceVersion()
|
||||
version = git_command.RepoSourceVersion()
|
||||
cmdlist = [
|
||||
[
|
||||
"help2man",
|
||||
*help2man,
|
||||
"-N",
|
||||
"-n",
|
||||
f"repo {cmd} - manual page for repo {cmd}",
|
||||
@@ -100,7 +104,7 @@ def main(argv: List[str]) -> int:
|
||||
]
|
||||
cmdlist.append(
|
||||
[
|
||||
"help2man",
|
||||
*help2man,
|
||||
"-N",
|
||||
"-n",
|
||||
"repository management tool built on top of git",
|
||||
@@ -178,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"),
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ import platform
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import NamedTuple
|
||||
from typing import NamedTuple, Tuple
|
||||
|
||||
|
||||
# These should never be newer than the main.py version since this needs to be a
|
||||
@@ -129,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, 54)
|
||||
VERSION = (2, 71)
|
||||
|
||||
# increment this if the MAINTAINER_KEYS block is modified
|
||||
KEYRING_VERSION = (2, 3)
|
||||
@@ -220,6 +220,10 @@ 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
|
||||
# The automated release process deletes the stable branch and then recreates it
|
||||
# at the new release, so a fetch can briefly miss it. Wait this long and
|
||||
# refetch once before giving up on --repo-rev.
|
||||
REV_REFETCH_DELAY_SEC = 10
|
||||
|
||||
|
||||
import errno
|
||||
@@ -228,6 +232,7 @@ import optparse
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
@@ -379,6 +384,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")
|
||||
@@ -400,20 +410,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",
|
||||
@@ -433,6 +443,20 @@ def InitParser(parser):
|
||||
dest="use_superproject",
|
||||
help="disable use of manifest superprojects",
|
||||
)
|
||||
group.add_option(
|
||||
"-s",
|
||||
"--smart-sync",
|
||||
action="store_true",
|
||||
default=None,
|
||||
help="default to smart sync when syncing projects",
|
||||
)
|
||||
group.add_option(
|
||||
"--no-smart-sync",
|
||||
"--head",
|
||||
action="store_false",
|
||||
dest="smart_sync",
|
||||
help="do not default to smart sync when syncing projects",
|
||||
)
|
||||
group.add_option(
|
||||
"--clone-bundle",
|
||||
action="store_true",
|
||||
@@ -626,6 +650,7 @@ def _Init(args):
|
||||
dst = dst_final + ".tmp"
|
||||
shutil.rmtree(dst, ignore_errors=True)
|
||||
_Clone(url, dst, opt.clone_bundle, opt.quiet, opt.verbose)
|
||||
_RefetchIfRevMissing(url, dst, rev, opt.quiet, opt.verbose)
|
||||
|
||||
remote_ref, rev = check_repo_rev(
|
||||
dst, rev, opt.repo_verify, quiet=opt.quiet
|
||||
@@ -742,6 +767,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:
|
||||
@@ -978,7 +1025,43 @@ def _Clone(url, cwd, clone_bundle, quiet, verbose):
|
||||
_Fetch(url, cwd, "origin", quiet, verbose)
|
||||
|
||||
|
||||
def resolve_repo_rev(cwd, committish):
|
||||
def _RefetchIfRevMissing(
|
||||
url: str, cwd: str, rev: str, quiet: bool, verbose: bool
|
||||
) -> None:
|
||||
"""Refetch once if |rev| doesn't resolve.
|
||||
|
||||
This doesn't fail if |rev| still doesn't resolve; check_repo_rev reports
|
||||
that.
|
||||
|
||||
Any unresolvable |rev| is retried, not just branches. Telling a branch
|
||||
apart from a tag or commit isn't possible while it's missing, and the only
|
||||
cost for a bad --repo-rev is a short delay before the same error.
|
||||
|
||||
Args:
|
||||
url: The URL of the repo source.
|
||||
cwd: The git checkout to fetch into.
|
||||
rev: The --repo-rev to wait for.
|
||||
quiet: Whether to suppress fetch output.
|
||||
verbose: Whether to show verbose fetch output.
|
||||
"""
|
||||
try:
|
||||
resolve_repo_rev(cwd, rev, report_errors=False)
|
||||
return
|
||||
except CloneFailure:
|
||||
pass
|
||||
# Print even with --quiet since this explains the delay.
|
||||
print(
|
||||
f'repo: warning: unable to resolve "{rev}"; '
|
||||
f"refetching in {REV_REFETCH_DELAY_SEC} seconds",
|
||||
file=sys.stderr,
|
||||
)
|
||||
time.sleep(REV_REFETCH_DELAY_SEC)
|
||||
_Fetch(url, cwd, "origin", quiet, verbose)
|
||||
|
||||
|
||||
def resolve_repo_rev(
|
||||
cwd: str, committish: str, report_errors: bool = True
|
||||
) -> Tuple[str, str]:
|
||||
"""Figure out what REPO_REV represents.
|
||||
|
||||
We support:
|
||||
@@ -989,6 +1072,7 @@ def resolve_repo_rev(cwd, committish):
|
||||
Args:
|
||||
cwd: The git checkout to run in.
|
||||
committish: The REPO_REV argument to resolve.
|
||||
report_errors: Whether to print an error if it can't be resolved.
|
||||
|
||||
Returns:
|
||||
A tuple of (remote ref, commit) as makes sense for the committish.
|
||||
@@ -1007,17 +1091,18 @@ def resolve_repo_rev(cwd, committish):
|
||||
)
|
||||
return None if ret.returncode else ret.stdout.strip()
|
||||
|
||||
def error(msg: str) -> CloneFailure:
|
||||
if report_errors:
|
||||
print(f"repo: error: {msg}", file=sys.stderr)
|
||||
return CloneFailure()
|
||||
|
||||
# An explicit branch.
|
||||
if committish.startswith("refs/heads/"):
|
||||
remote_ref = committish
|
||||
committish = committish[len("refs/heads/") :]
|
||||
rev = resolve("refs/remotes/origin/%s" % committish)
|
||||
if rev is None:
|
||||
print(
|
||||
f'repo: error: unknown branch "{committish}"',
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise CloneFailure()
|
||||
raise error(f'unknown branch "{committish}"')
|
||||
return (remote_ref, rev)
|
||||
|
||||
# An explicit tag.
|
||||
@@ -1026,11 +1111,7 @@ def resolve_repo_rev(cwd, committish):
|
||||
committish = committish[len("refs/tags/") :]
|
||||
rev = resolve(remote_ref)
|
||||
if rev is None:
|
||||
print(
|
||||
f'repo: error: unknown tag "{committish}"',
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise CloneFailure()
|
||||
raise error(f'unknown tag "{committish}"')
|
||||
return (remote_ref, rev)
|
||||
|
||||
# See if it's a short branch name.
|
||||
@@ -1049,11 +1130,7 @@ def resolve_repo_rev(cwd, committish):
|
||||
return (rev, rev)
|
||||
|
||||
# Give up!
|
||||
print(
|
||||
f'repo: error: unable to resolve "{committish}"',
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise CloneFailure()
|
||||
raise error(f'unable to resolve "{committish}"')
|
||||
|
||||
|
||||
def verify_rev(cwd, remote_ref, rev, quiet):
|
||||
@@ -1074,7 +1151,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
|
||||
|
||||
|
||||
+2
-2
@@ -17,7 +17,7 @@
|
||||
import logging
|
||||
|
||||
from color import Coloring
|
||||
from error import RepoExitError
|
||||
import error
|
||||
|
||||
|
||||
SEPARATOR = "=" * 80
|
||||
@@ -71,7 +71,7 @@ class RepoLogger(logging.Logger):
|
||||
handler.setFormatter(_LogColoringFormatter(config))
|
||||
self.addHandler(handler)
|
||||
|
||||
def log_aggregated_errors(self, err: RepoExitError):
|
||||
def log_aggregated_errors(self, err: error.RepoExitError) -> None:
|
||||
"""Print all aggregated logs."""
|
||||
self.error(SEPARATOR)
|
||||
|
||||
|
||||
+3
-3
@@ -57,10 +57,10 @@ def SetTraceToStderr():
|
||||
_TRACE_TO_STDERR = True
|
||||
|
||||
|
||||
def SetTrace():
|
||||
"""Enables tracing."""
|
||||
def SetTrace(value: bool = True) -> None:
|
||||
"""Enables by default, or disables tracing."""
|
||||
global _TRACE
|
||||
_TRACE = True
|
||||
_TRACE = value
|
||||
|
||||
|
||||
def _SetTraceFile(quiet):
|
||||
|
||||
@@ -13,6 +13,18 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# /// script
|
||||
# requires-python = '>=3.11,<3.12'
|
||||
# dependencies = [
|
||||
# 'pytest==8.3.4',
|
||||
# 'black==25.1.0',
|
||||
# 'flake8==6.0.0',
|
||||
# 'flake8-bugbear==24.12.12',
|
||||
# 'flake8-comprehensions==3.16.0',
|
||||
# 'isort==5.10.1'
|
||||
# ]
|
||||
# ///
|
||||
|
||||
"""Wrapper to run linters and pytest with the right settings."""
|
||||
|
||||
import functools
|
||||
@@ -42,10 +54,16 @@ def is_ci() -> bool:
|
||||
return os.getenv("LUCI_CQ") == "yes"
|
||||
|
||||
|
||||
def ci_pytest_args() -> list[str]:
|
||||
"""Returns extra pytest args for CI runs."""
|
||||
if not is_ci():
|
||||
return []
|
||||
return ["-m", "not skip_cq", "--fail-on-skip"]
|
||||
|
||||
|
||||
def run_pytest(argv: list[str]) -> int:
|
||||
"""Returns the exit code from pytest."""
|
||||
if is_ci():
|
||||
argv = ["-m", "not skip_cq"] + argv
|
||||
argv = ci_pytest_args() + argv
|
||||
|
||||
log_cmd("pytest", argv)
|
||||
return subprocess.run(
|
||||
@@ -57,8 +75,7 @@ def run_pytest(argv: list[str]) -> int:
|
||||
|
||||
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
|
||||
argv = ci_pytest_args() + argv
|
||||
|
||||
log_cmd("[vpython 3.8] pytest", argv)
|
||||
try:
|
||||
@@ -81,6 +98,14 @@ def run_pytest_py38(argv: list[str]) -> int:
|
||||
|
||||
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 = [
|
||||
@@ -100,6 +125,14 @@ def run_black():
|
||||
|
||||
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(
|
||||
@@ -111,7 +144,15 @@ def run_flake8():
|
||||
|
||||
def run_isort():
|
||||
"""Returns the exit code from isort."""
|
||||
argv = ["--check", ROOT_DIR]
|
||||
argv = ["--version-number"]
|
||||
log_cmd("isort", argv)
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "isort"] + argv,
|
||||
check=True,
|
||||
cwd=ROOT_DIR,
|
||||
)
|
||||
|
||||
argv = ["--check", "--diff", ROOT_DIR]
|
||||
log_cmd("isort", argv)
|
||||
return subprocess.run(
|
||||
[sys.executable, "-m", "isort"] + argv,
|
||||
@@ -133,11 +174,6 @@ def run_check_metadata():
|
||||
|
||||
def run_update_manpages() -> int:
|
||||
"""Returns the exit code from release/update-manpages."""
|
||||
# Allow this to fail on CI, but not local devs.
|
||||
if is_ci() and not shutil.which("help2man"):
|
||||
print("update-manpages: help2man not found; skipping test")
|
||||
return 0
|
||||
|
||||
argv = ["--check"]
|
||||
log_cmd("release/update-manpages", argv)
|
||||
return subprocess.run(
|
||||
@@ -147,8 +183,24 @@ def run_update_manpages() -> int:
|
||||
).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 = (
|
||||
functools.partial(run_pytest, argv),
|
||||
functools.partial(run_pytest_py38, argv),
|
||||
|
||||
Generated
+101
@@ -0,0 +1,101 @@
|
||||
attrs==24.2.0 \
|
||||
--hash=sha256:37c4653e51383dc1249dba15635d59011ca11714a8e61db8cd6dc73011e4b603 \
|
||||
--hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2
|
||||
# via flake8-bugbear
|
||||
black==25.1.0 \
|
||||
--hash=sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717 \
|
||||
--hash=sha256:c9ed033e41998dd45d6f19bf9923e2444cb401185ccb26d1a2101a9049637c1f
|
||||
click==8.0.3 \
|
||||
--hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \
|
||||
--hash=sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3 \
|
||||
--hash=sha256:b0a0492d07c8e7e4a88ee153ca6553d357a0746792d6868bfca79dc1a344bb1b
|
||||
# via black
|
||||
colorama==0.4.6 ; sys_platform == 'win32' \
|
||||
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 \
|
||||
--hash=sha256:8a5a9494346d7ae2d8f599e4661738007d2df56dbbf4ccbbf7a6a23679a3a302
|
||||
# via
|
||||
# click
|
||||
# pytest
|
||||
flake8==6.0.0 \
|
||||
--hash=sha256:9b5bf0ebd06aff1601e38dfc4e91f9b99ba3cdf446e74ee25a9cbbe01aef81ee \
|
||||
--hash=sha256:f34b5cb8bf908ff49e4673d3dbc557b2381a14132b637d53e9d3a59816f4b6ba
|
||||
# via
|
||||
# flake8-bugbear
|
||||
# flake8-comprehensions
|
||||
flake8-bugbear==24.12.12 \
|
||||
--hash=sha256:1b6967436f65ca22a42e5373aaa6f2d87966ade9aa38d4baf2a1be550767545e
|
||||
flake8-comprehensions==3.16.0 \
|
||||
--hash=sha256:7c1eadc9d22e765f39857798febe7766b4d9c519793c6c149e3e13bf99693f70
|
||||
iniconfig==1.1.1 \
|
||||
--hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \
|
||||
--hash=sha256:9d81514370059186276e9a1799e5a9752e4c060233c254d6954b53fad2ff8a5e
|
||||
# via pytest
|
||||
isort==5.10.1 \
|
||||
--hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
|
||||
--hash=sha256:ec0da0d88470e89ade75bf1e2ea7b4349ec906f95c84aff08e4d41dea6864d25
|
||||
mccabe==0.7.0 \
|
||||
--hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e \
|
||||
--hash=sha256:b85abd0a321cdd357d0be34adbf49cf01453a857a23e94d44612ec6f159cb261
|
||||
# via flake8
|
||||
mypy-extensions==0.4.3 \
|
||||
--hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \
|
||||
--hash=sha256:38cbac35a502c4f13b9d9b1d06258fa51e01da4f5828b171b4d1200d00378e7c
|
||||
# via black
|
||||
packaging==23.0 \
|
||||
--hash=sha256:37cb5b4759d17ae6bad78ee3347f0ee1057452895fcd1bbff48fb12908ce00a1 \
|
||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2
|
||||
# via
|
||||
# black
|
||||
# pytest
|
||||
pathspec==0.9.0 \
|
||||
--hash=sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a \
|
||||
--hash=sha256:aaf90f4ad38a16bd3ebe0fa64da7f6ee01819c881120db68cc6440941a9adc08
|
||||
# via black
|
||||
platformdirs==2.5.2 \
|
||||
--hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \
|
||||
--hash=sha256:e257a04b5d0528cb9836b766c8a16a85368e249cba7ae42888a403976ccd7d2d
|
||||
# via black
|
||||
pluggy==1.5.0 \
|
||||
--hash=sha256:40da5bf96b15e359e280fd760a80c11d7a8ea20c3c296c3f3e09f8be67cec10b \
|
||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 \
|
||||
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669
|
||||
# via pytest
|
||||
py==1.11.0 \
|
||||
--hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 \
|
||||
--hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378
|
||||
pycodestyle==2.10.0 \
|
||||
--hash=sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610 \
|
||||
--hash=sha256:aacf8d28ecea10fad31b2d14943a42d85d007f4d2fee8f212fc05751c8460647
|
||||
# via flake8
|
||||
pyflakes==3.0.1 \
|
||||
--hash=sha256:a968b8baa43926124b692c7e76279bc5fa8c6ef9a2281e4a2a475be69f954bdf \
|
||||
--hash=sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf
|
||||
# via flake8
|
||||
pyparsing==3.0.7 \
|
||||
--hash=sha256:943c220ca64e93940b74dd070152ed9ef1402bc3b28fd1f899843bee34e83708 \
|
||||
--hash=sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484
|
||||
pytest==8.3.4 \
|
||||
--hash=sha256:00cba7845d2dccfe5403025a688d60ceae228b1ddaa866d2eb86b2edb6923e73 \
|
||||
--hash=sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6
|
||||
toml==0.10.1 \
|
||||
--hash=sha256:4099063d9b50219bf09bfa693ab8dcd96ce585cee9fdadce5a48a08ee23b0646 \
|
||||
--hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88
|
||||
tomli==2.0.1 \
|
||||
--hash=sha256:864f310e021d68415d6ba45b23e29808f99703b471d884267d6942eecdf8d6c5 \
|
||||
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc
|
||||
typing-extensions==4.3.0 \
|
||||
--hash=sha256:09126dab54422810a541e078c37e62024e7260a022d4f67c075a6707d9553649 \
|
||||
--hash=sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02
|
||||
@@ -1,125 +0,0 @@
|
||||
# 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.11"
|
||||
|
||||
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/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"
|
||||
>
|
||||
|
||||
# NB: Keep in sync with constraints.txt.
|
||||
wheel: <
|
||||
name: "infra/python/wheels/black-py3"
|
||||
version: "version:25.1.0"
|
||||
>
|
||||
|
||||
# Required by black==25.1.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/mypy-extensions-py3"
|
||||
version: "version:0.4.3"
|
||||
>
|
||||
|
||||
# Required by black==25.1.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/tomli-py3"
|
||||
version: "version:2.0.1"
|
||||
>
|
||||
|
||||
# Required by black==25.1.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/platformdirs-py3"
|
||||
version: "version:2.5.2"
|
||||
>
|
||||
|
||||
# Required by black==25.1.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/pathspec-py3"
|
||||
version: "version:0.9.0"
|
||||
>
|
||||
|
||||
# Required by black==25.1.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/typing-extensions-py3"
|
||||
version: "version:4.3.0"
|
||||
>
|
||||
|
||||
# Required by black==25.1.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/click-py3"
|
||||
version: "version:8.0.3"
|
||||
>
|
||||
|
||||
wheel: <
|
||||
name: "infra/python/wheels/flake8-py2_py3"
|
||||
version: "version:6.0.0"
|
||||
>
|
||||
|
||||
# Required by flake8==6.0.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/mccabe-py2_py3"
|
||||
version: "version:0.7.0"
|
||||
>
|
||||
|
||||
# Required by flake8==6.0.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/pyflakes-py2_py3"
|
||||
version: "version:3.0.1"
|
||||
>
|
||||
|
||||
# Required by flake8==6.0.0
|
||||
wheel: <
|
||||
name: "infra/python/wheels/pycodestyle-py2_py3"
|
||||
version: "version:2.10.0"
|
||||
>
|
||||
|
||||
wheel: <
|
||||
name: "infra/python/wheels/isort-py3"
|
||||
version: "version:5.10.1"
|
||||
>
|
||||
@@ -24,9 +24,9 @@ import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from git_command import git
|
||||
import git_command
|
||||
import platform_utils
|
||||
from repo_trace import Trace
|
||||
import repo_trace
|
||||
|
||||
|
||||
PROXY_PATH = os.path.join(os.path.dirname(__file__), "git_ssh")
|
||||
@@ -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):
|
||||
@@ -192,7 +201,9 @@ class ProxyManager:
|
||||
# is the same as 'myhost.com' where "User git" is setup in the user's
|
||||
# ~/.ssh/config file.
|
||||
check_command = command_base + ["-O", "check"]
|
||||
with Trace("Call to ssh (check call): %s", " ".join(check_command)):
|
||||
with repo_trace.Trace(
|
||||
"Call to ssh (check call): %s", " ".join(check_command)
|
||||
):
|
||||
try:
|
||||
check_process = subprocess.Popen(
|
||||
check_command,
|
||||
@@ -241,7 +252,7 @@ class ProxyManager:
|
||||
)
|
||||
p = None
|
||||
try:
|
||||
with Trace("Call to ssh: %s", " ".join(command)):
|
||||
with repo_trace.Trace("Call to ssh: %s", " ".join(command)):
|
||||
p = subprocess.Popen(command)
|
||||
except Exception as e:
|
||||
self._master_broken.value = True
|
||||
@@ -282,6 +293,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 +320,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
|
||||
@@ -342,7 +359,7 @@ def _get_git_protocol_version() -> str:
|
||||
if e.returncode == 1:
|
||||
# Exit code 1 means that the git config key was not found.
|
||||
# Try to imitate the defaults that git would have used.
|
||||
git_version = git.version_tuple()
|
||||
git_version = git_command.git.version_tuple()
|
||||
if git_version >= (2, 26, 0):
|
||||
# Since git version 2.26, protocol v2 is the default.
|
||||
return "2"
|
||||
|
||||
+7
-8
@@ -18,17 +18,16 @@ import itertools
|
||||
|
||||
from command import Command
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
from error import RepoError
|
||||
from error import RepoExitError
|
||||
from git_command import git
|
||||
import error
|
||||
import git_command
|
||||
from progress import Progress
|
||||
from repo_logging import RepoLogger
|
||||
import repo_logging
|
||||
|
||||
|
||||
logger = RepoLogger(__file__)
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
|
||||
|
||||
class AbandonError(RepoExitError):
|
||||
class AbandonError(error.RepoExitError):
|
||||
"""Exit error when abandon command fails."""
|
||||
|
||||
|
||||
@@ -59,7 +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}")
|
||||
x for x in branches if not git_command.IsValidBranchName(x)
|
||||
]
|
||||
|
||||
if invalid_branches:
|
||||
@@ -84,7 +83,7 @@ It is equivalent to "git branch -D <branchname>".
|
||||
status = None
|
||||
try:
|
||||
status = project.AbandonBranch(name)
|
||||
except RepoError as e:
|
||||
except error.RepoError as e:
|
||||
status = False
|
||||
errors.append(e)
|
||||
if status is not None:
|
||||
|
||||
+9
-10
@@ -17,13 +17,12 @@ from typing import NamedTuple
|
||||
|
||||
from command import Command
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
from error import GitError
|
||||
from error import RepoExitError
|
||||
import error
|
||||
from progress import Progress
|
||||
from repo_logging import RepoLogger
|
||||
import repo_logging
|
||||
|
||||
|
||||
logger = RepoLogger(__file__)
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
|
||||
|
||||
class CheckoutBranchResult(NamedTuple):
|
||||
@@ -33,11 +32,11 @@ class CheckoutBranchResult(NamedTuple):
|
||||
error: Exception
|
||||
|
||||
|
||||
class CheckoutCommandError(RepoExitError):
|
||||
class CheckoutCommandError(error.RepoExitError):
|
||||
"""Exception thrown when checkout command fails."""
|
||||
|
||||
|
||||
class MissingBranchError(RepoExitError):
|
||||
class MissingBranchError(error.RepoExitError):
|
||||
"""Exception thrown when no project has specified branch."""
|
||||
|
||||
|
||||
@@ -64,14 +63,14 @@ The command is equivalent to:
|
||||
@classmethod
|
||||
def _ExecuteOne(cls, nb, project_idx):
|
||||
"""Checkout one project."""
|
||||
error = None
|
||||
err = 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_idx, error)
|
||||
except error.GitError as e:
|
||||
err = e
|
||||
return CheckoutBranchResult(result, project_idx, err)
|
||||
|
||||
def Execute(self, opt, args):
|
||||
nb = args[0]
|
||||
|
||||
+53
-38
@@ -14,15 +14,16 @@
|
||||
|
||||
import re
|
||||
import sys
|
||||
from typing import Tuple
|
||||
|
||||
from command import Command
|
||||
from error import GitError
|
||||
from git_command import GitCommand
|
||||
from repo_logging import RepoLogger
|
||||
import error
|
||||
import git_command
|
||||
import repo_logging
|
||||
|
||||
|
||||
CHANGE_ID_RE = re.compile(r"^\s*Change-Id: I([0-9a-f]{40})\s*$")
|
||||
logger = RepoLogger(__file__)
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
|
||||
|
||||
class CherryPick(Command):
|
||||
@@ -43,38 +44,10 @@ change id will be added.
|
||||
|
||||
def Execute(self, opt, args):
|
||||
reference = args[0]
|
||||
sha1, commit = self._ResolveReference(reference)
|
||||
old_msg = self._StripHeader(commit)
|
||||
|
||||
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)
|
||||
|
||||
p = GitCommand(
|
||||
p = git_command.GitCommand(
|
||||
None,
|
||||
["cherry-pick", sha1],
|
||||
capture_stdout=True,
|
||||
@@ -84,7 +57,7 @@ change id will be added.
|
||||
|
||||
try:
|
||||
p.Wait()
|
||||
except GitError as e:
|
||||
except error.GitError as e:
|
||||
logger.error(e)
|
||||
logger.warning(
|
||||
"NOTE: When committing (please see above) and editing the "
|
||||
@@ -103,7 +76,7 @@ change id will be added.
|
||||
# the commit message.
|
||||
new_msg = self._Reformat(old_msg, sha1)
|
||||
|
||||
p = GitCommand(
|
||||
p = git_command.GitCommand(
|
||||
None,
|
||||
["commit", "--amend", "-F", "-"],
|
||||
input=new_msg,
|
||||
@@ -113,10 +86,52 @@ change id will be added.
|
||||
)
|
||||
try:
|
||||
p.Wait()
|
||||
except GitError:
|
||||
except error.GitError:
|
||||
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 = git_command.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 error.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 (error.GitError, ValueError) as e:
|
||||
logger.error(
|
||||
"error: Failed to resolve or read commit %s", reference
|
||||
)
|
||||
if isinstance(e, error.GitError):
|
||||
raise
|
||||
raise error.GitError(str(e)) from e
|
||||
|
||||
return sha1, commit
|
||||
|
||||
def _IsChangeId(self, line):
|
||||
return CHANGE_ID_RE.match(line)
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# 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.
|
||||
|
||||
import optparse
|
||||
from typing import List, Optional
|
||||
|
||||
import command
|
||||
import error
|
||||
import git_config
|
||||
|
||||
|
||||
class Error(error.RepoExitError):
|
||||
"""Exit error when the config command fails."""
|
||||
|
||||
|
||||
class NotSetError(error.SilentRepoExitError):
|
||||
"""Exit quietly when a requested setting is not set, like `git config`."""
|
||||
|
||||
|
||||
# Subcommand name -> number of positional arguments it takes.
|
||||
_ACTIONS = {
|
||||
"get": 1,
|
||||
"set": 2,
|
||||
"unset": 1,
|
||||
"list": 0,
|
||||
}
|
||||
|
||||
|
||||
def _CanonicalizeBool(name: str, value: str) -> str:
|
||||
"""Returns |value| as "true"/"false", or raises Error."""
|
||||
ret = git_config.ParseBoolean(value)
|
||||
if ret is None:
|
||||
raise Error(f"error: {name}: '{value}' is not a boolean")
|
||||
return "true" if ret else "false"
|
||||
|
||||
|
||||
def _CanonicalizeInt(name: str, value: str) -> str:
|
||||
"""Returns |value| as a plain decimal integer, or raises Error."""
|
||||
ret = git_config.ParseInt(value)
|
||||
if ret is None:
|
||||
raise Error(f"error: {name}: '{value}' is not an integer")
|
||||
return str(ret)
|
||||
|
||||
|
||||
# --type name -> function turning a value into its canonical string form.
|
||||
_TYPES = {
|
||||
"bool": _CanonicalizeBool,
|
||||
"int": _CanonicalizeInt,
|
||||
}
|
||||
|
||||
|
||||
class Config(command.Command, command.MirrorSafeCommand):
|
||||
"""Read and write repo's per-user settings."""
|
||||
|
||||
COMMON = True
|
||||
MULTI_MANIFEST_SUPPORT = True
|
||||
helpSummary = "Read and write per-user repo settings"
|
||||
helpUsage = """
|
||||
%prog [--type=<type>] [-z] get <name>
|
||||
%prog [--type=<type>] set <name> <value>
|
||||
%prog unset <name>
|
||||
%prog [-z] list
|
||||
"""
|
||||
helpDescription = """
|
||||
The '%prog' command reads and writes repo's own per-user settings. These
|
||||
are separate from git's config and from a checkout's settings; see
|
||||
docs/repoconfig.md for where they live and which settings repo understands.
|
||||
|
||||
Settings are named <section>.<name>, e.g. sync.smartsync. 'get' prints
|
||||
every value of the setting and exits 1 without output if it is not set,
|
||||
like `git config`.
|
||||
|
||||
Values are stored as strings. With --type, 'set' validates the value and
|
||||
stores it in canonical form (e.g. "on" as "true", "1k" as "1024"), and
|
||||
'get' prints the canonical form, like `git config --type`.
|
||||
"""
|
||||
|
||||
def _Options(self, p: optparse.OptionParser) -> None:
|
||||
p.add_option(
|
||||
"--type",
|
||||
choices=sorted(_TYPES),
|
||||
help="validate and canonicalize the value as this type for "
|
||||
"get and set (%s)" % ", ".join(sorted(_TYPES)),
|
||||
)
|
||||
p.add_option(
|
||||
"-z",
|
||||
"--null",
|
||||
action="store_true",
|
||||
help="terminate output of get and list with NUL instead of "
|
||||
"newline, and separate key and value with a newline",
|
||||
)
|
||||
|
||||
def ValidateOptions(self, opt: optparse.Values, args: List[str]) -> None:
|
||||
if not args:
|
||||
self.Usage()
|
||||
action, rest = args[0], args[1:]
|
||||
if action not in _ACTIONS:
|
||||
self.OptionParser.error(f"unknown action '{action}'")
|
||||
if len(rest) != _ACTIONS[action]:
|
||||
self.Usage()
|
||||
if opt.type and action not in ("get", "set"):
|
||||
self.OptionParser.error("--type only applies to get and set")
|
||||
if opt.null and action not in ("get", "list"):
|
||||
self.OptionParser.error("-z/--null only applies to get and list")
|
||||
|
||||
def Execute(self, opt: optparse.Values, args: List[str]) -> None:
|
||||
config = git_config.RepoConfig.ForUser()
|
||||
action, rest = args[0], args[1:]
|
||||
dispatch = {
|
||||
"get": self._ExecuteGet,
|
||||
"set": self._ExecuteSet,
|
||||
"unset": self._ExecuteUnset,
|
||||
"list": self._ExecuteList,
|
||||
}[action]
|
||||
dispatch(opt, config, *rest)
|
||||
|
||||
@staticmethod
|
||||
def _Print(opt: optparse.Values, text: str) -> None:
|
||||
"""Prints |text| terminated by NUL with -z, else by newline."""
|
||||
print(text, end="\0" if opt.null else "\n")
|
||||
|
||||
@staticmethod
|
||||
def _Format(opt: optparse.Values, name: str, value: Optional[str]) -> str:
|
||||
"""Formats a value for output, canonicalizing it per --type."""
|
||||
if value is None:
|
||||
# A key without `=` in the file has no value; git prints a blank.
|
||||
return ""
|
||||
if opt.type:
|
||||
return _TYPES[opt.type](name, value)
|
||||
return value
|
||||
|
||||
def _ExecuteGet(
|
||||
self, opt: optparse.Values, config: git_config.RepoConfig, name: str
|
||||
) -> None:
|
||||
values = config.GetString(name, all_keys=True)
|
||||
if not values:
|
||||
raise NotSetError(exit_code=1)
|
||||
for value in values:
|
||||
self._Print(opt, self._Format(opt, name, value))
|
||||
|
||||
def _ExecuteSet(
|
||||
self,
|
||||
opt: optparse.Values,
|
||||
config: git_config.RepoConfig,
|
||||
name: str,
|
||||
value: str,
|
||||
) -> None:
|
||||
if opt.type:
|
||||
value = _TYPES[opt.type](name, value)
|
||||
self._Write(config, name, value)
|
||||
|
||||
def _ExecuteUnset(
|
||||
self, opt: optparse.Values, config: git_config.RepoConfig, name: str
|
||||
) -> None:
|
||||
self._Write(config, name, None)
|
||||
|
||||
def _ExecuteList(
|
||||
self, opt: optparse.Values, config: git_config.RepoConfig
|
||||
) -> None:
|
||||
sep = "\n" if opt.null else "="
|
||||
for key in sorted(config.DumpConfigDict()):
|
||||
for value in config.GetString(key, all_keys=True):
|
||||
self._Print(opt, key if value is None else f"{key}{sep}{value}")
|
||||
|
||||
@staticmethod
|
||||
def _Write(
|
||||
config: git_config.RepoConfig, name: str, value: Optional[str]
|
||||
) -> None:
|
||||
"""Writes |value| (None to delete) via `git config`, which also
|
||||
validates the name."""
|
||||
try:
|
||||
config.SetString(name, value)
|
||||
except error.GitError as e:
|
||||
raise Error(str(e))
|
||||
+9
-11
@@ -16,17 +16,15 @@ import re
|
||||
import sys
|
||||
|
||||
from command import Command
|
||||
from error import GitError
|
||||
from error import NoSuchProjectError
|
||||
from error import RepoExitError
|
||||
from repo_logging import RepoLogger
|
||||
import error
|
||||
import repo_logging
|
||||
|
||||
|
||||
CHANGE_RE = re.compile(r"^([1-9][0-9]*)(?:[/\.-]([1-9][0-9]*))?$")
|
||||
logger = RepoLogger(__file__)
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
|
||||
|
||||
class DownloadCommandError(RepoExitError):
|
||||
class DownloadCommandError(error.RepoExitError):
|
||||
"""Error raised when download command fails."""
|
||||
|
||||
|
||||
@@ -107,7 +105,7 @@ If no project is specified try to use current directory as a project.
|
||||
# If the cwd is one of the projects, assume they want that.
|
||||
try:
|
||||
project = self.GetProjects(".")[0]
|
||||
except NoSuchProjectError:
|
||||
except error.NoSuchProjectError:
|
||||
project = None
|
||||
if project not in projects:
|
||||
logger.error(
|
||||
@@ -121,7 +119,7 @@ If no project is specified try to use current directory as a project.
|
||||
project.RelPath(local=opt.this_manifest_only),
|
||||
project.revisionExpr,
|
||||
)
|
||||
raise NoSuchProjectError()
|
||||
raise error.NoSuchProjectError()
|
||||
else:
|
||||
project = projects[0]
|
||||
print("Defaulting to cwd project", project.name)
|
||||
@@ -143,7 +141,7 @@ If no project is specified try to use current directory as a project.
|
||||
try:
|
||||
self._ExecuteHelper(opt, args)
|
||||
except Exception as e:
|
||||
if isinstance(e, RepoExitError):
|
||||
if isinstance(e, error.RepoExitError):
|
||||
raise e
|
||||
raise DownloadCommandError(aggregate_errors=[e])
|
||||
|
||||
@@ -195,14 +193,14 @@ If no project is specified try to use current directory as a project.
|
||||
elif opt.revert:
|
||||
project._Revert(dl.commit)
|
||||
elif opt.ffonly:
|
||||
project._FastForward(dl.commit, ffonly=True)
|
||||
project._FastForward(dl.commit)
|
||||
else:
|
||||
if opt.branch:
|
||||
project.StartBranch(opt.branch, revision=dl.commit)
|
||||
else:
|
||||
project._Checkout(dl.commit)
|
||||
|
||||
except GitError:
|
||||
except error.GitError:
|
||||
logger.error(
|
||||
"[%s] Could not complete the %s of %s",
|
||||
project.name,
|
||||
|
||||
+18
-32
@@ -25,11 +25,10 @@ from color import Coloring
|
||||
from command import Command
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
from command import MirrorSafeCommand
|
||||
from error import ManifestInvalidRevisionError
|
||||
from repo_logging import RepoLogger
|
||||
import repo_logging
|
||||
|
||||
|
||||
logger = RepoLogger(__file__)
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
_CAN_COLOR = [
|
||||
"branch",
|
||||
"diff",
|
||||
@@ -102,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
|
||||
@@ -236,19 +243,15 @@ without iterating through the remaining projects.
|
||||
|
||||
mirror = self.manifest.IsMirror
|
||||
|
||||
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)
|
||||
projects = self.FindProjects(
|
||||
args,
|
||||
groups=opt.groups,
|
||||
all_manifests=all_trees,
|
||||
)
|
||||
elif opt.inverse_regex:
|
||||
projects = self.FindProjects(
|
||||
args, inverse=True, all_manifests=all_trees
|
||||
args, inverse=True, groups=opt.groups, all_manifests=all_trees
|
||||
)
|
||||
else:
|
||||
projects = self.GetProjects(
|
||||
@@ -339,25 +342,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)
|
||||
|
||||
+53
-17
@@ -16,7 +16,7 @@ import os
|
||||
from typing import List, Set
|
||||
|
||||
from command import Command
|
||||
from git_command import GitCommand
|
||||
import git_command
|
||||
import platform_utils
|
||||
from progress import Progress
|
||||
from project import Project
|
||||
@@ -27,6 +27,14 @@ class Gc(Command):
|
||||
helpSummary = "Cleaning up internal repo and Git state."
|
||||
helpUsage = """
|
||||
%prog
|
||||
"""
|
||||
helpDescription = """
|
||||
The '%prog' command removes internal Git data for projects that are no
|
||||
longer active in the current configuration.
|
||||
|
||||
Projects excluded by the configured manifest groups are inactive by
|
||||
default. Use --keep-manifest-projects to keep Git data for projects that
|
||||
are still in the manifests.
|
||||
"""
|
||||
|
||||
def _Options(self, p):
|
||||
@@ -52,6 +60,12 @@ class Gc(Command):
|
||||
help="repack all projects that use partial clone with "
|
||||
"filter=blob:none",
|
||||
)
|
||||
p.add_option(
|
||||
"--keep-manifest-projects",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="keep Git data for projects still in the manifests",
|
||||
)
|
||||
|
||||
def _find_git_to_delete(
|
||||
self, to_keep: Set[str], start_dir: str
|
||||
@@ -185,13 +199,13 @@ class Gc(Command):
|
||||
|
||||
# Prepare workspace for repacking - remove all unreachable refs and
|
||||
# their objects.
|
||||
GitCommand(
|
||||
git_command.GitCommand(
|
||||
project,
|
||||
["reflog", "expire", "--expire-unreachable=all"],
|
||||
verify_command=True,
|
||||
).Wait()
|
||||
pm.update(msg=f"{project.name} | gc", inc=0)
|
||||
GitCommand(
|
||||
git_command.GitCommand(
|
||||
project,
|
||||
["gc"],
|
||||
verify_command=True,
|
||||
@@ -199,11 +213,12 @@ class Gc(Command):
|
||||
|
||||
# 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(
|
||||
remote_objects_cmd = git_command.GitCommand(
|
||||
project,
|
||||
[
|
||||
"rev-list",
|
||||
"--objects",
|
||||
"--missing=allow-promisor",
|
||||
f"--remotes={project.remote.name}",
|
||||
"--filter=blob:none",
|
||||
"--tags",
|
||||
@@ -213,17 +228,23 @@ class Gc(Command):
|
||||
)
|
||||
|
||||
# Get all local objects and pack them.
|
||||
local_head_objects_cmd = GitCommand(
|
||||
project,
|
||||
["rev-list", "--objects", "HEAD^{tree}"],
|
||||
capture_stdout=True,
|
||||
verify_command=True,
|
||||
)
|
||||
local_objects_cmd = GitCommand(
|
||||
local_head_objects_cmd = git_command.GitCommand(
|
||||
project,
|
||||
[
|
||||
"rev-list",
|
||||
"--objects",
|
||||
"--missing=allow-promisor",
|
||||
"HEAD^{tree}",
|
||||
],
|
||||
capture_stdout=True,
|
||||
verify_command=True,
|
||||
)
|
||||
local_objects_cmd = git_command.GitCommand(
|
||||
project,
|
||||
[
|
||||
"rev-list",
|
||||
"--objects",
|
||||
"--missing=allow-promisor",
|
||||
"--all",
|
||||
"--reflog",
|
||||
"--indexed-objects",
|
||||
@@ -238,7 +259,7 @@ class Gc(Command):
|
||||
remote_objects_cmd.Wait()
|
||||
|
||||
pm.update(msg=f"{project.name} | remote repack", inc=0)
|
||||
GitCommand(
|
||||
git_command.GitCommand(
|
||||
project,
|
||||
["pack-objects", os.path.join(pack_dir, "pack")],
|
||||
input=remote_objects_cmd.stdout,
|
||||
@@ -254,7 +275,7 @@ class Gc(Command):
|
||||
local_objects_cmd.Wait()
|
||||
|
||||
pm.update(msg=f"{project.name} | local repack", inc=0)
|
||||
GitCommand(
|
||||
git_command.GitCommand(
|
||||
project,
|
||||
["pack-objects", os.path.join(pack_dir, "pack")],
|
||||
input=local_head_objects_cmd.stdout + local_objects_cmd.stdout,
|
||||
@@ -284,11 +305,26 @@ class Gc(Command):
|
||||
args, all_manifests=not opt.this_manifest_only
|
||||
)
|
||||
|
||||
ret = self.delete_unused_projects(projects, opt)
|
||||
if opt.keep_manifest_projects:
|
||||
projects_to_keep = self.GetProjects(
|
||||
[],
|
||||
groups="all",
|
||||
missing_ok=True,
|
||||
all_manifests=not opt.this_manifest_only,
|
||||
)
|
||||
elif args:
|
||||
projects_to_keep = self.GetProjects(
|
||||
[], all_manifests=not opt.this_manifest_only
|
||||
)
|
||||
else:
|
||||
projects_to_keep = projects
|
||||
|
||||
ret = self.delete_unused_projects(projects_to_keep, opt)
|
||||
if ret != 0:
|
||||
return ret
|
||||
|
||||
if not opt.repack:
|
||||
return
|
||||
if opt.repack:
|
||||
git_command.git_require((2, 17, 0), fail=True, msg="--repack")
|
||||
ret = self.repack_projects(projects, opt)
|
||||
|
||||
return self.repack_projects(projects, opt)
|
||||
return ret
|
||||
+14
-16
@@ -19,14 +19,12 @@ from typing import NamedTuple
|
||||
from color import Coloring
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
from command import PagedCommand
|
||||
from error import GitError
|
||||
from error import InvalidArgumentsError
|
||||
from error import SilentRepoExitError
|
||||
from git_command import GitCommand
|
||||
from repo_logging import RepoLogger
|
||||
import error
|
||||
import git_command
|
||||
import repo_logging
|
||||
|
||||
|
||||
logger = RepoLogger(__file__)
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
|
||||
|
||||
class GrepColoring(Coloring):
|
||||
@@ -43,10 +41,10 @@ class ExecuteOneResult(NamedTuple):
|
||||
rc: int
|
||||
stdout: str
|
||||
stderr: str
|
||||
error: GitError
|
||||
error: error.GitError
|
||||
|
||||
|
||||
class GrepCommandError(SilentRepoExitError):
|
||||
class GrepCommandError(error.SilentRepoExitError):
|
||||
"""Grep command failure. Since Grep command
|
||||
output already outputs errors ensure that
|
||||
aggregate errors exit silently."""
|
||||
@@ -93,7 +91,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("(")
|
||||
@@ -265,7 +263,7 @@ contain a line that matches both expressions:
|
||||
"""Process one project."""
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
try:
|
||||
p = GitCommand(
|
||||
p = git_command.GitCommand(
|
||||
project,
|
||||
cmd_argv,
|
||||
bare=False,
|
||||
@@ -273,16 +271,16 @@ contain a line that matches both expressions:
|
||||
capture_stderr=True,
|
||||
verify_command=True,
|
||||
)
|
||||
except GitError as e:
|
||||
except error.GitError as e:
|
||||
return ExecuteOneResult(project_idx, -1, None, str(e), e)
|
||||
|
||||
try:
|
||||
error = None
|
||||
err = None
|
||||
rc = p.Wait()
|
||||
except GitError as e:
|
||||
except error.GitError as e:
|
||||
rc = 1
|
||||
error = e
|
||||
return ExecuteOneResult(project_idx, rc, p.stdout, p.stderr, error)
|
||||
err = e
|
||||
return ExecuteOneResult(project_idx, rc, p.stdout, p.stderr, err)
|
||||
|
||||
@staticmethod
|
||||
def _ProcessResults(
|
||||
@@ -377,7 +375,7 @@ contain a line that matches both expressions:
|
||||
if "--cached" in cmd_argv:
|
||||
msg = "fatal: cannot combine --cached and --revision"
|
||||
logger.error(msg)
|
||||
raise InvalidArgumentsError(msg)
|
||||
raise error.InvalidArgumentsError(msg)
|
||||
have_rev = True
|
||||
cmd_argv.extend(opt.revision)
|
||||
cmd_argv.append("--")
|
||||
|
||||
+5
-7
@@ -19,12 +19,12 @@ import textwrap
|
||||
from color import Coloring
|
||||
from command import MirrorSafeCommand
|
||||
from command import PagedCommand
|
||||
from error import RepoExitError
|
||||
import error
|
||||
from subcmds import all_commands
|
||||
from wrapper import Wrapper
|
||||
|
||||
|
||||
class InvalidHelpCommand(RepoExitError):
|
||||
class InvalidHelpCommand(error.RepoExitError):
|
||||
"""Invalid command passed into help."""
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+376
-126
@@ -12,12 +12,38 @@
|
||||
# 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, Optional, Tuple
|
||||
|
||||
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
|
||||
import git_refs
|
||||
|
||||
|
||||
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):
|
||||
@@ -27,10 +53,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(
|
||||
@@ -46,6 +73,30 @@ class Info(PagedCommand):
|
||||
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",
|
||||
@@ -72,8 +123,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")
|
||||
@@ -84,157 +164,327 @@ 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.GetManifestGroupsStr()
|
||||
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()
|
||||
sp = self.manifest.superproject
|
||||
srev = sp.commit_id if sp and sp.commit_id else "None"
|
||||
self.heading("Superproject revision: ")
|
||||
self.headtext(srev)
|
||||
self.out.nl()
|
||||
|
||||
self.printSeparator()
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _GetCurrentBranch(branches: Dict[str, Any]) -> Optional[str]:
|
||||
"""Return the name of the current branch from a GetBranches mapping."""
|
||||
return next(
|
||||
(name for name, branch in branches.items() if branch.current), None
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _getProjectData(cls, project) -> Dict[str, Any]:
|
||||
"""Gather project data as a dict."""
|
||||
branches = project.GetBranches()
|
||||
currentBranch = cls._GetCurrentBranch(branches)
|
||||
data = {
|
||||
"name": project.name,
|
||||
"mount_path": project.worktree,
|
||||
"current_revision": project.GetHeadRevisionId()
|
||||
or project.GetRevisionId(),
|
||||
"manifest_revision": project.revisionExpr,
|
||||
"local_branches": list(branches),
|
||||
}
|
||||
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")
|
||||
|
||||
branches = project.GetBranches()
|
||||
currentBranch = cls._GetCurrentBranch(branches)
|
||||
|
||||
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()
|
||||
|
||||
if currentBranch:
|
||||
heading("Current branch: ")
|
||||
headtext(currentBranch)
|
||||
out.nl()
|
||||
|
||||
heading("Manifest revision: ")
|
||||
headtext(project.revisionExpr)
|
||||
out.nl()
|
||||
|
||||
localBranches = list(branches)
|
||||
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(git_refs.R_HEADS):
|
||||
branch = branch[len(git_refs.R_HEADS) :]
|
||||
logTarget = git_refs.R_M + branch
|
||||
|
||||
localCommits, originCommits = cls._GetDiffCommits(
|
||||
project, logTarget
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
@classmethod
|
||||
def _GetDiffCommits(
|
||||
cls, project: Any, log_target: str
|
||||
) -> Tuple[List[str], List[str]]:
|
||||
"""Return local-only and remote-only commits from one history walk."""
|
||||
git = getattr(project, "work_git", None) or getattr(
|
||||
project, "bare_git", None
|
||||
)
|
||||
if git is None:
|
||||
return [], []
|
||||
commits = git.rev_list(
|
||||
"--left-right",
|
||||
"--abbrev=8",
|
||||
"--abbrev-commit",
|
||||
"--pretty=oneline",
|
||||
f"HEAD...{log_target}",
|
||||
"--",
|
||||
)
|
||||
if isinstance(commits, str):
|
||||
commits = commits.splitlines()
|
||||
local = []
|
||||
remote = []
|
||||
for commit in commits:
|
||||
if commit.startswith("<"):
|
||||
local.append(commit[1:])
|
||||
elif commit.startswith(">"):
|
||||
remote.append(commit[1:])
|
||||
return local, remote
|
||||
|
||||
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 = []
|
||||
local_branches = project.GetBranches()
|
||||
current_branch = cls._GetCurrentBranch(local_branches)
|
||||
if opt.current_branch:
|
||||
candidate_branches = (
|
||||
[current_branch]
|
||||
if current_branch and current_branch in local_branches
|
||||
else []
|
||||
)
|
||||
else:
|
||||
candidate_branches = local_branches
|
||||
br = [project.GetUploadableBranch(x) for x in candidate_branches]
|
||||
br = [x for x in br if x]
|
||||
|
||||
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 == current_branch,
|
||||
)
|
||||
)
|
||||
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
|
||||
@@ -242,7 +492,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 "",
|
||||
|
||||
+14
-10
@@ -18,21 +18,21 @@ import sys
|
||||
from color import Coloring
|
||||
from command import InteractiveCommand
|
||||
from command import MirrorSafeCommand
|
||||
from error import RepoUnhandledExceptionError
|
||||
from error import UpdateManifestError
|
||||
from git_command import git_require
|
||||
from repo_logging import RepoLogger
|
||||
import error
|
||||
import git_command
|
||||
import repo_logging
|
||||
from wrapper import Wrapper
|
||||
from wrapper import WrapperDir
|
||||
|
||||
|
||||
logger = RepoLogger(__file__)
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
|
||||
_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 = """
|
||||
@@ -163,15 +163,17 @@ to update the working directory files.
|
||||
clone_bundle=opt.clone_bundle,
|
||||
git_lfs=opt.git_lfs,
|
||||
use_superproject=opt.use_superproject,
|
||||
smart_sync=opt.smart_sync,
|
||||
verbose=opt.verbose,
|
||||
current_branch_only=opt.current_branch_only,
|
||||
tags=opt.tags,
|
||||
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(
|
||||
raise error.UpdateManifestError(
|
||||
f"Unable to sync manifest {manifest_name}"
|
||||
)
|
||||
|
||||
@@ -345,9 +347,9 @@ to update the working directory files.
|
||||
wrapper = Wrapper()
|
||||
|
||||
reqs = wrapper.Requirements.from_dir(WrapperDir())
|
||||
git_require(reqs.get_hard_ver("git"), fail=True)
|
||||
git_command.git_require(reqs.get_hard_ver("git"), fail=True)
|
||||
min_git_version_soft = reqs.get_soft_ver("git")
|
||||
if not git_require(min_git_version_soft):
|
||||
if not git_command.git_require(min_git_version_soft):
|
||||
logger.warning(
|
||||
"repo: warning: git-%s+ will soon be required; "
|
||||
"please upgrade your version of git to maintain "
|
||||
@@ -376,7 +378,7 @@ to update the working directory files.
|
||||
err_msg = "fatal: double check your --repo-rev setting."
|
||||
logger.error(err_msg)
|
||||
self.git_event_log.ErrorEvent(err_msg)
|
||||
raise RepoUnhandledExceptionError(e)
|
||||
raise error.RepoUnhandledExceptionError(e)
|
||||
|
||||
branch = rp.GetBranch("default")
|
||||
branch.merge = remote_ref
|
||||
@@ -386,7 +388,9 @@ to update the working directory files.
|
||||
if opt.worktree:
|
||||
# Older versions of git supported worktree, but had dangerous gc
|
||||
# bugs.
|
||||
git_require((2, 15, 0), fail=True, msg="git gc worktree corruption")
|
||||
git_command.git_require(
|
||||
(2, 15, 0), fail=True, msg="git gc worktree corruption"
|
||||
)
|
||||
|
||||
# Provide a short notice that we're reinitializing an existing checkout.
|
||||
# Sometimes developers might not realize that they're in one, or that
|
||||
|
||||
+4
-1
@@ -109,7 +109,10 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
|
||||
)
|
||||
else:
|
||||
projects = self.FindProjects(
|
||||
args, all_manifests=not opt.this_manifest_only
|
||||
args,
|
||||
groups=opt.groups,
|
||||
missing_ok=opt.all,
|
||||
all_manifests=not opt.this_manifest_only,
|
||||
)
|
||||
|
||||
def _getpath(x):
|
||||
|
||||
+2
-2
@@ -19,10 +19,10 @@ import os
|
||||
import sys
|
||||
|
||||
from command import PagedCommand
|
||||
from repo_logging import RepoLogger
|
||||
import repo_logging
|
||||
|
||||
|
||||
logger = RepoLogger(__file__)
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
|
||||
|
||||
class OutputFormat(enum.Enum):
|
||||
|
||||
+9
-4
@@ -59,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:
|
||||
@@ -97,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 " ",
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ class Prune(PagedCommand):
|
||||
print(
|
||||
"%s %-33s "
|
||||
% (
|
||||
branch.name == project.CurrentBranch and "*" or " ",
|
||||
branch.current and "*" or " ",
|
||||
branch.name,
|
||||
),
|
||||
end="",
|
||||
|
||||
+20
-24
@@ -16,11 +16,13 @@ import sys
|
||||
|
||||
from color import Coloring
|
||||
from command import Command
|
||||
from git_command import GitCommand
|
||||
from repo_logging import RepoLogger
|
||||
import error
|
||||
import git_command
|
||||
from project import Project
|
||||
import repo_logging
|
||||
|
||||
|
||||
logger = RepoLogger(__file__)
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
|
||||
|
||||
class RebaseColoring(Coloring):
|
||||
@@ -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 error.GitError:
|
||||
return project.revisionExpr
|
||||
|
||||
|
||||
class Rebase(Command):
|
||||
COMMON = True
|
||||
helpSummary = "Rebase local branches on upstream branch"
|
||||
@@ -126,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)
|
||||
@@ -162,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)
|
||||
|
||||
@@ -175,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:
|
||||
if git_command.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)
|
||||
|
||||
@@ -16,16 +16,16 @@ import optparse
|
||||
|
||||
from command import Command
|
||||
from command import MirrorSafeCommand
|
||||
from error import RepoExitError
|
||||
from repo_logging import RepoLogger
|
||||
import error
|
||||
import repo_logging
|
||||
from subcmds.sync import _PostRepoFetch
|
||||
from subcmds.sync import _PostRepoUpgrade
|
||||
|
||||
|
||||
logger = RepoLogger(__file__)
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
|
||||
|
||||
class SelfupdateError(RepoExitError):
|
||||
class SelfupdateError(error.RepoExitError):
|
||||
"""Exit error for failed selfupdate command."""
|
||||
|
||||
|
||||
|
||||
+4
-4
@@ -16,11 +16,11 @@ import sys
|
||||
|
||||
from color import Coloring
|
||||
from command import InteractiveCommand
|
||||
from git_command import GitCommand
|
||||
from repo_logging import RepoLogger
|
||||
import git_command
|
||||
import repo_logging
|
||||
|
||||
|
||||
logger = RepoLogger(__file__)
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
|
||||
|
||||
class _ProjectList(Coloring):
|
||||
@@ -129,5 +129,5 @@ The '%prog' command stages files to prepare the next commit.
|
||||
|
||||
|
||||
def _AddI(project):
|
||||
p = GitCommand(project, ["add", "--interactive"], bare=False)
|
||||
p = git_command.GitCommand(project, ["add", "--interactive"], bare=False)
|
||||
p.Wait()
|
||||
+9
-9
@@ -17,14 +17,14 @@ from typing import NamedTuple
|
||||
|
||||
from command import Command
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
from error import RepoExitError
|
||||
from git_command import git
|
||||
import error
|
||||
import git_command
|
||||
from git_config import IsImmutable
|
||||
from progress import Progress
|
||||
from repo_logging import RepoLogger
|
||||
import repo_logging
|
||||
|
||||
|
||||
logger = RepoLogger(__file__)
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
|
||||
|
||||
class ExecuteOneResult(NamedTuple):
|
||||
@@ -32,7 +32,7 @@ class ExecuteOneResult(NamedTuple):
|
||||
error: Exception
|
||||
|
||||
|
||||
class StartError(RepoExitError):
|
||||
class StartError(error.RepoExitError):
|
||||
"""Exit error for failed start command."""
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ revision specified in the manifest.
|
||||
self.Usage()
|
||||
|
||||
nb = args[0]
|
||||
if not git.check_ref_format("heads/%s" % nb):
|
||||
if not git_command.IsValidBranchName(nb):
|
||||
self.OptionParser.error("'%s' is not a valid name" % nb)
|
||||
|
||||
@classmethod
|
||||
@@ -85,7 +85,7 @@ revision specified in the manifest.
|
||||
# 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
|
||||
err = None
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
if IsImmutable(project.revisionExpr):
|
||||
if project.dest_branch:
|
||||
@@ -99,8 +99,8 @@ 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_idx, error)
|
||||
err = e
|
||||
return ExecuteOneResult(project_idx, err)
|
||||
|
||||
def Execute(self, opt, args):
|
||||
nb = args[0]
|
||||
|
||||
@@ -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:
|
||||
|
||||
+897
-178
File diff suppressed because it is too large.
Load diff
+78
-51
@@ -22,22 +22,20 @@ from typing import List
|
||||
from command import DEFAULT_LOCAL_JOBS
|
||||
from command import InteractiveCommand
|
||||
from editor import Editor
|
||||
from error import GitError
|
||||
from error import SilentRepoExitError
|
||||
from error import UploadError
|
||||
from git_command import GitCommand
|
||||
from git_refs import R_HEADS
|
||||
import error
|
||||
import git_refs
|
||||
import git_superproject
|
||||
from hooks import RepoHook
|
||||
from project import ReviewableBranch
|
||||
from repo_logging import RepoLogger
|
||||
import repo_logging
|
||||
from subcmds.sync import LocalSyncState
|
||||
|
||||
|
||||
_DEFAULT_UNUSUAL_COMMIT_THRESHOLD = 5
|
||||
logger = RepoLogger(__file__)
|
||||
logger = repo_logging.RepoLogger(__file__)
|
||||
|
||||
|
||||
class UploadExitError(SilentRepoExitError):
|
||||
class UploadExitError(error.SilentRepoExitError):
|
||||
"""Indicates that there is an upload command error requiring a sys exit."""
|
||||
|
||||
|
||||
@@ -378,7 +376,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
default=True,
|
||||
help="disable verifying ssl certs (unsafe)",
|
||||
)
|
||||
RepoHook.AddOptionGroup(p, "pre-upload")
|
||||
RepoHook.AddOptionGroup(p, "pre-upload", allow_fix=True)
|
||||
|
||||
def _SingleBranch(self, opt, branch, people):
|
||||
project = branch.project
|
||||
@@ -435,6 +433,18 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
self._UploadAndReport(opt, [branch], people)
|
||||
|
||||
def _MultipleBranches(self, opt, pending, people):
|
||||
if opt.yes and (opt.current_branch or opt.branch):
|
||||
todo = [
|
||||
branch
|
||||
for _, avail in pending
|
||||
for branch in avail
|
||||
if branch is not None
|
||||
]
|
||||
if not todo:
|
||||
_die("nothing ready for upload")
|
||||
self._UploadAndReport(opt, todo, people)
|
||||
return
|
||||
|
||||
projects = {}
|
||||
branches = {}
|
||||
|
||||
@@ -597,11 +607,11 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
)
|
||||
|
||||
full_dest = destination
|
||||
if not full_dest.startswith(R_HEADS):
|
||||
full_dest = R_HEADS + full_dest
|
||||
if not full_dest.startswith(git_refs.R_HEADS):
|
||||
full_dest = git_refs.R_HEADS + full_dest
|
||||
full_revision = branch.project.revisionExpr
|
||||
if not full_revision.startswith(R_HEADS):
|
||||
full_revision = R_HEADS + full_revision
|
||||
if not full_revision.startswith(git_refs.R_HEADS):
|
||||
full_revision = git_refs.R_HEADS + full_revision
|
||||
|
||||
# If the merge branch of the local branch is different from
|
||||
# the project's revision AND destination, this might not be
|
||||
@@ -627,7 +637,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
# If using superproject, add the root repo as a push option.
|
||||
manifest = branch.project.manifest
|
||||
push_options = list(opt.push_options)
|
||||
if manifest.manifestProject.use_superproject:
|
||||
if git_superproject.UseSuperproject(None, manifest):
|
||||
sp = manifest.superproject
|
||||
if sp:
|
||||
r_id = sp.repo_id
|
||||
@@ -648,6 +658,8 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
validate_certs=opt.validate_certs,
|
||||
push_options=push_options,
|
||||
patchset_description=opt.patchset_description,
|
||||
git_event_log=self.git_event_log,
|
||||
quiet=opt.quiet,
|
||||
)
|
||||
|
||||
branch.uploaded = True
|
||||
@@ -658,15 +670,17 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
for branch in todo:
|
||||
try:
|
||||
self._UploadBranch(opt, branch, people)
|
||||
except (UploadError, GitError) as e:
|
||||
except (error.UploadError, error.GitError) as e:
|
||||
self.git_event_log.ErrorEvent(f"upload error: {e}")
|
||||
branch.error = e
|
||||
aggregate_errors.append(e)
|
||||
branch.uploaded = False
|
||||
have_errors = True
|
||||
|
||||
print(file=sys.stderr)
|
||||
print("-" * 70, file=sys.stderr)
|
||||
quiet = getattr(opt, "quiet", False)
|
||||
if not quiet or have_errors:
|
||||
print(file=sys.stderr)
|
||||
print("-" * 70, file=sys.stderr)
|
||||
|
||||
if have_errors:
|
||||
for branch in todo:
|
||||
@@ -687,52 +701,64 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
)
|
||||
print()
|
||||
|
||||
for branch in todo:
|
||||
if branch.uploaded:
|
||||
print(
|
||||
"[OK ] %-15s %s"
|
||||
% (
|
||||
branch.project.RelPath(local=opt.this_manifest_only)
|
||||
+ "/",
|
||||
branch.name,
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
if not quiet:
|
||||
for branch in todo:
|
||||
if branch.uploaded:
|
||||
print(
|
||||
"[OK ] %-15s %s"
|
||||
% (
|
||||
branch.project.RelPath(local=opt.this_manifest_only)
|
||||
+ "/",
|
||||
branch.name,
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if have_errors:
|
||||
raise UploadExitError(aggregate_errors=aggregate_errors)
|
||||
|
||||
def _GetMergeBranch(self, project, local_branch=None):
|
||||
"""Get the merge branch name for a local branch.
|
||||
|
||||
Resolves the merge branch in-memory via project configuration to
|
||||
avoid git subprocess overhead during upload.
|
||||
"""
|
||||
if local_branch is None:
|
||||
p = GitCommand(
|
||||
project,
|
||||
["rev-parse", "--abbrev-ref", "HEAD"],
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
)
|
||||
p.Wait()
|
||||
local_branch = p.stdout.strip()
|
||||
p = GitCommand(
|
||||
project,
|
||||
["config", "--get", "branch.%s.merge" % local_branch],
|
||||
capture_stdout=True,
|
||||
capture_stderr=True,
|
||||
)
|
||||
p.Wait()
|
||||
merge_branch = p.stdout.strip()
|
||||
return merge_branch
|
||||
local_branch = project.CurrentBranch
|
||||
if local_branch:
|
||||
branch = project.GetBranch(local_branch)
|
||||
if branch.merge:
|
||||
return branch.merge
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
def _GatherOne(cls, opt, project_idx):
|
||||
"""Figure out the upload status for |project|."""
|
||||
project = cls.get_parallel_context()["projects"][project_idx]
|
||||
cbr = None
|
||||
if opt.current_branch:
|
||||
cbr = project.CurrentBranch
|
||||
up_branch = project.GetUploadableBranch(cbr)
|
||||
avail = [up_branch] if up_branch else None
|
||||
else:
|
||||
avail = project.GetUploadableBranches(opt.branch)
|
||||
return (project_idx, avail)
|
||||
return (project_idx, avail, cbr)
|
||||
|
||||
def ValidateOptions(self, opt: optparse.Values, args: List[str]) -> None:
|
||||
if opt.fix:
|
||||
if opt.ignore_hooks:
|
||||
self.OptionParser.error(
|
||||
"--fix and --ignore-hooks are mutually exclusive options: "
|
||||
"with --ignore-hooks, the original commits are uploaded, "
|
||||
"so fixups would only modify the working tree and would "
|
||||
"not be part of the upload"
|
||||
)
|
||||
if opt.bypass_hooks:
|
||||
self.OptionParser.error(
|
||||
"--fix and --no-verify are mutually exclusive options: "
|
||||
"--no-verify skips the pre-upload hook, so --fix would "
|
||||
"have no effect"
|
||||
)
|
||||
|
||||
def Execute(self, opt, args):
|
||||
projects = self.GetProjects(
|
||||
@@ -742,7 +768,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
def _ProcessResults(_pool, _out, results):
|
||||
pending = []
|
||||
for result in results:
|
||||
project_idx, avail = result
|
||||
project_idx, avail, current_branch = result
|
||||
project = projects[project_idx]
|
||||
if avail is None:
|
||||
logger.error(
|
||||
@@ -750,7 +776,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
"You might be able to fix the branch by running:\n"
|
||||
" git branch --set-upstream-to m/%s",
|
||||
project.RelPath(local=opt.this_manifest_only),
|
||||
project.CurrentBranch,
|
||||
current_branch,
|
||||
project.manifest.branch,
|
||||
)
|
||||
elif avail:
|
||||
@@ -802,9 +828,10 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
||||
project_list=pending_proj_names, worktree_list=pending_worktrees
|
||||
):
|
||||
if LocalSyncState(manifest).IsPartiallySynced():
|
||||
logger.error(
|
||||
"Partially synced tree detected. Syncing all projects "
|
||||
"may resolve issues you're seeing."
|
||||
logger.info(
|
||||
"Tip: A partially synced tree was detected. "
|
||||
"If this failure involves cross-project dependencies, "
|
||||
"a full `repo sync` might help."
|
||||
)
|
||||
ret = 1
|
||||
if ret:
|
||||
|
||||
+25
-10
@@ -14,13 +14,12 @@
|
||||
|
||||
import platform
|
||||
import sys
|
||||
from typing import Any, Tuple
|
||||
|
||||
from command import Command
|
||||
from command import MirrorSafeCommand
|
||||
from git_command import git
|
||||
from git_command import RepoSourceVersion
|
||||
from git_command import user_agent
|
||||
from git_refs import HEAD
|
||||
import git_command
|
||||
import git_refs
|
||||
from wrapper import Wrapper
|
||||
|
||||
|
||||
@@ -34,18 +33,34 @@ class Version(Command, MirrorSafeCommand):
|
||||
%prog
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _RepoVersion(project: Any) -> Tuple[str, str]:
|
||||
"""Return repo's describe string and commit date."""
|
||||
if git_command.git_require((2, 32, 0)):
|
||||
output = project.bare_git.log(
|
||||
"-1", "--format=%(describe)%n%cD", git_refs.HEAD
|
||||
)
|
||||
description, commit_date = output.rstrip("\n").split("\n", 1)
|
||||
if description:
|
||||
return description, commit_date
|
||||
|
||||
return (
|
||||
project.bare_git.describe(git_refs.HEAD),
|
||||
project.bare_git.log("-1", "--format=%cD", git_refs.HEAD),
|
||||
)
|
||||
|
||||
def Execute(self, opt, args):
|
||||
rp = self.manifest.repoProject
|
||||
rem = rp.GetRemote()
|
||||
branch = rp.GetBranch("default")
|
||||
|
||||
# These might not be the same. Report them both.
|
||||
src_ver = RepoSourceVersion()
|
||||
rp_ver = rp.bare_git.describe(HEAD)
|
||||
src_ver = git_command.RepoSourceVersion()
|
||||
rp_ver, commit_date = self._RepoVersion(rp)
|
||||
print(f"repo version {rp_ver}")
|
||||
print(f" (from {rem.url})")
|
||||
print(f" (tracking {branch.merge})")
|
||||
print(f" ({rp.bare_git.log('-1', '--format=%cD', HEAD)})")
|
||||
print(f" ({commit_date})")
|
||||
|
||||
if self.wrapper_path is not None:
|
||||
print(f"repo launcher version {self.wrapper_version}")
|
||||
@@ -54,9 +69,9 @@ class Version(Command, MirrorSafeCommand):
|
||||
if src_ver != rp_ver:
|
||||
print(f" (currently at {src_ver})")
|
||||
|
||||
print(f"repo User-Agent {user_agent.repo}")
|
||||
print(f"git {git.version_tuple().full}")
|
||||
print(f"git User-Agent {user_agent.git}")
|
||||
print(f"repo User-Agent {git_command.user_agent.repo}")
|
||||
print(f"git {git_command.git.version_tuple().full}")
|
||||
print(f"git User-Agent {git_command.user_agent.git}")
|
||||
print(f"Python {sys.version}")
|
||||
uname = platform.uname()
|
||||
if sys.version_info.major < 3:
|
||||
|
||||
+3
-4
@@ -17,13 +17,12 @@ import sys
|
||||
from typing import List
|
||||
|
||||
from command import Command
|
||||
from error import GitError
|
||||
from error import RepoExitError
|
||||
import error
|
||||
import platform_utils
|
||||
from project import DeleteWorktreeError
|
||||
|
||||
|
||||
class Error(RepoExitError):
|
||||
class Error(error.RepoExitError):
|
||||
"""Exit error when wipe command fails."""
|
||||
|
||||
|
||||
@@ -101,7 +100,7 @@ Examples:
|
||||
try:
|
||||
if project.HasChanges():
|
||||
uncommitted_projects.append(project.name)
|
||||
except GitError:
|
||||
except error.GitError:
|
||||
uncommitted_projects.append(f"{project.name} (corrupted)")
|
||||
|
||||
users = objdir_map.get(project.objdir, {project.relpath})
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Repo Tests
|
||||
|
||||
There is a mixture of [pytest] & [Python unittest] in here. We adopted [pytest]
|
||||
later on but didn't migrate existing tests (since they still work). New tests
|
||||
should be written using [pytest] only.
|
||||
|
||||
## File layout
|
||||
|
||||
* `test_xxx.py`: Unittests for the `xxx` module in the main repo codebase.
|
||||
Modules that are in subdirs normalize the `/` into `_`.
|
||||
For example, [test_error.py](./test_error.py) is for the
|
||||
[error.py](../error.py) module, and
|
||||
[test_subcmds_forall.py](./test_subcmds_forall.py) is for the
|
||||
[subcmds/forall.py](../subcmds/forall.py) module.
|
||||
* [conftest.py](./conftest.py): Custom pytest fixtures for sharing.
|
||||
* [utils_for_test.py](./utils_for_test.py): Helpers for sharing in tests.
|
||||
* [e2e/](./e2e/): Hermetic end-to-end tests that run the real `repo` against
|
||||
a local git server.
|
||||
|
||||
|
||||
[pytest]: https://pytest.org/
|
||||
[Python unittest]: https://docs.python.org/3/library/unittest.html#unittest.TestCase
|
||||
@@ -15,19 +15,66 @@
|
||||
"""Common fixtures for pytests."""
|
||||
|
||||
import pathlib
|
||||
from typing import Any, Generator
|
||||
|
||||
import pytest
|
||||
|
||||
import color
|
||||
import platform_utils
|
||||
import repo_trace
|
||||
|
||||
|
||||
def pytest_addoption(parser: pytest.Parser) -> None:
|
||||
parser.addoption(
|
||||
"--fail-on-skip",
|
||||
action="store_true",
|
||||
help="fail skipped tests (run_tests sets this in CQ)",
|
||||
)
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_makereport(
|
||||
item: pytest.Item,
|
||||
) -> Generator[None, Any, None]:
|
||||
"""Fail skipped tests with --fail-on-skip.
|
||||
|
||||
A skip in CQ usually means the builder is missing a dependency (e.g. a new
|
||||
enough git), which would otherwise silently drop coverage. Tests that
|
||||
should not run in CQ are deselected via the `skip_cq` marker instead.
|
||||
"""
|
||||
outcome = yield
|
||||
report = outcome.get_result()
|
||||
if (
|
||||
item.config.getoption("fail_on_skip")
|
||||
and report.skipped
|
||||
# xfail outcomes are also reported as skipped.
|
||||
and not hasattr(report, "wasxfail")
|
||||
):
|
||||
reason = report.longrepr
|
||||
if isinstance(reason, tuple):
|
||||
# (path, lineno, reason)
|
||||
reason = reason[2]
|
||||
report.outcome = "failed"
|
||||
report.longrepr = f"Not allowed with --fail-on-skip: {reason}"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def disable_repo_trace(tmp_path):
|
||||
"""Set an environment marker to relax certain strict checks for test code.""" # noqa: E501
|
||||
repo_trace.SetTrace(False)
|
||||
repo_trace._TRACE_FILE = str(tmp_path / "TRACE_FILE_from_test")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def restore_default_coloring() -> None:
|
||||
"""Force disable color for reproducible test behavior.
|
||||
|
||||
The few tests that cover color behavior can still reset/change the color as
|
||||
needed.
|
||||
"""
|
||||
color.SetDefaultColoring("never")
|
||||
|
||||
|
||||
# adapted from pytest-home 0.5.1
|
||||
def _set_home(monkeypatch, path: pathlib.Path):
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Repo End-to-End Tests
|
||||
|
||||
These tests run the real `repo` launcher & code (as subprocesses) against a
|
||||
local git smart HTTP server. They are hermetic: everything is served from
|
||||
`127.0.0.1` out of temporary directories, and the user's git config &
|
||||
environment are ignored.
|
||||
|
||||
They run as part of `./run_tests` by default, after the unit tests.
|
||||
|
||||
## Running
|
||||
|
||||
```sh
|
||||
# Everything (unit + e2e).
|
||||
./run_tests
|
||||
|
||||
# Only the e2e tests.
|
||||
./run_tests -m e2e
|
||||
|
||||
# Everything but the e2e tests.
|
||||
./run_tests -m 'not e2e'
|
||||
```
|
||||
|
||||
The suite is skipped automatically when `git http-backend` isn't
|
||||
available, when git is older than 2.28 (need default branch support and
|
||||
superproject support), or when not run from a git checkout.
|
||||
|
||||
## Layout
|
||||
|
||||
* `harness.py`: the git server, server-side repos & the repo client.
|
||||
* `test_init_sync.py`: core `repo init` & `repo sync` options.
|
||||
* `test_partial_clone.py`: partial clones, incl. the standard Android
|
||||
configuration from go/repo-init & go/sync.
|
||||
* `test_sync_modes.py`: `sync -n`/`-l` & the other sync flags.
|
||||
* `test_groups.py`: manifest groups.
|
||||
* `test_failures.py`: failure paths (missing projects, dirty trees, ...).
|
||||
* `test_workflows.py`: multi-step workflows.
|
||||
* `test_manifest_regressions.py`: manifests that broke users in the past.
|
||||
@@ -0,0 +1,136 @@
|
||||
# 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.
|
||||
|
||||
"""Fixtures for the hermetic end-to-end tests.
|
||||
|
||||
Tests normally only need the `host` fixture (to create server repos) and the
|
||||
`client` fixture (to run repo). Override the `git_defaults` fixture via
|
||||
parametrize to run under Git 3 defaults.
|
||||
"""
|
||||
|
||||
import itertools
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterator, List
|
||||
|
||||
import harness
|
||||
import pytest
|
||||
|
||||
|
||||
# Run before pytest's own `-m` deselection so `-m e2e` & `-m 'not e2e'` see the
|
||||
# marker.
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_collection_modifyitems(items: List[pytest.Item]) -> None:
|
||||
"""Mark tests in this directory as `e2e` & run them after unit tests.
|
||||
|
||||
Note: pytest passes every collected item (not just this directory's), so
|
||||
this reorders the whole session.
|
||||
"""
|
||||
this_dir = Path(__file__).resolve().parent
|
||||
for item in items:
|
||||
if this_dir in item.path.resolve().parents:
|
||||
item.add_marker(pytest.mark.e2e)
|
||||
# Stable sort: unit tests keep their order & come first, then E2E tests.
|
||||
items.sort(key=lambda item: item.get_closest_marker("e2e") is not None)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def _require_e2e_support() -> None:
|
||||
"""Skip the suite when the host can't run it."""
|
||||
# The code under test is published from the git checkout (e.g. this isn't
|
||||
# the case when running from an unpacked sdist).
|
||||
if not (harness.ROOT_DIR / ".git").exists():
|
||||
pytest.skip("E2E tests must run from a git checkout")
|
||||
if not harness.has_http_backend():
|
||||
pytest.skip("git http-backend is not available")
|
||||
if harness.git_version() < (2, 28, 0):
|
||||
pytest.skip("E2E tests require git 2.28+")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def git_server(
|
||||
tmp_path_factory: pytest.TempPathFactory,
|
||||
) -> Iterator[harness.GitHttpServer]:
|
||||
"""A git smart HTTP server shared by all tests in the session."""
|
||||
with harness.serve(tmp_path_factory.mktemp("git-server")) as server:
|
||||
yield server
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def launcher(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
||||
"""A standalone copy of the repo launcher."""
|
||||
return harness.install_launcher(tmp_path_factory.mktemp("bin"))
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def repo_source(git_server: harness.GitHttpServer) -> harness.ServerRepo:
|
||||
"""The code under test, published as a repo on `git_server`."""
|
||||
return harness.publish_repo_source(
|
||||
git_server.root / "git-repo.git", f"{git_server.url}/git-repo"
|
||||
)
|
||||
|
||||
|
||||
NAMESPACE_IDS = itertools.count()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def host(git_server: harness.GitHttpServer) -> harness.GitHost:
|
||||
"""A per-test namespace for creating repos on the server."""
|
||||
return harness.GitHost(git_server, f"t{next(NAMESPACE_IDS)}")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def git_defaults() -> str:
|
||||
"""Client git defaults to emulate; a key of harness.GIT_DEFAULTS."""
|
||||
return "git2"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client_env(tmp_path: Path, git_defaults: str) -> Dict[str, str]:
|
||||
"""Isolated environment for running repo & git as a client."""
|
||||
home = tmp_path / "home"
|
||||
home.mkdir()
|
||||
settings = {
|
||||
"user.name": "E2E User",
|
||||
"user.email": "user@example.com",
|
||||
"color.ui": "false",
|
||||
# Don't let background maintenance race with assertions.
|
||||
"maintenance.auto": "false",
|
||||
"gc.auto": "0",
|
||||
}
|
||||
if git_defaults == "git3" and harness.git_version() < (2, 47, 0):
|
||||
# init.defaultRefFormat & init.defaultObjectFormat landed in git 2.47.
|
||||
pytest.skip("git is too old to emulate Git 3 defaults")
|
||||
settings.update(harness.GIT_DEFAULTS[git_defaults])
|
||||
harness.write_config(home / ".gitconfig", settings)
|
||||
# Python resolves `~` via USERPROFILE on Windows. The ceiling keeps git
|
||||
# from finding a repo above the test dir (e.g. with `--basetemp=./tmp`).
|
||||
return dict(
|
||||
harness.clean_environ(),
|
||||
HOME=str(home),
|
||||
USERPROFILE=str(home),
|
||||
GIT_CEILING_DIRECTORIES=str(tmp_path),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(
|
||||
tmp_path: Path,
|
||||
launcher: Path,
|
||||
repo_source: harness.ServerRepo,
|
||||
client_env: Dict[str, str],
|
||||
) -> harness.RepoClient:
|
||||
"""A repo client checkout directory that uses the code under test."""
|
||||
return harness.RepoClient(
|
||||
tmp_path / "client", launcher, client_env, repo_source.url
|
||||
)
|
||||
File diff suppressed because it is too large.
Load diff
Loaded 100 of 143 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user