mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-09-23 23:30:42 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d27d6829a8 | ||
|
|
0ea57e2eed | ||
|
|
ba8ddf396c | ||
|
|
d88ce8d952 | ||
|
|
5e8d2a6e3a | ||
|
|
e59c9cde99 | ||
|
|
83428a9b26 | ||
|
|
948abc85bc | ||
|
|
c63a2f92fa | ||
|
|
0039e39000 | ||
|
|
5f378458d2 | ||
|
|
d27034bf62 | ||
|
|
6541729a18 | ||
|
|
b85e76a86a | ||
|
|
e5bbb5c9e6 | ||
|
|
4fe87617ff | ||
|
|
d7299422ae | ||
|
|
3f087a8dd9 | ||
|
|
3a6e25af75 | ||
|
|
41c2597509 | ||
|
|
e6ad708009 | ||
|
|
09914bcab7 | ||
|
|
3f1775607f | ||
|
|
b85886fa9f | ||
|
|
d9da609d8c | ||
|
|
4bec297eb6 | ||
|
|
54fa31cd84 | ||
|
|
29b6630a65 | ||
|
|
0b82311632 | ||
|
|
06c4f9e1cc | ||
|
|
dd1130352b | ||
|
|
eeba6f268d | ||
|
|
1729aaebae | ||
|
|
978adb7ea5 | ||
|
|
0398c6718e | ||
|
|
3bb4871c44 | ||
|
|
35bbf701d0 | ||
|
|
881af15cdc | ||
|
|
fbc9c79192 | ||
|
|
ead4b2d7aa | ||
|
|
e7cac4bca6 | ||
|
|
91986011b0 | ||
|
|
a27dbcdb7b | ||
|
|
547dc9985c | ||
|
|
88a7e88e54 | ||
|
|
c21a41c7cc | ||
|
|
6586efe79a | ||
|
|
3af9e2f146 | ||
|
|
d32b70275c | ||
|
|
7f58543703 | ||
|
|
39c0b60900 | ||
|
|
cd307a6089 | ||
|
|
4b462634e0 | ||
|
|
d9d86fb595 | ||
|
|
f7a24df00c | ||
|
|
e0bd39c691 | ||
|
|
c883613e31 | ||
|
|
384c059f9e | ||
|
|
b8531133de |
@@ -12,3 +12,8 @@ __pycache__
|
|||||||
|
|
||||||
# PyCharm related
|
# PyCharm related
|
||||||
/.idea/
|
/.idea/
|
||||||
|
|
||||||
|
# AI tool related.
|
||||||
|
/AGENTS.md
|
||||||
|
/CLAUDE.md
|
||||||
|
/GEMINI.md
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# git-repo AI Review Agents (WIP)
|
||||||
|
|
||||||
|
**Note:** This project is a work in progress and is subject to change.
|
||||||
|
|
||||||
|
This directory contains configurations and skills for AI review agents that
|
||||||
|
automatically analyze changes in the `git-repo` codebase.
|
||||||
|
|
||||||
|
These agents help maintain code quality, enforce style guidelines, and catch
|
||||||
|
common pitfalls before code is merged.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
* [`agent_configs.txtpb`](agent_configs.txtpb): Defines the active AI
|
||||||
|
agents, their configurations, and which skills they are equipped with.
|
||||||
|
* [`skills/`](skills/): Contains the "skills" (rules, guidelines, and traps)
|
||||||
|
used by the agents.
|
||||||
|
* [`code_review_workflow/`](skills/code_review_workflow/SKILL.md):
|
||||||
|
Guidelines for code review processes, commit messages, and testing.
|
||||||
|
* [`core_internals/`](skills/core_internals/SKILL.md): Technical
|
||||||
|
guidelines for `git-repo` core logic (sync, manifest, git integration,
|
||||||
|
etc.).
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The agents defined in `agent_configs.txtpb` are configured to run
|
||||||
|
automatically on new changes. They analyze the diffs against the rules defined
|
||||||
|
in their respective skills and provide feedback in the code review interface
|
||||||
|
(e.g., Gerrit).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
To improve the agent's review quality or add new rules:
|
||||||
|
|
||||||
|
1. **Update existing skills**: Modify the `SKILL.md` files under `skills/` to
|
||||||
|
add new rules, "What" explanations, "Why" rationales, and "Traps"
|
||||||
|
(Don't/Do code examples).
|
||||||
|
2. **Add new skills**:
|
||||||
|
* Create a new directory under `skills/`.
|
||||||
|
* Add a `SKILL.md` following the established format (see existing skills
|
||||||
|
for reference).
|
||||||
|
* Register the new skill in `agent_configs.txtpb` by adding it to the
|
||||||
|
`skills` field of an agent configuration.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# proto-file: google/corp/android/engprod/codereviewagentconfiguration/v1/agent.proto
|
||||||
|
# proto-message: HostAgents
|
||||||
|
|
||||||
|
# Code Review Workflow Agent
|
||||||
|
configs {
|
||||||
|
id: "code-review-workflow"
|
||||||
|
display_name: "Code Review Workflow"
|
||||||
|
description: "Analyzes git-repo Gerrit submission labeling, commit metadata, Python linting, and testing strategy."
|
||||||
|
skills: "code_review_workflow"
|
||||||
|
include_filters {
|
||||||
|
project: "git-repo"
|
||||||
|
}
|
||||||
|
automatic: false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Core Internals Agent
|
||||||
|
configs {
|
||||||
|
id: "core-internals"
|
||||||
|
display_name: "Core Internals"
|
||||||
|
description: "Analyzes git-repo synchronization, multiprocessing, manifest parsing, git integration, worktree layouts, and CLI commands."
|
||||||
|
skills: "core_internals"
|
||||||
|
include_filters {
|
||||||
|
project: "git-repo"
|
||||||
|
}
|
||||||
|
automatic: false
|
||||||
|
}
|
||||||
@@ -0,0 +1,968 @@
|
|||||||
|
---
|
||||||
|
name: code-review-workflow
|
||||||
|
description: Provides guidance and best practices on Gerrit submission labeling, CI builder execution, Python code formatting/linting, commit metadata standardization, and testing strategy in git-repo.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Code Review Workflow Engineering Guide
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
Welcome to the authoritative engineering guide for the Code Review Workflow.
|
||||||
|
This living repository exists to capture critical folk knowledge, prevent the
|
||||||
|
recurrence of historical failure modes, and enforce strict architectural and
|
||||||
|
procedural boundaries across our integration pipeline. By standardizing these
|
||||||
|
protocols, we ensure high development velocity while maintaining rock-solid
|
||||||
|
codebase stability and traceability.
|
||||||
|
|
||||||
|
This guide covers the complete lifecycle of a change list (CL) from local
|
||||||
|
development to automated submission. It defines the strict Gerrit labeling
|
||||||
|
mechanisms required to trigger the Commit-Queue, mandates comprehensive CI
|
||||||
|
builder environment checks, and enforces centralized Python static analysis.
|
||||||
|
Furthermore, it outlines uncompromising standards for atomic commit metadata and
|
||||||
|
pragmatic testing state isolation to guarantee that every integration is fully
|
||||||
|
bisectable and verifiable.
|
||||||
|
|
||||||
|
For incoming engineers, adherence to these mandates eliminates the friction of
|
||||||
|
stalled pipelines, unreviewable monolithic changes, and silent CI regressions.
|
||||||
|
Treat this guide as your primary roadmap for navigating the repository's strict
|
||||||
|
submission requirements, enabling seamless transitions from peer approval to
|
||||||
|
successfully integrated code.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
| Chapter Theme / Title | Scope & Objective |
|
||||||
|
| :------------------------------- | :---------------------------------------- |
|
||||||
|
| **Gerrit Submission and Labeling | Dictates strict access controls, review |
|
||||||
|
: Workflow** : enforcement protocols, and Gerrit :
|
||||||
|
: : labeling mechanisms required to advance :
|
||||||
|
: : changes through the CI pipeline, ensuring :
|
||||||
|
: : seamless transitions to automated :
|
||||||
|
: : integration via the Commit-Queue. :
|
||||||
|
| **CI Builder Environment and | Defines guidelines for ensuring build |
|
||||||
|
: Execution Integrity** : script resilience against missing :
|
||||||
|
: : dependencies and managing process :
|
||||||
|
: : execution contexts within LUCI and local :
|
||||||
|
: : testing environments to prevent silent :
|
||||||
|
: : builder failures. :
|
||||||
|
| **Python Code Formatting and | Governs the automated enforcement of |
|
||||||
|
: Linting** : Python style guidelines, mandating strict :
|
||||||
|
: : PEP-8 compliance, import sorting, and :
|
||||||
|
: : consistent string quoting to ensure :
|
||||||
|
: : codebase uniformity and prevent CI :
|
||||||
|
: : regressions. :
|
||||||
|
| **Commit Metadata and History | Establishes the structural composition |
|
||||||
|
: Standardization** : and metadata formatting of change lists :
|
||||||
|
: : (CLs) to ensure precise issue tracker :
|
||||||
|
: : integration, reliable CI/CD parsing, and :
|
||||||
|
: : an atomic, bisectable repository history. :
|
||||||
|
| **Testing Strategy and State | Outlines test implementation boundaries, |
|
||||||
|
: Isolation** : emphasizing pragmatic mocking limits to :
|
||||||
|
: : prevent false positives and detailing :
|
||||||
|
: : acceptable workflows for deferred test :
|
||||||
|
: : coverage while maintaining verification :
|
||||||
|
: : integrity. :
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
## Chapter: Gerrit Submission and Labeling Workflow
|
||||||
|
|
||||||
|
**Context:** This domain dictates the strict access controls, review enforcement
|
||||||
|
protocols, and specific Gerrit labeling mechanisms required to advance changes
|
||||||
|
through the CI pipeline. Adherence ensures seamless transitions from peer
|
||||||
|
approval to automated integration via the Commit-Queue.
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Rule ID | Principle / Constraint | Priority | Primary Symptom / |
|
||||||
|
: : : : Trap :
|
||||||
|
| :-------- | :------------------------------ | :------- | :----------------- |
|
||||||
|
| **T1-01** | Explicit Labeling for Gerrit | High | Leaving a change |
|
||||||
|
: : Automated Submission : : idle after :
|
||||||
|
: : : : addressing :
|
||||||
|
: : : : comments or :
|
||||||
|
: : : : receiving a :
|
||||||
|
: : : : reviewer's LGTM, :
|
||||||
|
: : : : expecting the :
|
||||||
|
: : : : reviewer to merge :
|
||||||
|
: : : : it. :
|
||||||
|
| **T1-02** | Automated Submission via | Medium | Requesting a |
|
||||||
|
: : Commit-Queue (CQ) : : manual push or :
|
||||||
|
: : : : direct submit from :
|
||||||
|
: : : : repository :
|
||||||
|
: : : : maintainers after :
|
||||||
|
: : : : receiving code :
|
||||||
|
: : : : review approval. :
|
||||||
|
| **T1-03** | Gerrit Trusted Contributor | Medium | Relying on a |
|
||||||
|
: : Review Enforcement Verification : : standard +2 vote :
|
||||||
|
: : : : from a non-trusted :
|
||||||
|
: : : : contributor to :
|
||||||
|
: : : : fulfill strict :
|
||||||
|
: : : : Review-Enforcement :
|
||||||
|
: : : : requirements. :
|
||||||
|
| **T1-04** | Mandatory Gerrit Labels for | High | Acknowledging an |
|
||||||
|
: : Automated Submission : : approval but :
|
||||||
|
: : : : failing to apply :
|
||||||
|
: : : : the appropriate :
|
||||||
|
: : : : Gerrit labels to :
|
||||||
|
: : : : initiate the merge :
|
||||||
|
: : : : pipeline. :
|
||||||
|
| **T1-05** | Gerrit Automated Submission | Medium | Leaving an |
|
||||||
|
: : Triggers : : approved patchset :
|
||||||
|
: : : : idle and waiting :
|
||||||
|
: : : : for maintainers to :
|
||||||
|
: : : : manually merge it. :
|
||||||
|
| **T1-06** | Active Reviewer Rerouting for | Medium | Waiting weeks or |
|
||||||
|
: : Stalled Changes : : months for an :
|
||||||
|
: : : : inactive or OOO :
|
||||||
|
: : : : reviewer to :
|
||||||
|
: : : : respond to a :
|
||||||
|
: : : : patchset update. :
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
#### T1-01: Explicit Labeling for Gerrit Automated Submission
|
||||||
|
|
||||||
|
> **Rule:** Always apply `Verified+1` and `Commit-Queue+2` explicitly to trigger
|
||||||
|
> the final submission phase. Never assume a code approval automatically
|
||||||
|
> initiates the pipeline.
|
||||||
|
>
|
||||||
|
> **What:** Changes are not merged automatically upon receiving approval;
|
||||||
|
> contributors must explicitly set the `Verified+1` and `Commit-Queue+2` labels
|
||||||
|
> to trigger the final submission phase.
|
||||||
|
>
|
||||||
|
> **Applies To:** Gerrit review UI and change submission pipeline as defined in
|
||||||
|
> `CONTRIBUTING.md`.
|
||||||
|
>
|
||||||
|
> **Why:** Contributors often mistakenly assume an LGTM implies an immediate
|
||||||
|
> merge, leading to stalled changes. The project relies on explicitly triggering
|
||||||
|
> the Commit-Queue to finalize CI checks and perform the merge. Failing to
|
||||||
|
> adhere to this typically results in **Stalled Submission Pipeline**.
|
||||||
|
|
||||||
|
**Trap 1: Leaving a change idle after addressing comments or receiving a
|
||||||
|
reviewer's LGTM, expecting the reviewer to merge it.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
* Waiting indefinitely after reviewer posts 'LGTM'.
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
* Vote `Verified+1` and `Commit-Queue+2` manually to submit the change to the
|
||||||
|
automated queue.
|
||||||
|
|
||||||
|
**Exceptions:** Contributors lacking trusted permissions must ping a repository
|
||||||
|
maintainer to apply the final `Commit-Queue+2` vote.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#### T1-02: Automated Submission via Commit-Queue (CQ)
|
||||||
|
|
||||||
|
> **Rule:** Must utilize the Gerrit Commit-Queue (CQ) labeling system to merge
|
||||||
|
> code. Maintainers must never perform direct manual submissions.
|
||||||
|
>
|
||||||
|
> **What:** Merging code must be triggered via the Gerrit Commit-Queue (CQ)
|
||||||
|
> labeling system rather than relying on direct manual submission by
|
||||||
|
> maintainers.
|
||||||
|
>
|
||||||
|
> **Applies To:** Gerrit code review UI and CI/CD submission workflow.
|
||||||
|
>
|
||||||
|
> **Why:** Contributors would request maintainers to directly merge patches once
|
||||||
|
> approved, bypassing the automated commit-queue pipeline, which guarantees that
|
||||||
|
> final integration tests pass before pushing to the target branch. Failing to
|
||||||
|
> adhere to this typically results in **Bypassed CI / Direct Submit**.
|
||||||
|
|
||||||
|
**Trap 1: Requesting a manual push or direct submit from repository maintainers
|
||||||
|
after receiving code review approval.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
* Leaving a comment: "I believe everything is ready for integrating this. So
|
||||||
|
if either of you can submit it, it would be appreciated."
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
* Applying the `Commit-Queue+2` (CQ+2) label in Gerrit, which delegates
|
||||||
|
testing and the final merge operation to the automated bot.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#### T1-03: Gerrit Trusted Contributor Review Enforcement Verification
|
||||||
|
|
||||||
|
> **Rule:** Verify review enforcement requirements are satisfied by contributors
|
||||||
|
> within the explicitly configured trusted group. Never cast misleading +2 votes
|
||||||
|
> if you lack valid trusted group privileges.
|
||||||
|
>
|
||||||
|
> **What:** Gerrit submission requirements may mandate specific approval levels
|
||||||
|
> (e.g., two trusted contributors). Votes from users with +2 access who are not
|
||||||
|
> in the designated 'trusted' group do not satisfy the 'Review-Enforcement'
|
||||||
|
> submit requirement.
|
||||||
|
>
|
||||||
|
> **Applies To:** Gerrit repository administration and code review voting
|
||||||
|
> workflows.
|
||||||
|
>
|
||||||
|
> **Why:** Non-trusted contributors with +2 rights were casting +2 votes on
|
||||||
|
> changes. These votes did not fulfill the 'Two trusted contributors'
|
||||||
|
> Review-Enforcement requirement, leading to stalled submissions and confusion
|
||||||
|
> regarding why the UI showed a +2 but blocked submission. Failing to adhere to
|
||||||
|
> this typically results in **Blocked Submission / Silent Requirement Failure**.
|
||||||
|
|
||||||
|
**Trap 1: Relying on a standard +2 vote from a non-trusted contributor to
|
||||||
|
fulfill strict Review-Enforcement requirements.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
* Leaving a +2 vote on a change as a non-trusted contributor, creating the
|
||||||
|
false appearance that the Review-Enforcement requirement has been partially
|
||||||
|
or fully met.
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
* Verifying the reviewer is in the explicitly configured trusted group for the
|
||||||
|
repository. If not, the reviewer should manually downgrade their invalid +2
|
||||||
|
vote to a +1 to clearly indicate that their vote does not count toward the
|
||||||
|
enforcement threshold.
|
||||||
|
|
||||||
|
**Exceptions:** Repositories where specific non-employee groups have been
|
||||||
|
explicitly added to the trusted administrators list.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#### T1-04: Mandatory Gerrit Labels for Automated Submission
|
||||||
|
|
||||||
|
> **Rule:** Always apply `Verified+1` and `Commit-Queue+2` labels to initiate
|
||||||
|
> the CI merge process. Never leave an approved CL in a technically unlabeled
|
||||||
|
> state.
|
||||||
|
>
|
||||||
|
> **What:** A code change must receive explicit `Verified+1` and
|
||||||
|
> `Commit-Queue+2` labels by the author or reviewer to trigger the automated CI
|
||||||
|
> merge process.
|
||||||
|
>
|
||||||
|
> **Applies To:** Gerrit workflow / Merge execution phase.
|
||||||
|
>
|
||||||
|
> **Why:** Historically, leaving a Change List (CL) in an approved but unlabeled
|
||||||
|
> state causes the integration pipeline to stall indefinitely, requiring manual
|
||||||
|
> intervention or reviewer pinging to trigger the CI queue. Failing to adhere to
|
||||||
|
> this typically results in **Merge Pipeline Stall**.
|
||||||
|
|
||||||
|
**Trap 1: Acknowledging an approval but failing to apply the appropriate Gerrit
|
||||||
|
labels to initiate the merge pipeline.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
* Leaving the CL in an approved state and waiting for auto-submission without
|
||||||
|
applying the `Verified+1` or `Commit-Queue+2` labels.
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
* Explicitly applying `Verified+1` (and `Commit-Queue+2` if ready) once
|
||||||
|
reviewers have approved the logic, to instruct the automation to merge the
|
||||||
|
code.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#### T1-05: Gerrit Automated Submission Triggers
|
||||||
|
|
||||||
|
> **Rule:** Must actively signal patch readiness to Gerrit systems using proper
|
||||||
|
> label thresholds. Avoid leaving patchsets idle assuming upstream maintainer
|
||||||
|
> action.
|
||||||
|
>
|
||||||
|
> **What:** A patchset requires specific label thresholds ('Verified+1' and
|
||||||
|
> 'Commit-Queue+2') to trigger automated submission in the Gerrit workflow.
|
||||||
|
>
|
||||||
|
> **Applies To:** Gerrit review UI and automated CI/CD submission process for
|
||||||
|
> the git-repo codebase.
|
||||||
|
>
|
||||||
|
> **Why:** Contributors frequently asked how to integrate changes after
|
||||||
|
> receiving an approval, leading to stalled patches because the automated
|
||||||
|
> pipeline was not explicitly triggered. Failing to adhere to this typically
|
||||||
|
> results in **Stalled Patch Integration**.
|
||||||
|
|
||||||
|
**Trap 1: Leaving an approved patchset idle and waiting for maintainers to
|
||||||
|
manually merge it.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
* Waiting indefinitely after receiving an 'LGTM' without setting workflow
|
||||||
|
labels.
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
* The patch author manually sets the 'Verified' flag (if locally tested) and
|
||||||
|
applies the 'Commit-Queue+2' vote to signal readiness for automated merge.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#### T1-06: Active Reviewer Rerouting for Stalled Changes
|
||||||
|
|
||||||
|
> **Rule:** Actively reroute reviews stalled by unresponsive or out-of-office
|
||||||
|
> (OOO) primary reviewers. Must explicitly tag alternate maintainers and
|
||||||
|
> document the absence to prevent lifecycle stalls.
|
||||||
|
>
|
||||||
|
> **What:** If the primary reviewer is out-of-office (OOO) or unresponsive for
|
||||||
|
> an extended period, contributors must actively CC and reroute the review to
|
||||||
|
> another active maintainer.
|
||||||
|
>
|
||||||
|
> **Applies To:** Gerrit review cycle and reviewer assignment process.
|
||||||
|
>
|
||||||
|
> **Why:** Patchsets have historically stalled for over a month due to reviewers
|
||||||
|
> taking extended leave without actively delegating their review queues. Failing
|
||||||
|
> to adhere to this typically results in **Indefinite Review Stalls**.
|
||||||
|
|
||||||
|
**Trap 1: Waiting weeks or months for an inactive or OOO reviewer to respond to
|
||||||
|
a patchset update.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
* Leaving a review assigned strictly to an unresponsive reviewer without
|
||||||
|
notifying other maintainers or attempting to escalate.
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
* Tag a new reviewer with 'PTAL' (Please Take A Look) in the thread,
|
||||||
|
explicitly noting the original reviewer's absence, and confirm alignment
|
||||||
|
with the original author.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
### Cross-Domain Dependencies
|
||||||
|
|
||||||
|
* **Upstream:** T4 | Python Code Formatting and Linting - *Proper formatting
|
||||||
|
and static analysis are enforced before changes become eligible for final
|
||||||
|
Gerrit review and automated integration.*
|
||||||
|
* **Upstream:** T5 | Commit Metadata and History Standardization - *Accurate
|
||||||
|
commit messaging and isolated history must be validated by reviewers prior
|
||||||
|
to receiving approval labels.*
|
||||||
|
* **Downstream:** T3 | CI Builder Environment and Execution Integrity -
|
||||||
|
*Triggering the Commit-Queue directly invokes downstream LUCI environments
|
||||||
|
to guarantee execution integrity prior to branch merge.*
|
||||||
|
|
||||||
|
## Chapter: CI Builder Environment and Execution Integrity
|
||||||
|
|
||||||
|
**Context:** This section defines strict guidelines for ensuring the resilience
|
||||||
|
of build scripts against missing dependencies and managing process execution
|
||||||
|
contexts within LUCI and local testing environments. Adherence guarantees robust
|
||||||
|
verification across diverse operating systems and CI pipelines while preventing
|
||||||
|
silent builder failures.
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Rule ID | Principle / Constraint | Priority | Primary Symptom / Trap |
|
||||||
|
| :-------- | :------------------------ | :------- | :------------------------ |
|
||||||
|
| **T3-01** | Verification Against | High | Running a standard local |
|
||||||
|
: : Breaking Change Build : : `make` without testing :
|
||||||
|
: : Configurations : : strict configurations or :
|
||||||
|
: : : : breaking-change flags. :
|
||||||
|
| **T3-02** | Windows Developer Mode | Medium | Attempting to run full |
|
||||||
|
: : Requirements for Tool : : local verification on a :
|
||||||
|
: : Verification : : standard Windows user :
|
||||||
|
: : : : account. :
|
||||||
|
| **T3-03** | Graceful Degradation for | Medium | Assuming all local |
|
||||||
|
: : Missing Builder Utilities : : developer utilities exist :
|
||||||
|
: : : : in the strict CI builder :
|
||||||
|
: : : : environment and :
|
||||||
|
: : : : unconditionally executing :
|
||||||
|
: : : : them. :
|
||||||
|
| **T3-04** | Contextual Diagnostic | High | Observing a generic CI |
|
||||||
|
: : Logging for LUCI CI : : failure without isolating :
|
||||||
|
: : Failures : : the specific process :
|
||||||
|
: : : : execution context or :
|
||||||
|
: : : : dependency resolution :
|
||||||
|
: : : : step. :
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
#### T3-01: Verification Against Breaking Change Build Configurations
|
||||||
|
|
||||||
|
> **Rule:** Always explicitly test core build structure modifications with
|
||||||
|
> breaking changes enabled to ensure forward compatibility.
|
||||||
|
>
|
||||||
|
> **What:** When modifying core build structures, the build must be tested
|
||||||
|
> explicitly with breaking changes enabled to ensure forward compatibility and
|
||||||
|
> correct regeneration of generated files.
|
||||||
|
>
|
||||||
|
> **Applies To:** Local build environments and Makefile targets.
|
||||||
|
>
|
||||||
|
> **Why:** Changes might succeed in a standard default build but fail when
|
||||||
|
> breaking change toggles are activated, hiding underlying dependency or
|
||||||
|
> regeneration issues. Failing to adhere to this typically results in **Build
|
||||||
|
> Breakage / Stale Artifacts**.
|
||||||
|
|
||||||
|
**Trap 1: Running a standard local `make` without testing strict configurations
|
||||||
|
or breaking-change flags.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make -j
|
||||||
|
```
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make -j WITH_BREAKING_CHANGES=1
|
||||||
|
```
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#### T3-02: Windows Developer Mode Requirements for Tool Verification
|
||||||
|
|
||||||
|
> **Rule:** Must execute local tool verification on Windows (gWindows) using an
|
||||||
|
> Administrator account to enable Developer Mode.
|
||||||
|
>
|
||||||
|
> **What:** Local verification of git-repo tooling on Windows (gWindows)
|
||||||
|
> explicitly requires the host environment to be running with Administrator
|
||||||
|
> privileges to enable Developer Mode.
|
||||||
|
>
|
||||||
|
> **Applies To:** Windows (gWindows) test environments verifying file system
|
||||||
|
> operations.
|
||||||
|
>
|
||||||
|
> **Why:** Without Developer Mode enabled (which necessitates Admin rights),
|
||||||
|
> features relying on advanced OS-level file system operations (like symlinks)
|
||||||
|
> cannot execute, permanently blocking full local test suite execution on
|
||||||
|
> standard accounts. Failing to adhere to this typically results in
|
||||||
|
> **Verification Blocked / OS Permission Error**.
|
||||||
|
|
||||||
|
**Trap 1: Attempting to run full local verification on a standard Windows user
|
||||||
|
account.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
* Executing the test suite from a non-elevated command prompt on Windows
|
||||||
|
without Developer Mode.
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
* Elevate to an Administrator account to enable Developer Mode before
|
||||||
|
executing the test suite on gWindows.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#### T3-03: Graceful Degradation for Missing Builder Utilities
|
||||||
|
|
||||||
|
> **Rule:** Always implement auto-skip logic for optional utilities in build
|
||||||
|
> scripts rather than hard-failing when unavailable on the CI builder.
|
||||||
|
>
|
||||||
|
> **What:** Build scripts and test suites must implement auto-skip logic for
|
||||||
|
> optional, environment-specific utilities rather than hard-failing when the
|
||||||
|
> utility is unavailable on the CI builder.
|
||||||
|
>
|
||||||
|
> **Applies To:** CI Builder environment scripts and test suites, specifically
|
||||||
|
> testing external CLI utilities (e.g., `help2man`).
|
||||||
|
>
|
||||||
|
> **Why:** When a required utility was not pre-installed on the CI builder
|
||||||
|
> image, the build hard-failed. Adding auto-skip logic allows the CI pipeline to
|
||||||
|
> remain unblocked while still providing local testing benefits for developers
|
||||||
|
> who have the tool installed. Failing to adhere to this typically results in
|
||||||
|
> **Build Failure / Blocked CI**.
|
||||||
|
|
||||||
|
**Trap 1: Assuming all local developer utilities exist in the strict CI builder
|
||||||
|
environment and unconditionally executing them.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# BAD: Hard failure if utility is missing
|
||||||
|
subprocess.run(["help2man", "repo"], check=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# GOOD: Auto-skip test if utility is missing in the environment
|
||||||
|
if not shutil.which("help2man"):
|
||||||
|
self.skipTest("help2man not installed")
|
||||||
|
subprocess.run(["help2man", "repo"], check=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Exceptions:** Core dependencies required for fundamental build steps cannot be
|
||||||
|
skipped and must be installed on the bot image.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#### T3-04: Contextual Diagnostic Logging for LUCI CI Failures
|
||||||
|
|
||||||
|
> **Rule:** Must investigate CI builder failures by extracting and analyzing
|
||||||
|
> full execution context logs to isolate environmental roadblocks.
|
||||||
|
>
|
||||||
|
> **What:** CI builder failures must be investigated using full execution
|
||||||
|
> context logs (e.g., LUCI context, vpython3 resolution, and retcode outputs) to
|
||||||
|
> isolate environmental roadblocks.
|
||||||
|
>
|
||||||
|
> **Applies To:** LUCI builder execution environment, vpython3 resolution, and
|
||||||
|
> CI pipeline debugging.
|
||||||
|
>
|
||||||
|
> **Why:** CI commands failed with `retcode 1` due to external factors like
|
||||||
|
> specific URLs being flagged as suspect by internal security tools, breaking
|
||||||
|
> the build environment. Failing to adhere to this typically results in **Silent
|
||||||
|
> Builder Failure**.
|
||||||
|
|
||||||
|
**Trap 1: Observing a generic CI failure without isolating the specific process
|
||||||
|
execution context or dependency resolution step.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
* Restarting the CI pipeline blindly when a job fails with a generic retcode,
|
||||||
|
ignoring potential external network or security blockers.
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
* Extract the step-by-step LUCI context log, verify path resolution (e.g.,
|
||||||
|
CIPD packages), and explicitly document external blockers like security
|
||||||
|
flags in the review.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
### Cross-Domain Dependencies
|
||||||
|
|
||||||
|
* **Upstream:** T6 | Testing Strategy and State Isolation - *Test
|
||||||
|
implementation dictates how missing builder utilities are mocked or
|
||||||
|
gracefully skipped during execution.*
|
||||||
|
* **Downstream:** T1 | Gerrit Submission and Labeling Workflow - *Automated
|
||||||
|
Verified+1 labels rely entirely on the stable, unblocked execution of CI
|
||||||
|
builder pipelines.*
|
||||||
|
|
||||||
|
## Chapter: Python Code Formatting and Linting
|
||||||
|
|
||||||
|
**Context:** This domain governs the automated enforcement of Python style
|
||||||
|
guidelines, mandating strict PEP-8 compliance, import sorting, and consistent
|
||||||
|
string quoting. All Python modifications must pass centralized static analysis
|
||||||
|
pipelines before integration to ensure codebase uniformity and prevent CI
|
||||||
|
regressions.
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Rule ID | Principle / Constraint | Priority | Primary Symptom / Trap |
|
||||||
|
| :-------- | :----------------------- | :------- | :------------------------ |
|
||||||
|
| **T4-01** | Automated Flake8 | Medium | Relying purely on manual |
|
||||||
|
: : Post-Submit Verification : : code review or sporadic :
|
||||||
|
: : : : local linting without a :
|
||||||
|
: : : : continuous integration :
|
||||||
|
: : : : check. :
|
||||||
|
| **T4-02** | Mandatory Python | High | Using single quotes for |
|
||||||
|
: : Formatting and Import : : strings and appending new :
|
||||||
|
: : Sorting : : imports to the bottom of :
|
||||||
|
: : : : the import block without :
|
||||||
|
: : : : alphabetical or :
|
||||||
|
: : : : categorical sorting. :
|
||||||
|
| **T4-03** | Strict Python Import | High | Mixing local application |
|
||||||
|
: : Ordering : : imports with standard :
|
||||||
|
: : : : library imports, causing :
|
||||||
|
: : : : linting tools to fail the :
|
||||||
|
: : : : CQ job. :
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
#### T4-01: Automated Flake8 Post-Submit Verification
|
||||||
|
|
||||||
|
> **Rule:** Always configure and maintain centralized CI workflows to
|
||||||
|
> automatically run static analysis and validate Python code styling
|
||||||
|
> post-submit.
|
||||||
|
>
|
||||||
|
> **What:** Static analysis and Python linting must be automated via a
|
||||||
|
> centralized CI pipeline (e.g., Flake8 post-submit workflows) to enforce
|
||||||
|
> consistent style and prevent basic errors.
|
||||||
|
>
|
||||||
|
> **Applies To:** All Python files in the git-repo codebase; specifically
|
||||||
|
> validated via `.github/workflows/flake8-postsubmit.yml`.
|
||||||
|
>
|
||||||
|
> **Why:** Relying strictly on manual code review to catch styling and linting
|
||||||
|
> violations is error-prone. Automation ensures a baseline of code quality on
|
||||||
|
> every code push without consuming human review cycles. Failing to adhere to
|
||||||
|
> this typically results in **Linting Regression / Style Violation**.
|
||||||
|
|
||||||
|
**Trap 1: Relying purely on manual code review or sporadic local linting without
|
||||||
|
a continuous integration check.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
* Committing Python code without an active CI linting workflow configuration.
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
* Maintain `.github/workflows/flake8-postsubmit.yml` to automatically run
|
||||||
|
flake8 on target branches.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#### T4-02: Mandatory Python Formatting and Import Sorting
|
||||||
|
|
||||||
|
> **Rule:** Must format Python code to enforce double-quoted strings and
|
||||||
|
> alphabetically sorted import blocks to satisfy automated formatting checks.
|
||||||
|
>
|
||||||
|
> **What:** Python code modifications must pass automated style and linting
|
||||||
|
> checks ('Verify git-repo CL'), which strictly enforce string quote conventions
|
||||||
|
> (preferring double quotes), import block sorting, and PEP-8 style formatting.
|
||||||
|
>
|
||||||
|
> **Applies To:** All Python source files modified in the git-repo codebase.
|
||||||
|
>
|
||||||
|
> **Why:** Developers submitting patches with single-quoted strings or unsorted
|
||||||
|
> imports triggered automated CI failures in the `Verify git-repo CL` job,
|
||||||
|
> completely blocking code submission until formatting tools were executed
|
||||||
|
> locally. Failing to adhere to this typically results in **CI Pipeline
|
||||||
|
> Failure**.
|
||||||
|
|
||||||
|
**Trap 1: Using single quotes for strings and appending new imports to the
|
||||||
|
bottom of the import block without alphabetical or categorical sorting.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
msg = 'This is an error'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
msg = "This is an error"
|
||||||
|
```
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#### T4-03: Strict Python Import Ordering
|
||||||
|
|
||||||
|
> **Rule:** Always segment and order Python imports strictly according to
|
||||||
|
> project standards (standard library, third-party, local) to prevent CQ
|
||||||
|
> pipeline failures.
|
||||||
|
>
|
||||||
|
> **What:** Python module imports must adhere strictly to the project's
|
||||||
|
> formatting rules (e.g., standard library, third-party, local module ordering)
|
||||||
|
> to pass automated Commit-Queue (CQ) checks.
|
||||||
|
>
|
||||||
|
> **Applies To:** Python source files.
|
||||||
|
>
|
||||||
|
> **Why:** Non-standard import blocks cause the automated CI/CQ linting pipeline
|
||||||
|
> to fail, completely blocking submission even if the core functional logic of
|
||||||
|
> the patch is flawless. Failing to adhere to this typically results in **CI
|
||||||
|
> Linting Failure**.
|
||||||
|
|
||||||
|
**Trap 1: Mixing local application imports with standard library imports,
|
||||||
|
causing linting tools to fail the CQ job.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sys
|
||||||
|
import my_local_module
|
||||||
|
import os
|
||||||
|
```
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import my_local_module
|
||||||
|
```
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
### Cross-Domain Dependencies
|
||||||
|
|
||||||
|
* **Upstream:** T3 | CI Builder Environment and Execution Integrity -
|
||||||
|
*Reliable CI builder environments must be available to execute the static
|
||||||
|
analysis and Python formatting verifications.*
|
||||||
|
* **Downstream:** T1 | Gerrit Submission and Labeling Workflow - *Formatting
|
||||||
|
and linting rules must be fully satisfied before automated mechanisms like
|
||||||
|
the Commit-Queue (CQ+2) will merge code into the repository.*
|
||||||
|
|
||||||
|
## Chapter: Commit Metadata and History Standardization
|
||||||
|
|
||||||
|
**Context:** This domain governs the structural composition and metadata
|
||||||
|
formatting of change lists (CLs) within the git-repo codebase. Strict adherence
|
||||||
|
ensures precise issue tracker integration, reliable CI/CD parsing, and atomic,
|
||||||
|
bisectable repository history.
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Rule ID | Principle / Constraint | Priority | Primary Symptom / Trap |
|
||||||
|
| :-------- | :------------------------ | :------- | :------------------------ |
|
||||||
|
| **T5-01** | Strict Commit Message Bug | Medium | Providing free-text |
|
||||||
|
: : Tag Formatting : : descriptions, arbitrary :
|
||||||
|
: : : : prefixes, or non-standard :
|
||||||
|
: : : : bug references in the :
|
||||||
|
: : : : commit block. :
|
||||||
|
| **T5-02** | Atomic and Bisectable | High | Waiting for an entire |
|
||||||
|
: : Change Integration : : feature stack of multiple :
|
||||||
|
: : : : interdependent CLs to be :
|
||||||
|
: : : : approved before merging :
|
||||||
|
: : : : the base commits. :
|
||||||
|
| **T5-03** | Explicit Bug Tracker | Medium | Submitting a fix or |
|
||||||
|
: : Linking for Context : : revert without :
|
||||||
|
: : Restoration : : referencing the :
|
||||||
|
: : : : corresponding bug tracker :
|
||||||
|
: : : : issue detailing the :
|
||||||
|
: : : : specific regression or :
|
||||||
|
: : : : stack trace. :
|
||||||
|
| **T5-04** | Atomic Change List | Medium | Submitting a single large |
|
||||||
|
: : Decomposition : : CL that touches multiple :
|
||||||
|
: : : : isolated components or :
|
||||||
|
: : : : implements several :
|
||||||
|
: : : : distinct features :
|
||||||
|
: : : : simultaneously. :
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
#### T5-01: Strict Commit Message Bug Tag Formatting
|
||||||
|
|
||||||
|
> **Rule:** Must use the exact `Bug: <number>` syntax in commit messages to
|
||||||
|
> properly link issue trackers.
|
||||||
|
>
|
||||||
|
> **What:** Commit messages must link directly to issue trackers using the
|
||||||
|
> explicit 'Bug: <number>' syntax to allow reliable parsing by CI/CD and history
|
||||||
|
> tracking systems.
|
||||||
|
>
|
||||||
|
> **Applies To:** Commit messages across all git-repo changes.
|
||||||
|
>
|
||||||
|
> **Why:** Improperly formatted bug tags fail to link with the external issue
|
||||||
|
> tracker, severing historical context and breaking automated post-submit
|
||||||
|
> tracking workflows. Failing to adhere to this typically results in **Broken
|
||||||
|
> Traceability / Pre-submit Failure**.
|
||||||
|
|
||||||
|
**Trap 1: Providing free-text descriptions, arbitrary prefixes, or non-standard
|
||||||
|
bug references in the commit block.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
Fixes bug 486536908
|
||||||
|
Closes issue 486536908
|
||||||
|
```
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
Bug: 486536908
|
||||||
|
```
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#### T5-02: Atomic and Bisectable Change Integration
|
||||||
|
|
||||||
|
> **Rule:** Always submit code incrementally as isolated, functional units
|
||||||
|
> rather than hoarding monolithic stacks.
|
||||||
|
>
|
||||||
|
> **What:** Code changes must be submitted incrementally as isolated, functional
|
||||||
|
> units rather than waiting to merge a massive interdependent stack all at once.
|
||||||
|
>
|
||||||
|
> **Applies To:** Git commit history, PR structuring, and stack-based code
|
||||||
|
> integration.
|
||||||
|
>
|
||||||
|
> **Why:** Contributors accustomed to integrating full monolithic stacks at once
|
||||||
|
> held off on landing initial, stable changes. This practice hinders the ability
|
||||||
|
> to isolate regressions via `git bisect` and prevents foundational code from
|
||||||
|
> "baking" in production. Failing to adhere to this typically results in
|
||||||
|
> **Bisection Breakage / Monolithic Rollbacks**.
|
||||||
|
|
||||||
|
**Trap 1: Waiting for an entire feature stack of multiple interdependent CLs to
|
||||||
|
be approved before merging the base commits.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
* Holding all changes in a stack locally or in code review until the final
|
||||||
|
feature patch is approved, then landing 10+ patches simultaneously.
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
* Landing initial, independent CLs one-by-one as soon as they are approved.
|
||||||
|
Ensuring each commit is independently usable and does not break the build.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#### T5-03: Explicit Bug Tracker Linking for Context Restoration
|
||||||
|
|
||||||
|
> **Rule:** Must include a direct URL to the relevant bug tracker issue
|
||||||
|
> documenting the failure traceback when submitting a regression fix or revert.
|
||||||
|
>
|
||||||
|
> **What:** When submitting a change (especially a revert or bug fix) addressing
|
||||||
|
> a specific runtime regression, the commit metadata or patchset-level comments
|
||||||
|
> must include a direct link to the bug tracker issue documenting the failure
|
||||||
|
> traceback.
|
||||||
|
>
|
||||||
|
> **Applies To:** Commit messages and patchset documentation during code
|
||||||
|
> reviews, particularly for reverts.
|
||||||
|
>
|
||||||
|
> **Why:** A previous commit caused a runtime regression (e.g., an
|
||||||
|
> AttributeError related to a missing object attribute). Without linking the
|
||||||
|
> specific issue containing the traceback, reviewers lacked the necessary
|
||||||
|
> context to justify restoring the previous codebase state. Failing to adhere to
|
||||||
|
> this typically results in **Undocumented Regression / Context Loss**.
|
||||||
|
|
||||||
|
**Trap 1: Submitting a fix or revert without referencing the corresponding bug
|
||||||
|
tracker issue detailing the specific regression or stack trace.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
* Reverting a change with a vague description like "Fixing previous breakage"
|
||||||
|
or "Reverting due to pipeline failure" without providing the traceback
|
||||||
|
source.
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
* Linking the specific issue tracker URL containing the exact failure mode.
|
||||||
|
Example: "for more context, see
|
||||||
|
https://g-issues.gerritcodereview.com/issues/[ISSUE_ID]#comment4"
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#### T5-04: Atomic Change List Decomposition
|
||||||
|
|
||||||
|
> **Rule:** Never submit large, monolithic change lists; always decompose them
|
||||||
|
> into logically independent patchsets.
|
||||||
|
>
|
||||||
|
> **What:** Large, monolithic change lists (CLs) must be broken down into
|
||||||
|
> smaller, logically independent patchsets to ensure accurate review and
|
||||||
|
> historical bisectability.
|
||||||
|
>
|
||||||
|
> **Applies To:** Version control history and code review scoping.
|
||||||
|
>
|
||||||
|
> **Why:** Massive CLs heavily increase reviewer cognitive load, making thorough
|
||||||
|
> reviews impossible and complicating future `git bisect` operations when
|
||||||
|
> tracking down the origin of a regression. Failing to adhere to this typically
|
||||||
|
> results in **Unreviewable Monolithic Change**.
|
||||||
|
|
||||||
|
**Trap 1: Submitting a single large CL that touches multiple isolated components
|
||||||
|
or implements several distinct features simultaneously.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
* A single CL containing sweeping refactoring, new feature implementation, and
|
||||||
|
unrelated bug fixes.
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
* Breaking the monolithic change into smaller, logically dependent or
|
||||||
|
independent CLs where each addresses one specific piece of the feature or
|
||||||
|
refactor.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
### Cross-Domain Dependencies
|
||||||
|
|
||||||
|
* **Downstream:** T1 | Gerrit Submission and Labeling Workflow - *Gerrit and
|
||||||
|
CI pipelines strictly rely on standardized commit metadata to link tracking
|
||||||
|
issues and depend on atomic patchsets to execute automated review and
|
||||||
|
verification correctly.*
|
||||||
|
|
||||||
|
## Chapter: Testing Strategy and State Isolation
|
||||||
|
|
||||||
|
**Context:** This chapter governs test implementation boundaries, emphasizing
|
||||||
|
pragmatic mocking limits to prevent false positives and detailing acceptable
|
||||||
|
workflows for deferred test coverage. Strict adherence ensures robust state
|
||||||
|
isolation and maintains development velocity without compromising verification
|
||||||
|
integrity.
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Rule ID | Principle / Constraint | Priority | Primary Symptom / Trap |
|
||||||
|
| :-------- | :----------------------- | :------- | :------------------------- |
|
||||||
|
| **T6-01** | Pragmatic Mocking | Medium | Mocking the entire core |
|
||||||
|
: : Boundaries in Unit Tests : : state or framework :
|
||||||
|
: : : : dependencies just to force :
|
||||||
|
: : : : a unit test for a highly :
|
||||||
|
: : : : integrated function. :
|
||||||
|
| **T6-02** | Deferred Test | Medium | Submitting functional code |
|
||||||
|
: : Implementation via : : without matching test :
|
||||||
|
: : Follow-up : : coverage and stalling the :
|
||||||
|
: : : : merge while complex tests :
|
||||||
|
: : : : are written. :
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
#### T6-01: Pragmatic Mocking Boundaries in Unit Tests
|
||||||
|
|
||||||
|
> **Rule:** Always restrict unit tests to isolated methods and avoid aggressive
|
||||||
|
> mocking of core functionality to prevent brittle, false-positive verification.
|
||||||
|
>
|
||||||
|
> **What:** Do not aggressively mock core functionality in unit tests; restrict
|
||||||
|
> unit tests to isolated methods to avoid creating brittle tests based on false
|
||||||
|
> assumptions when an integration framework is unavailable.
|
||||||
|
>
|
||||||
|
> **Applies To:** Test suite implementation (Unit vs. Integration testing
|
||||||
|
> boundaries).
|
||||||
|
>
|
||||||
|
> **Why:** Over-mocking complex systems in unit tests leads to scenarios where
|
||||||
|
> tests pass but the core integration fails in production because the unit test
|
||||||
|
> mocks assumed incorrect behavior about the underlying environment. Failing to
|
||||||
|
> adhere to this typically results in **False Positive Test Passage**.
|
||||||
|
|
||||||
|
**Trap 1: Mocking the entire core state or framework dependencies just to force
|
||||||
|
a unit test for a highly integrated function.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
* Mocking file systems, external processes, and global state heavily to test a
|
||||||
|
core workflow orchestrator in a unit test suite.
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
* Limiting unit tests strictly to isolated utility methods (e.g., adding
|
||||||
|
promisor files) and explicitly documenting testing gaps that require
|
||||||
|
integration test frameworks.
|
||||||
|
|
||||||
|
**Exceptions:** Isolated helper methods or purely functional data
|
||||||
|
transformations should be fully unit tested with appropriate mocked inputs.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#### T6-02: Deferred Test Implementation via Follow-up
|
||||||
|
|
||||||
|
> **Rule:** Never stall critical feature merges indefinitely for test
|
||||||
|
> implementation if maintainers authorize formalized, immediate follow-up test
|
||||||
|
> coverage.
|
||||||
|
>
|
||||||
|
> **What:** New logic requires automated tests; however, reviewers may permit
|
||||||
|
> test coverage to be implemented in a subsequent follow-up CL to maintain
|
||||||
|
> development velocity.
|
||||||
|
>
|
||||||
|
> **Applies To:** Feature development, regression testing, and code review
|
||||||
|
> criteria.
|
||||||
|
>
|
||||||
|
> **Why:** Reviewers identified a lack of test coverage for new functionality
|
||||||
|
> but opted not to block the immediate patchset, instead formalizing the test
|
||||||
|
> requirement as a near-term follow-up task. Failing to adhere to this typically
|
||||||
|
> results in **Missing Test Coverage**.
|
||||||
|
|
||||||
|
**Trap 1: Submitting functional code without matching test coverage and stalling
|
||||||
|
the merge while complex tests are written.**
|
||||||
|
|
||||||
|
**Don't:**
|
||||||
|
|
||||||
|
* Blocking a necessary feature indefinitely due to missing unit tests when a
|
||||||
|
follow-up CL is viable and acceptable to maintainers.
|
||||||
|
|
||||||
|
**Do:**
|
||||||
|
|
||||||
|
* Approve the feature with an explicit, documented 'TODO' for a follow-up CL
|
||||||
|
dedicated strictly to adding the corresponding automated tests.
|
||||||
|
|
||||||
|
**Exceptions:** Critical path features or security fixes where a lack of
|
||||||
|
immediate coverage introduces an unacceptable regression risk.
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
### Cross-Domain Dependencies
|
||||||
|
|
||||||
|
* **Upstream:** T1 | Gerrit Submission and Labeling Workflow - *Reviewer
|
||||||
|
approval mechanisms and label enforcement dictate when a feature can merge
|
||||||
|
while deferring tests to a follow-up CL.*
|
||||||
|
* **Downstream:** T3 | CI Builder Environment and Execution Integrity -
|
||||||
|
*Pragmatically bounded unit and integration tests ensure reliable CI
|
||||||
|
pipeline execution without false-positive success markers.*
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -85,28 +85,43 @@ def _Color(fg=None, bg=None, attr=None):
|
|||||||
DEFAULT = None
|
DEFAULT = None
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
def SetDefaultColoring(state: Optional[str]) -> None:
|
||||||
"""Set coloring behavior to |state|.
|
"""Set coloring behavior to |state|.
|
||||||
|
|
||||||
This is useful for overriding config options via the command line.
|
This is useful for overriding config options via the command line.
|
||||||
"""
|
"""
|
||||||
if state is None:
|
|
||||||
# Leave it alone -- return quick!
|
|
||||||
return
|
|
||||||
|
|
||||||
global DEFAULT
|
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
|
DEFAULT = state
|
||||||
elif state in ("always", "yes", "true"):
|
|
||||||
DEFAULT = "always"
|
|
||||||
elif state in ("never", "no", "false"):
|
|
||||||
DEFAULT = "never"
|
|
||||||
|
|
||||||
|
|
||||||
class Coloring:
|
class Coloring:
|
||||||
def __init__(self, config, section_type):
|
def __init__(self, config, section_type):
|
||||||
self._section = "color.%s" % section_type
|
self._section = f"color.{section_type}"
|
||||||
self._config = config
|
self._config = config
|
||||||
self._out = sys.stdout
|
self._out = sys.stdout
|
||||||
|
|
||||||
@@ -115,16 +130,12 @@ class Coloring:
|
|||||||
on = self._config.GetString(self._section)
|
on = self._config.GetString(self._section)
|
||||||
if on is None:
|
if on is None:
|
||||||
on = self._config.GetString("color.ui")
|
on = self._config.GetString("color.ui")
|
||||||
|
if isinstance(on, str):
|
||||||
|
on = on.lower()
|
||||||
|
|
||||||
if on == "auto":
|
self._on = _CONFIG_TO_COLOR_SETTING.get(on, _CHECK_CONSOLE)
|
||||||
if pager.active or os.isatty(1):
|
if self._on is _CHECK_CONSOLE:
|
||||||
self._on = True
|
self._on = pager.active or os.isatty(1)
|
||||||
else:
|
|
||||||
self._on = False
|
|
||||||
elif on in ("true", "always"):
|
|
||||||
self._on = True
|
|
||||||
else:
|
|
||||||
self._on = False
|
|
||||||
|
|
||||||
def redirect(self, out):
|
def redirect(self, out):
|
||||||
self._out = out
|
self._out = out
|
||||||
|
|||||||
+26
-4
@@ -17,6 +17,7 @@ import multiprocessing
|
|||||||
import optparse
|
import optparse
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from error import InvalidProjectGroupsError
|
from error import InvalidProjectGroupsError
|
||||||
from error import NoSuchProjectError
|
from error import NoSuchProjectError
|
||||||
@@ -25,6 +26,10 @@ from event_log import EventLog
|
|||||||
import progress
|
import progress
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from project import Project
|
||||||
|
|
||||||
|
|
||||||
# Are we generating man-pages?
|
# Are we generating man-pages?
|
||||||
GENERATE_MANPAGES = os.environ.get("_REPO_GENERATE_MANPAGES_") == " indeed! "
|
GENERATE_MANPAGES = os.environ.get("_REPO_GENERATE_MANPAGES_") == " indeed! "
|
||||||
|
|
||||||
@@ -61,6 +66,10 @@ class Command:
|
|||||||
# command to show short-vs-full summaries.
|
# command to show short-vs-full summaries.
|
||||||
COMMON = False
|
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,
|
# Whether this command supports running in parallel. If greater than 0,
|
||||||
# it is the number of parallel jobs to default to.
|
# it is the number of parallel jobs to default to.
|
||||||
PARALLEL_JOBS = None
|
PARALLEL_JOBS = None
|
||||||
@@ -242,6 +251,12 @@ class Command:
|
|||||||
# from the user's perspective.
|
# from the user's perspective.
|
||||||
opt.outer_manifest = True
|
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):
|
def ValidateOptions(self, opt, args):
|
||||||
"""Validate the user options & arguments before executing.
|
"""Validate the user options & arguments before executing.
|
||||||
|
|
||||||
@@ -375,7 +390,7 @@ class Command:
|
|||||||
manifest=None,
|
manifest=None,
|
||||||
groups="",
|
groups="",
|
||||||
missing_ok=False,
|
missing_ok=False,
|
||||||
submodules_ok=False,
|
submodules_ok=None,
|
||||||
all_manifests=False,
|
all_manifests=False,
|
||||||
):
|
):
|
||||||
"""A list of projects that match the arguments.
|
"""A list of projects that match the arguments.
|
||||||
@@ -385,7 +400,9 @@ class Command:
|
|||||||
manifest: an XmlManifest, the manifest to use, or None for default.
|
manifest: an XmlManifest, the manifest to use, or None for default.
|
||||||
groups: a string, the manifest groups in use.
|
groups: a string, the manifest groups in use.
|
||||||
missing_ok: a boolean, whether to allow missing projects.
|
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
|
all_manifests: a boolean, if True then all manifests and
|
||||||
submanifests are used. If False, then only the local
|
submanifests are used. If False, then only the local
|
||||||
(sub)manifest is used.
|
(sub)manifest is used.
|
||||||
@@ -403,6 +420,11 @@ class Command:
|
|||||||
all_projects_list = manifest.projects
|
all_projects_list = manifest.projects
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
|
def should_include_submodules(project: "Project") -> bool:
|
||||||
|
if submodules_ok is None:
|
||||||
|
return project.sync_s
|
||||||
|
return submodules_ok
|
||||||
|
|
||||||
if not groups:
|
if not groups:
|
||||||
groups = manifest.GetManifestGroupsStr()
|
groups = manifest.GetManifestGroupsStr()
|
||||||
groups = [x for x in re.split(r"[,\s]+", groups) if x]
|
groups = [x for x in re.split(r"[,\s]+", groups) if x]
|
||||||
@@ -410,7 +432,7 @@ class Command:
|
|||||||
if not args:
|
if not args:
|
||||||
derived_projects = {}
|
derived_projects = {}
|
||||||
for project in all_projects_list:
|
for project in all_projects_list:
|
||||||
if submodules_ok or project.sync_s:
|
if should_include_submodules(project):
|
||||||
derived_projects.update(
|
derived_projects.update(
|
||||||
(p.RelPath(local=False), p)
|
(p.RelPath(local=False), p)
|
||||||
for p in project.GetDerivedSubprojects()
|
for p in project.GetDerivedSubprojects()
|
||||||
@@ -452,7 +474,7 @@ class Command:
|
|||||||
if (
|
if (
|
||||||
project
|
project
|
||||||
and not project.Derived
|
and not project.Derived
|
||||||
and (submodules_ok or project.sync_s)
|
and should_include_submodules(project)
|
||||||
):
|
):
|
||||||
search_again = False
|
search_again = False
|
||||||
for subproject in project.GetDerivedSubprojects():
|
for subproject in project.GetDerivedSubprojects():
|
||||||
|
|||||||
+5
-1
@@ -322,7 +322,10 @@ _repo() {
|
|||||||
'--no-clone-bundle[Do not use clone bundle]' \
|
'--no-clone-bundle[Do not use clone bundle]' \
|
||||||
'(-u --manifest-server-username)'{-u,--manifest-server-username=}'[Username for manifest server]:username:' \
|
'(-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:' \
|
'(-p --manifest-server-password)'{-p,--manifest-server-password=}'[Password for manifest server]:password:' \
|
||||||
'--fetch-submodules[Fetch submodules]' \
|
'--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]' \
|
'--use-superproject[Use superproject]' \
|
||||||
'--no-use-superproject[Do not use superproject]' \
|
'--no-use-superproject[Do not use superproject]' \
|
||||||
'--tags[Sync tags]' \
|
'--tags[Sync tags]' \
|
||||||
@@ -397,6 +400,7 @@ _repo() {
|
|||||||
'--no-verify[Do not verify]' \
|
'--no-verify[Do not verify]' \
|
||||||
'--verify[Verify]' \
|
'--verify[Verify]' \
|
||||||
'--ignore-hooks[Ignore hooks]' \
|
'--ignore-hooks[Ignore hooks]' \
|
||||||
|
'--fix[Automatically fix]' \
|
||||||
'*: :->project'
|
'*: :->project'
|
||||||
;;
|
;;
|
||||||
version)
|
version)
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Fetch Command Contract
|
||||||
|
|
||||||
|
The `repo.fetchcmd` configuration allows specifying a custom command to be
|
||||||
|
executed during `repo sync` to fetch objects, instead of using standard
|
||||||
|
`git fetch`. This is particularly useful in environments with virtualized
|
||||||
|
filesystems or lazy checkouts where fetching metadata and downloading file
|
||||||
|
contents should be decoupled.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
To use this feature, set the following in `.repo/manifests.git/config`:
|
||||||
|
```ini
|
||||||
|
[repo]
|
||||||
|
fetchcmd = "your custom command here"
|
||||||
|
uselocalgitdirs = true
|
||||||
|
```
|
||||||
|
Setting `repo.fetchcmd` **requires** `repo.uselocalgitdirs` to be set to `true`.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The custom command is executed in a subshell populated with standard
|
||||||
|
project-context environment variables. For details on standard variables (such
|
||||||
|
as `REPO_PROJECT`, `REPO_PATH`, `REPO_PROJECT_FETCH_URL`, etc.), see the
|
||||||
|
Environment section in `repo help forall` or `subcmds/forall.py`.
|
||||||
|
|
||||||
|
The following environment variable is specific to `repo.fetchcmd`:
|
||||||
|
|
||||||
|
* `REPO_TREV`: The target revision resolved to a full commit hash.
|
||||||
|
|
||||||
|
## Contract
|
||||||
|
|
||||||
|
### Postconditions on exit 0
|
||||||
|
|
||||||
|
After the fetch command exits with status 0, `repo` expects the following
|
||||||
|
postconditions to be met:
|
||||||
|
|
||||||
|
1. `git cat-file -e REPO_TREV` succeeds (the commit must exist in the object
|
||||||
|
store).
|
||||||
|
2. The mapped local tracking ref (e.g. `refs/remotes/REPO_REMOTE/<branch>`
|
||||||
|
for a branch revision, or the tag ref itself for a tag) must point to
|
||||||
|
`REPO_TREV`.
|
||||||
|
3. `FETCH_HEAD` must point to `REPO_TREV`.
|
||||||
|
4. The commit graph from `REPO_TREV` must be reachable far enough to compute
|
||||||
|
merge bases with local branches.
|
||||||
|
|
||||||
|
### Invariants
|
||||||
|
|
||||||
|
* The command should be idempotent; fetching the same `REPO_TREV` twice should
|
||||||
|
be a no-op.
|
||||||
|
* Only `FETCH_HEAD` and `refs/remotes/*` should be modified to preserve
|
||||||
|
`repo sync --network-only` semantics. `HEAD` and local branches must not be
|
||||||
|
touched by the fetch command.
|
||||||
|
* Dirty worktree state must be preserved.
|
||||||
|
* The command is **not** executed for `MetaProject`s (i.e. the internal `repo`
|
||||||
|
repository itself at `.repo/repo` and the `manifests` repository at
|
||||||
|
`.repo/manifests`).
|
||||||
|
|
||||||
|
### Failure
|
||||||
|
|
||||||
|
* A non-zero exit status aborts the project's sync, and the command's stderr
|
||||||
|
is surfaced to the user.
|
||||||
|
* `repo` verifies the tracking ref and target reachability after exit 0. Any
|
||||||
|
mismatch is treated as a failure.
|
||||||
@@ -182,7 +182,7 @@ User controlled settings are initialized when running `repo init`.
|
|||||||
| user.email | `--config-name` | User's e-mail address; Copied into `.git/config` when checking out a new project |
|
| user.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 |
|
| 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
|
[superproject]: https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects
|
||||||
|
|
||||||
### Repo hooks settings
|
### Repo hooks settings
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ following DTD:
|
|||||||
|
|
||||||
<!ELEMENT manifest-server EMPTY>
|
<!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>
|
<!ELEMENT submanifest EMPTY>
|
||||||
<!ATTLIST submanifest name ID #REQUIRED>
|
<!ATTLIST submanifest name ID #REQUIRED>
|
||||||
@@ -239,6 +240,11 @@ XML RPC service.
|
|||||||
|
|
||||||
See the [smart sync documentation](./smart-sync.md) for more details.
|
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
|
### Element submanifest
|
||||||
|
|
||||||
|
|||||||
+18
-1
@@ -83,6 +83,20 @@ then check it directly. Hooks should not normally modify the active git repo
|
|||||||
the user. Although user interaction is discouraged in the common case, it can
|
the user. Although user interaction is discouraged in the common case, it can
|
||||||
be useful when deploying automatic fixes.
|
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
|
### Shebang Handling
|
||||||
|
|
||||||
*** note
|
*** note
|
||||||
@@ -119,7 +133,7 @@ This hook runs when people run `repo upload`.
|
|||||||
The `pre-upload.py` file should be defined like:
|
The `pre-upload.py` file should be defined like:
|
||||||
|
|
||||||
```py
|
```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.
|
"""Main function invoked directly by repo.
|
||||||
|
|
||||||
We must use the name "main" as that is what repo requires.
|
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
|
project_list, so that each entry in project_list matches with a
|
||||||
directory in worktree_list. If None, we will attempt to calculate
|
directory in worktree_list. If None, we will attempt to calculate
|
||||||
the directories automatically.
|
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.
|
kwargs: Leave this here for forward-compatibility.
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
|
|||||||
+32
-1
@@ -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.
|
element. This is how the client knows what service to talk to.
|
||||||
|
|
||||||
```xml
|
```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
|
If the URL starts with `persistent-`, then the
|
||||||
[`git-remote-persistent-https` helper](https://github.com/git/git/blob/HEAD/contrib/persistent-https/README)
|
[`git-remote-persistent-https` helper](https://github.com/git/git/blob/HEAD/contrib/persistent-https/README)
|
||||||
is used to communicate with the server.
|
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
|
||||||
|
|
||||||
Credentials may be specified directly in typical `username:password`
|
Credentials may be specified directly in typical `username:password`
|
||||||
|
|||||||
@@ -70,6 +70,19 @@ class _GitCall:
|
|||||||
git = _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():
|
def RepoSourceVersion():
|
||||||
"""Return the version of the repo.git tree."""
|
"""Return the version of the repo.git tree."""
|
||||||
ver = getattr(RepoSourceVersion, "version", None)
|
ver = getattr(RepoSourceVersion, "version", None)
|
||||||
|
|||||||
+3
-3
@@ -40,7 +40,7 @@ from repo_trace import Trace
|
|||||||
# that is saved in the config.
|
# that is saved in the config.
|
||||||
SYNC_STATE_PREFIX = "repo.syncstate."
|
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 = {}
|
REVIEW_CACHE = {}
|
||||||
|
|
||||||
@@ -49,8 +49,8 @@ def IsChange(rev):
|
|||||||
return rev.startswith(R_CHANGES)
|
return rev.startswith(R_CHANGES)
|
||||||
|
|
||||||
|
|
||||||
def IsId(rev):
|
def IsId(rev: str) -> bool:
|
||||||
return ID_RE.match(rev)
|
return bool(ID_RE.match(rev))
|
||||||
|
|
||||||
|
|
||||||
def IsTag(rev):
|
def IsTag(rev):
|
||||||
|
|||||||
+37
-6
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from git_command import git_require
|
||||||
from git_command import GitCommand
|
from git_command import GitCommand
|
||||||
import platform_utils
|
import platform_utils
|
||||||
from repo_trace import Trace
|
from repo_trace import Trace
|
||||||
@@ -41,6 +42,17 @@ class GitRefs:
|
|||||||
self._EnsureLoaded()
|
self._EnsureLoaded()
|
||||||
return self._phyref
|
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):
|
def get(self, name):
|
||||||
try:
|
try:
|
||||||
return self.all[name]
|
return self.all[name]
|
||||||
@@ -87,8 +99,12 @@ class GitRefs:
|
|||||||
self._symref = {}
|
self._symref = {}
|
||||||
self._mtime = {}
|
self._mtime = {}
|
||||||
|
|
||||||
self._ReadRefs()
|
root_refs_loaded = self._ReadRefs()
|
||||||
self._ReadSymbolicRef(HEAD)
|
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
|
scan = self._symref
|
||||||
attempts = 0
|
attempts = 0
|
||||||
@@ -113,18 +129,32 @@ class GitRefs:
|
|||||||
"""Check if a ref_id is a null object ID."""
|
"""Check if a ref_id is a null object ID."""
|
||||||
return ref_id and all(ch == "0" for ch in ref_id)
|
return ref_id and all(ch == "0" for ch in ref_id)
|
||||||
|
|
||||||
def _ReadRefs(self) -> None:
|
def _ReadRefs(self) -> bool:
|
||||||
"""Read all references using git for-each-ref."""
|
"""Read all references using git for-each-ref.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Whether root refs, including HEAD when it exists, were loaded.
|
||||||
|
"""
|
||||||
|
include_root_refs = git_require((2, 45, 0))
|
||||||
|
cmd = [
|
||||||
|
"for-each-ref",
|
||||||
|
"--format=%(objectname)%00%(refname)%00%(symref)",
|
||||||
|
]
|
||||||
|
if include_root_refs:
|
||||||
|
cmd.insert(1, "--include-root-refs")
|
||||||
|
# Avoid caching volatile root refs such as ORIG_HEAD. HEAD and
|
||||||
|
# refs/* are the only namespaces GitRefs exposes to callers.
|
||||||
|
cmd.extend([HEAD, "refs"])
|
||||||
p = GitCommand(
|
p = GitCommand(
|
||||||
None,
|
None,
|
||||||
["for-each-ref", "--format=%(objectname)%00%(refname)%00%(symref)"],
|
cmd,
|
||||||
capture_stdout=True,
|
capture_stdout=True,
|
||||||
capture_stderr=True,
|
capture_stderr=True,
|
||||||
bare=True,
|
bare=True,
|
||||||
gitdir=self._gitdir,
|
gitdir=self._gitdir,
|
||||||
)
|
)
|
||||||
if p.Wait() != 0:
|
if p.Wait() != 0:
|
||||||
return
|
return False
|
||||||
|
|
||||||
for line in p.stdout.splitlines():
|
for line in p.stdout.splitlines():
|
||||||
ref_id, name, symref = line.split("\0")
|
ref_id, name, symref = line.split("\0")
|
||||||
@@ -132,6 +162,7 @@ class GitRefs:
|
|||||||
self._symref[name] = symref
|
self._symref[name] = symref
|
||||||
elif ref_id and not self._IsNullRef(ref_id):
|
elif ref_id and not self._IsNullRef(ref_id):
|
||||||
self._phyref[name] = ref_id
|
self._phyref[name] = ref_id
|
||||||
|
return include_root_refs
|
||||||
|
|
||||||
def _ReadSymbolicRef(self, name: str) -> None:
|
def _ReadSymbolicRef(self, name: str) -> None:
|
||||||
"""Read a symbolic reference."""
|
"""Read a symbolic reference."""
|
||||||
|
|||||||
+21
-4
@@ -36,7 +36,6 @@ from git_command import git_require
|
|||||||
from git_command import GitCommand
|
from git_command import GitCommand
|
||||||
from git_config import IsId
|
from git_config import IsId
|
||||||
from git_config import RepoConfig
|
from git_config import RepoConfig
|
||||||
from git_refs import GitRefs
|
|
||||||
import platform_utils
|
import platform_utils
|
||||||
|
|
||||||
|
|
||||||
@@ -189,7 +188,7 @@ class Superproject:
|
|||||||
if netloc:
|
if netloc:
|
||||||
parts = netloc.split("-review", 1)
|
parts = netloc.split("-review", 1)
|
||||||
host = parts[0]
|
host = parts[0]
|
||||||
rev = GitRefs(self._work_git).get("HEAD")
|
rev = self._GetRef("HEAD")
|
||||||
return f"{host}/{self.name}@{rev}"
|
return f"{host}/{self.name}@{rev}"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -314,7 +313,10 @@ class Superproject:
|
|||||||
# We use --negotiation-tip to speed up the fetch. Superproject branches
|
# 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
|
# do not share commits. So this lets git know it only needs to send
|
||||||
# commits reachable from the specified local refs.
|
# 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:
|
if rev_commit:
|
||||||
cmd.extend(["--negotiation-tip", rev_commit])
|
cmd.extend(["--negotiation-tip", rev_commit])
|
||||||
|
|
||||||
@@ -347,6 +349,21 @@ class Superproject:
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _GetRef(self, ref: str) -> str:
|
||||||
|
"""Resolve one local ref without loading the entire ref namespace."""
|
||||||
|
p = GitCommand(
|
||||||
|
None,
|
||||||
|
["rev-parse", "--verify", "--quiet", ref],
|
||||||
|
gitdir=self._work_git,
|
||||||
|
bare=True,
|
||||||
|
capture_stdout=True,
|
||||||
|
capture_stderr=True,
|
||||||
|
log_as_error=False,
|
||||||
|
)
|
||||||
|
if p.Wait() == 0:
|
||||||
|
return p.stdout.strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
def _LsTree(self):
|
def _LsTree(self):
|
||||||
"""Gets the commit ids for all projects.
|
"""Gets the commit ids for all projects.
|
||||||
|
|
||||||
@@ -473,7 +490,7 @@ class Superproject:
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
manifest_str = self._manifest.ToXml(
|
manifest_str = self._manifest.ToXml(
|
||||||
filter_groups=self._manifest.GetManifestGroupsStr(),
|
filter_groups="all",
|
||||||
omit_local=True,
|
omit_local=True,
|
||||||
).toxml()
|
).toxml()
|
||||||
manifest_path = self._manifest_path
|
manifest_path = self._manifest_path
|
||||||
|
|||||||
@@ -195,7 +195,13 @@ class BaseEventLog:
|
|||||||
|
|
||||||
def GetDataEventName(self, value):
|
def GetDataEventName(self, value):
|
||||||
"""Returns 'data-json' if the value is an array else returns 'data'."""
|
"""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):
|
def LogDataConfigEvents(self, config, prefix):
|
||||||
"""Append a 'data' event for each entry in |config| to the current log.
|
"""Append a 'data' event for each entry in |config| to the current log.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import optparse
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
@@ -68,6 +69,8 @@ class RepoHook:
|
|||||||
allow_all_hooks=False,
|
allow_all_hooks=False,
|
||||||
ignore_hooks=False,
|
ignore_hooks=False,
|
||||||
abort_if_user_denies=False,
|
abort_if_user_denies=False,
|
||||||
|
yes=False,
|
||||||
|
fix=False,
|
||||||
):
|
):
|
||||||
"""RepoHook constructor.
|
"""RepoHook constructor.
|
||||||
|
|
||||||
@@ -89,6 +92,8 @@ class RepoHook:
|
|||||||
ignore_hooks: If True, then 'Do not abort action if hooks fail'.
|
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
|
abort_if_user_denies: If True, we'll abort running the hook if the
|
||||||
user doesn't allow us to run the hook.
|
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._hook_type = hook_type
|
||||||
self._hooks_project = hooks_project
|
self._hooks_project = hooks_project
|
||||||
@@ -99,6 +104,8 @@ class RepoHook:
|
|||||||
self._allow_all_hooks = allow_all_hooks
|
self._allow_all_hooks = allow_all_hooks
|
||||||
self._ignore_hooks = ignore_hooks
|
self._ignore_hooks = ignore_hooks
|
||||||
self._abort_if_user_denies = abort_if_user_denies
|
self._abort_if_user_denies = abort_if_user_denies
|
||||||
|
self._yes = yes
|
||||||
|
self._fix = fix
|
||||||
|
|
||||||
# Store the full path to the script for convenience.
|
# Store the full path to the script for convenience.
|
||||||
self._script_fullpath = None
|
self._script_fullpath = None
|
||||||
@@ -374,8 +381,12 @@ class RepoHook:
|
|||||||
# def main(project_list, **kwargs):
|
# def main(project_list, **kwargs):
|
||||||
#
|
#
|
||||||
# This allows us to later expand the API without breaking old hooks.
|
# This allows us to later expand the API without breaking old hooks.
|
||||||
kwargs = kwargs.copy()
|
kwargs = {
|
||||||
kwargs["hook_should_take_kwargs"] = True
|
**kwargs,
|
||||||
|
"hook_should_take_kwargs": True,
|
||||||
|
"fix": self._fix,
|
||||||
|
"yes": self._yes,
|
||||||
|
}
|
||||||
|
|
||||||
# See what version of python the hook has been written against.
|
# See what version of python the hook has been written against.
|
||||||
data = open(self._script_fullpath).read()
|
data = open(self._script_fullpath).read()
|
||||||
@@ -497,12 +508,18 @@ class RepoHook:
|
|||||||
"origin"
|
"origin"
|
||||||
).url,
|
).url,
|
||||||
"bug_url": manifest.contactinfo.bugurl,
|
"bug_url": manifest.contactinfo.bugurl,
|
||||||
|
"yes": getattr(opt, "yes", False),
|
||||||
|
"fix": getattr(opt, "fix", False),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return cls(*args, **kwargs)
|
return cls(*args, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def AddOptionGroup(parser, name):
|
def AddOptionGroup(
|
||||||
|
parser: optparse.OptionParser,
|
||||||
|
name: str,
|
||||||
|
allow_fix: bool = False,
|
||||||
|
) -> None:
|
||||||
"""Help options relating to the various hooks."""
|
"""Help options relating to the various hooks."""
|
||||||
|
|
||||||
# Note that verify and no-verify are NOT opposites of each other, which
|
# Note that verify and no-verify are NOT opposites of each other, which
|
||||||
@@ -526,3 +543,10 @@ class RepoHook:
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Do not abort if %s hooks fail." % name,
|
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,
|
||||||
|
)
|
||||||
|
|||||||
+2
-3
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "July 2022" "repo abandon" "Repo Manual"
|
.TH REPO "1" "June 2026" "repo abandon" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo abandon - manual page for repo abandon
|
repo \- repo abandon - manual page for repo abandon
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -20,8 +20,7 @@ It is equivalent to "git branch \fB\-D\fR <branchname>".
|
|||||||
show this help message and exit
|
show this help message and exit
|
||||||
.TP
|
.TP
|
||||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||||
number of jobs to run in parallel (default: based on
|
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||||
number of CPU cores)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-all\fR
|
\fB\-\-all\fR
|
||||||
delete all branches in all projects
|
delete all branches in all projects
|
||||||
|
|||||||
+2
-3
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "July 2022" "repo branches" "Repo Manual"
|
.TH REPO "1" "June 2026" "repo branches" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo branches - manual page for repo branches
|
repo \- repo branches - manual page for repo branches
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -46,8 +46,7 @@ is shown, then the branch appears in all projects.
|
|||||||
show this help message and exit
|
show this help message and exit
|
||||||
.TP
|
.TP
|
||||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||||
number of jobs to run in parallel (default: based on
|
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||||
number of CPU cores)
|
|
||||||
.SS Logging options:
|
.SS Logging options:
|
||||||
.TP
|
.TP
|
||||||
\fB\-v\fR, \fB\-\-verbose\fR
|
\fB\-v\fR, \fB\-\-verbose\fR
|
||||||
|
|||||||
+2
-3
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "July 2022" "repo checkout" "Repo Manual"
|
.TH REPO "1" "June 2026" "repo checkout" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo checkout - manual page for repo checkout
|
repo \- repo checkout - manual page for repo checkout
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -15,8 +15,7 @@ Checkout a branch for development
|
|||||||
show this help message and exit
|
show this help message and exit
|
||||||
.TP
|
.TP
|
||||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||||
number of jobs to run in parallel (default: based on
|
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||||
number of CPU cores)
|
|
||||||
.SS Logging options:
|
.SS Logging options:
|
||||||
.TP
|
.TP
|
||||||
\fB\-v\fR, \fB\-\-verbose\fR
|
\fB\-v\fR, \fB\-\-verbose\fR
|
||||||
|
|||||||
+2
-3
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "July 2022" "repo diff" "Repo Manual"
|
.TH REPO "1" "June 2026" "repo diff" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo diff - manual page for repo diff
|
repo \- repo diff - manual page for repo diff
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -19,8 +19,7 @@ to the Unix 'patch' command.
|
|||||||
show this help message and exit
|
show this help message and exit
|
||||||
.TP
|
.TP
|
||||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||||
number of jobs to run in parallel (default: based on
|
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||||
number of CPU cores)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-u\fR, \fB\-\-absolute\fR
|
\fB\-u\fR, \fB\-\-absolute\fR
|
||||||
paths are relative to the repository root
|
paths are relative to the repository root
|
||||||
|
|||||||
+8
-11
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "May 2026" "repo forall" "Repo Manual"
|
.TH REPO "1" "June 2026" "repo forall" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo forall - manual page for repo forall
|
repo \- repo forall - manual page for repo forall
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -17,20 +17,16 @@ repo forall \fB\-r\fR str1 [str2] ... \fB\-c\fR <command> [<arg>...]
|
|||||||
show this help message and exit
|
show this help message and exit
|
||||||
.TP
|
.TP
|
||||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||||
number of jobs to run in parallel (default: based on
|
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||||
number of CPU cores)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-r\fR, \fB\-\-regex\fR
|
\fB\-r\fR, \fB\-\-regex\fR
|
||||||
execute the command only on projects matching regex or
|
execute the command only on projects matching regex or wildcard expression
|
||||||
wildcard expression
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-i\fR, \fB\-\-inverse\-regex\fR
|
\fB\-i\fR, \fB\-\-inverse\-regex\fR
|
||||||
execute the command only on projects not matching
|
execute the command only on projects not matching regex or wildcard expression
|
||||||
regex or wildcard expression
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-g\fR GROUPS, \fB\-\-groups\fR=\fI\,GROUPS\/\fR
|
\fB\-g\fR GROUPS, \fB\-\-groups\fR=\fI\,GROUPS\/\fR
|
||||||
execute the command only on projects matching the
|
execute the command only on projects matching the specified groups
|
||||||
specified groups
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-c\fR, \fB\-\-command\fR
|
\fB\-c\fR, \fB\-\-command\fR
|
||||||
command (and arguments) to execute
|
command (and arguments) to execute
|
||||||
@@ -39,8 +35,7 @@ command (and arguments) to execute
|
|||||||
abort if a command exits unsuccessfully
|
abort if a command exits unsuccessfully
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-ignore\-missing\fR
|
\fB\-\-ignore\-missing\fR
|
||||||
silently skip & do not exit non\-zero due missing
|
silently skip & do not exit non\-zero due missing checkouts
|
||||||
checkouts
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-interactive\fR
|
\fB\-\-interactive\fR
|
||||||
force interactive usage
|
force interactive usage
|
||||||
@@ -125,6 +120,8 @@ 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
|
REPO_DEST_BRANCH is the name of the destination branch for code review, as
|
||||||
specified in the manifest.
|
specified in the manifest.
|
||||||
.PP
|
.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.
|
REPO_COUNT is the total number of projects being iterated.
|
||||||
.PP
|
.PP
|
||||||
REPO_I is the current (1\-based) iteration count. Can be used in conjunction with
|
REPO_I is the current (1\-based) iteration count. Can be used in conjunction with
|
||||||
|
|||||||
+2
-3
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "April 2025" "repo gc" "Repo Manual"
|
.TH REPO "1" "June 2026" "repo gc" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo gc - manual page for repo gc
|
repo \- repo gc - manual page for repo gc
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -21,8 +21,7 @@ do everything except actually delete
|
|||||||
answer yes to all safe prompts
|
answer yes to all safe prompts
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-repack\fR
|
\fB\-\-repack\fR
|
||||||
repack all projects that use partial clone with
|
repack all projects that use partial clone with filter=blob:none
|
||||||
filter=blob:none
|
|
||||||
.SS Logging options:
|
.SS Logging options:
|
||||||
.TP
|
.TP
|
||||||
\fB\-v\fR, \fB\-\-verbose\fR
|
\fB\-v\fR, \fB\-\-verbose\fR
|
||||||
|
|||||||
+2
-3
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "July 2022" "repo grep" "Repo Manual"
|
.TH REPO "1" "June 2026" "repo grep" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo grep - manual page for repo grep
|
repo \- repo grep - manual page for repo grep
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -15,8 +15,7 @@ Print lines matching a pattern
|
|||||||
show this help message and exit
|
show this help message and exit
|
||||||
.TP
|
.TP
|
||||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||||
number of jobs to run in parallel (default: based on
|
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||||
number of CPU cores)
|
|
||||||
.SS Logging options:
|
.SS Logging options:
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-verbose\fR
|
\fB\-\-verbose\fR
|
||||||
|
|||||||
+3
-5
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "May 2026" "repo info" "Repo Manual"
|
.TH REPO "1" "June 2026" "repo info" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo info - manual page for repo info
|
repo \- repo info - manual page for repo info
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -15,12 +15,10 @@ Get info on the manifest branch, current branch or unmerged branches
|
|||||||
show this help message and exit
|
show this help message and exit
|
||||||
.TP
|
.TP
|
||||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||||
number of jobs to run in parallel (default: based on
|
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||||
number of CPU cores)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-d\fR, \fB\-\-diff\fR
|
\fB\-d\fR, \fB\-\-diff\fR
|
||||||
show full info and commit diff including remote
|
show full info and commit diff including remote branches
|
||||||
branches
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-o\fR, \fB\-\-overview\fR
|
\fB\-o\fR, \fB\-\-overview\fR
|
||||||
show overview of all local commits
|
show overview of all local commits
|
||||||
|
|||||||
+19
-30
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "September 2024" "repo init" "Repo Manual"
|
.TH REPO "1" "June 2026" "repo init" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo init - manual page for repo init
|
repo \- repo init - manual page for repo init
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -29,36 +29,29 @@ manifest repository location
|
|||||||
manifest branch or revision (use HEAD for default)
|
manifest branch or revision (use HEAD for default)
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-manifest\-upstream\-branch\fR=\fI\,BRANCH\/\fR
|
\fB\-\-manifest\-upstream\-branch\fR=\fI\,BRANCH\/\fR
|
||||||
when a commit is provided to \fB\-\-manifest\-branch\fR, this
|
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
|
||||||
is the name of the git ref in which the commit can be
|
|
||||||
found
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
|
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
|
||||||
initial manifest file
|
initial manifest file
|
||||||
.TP
|
.TP
|
||||||
\fB\-g\fR GROUP, \fB\-\-groups\fR=\fI\,GROUP\/\fR
|
\fB\-g\fR GROUP, \fB\-\-groups\fR=\fI\,GROUP\/\fR
|
||||||
restrict manifest projects to ones with specified
|
restrict manifest projects to ones with specified group(s) [default|all|G1,G2,G3|G4,\-G5,\-G6]
|
||||||
group(s) [default|all|G1,G2,G3|G4,\-G5,\-G6]
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-p\fR PLATFORM, \fB\-\-platform\fR=\fI\,PLATFORM\/\fR
|
\fB\-p\fR PLATFORM, \fB\-\-platform\fR=\fI\,PLATFORM\/\fR
|
||||||
restrict manifest projects to ones with a specified
|
restrict manifest projects to ones with a specified platform group [auto|all|none|linux|darwin|...]
|
||||||
platform group [auto|all|none|linux|darwin|...]
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-submodules\fR
|
\fB\-\-submodules\fR
|
||||||
sync any submodules associated with the manifest repo
|
sync any submodules associated with the manifest repo
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-standalone\-manifest\fR
|
\fB\-\-standalone\-manifest\fR
|
||||||
download the manifest as a static file rather then
|
download the manifest as a static file rather then create a git checkout of the manifest repo
|
||||||
create a git checkout of the manifest repo
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-manifest\-depth\fR=\fI\,DEPTH\/\fR
|
\fB\-\-manifest\-depth\fR=\fI\,DEPTH\/\fR
|
||||||
create a shallow clone of the manifest repo with given
|
create a shallow clone of the manifest repo with given depth (0 for full clone); see git clone (default: 0)
|
||||||
depth (0 for full clone); see git clone (default: 0)
|
|
||||||
.SS Manifest (only) checkout options:
|
.SS Manifest (only) checkout options:
|
||||||
.TP
|
.TP
|
||||||
\fB\-c\fR, \fB\-\-current\-branch\fR
|
\fB\-c\fR, \fB\-\-current\-branch\fR
|
||||||
fetch only current manifest branch from server
|
fetch only current manifest branch from server (default)
|
||||||
(default)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-no\-current\-branch\fR
|
\fB\-\-no\-current\-branch\fR
|
||||||
fetch all manifest branches from server
|
fetch all manifest branches from server
|
||||||
@@ -71,15 +64,16 @@ don't fetch tags in the manifest
|
|||||||
.SS Checkout modes:
|
.SS Checkout modes:
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-mirror\fR
|
\fB\-\-mirror\fR
|
||||||
create a replica of the remote repositories rather
|
create a replica of the remote repositories rather than a client working directory
|
||||||
than a client working directory
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-archive\fR
|
\fB\-\-archive\fR
|
||||||
checkout an archive instead of a git repository for
|
checkout an archive instead of a git repository for each project. See git archive.
|
||||||
each project. See git archive.
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-worktree\fR
|
\fB\-\-worktree\fR
|
||||||
use git\-worktree to manage projects
|
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:
|
.SS Project checkout optimizations:
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-reference\fR=\fI\,DIR\/\fR
|
\fB\-\-reference\fR=\fI\,DIR\/\fR
|
||||||
@@ -92,33 +86,28 @@ dissociate from reference mirrors after clone
|
|||||||
create a shallow clone with given depth; see git clone
|
create a shallow clone with given depth; see git clone
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-partial\-clone\fR
|
\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
|
.TP
|
||||||
\fB\-\-no\-partial\-clone\fR
|
\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
|
.TP
|
||||||
\fB\-\-partial\-clone\-exclude\fR=\fI\,PARTIAL_CLONE_EXCLUDE\/\fR
|
\fB\-\-partial\-clone\-exclude\fR=\fI\,PARTIAL_CLONE_EXCLUDE\/\fR
|
||||||
exclude the specified projects (a comma\-delimited
|
exclude the specified projects (a comma\-delimited project names) from partial clone (https://git\-scm.com/docs/partial\-clone)
|
||||||
project names) from partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-clone\-filter\fR=\fI\,CLONE_FILTER\/\fR
|
\fB\-\-clone\-filter\fR=\fI\,CLONE_FILTER\/\fR
|
||||||
filter for use with \fB\-\-partial\-clone\fR [default:
|
filter for use with \fB\-\-partial\-clone\fR [default: blob:none]
|
||||||
blob:none]
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-use\-superproject\fR
|
\fB\-\-use\-superproject\fR
|
||||||
use the manifest superproject to sync projects;
|
use the manifest superproject to sync projects; implies \fB\-c\fR
|
||||||
implies \fB\-c\fR
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-no\-use\-superproject\fR
|
\fB\-\-no\-use\-superproject\fR
|
||||||
disable use of manifest superprojects
|
disable use of manifest superprojects
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-clone\-bundle\fR
|
\fB\-\-clone\-bundle\fR
|
||||||
enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if
|
enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if not \fB\-\-partial\-clone\fR)
|
||||||
not \fB\-\-partial\-clone\fR)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-no\-clone\-bundle\fR
|
\fB\-\-no\-clone\-bundle\fR
|
||||||
disable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if
|
disable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if \fB\-\-partial\-clone\fR)
|
||||||
\fB\-\-partial\-clone\fR)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-git\-lfs\fR
|
\fB\-\-git\-lfs\fR
|
||||||
enable Git LFS support
|
enable Git LFS support
|
||||||
|
|||||||
+5
-9
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "July 2022" "repo list" "Repo Manual"
|
.TH REPO "1" "June 2026" "repo list" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo list - manual page for repo list
|
repo \- repo list - manual page for repo list
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -17,12 +17,10 @@ repo list [\-f] \fB\-r\fR str1 [str2]...
|
|||||||
show this help message and exit
|
show this help message and exit
|
||||||
.TP
|
.TP
|
||||||
\fB\-r\fR, \fB\-\-regex\fR
|
\fB\-r\fR, \fB\-\-regex\fR
|
||||||
filter the project list based on regex or wildcard
|
filter the project list based on regex or wildcard matching of strings
|
||||||
matching of strings
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-g\fR GROUPS, \fB\-\-groups\fR=\fI\,GROUPS\/\fR
|
\fB\-g\fR GROUPS, \fB\-\-groups\fR=\fI\,GROUPS\/\fR
|
||||||
filter the project list based on the groups the
|
filter the project list based on the groups the project is in
|
||||||
project is in
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-a\fR, \fB\-\-all\fR
|
\fB\-a\fR, \fB\-\-all\fR
|
||||||
show projects regardless of checkout state
|
show projects regardless of checkout state
|
||||||
@@ -34,12 +32,10 @@ display only the name of the repository
|
|||||||
display only the path of the repository
|
display only the path of the repository
|
||||||
.TP
|
.TP
|
||||||
\fB\-f\fR, \fB\-\-fullpath\fR
|
\fB\-f\fR, \fB\-\-fullpath\fR
|
||||||
display the full work tree path instead of the
|
display the full work tree path instead of the relative path
|
||||||
relative path
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-relative\-to\fR=\fI\,PATH\/\fR
|
\fB\-\-relative\-to\fR=\fI\,PATH\/\fR
|
||||||
display paths relative to this one (default: top of
|
display paths relative to this one (default: top of repo client checkout)
|
||||||
repo client checkout)
|
|
||||||
.SS Logging options:
|
.SS Logging options:
|
||||||
.TP
|
.TP
|
||||||
\fB\-v\fR, \fB\-\-verbose\fR
|
\fB\-v\fR, \fB\-\-verbose\fR
|
||||||
|
|||||||
+10
-9
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "April 2026" "repo manifest" "Repo Manual"
|
.TH REPO "1" "June 2026" "repo manifest" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo manifest - manual page for repo manifest
|
repo \- repo manifest - manual page for repo manifest
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -21,14 +21,10 @@ save revisions as current HEAD
|
|||||||
temporary manifest to use for this sync
|
temporary manifest to use for this sync
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-suppress\-upstream\-revision\fR
|
\fB\-\-suppress\-upstream\-revision\fR
|
||||||
if in \fB\-r\fR mode, do not write the upstream field (only
|
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)
|
||||||
of use if the branch names for a sha1 manifest are
|
|
||||||
sensitive)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-suppress\-dest\-branch\fR
|
\fB\-\-suppress\-dest\-branch\fR
|
||||||
if in \fB\-r\fR mode, do not write the dest\-branch field
|
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)
|
||||||
(only of use if the branch names for a sha1 manifest
|
|
||||||
are sensitive)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-format\fR=\fI\,FORMAT\/\fR
|
\fB\-\-format\fR=\fI\,FORMAT\/\fR
|
||||||
output format: xml, json (default: xml)
|
output format: xml, json (default: xml)
|
||||||
@@ -40,8 +36,7 @@ format output for humans to read
|
|||||||
ignore local manifests
|
ignore local manifests
|
||||||
.TP
|
.TP
|
||||||
\fB\-o\fR \-|NAME.xml, \fB\-\-output\-file\fR=\fI\,\-\/\fR|NAME.xml
|
\fB\-o\fR \-|NAME.xml, \fB\-\-output\-file\fR=\fI\,\-\/\fR|NAME.xml
|
||||||
file to save the manifest to. (Filename prefix for
|
file to save the manifest to. (Filename prefix for multi\-tree.)
|
||||||
multi\-tree.)
|
|
||||||
.SS Logging options:
|
.SS Logging options:
|
||||||
.TP
|
.TP
|
||||||
\fB\-v\fR, \fB\-\-verbose\fR
|
\fB\-v\fR, \fB\-\-verbose\fR
|
||||||
@@ -138,6 +133,7 @@ include*)>
|
|||||||
.IP
|
.IP
|
||||||
<!ELEMENT manifest\-server EMPTY>
|
<!ELEMENT manifest\-server EMPTY>
|
||||||
<!ATTLIST manifest\-server url CDATA #REQUIRED>
|
<!ATTLIST manifest\-server url CDATA #REQUIRED>
|
||||||
|
<!ATTLIST manifest\-server helper CDATA #IMPLIED>
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT submanifest EMPTY>
|
<!ELEMENT submanifest EMPTY>
|
||||||
<!ATTLIST submanifest name ID #REQUIRED>
|
<!ATTLIST submanifest name ID #REQUIRED>
|
||||||
@@ -347,6 +343,11 @@ specify the URL of a manifest server, which is an XML RPC service.
|
|||||||
.PP
|
.PP
|
||||||
See the [smart sync documentation](./smart\-sync.md) for more details.
|
See the [smart sync documentation](./smart\-sync.md) for more details.
|
||||||
.PP
|
.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
|
Element submanifest
|
||||||
.PP
|
.PP
|
||||||
One or more submanifest elements may be specified. Each element describes a
|
One or more submanifest elements may be specified. Each element describes a
|
||||||
|
|||||||
+2
-3
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "July 2022" "repo prune" "Repo Manual"
|
.TH REPO "1" "June 2026" "repo prune" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo prune - manual page for repo prune
|
repo \- repo prune - manual page for repo prune
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -15,8 +15,7 @@ Prune (delete) already merged topics
|
|||||||
show this help message and exit
|
show this help message and exit
|
||||||
.TP
|
.TP
|
||||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||||
number of jobs to run in parallel (default: based on
|
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||||
number of CPU cores)
|
|
||||||
.SS Logging options:
|
.SS Logging options:
|
||||||
.TP
|
.TP
|
||||||
\fB\-v\fR, \fB\-\-verbose\fR
|
\fB\-v\fR, \fB\-\-verbose\fR
|
||||||
|
|||||||
+2
-4
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "July 2022" "repo rebase" "Repo Manual"
|
.TH REPO "1" "June 2026" "repo rebase" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo rebase - manual page for repo rebase
|
repo \- repo rebase - manual page for repo rebase
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -33,9 +33,7 @@ pass \fB\-\-whitespace\fR to git rebase
|
|||||||
stash local modifications before starting
|
stash local modifications before starting
|
||||||
.TP
|
.TP
|
||||||
\fB\-m\fR, \fB\-\-onto\-manifest\fR
|
\fB\-m\fR, \fB\-\-onto\-manifest\fR
|
||||||
rebase onto the manifest version instead of upstream
|
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)
|
||||||
HEAD (this helps to make sure the local tree stays
|
|
||||||
consistent if you previously synced to a manifest)
|
|
||||||
.SS Logging options:
|
.SS Logging options:
|
||||||
.TP
|
.TP
|
||||||
\fB\-v\fR, \fB\-\-verbose\fR
|
\fB\-v\fR, \fB\-\-verbose\fR
|
||||||
|
|||||||
+22
-32
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "May 2026" "repo smartsync" "Repo Manual"
|
.TH REPO "1" "July 2026" "repo smartsync" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo smartsync - manual page for repo smartsync
|
repo \- repo smartsync - manual page for repo smartsync
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -15,16 +15,13 @@ Update working tree to the latest known good revision
|
|||||||
show this help message and exit
|
show this help message and exit
|
||||||
.TP
|
.TP
|
||||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||||
number of jobs to run in parallel (default: based on
|
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||||
number of CPU cores)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
|
\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
|
||||||
number of network jobs to run in parallel (defaults to
|
number of network jobs to run in parallel (defaults to \fB\-\-jobs\fR or 1). Ignored unless \fB\-\-no\-interleaved\fR is set
|
||||||
\fB\-\-jobs\fR or 1). Ignored unless \fB\-\-no\-interleaved\fR is set
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
|
\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
|
||||||
number of local checkout jobs to run in parallel
|
number of local checkout jobs to run in parallel (defaults to \fB\-\-jobs\fR or 8). Ignored unless \fB\-\-no\-interleaved\fR is set
|
||||||
(defaults to \fB\-\-jobs\fR or 8). Ignored unless \fB\-\-nointerleaved\fR is set
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-f\fR, \fB\-\-force\-broken\fR
|
\fB\-f\fR, \fB\-\-force\-broken\fR
|
||||||
obsolete option (to be deleted in the future)
|
obsolete option (to be deleted in the future)
|
||||||
@@ -33,30 +30,22 @@ obsolete option (to be deleted in the future)
|
|||||||
stop syncing after first error is hit
|
stop syncing after first error is hit
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-force\-sync\fR
|
\fB\-\-force\-sync\fR
|
||||||
overwrite an existing git directory if it needs to
|
overwrite an existing git directory if it needs to point to a different object directory. WARNING: this may cause loss of data
|
||||||
point to a different object directory. WARNING: this
|
|
||||||
may cause loss of data
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-force\-checkout\fR
|
\fB\-\-force\-checkout\fR
|
||||||
force checkout even if it results in throwing away
|
force checkout even if it results in throwing away uncommitted modifications. WARNING: this may cause loss of data
|
||||||
uncommitted modifications. WARNING: this may cause
|
|
||||||
loss of data
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-force\-remove\-dirty\fR
|
\fB\-\-force\-remove\-dirty\fR
|
||||||
force remove projects with uncommitted modifications
|
force remove projects with uncommitted modifications if projects no longer exist in the manifest. WARNING: this may cause loss of data
|
||||||
if projects no longer exist in the manifest. WARNING:
|
|
||||||
this may cause loss of data
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-rebase\fR
|
\fB\-\-rebase\fR
|
||||||
rebase local commits regardless of whether they are
|
rebase local commits regardless of whether they are published
|
||||||
published
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-l\fR, \fB\-\-local\-only\fR
|
\fB\-l\fR, \fB\-\-local\-only\fR
|
||||||
only update working tree, don't fetch
|
only update working tree, don't fetch
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-no\-manifest\-update\fR, \fB\-\-nmu\fR
|
\fB\-\-no\-manifest\-update\fR, \fB\-\-nmu\fR
|
||||||
use the existing manifest checkout as\-is. (do not
|
use the existing manifest checkout as\-is. (do not update to the latest revision)
|
||||||
update to the latest revision)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-interleaved\fR
|
\fB\-\-interleaved\fR
|
||||||
fetch and checkout projects in parallel (default)
|
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
|
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
|
||||||
temporary manifest to use for this sync
|
temporary manifest to use for this sync
|
||||||
.TP
|
.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
|
\fB\-\-clone\-bundle\fR
|
||||||
enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
|
enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
|
||||||
.TP
|
.TP
|
||||||
@@ -91,19 +83,20 @@ username to authenticate with the manifest server
|
|||||||
\fB\-p\fR MANIFEST_SERVER_PASSWORD, \fB\-\-manifest\-server\-password\fR=\fI\,MANIFEST_SERVER_PASSWORD\/\fR
|
\fB\-p\fR MANIFEST_SERVER_PASSWORD, \fB\-\-manifest\-server\-password\fR=\fI\,MANIFEST_SERVER_PASSWORD\/\fR
|
||||||
password to authenticate with the manifest server
|
password to authenticate with the manifest server
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-fetch\-submodules\fR
|
\fB\-\-recurse\-submodules\fR
|
||||||
fetch submodules from server
|
sync submodules from server
|
||||||
|
.TP
|
||||||
|
\fB\-\-no\-recurse\-submodules\fR
|
||||||
|
don't sync submodules from server
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-use\-superproject\fR
|
\fB\-\-use\-superproject\fR
|
||||||
use the manifest superproject to sync projects;
|
use the manifest superproject to sync projects; implies \fB\-c\fR
|
||||||
implies \fB\-c\fR
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-no\-use\-superproject\fR
|
\fB\-\-no\-use\-superproject\fR
|
||||||
disable use of manifest superprojects
|
disable use of manifest superprojects
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-superproject\-revision\fR=\fI\,SUPERPROJECT_REVISION\/\fR
|
\fB\-\-superproject\-revision\fR=\fI\,SUPERPROJECT_REVISION\/\fR
|
||||||
sync to superproject revision (applies to outer
|
sync to superproject revision (applies to outer manifest)
|
||||||
manifest)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-tags\fR
|
\fB\-\-tags\fR
|
||||||
fetch tags
|
fetch tags
|
||||||
@@ -112,15 +105,13 @@ fetch tags
|
|||||||
don't fetch tags (default)
|
don't fetch tags (default)
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-optimized\-fetch\fR
|
\fB\-\-optimized\-fetch\fR
|
||||||
only fetch projects fixed to sha1 if revision does not
|
only fetch projects fixed to sha1 if revision does not exist locally
|
||||||
exist locally
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-retry\-fetches\fR=\fI\,RETRY_FETCHES\/\fR
|
\fB\-\-retry\-fetches\fR=\fI\,RETRY_FETCHES\/\fR
|
||||||
number of times to retry fetches on transient errors
|
number of times to retry fetches on transient errors
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-prune\fR
|
\fB\-\-prune\fR
|
||||||
delete refs that no longer exist on the remote
|
delete refs that no longer exist on the remote (default)
|
||||||
(default)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-no\-prune\fR
|
\fB\-\-no\-prune\fR
|
||||||
do not delete refs that no longer exist on the remote
|
do not delete refs that no longer exist on the remote
|
||||||
@@ -129,8 +120,7 @@ do not delete refs that no longer exist on the remote
|
|||||||
run garbage collection on all synced projects
|
run garbage collection on all synced projects
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-no\-auto\-gc\fR
|
\fB\-\-no\-auto\-gc\fR
|
||||||
do not run garbage collection on any projects
|
do not run garbage collection on any projects (default)
|
||||||
(default)
|
|
||||||
.SS Logging options:
|
.SS Logging options:
|
||||||
.TP
|
.TP
|
||||||
\fB\-v\fR, \fB\-\-verbose\fR
|
\fB\-v\fR, \fB\-\-verbose\fR
|
||||||
|
|||||||
+2
-3
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "July 2022" "repo start" "Repo Manual"
|
.TH REPO "1" "June 2026" "repo start" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo start - manual page for repo start
|
repo \- repo start - manual page for repo start
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -15,8 +15,7 @@ Start a new branch for development
|
|||||||
show this help message and exit
|
show this help message and exit
|
||||||
.TP
|
.TP
|
||||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||||
number of jobs to run in parallel (default: based on
|
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||||
number of CPU cores)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-all\fR
|
\fB\-\-all\fR
|
||||||
begin branch in all projects
|
begin branch in all projects
|
||||||
|
|||||||
+12
-5
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "July 2022" "repo status" "Repo Manual"
|
.TH REPO "1" "June 2026" "repo status" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo status - manual page for repo status
|
repo \- repo status - manual page for repo status
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -15,12 +15,10 @@ Show the working tree status
|
|||||||
show this help message and exit
|
show this help message and exit
|
||||||
.TP
|
.TP
|
||||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||||
number of jobs to run in parallel (default: based on
|
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||||
number of CPU cores)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-o\fR, \fB\-\-orphans\fR
|
\fB\-o\fR, \fB\-\-orphans\fR
|
||||||
include objects in working directory outside of repo
|
include objects in working directory outside of repo projects
|
||||||
projects
|
|
||||||
.SS Logging options:
|
.SS Logging options:
|
||||||
.TP
|
.TP
|
||||||
\fB\-v\fR, \fB\-\-verbose\fR
|
\fB\-v\fR, \fB\-\-verbose\fR
|
||||||
@@ -70,6 +68,15 @@ branch devwork
|
|||||||
\fB\-m\fR
|
\fB\-m\fR
|
||||||
subcmds/status.py
|
subcmds/status.py
|
||||||
.PP
|
.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
|
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
|
commit (HEAD). Its values are always displayed in upper case and have the
|
||||||
following meanings:
|
following meanings:
|
||||||
|
|||||||
+29
-36
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "May 2026" "repo sync" "Repo Manual"
|
.TH REPO "1" "July 2026" "repo sync" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo sync - manual page for repo sync
|
repo \- repo sync - manual page for repo sync
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -15,16 +15,13 @@ Update working tree to the latest revision
|
|||||||
show this help message and exit
|
show this help message and exit
|
||||||
.TP
|
.TP
|
||||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||||
number of jobs to run in parallel (default: based on
|
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||||
number of CPU cores)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
|
\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
|
||||||
number of network jobs to run in parallel (defaults to
|
number of network jobs to run in parallel (defaults to \fB\-\-jobs\fR or 1). Ignored unless \fB\-\-no\-interleaved\fR is set
|
||||||
\fB\-\-jobs\fR or 1). Ignored unless \fB\-\-no\-interleaved\fR is set
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
|
\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
|
||||||
number of local checkout jobs to run in parallel
|
number of local checkout jobs to run in parallel (defaults to \fB\-\-jobs\fR or 8). Ignored unless \fB\-\-no\-interleaved\fR is set
|
||||||
(defaults to \fB\-\-jobs\fR or 8). Ignored unless \fB\-\-nointerleaved\fR is set
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-f\fR, \fB\-\-force\-broken\fR
|
\fB\-f\fR, \fB\-\-force\-broken\fR
|
||||||
obsolete option (to be deleted in the future)
|
obsolete option (to be deleted in the future)
|
||||||
@@ -33,30 +30,22 @@ obsolete option (to be deleted in the future)
|
|||||||
stop syncing after first error is hit
|
stop syncing after first error is hit
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-force\-sync\fR
|
\fB\-\-force\-sync\fR
|
||||||
overwrite an existing git directory if it needs to
|
overwrite an existing git directory if it needs to point to a different object directory. WARNING: this may cause loss of data
|
||||||
point to a different object directory. WARNING: this
|
|
||||||
may cause loss of data
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-force\-checkout\fR
|
\fB\-\-force\-checkout\fR
|
||||||
force checkout even if it results in throwing away
|
force checkout even if it results in throwing away uncommitted modifications. WARNING: this may cause loss of data
|
||||||
uncommitted modifications. WARNING: this may cause
|
|
||||||
loss of data
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-force\-remove\-dirty\fR
|
\fB\-\-force\-remove\-dirty\fR
|
||||||
force remove projects with uncommitted modifications
|
force remove projects with uncommitted modifications if projects no longer exist in the manifest. WARNING: this may cause loss of data
|
||||||
if projects no longer exist in the manifest. WARNING:
|
|
||||||
this may cause loss of data
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-rebase\fR
|
\fB\-\-rebase\fR
|
||||||
rebase local commits regardless of whether they are
|
rebase local commits regardless of whether they are published
|
||||||
published
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-l\fR, \fB\-\-local\-only\fR
|
\fB\-l\fR, \fB\-\-local\-only\fR
|
||||||
only update working tree, don't fetch
|
only update working tree, don't fetch
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-no\-manifest\-update\fR, \fB\-\-nmu\fR
|
\fB\-\-no\-manifest\-update\fR, \fB\-\-nmu\fR
|
||||||
use the existing manifest checkout as\-is. (do not
|
use the existing manifest checkout as\-is. (do not update to the latest revision)
|
||||||
update to the latest revision)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-interleaved\fR
|
\fB\-\-interleaved\fR
|
||||||
fetch and checkout projects in parallel (default)
|
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
|
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
|
||||||
temporary manifest to use for this sync
|
temporary manifest to use for this sync
|
||||||
.TP
|
.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
|
\fB\-\-clone\-bundle\fR
|
||||||
enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
|
enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
|
||||||
.TP
|
.TP
|
||||||
@@ -91,19 +83,20 @@ username to authenticate with the manifest server
|
|||||||
\fB\-p\fR MANIFEST_SERVER_PASSWORD, \fB\-\-manifest\-server\-password\fR=\fI\,MANIFEST_SERVER_PASSWORD\/\fR
|
\fB\-p\fR MANIFEST_SERVER_PASSWORD, \fB\-\-manifest\-server\-password\fR=\fI\,MANIFEST_SERVER_PASSWORD\/\fR
|
||||||
password to authenticate with the manifest server
|
password to authenticate with the manifest server
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-fetch\-submodules\fR
|
\fB\-\-recurse\-submodules\fR
|
||||||
fetch submodules from server
|
sync submodules from server
|
||||||
|
.TP
|
||||||
|
\fB\-\-no\-recurse\-submodules\fR
|
||||||
|
don't sync submodules from server
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-use\-superproject\fR
|
\fB\-\-use\-superproject\fR
|
||||||
use the manifest superproject to sync projects;
|
use the manifest superproject to sync projects; implies \fB\-c\fR
|
||||||
implies \fB\-c\fR
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-no\-use\-superproject\fR
|
\fB\-\-no\-use\-superproject\fR
|
||||||
disable use of manifest superprojects
|
disable use of manifest superprojects
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-superproject\-revision\fR=\fI\,SUPERPROJECT_REVISION\/\fR
|
\fB\-\-superproject\-revision\fR=\fI\,SUPERPROJECT_REVISION\/\fR
|
||||||
sync to superproject revision (applies to outer
|
sync to superproject revision (applies to outer manifest)
|
||||||
manifest)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-tags\fR
|
\fB\-\-tags\fR
|
||||||
fetch tags
|
fetch tags
|
||||||
@@ -112,15 +105,13 @@ fetch tags
|
|||||||
don't fetch tags (default)
|
don't fetch tags (default)
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-optimized\-fetch\fR
|
\fB\-\-optimized\-fetch\fR
|
||||||
only fetch projects fixed to sha1 if revision does not
|
only fetch projects fixed to sha1 if revision does not exist locally
|
||||||
exist locally
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-retry\-fetches\fR=\fI\,RETRY_FETCHES\/\fR
|
\fB\-\-retry\-fetches\fR=\fI\,RETRY_FETCHES\/\fR
|
||||||
number of times to retry fetches on transient errors
|
number of times to retry fetches on transient errors
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-prune\fR
|
\fB\-\-prune\fR
|
||||||
delete refs that no longer exist on the remote
|
delete refs that no longer exist on the remote (default)
|
||||||
(default)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-no\-prune\fR
|
\fB\-\-no\-prune\fR
|
||||||
do not delete refs that no longer exist on the remote
|
do not delete refs that no longer exist on the remote
|
||||||
@@ -129,12 +120,10 @@ do not delete refs that no longer exist on the remote
|
|||||||
run garbage collection on all synced projects
|
run garbage collection on all synced projects
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-no\-auto\-gc\fR
|
\fB\-\-no\-auto\-gc\fR
|
||||||
do not run garbage collection on any projects
|
do not run garbage collection on any projects (default)
|
||||||
(default)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-s\fR, \fB\-\-smart\-sync\fR
|
\fB\-s\fR, \fB\-\-smart\-sync\fR
|
||||||
smart sync using manifest from the latest known good
|
smart sync using manifest from the latest known good build
|
||||||
build
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-t\fR SMART_TAG, \fB\-\-smart\-tag\fR=\fI\,SMART_TAG\/\fR
|
\fB\-t\fR SMART_TAG, \fB\-\-smart\-tag\fR=\fI\,SMART_TAG\/\fR
|
||||||
smart sync using manifest from a known tag
|
smart sync using manifest from a known tag
|
||||||
@@ -229,8 +218,12 @@ bootstrap a new Git repository from a resumeable bundle file on a content
|
|||||||
delivery network. This may be necessary if there are problems with the local
|
delivery network. This may be necessary if there are problems with the local
|
||||||
Python HTTP client or proxy configuration, but the Git binary works.
|
Python HTTP client or proxy configuration, but the Git binary works.
|
||||||
.PP
|
.PP
|
||||||
The \fB\-\-fetch\-submodules\fR option enables fetching Git submodules of a project from
|
The \fB\-\-recurse\-submodules\fR option enables syncing Git submodules of all projects
|
||||||
server.
|
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
|
.PP
|
||||||
The \fB\-c\fR/\-\-current\-branch option can be used to only fetch objects that are on the
|
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.
|
branch specified by a project's revision.
|
||||||
|
|||||||
+5
-3
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "June 2024" "repo upload" "Repo Manual"
|
.TH REPO "1" "August 2026" "repo upload" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo upload - manual page for repo upload
|
repo \- repo upload - manual page for repo upload
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -15,8 +15,7 @@ Upload changes for code review
|
|||||||
show this help message and exit
|
show this help message and exit
|
||||||
.TP
|
.TP
|
||||||
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
|
||||||
number of jobs to run in parallel (default: based on
|
number of jobs to run in parallel (default: based on number of CPU cores)
|
||||||
number of CPU cores)
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-t\fR, \fB\-\-topic\-branch\fR
|
\fB\-t\fR, \fB\-\-topic\-branch\fR
|
||||||
set the topic to the local branch name
|
set the topic to the local branch name
|
||||||
@@ -113,6 +112,9 @@ Run the pre\-upload hook without prompting.
|
|||||||
.TP
|
.TP
|
||||||
\fB\-\-ignore\-hooks\fR
|
\fB\-\-ignore\-hooks\fR
|
||||||
Do not abort if pre\-upload hooks fail.
|
Do not abort if pre\-upload hooks fail.
|
||||||
|
.TP
|
||||||
|
\fB\-\-fix\fR
|
||||||
|
Automatically apply pre\-upload fixes without prompting.
|
||||||
.PP
|
.PP
|
||||||
Run `repo help upload` to view the detailed manual.
|
Run `repo help upload` to view the detailed manual.
|
||||||
.SH DETAILS
|
.SH DETAILS
|
||||||
|
|||||||
+2
-3
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "November 2025" "repo wipe" "Repo Manual"
|
.TH REPO "1" "June 2026" "repo wipe" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo wipe - manual page for repo wipe
|
repo \- repo wipe - manual page for repo wipe
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -21,8 +21,7 @@ force wipe shared projects and uncommitted changes
|
|||||||
force wipe even if there are uncommitted changes
|
force wipe even if there are uncommitted changes
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-force\-shared\fR
|
\fB\-\-force\-shared\fR
|
||||||
force wipe even if the project shares an object
|
force wipe even if the project shares an object directory
|
||||||
directory
|
|
||||||
.SS Logging options:
|
.SS Logging options:
|
||||||
.TP
|
.TP
|
||||||
\fB\-v\fR, \fB\-\-verbose\fR
|
\fB\-v\fR, \fB\-\-verbose\fR
|
||||||
|
|||||||
+3
-5
@@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "November 2025" "repo" "Repo Manual"
|
.TH REPO "1" "June 2026" "repo" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repository management tool built on top of git
|
repo \- repository management tool built on top of git
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -26,8 +26,7 @@ control color usage: auto, always, never
|
|||||||
trace git command execution (REPO_TRACE=1)
|
trace git command execution (REPO_TRACE=1)
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-trace\-to\-stderr\fR
|
\fB\-\-trace\-to\-stderr\fR
|
||||||
trace outputs go to stderr in addition to
|
trace outputs go to stderr in addition to .repo/TRACE_FILE
|
||||||
\&.repo/TRACE_FILE
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-trace\-python\fR
|
\fB\-\-trace\-python\fR
|
||||||
trace python command execution
|
trace python command execution
|
||||||
@@ -39,8 +38,7 @@ time repo command execution
|
|||||||
display this version of repo
|
display this version of repo
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-show\-toplevel\fR
|
\fB\-\-show\-toplevel\fR
|
||||||
display the path of the top\-level directory of the
|
display the path of the top\-level directory of the repo client checkout
|
||||||
repo client checkout
|
|
||||||
.TP
|
.TP
|
||||||
\fB\-\-event\-log\fR=\fI\,EVENT_LOG\/\fR
|
\fB\-\-event\-log\fR=\fI\,EVENT_LOG\/\fR
|
||||||
filename of event log to append timeline to
|
filename of event log to append timeline to
|
||||||
|
|||||||
+30
-10
@@ -651,6 +651,8 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
if self._manifest_server:
|
if self._manifest_server:
|
||||||
e = doc.createElement("manifest-server")
|
e = doc.createElement("manifest-server")
|
||||||
e.setAttribute("url", self._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(e)
|
||||||
root.appendChild(doc.createTextNode(""))
|
root.appendChild(doc.createTextNode(""))
|
||||||
|
|
||||||
@@ -690,9 +692,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
e.setAttribute("remote", remoteName)
|
e.setAttribute("remote", remoteName)
|
||||||
if peg_rev:
|
if peg_rev:
|
||||||
if self.IsMirror:
|
if self.IsMirror:
|
||||||
value = p.bare_git.rev_parse(p.revisionExpr + "^0")
|
value = p.bare_git.ResolveCommit(p.revisionExpr)
|
||||||
else:
|
else:
|
||||||
value = p.work_git.rev_parse(HEAD + "^0")
|
value = p.work_git.ResolveCommit(HEAD)
|
||||||
e.setAttribute("revision", value)
|
e.setAttribute("revision", value)
|
||||||
if peg_rev_upstream:
|
if peg_rev_upstream:
|
||||||
if p.upstream:
|
if p.upstream:
|
||||||
@@ -999,6 +1001,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
self._Load()
|
self._Load()
|
||||||
return self._manifest_server
|
return self._manifest_server
|
||||||
|
|
||||||
|
@property
|
||||||
|
def manifest_server_helper(self):
|
||||||
|
self._Load()
|
||||||
|
return self._manifest_server_helper
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def CloneBundle(self):
|
def CloneBundle(self):
|
||||||
clone_bundle = self.manifestProject.clone_bundle
|
clone_bundle = self.manifestProject.clone_bundle
|
||||||
@@ -1055,6 +1062,10 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
def UseGitWorktrees(self):
|
def UseGitWorktrees(self):
|
||||||
return self.manifestProject.use_worktree
|
return self.manifestProject.use_worktree
|
||||||
|
|
||||||
|
@property
|
||||||
|
def UseLocalGitDirs(self):
|
||||||
|
return self.manifestProject.use_local_gitdirs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def IsArchive(self):
|
def IsArchive(self):
|
||||||
return self.manifestProject.archive
|
return self.manifestProject.archive
|
||||||
@@ -1153,6 +1164,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
self._notice = None
|
self._notice = None
|
||||||
self.branch = None
|
self.branch = None
|
||||||
self._manifest_server = None
|
self._manifest_server = None
|
||||||
|
self._manifest_server_helper = None
|
||||||
|
|
||||||
def Load(self):
|
def Load(self):
|
||||||
"""Read the manifest into memory."""
|
"""Read the manifest into memory."""
|
||||||
@@ -1422,11 +1434,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
for node in itertools.chain(*node_list):
|
for node in itertools.chain(*node_list):
|
||||||
if node.nodeName == "manifest-server":
|
if node.nodeName == "manifest-server":
|
||||||
url = self._reqatt(node, "url")
|
url = self._reqatt(node, "url")
|
||||||
|
helper = node.getAttribute("helper") or None
|
||||||
if self._manifest_server is not None:
|
if self._manifest_server is not None:
|
||||||
raise ManifestParseError(
|
raise ManifestParseError(
|
||||||
"duplicate manifest-server in %s" % (self.manifestFile)
|
"duplicate manifest-server in %s" % (self.manifestFile)
|
||||||
)
|
)
|
||||||
self._manifest_server = url
|
self._manifest_server = url
|
||||||
|
self._manifest_server_helper = helper
|
||||||
|
|
||||||
def recursively_add_projects(project):
|
def recursively_add_projects(project):
|
||||||
projects = self._projects.setdefault(project.name, [])
|
projects = self._projects.setdefault(project.name, [])
|
||||||
@@ -2042,15 +2056,21 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
else:
|
else:
|
||||||
namepath = f"{name}.git"
|
namepath = f"{name}.git"
|
||||||
worktree = os.path.join(self.topdir, path).replace("\\", "/")
|
worktree = os.path.join(self.topdir, path).replace("\\", "/")
|
||||||
gitdir = os.path.join(self.subdir, "projects", "%s.git" % path)
|
if self.UseLocalGitDirs:
|
||||||
# We allow people to mix git worktrees & non-git worktrees for now.
|
gitdir = os.path.join(worktree, ".git")
|
||||||
# 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
|
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
|
return relpath, worktree, gitdir, objdir, use_git_worktrees
|
||||||
|
|
||||||
def GetProjectsWithName(self, name, all_manifests=False):
|
def GetProjectsWithName(self, name, all_manifests=False):
|
||||||
|
|||||||
+772
-157
File diff suppressed because it is too large
Load Diff
@@ -76,6 +76,7 @@ def main(argv: List[str]) -> int:
|
|||||||
# behavior (like probing active number of CPUs). We use a weird name &
|
# 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.
|
# value to make it less likely for users to set this var themselves.
|
||||||
os.environ["_REPO_GENERATE_MANPAGES_"] = " indeed! "
|
os.environ["_REPO_GENERATE_MANPAGES_"] = " indeed! "
|
||||||
|
os.environ["COLUMNS"] = "10000"
|
||||||
|
|
||||||
# "repo branch" is an alias for "repo branches".
|
# "repo branch" is an alias for "repo branches".
|
||||||
del subcmds.all_commands["branch"]
|
del subcmds.all_commands["branch"]
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ if not REPO_REV:
|
|||||||
BUG_URL = "https://issues.gerritcodereview.com/issues/new?component=1370071"
|
BUG_URL = "https://issues.gerritcodereview.com/issues/new?component=1370071"
|
||||||
|
|
||||||
# increment this whenever we make important changes to this script
|
# increment this whenever we make important changes to this script
|
||||||
VERSION = (2, 54)
|
VERSION = (2, 65)
|
||||||
|
|
||||||
# increment this if the MAINTAINER_KEYS block is modified
|
# increment this if the MAINTAINER_KEYS block is modified
|
||||||
KEYRING_VERSION = (2, 3)
|
KEYRING_VERSION = (2, 3)
|
||||||
@@ -379,6 +379,11 @@ def InitParser(parser):
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="use git-worktree to manage projects",
|
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.
|
# These are fundamentally different ways of structuring the checkout.
|
||||||
group = parser.add_option_group("Project checkout optimizations")
|
group = parser.add_option_group("Project checkout optimizations")
|
||||||
@@ -400,20 +405,20 @@ def InitParser(parser):
|
|||||||
"--partial-clone",
|
"--partial-clone",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="perform partial clone (https://git-scm.com/"
|
help="perform partial clone (https://git-scm.com/"
|
||||||
"docs/gitrepository-layout#_code_partialclone_code)",
|
"docs/partial-clone)",
|
||||||
)
|
)
|
||||||
group.add_option(
|
group.add_option(
|
||||||
"--no-partial-clone",
|
"--no-partial-clone",
|
||||||
action="store_false",
|
action="store_false",
|
||||||
help="disable use of partial clone (https://git-scm.com/"
|
help="disable use of partial clone (https://git-scm.com/"
|
||||||
"docs/gitrepository-layout#_code_partialclone_code)",
|
"docs/partial-clone)",
|
||||||
)
|
)
|
||||||
group.add_option(
|
group.add_option(
|
||||||
"--partial-clone-exclude",
|
"--partial-clone-exclude",
|
||||||
action="store",
|
action="store",
|
||||||
help="exclude the specified projects (a comma-delimited "
|
help="exclude the specified projects (a comma-delimited "
|
||||||
"project names) from partial clone (https://git-scm.com"
|
"project names) from partial clone (https://git-scm.com"
|
||||||
"/docs/gitrepository-layout#_code_partialclone_code)",
|
"/docs/partial-clone)",
|
||||||
)
|
)
|
||||||
group.add_option(
|
group.add_option(
|
||||||
"--clone-filter",
|
"--clone-filter",
|
||||||
@@ -742,6 +747,28 @@ def SetGitTrace2ParentSid(env=None):
|
|||||||
_setenv(KEY, value, env=env)
|
_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):
|
def _setenv(key, value, env=None):
|
||||||
"""Set |key| in the OS environment |env| to |value|."""
|
"""Set |key| in the OS environment |env| to |value|."""
|
||||||
if env is None:
|
if env is None:
|
||||||
@@ -1074,7 +1101,7 @@ def verify_rev(cwd, remote_ref, rev, quiet):
|
|||||||
print(file=sys.stderr)
|
print(file=sys.stderr)
|
||||||
|
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
_setenv("GNUPGHOME", gpg_dir, env)
|
_setenv("GNUPGHOME", _NormalizeGpgHomePath(gpg_dir), env)
|
||||||
run_git("tag", "-v", cur, cwd=cwd, env=env)
|
run_git("tag", "-v", cur, cwd=cwd, env=env)
|
||||||
return "%s^0" % cur
|
return "%s^0" % cur
|
||||||
|
|
||||||
|
|||||||
+2
-5
@@ -20,7 +20,7 @@ from command import Command
|
|||||||
from command import DEFAULT_LOCAL_JOBS
|
from command import DEFAULT_LOCAL_JOBS
|
||||||
from error import RepoError
|
from error import RepoError
|
||||||
from error import RepoExitError
|
from error import RepoExitError
|
||||||
from git_command import git
|
from git_command import IsValidBranchName
|
||||||
from progress import Progress
|
from progress import Progress
|
||||||
from repo_logging import RepoLogger
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
@@ -58,9 +58,7 @@ It is equivalent to "git branch -D <branchname>".
|
|||||||
|
|
||||||
if not opt.all:
|
if not opt.all:
|
||||||
branches = args[0].split()
|
branches = args[0].split()
|
||||||
invalid_branches = [
|
invalid_branches = [x for x in branches if not IsValidBranchName(x)]
|
||||||
x for x in branches if not git.check_ref_format(f"heads/{x}")
|
|
||||||
]
|
|
||||||
|
|
||||||
if invalid_branches:
|
if invalid_branches:
|
||||||
self.OptionParser.error(
|
self.OptionParser.error(
|
||||||
@@ -94,7 +92,6 @@ It is equivalent to "git branch -D <branchname>".
|
|||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
nb = args[0].split()
|
nb = args[0].split()
|
||||||
self.TryOverrideManifestWithSmartSync()
|
|
||||||
err = collections.defaultdict(list)
|
err = collections.defaultdict(list)
|
||||||
success = collections.defaultdict(list)
|
success = collections.defaultdict(list)
|
||||||
aggregate_errors = []
|
aggregate_errors = []
|
||||||
|
|||||||
+45
-30
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
from command import Command
|
from command import Command
|
||||||
from error import GitError
|
from error import GitError
|
||||||
@@ -43,36 +44,8 @@ change id will be added.
|
|||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
reference = args[0]
|
reference = args[0]
|
||||||
|
sha1, commit = self._ResolveReference(reference)
|
||||||
p = GitCommand(
|
old_msg = self._StripHeader(commit)
|
||||||
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 = GitCommand(
|
||||||
None,
|
None,
|
||||||
@@ -117,6 +90,48 @@ change id will be added.
|
|||||||
logger.error("error: Failed to update commit message")
|
logger.error("error: Failed to update commit message")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def _ResolveReference(self, reference: str) -> Tuple[str, str]:
|
||||||
|
"""Resolve a commit and read it through one cat-file batch request."""
|
||||||
|
expression = f"{reference}^{{commit}}"
|
||||||
|
p = GitCommand(
|
||||||
|
None,
|
||||||
|
["cat-file", "--batch"],
|
||||||
|
input=expression + "\n",
|
||||||
|
capture_stdout=True,
|
||||||
|
capture_stderr=True,
|
||||||
|
verify_command=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
p.Wait()
|
||||||
|
header, separator, output = p.stdout.partition("\n")
|
||||||
|
if not separator:
|
||||||
|
raise ValueError("missing cat-file header")
|
||||||
|
if header.endswith(" missing") or header.endswith(" ambiguous"):
|
||||||
|
raise GitError(f"commit {reference} not found")
|
||||||
|
|
||||||
|
parts = header.split(" ", 2)
|
||||||
|
if len(parts) != 3 or parts[1] != "commit":
|
||||||
|
raise ValueError(
|
||||||
|
f"unexpected object type {parts[1]!r}"
|
||||||
|
if len(parts) >= 2
|
||||||
|
else "invalid header"
|
||||||
|
)
|
||||||
|
sha1, _object_type, _size = parts
|
||||||
|
|
||||||
|
if not output.endswith("\n"):
|
||||||
|
raise ValueError("truncated cat-file object")
|
||||||
|
|
||||||
|
commit = output[:-1]
|
||||||
|
except (GitError, ValueError) as e:
|
||||||
|
logger.error(
|
||||||
|
"error: Failed to resolve or read commit %s", reference
|
||||||
|
)
|
||||||
|
if isinstance(e, GitError):
|
||||||
|
raise
|
||||||
|
raise GitError(str(e)) from e
|
||||||
|
|
||||||
|
return sha1, commit
|
||||||
|
|
||||||
def _IsChangeId(self, line):
|
def _IsChangeId(self, line):
|
||||||
return CHANGE_ID_RE.match(line)
|
return CHANGE_ID_RE.match(line)
|
||||||
|
|
||||||
|
|||||||
+4
-22
@@ -25,7 +25,6 @@ from color import Coloring
|
|||||||
from command import Command
|
from command import Command
|
||||||
from command import DEFAULT_LOCAL_JOBS
|
from command import DEFAULT_LOCAL_JOBS
|
||||||
from command import MirrorSafeCommand
|
from command import MirrorSafeCommand
|
||||||
from error import ManifestInvalidRevisionError
|
|
||||||
from repo_logging import RepoLogger
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
|
|
||||||
@@ -108,6 +107,8 @@ manifest.
|
|||||||
REPO_DEST_BRANCH is the name of the destination branch for code review,
|
REPO_DEST_BRANCH is the name of the destination branch for code review,
|
||||||
as specified in the manifest.
|
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_COUNT is the total number of projects being iterated.
|
||||||
|
|
||||||
REPO_I is the current (1-based) iteration count. Can be used in
|
REPO_I is the current (1-based) iteration count. Can be used in
|
||||||
@@ -242,8 +243,6 @@ without iterating through the remaining projects.
|
|||||||
|
|
||||||
mirror = self.manifest.IsMirror
|
mirror = self.manifest.IsMirror
|
||||||
|
|
||||||
self.TryOverrideManifestWithSmartSync()
|
|
||||||
|
|
||||||
if opt.regex:
|
if opt.regex:
|
||||||
projects = self.FindProjects(args, all_manifests=all_trees)
|
projects = self.FindProjects(args, all_manifests=all_trees)
|
||||||
elif opt.inverse_regex:
|
elif opt.inverse_regex:
|
||||||
@@ -339,25 +338,8 @@ def DoWork(project, mirror, opt, cmd, shell, cnt, config):
|
|||||||
val = ""
|
val = ""
|
||||||
env[name] = val
|
env[name] = val
|
||||||
|
|
||||||
setenv("REPO_PROJECT", project.name)
|
env.update(project.GetEnvVars(local=opt.this_manifest_only))
|
||||||
setenv("REPO_OUTERPATH", project.manifest.path_prefix)
|
env["REPO_I"] = str(cnt + 1)
|
||||||
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)
|
|
||||||
|
|
||||||
if mirror:
|
if mirror:
|
||||||
setenv("GIT_DIR", project.gitdir)
|
setenv("GIT_DIR", project.gitdir)
|
||||||
|
|||||||
+27
-6
@@ -147,8 +147,6 @@ class Info(PagedCommand):
|
|||||||
if not opt.this_manifest_only:
|
if not opt.this_manifest_only:
|
||||||
self.manifest = self.manifest.outer_client
|
self.manifest = self.manifest.outer_client
|
||||||
|
|
||||||
self.TryOverrideManifestWithSmartSync()
|
|
||||||
|
|
||||||
output_format = OutputFormat[opt.format.upper()]
|
output_format = OutputFormat[opt.format.upper()]
|
||||||
if output_format == OutputFormat.JSON:
|
if output_format == OutputFormat.JSON:
|
||||||
self._ExecuteJson(opt, args)
|
self._ExecuteJson(opt, args)
|
||||||
@@ -191,12 +189,14 @@ class Info(PagedCommand):
|
|||||||
"superproject_revision": srev,
|
"superproject_revision": srev,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _getProjectData(self, project) -> Dict[str, Any]:
|
@classmethod
|
||||||
|
def _getProjectData(cls, project) -> Dict[str, Any]:
|
||||||
"""Gather project data as a dict."""
|
"""Gather project data as a dict."""
|
||||||
data = {
|
data = {
|
||||||
"name": project.name,
|
"name": project.name,
|
||||||
"mount_path": project.worktree,
|
"mount_path": project.worktree,
|
||||||
"current_revision": project.GetRevisionId(),
|
"current_revision": project.GetHeadRevisionId()
|
||||||
|
or project.GetRevisionId(),
|
||||||
"manifest_revision": project.revisionExpr,
|
"manifest_revision": project.revisionExpr,
|
||||||
"local_branches": list(project.GetBranches()),
|
"local_branches": list(project.GetBranches()),
|
||||||
}
|
}
|
||||||
@@ -205,6 +205,12 @@ class Info(PagedCommand):
|
|||||||
data["current_branch"] = currentBranch
|
data["current_branch"] = currentBranch
|
||||||
return data
|
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:
|
def _ExecuteJson(self, opt, args) -> None:
|
||||||
"""Output info as JSON."""
|
"""Output info as JSON."""
|
||||||
result = {}
|
result = {}
|
||||||
@@ -214,7 +220,22 @@ class Info(PagedCommand):
|
|||||||
projs = self.GetProjects(
|
projs = self.GetProjects(
|
||||||
args, all_manifests=not opt.this_manifest_only
|
args, all_manifests=not opt.this_manifest_only
|
||||||
)
|
)
|
||||||
result["projects"] = [self._getProjectData(p) for p in projs]
|
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_settings = {
|
||||||
# JSON style guide says Unicode characters are fully allowed.
|
# JSON style guide says Unicode characters are fully allowed.
|
||||||
@@ -273,7 +294,7 @@ class Info(PagedCommand):
|
|||||||
out.nl()
|
out.nl()
|
||||||
|
|
||||||
heading("Current revision: ")
|
heading("Current revision: ")
|
||||||
headtext(project.GetRevisionId())
|
headtext(project.GetHeadRevisionId() or project.GetRevisionId())
|
||||||
out.nl()
|
out.nl()
|
||||||
|
|
||||||
currentBranch = project.CurrentBranch
|
currentBranch = project.CurrentBranch
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ _REPO_ALLOW_SHALLOW = os.environ.get("REPO_ALLOW_SHALLOW")
|
|||||||
|
|
||||||
class Init(InteractiveCommand, MirrorSafeCommand):
|
class Init(InteractiveCommand, MirrorSafeCommand):
|
||||||
COMMON = True
|
COMMON = True
|
||||||
|
RESPECT_SMART_SYNC_OVERRIDE = False
|
||||||
MULTI_MANIFEST_SUPPORT = True
|
MULTI_MANIFEST_SUPPORT = True
|
||||||
helpSummary = "Initialize a repo client checkout in the current directory"
|
helpSummary = "Initialize a repo client checkout in the current directory"
|
||||||
helpUsage = """
|
helpUsage = """
|
||||||
@@ -169,6 +170,7 @@ to update the working directory files.
|
|||||||
depth=opt.depth,
|
depth=opt.depth,
|
||||||
git_event_log=self.git_event_log,
|
git_event_log=self.git_event_log,
|
||||||
manifest_name=opt.manifest_name,
|
manifest_name=opt.manifest_name,
|
||||||
|
use_local_gitdirs=opt.use_local_gitdirs,
|
||||||
):
|
):
|
||||||
manifest_name = opt.manifest_name
|
manifest_name = opt.manifest_name
|
||||||
raise UpdateManifestError(
|
raise UpdateManifestError(
|
||||||
|
|||||||
+9
-4
@@ -59,10 +59,15 @@ are displayed.
|
|||||||
for project in self.GetProjects(
|
for project in self.GetProjects(
|
||||||
args, all_manifests=not opt.this_manifest_only
|
args, all_manifests=not opt.this_manifest_only
|
||||||
):
|
):
|
||||||
br = [project.GetUploadableBranch(x) for x in project.GetBranches()]
|
local_branches = project.GetBranches()
|
||||||
br = [x for x in br if x]
|
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:
|
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)
|
all_branches.extend(br)
|
||||||
|
|
||||||
if not all_branches:
|
if not all_branches:
|
||||||
@@ -97,7 +102,7 @@ are displayed.
|
|||||||
print(
|
print(
|
||||||
"%s %-33s (%2d commit%s, %s)"
|
"%s %-33s (%2d commit%s, %s)"
|
||||||
% (
|
% (
|
||||||
branch.name == project.CurrentBranch and "*" or " ",
|
branch.current and "*" or " ",
|
||||||
branch.name,
|
branch.name,
|
||||||
len(commits),
|
len(commits),
|
||||||
len(commits) != 1 and "s" or " ",
|
len(commits) != 1 and "s" or " ",
|
||||||
|
|||||||
+1
-1
@@ -80,7 +80,7 @@ class Prune(PagedCommand):
|
|||||||
print(
|
print(
|
||||||
"%s %-33s "
|
"%s %-33s "
|
||||||
% (
|
% (
|
||||||
branch.name == project.CurrentBranch and "*" or " ",
|
branch.current and "*" or " ",
|
||||||
branch.name,
|
branch.name,
|
||||||
),
|
),
|
||||||
end="",
|
end="",
|
||||||
|
|||||||
+16
-20
@@ -16,7 +16,9 @@ import sys
|
|||||||
|
|
||||||
from color import Coloring
|
from color import Coloring
|
||||||
from command import Command
|
from command import Command
|
||||||
|
from error import GitError
|
||||||
from git_command import GitCommand
|
from git_command import GitCommand
|
||||||
|
from project import Project
|
||||||
from repo_logging import RepoLogger
|
from repo_logging import RepoLogger
|
||||||
|
|
||||||
|
|
||||||
@@ -30,6 +32,17 @@ class RebaseColoring(Coloring):
|
|||||||
self.fail = self.printer("fail", fg="red")
|
self.fail = self.printer("fail", fg="red")
|
||||||
|
|
||||||
|
|
||||||
|
def _ResolveOntoManifest(project: Project) -> str:
|
||||||
|
"""Resolve project's revisionExpr to a local tracking branch.
|
||||||
|
|
||||||
|
Falls back to the raw revisionExpr if ToLocal fails or raises GitError.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return project.GetRemote().ToLocal(project.revisionExpr)
|
||||||
|
except GitError:
|
||||||
|
return project.revisionExpr
|
||||||
|
|
||||||
|
|
||||||
class Rebase(Command):
|
class Rebase(Command):
|
||||||
COMMON = True
|
COMMON = True
|
||||||
helpSummary = "Rebase local branches on upstream branch"
|
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")
|
common_args.append("--autosquash")
|
||||||
if opt.interactive:
|
if opt.interactive:
|
||||||
common_args.append("-i")
|
common_args.append("-i")
|
||||||
|
if opt.auto_stash:
|
||||||
|
common_args.append("--autostash")
|
||||||
|
|
||||||
config = self.manifest.manifestProject.config
|
config = self.manifest.manifestProject.config
|
||||||
out = RebaseColoring(config)
|
out = RebaseColoring(config)
|
||||||
@@ -162,7 +177,7 @@ branch but need to incorporate new upstream changes "underneath" them.
|
|||||||
args = common_args[:]
|
args = common_args[:]
|
||||||
if opt.onto_manifest:
|
if opt.onto_manifest:
|
||||||
args.append("--onto")
|
args.append("--onto")
|
||||||
args.append(project.revisionExpr)
|
args.append(_ResolveOntoManifest(project))
|
||||||
|
|
||||||
args.append(upbranch.LocalMerge)
|
args.append(upbranch.LocalMerge)
|
||||||
|
|
||||||
@@ -175,29 +190,10 @@ branch but need to incorporate new upstream changes "underneath" them.
|
|||||||
out.nl()
|
out.nl()
|
||||||
out.flush()
|
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 GitCommand(project, args).Wait() != 0:
|
||||||
ret += 1
|
ret += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if needs_stash:
|
|
||||||
stash_args.append("pop")
|
|
||||||
stash_args.append("--quiet")
|
|
||||||
if GitCommand(project, stash_args).Wait() != 0:
|
|
||||||
ret += 1
|
|
||||||
|
|
||||||
if ret:
|
if ret:
|
||||||
msg_fmt = "%d projects had errors"
|
msg_fmt = "%d projects had errors"
|
||||||
self.git_event_log.ErrorEvent(msg_fmt % (ret), msg_fmt)
|
self.git_event_log.ErrorEvent(msg_fmt % (ret), msg_fmt)
|
||||||
|
|||||||
+2
-3
@@ -18,7 +18,7 @@ from typing import NamedTuple
|
|||||||
from command import Command
|
from command import Command
|
||||||
from command import DEFAULT_LOCAL_JOBS
|
from command import DEFAULT_LOCAL_JOBS
|
||||||
from error import RepoExitError
|
from error import RepoExitError
|
||||||
from git_command import git
|
from git_command import IsValidBranchName
|
||||||
from git_config import IsImmutable
|
from git_config import IsImmutable
|
||||||
from progress import Progress
|
from progress import Progress
|
||||||
from repo_logging import RepoLogger
|
from repo_logging import RepoLogger
|
||||||
@@ -75,7 +75,7 @@ revision specified in the manifest.
|
|||||||
self.Usage()
|
self.Usage()
|
||||||
|
|
||||||
nb = args[0]
|
nb = args[0]
|
||||||
if not git.check_ref_format("heads/%s" % nb):
|
if not IsValidBranchName(nb):
|
||||||
self.OptionParser.error("'%s' is not a valid name" % nb)
|
self.OptionParser.error("'%s' is not a valid name" % nb)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -104,7 +104,6 @@ revision specified in the manifest.
|
|||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
nb = args[0]
|
nb = args[0]
|
||||||
self.TryOverrideManifestWithSmartSync()
|
|
||||||
err_projects = []
|
err_projects = []
|
||||||
err = []
|
err = []
|
||||||
projects = []
|
projects = []
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ project 'repo' on branch 'devwork':
|
|||||||
project repo/ branch devwork
|
project repo/ branch devwork
|
||||||
-m subcmds/status.py
|
-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 first column explains how the staging area (index) differs from
|
||||||
the last commit (HEAD). Its values are always displayed in upper
|
the last commit (HEAD). Its values are always displayed in upper
|
||||||
case and have the following meanings:
|
case and have the following meanings:
|
||||||
|
|||||||
+365
-61
@@ -23,10 +23,12 @@ import netrc
|
|||||||
import optparse
|
import optparse
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
from typing import List, NamedTuple, Optional, Set, Tuple, Union
|
from typing import Dict, List, NamedTuple, Optional, Set, Tuple, Union
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@@ -96,19 +98,27 @@ logger = RepoLogger(__file__)
|
|||||||
|
|
||||||
|
|
||||||
def _SafeCheckoutOrder(checkouts: List[Project]) -> List[List[Project]]:
|
def _SafeCheckoutOrder(checkouts: List[Project]) -> List[List[Project]]:
|
||||||
"""Generate a sequence of checkouts that is safe to perform. The client
|
"""Generate a sequence of checkouts that is safe to perform.
|
||||||
should checkout everything from n-th index before moving to n+1.
|
|
||||||
|
The client should checkout everything from n-th index before moving to
|
||||||
|
n+1.
|
||||||
|
|
||||||
This is only useful if manifest contains nested projects.
|
This is only useful if manifest contains nested projects.
|
||||||
|
|
||||||
E.g. if foo, foo/bar and foo/bar/baz are project paths, then foo needs to
|
E.g. if foo, foo/bar and foo/bar/baz are project paths, then foo needs to
|
||||||
finish before foo/bar can proceed, and foo/bar needs to finish before
|
finish before foo/bar can proceed, and foo/bar needs to finish before
|
||||||
foo/bar/baz."""
|
foo/bar/baz.
|
||||||
res = [[]]
|
|
||||||
current = res[0]
|
|
||||||
|
|
||||||
# depth_stack contains a current stack of parent paths.
|
Discovered submodules have an additional constraint: sibling submodules in
|
||||||
|
the same parent repository must not be checked out in parallel because they
|
||||||
|
all run `git submodule init` against the same parent .git/config.
|
||||||
|
"""
|
||||||
|
res = [[]]
|
||||||
|
|
||||||
|
# depth_stack contains the current stack of parent paths together with the
|
||||||
|
# effective checkout level assigned to each path.
|
||||||
depth_stack = []
|
depth_stack = []
|
||||||
|
submodule_parent_level = {}
|
||||||
# Checkouts are iterated in the hierarchical order. That way, it can easily
|
# Checkouts are iterated in the hierarchical order. That way, it can easily
|
||||||
# be determined if the previous checkout is parent of the current checkout.
|
# be determined if the previous checkout is parent of the current checkout.
|
||||||
# We are splitting by the path separator so the final result is
|
# We are splitting by the path separator so the final result is
|
||||||
@@ -119,25 +129,110 @@ def _SafeCheckoutOrder(checkouts: List[Project]) -> List[List[Project]]:
|
|||||||
checkout_path = Path(checkout.relpath)
|
checkout_path = Path(checkout.relpath)
|
||||||
while depth_stack:
|
while depth_stack:
|
||||||
try:
|
try:
|
||||||
checkout_path.relative_to(depth_stack[-1])
|
checkout_path.relative_to(depth_stack[-1][0])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Path.relative_to returns ValueError if paths are not relative.
|
# Path.relative_to returns ValueError if paths are not relative.
|
||||||
# TODO(sokcevic): Switch to is_relative_to once min supported
|
# TODO(sokcevic): Switch to is_relative_to once min supported
|
||||||
# version is py3.9.
|
# version is py3.9.
|
||||||
depth_stack.pop()
|
depth_stack.pop()
|
||||||
else:
|
else:
|
||||||
if len(depth_stack) >= len(res):
|
|
||||||
# Another depth created.
|
|
||||||
res.append([])
|
|
||||||
break
|
break
|
||||||
|
|
||||||
current = res[len(depth_stack)]
|
level = depth_stack[-1][1] + 1 if depth_stack else 0
|
||||||
|
parent = checkout.parent
|
||||||
|
if parent is not None:
|
||||||
|
level = max(
|
||||||
|
level,
|
||||||
|
submodule_parent_level.get(parent.worktree, level - 1) + 1,
|
||||||
|
)
|
||||||
|
submodule_parent_level[parent.worktree] = level
|
||||||
|
if level >= len(res):
|
||||||
|
res.extend([] for _ in range(level + 1 - len(res)))
|
||||||
|
|
||||||
|
current = res[level]
|
||||||
current.append(checkout)
|
current.append(checkout)
|
||||||
depth_stack.append(checkout_path)
|
depth_stack.append((checkout_path, level))
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def _ParentFirstBatches(projects: List[Project]) -> List[List[Project]]:
|
||||||
|
"""Group |projects| so that a parent is fetched before its submodules.
|
||||||
|
|
||||||
|
A discovered submodule can only be fetched at the right revision once the
|
||||||
|
project holding its gitlink has been fetched, so it is held back to a later
|
||||||
|
batch than its parent. Projects that are not discovered submodules all end
|
||||||
|
up in the first batch, which keeps manifests without submodules on a single
|
||||||
|
batch.
|
||||||
|
"""
|
||||||
|
batches = collections.defaultdict(list)
|
||||||
|
for project in projects:
|
||||||
|
depth = 0
|
||||||
|
ancestor = project
|
||||||
|
while ancestor.Derived and ancestor.parent:
|
||||||
|
depth += 1
|
||||||
|
ancestor = ancestor.parent
|
||||||
|
batches[depth].append(project)
|
||||||
|
return [batches[depth] for depth in sorted(batches)]
|
||||||
|
|
||||||
|
|
||||||
|
def _RefreshDerivedRevisions(
|
||||||
|
projects: List[Project],
|
||||||
|
submodule_revisions: Optional[Dict[Project, Dict[str, str]]] = None,
|
||||||
|
) -> List[Project]:
|
||||||
|
"""Re-resolve the gitlinks of the discovered submodules in |projects|.
|
||||||
|
|
||||||
|
The revision of a discovered submodule is read from its parent when the
|
||||||
|
manifest is loaded, so it is stale as soon as the parent gets fetched. It
|
||||||
|
has to be resolved again once the parent is up-to-date, and before the
|
||||||
|
submodule itself is fetched and checked out.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
projects: The projects whose discovered submodules to resolve.
|
||||||
|
submodule_revisions: Gitlinks already read, keyed by the project
|
||||||
|
holding them. Passing the same dict for project sets that follow
|
||||||
|
the same fetches, e.g. the levels of one checkout order, keeps a
|
||||||
|
project from being read more than once.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The submodules that their parent no longer holds a gitlink for.
|
||||||
|
"""
|
||||||
|
if submodule_revisions is None:
|
||||||
|
submodule_revisions = {}
|
||||||
|
|
||||||
|
subprojects_by_parent = collections.defaultdict(list)
|
||||||
|
for project in projects:
|
||||||
|
if project.Derived and project.parent:
|
||||||
|
subprojects_by_parent[project.parent].append(project)
|
||||||
|
|
||||||
|
removed = []
|
||||||
|
for parent, subprojects in subprojects_by_parent.items():
|
||||||
|
revisions = submodule_revisions.get(parent)
|
||||||
|
if revisions is None:
|
||||||
|
revisions = parent.GetSubmoduleRevisions()
|
||||||
|
if revisions is None:
|
||||||
|
# Leave the submodules of a parent we cannot read alone.
|
||||||
|
continue
|
||||||
|
submodule_revisions[parent] = revisions
|
||||||
|
for subproject in subprojects:
|
||||||
|
rev = revisions.get(subproject.gitlink_path)
|
||||||
|
if rev:
|
||||||
|
subproject.SetRevision(rev, revisionId=rev)
|
||||||
|
else:
|
||||||
|
removed.append(subproject)
|
||||||
|
return removed
|
||||||
|
|
||||||
|
|
||||||
|
def _WithoutProjects(
|
||||||
|
projects: List[Project], unwanted: List[Project]
|
||||||
|
) -> List[Project]:
|
||||||
|
"""Return |projects| without the projects in |unwanted|."""
|
||||||
|
if not unwanted:
|
||||||
|
return projects
|
||||||
|
dropped = set(unwanted)
|
||||||
|
return [p for p in projects if p not in dropped]
|
||||||
|
|
||||||
|
|
||||||
def _chunksize(projects: int, jobs: int) -> int:
|
def _chunksize(projects: int, jobs: int) -> int:
|
||||||
"""Calculate chunk size for the given number of projects and jobs."""
|
"""Calculate chunk size for the given number of projects and jobs."""
|
||||||
return min(max(1, projects // jobs), WORKER_BATCH_SIZE)
|
return min(max(1, projects // jobs), WORKER_BATCH_SIZE)
|
||||||
@@ -206,7 +301,8 @@ class _SyncResult(NamedTuple):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
project_index (int): The index of the project in the shared list.
|
project_index (int): The index of the project in the shared list.
|
||||||
relpath (str): The project's relative path from the repo client top.
|
relpath (str): The project's path relative to the tree being synced.
|
||||||
|
Unlike Project.relpath, it is unique across submanifests.
|
||||||
remote_fetched (bool): True if the remote was actually queried.
|
remote_fetched (bool): True if the remote was actually queried.
|
||||||
fetch_success (bool): True if the fetch operation was successful.
|
fetch_success (bool): True if the fetch operation was successful.
|
||||||
fetch_errors (List[Exception]): The Exceptions from a failed fetch.
|
fetch_errors (List[Exception]): The Exceptions from a failed fetch.
|
||||||
@@ -294,6 +390,7 @@ class TeeStringIO(io.StringIO):
|
|||||||
|
|
||||||
class Sync(Command, MirrorSafeCommand):
|
class Sync(Command, MirrorSafeCommand):
|
||||||
COMMON = True
|
COMMON = True
|
||||||
|
RESPECT_SMART_SYNC_OVERRIDE = False
|
||||||
MULTI_MANIFEST_SUPPORT = True
|
MULTI_MANIFEST_SUPPORT = True
|
||||||
helpSummary = "Update working tree to the latest revision"
|
helpSummary = "Update working tree to the latest revision"
|
||||||
helpUsage = """
|
helpUsage = """
|
||||||
@@ -360,8 +457,12 @@ resumeable bundle file on a content delivery network. This
|
|||||||
may be necessary if there are problems with the local Python
|
may be necessary if there are problems with the local Python
|
||||||
HTTP client or proxy configuration, but the Git binary works.
|
HTTP client or proxy configuration, but the Git binary works.
|
||||||
|
|
||||||
The --fetch-submodules option enables fetching Git submodules
|
The --recurse-submodules option enables syncing Git submodules of all projects
|
||||||
of a project from server.
|
from the server. The --no-recurse-submodules option disables syncing Git
|
||||||
|
submodules, even when a project has sync-s="true" in the manifest.
|
||||||
|
|
||||||
|
The --fetch-submodules and --no-fetch-submodules options are deprecated aliases
|
||||||
|
for --recurse-submodules and --no-recurse-submodules, respectively.
|
||||||
|
|
||||||
The -c/--current-branch option can be used to only fetch objects that
|
The -c/--current-branch option can be used to only fetch objects that
|
||||||
are on the branch specified by a project's revision.
|
are on the branch specified by a project's revision.
|
||||||
@@ -409,6 +510,15 @@ later is required to fix a server side protocol bug.
|
|||||||
|
|
||||||
_JOBS_WARN_THRESHOLD = 100
|
_JOBS_WARN_THRESHOLD = 100
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _deprecated_submodules_option(option, opt_str, _value, parser):
|
||||||
|
enabled = opt_str == "--fetch-submodules"
|
||||||
|
replacement = (
|
||||||
|
"--recurse-submodules" if enabled else "--no-recurse-submodules"
|
||||||
|
)
|
||||||
|
logger.warning("%s is deprecated; use %s instead", opt_str, replacement)
|
||||||
|
setattr(parser.values, option.dest, enabled)
|
||||||
|
|
||||||
def _Options(self, p, show_smart=True):
|
def _Options(self, p, show_smart=True):
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--jobs-network",
|
"--jobs-network",
|
||||||
@@ -527,6 +637,13 @@ later is required to fix a server side protocol bug.
|
|||||||
help="temporary manifest to use for this sync",
|
help="temporary manifest to use for this sync",
|
||||||
metavar="NAME.xml",
|
metavar="NAME.xml",
|
||||||
)
|
)
|
||||||
|
p.add_option(
|
||||||
|
"-g",
|
||||||
|
"--groups",
|
||||||
|
help="sync projects matching the specific groups. Not persistent "
|
||||||
|
"unlike when used on init",
|
||||||
|
metavar="GROUP",
|
||||||
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--clone-bundle",
|
"--clone-bundle",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@@ -551,9 +668,29 @@ later is required to fix a server side protocol bug.
|
|||||||
help="password to authenticate with the manifest server",
|
help="password to authenticate with the manifest server",
|
||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--fetch-submodules",
|
"--recurse-submodules",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="fetch submodules from server",
|
help="sync submodules from server",
|
||||||
|
)
|
||||||
|
p.add_option(
|
||||||
|
"--no-recurse-submodules",
|
||||||
|
dest="recurse_submodules",
|
||||||
|
action="store_false",
|
||||||
|
help="don't sync submodules from server",
|
||||||
|
)
|
||||||
|
p.add_option(
|
||||||
|
"--fetch-submodules",
|
||||||
|
dest="recurse_submodules",
|
||||||
|
action="callback",
|
||||||
|
callback=self._deprecated_submodules_option,
|
||||||
|
help=optparse.SUPPRESS_HELP,
|
||||||
|
)
|
||||||
|
p.add_option(
|
||||||
|
"--no-fetch-submodules",
|
||||||
|
dest="recurse_submodules",
|
||||||
|
action="callback",
|
||||||
|
callback=self._deprecated_submodules_option,
|
||||||
|
help=optparse.SUPPRESS_HELP,
|
||||||
)
|
)
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--use-superproject",
|
"--use-superproject",
|
||||||
@@ -723,8 +860,9 @@ later is required to fix a server side protocol bug.
|
|||||||
|
|
||||||
all_projects = self.GetProjects(
|
all_projects = self.GetProjects(
|
||||||
args,
|
args,
|
||||||
|
groups=opt.groups,
|
||||||
missing_ok=True,
|
missing_ok=True,
|
||||||
submodules_ok=opt.fetch_submodules,
|
submodules_ok=opt.recurse_submodules,
|
||||||
manifest=manifest,
|
manifest=manifest,
|
||||||
all_manifests=not opt.this_manifest_only,
|
all_manifests=not opt.this_manifest_only,
|
||||||
)
|
)
|
||||||
@@ -1007,6 +1145,40 @@ later is required to fix a server side protocol bug.
|
|||||||
|
|
||||||
return _FetchResult(ret, fetched)
|
return _FetchResult(ret, fetched)
|
||||||
|
|
||||||
|
def _FetchParentFirst(
|
||||||
|
self,
|
||||||
|
projects: List[Project],
|
||||||
|
opt: optparse.Values,
|
||||||
|
err_event: _threading.Event,
|
||||||
|
ssh_proxy: ssh.ProxyManager,
|
||||||
|
errors: List[Exception],
|
||||||
|
) -> _FetchResult:
|
||||||
|
"""Fetch |projects|, holding submodules back until their parent is done.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
projects: Projects to fetch.
|
||||||
|
opt: Program options returned from optparse. See _Options().
|
||||||
|
err_event: Whether an error was hit while processing.
|
||||||
|
ssh_proxy: SSH manager for clients & masters.
|
||||||
|
errors: A list to accumulate errors.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
_FetchResult for all the batches combined.
|
||||||
|
"""
|
||||||
|
success = True
|
||||||
|
fetched = set()
|
||||||
|
for batch in _ParentFirstBatches(projects):
|
||||||
|
batch = _WithoutProjects(batch, _RefreshDerivedRevisions(batch))
|
||||||
|
if not batch:
|
||||||
|
continue
|
||||||
|
batch.sort(key=self._fetch_times.Get, reverse=True)
|
||||||
|
result = self._Fetch(batch, opt, err_event, ssh_proxy, errors)
|
||||||
|
success = success and result.success
|
||||||
|
fetched.update(result.projects)
|
||||||
|
if not success and opt.fail_fast:
|
||||||
|
break
|
||||||
|
return _FetchResult(success, fetched)
|
||||||
|
|
||||||
def _FetchMain(
|
def _FetchMain(
|
||||||
self, opt, args, all_projects, err_event, ssh_proxy, manifest, errors
|
self, opt, args, all_projects, err_event, ssh_proxy, manifest, errors
|
||||||
):
|
):
|
||||||
@@ -1023,12 +1195,10 @@ later is required to fix a server side protocol bug.
|
|||||||
Returns:
|
Returns:
|
||||||
List of all projects that should be checked out.
|
List of all projects that should be checked out.
|
||||||
"""
|
"""
|
||||||
to_fetch = []
|
|
||||||
to_fetch.extend(all_projects)
|
|
||||||
to_fetch.sort(key=self._fetch_times.Get, reverse=True)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self._Fetch(to_fetch, opt, err_event, ssh_proxy, errors)
|
result = self._FetchParentFirst(
|
||||||
|
all_projects, opt, err_event, ssh_proxy, errors
|
||||||
|
)
|
||||||
success = result.success
|
success = result.success
|
||||||
fetched = result.projects
|
fetched = result.projects
|
||||||
if not success:
|
if not success:
|
||||||
@@ -1052,8 +1222,9 @@ later is required to fix a server side protocol bug.
|
|||||||
self._ReloadManifest(None, manifest)
|
self._ReloadManifest(None, manifest)
|
||||||
all_projects = self.GetProjects(
|
all_projects = self.GetProjects(
|
||||||
args,
|
args,
|
||||||
|
groups=opt.groups,
|
||||||
missing_ok=True,
|
missing_ok=True,
|
||||||
submodules_ok=opt.fetch_submodules,
|
submodules_ok=opt.recurse_submodules,
|
||||||
manifest=manifest,
|
manifest=manifest,
|
||||||
all_manifests=not opt.this_manifest_only,
|
all_manifests=not opt.this_manifest_only,
|
||||||
)
|
)
|
||||||
@@ -1070,7 +1241,9 @@ later is required to fix a server side protocol bug.
|
|||||||
if previously_missing_set == missing_set:
|
if previously_missing_set == missing_set:
|
||||||
break
|
break
|
||||||
previously_missing_set = missing_set
|
previously_missing_set = missing_set
|
||||||
result = self._Fetch(missing, opt, err_event, ssh_proxy, errors)
|
result = self._FetchParentFirst(
|
||||||
|
missing, opt, err_event, ssh_proxy, errors
|
||||||
|
)
|
||||||
success = result.success
|
success = result.success
|
||||||
new_fetched = result.projects
|
new_fetched = result.projects
|
||||||
if not success:
|
if not success:
|
||||||
@@ -1722,16 +1895,104 @@ later is required to fix a server side protocol bug.
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _SmartSyncSetup(self, opt, smart_sync_manifest_path, manifest):
|
def _ResolveManifestServerTransport(self, opt, manifest):
|
||||||
if not manifest.manifest_server:
|
"""Resolves the manifest server URL and transport.
|
||||||
raise SmartSyncError(
|
|
||||||
"error: cannot smart sync: no manifest server defined in "
|
|
||||||
"manifest"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[str, xmlrpc.client.Transport]: The resolved server URL and
|
||||||
|
transport.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SmartSyncError: If resolution fails (e.g. helper missing, helper
|
||||||
|
error, unsupported scheme).
|
||||||
|
"""
|
||||||
manifest_server = manifest.manifest_server
|
manifest_server = manifest.manifest_server
|
||||||
if not opt.quiet:
|
helper_binary = manifest.manifest_server_helper
|
||||||
print("Using manifest server %s" % manifest_server)
|
|
||||||
|
if helper_binary:
|
||||||
|
if not shutil.which(helper_binary):
|
||||||
|
raise SmartSyncError(
|
||||||
|
f"error: helper binary '{helper_binary}' declared in "
|
||||||
|
"manifest was not found in your PATH."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not opt.quiet:
|
||||||
|
print(f"Using remote helper {helper_binary}")
|
||||||
|
|
||||||
|
p = None
|
||||||
|
try:
|
||||||
|
p = subprocess.Popen(
|
||||||
|
[helper_binary, manifest_server],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
stdout, stderr = p.communicate(timeout=10)
|
||||||
|
output = stdout.strip()
|
||||||
|
stderr_content = stderr.strip() if stderr else ""
|
||||||
|
except subprocess.TimeoutExpired as e:
|
||||||
|
err_msg = f"helper {helper_binary} timed out after 10 seconds"
|
||||||
|
timeout_stderr = e.stderr.strip() if e.stderr else ""
|
||||||
|
if timeout_stderr:
|
||||||
|
err_msg += f". Stderr: {timeout_stderr}"
|
||||||
|
raise SmartSyncError(err_msg)
|
||||||
|
except OSError as e:
|
||||||
|
raise SmartSyncError(
|
||||||
|
"failed to start or communicate with helper "
|
||||||
|
f"{helper_binary}: {e}"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
if p and p.poll() is None:
|
||||||
|
p.kill()
|
||||||
|
p.wait()
|
||||||
|
|
||||||
|
if p.returncode != 0:
|
||||||
|
err_msg = (
|
||||||
|
f"helper {helper_binary} exited with exit code "
|
||||||
|
f"{p.returncode}."
|
||||||
|
)
|
||||||
|
if stderr_content:
|
||||||
|
err_msg += f" Stderr: {stderr_content}"
|
||||||
|
raise SmartSyncError(err_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = json.loads(output)
|
||||||
|
status = res.get("status")
|
||||||
|
msg = res.get("message")
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
err_msg = (
|
||||||
|
f"failed to parse JSON from helper {helper_binary}: {e}. "
|
||||||
|
f"Output was: {output}"
|
||||||
|
)
|
||||||
|
if stderr_content:
|
||||||
|
err_msg += f"\nStderr was: {stderr_content}"
|
||||||
|
raise SmartSyncError(err_msg)
|
||||||
|
|
||||||
|
if status != "ok":
|
||||||
|
err_msg = f"helper {helper_binary} returned error: {msg}"
|
||||||
|
if stderr_content:
|
||||||
|
err_msg += f"\nStderr was: {stderr_content}"
|
||||||
|
raise SmartSyncError(err_msg)
|
||||||
|
|
||||||
|
proxy_url = msg
|
||||||
|
transport = PersistentTransport(manifest_server, proxy=proxy_url)
|
||||||
|
server_url = manifest_server
|
||||||
|
if server_url.startswith("persistent-"):
|
||||||
|
server_url = server_url[len("persistent-") :]
|
||||||
|
return server_url, transport
|
||||||
|
|
||||||
|
# Fallback path if helper isn't specified
|
||||||
|
scheme = urllib.parse.urlparse(manifest_server).scheme
|
||||||
|
if scheme not in (
|
||||||
|
"http",
|
||||||
|
"https",
|
||||||
|
"persistent-http",
|
||||||
|
"persistent-https",
|
||||||
|
):
|
||||||
|
raise SmartSyncError(
|
||||||
|
f"error: unsupported manifest server scheme '{scheme}'."
|
||||||
|
)
|
||||||
|
|
||||||
if "@" not in manifest_server:
|
if "@" not in manifest_server:
|
||||||
username = None
|
username = None
|
||||||
@@ -1766,20 +2027,34 @@ later is required to fix a server side protocol bug.
|
|||||||
)
|
)
|
||||||
|
|
||||||
transport = PersistentTransport(manifest_server)
|
transport = PersistentTransport(manifest_server)
|
||||||
if manifest_server.startswith("persistent-"):
|
server_url = manifest_server
|
||||||
manifest_server = manifest_server[len("persistent-") :]
|
if server_url.startswith("persistent-"):
|
||||||
|
server_url = server_url[len("persistent-") :]
|
||||||
|
|
||||||
|
return server_url, transport
|
||||||
|
|
||||||
|
def _SmartSyncSetup(self, opt, smart_sync_manifest_path, manifest):
|
||||||
|
if not manifest.manifest_server:
|
||||||
|
raise SmartSyncError(
|
||||||
|
"error: cannot smart sync: no manifest server defined in "
|
||||||
|
"manifest"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not opt.quiet:
|
||||||
|
print("Using manifest server %s" % manifest.manifest_server)
|
||||||
|
|
||||||
|
server_url, transport = self._ResolveManifestServerTransport(
|
||||||
|
opt, manifest
|
||||||
|
)
|
||||||
|
|
||||||
# Changes in behavior should update docs/smart-sync.md accordingly.
|
# Changes in behavior should update docs/smart-sync.md accordingly.
|
||||||
try:
|
try:
|
||||||
server = xmlrpc.client.Server(manifest_server, transport=transport)
|
server = xmlrpc.client.Server(server_url, transport=transport)
|
||||||
if opt.smart_sync:
|
if opt.smart_sync:
|
||||||
branch = self._GetBranch(manifest.manifestProject)
|
branch = self._GetBranch(manifest.manifestProject)
|
||||||
|
target = None
|
||||||
if "SYNC_TARGET" in os.environ:
|
if "SYNC_TARGET" in os.environ:
|
||||||
target = os.environ["SYNC_TARGET"]
|
target = os.environ["SYNC_TARGET"]
|
||||||
[success, manifest_str] = server.GetApprovedManifest(
|
|
||||||
branch, target
|
|
||||||
)
|
|
||||||
elif (
|
elif (
|
||||||
"TARGET_PRODUCT" in os.environ
|
"TARGET_PRODUCT" in os.environ
|
||||||
and "TARGET_BUILD_VARIANT" in os.environ
|
and "TARGET_BUILD_VARIANT" in os.environ
|
||||||
@@ -1790,9 +2065,6 @@ later is required to fix a server side protocol bug.
|
|||||||
os.environ["TARGET_RELEASE"],
|
os.environ["TARGET_RELEASE"],
|
||||||
os.environ["TARGET_BUILD_VARIANT"],
|
os.environ["TARGET_BUILD_VARIANT"],
|
||||||
)
|
)
|
||||||
[success, manifest_str] = server.GetApprovedManifest(
|
|
||||||
branch, target
|
|
||||||
)
|
|
||||||
elif (
|
elif (
|
||||||
"TARGET_PRODUCT" in os.environ
|
"TARGET_PRODUCT" in os.environ
|
||||||
and "TARGET_BUILD_VARIANT" in os.environ
|
and "TARGET_BUILD_VARIANT" in os.environ
|
||||||
@@ -1801,6 +2073,8 @@ later is required to fix a server side protocol bug.
|
|||||||
os.environ["TARGET_PRODUCT"],
|
os.environ["TARGET_PRODUCT"],
|
||||||
os.environ["TARGET_BUILD_VARIANT"],
|
os.environ["TARGET_BUILD_VARIANT"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if target:
|
||||||
[success, manifest_str] = server.GetApprovedManifest(
|
[success, manifest_str] = server.GetApprovedManifest(
|
||||||
branch, target
|
branch, target
|
||||||
)
|
)
|
||||||
@@ -1822,25 +2096,34 @@ later is required to fix a server side protocol bug.
|
|||||||
aggregate_errors=[e],
|
aggregate_errors=[e],
|
||||||
)
|
)
|
||||||
self._ReloadManifest(manifest_name, manifest)
|
self._ReloadManifest(manifest_name, manifest)
|
||||||
else:
|
return manifest_name
|
||||||
raise SmartSyncError(
|
|
||||||
"error: manifest server RPC call failed: %s" % manifest_str
|
raise SmartSyncError(
|
||||||
)
|
"error: manifest server RPC call failed: %s" % manifest_str
|
||||||
|
)
|
||||||
except (OSError, xmlrpc.client.Fault) as e:
|
except (OSError, xmlrpc.client.Fault) as e:
|
||||||
|
if manifest.manifest_server_helper:
|
||||||
|
raise SmartSyncError(
|
||||||
|
"error: failed to communicate with manifest server via "
|
||||||
|
f"helper: {e}"
|
||||||
|
)
|
||||||
raise SmartSyncError(
|
raise SmartSyncError(
|
||||||
"error: cannot connect to manifest server %s:\n%s"
|
"error: cannot connect to manifest server %s:\n%s"
|
||||||
% (manifest.manifest_server, e),
|
% (manifest.manifest_server, e),
|
||||||
aggregate_errors=[e],
|
aggregate_errors=[e],
|
||||||
)
|
)
|
||||||
except xmlrpc.client.ProtocolError as e:
|
except xmlrpc.client.ProtocolError as e:
|
||||||
|
if manifest.manifest_server_helper:
|
||||||
|
raise SmartSyncError(
|
||||||
|
"error: failed to communicate with manifest server via "
|
||||||
|
f"helper: {e}"
|
||||||
|
)
|
||||||
raise SmartSyncError(
|
raise SmartSyncError(
|
||||||
"error: cannot connect to manifest server %s:\n%d %s"
|
"error: cannot connect to manifest server %s:\n%d %s"
|
||||||
% (manifest.manifest_server, e.errcode, e.errmsg),
|
% (manifest.manifest_server, e.errcode, e.errmsg),
|
||||||
aggregate_errors=[e],
|
aggregate_errors=[e],
|
||||||
)
|
)
|
||||||
|
|
||||||
return manifest_name
|
|
||||||
|
|
||||||
def _UpdateAllManifestProjects(self, opt, mp, manifest_name, errors):
|
def _UpdateAllManifestProjects(self, opt, mp, manifest_name, errors):
|
||||||
"""Fetch & update the local manifest project.
|
"""Fetch & update the local manifest project.
|
||||||
|
|
||||||
@@ -2189,8 +2472,9 @@ later is required to fix a server side protocol bug.
|
|||||||
|
|
||||||
all_projects = self.GetProjects(
|
all_projects = self.GetProjects(
|
||||||
args,
|
args,
|
||||||
|
groups=opt.groups,
|
||||||
missing_ok=True,
|
missing_ok=True,
|
||||||
submodules_ok=opt.fetch_submodules,
|
submodules_ok=opt.recurse_submodules,
|
||||||
manifest=manifest,
|
manifest=manifest,
|
||||||
all_manifests=not opt.this_manifest_only,
|
all_manifests=not opt.this_manifest_only,
|
||||||
)
|
)
|
||||||
@@ -2586,7 +2870,7 @@ later is required to fix a server side protocol bug.
|
|||||||
|
|
||||||
return _SyncResult(
|
return _SyncResult(
|
||||||
project_index=project_index,
|
project_index=project_index,
|
||||||
relpath=project.relpath,
|
relpath=project.RelPath(local=opt.this_manifest_only),
|
||||||
fetch_success=fetch_success,
|
fetch_success=fetch_success,
|
||||||
remote_fetched=remote_fetched,
|
remote_fetched=remote_fetched,
|
||||||
checkout_success=checkout_success,
|
checkout_success=checkout_success,
|
||||||
@@ -2734,6 +3018,10 @@ later is required to fix a server side protocol bug.
|
|||||||
self._interleaved_err_checkout = False
|
self._interleaved_err_checkout = False
|
||||||
self._interleaved_err_checkout_results = []
|
self._interleaved_err_checkout_results = []
|
||||||
|
|
||||||
|
# Project.relpath is relative to its own (sub)manifest, so it does not
|
||||||
|
# tell apart projects of different manifests being synced together.
|
||||||
|
_RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
|
||||||
|
|
||||||
err_event = multiprocessing.Event()
|
err_event = multiprocessing.Event()
|
||||||
finished_relpaths = set()
|
finished_relpaths = set()
|
||||||
project_list = list(all_projects)
|
project_list = list(all_projects)
|
||||||
@@ -2770,13 +3058,13 @@ later is required to fix a server side protocol bug.
|
|||||||
projects_to_sync = [
|
projects_to_sync = [
|
||||||
p
|
p
|
||||||
for p in project_list
|
for p in project_list
|
||||||
if p.relpath not in finished_relpaths
|
if _RelPath(p) not in finished_relpaths
|
||||||
]
|
]
|
||||||
if not projects_to_sync:
|
if not projects_to_sync:
|
||||||
break
|
break
|
||||||
|
|
||||||
pending_relpaths = {
|
pending_relpaths = {
|
||||||
p.relpath for p in projects_to_sync
|
_RelPath(p) for p in projects_to_sync
|
||||||
}
|
}
|
||||||
if previously_pending_relpaths == pending_relpaths:
|
if previously_pending_relpaths == pending_relpaths:
|
||||||
stalled_projects_str = "\n".join(
|
stalled_projects_str = "\n".join(
|
||||||
@@ -2805,12 +3093,22 @@ later is required to fix a server side protocol bug.
|
|||||||
# projects in one level can be processed in
|
# projects in one level can be processed in
|
||||||
# parallel, but we must wait for a level to complete
|
# parallel, but we must wait for a level to complete
|
||||||
# before starting the next.
|
# before starting the next.
|
||||||
|
submodule_revisions = {}
|
||||||
for level_projects in _SafeCheckoutOrder(
|
for level_projects in _SafeCheckoutOrder(
|
||||||
projects_to_sync
|
projects_to_sync
|
||||||
):
|
):
|
||||||
if not level_projects:
|
if not level_projects:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
level_projects = _WithoutProjects(
|
||||||
|
level_projects,
|
||||||
|
_RefreshDerivedRevisions(
|
||||||
|
level_projects, submodule_revisions
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if not level_projects:
|
||||||
|
continue
|
||||||
|
|
||||||
objdir_project_map = collections.defaultdict(
|
objdir_project_map = collections.defaultdict(
|
||||||
list
|
list
|
||||||
)
|
)
|
||||||
@@ -2852,8 +3150,9 @@ later is required to fix a server side protocol bug.
|
|||||||
self._ReloadManifest(None, manifest)
|
self._ReloadManifest(None, manifest)
|
||||||
project_list = self.GetProjects(
|
project_list = self.GetProjects(
|
||||||
args,
|
args,
|
||||||
|
groups=opt.groups,
|
||||||
missing_ok=True,
|
missing_ok=True,
|
||||||
submodules_ok=opt.fetch_submodules,
|
submodules_ok=opt.recurse_submodules,
|
||||||
manifest=manifest,
|
manifest=manifest,
|
||||||
all_manifests=not opt.this_manifest_only,
|
all_manifests=not opt.this_manifest_only,
|
||||||
)
|
)
|
||||||
@@ -3078,14 +3377,15 @@ class LocalSyncState:
|
|||||||
# request to request like the normal transport, the real url
|
# request to request like the normal transport, the real url
|
||||||
# is passed during initialization.
|
# is passed during initialization.
|
||||||
class PersistentTransport(xmlrpc.client.Transport):
|
class PersistentTransport(xmlrpc.client.Transport):
|
||||||
def __init__(self, orig_host):
|
def __init__(self, orig_host, proxy=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.orig_host = orig_host
|
self.orig_host = orig_host
|
||||||
|
self.proxy = proxy
|
||||||
|
|
||||||
def request(self, host, handler, request_body, verbose=False):
|
def request(self, host, handler, request_body, verbose=False):
|
||||||
with GetUrlCookieFile(self.orig_host, not verbose) as (
|
with GetUrlCookieFile(self.orig_host, not verbose) as (
|
||||||
cookiefile,
|
cookiefile,
|
||||||
proxy,
|
cookie_proxy,
|
||||||
):
|
):
|
||||||
# Python doesn't understand cookies with the #HttpOnly_ prefix
|
# Python doesn't understand cookies with the #HttpOnly_ prefix
|
||||||
# Since we're only using them for HTTP, copy the file temporarily,
|
# Since we're only using them for HTTP, copy the file temporarily,
|
||||||
@@ -3111,10 +3411,11 @@ class PersistentTransport(xmlrpc.client.Transport):
|
|||||||
else:
|
else:
|
||||||
cookiejar = cookielib.CookieJar()
|
cookiejar = cookielib.CookieJar()
|
||||||
|
|
||||||
|
active_proxy = self.proxy or cookie_proxy
|
||||||
proxyhandler = urllib.request.ProxyHandler
|
proxyhandler = urllib.request.ProxyHandler
|
||||||
if proxy:
|
if active_proxy:
|
||||||
proxyhandler = urllib.request.ProxyHandler(
|
proxyhandler = urllib.request.ProxyHandler(
|
||||||
{"http": proxy, "https": proxy}
|
{"http": active_proxy, "https": active_proxy}
|
||||||
)
|
)
|
||||||
|
|
||||||
opener = urllib.request.build_opener(
|
opener = urllib.request.build_opener(
|
||||||
@@ -3127,13 +3428,16 @@ class PersistentTransport(xmlrpc.client.Transport):
|
|||||||
scheme = parse_results.scheme
|
scheme = parse_results.scheme
|
||||||
if scheme == "persistent-http":
|
if scheme == "persistent-http":
|
||||||
scheme = "http"
|
scheme = "http"
|
||||||
if scheme == "persistent-https":
|
elif scheme == "persistent-https":
|
||||||
# If we're proxying through persistent-https, use http. The
|
# If we're proxying through persistent-https, use http. The
|
||||||
# proxy itself will do the https.
|
# proxy itself will do the https.
|
||||||
if proxy:
|
if active_proxy:
|
||||||
scheme = "http"
|
scheme = "http"
|
||||||
else:
|
else:
|
||||||
scheme = "https"
|
scheme = "https"
|
||||||
|
elif scheme not in ("http", "https"):
|
||||||
|
if active_proxy:
|
||||||
|
scheme = "http"
|
||||||
|
|
||||||
# Parse out any authentication information using the base class.
|
# Parse out any authentication information using the base class.
|
||||||
host, extra_headers, _ = self.get_host_info(parse_results.netloc)
|
host, extra_headers, _ = self.get_host_info(parse_results.netloc)
|
||||||
|
|||||||
+17
-22
@@ -25,7 +25,6 @@ from editor import Editor
|
|||||||
from error import GitError
|
from error import GitError
|
||||||
from error import SilentRepoExitError
|
from error import SilentRepoExitError
|
||||||
from error import UploadError
|
from error import UploadError
|
||||||
from git_command import GitCommand
|
|
||||||
from git_refs import R_HEADS
|
from git_refs import R_HEADS
|
||||||
import git_superproject
|
import git_superproject
|
||||||
from hooks import RepoHook
|
from hooks import RepoHook
|
||||||
@@ -379,7 +378,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
default=True,
|
default=True,
|
||||||
help="disable verifying ssl certs (unsafe)",
|
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):
|
def _SingleBranch(self, opt, branch, people):
|
||||||
project = branch.project
|
project = branch.project
|
||||||
@@ -649,6 +648,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
validate_certs=opt.validate_certs,
|
validate_certs=opt.validate_certs,
|
||||||
push_options=push_options,
|
push_options=push_options,
|
||||||
patchset_description=opt.patchset_description,
|
patchset_description=opt.patchset_description,
|
||||||
|
git_event_log=self.git_event_log,
|
||||||
)
|
)
|
||||||
|
|
||||||
branch.uploaded = True
|
branch.uploaded = True
|
||||||
@@ -704,36 +704,31 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
raise UploadExitError(aggregate_errors=aggregate_errors)
|
raise UploadExitError(aggregate_errors=aggregate_errors)
|
||||||
|
|
||||||
def _GetMergeBranch(self, project, local_branch=None):
|
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:
|
if local_branch is None:
|
||||||
p = GitCommand(
|
local_branch = project.CurrentBranch
|
||||||
project,
|
if local_branch:
|
||||||
["rev-parse", "--abbrev-ref", "HEAD"],
|
branch = project.GetBranch(local_branch)
|
||||||
capture_stdout=True,
|
if branch.merge:
|
||||||
capture_stderr=True,
|
return branch.merge
|
||||||
)
|
return ""
|
||||||
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
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _GatherOne(cls, opt, project_idx):
|
def _GatherOne(cls, opt, project_idx):
|
||||||
"""Figure out the upload status for |project|."""
|
"""Figure out the upload status for |project|."""
|
||||||
project = cls.get_parallel_context()["projects"][project_idx]
|
project = cls.get_parallel_context()["projects"][project_idx]
|
||||||
|
cbr = None
|
||||||
if opt.current_branch:
|
if opt.current_branch:
|
||||||
cbr = project.CurrentBranch
|
cbr = project.CurrentBranch
|
||||||
up_branch = project.GetUploadableBranch(cbr)
|
up_branch = project.GetUploadableBranch(cbr)
|
||||||
avail = [up_branch] if up_branch else None
|
avail = [up_branch] if up_branch else None
|
||||||
else:
|
else:
|
||||||
avail = project.GetUploadableBranches(opt.branch)
|
avail = project.GetUploadableBranches(opt.branch)
|
||||||
return (project_idx, avail)
|
return (project_idx, avail, cbr)
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
projects = self.GetProjects(
|
projects = self.GetProjects(
|
||||||
@@ -743,7 +738,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
def _ProcessResults(_pool, _out, results):
|
def _ProcessResults(_pool, _out, results):
|
||||||
pending = []
|
pending = []
|
||||||
for result in results:
|
for result in results:
|
||||||
project_idx, avail = result
|
project_idx, avail, current_branch = result
|
||||||
project = projects[project_idx]
|
project = projects[project_idx]
|
||||||
if avail is None:
|
if avail is None:
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -751,7 +746,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
"You might be able to fix the branch by running:\n"
|
"You might be able to fix the branch by running:\n"
|
||||||
" git branch --set-upstream-to m/%s",
|
" git branch --set-upstream-to m/%s",
|
||||||
project.RelPath(local=opt.this_manifest_only),
|
project.RelPath(local=opt.this_manifest_only),
|
||||||
project.CurrentBranch,
|
current_branch,
|
||||||
project.manifest.branch,
|
project.manifest.branch,
|
||||||
)
|
)
|
||||||
elif avail:
|
elif avail:
|
||||||
|
|||||||
+20
-2
@@ -14,10 +14,12 @@
|
|||||||
|
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
|
from typing import Any, Tuple
|
||||||
|
|
||||||
from command import Command
|
from command import Command
|
||||||
from command import MirrorSafeCommand
|
from command import MirrorSafeCommand
|
||||||
from git_command import git
|
from git_command import git
|
||||||
|
from git_command import git_require
|
||||||
from git_command import RepoSourceVersion
|
from git_command import RepoSourceVersion
|
||||||
from git_command import user_agent
|
from git_command import user_agent
|
||||||
from git_refs import HEAD
|
from git_refs import HEAD
|
||||||
@@ -34,6 +36,22 @@ class Version(Command, MirrorSafeCommand):
|
|||||||
%prog
|
%prog
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _RepoVersion(project: Any) -> Tuple[str, str]:
|
||||||
|
"""Return repo's describe string and commit date."""
|
||||||
|
if git_require((2, 32, 0)):
|
||||||
|
output = project.bare_git.log(
|
||||||
|
"-1", "--format=%(describe)%n%cD", HEAD
|
||||||
|
)
|
||||||
|
description, commit_date = output.rstrip("\n").split("\n", 1)
|
||||||
|
if description:
|
||||||
|
return description, commit_date
|
||||||
|
|
||||||
|
return (
|
||||||
|
project.bare_git.describe(HEAD),
|
||||||
|
project.bare_git.log("-1", "--format=%cD", HEAD),
|
||||||
|
)
|
||||||
|
|
||||||
def Execute(self, opt, args):
|
def Execute(self, opt, args):
|
||||||
rp = self.manifest.repoProject
|
rp = self.manifest.repoProject
|
||||||
rem = rp.GetRemote()
|
rem = rp.GetRemote()
|
||||||
@@ -41,11 +59,11 @@ class Version(Command, MirrorSafeCommand):
|
|||||||
|
|
||||||
# These might not be the same. Report them both.
|
# These might not be the same. Report them both.
|
||||||
src_ver = RepoSourceVersion()
|
src_ver = RepoSourceVersion()
|
||||||
rp_ver = rp.bare_git.describe(HEAD)
|
rp_ver, commit_date = self._RepoVersion(rp)
|
||||||
print(f"repo version {rp_ver}")
|
print(f"repo version {rp_ver}")
|
||||||
print(f" (from {rem.url})")
|
print(f" (from {rem.url})")
|
||||||
print(f" (tracking {branch.merge})")
|
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:
|
if self.wrapper_path is not None:
|
||||||
print(f"repo launcher version {self.wrapper_version}")
|
print(f"repo launcher version {self.wrapper_version}")
|
||||||
|
|||||||
+81
-1
@@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
"""Unittests for the color.py module."""
|
"""Unittests for the color.py module."""
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import utils_for_test
|
import utils_for_test
|
||||||
|
|
||||||
@@ -24,9 +26,14 @@ import git_config
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def coloring() -> color.Coloring:
|
def coloring() -> color.Coloring:
|
||||||
"""Create a Coloring object for testing."""
|
"""Create a Coloring object for testing."""
|
||||||
|
return _make_coloring("always")
|
||||||
|
|
||||||
|
|
||||||
|
def _make_coloring(default_state: str) -> color.Coloring:
|
||||||
|
"""Set the default color mode and return a Coloring using test config."""
|
||||||
config_fixture = utils_for_test.FIXTURES_DIR / "test.gitconfig"
|
config_fixture = utils_for_test.FIXTURES_DIR / "test.gitconfig"
|
||||||
config = git_config.GitConfig(config_fixture)
|
config = git_config.GitConfig(config_fixture)
|
||||||
color.SetDefaultColoring("true")
|
color.SetDefaultColoring(default_state)
|
||||||
return color.Coloring(config, "status")
|
return color.Coloring(config, "status")
|
||||||
|
|
||||||
|
|
||||||
@@ -72,3 +79,76 @@ def test_Color_Parse_empty_entry(coloring: color.Coloring) -> None:
|
|||||||
assert val == "\033[2;34;47m"
|
assert val == "\033[2;34;47m"
|
||||||
val = coloring._parse("empty", "green", "white", "bold")
|
val = coloring._parse("empty", "green", "white", "bold")
|
||||||
assert val == "\033[1;32;47m"
|
assert val == "\033[1;32;47m"
|
||||||
|
|
||||||
|
|
||||||
|
class TestSetDefaultColoring:
|
||||||
|
"""Tests for SetDefaultColoring."""
|
||||||
|
|
||||||
|
def test_none_leaves_default_unchanged(self) -> None:
|
||||||
|
color.DEFAULT = "auto"
|
||||||
|
color.SetDefaultColoring(None)
|
||||||
|
assert color.DEFAULT == "auto"
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"value, expected",
|
||||||
|
(
|
||||||
|
# auto/true/yes all store their lowercase form.
|
||||||
|
("auto", "auto"),
|
||||||
|
("Auto", "auto"),
|
||||||
|
("true", "true"),
|
||||||
|
("True", "true"),
|
||||||
|
("yes", "yes"),
|
||||||
|
("Yes", "yes"),
|
||||||
|
# "always" stores as "always".
|
||||||
|
("always", "always"),
|
||||||
|
("Always", "always"),
|
||||||
|
# never/no/false store their lowercase form.
|
||||||
|
("never", "never"),
|
||||||
|
("no", "no"),
|
||||||
|
("false", "false"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_maps_to_expected(self, value: str, expected: str) -> None:
|
||||||
|
color.SetDefaultColoring(value)
|
||||||
|
assert color.DEFAULT == expected
|
||||||
|
|
||||||
|
def test_unrecognised_leaves_default_unchanged(self) -> None:
|
||||||
|
color.DEFAULT = "auto"
|
||||||
|
color.SetDefaultColoring("garbage")
|
||||||
|
assert color.DEFAULT == "auto"
|
||||||
|
|
||||||
|
|
||||||
|
class TestColoringInit:
|
||||||
|
"""Tests for Coloring.__init__ color mode logic."""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"state, isatty, pager_active, expected",
|
||||||
|
(
|
||||||
|
# "always" enables color unconditionally.
|
||||||
|
("always", False, False, True),
|
||||||
|
# "never" disables color unconditionally.
|
||||||
|
("never", True, True, False),
|
||||||
|
# auto/true/yes enable color only on a TTY or active pager.
|
||||||
|
("auto", True, False, True),
|
||||||
|
("auto", False, False, False),
|
||||||
|
("auto", False, True, True),
|
||||||
|
("true", True, False, True),
|
||||||
|
("true", False, False, False),
|
||||||
|
("true", False, True, True),
|
||||||
|
("yes", True, False, True),
|
||||||
|
("yes", False, False, False),
|
||||||
|
("yes", False, True, True),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_color_mode(
|
||||||
|
self,
|
||||||
|
state: str,
|
||||||
|
isatty: bool,
|
||||||
|
pager_active: bool,
|
||||||
|
expected: bool,
|
||||||
|
) -> None:
|
||||||
|
with mock.patch("os.isatty", return_value=isatty), mock.patch(
|
||||||
|
"pager.active", pager_active
|
||||||
|
):
|
||||||
|
c = _make_coloring(state)
|
||||||
|
assert c.is_on is expected
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
"""Unittests for the command.py module."""
|
"""Unittests for the command.py module."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from command import Command
|
from command import Command
|
||||||
|
|
||||||
|
|
||||||
@@ -86,3 +88,32 @@ def test_get_projects_keeps_derived_subprojects_for_repeated_repo():
|
|||||||
projects = cmd.GetProjects([])
|
projects = cmd.GetProjects([])
|
||||||
|
|
||||||
assert set(projects) == {project_a, project_b, submodule_a, submodule_b}
|
assert set(projects) == {project_a, project_b, submodule_a, submodule_b}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"submodules_ok, sync_s, includes_submodule",
|
||||||
|
[
|
||||||
|
(None, False, False),
|
||||||
|
(None, True, True),
|
||||||
|
(True, False, True),
|
||||||
|
(True, True, True),
|
||||||
|
(False, False, False),
|
||||||
|
(False, True, False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_projects_submodule_override(
|
||||||
|
submodules_ok, sync_s, includes_submodule
|
||||||
|
):
|
||||||
|
"""The CLI override takes precedence over a project's sync-s setting."""
|
||||||
|
submodule = FakeProject("submodule", "project/submodule")
|
||||||
|
project = FakeProject(
|
||||||
|
"project",
|
||||||
|
"project",
|
||||||
|
derived_subprojects=[submodule],
|
||||||
|
sync_s=sync_s,
|
||||||
|
)
|
||||||
|
cmd = Command(manifest=FakeManifest([project]))
|
||||||
|
|
||||||
|
projects = cmd.GetProjects([], submodules_ok=submodules_ok)
|
||||||
|
|
||||||
|
assert (submodule in projects) is includes_submodule
|
||||||
|
|||||||
@@ -231,6 +231,24 @@ class GitCommandStreamLogsTest(unittest.TestCase):
|
|||||||
class GitCallUnitTest(unittest.TestCase):
|
class GitCallUnitTest(unittest.TestCase):
|
||||||
"""Tests the _GitCall class (via git_command.git)."""
|
"""Tests the _GitCall class (via git_command.git)."""
|
||||||
|
|
||||||
|
def test_valid_branch_name_uses_branch_mode(self) -> None:
|
||||||
|
"""Branch validation applies Git's branch-specific restrictions."""
|
||||||
|
command = mock.MagicMock()
|
||||||
|
command.Wait.return_value = 1
|
||||||
|
with mock.patch.object(
|
||||||
|
git_command, "GitCommand", return_value=command
|
||||||
|
) as check:
|
||||||
|
self.assertFalse(git_command.IsValidBranchName("-topic"))
|
||||||
|
|
||||||
|
check.assert_called_once_with(
|
||||||
|
None,
|
||||||
|
["check-ref-format", "--branch", "-topic"],
|
||||||
|
capture_stdout=True,
|
||||||
|
capture_stderr=True,
|
||||||
|
add_event_log=False,
|
||||||
|
log_as_error=False,
|
||||||
|
)
|
||||||
|
|
||||||
def test_version_tuple(self):
|
def test_version_tuple(self):
|
||||||
"""Check git.version_tuple() handling."""
|
"""Check git.version_tuple() handling."""
|
||||||
ver = git_command.git.version_tuple()
|
ver = git_command.git.version_tuple()
|
||||||
|
|||||||
@@ -244,3 +244,27 @@ def test_remote_save_with_push_url_without_projectname(
|
|||||||
assert (
|
assert (
|
||||||
written_config.GetString("remote.origin.pushurl") == "ssh://example.com"
|
written_config.GetString("remote.origin.pushurl") == "ssh://example.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"rev, expected",
|
||||||
|
(
|
||||||
|
("a" * 40, True),
|
||||||
|
("0" * 40, True),
|
||||||
|
("f" * 40, True),
|
||||||
|
("a" * 64, True),
|
||||||
|
("0" * 64, True),
|
||||||
|
("f" * 64, True),
|
||||||
|
("a" * 39, False),
|
||||||
|
("a" * 41, False),
|
||||||
|
("a" * 63, False),
|
||||||
|
("a" * 65, False),
|
||||||
|
("g" * 40, False),
|
||||||
|
("g" * 64, False),
|
||||||
|
("refs/heads/master", False),
|
||||||
|
("refs/tags/v1.0", False),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_is_id(rev: str, expected: bool) -> None:
|
||||||
|
"""Test IsId identifies both SHA-1 and SHA-256 hashes."""
|
||||||
|
assert git_config.IsId(rev) == expected
|
||||||
|
|||||||
+148
-2
@@ -17,6 +17,8 @@
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from typing import Any, List
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import utils_for_test
|
import utils_for_test
|
||||||
@@ -24,7 +26,7 @@ import utils_for_test
|
|||||||
import git_refs
|
import git_refs
|
||||||
|
|
||||||
|
|
||||||
def _run(repo, *args):
|
def _run(repo: str, *args: str) -> str:
|
||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
["git", "-C", repo, *args],
|
["git", "-C", repo, *args],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
@@ -34,7 +36,7 @@ def _run(repo, *args):
|
|||||||
).stdout.strip()
|
).stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
def _init_repo(tmp_path, reftable=False):
|
def _init_repo(tmp_path: Path, reftable: bool = False) -> str:
|
||||||
repo = os.path.join(tmp_path, "repo")
|
repo = os.path.join(tmp_path, "repo")
|
||||||
ref_format = "reftable" if reftable else "files"
|
ref_format = "reftable" if reftable else "files"
|
||||||
utils_for_test.init_git_tree(repo, ref_format=ref_format)
|
utils_for_test.init_git_tree(repo, ref_format=ref_format)
|
||||||
@@ -57,10 +59,154 @@ def test_reads_refs(tmp_path, reftable):
|
|||||||
branch = _run(repo, "symbolic-ref", "--short", "HEAD")
|
branch = _run(repo, "symbolic-ref", "--short", "HEAD")
|
||||||
head = _run(repo, "rev-parse", "HEAD")
|
head = _run(repo, "rev-parse", "HEAD")
|
||||||
assert refs.symref("HEAD") == f"refs/heads/{branch}"
|
assert refs.symref("HEAD") == f"refs/heads/{branch}"
|
||||||
|
assert refs.head == f"refs/heads/{branch}"
|
||||||
assert refs.get("HEAD") == head
|
assert refs.get("HEAD") == head
|
||||||
assert refs.get(f"refs/heads/{branch}") == head
|
assert refs.get(f"refs/heads/{branch}") == head
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("reftable", [False, True])
|
||||||
|
def test_reads_detached_head(tmp_path: Path, reftable: bool) -> None:
|
||||||
|
if reftable and not utils_for_test.supports_reftable():
|
||||||
|
pytest.skip("reftable not supported")
|
||||||
|
|
||||||
|
repo = _init_repo(tmp_path, reftable=reftable)
|
||||||
|
head = _run(repo, "rev-parse", "HEAD")
|
||||||
|
_run(repo, "checkout", "--detach", head)
|
||||||
|
refs = git_refs.GitRefs(os.path.join(repo, ".git"))
|
||||||
|
|
||||||
|
assert refs.symref("HEAD") == ""
|
||||||
|
assert refs.head == head
|
||||||
|
assert refs.get("HEAD") == head
|
||||||
|
|
||||||
|
|
||||||
|
def test_reads_head_with_root_refs_in_one_command(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Git 2.45 and newer include HEAD in the ref snapshot."""
|
||||||
|
head = "1" * 40
|
||||||
|
commands = []
|
||||||
|
|
||||||
|
class FakeGitCommand:
|
||||||
|
def __init__(
|
||||||
|
self, _project: Any, cmdv: List[str], **_kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
commands.append(cmdv)
|
||||||
|
self.stdout = (
|
||||||
|
f"{head}\0HEAD\0refs/heads/main\n"
|
||||||
|
f"{head}\0refs/heads/main\0\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
def Wait(self) -> int:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
monkeypatch.setattr(git_refs, "GitCommand", FakeGitCommand)
|
||||||
|
monkeypatch.setattr(git_refs, "git_require", lambda _version: True)
|
||||||
|
refs = git_refs.GitRefs("/nonexistent")
|
||||||
|
with mock.patch.object(refs, "_ReadSymbolicRef") as read_head:
|
||||||
|
assert refs.get("HEAD") == head
|
||||||
|
|
||||||
|
assert commands == [
|
||||||
|
[
|
||||||
|
"for-each-ref",
|
||||||
|
"--include-root-refs",
|
||||||
|
"--format=%(objectname)%00%(refname)%00%(symref)",
|
||||||
|
"HEAD",
|
||||||
|
"refs",
|
||||||
|
]
|
||||||
|
]
|
||||||
|
read_head.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_root_ref_snapshot_falls_back_for_unborn_head(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""An unborn HEAD still uses symbolic-ref after the ref snapshot."""
|
||||||
|
|
||||||
|
class FakeGitCommand:
|
||||||
|
def __init__(
|
||||||
|
self, _project: Any, _cmdv: List[str], **_kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
self.stdout = ""
|
||||||
|
|
||||||
|
def Wait(self) -> int:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
monkeypatch.setattr(git_refs, "GitCommand", FakeGitCommand)
|
||||||
|
monkeypatch.setattr(git_refs, "git_require", lambda _version: True)
|
||||||
|
refs = git_refs.GitRefs("/nonexistent")
|
||||||
|
|
||||||
|
def read_head(name: str) -> None:
|
||||||
|
assert name == "HEAD"
|
||||||
|
refs._symref[name] = "refs/heads/main"
|
||||||
|
|
||||||
|
monkeypatch.setattr(refs, "_ReadSymbolicRef", read_head)
|
||||||
|
|
||||||
|
assert refs.symref("HEAD") == "refs/heads/main"
|
||||||
|
|
||||||
|
|
||||||
|
def test_old_git_keeps_separate_head_fallback(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Git before 2.45 uses the original for-each-ref and HEAD calls."""
|
||||||
|
head = "1" * 40
|
||||||
|
commands = []
|
||||||
|
|
||||||
|
class FakeGitCommand:
|
||||||
|
def __init__(
|
||||||
|
self, _project: Any, cmdv: List[str], **_kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
commands.append(cmdv)
|
||||||
|
self.stdout = f"{head}\0refs/heads/main\0\n"
|
||||||
|
|
||||||
|
def Wait(self) -> int:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
monkeypatch.setattr(git_refs, "GitCommand", FakeGitCommand)
|
||||||
|
monkeypatch.setattr(git_refs, "git_require", lambda _version: False)
|
||||||
|
refs = git_refs.GitRefs("/nonexistent")
|
||||||
|
|
||||||
|
def read_head(name: str) -> None:
|
||||||
|
assert name == "HEAD"
|
||||||
|
refs._symref[name] = "refs/heads/main"
|
||||||
|
|
||||||
|
monkeypatch.setattr(refs, "_ReadSymbolicRef", read_head)
|
||||||
|
|
||||||
|
assert refs.get("HEAD") == head
|
||||||
|
assert commands == [
|
||||||
|
[
|
||||||
|
"for-each-ref",
|
||||||
|
"--format=%(objectname)%00%(refname)%00%(symref)",
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_for_each_ref_failure_falls_back_to_symbolic_ref(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""When for-each-ref fails, HEAD is still resolved via symbolic-ref."""
|
||||||
|
|
||||||
|
class FakeGitCommand:
|
||||||
|
def __init__(
|
||||||
|
self, _project: Any, _cmdv: List[str], **_kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
self.stdout = ""
|
||||||
|
|
||||||
|
def Wait(self) -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
monkeypatch.setattr(git_refs, "GitCommand", FakeGitCommand)
|
||||||
|
monkeypatch.setattr(git_refs, "git_require", lambda _version: True)
|
||||||
|
refs = git_refs.GitRefs("/nonexistent")
|
||||||
|
|
||||||
|
def read_head(name: str) -> None:
|
||||||
|
assert name == "HEAD"
|
||||||
|
refs._symref[name] = "refs/heads/main"
|
||||||
|
|
||||||
|
monkeypatch.setattr(refs, "_ReadSymbolicRef", read_head)
|
||||||
|
|
||||||
|
assert refs.symref("HEAD") == "refs/heads/main"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("reftable", [False, True])
|
@pytest.mark.parametrize("reftable", [False, True])
|
||||||
def test_updates_when_refs_change(tmp_path, reftable):
|
def test_updates_when_refs_change(tmp_path, reftable):
|
||||||
if reftable and not utils_for_test.supports_reftable():
|
if reftable and not utils_for_test.supports_reftable():
|
||||||
|
|||||||
@@ -542,14 +542,15 @@ class SuperprojectTestCase(unittest.TestCase):
|
|||||||
with mock.patch(
|
with mock.patch(
|
||||||
"git_superproject.GitCommand", autospec=True
|
"git_superproject.GitCommand", autospec=True
|
||||||
) as mock_git_command:
|
) as mock_git_command:
|
||||||
with mock.patch(
|
with mock.patch.object(
|
||||||
"git_superproject.GitRefs.get", autospec=True
|
self._superproject, "_GetRef"
|
||||||
) as mock_git_refs:
|
) as get_ref:
|
||||||
instance = mock_git_command.return_value
|
instance = mock_git_command.return_value
|
||||||
instance.Wait.return_value = 0
|
instance.Wait.return_value = 0
|
||||||
mock_git_refs.side_effect = ["", "1234"]
|
get_ref.side_effect = ["", "1234"]
|
||||||
|
|
||||||
self.assertTrue(self._superproject._Fetch())
|
self.assertTrue(self._superproject._Fetch())
|
||||||
|
get_ref.assert_called_with("refs/heads/main")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
# TODO: Once we require Python 3.8+,
|
# TODO: Once we require Python 3.8+,
|
||||||
# use 'mock_git_command.call_args.args'.
|
# use 'mock_git_command.call_args.args'.
|
||||||
@@ -572,6 +573,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
# If branch for revision exists, set as --negotiation-tip.
|
# If branch for revision exists, set as --negotiation-tip.
|
||||||
self.assertTrue(self._superproject._Fetch())
|
self.assertTrue(self._superproject._Fetch())
|
||||||
|
get_ref.assert_called_with("refs/heads/main")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
# TODO: Once we require Python 3.8+,
|
# TODO: Once we require Python 3.8+,
|
||||||
# use 'mock_git_command.call_args.args'.
|
# use 'mock_git_command.call_args.args'.
|
||||||
@@ -593,3 +595,21 @@ class SuperprojectTestCase(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_GetRef_resolves_only_the_requested_ref(self) -> None:
|
||||||
|
command = mock.MagicMock(stdout="1234\n")
|
||||||
|
command.Wait.return_value = 0
|
||||||
|
with mock.patch(
|
||||||
|
"git_superproject.GitCommand", return_value=command
|
||||||
|
) as git_command:
|
||||||
|
self.assertEqual("1234", self._superproject._GetRef("HEAD"))
|
||||||
|
|
||||||
|
git_command.assert_called_once_with(
|
||||||
|
None,
|
||||||
|
["rev-parse", "--verify", "--quiet", "HEAD"],
|
||||||
|
gitdir=self._superproject._work_git,
|
||||||
|
bare=True,
|
||||||
|
capture_stdout=True,
|
||||||
|
capture_stderr=True,
|
||||||
|
log_as_error=False,
|
||||||
|
)
|
||||||
|
|||||||
@@ -315,6 +315,7 @@ def test_data_event_config(event_log: git_trace2_event_log.EventLog) -> None:
|
|||||||
"repo.partialclone": "false",
|
"repo.partialclone": "false",
|
||||||
"repo.syncstate.superproject.hassuperprojecttag": "true",
|
"repo.syncstate.superproject.hassuperprojecttag": "true",
|
||||||
"repo.syncstate.superproject.sys.argv": ["--", "sync", "protobuf"],
|
"repo.syncstate.superproject.sys.argv": ["--", "sync", "protobuf"],
|
||||||
|
"repo.syncstate.emptykey": "",
|
||||||
}
|
}
|
||||||
prefix_value = "prefix"
|
prefix_value = "prefix"
|
||||||
event_log.LogDataConfigEvents(config, prefix_value)
|
event_log.LogDataConfigEvents(config, prefix_value)
|
||||||
@@ -323,7 +324,7 @@ def test_data_event_config(event_log: git_trace2_event_log.EventLog) -> None:
|
|||||||
log_path = event_log.Write(path=tempdir)
|
log_path = event_log.Write(path=tempdir)
|
||||||
log_data = read_log(log_path)
|
log_data = read_log(log_path)
|
||||||
|
|
||||||
assert len(log_data) == 5
|
assert len(log_data) == 6
|
||||||
data_events = log_data[1:]
|
data_events = log_data[1:]
|
||||||
verify_common_keys(log_data[0], expected_event_name="version")
|
verify_common_keys(log_data[0], expected_event_name="version")
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"""Unittests for the hooks.py module."""
|
"""Unittests for the hooks.py module."""
|
||||||
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -105,3 +106,98 @@ def test_post_sync_argument_validation() -> None:
|
|||||||
|
|
||||||
finally:
|
finally:
|
||||||
sys.stderr = old_stderr
|
sys.stderr = old_stderr
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("yes_val", (True, False))
|
||||||
|
def test_repo_upload_yes_arg(tmp_path: Path, yes_val: bool) -> None:
|
||||||
|
"""Test that yes is passed in kwargs during hook execution."""
|
||||||
|
|
||||||
|
class FakeProject:
|
||||||
|
def __init__(self, worktree: str) -> None:
|
||||||
|
self.worktree = worktree
|
||||||
|
self.enabled_repo_hooks = ["pre-upload"]
|
||||||
|
self.config = None
|
||||||
|
|
||||||
|
hook_file = tmp_path / "pre-upload.py"
|
||||||
|
|
||||||
|
hook_content = """
|
||||||
|
def main(project_list, **kwargs):
|
||||||
|
project_list.append(kwargs.get("yes"))
|
||||||
|
"""
|
||||||
|
hook_file.write_text(hook_content)
|
||||||
|
|
||||||
|
hook = hooks.RepoHook(
|
||||||
|
hook_type="pre-upload",
|
||||||
|
hooks_project=FakeProject(str(tmp_path)),
|
||||||
|
repo_topdir=str(tmp_path),
|
||||||
|
manifest_url="https://gerrit",
|
||||||
|
allow_all_hooks=True,
|
||||||
|
yes=yes_val,
|
||||||
|
)
|
||||||
|
|
||||||
|
project_list = []
|
||||||
|
res = hook.Run(project_list=project_list, worktree_list=[])
|
||||||
|
|
||||||
|
assert res is True
|
||||||
|
assert project_list == [yes_val]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("fix_val", (True, False))
|
||||||
|
def test_repo_upload_fix_arg(tmp_path: Path, fix_val: bool) -> None:
|
||||||
|
"""Test that fix is passed in kwargs during hook execution."""
|
||||||
|
|
||||||
|
class FakeProject:
|
||||||
|
def __init__(self, worktree: str) -> None:
|
||||||
|
self.worktree = worktree
|
||||||
|
self.enabled_repo_hooks = ["pre-upload"]
|
||||||
|
self.config = None
|
||||||
|
|
||||||
|
hook_file = tmp_path / "pre-upload.py"
|
||||||
|
|
||||||
|
hook_content = """
|
||||||
|
def main(project_list, **kwargs):
|
||||||
|
project_list.append(kwargs.get("fix"))
|
||||||
|
"""
|
||||||
|
hook_file.write_text(hook_content)
|
||||||
|
|
||||||
|
hook = hooks.RepoHook(
|
||||||
|
hook_type="pre-upload",
|
||||||
|
hooks_project=FakeProject(str(tmp_path)),
|
||||||
|
repo_topdir=str(tmp_path),
|
||||||
|
manifest_url="https://gerrit",
|
||||||
|
allow_all_hooks=True,
|
||||||
|
fix=fix_val,
|
||||||
|
)
|
||||||
|
|
||||||
|
project_list = []
|
||||||
|
res = hook.Run(project_list=project_list, worktree_list=[])
|
||||||
|
|
||||||
|
assert res is True
|
||||||
|
assert project_list == [fix_val]
|
||||||
|
|
||||||
|
|
||||||
|
def test_from_subcmd_without_fix_option() -> None:
|
||||||
|
"""Test that FromSubcmd works when opt does not have fix attribute."""
|
||||||
|
|
||||||
|
class Remote:
|
||||||
|
url = "https://gerrit"
|
||||||
|
|
||||||
|
class FakeManifest:
|
||||||
|
repo_hooks_project = None
|
||||||
|
topdir = "/fake/topdir"
|
||||||
|
|
||||||
|
class manifestProject:
|
||||||
|
@staticmethod
|
||||||
|
def GetRemote(name: str) -> "Remote":
|
||||||
|
return Remote()
|
||||||
|
|
||||||
|
class contactinfo:
|
||||||
|
bugurl = "https://bugs"
|
||||||
|
|
||||||
|
class FakeOpt:
|
||||||
|
bypass_hooks = False
|
||||||
|
allow_all_hooks = False
|
||||||
|
ignore_hooks = False
|
||||||
|
|
||||||
|
hook = hooks.RepoHook.FromSubcmd(FakeManifest(), FakeOpt(), "post-sync")
|
||||||
|
assert hook._fix is False
|
||||||
|
|||||||
@@ -819,6 +819,26 @@ class TestProjectElement:
|
|||||||
str(repo_client.topdir), ".repo", "projects", "..git"
|
str(repo_client.topdir), ".repo", "projects", "..git"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_get_project_paths_local_gitdirs(
|
||||||
|
self, repo_client: RepoClient
|
||||||
|
) -> None:
|
||||||
|
"""Check GetProjectPaths with UseLocalGitDirs."""
|
||||||
|
manifest = repo_client.get_xml_manifest(
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?><manifest></manifest>'
|
||||||
|
)
|
||||||
|
manifest.manifestProject.config.SetBoolean("repo.uselocalgitdirs", True)
|
||||||
|
|
||||||
|
relpath, worktree, gitdir, objdir, use_git_worktrees = (
|
||||||
|
manifest.GetProjectPaths("foo", "bar", "origin")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert os.path.normpath(gitdir) == os.path.normpath(
|
||||||
|
os.path.join(str(repo_client.topdir), "bar", ".git")
|
||||||
|
)
|
||||||
|
assert os.path.normpath(objdir) == os.path.normpath(
|
||||||
|
os.path.join(str(repo_client.topdir), "bar", ".git")
|
||||||
|
)
|
||||||
|
|
||||||
def test_bad_path_name_checks(self, repo_client: RepoClient) -> None:
|
def test_bad_path_name_checks(self, repo_client: RepoClient) -> None:
|
||||||
"""Check handling of bad path & name attributes."""
|
"""Check handling of bad path & name attributes."""
|
||||||
|
|
||||||
|
|||||||
+1940
-50
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
|||||||
|
# Copyright (C) 2026 The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Unittests for subcmds/cherry_pick.py."""
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from error import GitError
|
||||||
|
from git_command import GitCommand
|
||||||
|
from subcmds import cherry_pick
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_reference_uses_one_typed_batch_request(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
oid = "1" * 40
|
||||||
|
commit = "tree " + "2" * 40 + "\n\nSubject 🚀\n\nBody\n"
|
||||||
|
output = oid + " commit " + str(len(commit.encode("utf-8"))) + "\n"
|
||||||
|
output += commit + "\n"
|
||||||
|
command = mock.create_autospec(GitCommand, instance=True)
|
||||||
|
command.stdout = output
|
||||||
|
command.stderr = ""
|
||||||
|
command.Wait.return_value = 0
|
||||||
|
run_git = mock.create_autospec(GitCommand, return_value=command)
|
||||||
|
monkeypatch.setattr(cherry_pick, "GitCommand", run_git)
|
||||||
|
|
||||||
|
resolved, contents = cherry_pick.CherryPick()._ResolveReference("topic")
|
||||||
|
|
||||||
|
assert resolved == oid
|
||||||
|
assert contents == commit
|
||||||
|
run_git.assert_called_once_with(
|
||||||
|
None,
|
||||||
|
["cat-file", "--batch"],
|
||||||
|
input="topic^{commit}\n",
|
||||||
|
capture_stdout=True,
|
||||||
|
capture_stderr=True,
|
||||||
|
verify_command=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_reference_rejects_missing_object(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
command = mock.create_autospec(GitCommand, instance=True)
|
||||||
|
command.stdout = "topic^{commit} missing\n"
|
||||||
|
command.stderr = ""
|
||||||
|
command.Wait.return_value = 0
|
||||||
|
monkeypatch.setattr(
|
||||||
|
cherry_pick,
|
||||||
|
"GitCommand",
|
||||||
|
mock.create_autospec(GitCommand, return_value=command),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(GitError, match="commit topic not found"):
|
||||||
|
cherry_pick.CherryPick()._ResolveReference("topic")
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_reference_rejects_ambiguous_object(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
command = mock.create_autospec(GitCommand, instance=True)
|
||||||
|
command.stdout = "topic^{commit} ambiguous\n"
|
||||||
|
command.stderr = ""
|
||||||
|
command.Wait.return_value = 0
|
||||||
|
monkeypatch.setattr(
|
||||||
|
cherry_pick,
|
||||||
|
"GitCommand",
|
||||||
|
mock.create_autospec(GitCommand, return_value=command),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(GitError, match="commit topic not found"):
|
||||||
|
cherry_pick.CherryPick()._ResolveReference("topic")
|
||||||
@@ -17,12 +17,10 @@
|
|||||||
import contextlib
|
import contextlib
|
||||||
import io
|
import io
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
import utils_for_test
|
import utils_for_test
|
||||||
|
|
||||||
import manifest_xml
|
import manifest_xml
|
||||||
import project
|
|
||||||
import subcmds
|
import subcmds
|
||||||
|
|
||||||
|
|
||||||
@@ -87,13 +85,16 @@ def test_forall_all_projects_called_once(tmp_path: Path) -> None:
|
|||||||
opts, args = cmd.OptionParser.parse_args(["-c", "echo $REPO_PROJECT"])
|
opts, args = cmd.OptionParser.parse_args(["-c", "echo $REPO_PROJECT"])
|
||||||
opts.verbose = False
|
opts.verbose = False
|
||||||
|
|
||||||
|
# Set revisionId directly so GetRevisionId() short-circuits without
|
||||||
|
# touching git. Using mock.patch.object on the class does not work
|
||||||
|
# with Python 3.14+, which defaults to "forkserver" on Linux —
|
||||||
|
# class-level patches do not survive into forkserver worker processes.
|
||||||
|
for proj in manifest.projects:
|
||||||
|
proj.revisionId = "refs/heads/main"
|
||||||
|
|
||||||
with contextlib.redirect_stdout(io.StringIO()) as stdout:
|
with contextlib.redirect_stdout(io.StringIO()) as stdout:
|
||||||
# Mock to not have the Execute fail on remote check.
|
# Run the forall command.
|
||||||
with mock.patch.object(
|
cmd.Execute(opts, args)
|
||||||
project.Project, "GetRevisionId", return_value="refs/heads/main"
|
|
||||||
):
|
|
||||||
# Run the forall command.
|
|
||||||
cmd.Execute(opts, args)
|
|
||||||
|
|
||||||
output = stdout.getvalue()
|
output = stdout.getvalue()
|
||||||
# Verify that we got every project name in the output.
|
# Verify that we got every project name in the output.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class GcCommand(unittest.TestCase):
|
|||||||
"""Tests for gc command."""
|
"""Tests for gc command."""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.cmd = gc.Gc()
|
self.cmd = gc.Gc(manifest=mock.MagicMock())
|
||||||
self.opt, self.args = self.cmd.OptionParser.parse_args([])
|
self.opt, self.args = self.cmd.OptionParser.parse_args([])
|
||||||
self.opt.this_manifest_only = False
|
self.opt.this_manifest_only = False
|
||||||
self.opt.repack = False
|
self.opt.repack = False
|
||||||
|
|||||||
@@ -199,3 +199,57 @@ def test_text_enables_pager() -> None:
|
|||||||
cmd = _get_cmd()
|
cmd = _get_cmd()
|
||||||
opts, _ = cmd.OptionParser.parse_args([])
|
opts, _ = cmd.OptionParser.parse_args([])
|
||||||
assert cmd.WantPager(opts)
|
assert cmd.WantPager(opts)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_project_data_uses_head_revision() -> None:
|
||||||
|
"""_getProjectData should use GetHeadRevisionId if available."""
|
||||||
|
cmd = _get_cmd()
|
||||||
|
project = mock.MagicMock()
|
||||||
|
project.name = "foo"
|
||||||
|
project.worktree = "/path/to/foo"
|
||||||
|
project.revisionExpr = "refs/heads/main"
|
||||||
|
project.GetBranches.return_value = []
|
||||||
|
|
||||||
|
# GetHeadRevisionId() returns a SHA, it should be used.
|
||||||
|
project.GetHeadRevisionId.return_value = "head_sha_12345"
|
||||||
|
project.GetRevisionId.return_value = "manifest_sha_54321"
|
||||||
|
|
||||||
|
data = cmd._getProjectData(project)
|
||||||
|
assert data["current_revision"] == "head_sha_12345"
|
||||||
|
project.GetHeadRevisionId.assert_called_once()
|
||||||
|
|
||||||
|
# GetHeadRevisionId() is None, fall back to GetRevisionId().
|
||||||
|
project.GetHeadRevisionId.reset_mock()
|
||||||
|
project.GetHeadRevisionId.return_value = None
|
||||||
|
data = cmd._getProjectData(project)
|
||||||
|
assert data["current_revision"] == "manifest_sha_54321"
|
||||||
|
|
||||||
|
|
||||||
|
def test_json_with_projects(capsys) -> None:
|
||||||
|
"""--format=json should emit project data."""
|
||||||
|
cmd = _get_cmd()
|
||||||
|
opts, args = cmd.OptionParser.parse_args(["--format=json"])
|
||||||
|
opts.jobs = 1 # To avoid multiprocessing pickle issues with mocks
|
||||||
|
|
||||||
|
project = mock.MagicMock()
|
||||||
|
project.name = "foo"
|
||||||
|
project.worktree = "/path/to/foo"
|
||||||
|
project.revisionExpr = "refs/heads/main"
|
||||||
|
project.GetBranches.return_value = {"branch1": mock.MagicMock()}
|
||||||
|
project.GetHeadRevisionId.return_value = "head_sha_12345"
|
||||||
|
project.CurrentBranch = "branch1"
|
||||||
|
|
||||||
|
cmd.GetProjects = mock.MagicMock(return_value=[project])
|
||||||
|
|
||||||
|
cmd.Execute(opts, args)
|
||||||
|
|
||||||
|
data = json.loads(capsys.readouterr().out)
|
||||||
|
assert "projects" in data
|
||||||
|
assert len(data["projects"]) == 1
|
||||||
|
project_data = data["projects"][0]
|
||||||
|
assert project_data["name"] == "foo"
|
||||||
|
assert project_data["mount_path"] == "/path/to/foo"
|
||||||
|
assert project_data["current_revision"] == "head_sha_12345"
|
||||||
|
assert project_data["manifest_revision"] == "refs/heads/main"
|
||||||
|
assert project_data["local_branches"] == ["branch1"]
|
||||||
|
assert project_data["current_branch"] == "branch1"
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Copyright (C) 2026 The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Unittests for the subcmds/rebase.py module."""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import io
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from error import GitError
|
||||||
|
from subcmds import rebase
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_onto_manifest_success() -> None:
|
||||||
|
"""Test _ResolveOntoManifest when ToLocal succeeds."""
|
||||||
|
project = mock.MagicMock()
|
||||||
|
project.revisionExpr = "main"
|
||||||
|
|
||||||
|
remote = mock.MagicMock()
|
||||||
|
remote.ToLocal.return_value = "refs/remotes/goog/main"
|
||||||
|
project.GetRemote.return_value = remote
|
||||||
|
|
||||||
|
res = rebase._ResolveOntoManifest(project)
|
||||||
|
assert res == "refs/remotes/goog/main"
|
||||||
|
project.GetRemote.assert_called_once()
|
||||||
|
remote.ToLocal.assert_called_once_with("main")
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_onto_manifest_fallback() -> None:
|
||||||
|
"""Test _ResolveOntoManifest when ToLocal raises GitError."""
|
||||||
|
project = mock.MagicMock()
|
||||||
|
project.revisionExpr = "main"
|
||||||
|
|
||||||
|
remote = mock.MagicMock()
|
||||||
|
remote.ToLocal.side_effect = GitError("Failed to resolve")
|
||||||
|
project.GetRemote.return_value = remote
|
||||||
|
|
||||||
|
res = rebase._ResolveOntoManifest(project)
|
||||||
|
assert res == "main"
|
||||||
|
project.GetRemote.assert_called_once()
|
||||||
|
remote.ToLocal.assert_called_once_with("main")
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_delegates_autostash_to_rebase() -> None:
|
||||||
|
"""--auto-stash is one rebase process, including staged-only changes."""
|
||||||
|
cmd = rebase.Rebase()
|
||||||
|
cmd.manifest = mock.MagicMock()
|
||||||
|
cmd.git_event_log = mock.MagicMock()
|
||||||
|
project = mock.MagicMock()
|
||||||
|
project.CurrentBranch = "topic"
|
||||||
|
project.RelPath.return_value = "project"
|
||||||
|
branch = mock.MagicMock()
|
||||||
|
branch.LocalMerge = "refs/remotes/origin/main"
|
||||||
|
project.GetBranch.return_value = branch
|
||||||
|
cmd.GetProjects = mock.MagicMock(return_value=[project])
|
||||||
|
opt = SimpleNamespace(
|
||||||
|
interactive=False,
|
||||||
|
fail_fast=False,
|
||||||
|
whitespace=None,
|
||||||
|
quiet=False,
|
||||||
|
force_rebase=False,
|
||||||
|
ff=True,
|
||||||
|
autosquash=False,
|
||||||
|
auto_stash=True,
|
||||||
|
onto_manifest=False,
|
||||||
|
this_manifest_only=False,
|
||||||
|
)
|
||||||
|
git_command = mock.MagicMock()
|
||||||
|
git_command.Wait.return_value = 0
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
rebase, "GitCommand", return_value=git_command
|
||||||
|
) as run_git, contextlib.redirect_stdout(io.StringIO()):
|
||||||
|
assert cmd.Execute(opt, []) == 0
|
||||||
|
|
||||||
|
run_git.assert_called_once_with(
|
||||||
|
project,
|
||||||
|
["rebase", "--autostash", "refs/remotes/origin/main"],
|
||||||
|
)
|
||||||
@@ -107,6 +107,18 @@ def _assert_project_header(line: str, project_path: str, branch: str) -> None:
|
|||||||
assert line == expected
|
assert line == expected
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_project_header_with_ahead_behind(
|
||||||
|
line: str,
|
||||||
|
project_path: str,
|
||||||
|
branch: str,
|
||||||
|
ahead_behind: str,
|
||||||
|
) -> None:
|
||||||
|
"""Assert a status project header line includes ahead/behind info."""
|
||||||
|
suffix = f"branch {branch}{ahead_behind}"
|
||||||
|
expected = f"project {(project_path + '/ '):<40}{suffix}"
|
||||||
|
assert line == expected
|
||||||
|
|
||||||
|
|
||||||
def _assert_orphan_block(lines: List[str], expected: List[str]) -> None:
|
def _assert_orphan_block(lines: List[str], expected: List[str]) -> None:
|
||||||
"""Assert orphan block header and entries, independent of entry ordering."""
|
"""Assert orphan block header and entries, independent of entry ordering."""
|
||||||
assert lines
|
assert lines
|
||||||
@@ -218,3 +230,224 @@ def test_empty_status_after_start_shows_started_branch(
|
|||||||
lines = _status_lines(stdout.getvalue())
|
lines = _status_lines(stdout.getvalue())
|
||||||
assert len(lines) == 1
|
assert len(lines) == 1
|
||||||
_assert_project_header(lines[0], project_path, started_branch)
|
_assert_project_header(lines[0], project_path, started_branch)
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_remote_tracking_branch(
|
||||||
|
manifest: manifest_xml.XmlManifest,
|
||||||
|
branch_name: str,
|
||||||
|
) -> None:
|
||||||
|
"""Create a branch tracking a remote ref, like ``repo start``.
|
||||||
|
|
||||||
|
In a real repo checkout, ``repo start`` creates a branch that
|
||||||
|
tracks a remote tracking ref (e.g. refs/remotes/origin/main).
|
||||||
|
This sets up the same config in both the worktree and the
|
||||||
|
project gitdir (where repo reads its config from).
|
||||||
|
"""
|
||||||
|
proj = list(manifest.paths.values())[0]
|
||||||
|
worktree = Path(proj.worktree)
|
||||||
|
proj_gitdir = Path(proj.gitdir)
|
||||||
|
|
||||||
|
# Create the remote tracking ref in the worktree.
|
||||||
|
subprocess.check_call(
|
||||||
|
["git", "update-ref", "refs/remotes/origin/main", "main"],
|
||||||
|
cwd=worktree,
|
||||||
|
)
|
||||||
|
# Create the new branch from main in the worktree.
|
||||||
|
subprocess.check_call(
|
||||||
|
["git", "checkout", "-q", "-b", branch_name, "main"],
|
||||||
|
cwd=worktree,
|
||||||
|
)
|
||||||
|
# Write remote and branch config into the *project gitdir* config,
|
||||||
|
# which is where repo's Project.config reads from.
|
||||||
|
cfg = str(proj_gitdir / "config")
|
||||||
|
subprocess.check_call(
|
||||||
|
[
|
||||||
|
"git",
|
||||||
|
"config",
|
||||||
|
"-f",
|
||||||
|
cfg,
|
||||||
|
"remote.origin.url",
|
||||||
|
"http://localhost/fake",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
subprocess.check_call(
|
||||||
|
[
|
||||||
|
"git",
|
||||||
|
"config",
|
||||||
|
"-f",
|
||||||
|
cfg,
|
||||||
|
"remote.origin.fetch",
|
||||||
|
"+refs/heads/*:refs/remotes/origin/*",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
subprocess.check_call(
|
||||||
|
["git", "config", "-f", cfg, f"branch.{branch_name}.remote", "origin"],
|
||||||
|
)
|
||||||
|
subprocess.check_call(
|
||||||
|
[
|
||||||
|
"git",
|
||||||
|
"config",
|
||||||
|
"-f",
|
||||||
|
cfg,
|
||||||
|
f"branch.{branch_name}.merge",
|
||||||
|
"refs/heads/main",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_branch_ahead_of_upstream(
|
||||||
|
repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest],
|
||||||
|
) -> None:
|
||||||
|
"""Verify status shows [ahead N] for local commits."""
|
||||||
|
topdir, manifest = repo_client_checkout
|
||||||
|
project_path = next(iter(manifest.paths.keys()))
|
||||||
|
project_worktree = topdir / project_path
|
||||||
|
|
||||||
|
_setup_remote_tracking_branch(manifest, "feature")
|
||||||
|
subprocess.check_call(
|
||||||
|
["git", "commit", "-q", "--allow-empty", "-m", "c1"],
|
||||||
|
cwd=project_worktree,
|
||||||
|
)
|
||||||
|
subprocess.check_call(
|
||||||
|
["git", "commit", "-q", "--allow-empty", "-m", "c2"],
|
||||||
|
cwd=project_worktree,
|
||||||
|
)
|
||||||
|
|
||||||
|
with contextlib.redirect_stdout(io.StringIO()) as stdout:
|
||||||
|
_run_status(manifest, [])
|
||||||
|
|
||||||
|
lines = _status_lines(stdout.getvalue())
|
||||||
|
assert len(lines) == 1
|
||||||
|
_assert_project_header_with_ahead_behind(
|
||||||
|
lines[0], project_path, "feature", " [ahead 2]"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_branch_behind_upstream(
|
||||||
|
repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest],
|
||||||
|
) -> None:
|
||||||
|
"""Verify status shows [behind N] when upstream is ahead."""
|
||||||
|
topdir, manifest = repo_client_checkout
|
||||||
|
project_path = next(iter(manifest.paths.keys()))
|
||||||
|
project_worktree = topdir / project_path
|
||||||
|
|
||||||
|
_setup_remote_tracking_branch(manifest, "feature")
|
||||||
|
# Advance the remote tracking ref past the feature branch.
|
||||||
|
subprocess.check_call(
|
||||||
|
["git", "checkout", "-q", "main"], cwd=project_worktree
|
||||||
|
)
|
||||||
|
subprocess.check_call(
|
||||||
|
["git", "commit", "-q", "--allow-empty", "-m", "upstream"],
|
||||||
|
cwd=project_worktree,
|
||||||
|
)
|
||||||
|
subprocess.check_call(
|
||||||
|
["git", "update-ref", "refs/remotes/origin/main", "main"],
|
||||||
|
cwd=project_worktree,
|
||||||
|
)
|
||||||
|
subprocess.check_call(
|
||||||
|
["git", "checkout", "-q", "feature"],
|
||||||
|
cwd=project_worktree,
|
||||||
|
)
|
||||||
|
|
||||||
|
with contextlib.redirect_stdout(io.StringIO()) as stdout:
|
||||||
|
_run_status(manifest, [])
|
||||||
|
|
||||||
|
lines = _status_lines(stdout.getvalue())
|
||||||
|
assert len(lines) == 1
|
||||||
|
_assert_project_header_with_ahead_behind(
|
||||||
|
lines[0], project_path, "feature", " [behind 1]"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_branch_ahead_and_behind(
|
||||||
|
repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest],
|
||||||
|
) -> None:
|
||||||
|
"""Verify [ahead N, behind M] when branch has diverged."""
|
||||||
|
topdir, manifest = repo_client_checkout
|
||||||
|
project_path = next(iter(manifest.paths.keys()))
|
||||||
|
project_worktree = topdir / project_path
|
||||||
|
|
||||||
|
_setup_remote_tracking_branch(manifest, "feature")
|
||||||
|
# Add a local commit on feature.
|
||||||
|
subprocess.check_call(
|
||||||
|
["git", "commit", "-q", "--allow-empty", "-m", "local"],
|
||||||
|
cwd=project_worktree,
|
||||||
|
)
|
||||||
|
# Advance the remote tracking ref independently.
|
||||||
|
subprocess.check_call(
|
||||||
|
["git", "checkout", "-q", "main"], cwd=project_worktree
|
||||||
|
)
|
||||||
|
subprocess.check_call(
|
||||||
|
["git", "commit", "-q", "--allow-empty", "-m", "upstream"],
|
||||||
|
cwd=project_worktree,
|
||||||
|
)
|
||||||
|
subprocess.check_call(
|
||||||
|
["git", "update-ref", "refs/remotes/origin/main", "main"],
|
||||||
|
cwd=project_worktree,
|
||||||
|
)
|
||||||
|
subprocess.check_call(
|
||||||
|
["git", "checkout", "-q", "feature"],
|
||||||
|
cwd=project_worktree,
|
||||||
|
)
|
||||||
|
|
||||||
|
with contextlib.redirect_stdout(io.StringIO()) as stdout:
|
||||||
|
_run_status(manifest, [])
|
||||||
|
|
||||||
|
lines = _status_lines(stdout.getvalue())
|
||||||
|
assert len(lines) == 1
|
||||||
|
_assert_project_header_with_ahead_behind(
|
||||||
|
lines[0],
|
||||||
|
project_path,
|
||||||
|
"feature",
|
||||||
|
" [ahead 1, behind 1]",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_branch_no_tracking_no_ahead_behind(
|
||||||
|
repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest],
|
||||||
|
) -> None:
|
||||||
|
"""Verify no ahead/behind when branch has no upstream."""
|
||||||
|
topdir, manifest = repo_client_checkout
|
||||||
|
project_path = next(iter(manifest.paths.keys()))
|
||||||
|
project_worktree = topdir / project_path
|
||||||
|
|
||||||
|
subprocess.check_call(
|
||||||
|
[
|
||||||
|
"git",
|
||||||
|
"checkout",
|
||||||
|
"-q",
|
||||||
|
"-b",
|
||||||
|
"orphan-branch",
|
||||||
|
"--no-track",
|
||||||
|
"main",
|
||||||
|
],
|
||||||
|
cwd=project_worktree,
|
||||||
|
)
|
||||||
|
subprocess.check_call(
|
||||||
|
["git", "commit", "-q", "--allow-empty", "-m", "c1"],
|
||||||
|
cwd=project_worktree,
|
||||||
|
)
|
||||||
|
|
||||||
|
with contextlib.redirect_stdout(io.StringIO()) as stdout:
|
||||||
|
_run_status(manifest, [])
|
||||||
|
|
||||||
|
lines = _status_lines(stdout.getvalue())
|
||||||
|
assert len(lines) == 1
|
||||||
|
_assert_project_header(lines[0], project_path, "orphan-branch")
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_branch_synced_no_ahead_behind(
|
||||||
|
repo_client_checkout: Tuple[Path, manifest_xml.XmlManifest],
|
||||||
|
) -> None:
|
||||||
|
"""Verify no ahead/behind when branch is fully synced."""
|
||||||
|
topdir, manifest = repo_client_checkout
|
||||||
|
project_path = next(iter(manifest.paths.keys()))
|
||||||
|
|
||||||
|
_setup_remote_tracking_branch(manifest, "synced")
|
||||||
|
|
||||||
|
with contextlib.redirect_stdout(io.StringIO()) as stdout:
|
||||||
|
_run_status(manifest, [])
|
||||||
|
|
||||||
|
lines = _status_lines(stdout.getvalue())
|
||||||
|
assert len(lines) == 1
|
||||||
|
_assert_project_header(lines[0], project_path, "synced")
|
||||||
|
|||||||
+856
-3
@@ -14,10 +14,13 @@
|
|||||||
"""Unittests for the subcmds/sync.py module."""
|
"""Unittests for the subcmds/sync.py module."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import optparse
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
from typing import Dict, List, Optional
|
||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
@@ -26,10 +29,51 @@ import pytest
|
|||||||
import command
|
import command
|
||||||
from error import GitError
|
from error import GitError
|
||||||
from error import RepoExitError
|
from error import RepoExitError
|
||||||
|
import manifest_xml
|
||||||
from project import SyncNetworkHalfResult
|
from project import SyncNetworkHalfResult
|
||||||
from subcmds import sync
|
from subcmds import sync
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cli_args, expected",
|
||||||
|
[
|
||||||
|
([], None),
|
||||||
|
(["--recurse-submodules"], True),
|
||||||
|
(["--no-recurse-submodules"], False),
|
||||||
|
(["--fetch-submodules"], True),
|
||||||
|
(["--no-fetch-submodules"], False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_recurse_submodules_option(cli_args, expected):
|
||||||
|
"""The submodule flags preserve an unset manifest-driven state."""
|
||||||
|
cmd = sync.Sync()
|
||||||
|
|
||||||
|
opts, _ = cmd.OptionParser.parse_args(cli_args)
|
||||||
|
|
||||||
|
assert opts.recurse_submodules is expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"old_flag, new_flag",
|
||||||
|
[
|
||||||
|
("--fetch-submodules", "--recurse-submodules"),
|
||||||
|
("--no-fetch-submodules", "--no-recurse-submodules"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_recurse_submodules_option_deprecation(old_flag, new_flag):
|
||||||
|
"""The old submodule flags warn and direct users to their replacements."""
|
||||||
|
cmd = sync.Sync()
|
||||||
|
|
||||||
|
with mock.patch.object(sync.logger, "warning") as warning:
|
||||||
|
cmd.OptionParser.parse_args([old_flag])
|
||||||
|
|
||||||
|
warning.assert_called_once_with(
|
||||||
|
"%s is deprecated; use %s instead", old_flag, new_flag
|
||||||
|
)
|
||||||
|
|
||||||
|
assert old_flag not in cmd.OptionParser.format_help()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"use_superproject, cli_args, result",
|
"use_superproject, cli_args, result",
|
||||||
[
|
[
|
||||||
@@ -56,6 +100,120 @@ def test_get_current_branch_only(use_superproject, cli_args, result):
|
|||||||
assert cmd._GetCurrentBranchOnly(opts, cmd.manifest) == result
|
assert cmd._GetCurrentBranchOnly(opts, cmd.manifest) == result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cli_args, expected_groups",
|
||||||
|
[
|
||||||
|
([], None),
|
||||||
|
(["-g", "groupA"], "groupA"),
|
||||||
|
(["--groups=groupB,groupC"], "groupB,groupC"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_groups_option_parsing(cli_args, expected_groups):
|
||||||
|
"""Test --groups / -g option parsing."""
|
||||||
|
cmd = sync.Sync()
|
||||||
|
opts, _ = cmd.OptionParser.parse_args(cli_args)
|
||||||
|
assert opts.groups == expected_groups
|
||||||
|
|
||||||
|
|
||||||
|
def _create_manifest_with_groups(topdir: Path) -> manifest_xml.XmlManifest:
|
||||||
|
"""Create a test XmlManifest with projects assigned to various groups."""
|
||||||
|
repodir = topdir / ".repo"
|
||||||
|
manifest_dir = repodir / "manifests"
|
||||||
|
manifest_file = repodir / manifest_xml.MANIFEST_FILE_NAME
|
||||||
|
|
||||||
|
repodir.mkdir(exist_ok=True)
|
||||||
|
manifest_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
gitdir = repodir / "manifests.git"
|
||||||
|
gitdir.mkdir(exist_ok=True)
|
||||||
|
(gitdir / "config").write_text(
|
||||||
|
"""[remote "origin"]
|
||||||
|
url = https://localhost:0/manifest
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
manifest_file.write_text(
|
||||||
|
"""
|
||||||
|
<manifest>
|
||||||
|
<remote name="origin" fetch="http://localhost" />
|
||||||
|
<default remote="origin" revision="refs/heads/main" />
|
||||||
|
<project name="proj_g1" path="path_g1" groups="group1" />
|
||||||
|
<project name="proj_g2" path="path_g2" groups="group2" />
|
||||||
|
<project name="proj_g1_g2" path="path_g1_g2"
|
||||||
|
groups="group1,group2" />
|
||||||
|
<project name="proj_default" path="path_default" />
|
||||||
|
<project name="proj_notdefault" path="path_notdefault"
|
||||||
|
groups="notdefault" />
|
||||||
|
</manifest>
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
for p in [
|
||||||
|
"proj_g1",
|
||||||
|
"proj_g2",
|
||||||
|
"proj_g1_g2",
|
||||||
|
"proj_default",
|
||||||
|
"proj_notdefault",
|
||||||
|
]:
|
||||||
|
(repodir / "projects" / f"{p}.git").mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
return manifest_xml.XmlManifest(str(repodir), str(manifest_file))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cli_args, expected_projects",
|
||||||
|
[
|
||||||
|
(["-g", "group1"], ["proj_g1", "proj_g1_g2"]),
|
||||||
|
(["-g", "group2"], ["proj_g2", "proj_g1_g2"]),
|
||||||
|
(["-g", "group1,group2"], ["proj_g1", "proj_g1_g2", "proj_g2"]),
|
||||||
|
(["-g", "default,-group1"], ["proj_default", "proj_g2"]),
|
||||||
|
([], ["proj_default", "proj_g1", "proj_g1_g2", "proj_g2"]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_sync_groups_manifest_filtering(
|
||||||
|
tmp_path: Path, cli_args, expected_projects
|
||||||
|
):
|
||||||
|
"""Test that repo sync -g selects only matching projects."""
|
||||||
|
manifest = _create_manifest_with_groups(tmp_path)
|
||||||
|
cmd = sync.Sync()
|
||||||
|
cmd.manifest = manifest
|
||||||
|
|
||||||
|
opts, args = cmd.OptionParser.parse_args(cli_args)
|
||||||
|
projects = cmd.GetProjects(args, groups=opts.groups, missing_ok=True)
|
||||||
|
project_names = sorted([p.name for p in projects])
|
||||||
|
assert project_names == sorted(expected_projects)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sync_update_projects_revision_id_respects_groups(tmp_path: Path):
|
||||||
|
"""Test that _UpdateProjectsRevisionId filters projects using opt.groups."""
|
||||||
|
manifest = _create_manifest_with_groups(tmp_path)
|
||||||
|
cmd = sync.Sync()
|
||||||
|
cmd.manifest = manifest
|
||||||
|
|
||||||
|
superproject = mock.MagicMock()
|
||||||
|
superproject.UpdateProjectsRevisionId.return_value = mock.MagicMock(
|
||||||
|
manifest_path=None
|
||||||
|
)
|
||||||
|
manifest._superproject = superproject
|
||||||
|
|
||||||
|
opts, args = cmd.OptionParser.parse_args(["-g", "group1"])
|
||||||
|
opts.verbose = False
|
||||||
|
opts.fetch_submodules = False
|
||||||
|
opts.this_manifest_only = True
|
||||||
|
opts.local_only = False
|
||||||
|
|
||||||
|
with mock.patch.object(
|
||||||
|
cmd, "GetProjects", wraps=cmd.GetProjects
|
||||||
|
) as spy_get_projects:
|
||||||
|
with mock.patch.object(cmd, "ManifestList", return_value=[manifest]):
|
||||||
|
cmd._UpdateProjectsRevisionId(opts, args, {}, manifest)
|
||||||
|
spy_get_projects.assert_called_once()
|
||||||
|
_, kwargs = spy_get_projects.call_args
|
||||||
|
assert kwargs.get("groups") == "group1"
|
||||||
|
|
||||||
|
|
||||||
# Used to patch os.cpu_count() for reliable results.
|
# Used to patch os.cpu_count() for reliable results.
|
||||||
OS_CPU_COUNT = 24
|
OS_CPU_COUNT = 24
|
||||||
|
|
||||||
@@ -335,11 +493,26 @@ class LocalSyncState(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class FakeProject:
|
class FakeProject:
|
||||||
def __init__(self, relpath, name=None, objdir=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
relpath: str,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
objdir: Optional[str] = None,
|
||||||
|
parent: Optional["FakeProject"] = None,
|
||||||
|
is_derived: bool = False,
|
||||||
|
revisionId: Optional[str] = None,
|
||||||
|
gitlink_path: Optional[str] = None,
|
||||||
|
path_prefix: str = "",
|
||||||
|
) -> None:
|
||||||
self.relpath = relpath
|
self.relpath = relpath
|
||||||
|
self.path_prefix = path_prefix
|
||||||
self.name = name or relpath
|
self.name = name or relpath
|
||||||
self.objdir = objdir or relpath
|
self.objdir = objdir or relpath
|
||||||
self.worktree = relpath
|
self.worktree = relpath
|
||||||
|
self.parent = parent
|
||||||
|
self.is_derived = is_derived
|
||||||
|
self.revisionId = revisionId
|
||||||
|
self.gitlink_path = gitlink_path
|
||||||
|
|
||||||
self.use_git_worktrees = False
|
self.use_git_worktrees = False
|
||||||
self.UseAlternates = False
|
self.UseAlternates = False
|
||||||
@@ -348,8 +521,20 @@ class FakeProject:
|
|||||||
self.config = mock.MagicMock()
|
self.config = mock.MagicMock()
|
||||||
self.EnableRepositoryExtension = mock.MagicMock()
|
self.EnableRepositoryExtension = mock.MagicMock()
|
||||||
|
|
||||||
def RelPath(self, local=None):
|
@property
|
||||||
return self.relpath
|
def Derived(self) -> bool:
|
||||||
|
return self.is_derived
|
||||||
|
|
||||||
|
def SetRevision(
|
||||||
|
self, revisionExpr: str, revisionId: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
self.revisionExpr = revisionExpr
|
||||||
|
self.revisionId = revisionId or revisionExpr
|
||||||
|
|
||||||
|
def RelPath(self, local: bool = True) -> str:
|
||||||
|
if local:
|
||||||
|
return self.relpath
|
||||||
|
return os.path.join(self.path_prefix, self.relpath)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"project: {self.relpath}"
|
return f"project: {self.relpath}"
|
||||||
@@ -397,6 +582,218 @@ class SafeCheckoutOrder(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_sibling_submodules_with_shared_parent_are_serialized(self):
|
||||||
|
parent = mock.Mock(worktree="/worktree/parent")
|
||||||
|
other_parent = mock.Mock(worktree="/worktree/other")
|
||||||
|
p_parent = FakeProject("parent")
|
||||||
|
p_other = FakeProject("other")
|
||||||
|
p_parent_sub1 = FakeProject("parent/sub1")
|
||||||
|
p_parent_sub1.parent = parent
|
||||||
|
p_parent_sub2 = FakeProject("parent/sub2")
|
||||||
|
p_parent_sub2.parent = parent
|
||||||
|
p_other_sub = FakeProject("other/sub")
|
||||||
|
p_other_sub.parent = other_parent
|
||||||
|
|
||||||
|
out = sync._SafeCheckoutOrder(
|
||||||
|
[p_parent_sub2, p_other_sub, p_parent, p_parent_sub1, p_other]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
out,
|
||||||
|
[
|
||||||
|
[p_other, p_parent],
|
||||||
|
[p_other_sub, p_parent_sub1],
|
||||||
|
[p_parent_sub2],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_nested_submodules_respect_delayed_parent_level(self):
|
||||||
|
parent = mock.Mock(worktree="/worktree/parent")
|
||||||
|
sub1 = mock.Mock(worktree="/worktree/parent/sub1")
|
||||||
|
sub2 = mock.Mock(worktree="/worktree/parent/sub2")
|
||||||
|
p_parent = FakeProject("parent")
|
||||||
|
p_parent_sub1 = FakeProject("parent/sub1")
|
||||||
|
p_parent_sub1.parent = parent
|
||||||
|
p_parent_sub1_nested = FakeProject("parent/sub1/nested")
|
||||||
|
p_parent_sub1_nested.parent = sub1
|
||||||
|
p_parent_sub2 = FakeProject("parent/sub2")
|
||||||
|
p_parent_sub2.parent = parent
|
||||||
|
p_parent_sub2_nested = FakeProject("parent/sub2/nested")
|
||||||
|
p_parent_sub2_nested.parent = sub2
|
||||||
|
|
||||||
|
out = sync._SafeCheckoutOrder(
|
||||||
|
[
|
||||||
|
p_parent_sub2_nested,
|
||||||
|
p_parent_sub2,
|
||||||
|
p_parent_sub1_nested,
|
||||||
|
p_parent,
|
||||||
|
p_parent_sub1,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
out,
|
||||||
|
[
|
||||||
|
[p_parent],
|
||||||
|
[p_parent_sub1],
|
||||||
|
[p_parent_sub1_nested, p_parent_sub2],
|
||||||
|
[p_parent_sub2_nested],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ParentFirstBatches(unittest.TestCase):
|
||||||
|
def test_no_submodules(self) -> None:
|
||||||
|
p_a = FakeProject("a")
|
||||||
|
p_a_b = FakeProject("a/b")
|
||||||
|
out = sync._ParentFirstBatches([p_a, p_a_b])
|
||||||
|
self.assertEqual(out, [[p_a, p_a_b]])
|
||||||
|
|
||||||
|
def test_submodules_follow_their_parent(self) -> None:
|
||||||
|
p_a = FakeProject("a")
|
||||||
|
p_a_b = FakeProject("a/b", parent=p_a, is_derived=True)
|
||||||
|
p_a_b_c = FakeProject("a/b/c", parent=p_a_b, is_derived=True)
|
||||||
|
out = sync._ParentFirstBatches([p_a_b_c, p_a, p_a_b])
|
||||||
|
self.assertEqual(out, [[p_a], [p_a_b], [p_a_b_c]])
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshDerivedRevisions(unittest.TestCase):
|
||||||
|
def _parent_with_submodules(self, **gitlinks: str) -> FakeProject:
|
||||||
|
p_a = FakeProject("a")
|
||||||
|
p_a.GetSubmoduleRevisions = mock.Mock(return_value=gitlinks)
|
||||||
|
return p_a
|
||||||
|
|
||||||
|
def _submodule(self, parent: FakeProject, path: str) -> FakeProject:
|
||||||
|
return FakeProject(
|
||||||
|
f"a/{path}", parent=parent, is_derived=True, gitlink_path=path
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_reads_each_parent_once(self) -> None:
|
||||||
|
p_a = self._parent_with_submodules(b="beef1234", c="cafe1234")
|
||||||
|
p_a_b = self._submodule(p_a, "b")
|
||||||
|
p_a_c = self._submodule(p_a, "c")
|
||||||
|
|
||||||
|
sync._RefreshDerivedRevisions([p_a, p_a_b, p_a_c])
|
||||||
|
|
||||||
|
p_a.GetSubmoduleRevisions.assert_called_once_with()
|
||||||
|
self.assertEqual(p_a_b.revisionId, "beef1234")
|
||||||
|
self.assertEqual(p_a_c.revisionId, "cafe1234")
|
||||||
|
|
||||||
|
def test_reuses_gitlinks_read_for_an_earlier_level(self) -> None:
|
||||||
|
p_a = self._parent_with_submodules(b="beef1234", c="cafe1234")
|
||||||
|
p_a_b = self._submodule(p_a, "b")
|
||||||
|
p_a_c = self._submodule(p_a, "c")
|
||||||
|
submodule_revisions = {}
|
||||||
|
|
||||||
|
sync._RefreshDerivedRevisions([p_a_b], submodule_revisions)
|
||||||
|
sync._RefreshDerivedRevisions([p_a_c], submodule_revisions)
|
||||||
|
|
||||||
|
p_a.GetSubmoduleRevisions.assert_called_once_with()
|
||||||
|
self.assertEqual(p_a_c.revisionId, "cafe1234")
|
||||||
|
|
||||||
|
def test_tells_apart_projects_with_the_same_path(self) -> None:
|
||||||
|
# Paths are relative to their own (sub)manifest, so two projects can
|
||||||
|
# share one.
|
||||||
|
first = self._parent_with_submodules(b="beef1234")
|
||||||
|
second = self._parent_with_submodules(b="cafe1234")
|
||||||
|
first_sub = self._submodule(first, "b")
|
||||||
|
second_sub = self._submodule(second, "b")
|
||||||
|
submodule_revisions = {}
|
||||||
|
|
||||||
|
sync._RefreshDerivedRevisions([first_sub], submodule_revisions)
|
||||||
|
sync._RefreshDerivedRevisions([second_sub], submodule_revisions)
|
||||||
|
|
||||||
|
self.assertEqual(first_sub.revisionId, "beef1234")
|
||||||
|
self.assertEqual(second_sub.revisionId, "cafe1234")
|
||||||
|
|
||||||
|
def test_ignores_projects_from_the_manifest(self) -> None:
|
||||||
|
p_a = self._parent_with_submodules()
|
||||||
|
|
||||||
|
sync._RefreshDerivedRevisions([p_a])
|
||||||
|
|
||||||
|
p_a.GetSubmoduleRevisions.assert_not_called()
|
||||||
|
|
||||||
|
def test_reports_submodules_removed_from_their_parent(self) -> None:
|
||||||
|
p_a = self._parent_with_submodules(b="beef1234")
|
||||||
|
p_a_c = self._submodule(p_a, "c")
|
||||||
|
|
||||||
|
removed = sync._RefreshDerivedRevisions([p_a, p_a_c])
|
||||||
|
|
||||||
|
self.assertEqual(removed, [p_a_c])
|
||||||
|
|
||||||
|
def test_keeps_submodules_of_an_unreadable_parent(self) -> None:
|
||||||
|
p_a = FakeProject("a")
|
||||||
|
p_a.GetSubmoduleRevisions = mock.Mock(return_value=None)
|
||||||
|
p_a_b = FakeProject(
|
||||||
|
"a/b",
|
||||||
|
parent=p_a,
|
||||||
|
is_derived=True,
|
||||||
|
revisionId="stale",
|
||||||
|
gitlink_path="b",
|
||||||
|
)
|
||||||
|
|
||||||
|
removed = sync._RefreshDerivedRevisions([p_a, p_a_b])
|
||||||
|
|
||||||
|
self.assertEqual(removed, [])
|
||||||
|
self.assertEqual(p_a_b.revisionId, "stale")
|
||||||
|
|
||||||
|
|
||||||
|
class FetchParentFirst(unittest.TestCase):
|
||||||
|
def test_submodules_are_fetched_after_their_parent(self) -> None:
|
||||||
|
cmd = sync.Sync()
|
||||||
|
cmd._fetch_times = mock.Mock()
|
||||||
|
cmd._fetch_times.Get = mock.Mock(return_value=0)
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
p_a = FakeProject("a")
|
||||||
|
|
||||||
|
def fake_read() -> Dict[str, str]:
|
||||||
|
calls.append(("read gitlinks of", p_a.relpath))
|
||||||
|
return {"b": "beef1234"}
|
||||||
|
|
||||||
|
p_a.GetSubmoduleRevisions = mock.Mock(side_effect=fake_read)
|
||||||
|
p_a_b = FakeProject(
|
||||||
|
"a/b", parent=p_a, is_derived=True, gitlink_path="b"
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_fetch(
|
||||||
|
projects: List[FakeProject], *_args: object
|
||||||
|
) -> sync._FetchResult:
|
||||||
|
calls.append(("fetch", [p.relpath for p in projects]))
|
||||||
|
return sync._FetchResult(True, {p.objdir for p in projects})
|
||||||
|
|
||||||
|
opt = mock.Mock(fail_fast=False)
|
||||||
|
with mock.patch.object(cmd, "_Fetch", side_effect=fake_fetch):
|
||||||
|
result = cmd._FetchParentFirst([p_a_b, p_a], opt, None, None, [])
|
||||||
|
|
||||||
|
self.assertTrue(result.success)
|
||||||
|
self.assertEqual(result.projects, {"a", "a/b"})
|
||||||
|
self.assertEqual(
|
||||||
|
calls,
|
||||||
|
[
|
||||||
|
("fetch", ["a"]),
|
||||||
|
("read gitlinks of", "a"),
|
||||||
|
("fetch", ["a/b"]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WithoutProjects(unittest.TestCase):
|
||||||
|
def test_drops_the_unwanted_projects(self) -> None:
|
||||||
|
p_a = FakeProject("a")
|
||||||
|
p_a_b = FakeProject("a/b")
|
||||||
|
self.assertEqual(sync._WithoutProjects([p_a, p_a_b], [p_a_b]), [p_a])
|
||||||
|
self.assertEqual(sync._WithoutProjects([p_a, p_a_b], []), [p_a, p_a_b])
|
||||||
|
|
||||||
|
def test_keeps_projects_with_the_same_path(self) -> None:
|
||||||
|
# Paths are relative to their own (sub)manifest, so two projects can
|
||||||
|
# share one.
|
||||||
|
first = FakeProject("a/b")
|
||||||
|
second = FakeProject("a/b")
|
||||||
|
self.assertEqual(
|
||||||
|
sync._WithoutProjects([first, second], [second]), [first]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Chunksize(unittest.TestCase):
|
class Chunksize(unittest.TestCase):
|
||||||
"""Tests for _chunksize."""
|
"""Tests for _chunksize."""
|
||||||
@@ -732,6 +1129,19 @@ class SyncCommand(unittest.TestCase):
|
|||||||
self.assertIn(self.sync_local_half_error, e.aggregate_errors)
|
self.assertIn(self.sync_local_half_error, e.aggregate_errors)
|
||||||
self.assertIn(self.sync_network_half_error, e.aggregate_errors)
|
self.assertIn(self.sync_network_half_error, e.aggregate_errors)
|
||||||
|
|
||||||
|
def test_groups_passed_to_get_projects(self):
|
||||||
|
"""Ensure Execute passes opt.groups to GetProjects."""
|
||||||
|
self.opt.groups = "my_group"
|
||||||
|
self.opt.mp_update = False
|
||||||
|
with mock.patch.object(self.cmd, "_UpdateRepoProject"):
|
||||||
|
with mock.patch.object(self.cmd, "_ValidateOptionsWithManifest"):
|
||||||
|
with mock.patch.object(self.cmd, "_SyncInterleaved"):
|
||||||
|
with mock.patch.object(self.cmd, "_RunPostSyncHook"):
|
||||||
|
self.cmd.Execute(self.opt, [])
|
||||||
|
self.cmd.GetProjects.assert_called()
|
||||||
|
_, kwargs = self.cmd.GetProjects.call_args
|
||||||
|
self.assertEqual(kwargs.get("groups"), "my_group")
|
||||||
|
|
||||||
|
|
||||||
class SyncUpdateRepoProject(unittest.TestCase):
|
class SyncUpdateRepoProject(unittest.TestCase):
|
||||||
"""Tests for Sync._UpdateRepoProject."""
|
"""Tests for Sync._UpdateRepoProject."""
|
||||||
@@ -929,6 +1339,203 @@ class InterleavedSyncTest(unittest.TestCase):
|
|||||||
|
|
||||||
execute_mock.assert_called_once()
|
execute_mock.assert_called_once()
|
||||||
|
|
||||||
|
def test_interleaved_refreshes_submodule_revision(self) -> None:
|
||||||
|
"""Test submodules are synced at the revision of the fetched parent."""
|
||||||
|
opt, args = self.cmd.OptionParser.parse_args(["--interleaved", "-j4"])
|
||||||
|
opt.quiet = True
|
||||||
|
|
||||||
|
submodule = FakeProject(
|
||||||
|
"projA/sub",
|
||||||
|
name="projA_sub",
|
||||||
|
objdir="objA_sub",
|
||||||
|
parent=self.projA,
|
||||||
|
is_derived=True,
|
||||||
|
revisionId="stale",
|
||||||
|
gitlink_path="sub",
|
||||||
|
)
|
||||||
|
all_projects = [self.projA, submodule]
|
||||||
|
mock.patch.object(
|
||||||
|
self.cmd, "GetProjects", return_value=all_projects
|
||||||
|
).start()
|
||||||
|
|
||||||
|
self.projA.GetSubmoduleRevisions = mock.Mock(
|
||||||
|
return_value={"sub": "fetched"}
|
||||||
|
)
|
||||||
|
|
||||||
|
synced = []
|
||||||
|
|
||||||
|
def execute_side_effect(
|
||||||
|
jobs: int,
|
||||||
|
target: object,
|
||||||
|
work_items: List[List[int]],
|
||||||
|
**kwargs: object,
|
||||||
|
) -> bool:
|
||||||
|
synced_relpaths_set = kwargs["callback"].args[0]
|
||||||
|
projects_in_pass = self.cmd.get_parallel_context()["projects"]
|
||||||
|
for item in work_items:
|
||||||
|
for project_idx in item:
|
||||||
|
project = projects_in_pass[project_idx]
|
||||||
|
synced.append((project.relpath, project.revisionId))
|
||||||
|
synced_relpaths_set.add(project.relpath)
|
||||||
|
return True
|
||||||
|
|
||||||
|
mock.patch.object(
|
||||||
|
self.cmd, "ExecuteInParallel", side_effect=execute_side_effect
|
||||||
|
).start()
|
||||||
|
|
||||||
|
self.cmd._SyncInterleaved(
|
||||||
|
opt,
|
||||||
|
args,
|
||||||
|
[],
|
||||||
|
self.manifest,
|
||||||
|
self.manifest.manifestProject,
|
||||||
|
all_projects,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn(("projA/sub", "fetched"), synced)
|
||||||
|
|
||||||
|
def test_interleaved_skips_removed_submodule(self) -> None:
|
||||||
|
"""Test submodules dropped by their parent are not checked out."""
|
||||||
|
opt, args = self.cmd.OptionParser.parse_args(["--interleaved", "-j4"])
|
||||||
|
opt.quiet = True
|
||||||
|
|
||||||
|
submodule = FakeProject(
|
||||||
|
"projA/sub",
|
||||||
|
name="projA_sub",
|
||||||
|
objdir="objA_sub",
|
||||||
|
parent=self.projA,
|
||||||
|
is_derived=True,
|
||||||
|
revisionId="stale",
|
||||||
|
gitlink_path="sub",
|
||||||
|
)
|
||||||
|
# The parent no longer holds a gitlink for the submodule.
|
||||||
|
self.projA.GetSubmoduleRevisions = mock.Mock(return_value={})
|
||||||
|
# The reloaded manifest no longer derives the removed submodule.
|
||||||
|
mock.patch.object(
|
||||||
|
self.cmd, "GetProjects", return_value=[self.projA]
|
||||||
|
).start()
|
||||||
|
|
||||||
|
synced = []
|
||||||
|
|
||||||
|
def execute_side_effect(
|
||||||
|
jobs: int,
|
||||||
|
target: object,
|
||||||
|
work_items: List[List[int]],
|
||||||
|
**kwargs: object,
|
||||||
|
) -> bool:
|
||||||
|
synced_relpaths_set = kwargs["callback"].args[0]
|
||||||
|
projects_in_pass = self.cmd.get_parallel_context()["projects"]
|
||||||
|
for item in work_items:
|
||||||
|
for project_idx in item:
|
||||||
|
project = projects_in_pass[project_idx]
|
||||||
|
synced.append(project.relpath)
|
||||||
|
synced_relpaths_set.add(project.relpath)
|
||||||
|
return True
|
||||||
|
|
||||||
|
mock.patch.object(
|
||||||
|
self.cmd, "ExecuteInParallel", side_effect=execute_side_effect
|
||||||
|
).start()
|
||||||
|
|
||||||
|
self.cmd._SyncInterleaved(
|
||||||
|
opt,
|
||||||
|
args,
|
||||||
|
[],
|
||||||
|
self.manifest,
|
||||||
|
self.manifest.manifestProject,
|
||||||
|
[self.projA, submodule],
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(synced, ["projA"])
|
||||||
|
|
||||||
|
def _make_syncable(self, project: FakeProject) -> FakeProject:
|
||||||
|
project.Sync_NetworkHalf = mock.Mock(
|
||||||
|
return_value=SyncNetworkHalfResult(error=None, remote_fetched=True)
|
||||||
|
)
|
||||||
|
project.Sync_LocalHalf = mock.Mock()
|
||||||
|
return project
|
||||||
|
|
||||||
|
def _run_interleaved(
|
||||||
|
self,
|
||||||
|
opt: optparse.Values,
|
||||||
|
initial_projects: List[FakeProject],
|
||||||
|
reloaded_projects: List[FakeProject],
|
||||||
|
) -> None:
|
||||||
|
"""Run _SyncInterleaved with the real workers and callback.
|
||||||
|
|
||||||
|
|initial_projects| make up the first pass, |reloaded_projects| every
|
||||||
|
later one, the way reloading the manifest between passes does.
|
||||||
|
"""
|
||||||
|
mock.patch.object(
|
||||||
|
self.cmd, "GetProjects", return_value=reloaded_projects
|
||||||
|
).start()
|
||||||
|
mock.patch.object(self.cmd, "event_log").start()
|
||||||
|
|
||||||
|
def execute_side_effect(
|
||||||
|
jobs: int,
|
||||||
|
target: object,
|
||||||
|
work_items: List[List[int]],
|
||||||
|
**kwargs: object,
|
||||||
|
) -> bool:
|
||||||
|
results = [target(item) for item in work_items]
|
||||||
|
return kwargs["callback"](None, kwargs["output"], results)
|
||||||
|
|
||||||
|
mock.patch.object(
|
||||||
|
self.cmd, "ExecuteInParallel", side_effect=execute_side_effect
|
||||||
|
).start()
|
||||||
|
|
||||||
|
with mock.patch("subcmds.sync.SyncBuffer") as mock_sync_buffer:
|
||||||
|
mock_sync_buffer.return_value.Finish.return_value = True
|
||||||
|
mock_sync_buffer.return_value.errors = []
|
||||||
|
self.cmd._SyncInterleaved(
|
||||||
|
opt,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
self.manifest,
|
||||||
|
self.manifest.manifestProject,
|
||||||
|
initial_projects,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_interleaved_syncs_same_path_projects_of_every_manifest(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
"""Test a project is not skipped because another shares its path."""
|
||||||
|
opt = self._get_opts(["--interleaved", "-j4"])
|
||||||
|
outer = self._make_syncable(
|
||||||
|
FakeProject("foo", name="outer", objdir="a")
|
||||||
|
)
|
||||||
|
sub = self._make_syncable(
|
||||||
|
FakeProject("foo", name="sub", objdir="b", path_prefix="sub")
|
||||||
|
)
|
||||||
|
|
||||||
|
# |sub| is only discovered once the manifest is reloaded after the
|
||||||
|
# first pass has synced |outer|.
|
||||||
|
self._run_interleaved(opt, [outer], [outer, sub])
|
||||||
|
|
||||||
|
outer.Sync_LocalHalf.assert_called_once()
|
||||||
|
sub.Sync_LocalHalf.assert_called_once()
|
||||||
|
|
||||||
|
def test_interleaved_reports_failures_by_a_unique_path(self) -> None:
|
||||||
|
"""Test failing projects are listed by a path that is theirs alone."""
|
||||||
|
opt = self._get_opts(["--interleaved", "-j4"])
|
||||||
|
outer = self._make_syncable(
|
||||||
|
FakeProject("foo", name="outer", objdir="a")
|
||||||
|
)
|
||||||
|
sub = self._make_syncable(
|
||||||
|
FakeProject("foo", name="sub", objdir="b", path_prefix="sub")
|
||||||
|
)
|
||||||
|
sub.Sync_LocalHalf.side_effect = GitError("checkout failed")
|
||||||
|
self.cmd.git_event_log = mock.MagicMock()
|
||||||
|
|
||||||
|
with self.assertRaises(sync.SyncError):
|
||||||
|
self._run_interleaved(opt, [outer, sub], [outer, sub])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.cmd._interleaved_err_checkout_results, ["sub/foo"]
|
||||||
|
)
|
||||||
|
|
||||||
def test_interleaved_shared_objdir_serial(self):
|
def test_interleaved_shared_objdir_serial(self):
|
||||||
"""Test that projects with shared objdir are processed serially."""
|
"""Test that projects with shared objdir are processed serially."""
|
||||||
opt, args = self.cmd.OptionParser.parse_args(["--interleaved", "-j4"])
|
opt, args = self.cmd.OptionParser.parse_args(["--interleaved", "-j4"])
|
||||||
@@ -1023,6 +1630,23 @@ class InterleavedSyncTest(unittest.TestCase):
|
|||||||
project.Sync_NetworkHalf.assert_called_once()
|
project.Sync_NetworkHalf.assert_called_once()
|
||||||
project.Sync_LocalHalf.assert_called_once()
|
project.Sync_LocalHalf.assert_called_once()
|
||||||
|
|
||||||
|
def test_worker_reports_a_path_unique_across_manifests(self) -> None:
|
||||||
|
"""Test _SyncResult.relpath tells apart same-path projects."""
|
||||||
|
project = FakeProject("foo", objdir="objA", path_prefix="sub")
|
||||||
|
self._make_syncable(project)
|
||||||
|
self.mock_context["projects"] = [project]
|
||||||
|
|
||||||
|
for this_manifest_only, expected in ((False, "sub/foo"), (True, "foo")):
|
||||||
|
with self.subTest(this_manifest_only=this_manifest_only):
|
||||||
|
opt = self._get_opts()
|
||||||
|
opt.this_manifest_only = this_manifest_only
|
||||||
|
with mock.patch("subcmds.sync.SyncBuffer") as mock_sync_buffer:
|
||||||
|
mock_sync_buffer.return_value.Finish.return_value = True
|
||||||
|
mock_sync_buffer.return_value.errors = []
|
||||||
|
result_obj = self.cmd._SyncProjectList(opt, [0])
|
||||||
|
|
||||||
|
self.assertEqual(result_obj.results[0].relpath, expected)
|
||||||
|
|
||||||
def test_worker_fetch_fails(self):
|
def test_worker_fetch_fails(self):
|
||||||
"""Test _SyncProjectList with a failed fetch."""
|
"""Test _SyncProjectList with a failed fetch."""
|
||||||
opt = self._get_opts()
|
opt = self._get_opts()
|
||||||
@@ -1454,3 +2078,232 @@ class UpdateAllManifestProjectsTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
mock_sync_to_rev.assert_not_called()
|
mock_sync_to_rev.assert_not_called()
|
||||||
mock_update_manifest.assert_called_once()
|
mock_update_manifest.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
class TestSmartSyncSetupRemoteHelper(unittest.TestCase):
|
||||||
|
"""Tests for _SmartSyncSetup with remote helpers."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.cmd = sync.Sync()
|
||||||
|
self.opt = mock.MagicMock()
|
||||||
|
self.opt.quiet = False
|
||||||
|
self.opt.smart_sync = True
|
||||||
|
self.manifest = mock.MagicMock()
|
||||||
|
self.smart_sync_manifest_path = "/fake/path/to/manifest.xml"
|
||||||
|
|
||||||
|
@mock.patch("shutil.which")
|
||||||
|
@mock.patch("subprocess.Popen")
|
||||||
|
@mock.patch("xmlrpc.client.Server")
|
||||||
|
@mock.patch("subcmds.sync.PersistentTransport")
|
||||||
|
def test_smart_sync_setup_with_helper(
|
||||||
|
self, mock_transport_class, mock_server_class, mock_popen, mock_which
|
||||||
|
):
|
||||||
|
"""Test _SmartSyncSetup when a helper is present and succeeds."""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
self.manifest.manifest_server = (
|
||||||
|
"persistent-https://android-smartsync.corp.google.com/"
|
||||||
|
"manifestserver"
|
||||||
|
)
|
||||||
|
self.manifest.manifest_server_helper = "repo-remote-sso"
|
||||||
|
mock_which.return_value = "/fake/bin/repo-remote-sso"
|
||||||
|
|
||||||
|
# Mock subprocess to return a JSON with status ok and proxy address
|
||||||
|
mock_process = mock.MagicMock()
|
||||||
|
mock_process.communicate.return_value = (
|
||||||
|
'{"status":"ok","message":"http://127.0.0.1:999"}\n',
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
mock_process.returncode = 0
|
||||||
|
mock_popen.return_value = mock_process
|
||||||
|
|
||||||
|
# Mock XML-RPC server call
|
||||||
|
mock_server = mock.MagicMock()
|
||||||
|
mock_server.GetApprovedManifest.return_value = [
|
||||||
|
True,
|
||||||
|
"<manifest></manifest>",
|
||||||
|
]
|
||||||
|
mock_server_class.return_value = mock_server
|
||||||
|
|
||||||
|
# Mock manifest project branch
|
||||||
|
self.cmd._GetBranch = mock.MagicMock(return_value="main")
|
||||||
|
self.cmd._ReloadManifest = mock.MagicMock()
|
||||||
|
|
||||||
|
# Mock open to avoid writing to disk
|
||||||
|
with mock.patch("builtins.open", mock.mock_open()):
|
||||||
|
manifest_name = self.cmd._SmartSyncSetup(
|
||||||
|
self.opt, self.smart_sync_manifest_path, self.manifest
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
mock_which.assert_called_once_with("repo-remote-sso")
|
||||||
|
mock_popen.assert_called_once_with(
|
||||||
|
["repo-remote-sso", self.manifest.manifest_server],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify transport was created with the proxy returned by helper (with
|
||||||
|
# http:// prepended)
|
||||||
|
mock_transport_class.assert_called_once_with(
|
||||||
|
self.manifest.manifest_server, proxy="http://127.0.0.1:999"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify Server was created with the same URL, with persistent- stripped
|
||||||
|
mock_server_class.assert_called_once_with(
|
||||||
|
"https://android-smartsync.corp.google.com/manifestserver",
|
||||||
|
transport=mock_transport_class.return_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(manifest_name, "manifest.xml")
|
||||||
|
|
||||||
|
@mock.patch("shutil.which")
|
||||||
|
@mock.patch("subprocess.Popen")
|
||||||
|
def test_smart_sync_setup_helper_error(self, mock_popen, mock_which):
|
||||||
|
"""Test _SmartSyncSetup when helper returns an error status."""
|
||||||
|
self.manifest.manifest_server = (
|
||||||
|
"http://android-smartsync.corp.google.com/manifestserver"
|
||||||
|
)
|
||||||
|
self.manifest.manifest_server_helper = "repo-remote-sso"
|
||||||
|
mock_which.return_value = "/fake/bin/repo-remote-sso"
|
||||||
|
|
||||||
|
# Mock subprocess to return a JSON with status error
|
||||||
|
mock_process = mock.MagicMock()
|
||||||
|
mock_process.communicate.return_value = (
|
||||||
|
'{"status":"error","message":"uplink-helper failed"}\n',
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
mock_process.returncode = 0
|
||||||
|
mock_popen.return_value = mock_process
|
||||||
|
|
||||||
|
with self.assertRaises(sync.SmartSyncError) as context:
|
||||||
|
self.cmd._SmartSyncSetup(
|
||||||
|
self.opt, self.smart_sync_manifest_path, self.manifest
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
"helper repo-remote-sso returned error: uplink-helper failed",
|
||||||
|
str(context.exception),
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch("shutil.which")
|
||||||
|
def test_smart_sync_setup_missing_declared_helper(self, mock_which):
|
||||||
|
"""Test _SmartSyncSetup when helper declared in manifest is missing."""
|
||||||
|
self.manifest.manifest_server = (
|
||||||
|
"http://android-smartsync.corp.google.com/manifestserver"
|
||||||
|
)
|
||||||
|
self.manifest.manifest_server_helper = "repo-remote-sso"
|
||||||
|
mock_which.return_value = None
|
||||||
|
|
||||||
|
with self.assertRaises(sync.SmartSyncError) as context:
|
||||||
|
self.cmd._SmartSyncSetup(
|
||||||
|
self.opt, self.smart_sync_manifest_path, self.manifest
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
"helper binary 'repo-remote-sso' declared in manifest was not "
|
||||||
|
"found",
|
||||||
|
str(context.exception),
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch("shutil.which")
|
||||||
|
@mock.patch("subprocess.Popen")
|
||||||
|
def test_smart_sync_setup_helper_exit_code_error(
|
||||||
|
self, mock_popen, mock_which
|
||||||
|
):
|
||||||
|
"""Test _SmartSyncSetup when helper exits with non-zero and stderr."""
|
||||||
|
self.manifest.manifest_server = (
|
||||||
|
"http://android-smartsync.corp.google.com/manifestserver"
|
||||||
|
)
|
||||||
|
self.manifest.manifest_server_helper = "repo-remote-sso"
|
||||||
|
mock_which.return_value = "/fake/bin/repo-remote-sso"
|
||||||
|
|
||||||
|
# Mock subprocess: exit code 1, stderr, and no JSON on stdout
|
||||||
|
mock_process = mock.MagicMock()
|
||||||
|
mock_process.communicate.return_value = (
|
||||||
|
"",
|
||||||
|
"internal binary error occurred\n",
|
||||||
|
)
|
||||||
|
mock_process.returncode = 1
|
||||||
|
mock_popen.return_value = mock_process
|
||||||
|
|
||||||
|
with self.assertRaises(sync.SmartSyncError) as context:
|
||||||
|
self.cmd._SmartSyncSetup(
|
||||||
|
self.opt, self.smart_sync_manifest_path, self.manifest
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
"helper repo-remote-sso exited with exit code 1. "
|
||||||
|
"Stderr: internal binary error occurred",
|
||||||
|
str(context.exception),
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch("shutil.which")
|
||||||
|
@mock.patch("subprocess.Popen")
|
||||||
|
def test_smart_sync_setup_helper_json_decode_error_with_stderr(
|
||||||
|
self, mock_popen, mock_which
|
||||||
|
):
|
||||||
|
"""Test _SmartSyncSetup when helper returns invalid JSON and stderr."""
|
||||||
|
self.manifest.manifest_server = (
|
||||||
|
"http://android-smartsync.corp.google.com/manifestserver"
|
||||||
|
)
|
||||||
|
self.manifest.manifest_server_helper = "repo-remote-sso"
|
||||||
|
mock_which.return_value = "/fake/bin/repo-remote-sso"
|
||||||
|
|
||||||
|
mock_process = mock.MagicMock()
|
||||||
|
mock_process.communicate.return_value = (
|
||||||
|
"not a json",
|
||||||
|
"some warning messages\n",
|
||||||
|
)
|
||||||
|
mock_process.returncode = 0
|
||||||
|
mock_popen.return_value = mock_process
|
||||||
|
|
||||||
|
with self.assertRaises(sync.SmartSyncError) as context:
|
||||||
|
self.cmd._SmartSyncSetup(
|
||||||
|
self.opt, self.smart_sync_manifest_path, self.manifest
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
"failed to parse JSON from helper repo-remote-sso",
|
||||||
|
str(context.exception),
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"Stderr was: some warning messages",
|
||||||
|
str(context.exception),
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch("shutil.which")
|
||||||
|
@mock.patch("subprocess.Popen")
|
||||||
|
def test_smart_sync_setup_helper_error_with_stderr(
|
||||||
|
self, mock_popen, mock_which
|
||||||
|
):
|
||||||
|
"""Test _SmartSyncSetup when helper returns error status and stderr."""
|
||||||
|
self.manifest.manifest_server = (
|
||||||
|
"http://android-smartsync.corp.google.com/manifestserver"
|
||||||
|
)
|
||||||
|
self.manifest.manifest_server_helper = "repo-remote-sso"
|
||||||
|
mock_which.return_value = "/fake/bin/repo-remote-sso"
|
||||||
|
|
||||||
|
mock_process = mock.MagicMock()
|
||||||
|
mock_process.communicate.return_value = (
|
||||||
|
'{"status":"error","message":"uplink-helper failed"}\n',
|
||||||
|
"debugging logs\n",
|
||||||
|
)
|
||||||
|
mock_process.returncode = 0
|
||||||
|
mock_popen.return_value = mock_process
|
||||||
|
|
||||||
|
with self.assertRaises(sync.SmartSyncError) as context:
|
||||||
|
self.cmd._SmartSyncSetup(
|
||||||
|
self.opt, self.smart_sync_manifest_path, self.manifest
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
"helper repo-remote-sso returned error: uplink-helper failed",
|
||||||
|
str(context.exception),
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"Stderr was: debugging logs",
|
||||||
|
str(context.exception),
|
||||||
|
)
|
||||||
|
|||||||
@@ -63,3 +63,57 @@ def test_UploadAndReport_UnhandledError(cmd: upload.Upload) -> None:
|
|||||||
with mock.patch.object(cmd, "_UploadBranch", side_effect=UnexpectedError):
|
with mock.patch.object(cmd, "_UploadBranch", side_effect=UnexpectedError):
|
||||||
with pytest.raises(UnexpectedError):
|
with pytest.raises(UnexpectedError):
|
||||||
cmd._UploadAndReport(opt, [mock.MagicMock()], _STUB_PEOPLE)
|
cmd._UploadAndReport(opt, [mock.MagicMock()], _STUB_PEOPLE)
|
||||||
|
|
||||||
|
|
||||||
|
def test_GetMergeBranch_explicit_branch(cmd: upload.Upload) -> None:
|
||||||
|
"""Verify _GetMergeBranch reads branch.merge for explicit local_branch."""
|
||||||
|
mock_project = mock.MagicMock()
|
||||||
|
mock_branch = mock.MagicMock()
|
||||||
|
mock_branch.merge = "refs/heads/main"
|
||||||
|
mock_project.GetBranch.return_value = mock_branch
|
||||||
|
|
||||||
|
res = cmd._GetMergeBranch(mock_project, local_branch="feature")
|
||||||
|
assert res == "refs/heads/main"
|
||||||
|
mock_project.GetBranch.assert_called_once_with("feature")
|
||||||
|
|
||||||
|
|
||||||
|
def test_GetMergeBranch_current_branch(cmd: upload.Upload) -> None:
|
||||||
|
"""Verify _GetMergeBranch falls back to project.CurrentBranch."""
|
||||||
|
mock_project = mock.MagicMock()
|
||||||
|
mock_project.CurrentBranch = "auto-cbr"
|
||||||
|
mock_branch = mock.MagicMock()
|
||||||
|
mock_branch.merge = "refs/heads/upstream-main"
|
||||||
|
mock_project.GetBranch.return_value = mock_branch
|
||||||
|
|
||||||
|
res = cmd._GetMergeBranch(mock_project, local_branch=None)
|
||||||
|
assert res == "refs/heads/upstream-main"
|
||||||
|
mock_project.GetBranch.assert_called_once_with("auto-cbr")
|
||||||
|
|
||||||
|
|
||||||
|
def test_GetMergeBranch_none_when_no_branch(cmd: upload.Upload) -> None:
|
||||||
|
"""Verify _GetMergeBranch returns empty string when detached HEAD."""
|
||||||
|
mock_project = mock.MagicMock()
|
||||||
|
mock_project.CurrentBranch = None
|
||||||
|
|
||||||
|
res = cmd._GetMergeBranch(mock_project, local_branch=None)
|
||||||
|
assert res == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_GatherOne_returns_resolved_current_branch(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Upload error reporting reuses the branch gathered by the worker."""
|
||||||
|
project = mock.MagicMock()
|
||||||
|
project.CurrentBranch = "topic"
|
||||||
|
branch = mock.sentinel.branch
|
||||||
|
project.GetUploadableBranch.return_value = branch
|
||||||
|
monkeypatch.setattr(
|
||||||
|
upload.Upload,
|
||||||
|
"get_parallel_context",
|
||||||
|
lambda: {"projects": [project]},
|
||||||
|
)
|
||||||
|
opt = mock.MagicMock(current_branch=True)
|
||||||
|
|
||||||
|
assert upload.Upload._GatherOne(opt, 0) == (0, [branch], "topic")
|
||||||
|
|
||||||
|
project.GetUploadableBranch.assert_called_once_with("topic")
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Copyright (C) 2026 The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Unittests for subcmds/version.py."""
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from subcmds import version
|
||||||
|
|
||||||
|
|
||||||
|
def test_repo_version_uses_one_pretty_format_call(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
project = mock.MagicMock()
|
||||||
|
project.bare_git.log.return_value = "v2.0-1-g12345678\nTue, 25 Aug\n"
|
||||||
|
monkeypatch.setattr(version, "git_require", lambda _version: True)
|
||||||
|
|
||||||
|
result = version.Version._RepoVersion(project)
|
||||||
|
|
||||||
|
assert result == ("v2.0-1-g12345678", "Tue, 25 Aug")
|
||||||
|
project.bare_git.log.assert_called_once_with(
|
||||||
|
"-1", "--format=%(describe)%n%cD", "HEAD"
|
||||||
|
)
|
||||||
|
project.bare_git.describe.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_repo_version_keeps_old_git_fallback(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
project = mock.MagicMock()
|
||||||
|
project.bare_git.describe.return_value = "v2.0"
|
||||||
|
project.bare_git.log.return_value = "Tue, 25 Aug"
|
||||||
|
monkeypatch.setattr(version, "git_require", lambda _version: False)
|
||||||
|
|
||||||
|
result = version.Version._RepoVersion(project)
|
||||||
|
|
||||||
|
assert result == ("v2.0", "Tue, 25 Aug")
|
||||||
|
project.bare_git.describe.assert_called_once_with("HEAD")
|
||||||
|
project.bare_git.log.assert_called_once_with("-1", "--format=%cD", "HEAD")
|
||||||
Reference in New Issue
Block a user