Merge history of v2.14.5.
The v2.14.[345] releases were cut on a branch based on v2.14.2. We
had some regression fixes we wanted in v2.14, but too many risky
changes landed in main since to cut another v2.14.x directly, and
we didn't want to destabilize even more by pushing a v2.15 right
away. So we branched to keep things healthy.
But people with old checkouts trying to upgrade from those versions
run into an old repo bug where it only selfupdates with fast-forwards,
and repo can't fast-forward from those divergent histories. So let's
do a merge commit to stitch the history back together.
There's no actual changes in here.
Change-Id: I05a96048e3846321e57c5f5224fb8dcf3c191d35
diff --git a/.github/workflows/test-ci.yml b/.github/workflows/test-ci.yml
index ec6f379..1988185 100644
--- a/.github/workflows/test-ci.yml
+++ b/.github/workflows/test-ci.yml
@@ -14,7 +14,7 @@
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
- python-version: [3.5, 3.6, 3.7, 3.8, 3.9]
+ python-version: [3.6, 3.7, 3.8, 3.9]
runs-on: ${{ matrix.os }}
steps:
diff --git a/command.py b/command.py
index 9b1220d..b972a0b 100644
--- a/command.py
+++ b/command.py
@@ -15,7 +15,6 @@
import multiprocessing
import os
import optparse
-import platform
import re
import sys
@@ -25,6 +24,10 @@
import progress
+# Are we generating man-pages?
+GENERATE_MANPAGES = os.environ.get('_REPO_GENERATE_MANPAGES_') == ' indeed! '
+
+
# Number of projects to submit to a single worker process at a time.
# This number represents a tradeoff between the overhead of IPC and finer
# grained opportunity for parallelism. This particular value was chosen by
@@ -43,15 +46,32 @@
"""Base class for any command line action in repo.
"""
- common = False
+ # Singleton for all commands to track overall repo command execution and
+ # provide event summary to callers. Only used by sync subcommand currently.
+ #
+ # NB: This is being replaced by git trace2 events. See git_trace2_event_log.
event_log = EventLog()
- manifest = None
- _optparse = None
+
+ # Whether this command is a "common" one, i.e. whether the user would commonly
+ # use it or it's a more uncommon command. This is used by the help command to
+ # show short-vs-full summaries.
+ COMMON = False
# Whether this command supports running in parallel. If greater than 0,
# it is the number of parallel jobs to default to.
PARALLEL_JOBS = None
+ def __init__(self, repodir=None, client=None, manifest=None, gitc_manifest=None,
+ git_event_log=None):
+ self.repodir = repodir
+ self.client = client
+ self.manifest = manifest
+ self.gitc_manifest = gitc_manifest
+ self.git_event_log = git_event_log
+
+ # Cache for the OptionParser property.
+ self._optparse = None
+
def WantPager(self, _opt):
return False
@@ -106,10 +126,14 @@
help='only show errors')
if self.PARALLEL_JOBS is not None:
+ default = 'based on number of CPU cores'
+ if not GENERATE_MANPAGES:
+ # Only include active cpu count if we aren't generating man pages.
+ default = f'%default; {default}'
p.add_option(
'-j', '--jobs',
type=int, default=self.PARALLEL_JOBS,
- help='number of jobs to run in parallel (default: %s)' % self.PARALLEL_JOBS)
+ help=f'number of jobs to run in parallel (default: {default})')
def _Options(self, p):
"""Initialize the option parser with subcommand-specific options."""
diff --git a/completion.bash b/completion.bash
index 0b52d29..09291d5 100644
--- a/completion.bash
+++ b/completion.bash
@@ -14,6 +14,9 @@
# Programmable bash completion. https://github.com/scop/bash-completion
+# TODO: Handle interspersed options. We handle `repo h<tab>`, but not
+# `repo --time h<tab>`.
+
# Complete the list of repo subcommands.
__complete_repo_list_commands() {
local repo=${COMP_WORDS[0]}
@@ -37,6 +40,7 @@
__complete_repo_list_projects() {
local repo=${COMP_WORDS[0]}
"${repo}" list -n 2>/dev/null
+ "${repo}" list -p --relative-to=. 2>/dev/null
}
# Complete the repo <command> argument.
@@ -66,6 +70,48 @@
COMPREPLY=($(compgen -W "$(__complete_repo_list_projects)" -- "${current}"))
}
+# Complete `repo help`.
+__complete_repo_command_help() {
+ local current=$1
+ # CWORD=1 is "start".
+ # CWORD=2 is the <subcommand> which we complete here.
+ if [[ ${COMP_CWORD} -eq 2 ]]; then
+ COMPREPLY=(
+ $(compgen -W "$(__complete_repo_list_commands)" -- "${current}")
+ )
+ fi
+}
+
+# Complete `repo forall`.
+__complete_repo_command_forall() {
+ local current=$1
+ # CWORD=1 is "forall".
+ # CWORD=2+ are <projects> *until* we hit the -c option.
+ local i
+ for (( i = 0; i < COMP_CWORD; ++i )); do
+ if [[ "${COMP_WORDS[i]}" == "-c" ]]; then
+ return 0
+ fi
+ done
+
+ COMPREPLY=(
+ $(compgen -W "$(__complete_repo_list_projects)" -- "${current}")
+ )
+}
+
+# Complete `repo start`.
+__complete_repo_command_start() {
+ local current=$1
+ # CWORD=1 is "start".
+ # CWORD=2 is the <branch> which we don't complete.
+ # CWORD=3+ are <projects> which we complete here.
+ if [[ ${COMP_CWORD} -gt 2 ]]; then
+ COMPREPLY=(
+ $(compgen -W "$(__complete_repo_list_projects)" -- "${current}")
+ )
+ fi
+}
+
# Complete the repo subcommand arguments.
__complete_repo_arg() {
if [[ ${COMP_CWORD} -le 1 ]]; then
@@ -86,21 +132,8 @@
return 0
;;
- help)
- if [[ ${COMP_CWORD} -eq 2 ]]; then
- COMPREPLY=(
- $(compgen -W "$(__complete_repo_list_commands)" -- "${current}")
- )
- fi
- return 0
- ;;
-
- start)
- if [[ ${COMP_CWORD} -gt 2 ]]; then
- COMPREPLY=(
- $(compgen -W "$(__complete_repo_list_projects)" -- "${current}")
- )
- fi
+ help|start|forall)
+ __complete_repo_command_${command} "${current}"
return 0
;;
@@ -118,4 +151,6 @@
return 0
}
-complete -F __complete_repo repo
+# Fallback to the default complete methods if we aren't able to provide anything
+# useful. This will allow e.g. local paths to be used when it makes sense.
+complete -F __complete_repo -o bashdefault -o default repo
diff --git a/docs/internal-fs-layout.md b/docs/internal-fs-layout.md
index 0c59f98..af6a452 100644
--- a/docs/internal-fs-layout.md
+++ b/docs/internal-fs-layout.md
@@ -110,6 +110,8 @@
[gitsubmodules] with [superprojects].
***
+* `copy-link-files.json`: Tracking file used by `repo sync` to determine when
+ copyfile or linkfile are added or removed and need corresponding updates.
* `project.list`: Tracking file used by `repo sync` to determine when projects
are added or removed and need corresponding updates in the checkout.
* `projects/`: Bare checkouts of every project synced by the manifest. The
@@ -144,12 +146,18 @@
The `.repo/manifests.git/config` file is used to track settings for the entire
repo client checkout.
+
Most settings use the `[repo]` section to avoid conflicts with git.
+
+Everything under `[repo.syncstate.*]` is used to keep track of sync details for logging
+purposes.
+
User controlled settings are initialized when running `repo init`.
| Setting | `repo init` Option | Use/Meaning |
|------------------- |---------------------------|-------------|
| manifest.groups | `--groups` & `--platform` | The manifest groups to sync |
+| manifest.standalone | `--standalone-manifest` | Download manifest as static file instead of creating checkout |
| repo.archive | `--archive` | Use `git archive` for checkouts |
| repo.clonebundle | `--clone-bundle` | Whether the initial sync used clone.bundle explicitly |
| repo.clonefilter | `--clone-filter` | Filter setting when using [partial git clones] |
diff --git a/docs/manifest-format.md b/docs/manifest-format.md
index da83d0d..8e0049b 100644
--- a/docs/manifest-format.md
+++ b/docs/manifest-format.md
@@ -31,11 +31,12 @@
extend-project*,
repo-hooks?,
superproject?,
+ contactinfo?,
include*)>
<!ELEMENT notice (#PCDATA)>
- <!ELEMENT remote EMPTY>
+ <!ELEMENT remote (annotation*)>
<!ATTLIST remote name ID #REQUIRED>
<!ATTLIST remote alias CDATA #IMPLIED>
<!ATTLIST remote fetch CDATA #REQUIRED>
@@ -89,20 +90,26 @@
<!ELEMENT extend-project EMPTY>
<!ATTLIST extend-project name CDATA #REQUIRED>
<!ATTLIST extend-project path CDATA #IMPLIED>
+ <!ATTLIST extend-project dest-path CDATA #IMPLIED>
<!ATTLIST extend-project groups CDATA #IMPLIED>
<!ATTLIST extend-project revision CDATA #IMPLIED>
<!ATTLIST extend-project remote CDATA #IMPLIED>
<!ELEMENT remove-project EMPTY>
<!ATTLIST remove-project name CDATA #REQUIRED>
+ <!ATTLIST remove-project optional CDATA #IMPLIED>
<!ELEMENT repo-hooks EMPTY>
<!ATTLIST repo-hooks in-project CDATA #REQUIRED>
<!ATTLIST repo-hooks enabled-list CDATA #REQUIRED>
- <!ELEMENT superproject (EMPTY)>
- <!ATTLIST superproject name CDATA #REQUIRED>
- <!ATTLIST superproject remote IDREF #IMPLIED>
+ <!ELEMENT superproject EMPTY>
+ <!ATTLIST superproject name CDATA #REQUIRED>
+ <!ATTLIST superproject remote IDREF #IMPLIED>
+ <!ATTLIST superproject revision CDATA #IMPLIED>
+
+ <!ELEMENT contactinfo EMPTY>
+ <!ATTLIST contactinfo bugurl CDATA #REQUIRED>
<!ELEMENT include EMPTY>
<!ATTLIST include name CDATA #REQUIRED>
@@ -331,6 +338,11 @@
Attribute `path`: If specified, limit the change to projects checked out
at the specified path, rather than all projects with the given name.
+Attribute `dest-path`: If specified, a path relative to the top directory
+of the repo client where the Git working directory for this project
+should be placed. This is used to move a project in the checkout by
+overriding the existing `path` setting.
+
Attribute `groups`: List of additional groups to which this project
belongs. Same syntax as the corresponding element of `project`.
@@ -343,12 +355,12 @@
### Element annotation
Zero or more annotation elements may be specified as children of a
-project element. Each element describes a name-value pair that will be
-exported into each project's environment during a 'forall' command,
-prefixed with REPO__. In addition, there is an optional attribute
-"keep" which accepts the case insensitive values "true" (default) or
-"false". This attribute determines whether or not the annotation will
-be kept when exported with the manifest subcommand.
+project or remote element. Each element describes a name-value pair.
+For projects, this name-value pair will be exported into each project's
+environment during a 'forall' command, prefixed with `REPO__`. In addition,
+there is an optional attribute "keep" which accepts the case insensitive values
+"true" (default) or "false". This attribute determines whether or not the
+annotation will be kept when exported with the manifest subcommand.
### Element copyfile
@@ -389,6 +401,9 @@
the user can remove a project, and possibly replace it with their
own definition.
+Attribute `optional`: Set to true to ignore remove-project elements with no
+matching `project` element.
+
### Element repo-hooks
NB: See the [practical documentation](./repo-hooks.md) for using repo hooks.
@@ -405,7 +420,7 @@
### Element superproject
***
- *Note*: This is currently a WIP.
+*Note*: This is currently a WIP.
***
NB: See the [git superprojects documentation](
@@ -424,6 +439,24 @@
Attribute `remote`: Name of a previously defined remote element.
If not supplied the remote given by the default element is used.
+Attribute `revision`: Name of the Git branch the manifest wants
+to track for this superproject. If not supplied the revision given
+by the remote element is used if applicable, else the default
+element is used.
+
+### Element contactinfo
+
+***
+*Note*: This is currently a WIP.
+***
+
+This element is used to let manifest authors self-register contact info.
+It has "bugurl" as a required atrribute. This element can be repeated,
+and any later entries will clobber earlier ones. This would allow manifest
+authors who extend manifests to specify their own contact info.
+
+Attribute `bugurl`: The URL to file a bug against the manifest owner.
+
### Element include
This element provides the capability of including another manifest
@@ -468,6 +501,9 @@
Manifest files stored in `$TOP_DIR/.repo/local_manifests/*.xml` will
be loaded in alphabetical order.
+Projects from local manifest files are added into
+local::<local manifest filename> group.
+
The legacy `$TOP_DIR/.repo/local_manifest.xml` path is no longer supported.
diff --git a/docs/release-process.md b/docs/release-process.md
index 43209eb..f71a411 100644
--- a/docs/release-process.md
+++ b/docs/release-process.md
@@ -83,7 +83,8 @@
* `--repo-rev`: This tells repo which branch to use for the full project.
It defaults to the `stable` branch (`REPO_REV` in the launcher script).
-Whenever `repo sync` is run, repo will check to see if an update is available.
+Whenever `repo sync` is run, repo will, once every 24 hours, see if an update
+is available.
It fetches the latest repo-rev from the repo-url.
Then it verifies that the latest commit in the branch has a valid signed tag
using `git tag -v` (which uses gpg).
@@ -95,6 +96,11 @@
If that tag cannot be verified, it gives up and forces the user to resolve.
+### Force an update
+
+The `repo selfupdate` command can be used to force an immediate update.
+It is not subject to the 24 hour limitation.
+
## Branch management
All development happens on the `main` branch and should generally be stable.
@@ -202,80 +208,132 @@
still support them.
Things in italics are things we used to care about but probably don't anymore.
-| Date | EOL | [Git][rel-g] | [Python][rel-p] | [Ubuntu][rel-u] / [Debian][rel-d] | Git | Python |
-|:--------:|:------------:|--------------|-----------------|-----------------------------------|-----|--------|
-| Oct 2008 | *Oct 2013* | | 2.6.0 | *10.04 Lucid* - 10.10 Maverick / *Squeeze* |
+| Date | EOL | [Git][rel-g] | [Python][rel-p] | [SSH][rel-o] | [Ubuntu][rel-u] / [Debian][rel-d] | Git | Python | SSH |
+|:--------:|:------------:|:------------:|:---------------:|:------------:|-----------------------------------|-----|--------|-----|
+| Apr 2008 | | | | 5.0 |
+| Jun 2008 | | | | 5.1 |
+| Oct 2008 | *Oct 2013* | | 2.6.0 | | *10.04 Lucid* - 10.10 Maverick / *Squeeze* |
| Dec 2008 | *Feb 2009* | | 3.0.0 |
-| Feb 2009 | *Mar 2012* | | | Debian 5 Lenny | 1.5.6.5 | 2.5.2 |
-| Jun 2009 | *Jun 2016* | | 3.1.0 | *10.04 Lucid* - 10.10 Maverick / *Squeeze* |
-| Feb 2010 | *Oct 2012* | 1.7.0 | | *10.04 Lucid* - *12.04 Precise* - 12.10 Quantal |
-| Apr 2010 | *Apr 2015* | | | *10.04 Lucid* | 1.7.0.4 | 2.6.5 3.1.2 |
-| Jul 2010 | *Dec 2019* | | **2.7.0** | 11.04 Natty - **<current>** |
-| Oct 2010 | | | | 10.10 Maverick | 1.7.1 | 2.6.6 3.1.3 |
-| Feb 2011 | *Feb 2016* | | | Debian 6 Squeeze | 1.7.2.5 | 2.6.6 3.1.3 |
-| Apr 2011 | | | | 11.04 Natty | 1.7.4 | 2.7.1 3.2.0 |
-| Oct 2011 | *Feb 2016* | | 3.2.0 | 11.04 Natty - 12.10 Quantal |
-| Oct 2011 | | | | 11.10 Ocelot | 1.7.5.4 | 2.7.2 3.2.2 |
-| Apr 2012 | *Apr 2019* | | | *12.04 Precise* | 1.7.9.5 | 2.7.3 3.2.3 |
-| Sep 2012 | *Sep 2017* | | 3.3.0 | 13.04 Raring - 13.10 Saucy |
-| Oct 2012 | *Dec 2014* | 1.8.0 | | 13.04 Raring - 13.10 Saucy |
-| Oct 2012 | | | | 12.10 Quantal | 1.7.10.4 | 2.7.3 3.2.3 |
-| Apr 2013 | | | | 13.04 Raring | 1.8.1.2 | 2.7.4 3.3.1 |
-| May 2013 | *May 2018* | | | Debian 7 Wheezy | 1.7.10.4 | 2.7.3 3.2.3 |
-| Oct 2013 | | | | 13.10 Saucy | 1.8.3.2 | 2.7.5 3.3.2 |
-| Feb 2014 | *Dec 2014* | **1.9.0** | | **14.04 Trusty** |
-| Mar 2014 | *Mar 2019* | | **3.4.0** | **14.04 Trusty** - 15.10 Wily / **Jessie** |
-| Apr 2014 | **Apr 2022** | | | **14.04 Trusty** | 1.9.1 | 2.7.5 3.4.0 |
+| Feb 2009 | | | | 5.2 |
+| Feb 2009 | *Mar 2012* | | | | Debian 5 Lenny | 1.5.6.5 | 2.5.2 |
+| Jun 2009 | *Jun 2016* | | 3.1.0 | | *10.04 Lucid* - 10.10 Maverick / *Squeeze* |
+| Sep 2009 | | | | 5.3 | *10.04 Lucid* |
+| Feb 2010 | *Oct 2012* | 1.7.0 | | | *10.04 Lucid* - *12.04 Precise* - 12.10 Quantal |
+| Mar 2010 | | | | 5.4 |
+| Apr 2010 | | | | 5.5 | 10.10 Maverick |
+| Apr 2010 | *Apr 2015* | | | | *10.04 Lucid* | 1.7.0.4 | 2.6.5 3.1.2 | 5.3 |
+| Jul 2010 | *Dec 2019* | | *2.7.0* | | 11.04 Natty - *<current>* |
+| Aug 2010 | | | | 5.6 |
+| Oct 2010 | | | | | 10.10 Maverick | 1.7.1 | 2.6.6 3.1.3 | 5.5 |
+| Jan 2011 | | | | 5.7 |
+| Feb 2011 | | | | 5.8 | 11.04 Natty |
+| Feb 2011 | *Feb 2016* | | | | Debian 6 Squeeze | 1.7.2.5 | 2.6.6 3.1.3 |
+| Apr 2011 | | | | | 11.04 Natty | 1.7.4 | 2.7.1 3.2.0 | 5.8 |
+| Sep 2011 | | | | 5.9 | *12.04 Precise* |
+| Oct 2011 | *Feb 2016* | | 3.2.0 | | 11.04 Natty - 12.10 Quantal |
+| Oct 2011 | | | | | 11.10 Ocelot | 1.7.5.4 | 2.7.2 3.2.2 | 5.8 |
+| Apr 2012 | | | | 6.0 | 12.10 Quantal |
+| Apr 2012 | *Apr 2019* | | | | *12.04 Precise* | 1.7.9.5 | 2.7.3 3.2.3 | 5.9 |
+| Aug 2012 | | | | 6.1 | 13.04 Raring |
+| Sep 2012 | *Sep 2017* | | 3.3.0 | | 13.04 Raring - 13.10 Saucy |
+| Oct 2012 | *Dec 2014* | 1.8.0 | | | 13.04 Raring - 13.10 Saucy |
+| Oct 2012 | | | | | 12.10 Quantal | 1.7.10.4 | 2.7.3 3.2.3 | 6.0 |
+| Mar 2013 | | | | 6.2 | 13.10 Saucy |
+| Apr 2013 | | | | | 13.04 Raring | 1.8.1.2 | 2.7.4 3.3.1 | 6.1 |
+| May 2013 | *May 2018* | | | | Debian 7 Wheezy | 1.7.10.4 | 2.7.3 3.2.3 |
+| Sep 2013 | | | | 6.3 |
+| Oct 2013 | | | | | 13.10 Saucy | 1.8.3.2 | 2.7.5 3.3.2 | 6.2 |
+| Nov 2013 | | | | 6.4 |
+| Jan 2014 | | | | 6.5 |
+| Feb 2014 | *Dec 2014* | **1.9.0** | | | *14.04 Trusty* |
+| Mar 2014 | *Mar 2019* | | *3.4.0* | | *14.04 Trusty* - 15.10 Wily / *Jessie* |
+| Mar 2014 | | | | 6.6 | *14.04 Trusty* - 14.10 Utopic |
+| Apr 2014 | *Apr 2022* | | | | *14.04 Trusty* | 1.9.1 | 2.7.5 3.4.0 | 6.6 |
| May 2014 | *Dec 2014* | 2.0.0 |
-| Aug 2014 | *Dec 2014* | **2.1.0** | | 14.10 Utopic - 15.04 Vivid / **Jessie** |
-| Oct 2014 | | | | 14.10 Utopic | 2.1.0 | 2.7.8 3.4.2 |
+| Aug 2014 | *Dec 2014* | *2.1.0* | | | 14.10 Utopic - 15.04 Vivid / *Jessie* |
+| Oct 2014 | | | | 6.7 | 15.04 Vivid |
+| Oct 2014 | | | | | 14.10 Utopic | 2.1.0 | 2.7.8 3.4.2 | 6.6 |
| Nov 2014 | *Sep 2015* | 2.2.0 |
| Feb 2015 | *Sep 2015* | 2.3.0 |
+| Mar 2015 | | | | 6.8 |
| Apr 2015 | *May 2017* | 2.4.0 |
-| Apr 2015 | **Jun 2020** | | | **Debian 8 Jessie** | 2.1.4 | 2.7.9 3.4.2 |
-| Apr 2015 | | | | 15.04 Vivid | 2.1.4 | 2.7.9 3.4.3 |
-| Jul 2015 | *May 2017* | 2.5.0 | | 15.10 Wily |
+| Apr 2015 | *Jun 2020* | | | | *Debian 8 Jessie* | 2.1.4 | 2.7.9 3.4.2 |
+| Apr 2015 | | | | | 15.04 Vivid | 2.1.4 | 2.7.9 3.4.3 | 6.7 |
+| Jul 2015 | *May 2017* | 2.5.0 | | | 15.10 Wily |
+| Jul 2015 | | | | 6.9 | 15.10 Wily |
+| Aug 2015 | | | | 7.0 |
+| Aug 2015 | | | | 7.1 |
| Sep 2015 | *May 2017* | 2.6.0 |
-| Sep 2015 | **Sep 2020** | | **3.5.0** | **16.04 Xenial** - 17.04 Zesty / **Stretch** |
-| Oct 2015 | | | | 15.10 Wily | 2.5.0 | 2.7.9 3.4.3 |
-| Jan 2016 | *Jul 2017* | **2.7.0** | | **16.04 Xenial** |
+| Sep 2015 | *Sep 2020* | | *3.5.0* | | *16.04 Xenial* - 17.04 Zesty / *Stretch* |
+| Oct 2015 | | | | | 15.10 Wily | 2.5.0 | 2.7.9 3.4.3 | 6.9 |
+| Jan 2016 | *Jul 2017* | *2.7.0* | | | *16.04 Xenial* |
+| Feb 2016 | | | | 7.2 | *16.04 Xenial* |
| Mar 2016 | *Jul 2017* | 2.8.0 |
-| Apr 2016 | **Apr 2024** | | | **16.04 Xenial** | 2.7.4 | 2.7.11 3.5.1 |
-| Jun 2016 | *Jul 2017* | 2.9.0 | | 16.10 Yakkety |
+| Apr 2016 | *Apr 2024* | | | | *16.04 Xenial* | 2.7.4 | 2.7.11 3.5.1 | 7.2 |
+| Jun 2016 | *Jul 2017* | 2.9.0 | | | 16.10 Yakkety |
+| Jul 2016 | | | | 7.3 | 16.10 Yakkety |
| Sep 2016 | *Sep 2017* | 2.10.0 |
-| Oct 2016 | | | | 16.10 Yakkety | 2.9.3 | 2.7.11 3.5.1 |
-| Nov 2016 | *Sep 2017* | **2.11.0** | | 17.04 Zesty / **Stretch** |
-| Dec 2016 | **Dec 2021** | | **3.6.0** | 17.10 Artful - **18.04 Bionic** - 18.10 Cosmic |
+| Oct 2016 | | | | | 16.10 Yakkety | 2.9.3 | 2.7.11 3.5.1 | 7.3 |
+| Nov 2016 | *Sep 2017* | *2.11.0* | | | 17.04 Zesty / *Stretch* |
+| Dec 2016 | **Dec 2021** | | **3.6.0** | | 17.10 Artful - **18.04 Bionic** - 18.10 Cosmic |
+| Dec 2016 | | | | 7.4 | 17.04 Zesty / *Debian 9 Stretch* |
| Feb 2017 | *Sep 2017* | 2.12.0 |
-| Apr 2017 | | | | 17.04 Zesty | 2.11.0 | 2.7.13 3.5.3 |
+| Mar 2017 | | | | 7.5 | 17.10 Artful |
+| Apr 2017 | | | | | 17.04 Zesty | 2.11.0 | 2.7.13 3.5.3 | 7.4 |
| May 2017 | *May 2018* | 2.13.0 |
-| Jun 2017 | **Jun 2022** | | | **Debian 9 Stretch** | 2.11.0 | 2.7.13 3.5.3 |
-| Aug 2017 | *Dec 2019* | 2.14.0 | | 17.10 Artful |
+| Jun 2017 | *Jun 2022* | | | | *Debian 9 Stretch* | 2.11.0 | 2.7.13 3.5.3 | 7.4 |
+| Aug 2017 | *Dec 2019* | 2.14.0 | | | 17.10 Artful |
| Oct 2017 | *Dec 2019* | 2.15.0 |
-| Oct 2017 | | | | 17.10 Artful | 2.14.1 | 2.7.14 3.6.3 |
+| Oct 2017 | | | | 7.6 | **18.04 Bionic** |
+| Oct 2017 | | | | | 17.10 Artful | 2.14.1 | 2.7.14 3.6.3 | 7.5 |
| Jan 2018 | *Dec 2019* | 2.16.0 |
-| Apr 2018 | *Dec 2019* | 2.17.0 | | **18.04 Bionic** |
-| Apr 2018 | **Apr 2028** | | | **18.04 Bionic** | 2.17.0 | 2.7.15 3.6.5 |
-| Jun 2018 | *Dec 2019* | 2.18.0 |
-| Jun 2018 | **Jun 2023** | | 3.7.0 | 19.04 Disco - **20.04 Focal** / **Buster** |
-| Sep 2018 | *Dec 2019* | 2.19.0 | | 18.10 Cosmic |
-| Oct 2018 | | | | 18.10 Cosmic | 2.19.1 | 2.7.15 3.6.6 |
-| Dec 2018 | *Dec 2019* | **2.20.0** | | 19.04 Disco / **Buster** |
-| Feb 2019 | *Dec 2019* | 2.21.0 |
-| Apr 2019 | | | | 19.04 Disco | 2.20.1 | 2.7.16 3.7.3 |
+| Apr 2018 | *Mar 2021* | **2.17.0** | | | **18.04 Bionic** |
+| Apr 2018 | | | | 7.7 | 18.10 Cosmic |
+| Apr 2018 | **Apr 2028** | | | | **18.04 Bionic** | 2.17.0 | 2.7.15 3.6.5 | 7.6 |
+| Jun 2018 | *Mar 2021* | 2.18.0 |
+| Jun 2018 | **Jun 2023** | | 3.7.0 | | 19.04 Disco - **20.04 Focal** / **Buster** |
+| Aug 2018 | | | | 7.8 |
+| Sep 2018 | *Mar 2021* | 2.19.0 | | | 18.10 Cosmic |
+| Oct 2018 | | | | 7.9 | 19.04 Disco / **Buster** |
+| Oct 2018 | | | | | 18.10 Cosmic | 2.19.1 | 2.7.15 3.6.6 | 7.7 |
+| Dec 2018 | *Mar 2021* | **2.20.0** | | | 19.04 Disco - 19.10 Eoan / **Buster** |
+| Feb 2019 | *Mar 2021* | 2.21.0 |
+| Apr 2019 | | | | 8.0 | 19.10 Eoan |
+| Apr 2019 | | | | | 19.04 Disco | 2.20.1 | 2.7.16 3.7.3 | 7.9 |
| Jun 2019 | | 2.22.0 |
-| Jul 2019 | **Jul 2024** | | | **Debian 10 Buster** | 2.20.1 | 2.7.16 3.7.3 |
-| Aug 2019 | | 2.23.0 |
-| Oct 2019 | **Oct 2024** | | 3.8.0 |
-| Oct 2019 | | | | 19.10 Eoan | 2.20.1 | 2.7.17 3.7.5 |
-| Nov 2019 | | 2.24.0 |
-| Jan 2020 | | 2.25.0 | | **20.04 Focal** |
-| Apr 2020 | **Apr 2030** | | | **20.04 Focal** | 2.25.0 | 2.7.17 3.7.5 |
+| Jul 2019 | **Jul 2024** | | | | **Debian 10 Buster** | 2.20.1 | 2.7.16 3.7.3 | 7.9 |
+| Aug 2019 | *Mar 2021* | 2.23.0 |
+| Oct 2019 | **Oct 2024** | | 3.8.0 | | **20.04 Focal** - 20.10 Groovy |
+| Oct 2019 | | | | 8.1 |
+| Oct 2019 | | | | | 19.10 Eoan | 2.20.1 | 2.7.17 3.7.5 | 8.0 |
+| Nov 2019 | *Mar 2021* | 2.24.0 |
+| Jan 2020 | *Mar 2021* | 2.25.0 | | | **20.04 Focal** |
+| Feb 2020 | | | | 8.2 | **20.04 Focal** |
+| Mar 2020 | *Mar 2021* | 2.26.0 |
+| Apr 2020 | **Apr 2030** | | | | **20.04 Focal** | 2.25.1 | 2.7.17 3.8.2 | 8.2 |
+| May 2020 | *Mar 2021* | 2.27.0 | | | 20.10 Groovy |
+| May 2020 | | | | 8.3 |
+| Jul 2020 | *Mar 2021* | 2.28.0 |
+| Sep 2020 | | | | 8.4 | 21.04 Hirsute / **Bullseye** |
+| Oct 2020 | *Mar 2021* | 2.29.0 |
+| Oct 2020 | | | | | 20.10 Groovy | 2.27.0 | 2.7.18 3.8.6 | 8.3 |
+| Oct 2020 | **Oct 2025** | | 3.9.0 | | 21.04 Hirsute / **Bullseye** |
+| Dec 2020 | *Mar 2021* | 2.30.0 | | | 21.04 Hirsute / **Bullseye** |
+| Mar 2021 | | 2.31.0 |
+| Mar 2021 | | | | 8.5 |
+| Apr 2021 | | | | 8.6 |
+| Apr 2021 | *Jan 2022* | | | | 21.04 Hirsute | 2.30.2 | 2.7.18 3.9.4 | 8.4 |
+| Jun 2021 | | 2.32.0 |
+| Aug 2021 | | 2.33.0 |
+| Aug 2021 | | | | 8.7 |
+| Aug 2021 | **Aug 2026** | | | | **Debian 11 Bullseye** | 2.30.2 | 2.7.18 3.9.2 | 8.4 |
+| **Date** | **EOL** | **[Git][rel-g]** | **[Python][rel-p]** | **[SSH][rel-o]** | **[Ubuntu][rel-u] / [Debian][rel-d]** | **Git** | **Python** | **SSH** |
[contact]: ../README.md#contact
[rel-d]: https://en.wikipedia.org/wiki/Debian_version_history
[rel-g]: https://en.wikipedia.org/wiki/Git#Releases
+[rel-o]: https://www.openssh.com/releasenotes.html
[rel-p]: https://en.wikipedia.org/wiki/History_of_Python#Table_of_versions
[rel-u]: https://en.wikipedia.org/wiki/Ubuntu_version_history#Table_of_versions
[example announcement]: https://groups.google.com/d/topic/repo-discuss/UGBNismWo1M/discussion
diff --git a/error.py b/error.py
index 25ff80d..cbefcb7 100644
--- a/error.py
+++ b/error.py
@@ -13,10 +13,6 @@
# limitations under the License.
-# URL to file bug reports for repo tool issues.
-BUG_REPORT_URL = 'https://bugs.chromium.org/p/gerrit/issues/entry?template=Repo+tool+issue'
-
-
class ManifestParseError(Exception):
"""Failed to parse the manifest file.
"""
diff --git a/fetch.py b/fetch.py
new file mode 100644
index 0000000..91d40cd
--- /dev/null
+++ b/fetch.py
@@ -0,0 +1,41 @@
+# Copyright (C) 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This module contains functions used to fetch files from various sources."""
+
+import subprocess
+import sys
+from urllib.parse import urlparse
+
+def fetch_file(url):
+ """Fetch a file from the specified source using the appropriate protocol.
+
+ Returns:
+ The contents of the file as bytes.
+ """
+ scheme = urlparse(url).scheme
+ if scheme == 'gs':
+ cmd = ['gsutil', 'cat', url]
+ try:
+ result = subprocess.run(
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ return result.stdout
+ except subprocess.CalledProcessError as e:
+ print('fatal: error running "gsutil": %s' % e.output,
+ file=sys.stderr)
+ sys.exit(1)
+ if scheme == 'file':
+ with open(url[len('file://'):], 'rb') as f:
+ return f.read()
+ raise ValueError('unsupported url %s' % url)
diff --git a/git_command.py b/git_command.py
index d06fc77..95db91f 100644
--- a/git_command.py
+++ b/git_command.py
@@ -12,12 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import functools
import os
-import re
import sys
import subprocess
-import tempfile
-from signal import SIGTERM
from error import GitError
from git_refs import HEAD
@@ -42,101 +40,15 @@
LAST_GITDIR = None
LAST_CWD = None
-_ssh_proxy_path = None
-_ssh_sock_path = None
-_ssh_clients = []
-_ssh_version = None
-
-
-def _run_ssh_version():
- """run ssh -V to display the version number"""
- return subprocess.check_output(['ssh', '-V'], stderr=subprocess.STDOUT).decode()
-
-
-def _parse_ssh_version(ver_str=None):
- """parse a ssh version string into a tuple"""
- if ver_str is None:
- ver_str = _run_ssh_version()
- m = re.match(r'^OpenSSH_([0-9.]+)(p[0-9]+)?\s', ver_str)
- if m:
- return tuple(int(x) for x in m.group(1).split('.'))
- else:
- return ()
-
-
-def ssh_version():
- """return ssh version as a tuple"""
- global _ssh_version
- if _ssh_version is None:
- try:
- _ssh_version = _parse_ssh_version()
- except subprocess.CalledProcessError:
- print('fatal: unable to detect ssh version', file=sys.stderr)
- sys.exit(1)
- return _ssh_version
-
-
-def ssh_sock(create=True):
- global _ssh_sock_path
- if _ssh_sock_path is None:
- if not create:
- return None
- tmp_dir = '/tmp'
- if not os.path.exists(tmp_dir):
- tmp_dir = tempfile.gettempdir()
- if ssh_version() < (6, 7):
- tokens = '%r@%h:%p'
- else:
- tokens = '%C' # hash of %l%h%p%r
- _ssh_sock_path = os.path.join(
- tempfile.mkdtemp('', 'ssh-', tmp_dir),
- 'master-' + tokens)
- return _ssh_sock_path
-
-
-def _ssh_proxy():
- global _ssh_proxy_path
- if _ssh_proxy_path is None:
- _ssh_proxy_path = os.path.join(
- os.path.dirname(__file__),
- 'git_ssh')
- return _ssh_proxy_path
-
-
-def _add_ssh_client(p):
- _ssh_clients.append(p)
-
-
-def _remove_ssh_client(p):
- try:
- _ssh_clients.remove(p)
- except ValueError:
- pass
-
-
-def terminate_ssh_clients():
- global _ssh_clients
- for p in _ssh_clients:
- try:
- os.kill(p.pid, SIGTERM)
- p.wait()
- except OSError:
- pass
- _ssh_clients = []
-
-
-_git_version = None
-
class _GitCall(object):
+ @functools.lru_cache(maxsize=None)
def version_tuple(self):
- global _git_version
- if _git_version is None:
- _git_version = Wrapper().ParseGitVersion()
- if _git_version is None:
- print('fatal: unable to detect git version', file=sys.stderr)
- sys.exit(1)
- return _git_version
+ ret = Wrapper().ParseGitVersion()
+ if ret is None:
+ print('fatal: unable to detect git version', file=sys.stderr)
+ sys.exit(1)
+ return ret
def __getattr__(self, name):
name = name.replace('_', '-')
@@ -163,7 +75,8 @@
proj = os.path.dirname(os.path.abspath(__file__))
env[GIT_DIR] = os.path.join(proj, '.git')
result = subprocess.run([GIT, 'describe', HEAD], stdout=subprocess.PIPE,
- encoding='utf-8', env=env, check=False)
+ stderr=subprocess.DEVNULL, encoding='utf-8',
+ env=env, check=False)
if result.returncode == 0:
ver = result.stdout.strip()
if ver.startswith('v'):
@@ -254,7 +167,7 @@
capture_stderr=False,
merge_output=False,
disable_editor=False,
- ssh_proxy=False,
+ ssh_proxy=None,
cwd=None,
gitdir=None):
env = self._GetBasicEnv()
@@ -262,8 +175,8 @@
if disable_editor:
env['GIT_EDITOR'] = ':'
if ssh_proxy:
- env['REPO_SSH_SOCK'] = ssh_sock()
- env['GIT_SSH'] = _ssh_proxy()
+ env['REPO_SSH_SOCK'] = ssh_proxy.sock()
+ env['GIT_SSH'] = ssh_proxy.proxy
env['GIT_SSH_VARIANT'] = 'ssh'
if 'http_proxy' in env and 'darwin' == sys.platform:
s = "'http.proxy=%s'" % (env['http_proxy'],)
@@ -346,7 +259,7 @@
raise GitError('%s: %s' % (command[1], e))
if ssh_proxy:
- _add_ssh_client(p)
+ ssh_proxy.add_client(p)
self.process = p
if input:
@@ -358,7 +271,8 @@
try:
self.stdout, self.stderr = p.communicate()
finally:
- _remove_ssh_client(p)
+ if ssh_proxy:
+ ssh_proxy.remove_client(p)
self.rc = p.wait()
@staticmethod
diff --git a/git_config.py b/git_config.py
index fcd0446..3cd0939 100644
--- a/git_config.py
+++ b/git_config.py
@@ -13,32 +13,28 @@
# limitations under the License.
import contextlib
+import datetime
import errno
from http.client import HTTPException
import json
import os
import re
-import signal
import ssl
import subprocess
import sys
-try:
- import threading as _threading
-except ImportError:
- import dummy_threading as _threading
-import time
import urllib.error
import urllib.request
from error import GitError, UploadError
import platform_utils
from repo_trace import Trace
-
from git_command import GitCommand
-from git_command import ssh_sock
-from git_command import terminate_ssh_clients
from git_refs import R_CHANGES, R_HEADS, R_TAGS
+# Prefix that is prepended to all the keys of SyncAnalysisState's data
+# that is saved in the config.
+SYNC_STATE_PREFIX = 'repo.syncstate.'
+
ID_RE = re.compile(r'^[0-9a-f]{40}$')
REVIEW_CACHE = dict()
@@ -74,6 +70,15 @@
_USER_CONFIG = '~/.gitconfig'
+ _ForSystem = None
+ _SYSTEM_CONFIG = '/etc/gitconfig'
+
+ @classmethod
+ def ForSystem(cls):
+ if cls._ForSystem is None:
+ cls._ForSystem = cls(configfile=cls._SYSTEM_CONFIG)
+ return cls._ForSystem
+
@classmethod
def ForUser(cls):
if cls._ForUser is None:
@@ -99,6 +104,10 @@
os.path.dirname(self.file),
'.repo_' + os.path.basename(self.file) + '.json')
+ def ClearCache(self):
+ """Clear the in-memory cache of config."""
+ self._cache_dict = None
+
def Has(self, name, include_defaults=True):
"""Return true if this configuration file has the key.
"""
@@ -262,6 +271,22 @@
self._branches[b.name] = b
return b
+ def GetSyncAnalysisStateData(self):
+ """Returns data to be logged for the analysis of sync performance."""
+ return {k: v for k, v in self.DumpConfigDict().items() if k.startswith(SYNC_STATE_PREFIX)}
+
+ def UpdateSyncAnalysisState(self, options, superproject_logging_data):
+ """Update Config's SYNC_STATE_PREFIX* data with the latest sync data.
+
+ Args:
+ options: Options passed to sync returned from optparse. See _Options().
+ superproject_logging_data: A dictionary of superproject data that is to be logged.
+
+ Returns:
+ SyncAnalysisState object.
+ """
+ return SyncAnalysisState(self, options, superproject_logging_data)
+
def GetSubSections(self, section):
"""List all subsection names matching $section.*.*
"""
@@ -327,8 +352,8 @@
Trace(': parsing %s', self.file)
with open(self._json) as fd:
return json.load(fd)
- except (IOError, ValueError):
- platform_utils.remove(self._json)
+ except (IOError, ValueErrorl):
+ platform_utils.remove(self._json, missing_ok=True)
return None
def _SaveJson(self, cache):
@@ -336,8 +361,7 @@
with open(self._json, 'w') as fd:
json.dump(cache, fd, indent=2)
except (IOError, TypeError):
- if os.path.exists(self._json):
- platform_utils.remove(self._json)
+ platform_utils.remove(self._json, missing_ok=True)
def _ReadGit(self):
"""
@@ -347,9 +371,10 @@
"""
c = {}
- d = self._do('--null', '--list')
- if d is None:
+ if not os.path.exists(self.file):
return c
+
+ d = self._do('--null', '--list')
for line in d.rstrip('\0').split('\0'):
if '\n' in line:
key, val = line.split('\n', 1)
@@ -365,7 +390,10 @@
return c
def _do(self, *args):
- command = ['config', '--file', self.file, '--includes']
+ if self.file == self._SYSTEM_CONFIG:
+ command = ['config', '--system', '--includes']
+ else:
+ command = ['config', '--file', self.file, '--includes']
command.extend(args)
p = GitCommand(None,
@@ -375,7 +403,7 @@
if p.Wait() == 0:
return p.stdout
else:
- GitError('git config %s: %s' % (str(args), p.stderr))
+ raise GitError('git config %s: %s' % (str(args), p.stderr))
class RepoConfig(GitConfig):
@@ -440,129 +468,6 @@
return s
-_master_processes = []
-_master_keys = set()
-_ssh_master = True
-_master_keys_lock = None
-
-
-def init_ssh():
- """Should be called once at the start of repo to init ssh master handling.
-
- At the moment, all we do is to create our lock.
- """
- global _master_keys_lock
- assert _master_keys_lock is None, "Should only call init_ssh once"
- _master_keys_lock = _threading.Lock()
-
-
-def _open_ssh(host, port=None):
- global _ssh_master
-
- # Bail before grabbing the lock if we already know that we aren't going to
- # try creating new masters below.
- if sys.platform in ('win32', 'cygwin'):
- return False
-
- # Acquire the lock. This is needed to prevent opening multiple masters for
- # the same host when we're running "repo sync -jN" (for N > 1) _and_ the
- # manifest <remote fetch="ssh://xyz"> specifies a different host from the
- # one that was passed to repo init.
- _master_keys_lock.acquire()
- try:
-
- # Check to see whether we already think that the master is running; if we
- # think it's already running, return right away.
- if port is not None:
- key = '%s:%s' % (host, port)
- else:
- key = host
-
- if key in _master_keys:
- return True
-
- if not _ssh_master or 'GIT_SSH' in os.environ:
- # Failed earlier, so don't retry.
- return False
-
- # We will make two calls to ssh; this is the common part of both calls.
- command_base = ['ssh',
- '-o', 'ControlPath %s' % ssh_sock(),
- host]
- if port is not None:
- command_base[1:1] = ['-p', str(port)]
-
- # Since the key wasn't in _master_keys, we think that master isn't running.
- # ...but before actually starting a master, we'll double-check. This can
- # be important because we can't tell that that 'git@myhost.com' is the same
- # as 'myhost.com' where "User git" is setup in the user's ~/.ssh/config file.
- check_command = command_base + ['-O', 'check']
- try:
- Trace(': %s', ' '.join(check_command))
- check_process = subprocess.Popen(check_command,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- check_process.communicate() # read output, but ignore it...
- isnt_running = check_process.wait()
-
- if not isnt_running:
- # Our double-check found that the master _was_ infact running. Add to
- # the list of keys.
- _master_keys.add(key)
- return True
- except Exception:
- # Ignore excpetions. We we will fall back to the normal command and print
- # to the log there.
- pass
-
- command = command_base[:1] + ['-M', '-N'] + command_base[1:]
- try:
- Trace(': %s', ' '.join(command))
- p = subprocess.Popen(command)
- except Exception as e:
- _ssh_master = False
- print('\nwarn: cannot enable ssh control master for %s:%s\n%s'
- % (host, port, str(e)), file=sys.stderr)
- return False
-
- time.sleep(1)
- ssh_died = (p.poll() is not None)
- if ssh_died:
- return False
-
- _master_processes.append(p)
- _master_keys.add(key)
- return True
- finally:
- _master_keys_lock.release()
-
-
-def close_ssh():
- global _master_keys_lock
-
- terminate_ssh_clients()
-
- for p in _master_processes:
- try:
- os.kill(p.pid, signal.SIGTERM)
- p.wait()
- except OSError:
- pass
- del _master_processes[:]
- _master_keys.clear()
-
- d = ssh_sock(create=False)
- if d:
- try:
- platform_utils.rmdir(os.path.dirname(d))
- except OSError:
- pass
-
- # We're done with the lock, so we can delete it.
- _master_keys_lock = None
-
-
-URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):')
URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/')
@@ -614,27 +519,6 @@
yield cookiefile, None
-def _preconnect(url):
- m = URI_ALL.match(url)
- if m:
- scheme = m.group(1)
- host = m.group(2)
- if ':' in host:
- host, port = host.split(':')
- else:
- port = None
- if scheme in ('ssh', 'git+ssh', 'ssh+git'):
- return _open_ssh(host, port)
- return False
-
- m = URI_SCP.match(url)
- if m:
- host = m.group(1)
- return _open_ssh(host)
-
- return False
-
-
class Remote(object):
"""Configuration options related to a remote.
"""
@@ -671,9 +555,23 @@
return self.url.replace(longest, longestUrl, 1)
- def PreConnectFetch(self):
+ def PreConnectFetch(self, ssh_proxy):
+ """Run any setup for this remote before we connect to it.
+
+ In practice, if the remote is using SSH, we'll attempt to create a new
+ SSH master session to it for reuse across projects.
+
+ Args:
+ ssh_proxy: The SSH settings for managing master sessions.
+
+ Returns:
+ Whether the preconnect phase for this remote was successful.
+ """
+ if not ssh_proxy:
+ return True
+
connectionUrl = self._InsteadOf()
- return _preconnect(connectionUrl)
+ return ssh_proxy.preconnect(connectionUrl)
def ReviewUrl(self, userEmail, validate_certs):
if self._review_url is None:
@@ -844,3 +742,70 @@
def _Get(self, key, all_keys=False):
key = 'branch.%s.%s' % (self.name, key)
return self._config.GetString(key, all_keys=all_keys)
+
+
+class SyncAnalysisState:
+ """Configuration options related to logging of sync state for analysis.
+
+ This object is versioned.
+ """
+ def __init__(self, config, options, superproject_logging_data):
+ """Initializes SyncAnalysisState.
+
+ Saves the following data into the |config| object.
+ - sys.argv, options, superproject's logging data.
+ - repo.*, branch.* and remote.* parameters from config object.
+ - Current time as synctime.
+ - Version number of the object.
+
+ All the keys saved by this object are prepended with SYNC_STATE_PREFIX.
+
+ Args:
+ config: GitConfig object to store all options.
+ options: Options passed to sync returned from optparse. See _Options().
+ superproject_logging_data: A dictionary of superproject data that is to be logged.
+ """
+ self._config = config
+ now = datetime.datetime.utcnow()
+ self._Set('main.synctime', now.isoformat() + 'Z')
+ self._Set('main.version', '1')
+ self._Set('sys.argv', sys.argv)
+ for key, value in superproject_logging_data.items():
+ self._Set(f'superproject.{key}', value)
+ for key, value in options.__dict__.items():
+ self._Set(f'options.{key}', value)
+ config_items = config.DumpConfigDict().items()
+ EXTRACT_NAMESPACES = {'repo', 'branch', 'remote'}
+ self._SetDictionary({k: v for k, v in config_items
+ if not k.startswith(SYNC_STATE_PREFIX) and
+ k.split('.', 1)[0] in EXTRACT_NAMESPACES})
+
+ def _SetDictionary(self, data):
+ """Save all key/value pairs of |data| dictionary.
+
+ Args:
+ data: A dictionary whose key/value are to be saved.
+ """
+ for key, value in data.items():
+ self._Set(key, value)
+
+ def _Set(self, key, value):
+ """Set the |value| for a |key| in the |_config| member.
+
+ |key| is prepended with the value of SYNC_STATE_PREFIX constant.
+
+ Args:
+ key: Name of the key.
+ value: |value| could be of any type. If it is 'bool', it will be saved
+ as a Boolean and for all other types, it will be saved as a String.
+ """
+ if value is None:
+ return
+ sync_key = f'{SYNC_STATE_PREFIX}{key}'
+ sync_key = sync_key.replace('_', '')
+ if isinstance(value, str):
+ self._config.SetString(sync_key, value)
+ elif isinstance(value, bool):
+ self._config.SetBoolean(sync_key, value)
+ else:
+ self._config.SetString(sync_key, str(value))
diff --git a/git_superproject.py b/git_superproject.py
index 8932097..4ca84a5 100644
--- a/git_superproject.py
+++ b/git_superproject.py
@@ -19,21 +19,52 @@
Examples:
superproject = Superproject()
- project_commit_ids = superproject.UpdateProjectsRevisionId(projects)
+ UpdateProjectsResult = superproject.UpdateProjectsRevisionId(projects)
"""
import hashlib
+import functools
import os
import sys
+import time
+from typing import NamedTuple
-from error import BUG_REPORT_URL
-from git_command import GitCommand
+from git_command import git_require, GitCommand
+from git_config import RepoConfig
from git_refs import R_HEADS
+from manifest_xml import LOCAL_MANIFEST_GROUP_PREFIX
_SUPERPROJECT_GIT_NAME = 'superproject.git'
_SUPERPROJECT_MANIFEST_NAME = 'superproject_override.xml'
+class SyncResult(NamedTuple):
+ """Return the status of sync and whether caller should exit."""
+
+ # Whether the superproject sync was successful.
+ success: bool
+ # Whether the caller should exit.
+ fatal: bool
+
+
+class CommitIdsResult(NamedTuple):
+ """Return the commit ids and whether caller should exit."""
+
+ # A dictionary with the projects/commit ids on success, otherwise None.
+ commit_ids: dict
+ # Whether the caller should exit.
+ fatal: bool
+
+
+class UpdateProjectsResult(NamedTuple):
+ """Return the overriding manifest file and whether caller should exit."""
+
+ # Path name of the overriding manifest file if successful, otherwise None.
+ manifest_path: str
+ # Whether the caller should exit.
+ fatal: bool
+
+
class Superproject(object):
"""Get commit ids from superproject.
@@ -41,21 +72,25 @@
lookup of commit ids for all projects. It contains _project_commit_ids which
is a dictionary with project/commit id entries.
"""
- def __init__(self, manifest, repodir, superproject_dir='exp-superproject',
- quiet=False):
+ def __init__(self, manifest, repodir, git_event_log,
+ superproject_dir='exp-superproject', quiet=False, print_messages=False):
"""Initializes superproject.
Args:
manifest: A Manifest object that is to be written to a file.
repodir: Path to the .repo/ dir for holding all internal checkout state.
It must be in the top directory of the repo client checkout.
+ git_event_log: A git trace2 event log to log events.
superproject_dir: Relative path under |repodir| to checkout superproject.
quiet: If True then only print the progress messages.
+ print_messages: if True then print error/warning messages.
"""
self._project_commit_ids = None
self._manifest = manifest
+ self._git_event_log = git_event_log
self._quiet = quiet
- self._branch = self._GetBranch()
+ self._print_messages = print_messages
+ self._branch = manifest.branch
self._repodir = os.path.abspath(repodir)
self._superproject_dir = superproject_dir
self._superproject_path = os.path.join(self._repodir, superproject_dir)
@@ -63,8 +98,12 @@
_SUPERPROJECT_MANIFEST_NAME)
git_name = ''
if self._manifest.superproject:
- remote_name = self._manifest.superproject['remote'].name
- git_name = hashlib.md5(remote_name.encode('utf8')).hexdigest() + '-'
+ remote = self._manifest.superproject['remote']
+ git_name = hashlib.md5(remote.name.encode('utf8')).hexdigest() + '-'
+ self._branch = self._manifest.superproject['revision']
+ self._remote_url = remote.url
+ else:
+ self._remote_url = None
self._work_git_name = git_name + _SUPERPROJECT_GIT_NAME
self._work_git = os.path.join(self._superproject_path, self._work_git_name)
@@ -73,16 +112,28 @@
"""Returns a dictionary of projects and their commit ids."""
return self._project_commit_ids
- def _GetBranch(self):
- """Returns the branch name for getting the approved manifest."""
- p = self._manifest.manifestProject
- b = p.GetBranch(p.CurrentBranch)
- if not b:
- return None
- branch = b.merge
- if branch and branch.startswith(R_HEADS):
- branch = branch[len(R_HEADS):]
- return branch
+ @property
+ def manifest_path(self):
+ """Returns the manifest path if the path exists or None."""
+ return self._manifest_path if os.path.exists(self._manifest_path) else None
+
+ def _LogMessage(self, message):
+ """Logs message to stderr and _git_event_log."""
+ if self._print_messages:
+ print(message, file=sys.stderr)
+ self._git_event_log.ErrorEvent(message, f'{message}')
+
+ def _LogMessagePrefix(self):
+ """Returns the prefix string to be logged in each log message"""
+ return f'repo superproject branch: {self._branch} url: {self._remote_url}'
+
+ def _LogError(self, message):
+ """Logs error message to stderr and _git_event_log."""
+ self._LogMessage(f'{self._LogMessagePrefix()} error: {message}')
+
+ def _LogWarning(self, message):
+ """Logs warning message to stderr and _git_event_log."""
+ self._LogMessage(f'{self._LogMessagePrefix()} warning: {message}')
def _Init(self):
"""Sets up a local Git repository to get a copy of a superproject.
@@ -103,25 +154,25 @@
capture_stderr=True)
retval = p.Wait()
if retval:
- print('repo: error: git init call failed with return code: %r, stderr: %r' %
- (retval, p.stderr), file=sys.stderr)
+ self._LogWarning(f'git init call failed, command: git {cmd}, '
+ f'return code: {retval}, stderr: {p.stderr}')
return False
return True
- def _Fetch(self, url):
- """Fetches a local copy of a superproject for the manifest based on url.
-
- Args:
- url: superproject's url.
+ def _Fetch(self):
+ """Fetches a local copy of a superproject for the manifest based on |_remote_url|.
Returns:
True if fetch is successful, or False.
"""
if not os.path.exists(self._work_git):
- print('git fetch missing drectory: %s' % self._work_git,
- file=sys.stderr)
+ self._LogWarning(f'git fetch missing directory: {self._work_git}')
return False
- cmd = ['fetch', url, '--depth', '1', '--force', '--no-tags', '--filter', 'blob:none']
+ if not git_require((2, 28, 0)):
+ self._LogWarning('superproject requires a git version 2.28 or later')
+ return False
+ cmd = ['fetch', self._remote_url, '--depth', '1', '--force', '--no-tags',
+ '--filter', 'blob:none']
if self._branch:
cmd += [self._branch + ':' + self._branch]
p = GitCommand(None,
@@ -131,8 +182,8 @@
capture_stderr=True)
retval = p.Wait()
if retval:
- print('repo: error: git fetch call failed with return code: %r, stderr: %r' %
- (retval, p.stderr), file=sys.stderr)
+ self._LogWarning(f'git fetch call failed, command: git {cmd}, '
+ f'return code: {retval}, stderr: {p.stderr}')
return False
return True
@@ -145,8 +196,7 @@
data: data returned from 'git ls-tree ...' instead of None.
"""
if not os.path.exists(self._work_git):
- print('git ls-tree missing drectory: %s' % self._work_git,
- file=sys.stderr)
+ self._LogWarning(f'git ls-tree missing directory: {self._work_git}')
return None
data = None
branch = 'HEAD' if not self._branch else self._branch
@@ -161,52 +211,52 @@
if retval == 0:
data = p.stdout
else:
- print('repo: error: git ls-tree call failed with return code: %r, stderr: %r' % (
- retval, p.stderr), file=sys.stderr)
+ self._LogWarning(f'git ls-tree call failed, command: git {cmd}, '
+ f'return code: {retval}, stderr: {p.stderr}')
return data
def Sync(self):
"""Gets a local copy of a superproject for the manifest.
Returns:
- True if sync of superproject is successful, or False.
+ SyncResult
"""
- print('WARNING: --use-superproject is experimental and not '
- 'for general use', file=sys.stderr)
-
if not self._manifest.superproject:
- print('error: superproject tag is not defined in manifest',
- file=sys.stderr)
- return False
+ self._LogWarning(f'superproject tag is not defined in manifest: '
+ f'{self._manifest.manifestFile}')
+ return SyncResult(False, False)
- url = self._manifest.superproject['remote'].url
- if not url:
- print('error: superproject URL is not defined in manifest',
- file=sys.stderr)
- return False
+ print('NOTICE: --use-superproject is in beta; report any issues to the '
+ 'address described in `repo version`', file=sys.stderr)
+ should_exit = True
+ if not self._remote_url:
+ self._LogWarning(f'superproject URL is not defined in manifest: '
+ f'{self._manifest.manifestFile}')
+ return SyncResult(False, should_exit)
if not self._Init():
- return False
- if not self._Fetch(url):
- return False
+ return SyncResult(False, should_exit)
+ if not self._Fetch():
+ return SyncResult(False, should_exit)
if not self._quiet:
print('%s: Initial setup for superproject completed.' % self._work_git)
- return True
+ return SyncResult(True, False)
def _GetAllProjectsCommitIds(self):
"""Get commit ids for all projects from superproject and save them in _project_commit_ids.
Returns:
- A dictionary with the projects/commit ids on success, otherwise None.
+ CommitIdsResult
"""
- if not self.Sync():
- return None
+ sync_result = self.Sync()
+ if not sync_result.success:
+ return CommitIdsResult(None, sync_result.fatal)
data = self._LsTree()
if not data:
- print('error: git ls-tree failed to return data for superproject',
- file=sys.stderr)
- return None
+ self._LogWarning(f'git ls-tree failed to return data for manifest: '
+ f'{self._manifest.manifestFile}')
+ return CommitIdsResult(None, True)
# Parse lines like the following to select lines starting with '160000' and
# build a dictionary with project path (last element) and its commit id (3rd element).
@@ -222,18 +272,16 @@
commit_ids[ls_data[3]] = ls_data[2]
self._project_commit_ids = commit_ids
- return commit_ids
+ return CommitIdsResult(commit_ids, False)
- def _WriteManfiestFile(self):
+ def _WriteManifestFile(self):
"""Writes manifest to a file.
Returns:
manifest_path: Path name of the file into which manifest is written instead of None.
"""
if not os.path.exists(self._superproject_path):
- print('error: missing superproject directory %s' %
- self._superproject_path,
- file=sys.stderr)
+ self._LogWarning(f'missing superproject directory: {self._superproject_path}')
return None
manifest_str = self._manifest.ToXml(groups=self._manifest.GetGroupsStr()).toxml()
manifest_path = self._manifest_path
@@ -241,12 +289,30 @@
with open(manifest_path, 'w', encoding='utf-8') as fp:
fp.write(manifest_str)
except IOError as e:
- print('error: cannot write manifest to %s:\n%s'
- % (manifest_path, e),
- file=sys.stderr)
+ self._LogError(f'cannot write manifest to : {manifest_path} {e}')
return None
return manifest_path
+ def _SkipUpdatingProjectRevisionId(self, project):
+ """Checks if a project's revision id needs to be updated or not.
+
+ Revision id for projects from local manifest will not be updated.
+
+ Args:
+ project: project whose revision id is being updated.
+
+ Returns:
+ True if a project's revision id should not be updated, or False,
+ """
+ path = project.relpath
+ if not path:
+ return True
+ # Skip the project with revisionId.
+ if project.revisionId:
+ return True
+ # Skip the project if it comes from the local manifest.
+ return any(s.startswith(LOCAL_MANIFEST_GROUP_PREFIX) for s in project.groups)
+
def UpdateProjectsRevisionId(self, projects):
"""Update revisionId of every project in projects with the commit id.
@@ -254,27 +320,96 @@
projects: List of projects whose revisionId needs to be updated.
Returns:
- manifest_path: Path name of the overriding manfiest file instead of None.
+ UpdateProjectsResult
"""
- commit_ids = self._GetAllProjectsCommitIds()
+ commit_ids_result = self._GetAllProjectsCommitIds()
+ commit_ids = commit_ids_result.commit_ids
if not commit_ids:
- print('error: Cannot get project commit ids from manifest', file=sys.stderr)
- return None
+ return UpdateProjectsResult(None, commit_ids_result.fatal)
projects_missing_commit_ids = []
for project in projects:
- path = project.relpath
- if not path:
+ if self._SkipUpdatingProjectRevisionId(project):
continue
+ path = project.relpath
commit_id = commit_ids.get(path)
- if commit_id:
- project.SetRevisionId(commit_id)
- else:
+ if not commit_id:
projects_missing_commit_ids.append(path)
- if projects_missing_commit_ids:
- print('error: please file a bug using %s to report missing commit_ids for: %s' %
- (BUG_REPORT_URL, projects_missing_commit_ids), file=sys.stderr)
- return None
- manifest_path = self._WriteManfiestFile()
- return manifest_path
+ # If superproject doesn't have a commit id for a project, then report an
+ # error event and continue as if do not use superproject is specified.
+ if projects_missing_commit_ids:
+ self._LogWarning(f'please file a bug using {self._manifest.contactinfo.bugurl} '
+ f'to report missing commit_ids for: {projects_missing_commit_ids}')
+ return UpdateProjectsResult(None, False)
+
+ for project in projects:
+ if not self._SkipUpdatingProjectRevisionId(project):
+ project.SetRevisionId(commit_ids.get(project.relpath))
+
+ manifest_path = self._WriteManifestFile()
+ return UpdateProjectsResult(manifest_path, False)
+
+
+@functools.lru_cache(maxsize=None)
+def _UseSuperprojectFromConfiguration():
+ """Returns the user choice of whether to use superproject."""
+ user_cfg = RepoConfig.ForUser()
+ time_now = int(time.time())
+
+ user_value = user_cfg.GetBoolean('repo.superprojectChoice')
+ if user_value is not None:
+ user_expiration = user_cfg.GetInt('repo.superprojectChoiceExpire')
+ if user_expiration is None or user_expiration <= 0 or user_expiration >= time_now:
+ # TODO(b/190688390) - Remove prompt when we are comfortable with the new
+ # default value.
+ if user_value:
+ print(('You are currently enrolled in Git submodules experiment '
+ '(go/android-submodules-quickstart). Use --no-use-superproject '
+ 'to override.\n'), file=sys.stderr)
+ else:
+ print(('You are not currently enrolled in Git submodules experiment '
+ '(go/android-submodules-quickstart). Use --use-superproject '
+ 'to override.\n'), file=sys.stderr)
+ return user_value
+
+ # We don't have an unexpired choice, ask for one.
+ system_cfg = RepoConfig.ForSystem()
+ system_value = system_cfg.GetBoolean('repo.superprojectChoice')
+ if system_value:
+ # The system configuration is proposing that we should enable the
+ # use of superproject. Treat the user as enrolled for two weeks.
+ #
+ # TODO(b/190688390) - Remove prompt when we are comfortable with the new
+ # default value.
+ userchoice = True
+ time_choiceexpire = time_now + (86400 * 14)
+ user_cfg.SetString('repo.superprojectChoiceExpire', str(time_choiceexpire))
+ user_cfg.SetBoolean('repo.superprojectChoice', userchoice)
+ print('You are automatically enrolled in Git submodules experiment '
+ '(go/android-submodules-quickstart) for another two weeks.\n',
+ file=sys.stderr)
+ return True
+
+ # For all other cases, we would not use superproject by default.
+ return False
+
+
+def PrintMessages(opt, manifest):
+ """Returns a boolean if error/warning messages are to be printed."""
+ return opt.use_superproject is not None or manifest.superproject
+
+
+def UseSuperproject(opt, manifest):
+ """Returns a boolean if use-superproject option is enabled."""
+
+ if opt.use_superproject is not None:
+ return opt.use_superproject
+ else:
+ client_value = manifest.manifestProject.config.GetBoolean('repo.superproject')
+ if client_value is not None:
+ return client_value
+ else:
+ if not manifest.superproject:
+ return False
+ return _UseSuperprojectFromConfiguration()
diff --git a/git_trace2_event_log.py b/git_trace2_event_log.py
index 8f12d1a..0e5e908 100644
--- a/git_trace2_event_log.py
+++ b/git_trace2_event_log.py
@@ -144,6 +144,19 @@
command_event['subcommands'] = subcommands
self._log.append(command_event)
+ def LogConfigEvents(self, config, event_dict_name):
+ """Append a |event_dict_name| event for each config key in |config|.
+
+ Args:
+ config: Configuration dictionary.
+ event_dict_name: Name of the event dictionary for items to be logged under.
+ """
+ for param, value in config.items():
+ event = self._CreateEventDict(event_dict_name)
+ event['param'] = param
+ event['value'] = value
+ self._log.append(event)
+
def DefParamRepoEvents(self, config):
"""Append a 'def_param' event for each repo.* config key to the current log.
@@ -152,12 +165,34 @@
"""
# Only output the repo.* config parameters.
repo_config = {k: v for k, v in config.items() if k.startswith('repo.')}
+ self.LogConfigEvents(repo_config, 'def_param')
- for param, value in repo_config.items():
- def_param_event = self._CreateEventDict('def_param')
- def_param_event['param'] = param
- def_param_event['value'] = value
- self._log.append(def_param_event)
+ def GetDataEventName(self, value):
+ """Returns 'data-json' if the value is an array else returns 'data'."""
+ return 'data-json' if value[0] == '[' and value[-1] == ']' else 'data'
+
+ def LogDataConfigEvents(self, config, prefix):
+ """Append a 'data' event for each config key/value in |config| to the current log.
+
+ For each keyX and valueX of the config, "key" field of the event is '|prefix|/keyX'
+ and the "value" of the "key" field is valueX.
+
+ Args:
+ config: Configuration dictionary.
+ prefix: Prefix for each key that is logged.
+ """
+ for key, value in config.items():
+ event = self._CreateEventDict(self.GetDataEventName(value))
+ event['key'] = f'{prefix}/{key}'
+ event['value'] = value
+ self._log.append(event)
+
+ def ErrorEvent(self, msg, fmt):
+ """Append a 'error' event to the current log."""
+ error_event = self._CreateEventDict('error')
+ error_event['msg'] = msg
+ error_event['fmt'] = fmt
+ self._log.append(error_event)
def _GetEventTargetPath(self):
"""Get the 'trace2.eventtarget' path from git configuration.
diff --git a/main.py b/main.py
index 8aba2ec..2050cab 100755
--- a/main.py
+++ b/main.py
@@ -39,7 +39,7 @@
import event_log
from repo_trace import SetTrace
from git_command import user_agent
-from git_config import init_ssh, close_ssh, RepoConfig
+from git_config import RepoConfig
from git_trace2_event_log import EventLog
from command import InteractiveCommand
from command import MirrorSafeCommand
@@ -71,7 +71,7 @@
#
# python-3.6 is in Ubuntu Bionic.
MIN_PYTHON_VERSION_SOFT = (3, 6)
-MIN_PYTHON_VERSION_HARD = (3, 5)
+MIN_PYTHON_VERSION_HARD = (3, 6)
if sys.version_info.major < 3:
print('repo: error: Python 2 is no longer supported; '
@@ -95,6 +95,8 @@
add_help_option=False)
global_options.add_option('-h', '--help', action='store_true',
help='show this help message and exit')
+global_options.add_option('--help-all', action='store_true',
+ help='show this help message with all subcommands and exit')
global_options.add_option('-p', '--paginate',
dest='pager', action='store_true',
help='display command output in the pager')
@@ -116,6 +118,10 @@
global_options.add_option('--version',
dest='show_version', action='store_true',
help='display this version of repo')
+global_options.add_option('--show-toplevel',
+ action='store_true',
+ help='display the path of the top-level directory of '
+ 'the repo client checkout')
global_options.add_option('--event-log',
dest='event_log', action='store',
help='filename of event log to append timeline to')
@@ -128,34 +134,40 @@
self.repodir = repodir
self.commands = all_commands
+ def _PrintHelp(self, short: bool = False, all_commands: bool = False):
+ """Show --help screen."""
+ global_options.print_help()
+ print()
+ if short:
+ commands = ' '.join(sorted(self.commands))
+ wrapped_commands = textwrap.wrap(commands, width=77)
+ print('Available commands:\n %s' % ('\n '.join(wrapped_commands),))
+ print('\nRun `repo help <command>` for command-specific details.')
+ print('Bug reports:', Wrapper().BUG_URL)
+ else:
+ cmd = self.commands['help']()
+ if all_commands:
+ cmd.PrintAllCommandsBody()
+ else:
+ cmd.PrintCommonCommandsBody()
+
def _ParseArgs(self, argv):
"""Parse the main `repo` command line options."""
- name = None
- glob = []
-
- for i in range(len(argv)):
- if not argv[i].startswith('-'):
- name = argv[i]
- if i > 0:
- glob = argv[:i]
+ for i, arg in enumerate(argv):
+ if not arg.startswith('-'):
+ name = arg
+ glob = argv[:i]
argv = argv[i + 1:]
break
- if not name:
+ else:
+ name = None
glob = argv
- name = 'help'
argv = []
gopts, _gargs = global_options.parse_args(glob)
- name, alias_args = self._ExpandAlias(name)
- argv = alias_args + argv
-
- if gopts.help:
- global_options.print_help()
- commands = ' '.join(sorted(self.commands))
- wrapped_commands = textwrap.wrap(commands, width=77)
- print('\nAvailable commands:\n %s' % ('\n '.join(wrapped_commands),))
- print('\nRun `repo help <command>` for command-specific details.')
- global_options.exit()
+ if name:
+ name, alias_args = self._ExpandAlias(name)
+ argv = alias_args + argv
return (name, gopts, argv)
@@ -186,32 +198,44 @@
if gopts.trace:
SetTrace()
- if gopts.show_version:
- if name == 'help':
- name = 'version'
- else:
- print('fatal: invalid usage of --version', file=sys.stderr)
- return 1
+
+ # Handle options that terminate quickly first.
+ if gopts.help or gopts.help_all:
+ self._PrintHelp(short=False, all_commands=gopts.help_all)
+ return 0
+ elif gopts.show_version:
+ # Always allow global --version regardless of subcommand validity.
+ name = 'version'
+ elif gopts.show_toplevel:
+ print(os.path.dirname(self.repodir))
+ return 0
+ elif not name:
+ # No subcommand specified, so show the help/subcommand.
+ self._PrintHelp(short=True)
+ return 1
SetDefaultColoring(gopts.color)
+ git_trace2_event_log = EventLog()
+ repo_client = RepoClient(self.repodir)
+ gitc_manifest = None
+ gitc_client_name = gitc_utils.parse_clientdir(os.getcwd())
+ if gitc_client_name:
+ gitc_manifest = GitcClient(self.repodir, gitc_client_name)
+ repo_client.isGitcClient = True
+
try:
- cmd = self.commands[name]()
+ cmd = self.commands[name](
+ repodir=self.repodir,
+ client=repo_client,
+ manifest=repo_client.manifest,
+ gitc_manifest=gitc_manifest,
+ git_event_log=git_trace2_event_log)
except KeyError:
print("repo: '%s' is not a repo command. See 'repo help'." % name,
file=sys.stderr)
return 1
- git_trace2_event_log = EventLog()
- cmd.repodir = self.repodir
- cmd.client = RepoClient(cmd.repodir)
- cmd.manifest = cmd.client.manifest
- cmd.gitc_manifest = None
- gitc_client_name = gitc_utils.parse_clientdir(os.getcwd())
- if gitc_client_name:
- cmd.gitc_manifest = GitcClient(cmd.repodir, gitc_client_name)
- cmd.client.isGitcClient = True
-
Editor.globalConfig = cmd.client.globalConfig
if not isinstance(cmd, MirrorSafeCommand) and cmd.manifest.IsMirror:
@@ -591,20 +615,16 @@
repo = _Repo(opt.repodir)
try:
- try:
- init_ssh()
- init_http()
- name, gopts, argv = repo._ParseArgs(argv)
- run = lambda: repo._Run(name, gopts, argv) or 0
- if gopts.trace_python:
- import trace
- tracer = trace.Trace(count=False, trace=True, timing=True,
- ignoredirs=set(sys.path[1:]))
- result = tracer.runfunc(run)
- else:
- result = run()
- finally:
- close_ssh()
+ init_http()
+ name, gopts, argv = repo._ParseArgs(argv)
+ run = lambda: repo._Run(name, gopts, argv) or 0
+ if gopts.trace_python:
+ import trace
+ tracer = trace.Trace(count=False, trace=True, timing=True,
+ ignoredirs=set(sys.path[1:]))
+ result = tracer.runfunc(run)
+ else:
+ result = run()
except KeyboardInterrupt:
print('aborted by user', file=sys.stderr)
result = 1
diff --git a/man/repo-abandon.1 b/man/repo-abandon.1
new file mode 100644
index 0000000..b3c0422
--- /dev/null
+++ b/man/repo-abandon.1
@@ -0,0 +1,36 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo abandon" "Repo Manual"
+.SH NAME
+repo \- repo abandon - manual page for repo abandon
+.SH SYNOPSIS
+.B repo
+\fI\,abandon \/\fR[\fI\,--all | <branchname>\/\fR] [\fI\,<project>\/\fR...]
+.SH DESCRIPTION
+Summary
+.PP
+Permanently abandon a development branch
+.PP
+This subcommand permanently abandons a development branch by
+deleting it (and all its history) from your local repository.
+.PP
+It is equivalent to "git branch \fB\-D\fR <branchname>".
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
+number of jobs to run in parallel (default: based on
+number of CPU cores)
+.TP
+\fB\-\-all\fR
+delete all branches in all projects
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.PP
+Run `repo help abandon` to view the detailed manual.
diff --git a/man/repo-branch.1 b/man/repo-branch.1
new file mode 100644
index 0000000..854ee98
--- /dev/null
+++ b/man/repo-branch.1
@@ -0,0 +1 @@
+.so man1/repo-branches.1
\ No newline at end of file
diff --git a/man/repo-branches.1 b/man/repo-branches.1
new file mode 100644
index 0000000..7fe0b02
--- /dev/null
+++ b/man/repo-branches.1
@@ -0,0 +1,59 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo branches" "Repo Manual"
+.SH NAME
+repo \- repo branches - manual page for repo branches
+.SH SYNOPSIS
+.B repo
+\fI\,branches \/\fR[\fI\,<project>\/\fR...]
+.SH DESCRIPTION
+Summary
+.PP
+View current topic branches
+.PP
+Summarizes the currently available topic branches.
+.PP
+# Branch Display
+.PP
+The branch display output by this command is organized into four
+columns of information; for example:
+.TP
+*P nocolor
+| in repo
+.TP
+repo2
+|
+.PP
+The first column contains a * if the branch is the currently
+checked out branch in any of the specified projects, or a blank
+if no project has the branch checked out.
+.PP
+The second column contains either blank, p or P, depending upon
+the upload status of the branch.
+.IP
+(blank): branch not yet published by repo upload
+.IP
+P: all commits were published by repo upload
+p: only some commits were published by repo upload
+.PP
+The third column contains the branch name.
+.PP
+The fourth column (after the | separator) lists the projects that
+the branch appears in, or does not appear in. If no project list
+is shown, then the branch appears in all projects.
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
+number of jobs to run in parallel (default: based on
+number of CPU cores)
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.PP
+Run `repo help branches` to view the detailed manual.
diff --git a/man/repo-checkout.1 b/man/repo-checkout.1
new file mode 100644
index 0000000..6dd3e6c
--- /dev/null
+++ b/man/repo-checkout.1
@@ -0,0 +1,36 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo checkout" "Repo Manual"
+.SH NAME
+repo \- repo checkout - manual page for repo checkout
+.SH SYNOPSIS
+.B repo
+\fI\,checkout <branchname> \/\fR[\fI\,<project>\/\fR...]
+.SH DESCRIPTION
+Summary
+.PP
+Checkout a branch for development
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
+number of jobs to run in parallel (default: based on
+number of CPU cores)
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.PP
+Run `repo help checkout` to view the detailed manual.
+.SH DETAILS
+.PP
+The 'repo checkout' command checks out an existing branch that was previously
+created by 'repo start'.
+.PP
+The command is equivalent to:
+.IP
+repo forall [<project>...] \fB\-c\fR git checkout <branchname>
diff --git a/man/repo-cherry-pick.1 b/man/repo-cherry-pick.1
new file mode 100644
index 0000000..e7716c5
--- /dev/null
+++ b/man/repo-cherry-pick.1
@@ -0,0 +1,28 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo cherry-pick" "Repo Manual"
+.SH NAME
+repo \- repo cherry-pick - manual page for repo cherry-pick
+.SH SYNOPSIS
+.B repo
+\fI\,cherry-pick <sha1>\/\fR
+.SH DESCRIPTION
+Summary
+.PP
+Cherry\-pick a change.
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.PP
+Run `repo help cherry\-pick` to view the detailed manual.
+.SH DETAILS
+.PP
+\&'repo cherry\-pick' cherry\-picks a change from one branch to another. The change
+id will be updated, and a reference to the old change id will be added.
diff --git a/man/repo-diff.1 b/man/repo-diff.1
new file mode 100644
index 0000000..890f8d2
--- /dev/null
+++ b/man/repo-diff.1
@@ -0,0 +1,35 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo diff" "Repo Manual"
+.SH NAME
+repo \- repo diff - manual page for repo diff
+.SH SYNOPSIS
+.B repo
+\fI\,diff \/\fR[\fI\,<project>\/\fR...]
+.SH DESCRIPTION
+Summary
+.PP
+Show changes between commit and working tree
+.PP
+The \fB\-u\fR option causes 'repo diff' to generate diff output with file paths
+relative to the repository root, so the output can be applied
+to the Unix 'patch' command.
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
+number of jobs to run in parallel (default: based on
+number of CPU cores)
+.TP
+\fB\-u\fR, \fB\-\-absolute\fR
+paths are relative to the repository root
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.PP
+Run `repo help diff` to view the detailed manual.
diff --git a/man/repo-diffmanifests.1 b/man/repo-diffmanifests.1
new file mode 100644
index 0000000..add50f1
--- /dev/null
+++ b/man/repo-diffmanifests.1
@@ -0,0 +1,61 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo diffmanifests" "Repo Manual"
+.SH NAME
+repo \- repo diffmanifests - manual page for repo diffmanifests
+.SH SYNOPSIS
+.B repo
+\fI\,diffmanifests manifest1.xml \/\fR[\fI\,manifest2.xml\/\fR] [\fI\,options\/\fR]
+.SH DESCRIPTION
+Summary
+.PP
+Manifest diff utility
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-\-raw\fR
+display raw diff
+.TP
+\fB\-\-no\-color\fR
+does not display the diff in color
+.TP
+\fB\-\-pretty\-format=\fR<FORMAT>
+print the log using a custom git pretty format string
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.PP
+Run `repo help diffmanifests` to view the detailed manual.
+.SH DETAILS
+.PP
+The repo diffmanifests command shows differences between project revisions of
+manifest1 and manifest2. if manifest2 is not specified, current manifest.xml
+will be used instead. Both absolute and relative paths may be used for
+manifests. Relative paths start from project's ".repo/manifests" folder.
+.PP
+The \fB\-\-raw\fR option Displays the diff in a way that facilitates parsing, the
+project pattern will be <status> <path> <revision from> [<revision to>] and the
+commit pattern will be <status> <onelined log> with status values respectively :
+.IP
+A = Added project
+R = Removed project
+C = Changed project
+U = Project with unreachable revision(s) (revision(s) not found)
+.PP
+for project, and
+.IP
+A = Added commit
+R = Removed commit
+.PP
+for a commit.
+.PP
+Only changed projects may contain commits, and commit status always starts with
+a space, and are part of last printed project. Unreachable revisions may occur
+if project is not up to date or if repo has not been initialized with all the
+groups, in which case some projects won't be synced and their revisions won't be
+found.
diff --git a/man/repo-download.1 b/man/repo-download.1
new file mode 100644
index 0000000..cf7f767
--- /dev/null
+++ b/man/repo-download.1
@@ -0,0 +1,44 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo download" "Repo Manual"
+.SH NAME
+repo \- repo download - manual page for repo download
+.SH SYNOPSIS
+.B repo
+\fI\,download {\/\fR[\fI\,project\/\fR] \fI\,change\/\fR[\fI\,/patchset\/\fR]\fI\,}\/\fR...
+.SH DESCRIPTION
+Summary
+.PP
+Download and checkout a change
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-b\fR BRANCH, \fB\-\-branch\fR=\fI\,BRANCH\/\fR
+create a new branch first
+.TP
+\fB\-c\fR, \fB\-\-cherry\-pick\fR
+cherry\-pick instead of checkout
+.TP
+\fB\-x\fR, \fB\-\-record\-origin\fR
+pass \fB\-x\fR when cherry\-picking
+.TP
+\fB\-r\fR, \fB\-\-revert\fR
+revert instead of checkout
+.TP
+\fB\-f\fR, \fB\-\-ff\-only\fR
+force fast\-forward merge
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.PP
+Run `repo help download` to view the detailed manual.
+.SH DETAILS
+.PP
+The 'repo download' command downloads a change from the review system and makes
+it available in your project's local working directory. If no project is
+specified try to use current directory as a project.
diff --git a/man/repo-forall.1 b/man/repo-forall.1
new file mode 100644
index 0000000..eb2ad57
--- /dev/null
+++ b/man/repo-forall.1
@@ -0,0 +1,128 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo forall" "Repo Manual"
+.SH NAME
+repo \- repo forall - manual page for repo forall
+.SH SYNOPSIS
+.B repo
+\fI\,forall \/\fR[\fI\,<project>\/\fR...] \fI\,-c <command> \/\fR[\fI\,<arg>\/\fR...]
+.SH DESCRIPTION
+Summary
+.PP
+Run a shell command in each project
+.PP
+repo forall \fB\-r\fR str1 [str2] ... \fB\-c\fR <command> [<arg>...]
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
+number of jobs to run in parallel (default: based on
+number of CPU cores)
+.TP
+\fB\-r\fR, \fB\-\-regex\fR
+execute the command only on projects matching regex or
+wildcard expression
+.TP
+\fB\-i\fR, \fB\-\-inverse\-regex\fR
+execute the command only on projects not matching
+regex or wildcard expression
+.TP
+\fB\-g\fR GROUPS, \fB\-\-groups\fR=\fI\,GROUPS\/\fR
+execute the command only on projects matching the
+specified groups
+.TP
+\fB\-c\fR, \fB\-\-command\fR
+command (and arguments) to execute
+.TP
+\fB\-e\fR, \fB\-\-abort\-on\-errors\fR
+abort if a command exits unsuccessfully
+.TP
+\fB\-\-ignore\-missing\fR
+silently skip & do not exit non\-zero due missing
+checkouts
+.TP
+\fB\-\-interactive\fR
+force interactive usage
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.TP
+\fB\-p\fR
+show project headers before output
+.PP
+Run `repo help forall` to view the detailed manual.
+.SH DETAILS
+.PP
+Executes the same shell command in each project.
+.PP
+The \fB\-r\fR option allows running the command only on projects matching regex or
+wildcard expression.
+.PP
+By default, projects are processed non\-interactively in parallel. If you want to
+run interactive commands, make sure to pass \fB\-\-interactive\fR to force \fB\-\-jobs\fR 1.
+While the processing order of projects is not guaranteed, the order of project
+output is stable.
+.PP
+Output Formatting
+.PP
+The \fB\-p\fR option causes 'repo forall' to bind pipes to the command's stdin, stdout
+and stderr streams, and pipe all output into a continuous stream that is
+displayed in a single pager session. Project headings are inserted before the
+output of each command is displayed. If the command produces no output in a
+project, no heading is displayed.
+.PP
+The formatting convention used by \fB\-p\fR is very suitable for some types of
+searching, e.g. `repo forall \fB\-p\fR \fB\-c\fR git log \fB\-SFoo\fR` will print all commits that
+add or remove references to Foo.
+.PP
+The \fB\-v\fR option causes 'repo forall' to display stderr messages if a command
+produces output only on stderr. Normally the \fB\-p\fR option causes command output to
+be suppressed until the command produces at least one byte of output on stdout.
+.PP
+Environment
+.PP
+pwd is the project's working directory. If the current client is a mirror
+client, then pwd is the Git repository.
+.PP
+REPO_PROJECT is set to the unique name of the project.
+.PP
+REPO_PATH is the path relative the the root of the client.
+.PP
+REPO_REMOTE is the name of the remote system from the manifest.
+.PP
+REPO_LREV is the name of the revision from the manifest, translated to a local
+tracking branch. If you need to pass the manifest revision to a locally executed
+git command, use REPO_LREV.
+.PP
+REPO_RREV is the name of the revision from the manifest, exactly as written in
+the manifest.
+.PP
+REPO_COUNT is the total number of projects being iterated.
+.PP
+REPO_I is the current (1\-based) iteration count. Can be used in conjunction with
+REPO_COUNT to add a simple progress indicator to your command.
+.PP
+REPO__* are any extra environment variables, specified by the "annotation"
+element under any project element. This can be useful for differentiating trees
+based on user\-specific criteria, or simply annotating tree details.
+.PP
+shell positional arguments ($1, $2, .., $#) are set to any arguments following
+<command>.
+.PP
+Example: to list projects:
+.IP
+repo forall \fB\-c\fR 'echo $REPO_PROJECT'
+.PP
+Notice that $REPO_PROJECT is quoted to ensure it is expanded in the context of
+running <command> instead of in the calling shell.
+.PP
+Unless \fB\-p\fR is used, stdin, stdout, stderr are inherited from the terminal and are
+not redirected.
+.PP
+If \fB\-e\fR is used, when a command exits unsuccessfully, 'repo forall' will abort
+without iterating through the remaining projects.
diff --git a/man/repo-gitc-delete.1 b/man/repo-gitc-delete.1
new file mode 100644
index 0000000..c84c6e4
--- /dev/null
+++ b/man/repo-gitc-delete.1
@@ -0,0 +1,31 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo gitc-delete" "Repo Manual"
+.SH NAME
+repo \- repo gitc-delete - manual page for repo gitc-delete
+.SH SYNOPSIS
+.B repo
+\fI\,gitc-delete\/\fR
+.SH DESCRIPTION
+Summary
+.PP
+Delete a GITC Client.
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-f\fR, \fB\-\-force\fR
+force the deletion (no prompt)
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.PP
+Run `repo help gitc\-delete` to view the detailed manual.
+.SH DETAILS
+.PP
+This subcommand deletes the current GITC client, deleting the GITC manifest and
+all locally downloaded sources.
diff --git a/man/repo-gitc-init.1 b/man/repo-gitc-init.1
new file mode 100644
index 0000000..9b61866
--- /dev/null
+++ b/man/repo-gitc-init.1
@@ -0,0 +1,150 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "September 2021" "repo gitc-init" "Repo Manual"
+.SH NAME
+repo \- repo gitc-init - manual page for repo gitc-init
+.SH SYNOPSIS
+.B repo
+\fI\,gitc-init \/\fR[\fI\,options\/\fR] [\fI\,client name\/\fR]
+.SH DESCRIPTION
+Summary
+.PP
+Initialize a GITC Client.
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.SS Manifest options:
+.TP
+\fB\-u\fR URL, \fB\-\-manifest\-url\fR=\fI\,URL\/\fR
+manifest repository location
+.TP
+\fB\-b\fR REVISION, \fB\-\-manifest\-branch\fR=\fI\,REVISION\/\fR
+manifest branch or revision (use HEAD for default)
+.TP
+\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
+initial manifest file
+.TP
+\fB\-\-standalone\-manifest\fR
+download the manifest as a static file rather then
+create a git checkout of the manifest repo
+.TP
+\fB\-g\fR GROUP, \fB\-\-groups\fR=\fI\,GROUP\/\fR
+restrict manifest projects to ones with specified
+group(s) [default|all|G1,G2,G3|G4,\-G5,\-G6]
+.TP
+\fB\-p\fR PLATFORM, \fB\-\-platform\fR=\fI\,PLATFORM\/\fR
+restrict manifest projects to ones with a specified
+platform group [auto|all|none|linux|darwin|...]
+.TP
+\fB\-\-submodules\fR
+sync any submodules associated with the manifest repo
+.SS Manifest (only) checkout options:
+.TP
+\fB\-\-current\-branch\fR
+fetch only current manifest branch from server
+.TP
+\fB\-\-no\-current\-branch\fR
+fetch all manifest branches from server
+.TP
+\fB\-\-tags\fR
+fetch tags in the manifest
+.TP
+\fB\-\-no\-tags\fR
+don't fetch tags in the manifest
+.SS Checkout modes:
+.TP
+\fB\-\-mirror\fR
+create a replica of the remote repositories rather
+than a client working directory
+.TP
+\fB\-\-archive\fR
+checkout an archive instead of a git repository for
+each project. See git archive.
+.TP
+\fB\-\-worktree\fR
+use git\-worktree to manage projects
+.SS Project checkout optimizations:
+.TP
+\fB\-\-reference\fR=\fI\,DIR\/\fR
+location of mirror directory
+.TP
+\fB\-\-dissociate\fR
+dissociate from reference mirrors after clone
+.TP
+\fB\-\-depth\fR=\fI\,DEPTH\/\fR
+create a shallow clone with given depth; see git clone
+.TP
+\fB\-\-partial\-clone\fR
+perform partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
+.TP
+\fB\-\-no\-partial\-clone\fR
+disable use of partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
+.TP
+\fB\-\-partial\-clone\-exclude\fR=\fI\,PARTIAL_CLONE_EXCLUDE\/\fR
+exclude the specified projects (a comma\-delimited
+project names) from partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
+.TP
+\fB\-\-clone\-filter\fR=\fI\,CLONE_FILTER\/\fR
+filter for use with \fB\-\-partial\-clone\fR [default:
+blob:none]
+.TP
+\fB\-\-use\-superproject\fR
+use the manifest superproject to sync projects
+.TP
+\fB\-\-no\-use\-superproject\fR
+disable use of manifest superprojects
+.TP
+\fB\-\-clone\-bundle\fR
+enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if
+not \fB\-\-partial\-clone\fR)
+.TP
+\fB\-\-no\-clone\-bundle\fR
+disable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if
+\fB\-\-partial\-clone\fR)
+.SS repo Version options:
+.TP
+\fB\-\-repo\-url\fR=\fI\,URL\/\fR
+repo repository location ($REPO_URL)
+.TP
+\fB\-\-repo\-rev\fR=\fI\,REV\/\fR
+repo branch or revision ($REPO_REV)
+.TP
+\fB\-\-no\-repo\-verify\fR
+do not verify repo source code
+.SS Other options:
+.TP
+\fB\-\-config\-name\fR
+Always prompt for name/e\-mail
+.SS GITC options:
+.TP
+\fB\-f\fR MANIFEST_FILE, \fB\-\-manifest\-file\fR=\fI\,MANIFEST_FILE\/\fR
+Optional manifest file to use for this GITC client.
+.TP
+\fB\-c\fR GITC_CLIENT, \fB\-\-gitc\-client\fR=\fI\,GITC_CLIENT\/\fR
+Name of the gitc_client instance to create or modify.
+.PP
+Run `repo help gitc\-init` to view the detailed manual.
+.SH DETAILS
+.PP
+The 'repo gitc\-init' command is ran to initialize a new GITC client for use with
+the GITC file system.
+.PP
+This command will setup the client directory, initialize repo, just like repo
+init does, and then downloads the manifest collection and installs it in the
+\&.repo/directory of the GITC client.
+.PP
+Once this is done, a GITC manifest is generated by pulling the HEAD SHA for each
+project and generates the properly formatted XML file and installs it as
+\&.manifest in the GITC client directory.
+.PP
+The \fB\-c\fR argument is required to specify the GITC client name.
+.PP
+The optional \fB\-f\fR argument can be used to specify the manifest file to use for
+this GITC client.
diff --git a/man/repo-grep.1 b/man/repo-grep.1
new file mode 100644
index 0000000..be41058
--- /dev/null
+++ b/man/repo-grep.1
@@ -0,0 +1,119 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo grep" "Repo Manual"
+.SH NAME
+repo \- repo grep - manual page for repo grep
+.SH SYNOPSIS
+.B repo
+\fI\,grep {pattern | -e pattern} \/\fR[\fI\,<project>\/\fR...]
+.SH DESCRIPTION
+Summary
+.PP
+Print lines matching a pattern
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
+number of jobs to run in parallel (default: based on
+number of CPU cores)
+.SS Logging options:
+.TP
+\fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.SS Sources:
+.TP
+\fB\-\-cached\fR
+Search the index, instead of the work tree
+.TP
+\fB\-r\fR TREEish, \fB\-\-revision\fR=\fI\,TREEish\/\fR
+Search TREEish, instead of the work tree
+.SS Pattern:
+.TP
+\fB\-e\fR PATTERN
+Pattern to search for
+.TP
+\fB\-i\fR, \fB\-\-ignore\-case\fR
+Ignore case differences
+.TP
+\fB\-a\fR, \fB\-\-text\fR
+Process binary files as if they were text
+.TP
+\fB\-I\fR
+Don't match the pattern in binary files
+.TP
+\fB\-w\fR, \fB\-\-word\-regexp\fR
+Match the pattern only at word boundaries
+.TP
+\fB\-v\fR, \fB\-\-invert\-match\fR
+Select non\-matching lines
+.TP
+\fB\-G\fR, \fB\-\-basic\-regexp\fR
+Use POSIX basic regexp for patterns (default)
+.TP
+\fB\-E\fR, \fB\-\-extended\-regexp\fR
+Use POSIX extended regexp for patterns
+.TP
+\fB\-F\fR, \fB\-\-fixed\-strings\fR
+Use fixed strings (not regexp) for pattern
+.SS Pattern Grouping:
+.TP
+\fB\-\-all\-match\fR
+Limit match to lines that have all patterns
+.TP
+\fB\-\-and\fR, \fB\-\-or\fR, \fB\-\-not\fR
+Boolean operators to combine patterns
+.TP
+\-(, \-)
+Boolean operator grouping
+.SS Output:
+.TP
+\fB\-n\fR
+Prefix the line number to matching lines
+.TP
+\fB\-C\fR CONTEXT
+Show CONTEXT lines around match
+.TP
+\fB\-B\fR CONTEXT
+Show CONTEXT lines before match
+.TP
+\fB\-A\fR CONTEXT
+Show CONTEXT lines after match
+.TP
+\fB\-l\fR, \fB\-\-name\-only\fR, \fB\-\-files\-with\-matches\fR
+Show only file names containing matching lines
+.TP
+\fB\-L\fR, \fB\-\-files\-without\-match\fR
+Show only file names not containing matching lines
+.PP
+Run `repo help grep` to view the detailed manual.
+.SH DETAILS
+.PP
+Search for the specified patterns in all project files.
+.PP
+Boolean Options
+.PP
+The following options can appear as often as necessary to express the pattern to
+locate:
+.HP
+\fB\-e\fR PATTERN
+.HP
+\fB\-\-and\fR, \fB\-\-or\fR, \fB\-\-not\fR, \-(, \-)
+.PP
+Further, the \fB\-r\fR/\-\-revision option may be specified multiple times in order to
+scan multiple trees. If the same file matches in more than one tree, only the
+first result is reported, prefixed by the revision name it was found under.
+.PP
+Examples
+.PP
+Look for a line that has '#define' and either 'MAX_PATH or 'PATH_MAX':
+.IP
+repo grep \fB\-e\fR '#define' \fB\-\-and\fR \-\e( \fB\-e\fR MAX_PATH \fB\-e\fR PATH_MAX \e)
+.PP
+Look for a line that has 'NODE' or 'Unexpected' in files that contain a line
+that matches both expressions:
+.IP
+repo grep \fB\-\-all\-match\fR \fB\-e\fR NODE \fB\-e\fR Unexpected
diff --git a/man/repo-help.1 b/man/repo-help.1
new file mode 100644
index 0000000..d6da3c5
--- /dev/null
+++ b/man/repo-help.1
@@ -0,0 +1,33 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo help" "Repo Manual"
+.SH NAME
+repo \- repo help - manual page for repo help
+.SH SYNOPSIS
+.B repo
+\fI\,help \/\fR[\fI\,--all|command\/\fR]
+.SH DESCRIPTION
+Summary
+.PP
+Display detailed help on a command
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-a\fR, \fB\-\-all\fR
+show the complete list of commands
+.TP
+\fB\-\-help\-all\fR
+show the \fB\-\-help\fR of all commands
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.PP
+Run `repo help help` to view the detailed manual.
+.SH DETAILS
+.PP
+Displays detailed usage information about a command.
diff --git a/man/repo-info.1 b/man/repo-info.1
new file mode 100644
index 0000000..cf7c17b
--- /dev/null
+++ b/man/repo-info.1
@@ -0,0 +1,40 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo info" "Repo Manual"
+.SH NAME
+repo \- repo info - manual page for repo info
+.SH SYNOPSIS
+.B repo
+\fI\,info \/\fR[\fI\,-dl\/\fR] [\fI\,-o \/\fR[\fI\,-c\/\fR]] [\fI\,<project>\/\fR...]
+.SH DESCRIPTION
+Summary
+.PP
+Get info on the manifest branch, current branch or unmerged branches
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-d\fR, \fB\-\-diff\fR
+show full info and commit diff including remote
+branches
+.TP
+\fB\-o\fR, \fB\-\-overview\fR
+show overview of all local commits
+.TP
+\fB\-c\fR, \fB\-\-current\-branch\fR
+consider only checked out branches
+.TP
+\fB\-\-no\-current\-branch\fR
+consider all local branches
+.TP
+\fB\-l\fR, \fB\-\-local\-only\fR
+disable all remote operations
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.PP
+Run `repo help info` to view the detailed manual.
diff --git a/man/repo-init.1 b/man/repo-init.1
new file mode 100644
index 0000000..9957b64
--- /dev/null
+++ b/man/repo-init.1
@@ -0,0 +1,170 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "September 2021" "repo init" "Repo Manual"
+.SH NAME
+repo \- repo init - manual page for repo init
+.SH SYNOPSIS
+.B repo
+\fI\,init \/\fR[\fI\,options\/\fR] [\fI\,manifest url\/\fR]
+.SH DESCRIPTION
+Summary
+.PP
+Initialize a repo client checkout in the current directory
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.SS Manifest options:
+.TP
+\fB\-u\fR URL, \fB\-\-manifest\-url\fR=\fI\,URL\/\fR
+manifest repository location
+.TP
+\fB\-b\fR REVISION, \fB\-\-manifest\-branch\fR=\fI\,REVISION\/\fR
+manifest branch or revision (use HEAD for default)
+.TP
+\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
+initial manifest file
+.TP
+\fB\-\-standalone\-manifest\fR
+download the manifest as a static file rather then
+create a git checkout of the manifest repo
+.TP
+\fB\-g\fR GROUP, \fB\-\-groups\fR=\fI\,GROUP\/\fR
+restrict manifest projects to ones with specified
+group(s) [default|all|G1,G2,G3|G4,\-G5,\-G6]
+.TP
+\fB\-p\fR PLATFORM, \fB\-\-platform\fR=\fI\,PLATFORM\/\fR
+restrict manifest projects to ones with a specified
+platform group [auto|all|none|linux|darwin|...]
+.TP
+\fB\-\-submodules\fR
+sync any submodules associated with the manifest repo
+.SS Manifest (only) checkout options:
+.TP
+\fB\-c\fR, \fB\-\-current\-branch\fR
+fetch only current manifest branch from server
+.TP
+\fB\-\-no\-current\-branch\fR
+fetch all manifest branches from server
+.TP
+\fB\-\-tags\fR
+fetch tags in the manifest
+.TP
+\fB\-\-no\-tags\fR
+don't fetch tags in the manifest
+.SS Checkout modes:
+.TP
+\fB\-\-mirror\fR
+create a replica of the remote repositories rather
+than a client working directory
+.TP
+\fB\-\-archive\fR
+checkout an archive instead of a git repository for
+each project. See git archive.
+.TP
+\fB\-\-worktree\fR
+use git\-worktree to manage projects
+.SS Project checkout optimizations:
+.TP
+\fB\-\-reference\fR=\fI\,DIR\/\fR
+location of mirror directory
+.TP
+\fB\-\-dissociate\fR
+dissociate from reference mirrors after clone
+.TP
+\fB\-\-depth\fR=\fI\,DEPTH\/\fR
+create a shallow clone with given depth; see git clone
+.TP
+\fB\-\-partial\-clone\fR
+perform partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
+.TP
+\fB\-\-no\-partial\-clone\fR
+disable use of partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
+.TP
+\fB\-\-partial\-clone\-exclude\fR=\fI\,PARTIAL_CLONE_EXCLUDE\/\fR
+exclude the specified projects (a comma\-delimited
+project names) from partial clone (https://gitscm.com/docs/gitrepositorylayout#_code_partialclone_code)
+.TP
+\fB\-\-clone\-filter\fR=\fI\,CLONE_FILTER\/\fR
+filter for use with \fB\-\-partial\-clone\fR [default:
+blob:none]
+.TP
+\fB\-\-use\-superproject\fR
+use the manifest superproject to sync projects
+.TP
+\fB\-\-no\-use\-superproject\fR
+disable use of manifest superprojects
+.TP
+\fB\-\-clone\-bundle\fR
+enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if
+not \fB\-\-partial\-clone\fR)
+.TP
+\fB\-\-no\-clone\-bundle\fR
+disable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if
+\fB\-\-partial\-clone\fR)
+.SS repo Version options:
+.TP
+\fB\-\-repo\-url\fR=\fI\,URL\/\fR
+repo repository location ($REPO_URL)
+.TP
+\fB\-\-repo\-rev\fR=\fI\,REV\/\fR
+repo branch or revision ($REPO_REV)
+.TP
+\fB\-\-no\-repo\-verify\fR
+do not verify repo source code
+.SS Other options:
+.TP
+\fB\-\-config\-name\fR
+Always prompt for name/e\-mail
+.PP
+Run `repo help init` to view the detailed manual.
+.SH DETAILS
+.PP
+The 'repo init' command is run once to install and initialize repo. The latest
+repo source code and manifest collection is downloaded from the server and is
+installed in the .repo/ directory in the current working directory.
+.PP
+When creating a new checkout, the manifest URL is the only required setting. It
+may be specified using the \fB\-\-manifest\-url\fR option, or as the first optional
+argument.
+.PP
+The optional \fB\-b\fR argument can be used to select the manifest branch to checkout
+and use. If no branch is specified, the remote's default branch is used. This is
+equivalent to using \fB\-b\fR HEAD.
+.PP
+The optional \fB\-m\fR argument can be used to specify an alternate manifest to be
+used. If no manifest is specified, the manifest default.xml will be used.
+.PP
+If the \fB\-\-standalone\-manifest\fR argument is set, the manifest will be downloaded
+directly from the specified \fB\-\-manifest\-url\fR as a static file (rather than setting
+up a manifest git checkout). With \fB\-\-standalone\-manifest\fR, the manifest will be
+fully static and will not be re\-downloaded during subsesquent `repo init` and
+`repo sync` calls.
+.PP
+The \fB\-\-reference\fR option can be used to point to a directory that has the content
+of a \fB\-\-mirror\fR sync. This will make the working directory use as much data as
+possible from the local reference directory when fetching from the server. This
+will make the sync go a lot faster by reducing data traffic on the network.
+.PP
+The \fB\-\-dissociate\fR option can be used to borrow the objects from the directory
+specified with the \fB\-\-reference\fR option only to reduce network transfer, and stop
+borrowing from them after a first clone is made by making necessary local copies
+of borrowed objects.
+.PP
+The \fB\-\-no\-clone\-bundle\fR option disables any attempt to use \fI\,$URL/clone.bundle\/\fP to
+bootstrap a new Git repository from a resumeable bundle file on a content
+delivery network. This may be necessary if there are problems with the local
+Python HTTP client or proxy configuration, but the Git binary works.
+.PP
+Switching Manifest Branches
+.PP
+To switch to another manifest branch, `repo init \fB\-b\fR otherbranch` may be used in
+an existing client. However, as this only updates the manifest, a subsequent
+`repo sync` (or `repo sync \fB\-d\fR`) is necessary to update the working directory
+files.
diff --git a/man/repo-list.1 b/man/repo-list.1
new file mode 100644
index 0000000..7f85e61
--- /dev/null
+++ b/man/repo-list.1
@@ -0,0 +1,61 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo list" "Repo Manual"
+.SH NAME
+repo \- repo list - manual page for repo list
+.SH SYNOPSIS
+.B repo
+\fI\,list \/\fR[\fI\,-f\/\fR] [\fI\,<project>\/\fR...]
+.SH DESCRIPTION
+Summary
+.PP
+List projects and their associated directories
+.PP
+repo list [\-f] \fB\-r\fR str1 [str2]...
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-r\fR, \fB\-\-regex\fR
+filter the project list based on regex or wildcard
+matching of strings
+.TP
+\fB\-g\fR GROUPS, \fB\-\-groups\fR=\fI\,GROUPS\/\fR
+filter the project list based on the groups the
+project is in
+.TP
+\fB\-a\fR, \fB\-\-all\fR
+show projects regardless of checkout state
+.TP
+\fB\-n\fR, \fB\-\-name\-only\fR
+display only the name of the repository
+.TP
+\fB\-p\fR, \fB\-\-path\-only\fR
+display only the path of the repository
+.TP
+\fB\-f\fR, \fB\-\-fullpath\fR
+display the full work tree path instead of the
+relative path
+.TP
+\fB\-\-relative\-to\fR=\fI\,PATH\/\fR
+display paths relative to this one (default: top of
+repo client checkout)
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.PP
+Run `repo help list` to view the detailed manual.
+.SH DETAILS
+.PP
+List all projects; pass '.' to list the project for the cwd.
+.PP
+By default, only projects that currently exist in the checkout are shown. If you
+want to list all projects (using the specified filter settings), use the \fB\-\-all\fR
+option. If you want to show all projects regardless of the manifest groups, then
+also pass \fB\-\-groups\fR all.
+.PP
+This is similar to running: repo forall \fB\-c\fR 'echo "$REPO_PATH : $REPO_PROJECT"'.
diff --git a/man/repo-manifest.1 b/man/repo-manifest.1
new file mode 100644
index 0000000..be46760
--- /dev/null
+++ b/man/repo-manifest.1
@@ -0,0 +1,548 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo manifest" "Repo Manual"
+.SH NAME
+repo \- repo manifest - manual page for repo manifest
+.SH SYNOPSIS
+.B repo
+\fI\,manifest \/\fR[\fI\,-o {-|NAME.xml}\/\fR] [\fI\,-m MANIFEST.xml\/\fR] [\fI\,-r\/\fR]
+.SH DESCRIPTION
+Summary
+.PP
+Manifest inspection utility
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-r\fR, \fB\-\-revision\-as\-HEAD\fR
+save revisions as current HEAD
+.TP
+\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
+temporary manifest to use for this sync
+.TP
+\fB\-\-suppress\-upstream\-revision\fR
+if in \fB\-r\fR mode, do not write the upstream field (only
+of use if the branch names for a sha1 manifest are
+sensitive)
+.TP
+\fB\-\-suppress\-dest\-branch\fR
+if in \fB\-r\fR mode, do not write the dest\-branch field
+(only of use if the branch names for a sha1 manifest
+are sensitive)
+.TP
+\fB\-\-json\fR
+output manifest in JSON format (experimental)
+.TP
+\fB\-\-pretty\fR
+format output for humans to read
+.TP
+\fB\-\-no\-local\-manifests\fR
+ignore local manifests
+.TP
+\fB\-o\fR \-|NAME.xml, \fB\-\-output\-file\fR=\fI\,\-\/\fR|NAME.xml
+file to save the manifest to
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.PP
+Run `repo help manifest` to view the detailed manual.
+.SH DETAILS
+.PP
+With the \fB\-o\fR option, exports the current manifest for inspection. The manifest
+and (if present) local_manifests/ are combined together to produce a single
+manifest file. This file can be stored in a Git repository for use during future
+\&'repo init' invocations.
+.PP
+The \fB\-r\fR option can be used to generate a manifest file with project revisions set
+to the current commit hash. These are known as "revision locked manifests", as
+they don't follow a particular branch. In this case, the 'upstream' attribute is
+set to the ref we were on when the manifest was generated. The 'dest\-branch'
+attribute is set to indicate the remote ref to push changes to via 'repo
+upload'.
+.PP
+repo Manifest Format
+.PP
+A repo manifest describes the structure of a repo client; that is the
+directories that are visible and where they should be obtained from with git.
+.PP
+The basic structure of a manifest is a bare Git repository holding a single
+`default.xml` XML file in the top level directory.
+.PP
+Manifests are inherently version controlled, since they are kept within a Git
+repository. Updates to manifests are automatically obtained by clients during
+`repo sync`.
+.PP
+[TOC]
+.PP
+XML File Format
+.PP
+A manifest XML file (e.g. `default.xml`) roughly conforms to the following DTD:
+.PP
+```xml <!DOCTYPE manifest [
+.TP
+<!ELEMENT manifest (notice?,
+remote*,
+default?,
+manifest\-server?,
+remove\-project*,
+project*,
+extend\-project*,
+repo\-hooks?,
+superproject?,
+contactinfo?,
+include*)>
+.IP
+<!ELEMENT notice (#PCDATA)>
+.IP
+<!ELEMENT remote (annotation*)>
+<!ATTLIST remote name ID #REQUIRED>
+<!ATTLIST remote alias CDATA #IMPLIED>
+<!ATTLIST remote fetch CDATA #REQUIRED>
+<!ATTLIST remote pushurl CDATA #IMPLIED>
+<!ATTLIST remote review CDATA #IMPLIED>
+<!ATTLIST remote revision CDATA #IMPLIED>
+.IP
+<!ELEMENT default EMPTY>
+<!ATTLIST default remote IDREF #IMPLIED>
+<!ATTLIST default revision CDATA #IMPLIED>
+<!ATTLIST default dest\-branch CDATA #IMPLIED>
+<!ATTLIST default upstream CDATA #IMPLIED>
+<!ATTLIST default sync\-j CDATA #IMPLIED>
+<!ATTLIST default sync\-c CDATA #IMPLIED>
+<!ATTLIST default sync\-s CDATA #IMPLIED>
+<!ATTLIST default sync\-tags CDATA #IMPLIED>
+.IP
+<!ELEMENT manifest\-server EMPTY>
+<!ATTLIST manifest\-server url CDATA #REQUIRED>
+.TP
+<!ELEMENT project (annotation*,
+project*,
+copyfile*,
+linkfile*)>
+.TP
+<!ATTLIST project name
+CDATA #REQUIRED>
+.TP
+<!ATTLIST project path
+CDATA #IMPLIED>
+.TP
+<!ATTLIST project remote
+IDREF #IMPLIED>
+.TP
+<!ATTLIST project revision
+CDATA #IMPLIED>
+.IP
+<!ATTLIST project dest\-branch CDATA #IMPLIED>
+<!ATTLIST project groups CDATA #IMPLIED>
+<!ATTLIST project sync\-c CDATA #IMPLIED>
+<!ATTLIST project sync\-s CDATA #IMPLIED>
+<!ATTLIST project sync\-tags CDATA #IMPLIED>
+<!ATTLIST project upstream CDATA #IMPLIED>
+<!ATTLIST project clone\-depth CDATA #IMPLIED>
+<!ATTLIST project force\-path CDATA #IMPLIED>
+.IP
+<!ELEMENT annotation EMPTY>
+<!ATTLIST annotation name CDATA #REQUIRED>
+<!ATTLIST annotation value CDATA #REQUIRED>
+<!ATTLIST annotation keep CDATA "true">
+.IP
+<!ELEMENT copyfile EMPTY>
+<!ATTLIST copyfile src CDATA #REQUIRED>
+<!ATTLIST copyfile dest CDATA #REQUIRED>
+.IP
+<!ELEMENT linkfile EMPTY>
+<!ATTLIST linkfile src CDATA #REQUIRED>
+<!ATTLIST linkfile dest CDATA #REQUIRED>
+.IP
+<!ELEMENT extend\-project EMPTY>
+<!ATTLIST extend\-project name CDATA #REQUIRED>
+<!ATTLIST extend\-project path CDATA #IMPLIED>
+<!ATTLIST extend\-project groups CDATA #IMPLIED>
+<!ATTLIST extend\-project revision CDATA #IMPLIED>
+<!ATTLIST extend\-project remote CDATA #IMPLIED>
+.IP
+<!ELEMENT remove\-project EMPTY>
+<!ATTLIST remove\-project name CDATA #REQUIRED>
+<!ATTLIST remove\-project optional CDATA #IMPLIED>
+.IP
+<!ELEMENT repo\-hooks EMPTY>
+<!ATTLIST repo\-hooks in\-project CDATA #REQUIRED>
+<!ATTLIST repo\-hooks enabled\-list CDATA #REQUIRED>
+.IP
+<!ELEMENT superproject EMPTY>
+<!ATTLIST superproject name CDATA #REQUIRED>
+<!ATTLIST superproject remote IDREF #IMPLIED>
+.IP
+<!ELEMENT contactinfo EMPTY>
+<!ATTLIST contactinfo bugurl CDATA #REQUIRED>
+.IP
+<!ELEMENT include EMPTY>
+<!ATTLIST include name CDATA #REQUIRED>
+<!ATTLIST include groups CDATA #IMPLIED>
+.PP
+]>
+```
+.PP
+For compatibility purposes across repo releases, all unknown elements are
+silently ignored. However, repo reserves all possible names for itself for
+future use. If you want to use custom elements, the `x\-*` namespace is reserved
+for that purpose, and repo guarantees to never allocate any corresponding names.
+.PP
+A description of the elements and their attributes follows.
+.PP
+Element manifest
+.PP
+The root element of the file.
+.PP
+Element notice
+.PP
+Arbitrary text that is displayed to users whenever `repo sync` finishes. The
+content is simply passed through as it exists in the manifest.
+.PP
+Element remote
+.PP
+One or more remote elements may be specified. Each remote element specifies a
+Git URL shared by one or more projects and (optionally) the Gerrit review server
+those projects upload changes through.
+.PP
+Attribute `name`: A short name unique to this manifest file. The name specified
+here is used as the remote name in each project's .git/config, and is therefore
+automatically available to commands like `git fetch`, `git remote`, `git pull`
+and `git push`.
+.PP
+Attribute `alias`: The alias, if specified, is used to override `name` to be set
+as the remote name in each project's .git/config. Its value can be duplicated
+while attribute `name` has to be unique in the manifest file. This helps each
+project to be able to have same remote name which actually points to different
+remote url.
+.PP
+Attribute `fetch`: The Git URL prefix for all projects which use this remote.
+Each project's name is appended to this prefix to form the actual URL used to
+clone the project.
+.PP
+Attribute `pushurl`: The Git "push" URL prefix for all projects which use this
+remote. Each project's name is appended to this prefix to form the actual URL
+used to "git push" the project. This attribute is optional; if not specified
+then "git push" will use the same URL as the `fetch` attribute.
+.PP
+Attribute `review`: Hostname of the Gerrit server where reviews are uploaded to
+by `repo upload`. This attribute is optional; if not specified then `repo
+upload` will not function.
+.PP
+Attribute `revision`: Name of a Git branch (e.g. `main` or `refs/heads/main`).
+Remotes with their own revision will override the default revision.
+.PP
+Element default
+.PP
+At most one default element may be specified. Its remote and revision attributes
+are used when a project element does not specify its own remote or revision
+attribute.
+.PP
+Attribute `remote`: Name of a previously defined remote element. Project
+elements lacking a remote attribute of their own will use this remote.
+.PP
+Attribute `revision`: Name of a Git branch (e.g. `main` or `refs/heads/main`).
+Project elements lacking their own revision attribute will use this revision.
+.PP
+Attribute `dest\-branch`: Name of a Git branch (e.g. `main`). Project elements
+not setting their own `dest\-branch` will inherit this value. If this value is
+not set, projects will use `revision` by default instead.
+.PP
+Attribute `upstream`: Name of the Git ref in which a sha1 can be found. Used
+when syncing a revision locked manifest in \fB\-c\fR mode to avoid having to sync the
+entire ref space. Project elements not setting their own `upstream` will inherit
+this value.
+.PP
+Attribute `sync\-j`: Number of parallel jobs to use when synching.
+.PP
+Attribute `sync\-c`: Set to true to only sync the given Git branch (specified in
+the `revision` attribute) rather than the whole ref space. Project elements
+lacking a sync\-c element of their own will use this value.
+.PP
+Attribute `sync\-s`: Set to true to also sync sub\-projects.
+.PP
+Attribute `sync\-tags`: Set to false to only sync the given Git branch (specified
+in the `revision` attribute) rather than the other ref tags.
+.PP
+Element manifest\-server
+.PP
+At most one manifest\-server may be specified. The url attribute is used to
+specify the URL of a manifest server, which is an XML RPC service.
+.PP
+The manifest server should implement the following RPC methods:
+.IP
+GetApprovedManifest(branch, target)
+.PP
+Return a manifest in which each project is pegged to a known good revision for
+the current branch and target. This is used by repo sync when the \fB\-\-smart\-sync\fR
+option is given.
+.PP
+The target to use is defined by environment variables TARGET_PRODUCT and
+TARGET_BUILD_VARIANT. These variables are used to create a string of the form
+$TARGET_PRODUCT\-$TARGET_BUILD_VARIANT, e.g. passion\-userdebug. If one of those
+variables or both are not present, the program will call GetApprovedManifest
+without the target parameter and the manifest server should choose a reasonable
+default target.
+.IP
+GetManifest(tag)
+.PP
+Return a manifest in which each project is pegged to the revision at the
+specified tag. This is used by repo sync when the \fB\-\-smart\-tag\fR option is given.
+.PP
+Element project
+.PP
+One or more project elements may be specified. Each element describes a single
+Git repository to be cloned into the repo client workspace. You may specify
+Git\-submodules by creating a nested project. Git\-submodules will be
+automatically recognized and inherit their parent's attributes, but those may be
+overridden by an explicitly specified project element.
+.PP
+Attribute `name`: A unique name for this project. The project's name is appended
+onto its remote's fetch URL to generate the actual URL to configure the Git
+remote with. The URL gets formed as:
+.IP
+${remote_fetch}/${project_name}.git
+.PP
+where ${remote_fetch} is the remote's fetch attribute and ${project_name} is the
+project's name attribute. The suffix ".git" is always appended as repo assumes
+the upstream is a forest of bare Git repositories. If the project has a parent
+element, its name will be prefixed by the parent's.
+.PP
+The project name must match the name Gerrit knows, if Gerrit is being used for
+code reviews.
+.PP
+"name" must not be empty, and may not be an absolute path or use "." or ".."
+path components. It is always interpreted relative to the remote's fetch
+settings, so if a different base path is needed, declare a different remote with
+the new settings needed. These restrictions are not enforced for [Local
+Manifests].
+.PP
+Attribute `path`: An optional path relative to the top directory of the repo
+client where the Git working directory for this project should be placed. If not
+supplied the project "name" is used. If the project has a parent element, its
+path will be prefixed by the parent's.
+.PP
+"path" may not be an absolute path or use "." or ".." path components. These
+restrictions are not enforced for [Local Manifests].
+.PP
+If you want to place files into the root of the checkout (e.g. a README or
+Makefile or another build script), use the [copyfile] or [linkfile] elements
+instead.
+.PP
+Attribute `remote`: Name of a previously defined remote element. If not supplied
+the remote given by the default element is used.
+.PP
+Attribute `revision`: Name of the Git branch the manifest wants to track for
+this project. Names can be relative to refs/heads (e.g. just "main") or absolute
+(e.g. "refs/heads/main"). Tags and/or explicit SHA\-1s should work in theory, but
+have not been extensively tested. If not supplied the revision given by the
+remote element is used if applicable, else the default element is used.
+.PP
+Attribute `dest\-branch`: Name of a Git branch (e.g. `main`). When using `repo
+upload`, changes will be submitted for code review on this branch. If
+unspecified both here and in the default element, `revision` is used instead.
+.PP
+Attribute `groups`: List of groups to which this project belongs, whitespace or
+comma separated. All projects belong to the group "all", and each project
+automatically belongs to a group of its name:`name` and path:`path`. E.g. for
+`<project name="monkeys" path="barrel\-of"/>`, that project definition is
+implicitly in the following manifest groups: default, name:monkeys, and
+path:barrel\-of. If you place a project in the group "notdefault", it will not be
+automatically downloaded by repo. If the project has a parent element, the
+`name` and `path` here are the prefixed ones.
+.PP
+Attribute `sync\-c`: Set to true to only sync the given Git branch (specified in
+the `revision` attribute) rather than the whole ref space.
+.PP
+Attribute `sync\-s`: Set to true to also sync sub\-projects.
+.PP
+Attribute `upstream`: Name of the Git ref in which a sha1 can be found. Used
+when syncing a revision locked manifest in \fB\-c\fR mode to avoid having to sync the
+entire ref space.
+.PP
+Attribute `clone\-depth`: Set the depth to use when fetching this project. If
+specified, this value will override any value given to repo init with the
+\fB\-\-depth\fR option on the command line.
+.PP
+Attribute `force\-path`: Set to true to force this project to create the local
+mirror repository according to its `path` attribute (if supplied) rather than
+the `name` attribute. This attribute only applies to the local mirrors syncing,
+it will be ignored when syncing the projects in a client working directory.
+.PP
+Element extend\-project
+.PP
+Modify the attributes of the named project.
+.PP
+This element is mostly useful in a local manifest file, to modify the attributes
+of an existing project without completely replacing the existing project
+definition. This makes the local manifest more robust against changes to the
+original manifest.
+.PP
+Attribute `path`: If specified, limit the change to projects checked out at the
+specified path, rather than all projects with the given name.
+.PP
+Attribute `groups`: List of additional groups to which this project belongs.
+Same syntax as the corresponding element of `project`.
+.PP
+Attribute `revision`: If specified, overrides the revision of the original
+project. Same syntax as the corresponding element of `project`.
+.PP
+Attribute `remote`: If specified, overrides the remote of the original project.
+Same syntax as the corresponding element of `project`.
+.PP
+Element annotation
+.PP
+Zero or more annotation elements may be specified as children of a project or
+remote element. Each element describes a name\-value pair. For projects, this
+name\-value pair will be exported into each project's environment during a
+\&'forall' command, prefixed with `REPO__`. In addition, there is an optional
+attribute "keep" which accepts the case insensitive values "true" (default) or
+"false". This attribute determines whether or not the annotation will be kept
+when exported with the manifest subcommand.
+.PP
+Element copyfile
+.PP
+Zero or more copyfile elements may be specified as children of a project
+element. Each element describes a src\-dest pair of files; the "src" file will be
+copied to the "dest" place during `repo sync` command.
+.PP
+"src" is project relative, "dest" is relative to the top of the tree. Copying
+from paths outside of the project or to paths outside of the repo client is not
+allowed.
+.PP
+"src" and "dest" must be files. Directories or symlinks are not allowed.
+Intermediate paths must not be symlinks either.
+.PP
+Parent directories of "dest" will be automatically created if missing.
+.PP
+Element linkfile
+.PP
+It's just like copyfile and runs at the same time as copyfile but instead of
+copying it creates a symlink.
+.PP
+The symlink is created at "dest" (relative to the top of the tree) and points to
+the path specified by "src" which is a path in the project.
+.PP
+Parent directories of "dest" will be automatically created if missing.
+.PP
+The symlink target may be a file or directory, but it may not point outside of
+the repo client.
+.PP
+Element remove\-project
+.PP
+Deletes the named project from the internal manifest table, possibly allowing a
+subsequent project element in the same manifest file to replace the project with
+a different source.
+.PP
+This element is mostly useful in a local manifest file, where the user can
+remove a project, and possibly replace it with their own definition.
+.PP
+Attribute `optional`: Set to true to ignore remove\-project elements with no
+matching `project` element.
+.PP
+Element repo\-hooks
+.PP
+NB: See the [practical documentation](./repo\-hooks.md) for using repo hooks.
+.PP
+Only one repo\-hooks element may be specified at a time. Attempting to redefine
+it will fail to parse.
+.PP
+Attribute `in\-project`: The project where the hooks are defined. The value must
+match the `name` attribute (**not** the `path` attribute) of a previously
+defined `project` element.
+.PP
+Attribute `enabled\-list`: List of hooks to use, whitespace or comma separated.
+.PP
+Element superproject
+.PP
+*** *Note*: This is currently a WIP. ***
+.PP
+NB: See the [git superprojects documentation](
+https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects) for background
+information.
+.PP
+This element is used to specify the URL of the superproject. It has "name" and
+"remote" as atrributes. Only "name" is required while the others have reasonable
+defaults. At most one superproject may be specified. Attempting to redefine it
+will fail to parse.
+.PP
+Attribute `name`: A unique name for the superproject. This attribute has the
+same meaning as project's name attribute. See the [element
+project](#element\-project) for more information.
+.PP
+Attribute `remote`: Name of a previously defined remote element. If not supplied
+the remote given by the default element is used.
+.PP
+Element contactinfo
+.PP
+*** *Note*: This is currently a WIP. ***
+.PP
+This element is used to let manifest authors self\-register contact info. It has
+"bugurl" as a required atrribute. This element can be repeated, and any later
+entries will clobber earlier ones. This would allow manifest authors who extend
+manifests to specify their own contact info.
+.PP
+Attribute `bugurl`: The URL to file a bug against the manifest owner.
+.PP
+Element include
+.PP
+This element provides the capability of including another manifest file into the
+originating manifest. Normal rules apply for the target manifest to include \- it
+must be a usable manifest on its own.
+.PP
+Attribute `name`: the manifest to include, specified relative to the manifest
+repository's root.
+.PP
+"name" may not be an absolute path or use "." or ".." path components. These
+restrictions are not enforced for [Local Manifests].
+.PP
+Attribute `groups`: List of additional groups to which all projects in the
+included manifest belong. This appends and recurses, meaning all projects in
+sub\-manifests carry all parent include groups. Same syntax as the corresponding
+element of `project`.
+.PP
+Local Manifests
+.PP
+Additional remotes and projects may be added through local manifest files stored
+in `$TOP_DIR/.repo/local_manifests/*.xml`.
+.PP
+For example:
+.IP
+\f(CW$ ls .repo/local_manifests\fR
+.IP
+local_manifest.xml
+another_local_manifest.xml
+.IP
+\f(CW$ cat .repo/local_manifests/local_manifest.xml\fR
+.IP
+<?xml version="1.0" encoding="UTF\-8"?>
+<manifest>
+.IP
+<project path="manifest"
+.IP
+name="tools/manifest" />
+.IP
+<project path="platform\-manifest"
+.IP
+name="platform/manifest" />
+.IP
+</manifest>
+.PP
+Users may add projects to the local manifest(s) prior to a `repo sync`
+invocation, instructing repo to automatically download and manage these extra
+projects.
+.PP
+Manifest files stored in `$TOP_DIR/.repo/local_manifests/*.xml` will be loaded
+in alphabetical order.
+.PP
+Projects from local manifest files are added into local::<local manifest
+filename> group.
+.PP
+The legacy `$TOP_DIR/.repo/local_manifest.xml` path is no longer supported.
+.SS [copyfile]: #Element\-copyfile [linkfile]: #Element\-linkfile [Local Manifests]:
+.PP
+#local\-manifests
diff --git a/man/repo-overview.1 b/man/repo-overview.1
new file mode 100644
index 0000000..a12c764
--- /dev/null
+++ b/man/repo-overview.1
@@ -0,0 +1,39 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo overview" "Repo Manual"
+.SH NAME
+repo \- repo overview - manual page for repo overview
+.SH SYNOPSIS
+.B repo
+\fI\,overview \/\fR[\fI\,--current-branch\/\fR] [\fI\,<project>\/\fR...]
+.SH DESCRIPTION
+Summary
+.PP
+Display overview of unmerged project branches
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-c\fR, \fB\-\-current\-branch\fR
+consider only checked out branches
+.TP
+\fB\-\-no\-current\-branch\fR
+consider all local branches
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.PP
+Run `repo help overview` to view the detailed manual.
+.SH DETAILS
+.PP
+The 'repo overview' command is used to display an overview of the projects
+branches, and list any local commits that have not yet been merged into the
+project.
+.PP
+The \fB\-c\fR/\-\-current\-branch option can be used to restrict the output to only
+branches currently checked out in each project. By default, all branches are
+displayed.
diff --git a/man/repo-prune.1 b/man/repo-prune.1
new file mode 100644
index 0000000..bd68a37
--- /dev/null
+++ b/man/repo-prune.1
@@ -0,0 +1,28 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo prune" "Repo Manual"
+.SH NAME
+repo \- repo prune - manual page for repo prune
+.SH SYNOPSIS
+.B repo
+\fI\,prune \/\fR[\fI\,<project>\/\fR...]
+.SH DESCRIPTION
+Summary
+.PP
+Prune (delete) already merged topics
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
+number of jobs to run in parallel (default: based on
+number of CPU cores)
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.PP
+Run `repo help prune` to view the detailed manual.
diff --git a/man/repo-rebase.1 b/man/repo-rebase.1
new file mode 100644
index 0000000..aa26103
--- /dev/null
+++ b/man/repo-rebase.1
@@ -0,0 +1,55 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo rebase" "Repo Manual"
+.SH NAME
+repo \- repo rebase - manual page for repo rebase
+.SH SYNOPSIS
+.B repo
+\fI\,rebase {\/\fR[\fI\,<project>\/\fR...] \fI\,| -i <project>\/\fR...\fI\,}\/\fR
+.SH DESCRIPTION
+Summary
+.PP
+Rebase local branches on upstream branch
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-\-fail\-fast\fR
+stop rebasing after first error is hit
+.TP
+\fB\-f\fR, \fB\-\-force\-rebase\fR
+pass \fB\-\-force\-rebase\fR to git rebase
+.TP
+\fB\-\-no\-ff\fR
+pass \fB\-\-no\-ff\fR to git rebase
+.TP
+\fB\-\-autosquash\fR
+pass \fB\-\-autosquash\fR to git rebase
+.TP
+\fB\-\-whitespace\fR=\fI\,WS\/\fR
+pass \fB\-\-whitespace\fR to git rebase
+.TP
+\fB\-\-auto\-stash\fR
+stash local modifications before starting
+.TP
+\fB\-m\fR, \fB\-\-onto\-manifest\fR
+rebase onto the manifest version instead of upstream
+HEAD (this helps to make sure the local tree stays
+consistent if you previously synced to a manifest)
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.TP
+\fB\-i\fR, \fB\-\-interactive\fR
+interactive rebase (single project only)
+.PP
+Run `repo help rebase` to view the detailed manual.
+.SH DETAILS
+.PP
+\&'repo rebase' uses git rebase to move local changes in the current topic branch
+to the HEAD of the upstream history, useful when you have made commits in a
+topic branch but need to incorporate new upstream changes "underneath" them.
diff --git a/man/repo-selfupdate.1 b/man/repo-selfupdate.1
new file mode 100644
index 0000000..70c855a
--- /dev/null
+++ b/man/repo-selfupdate.1
@@ -0,0 +1,35 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo selfupdate" "Repo Manual"
+.SH NAME
+repo \- repo selfupdate - manual page for repo selfupdate
+.SH SYNOPSIS
+.B repo
+\fI\,selfupdate\/\fR
+.SH DESCRIPTION
+Summary
+.PP
+Update repo to the latest version
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.SS repo Version options:
+.TP
+\fB\-\-no\-repo\-verify\fR
+do not verify repo source code
+.PP
+Run `repo help selfupdate` to view the detailed manual.
+.SH DETAILS
+.PP
+The 'repo selfupdate' command upgrades repo to the latest version, if a newer
+version is available.
+.PP
+Normally this is done automatically by 'repo sync' and does not need to be
+performed by an end\-user.
diff --git a/man/repo-smartsync.1 b/man/repo-smartsync.1
new file mode 100644
index 0000000..5d93911
--- /dev/null
+++ b/man/repo-smartsync.1
@@ -0,0 +1,118 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo smartsync" "Repo Manual"
+.SH NAME
+repo \- repo smartsync - manual page for repo smartsync
+.SH SYNOPSIS
+.B repo
+\fI\,smartsync \/\fR[\fI\,<project>\/\fR...]
+.SH DESCRIPTION
+Summary
+.PP
+Update working tree to the latest known good revision
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
+number of jobs to run in parallel (default: based on
+number of CPU cores)
+.TP
+\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
+number of network jobs to run in parallel (defaults to
+\fB\-\-jobs\fR)
+.TP
+\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
+number of local checkout jobs to run in parallel
+(defaults to \fB\-\-jobs\fR)
+.TP
+\fB\-f\fR, \fB\-\-force\-broken\fR
+obsolete option (to be deleted in the future)
+.TP
+\fB\-\-fail\-fast\fR
+stop syncing after first error is hit
+.TP
+\fB\-\-force\-sync\fR
+overwrite an existing git directory if it needs to
+point to a different object directory. WARNING: this
+may cause loss of data
+.TP
+\fB\-\-force\-remove\-dirty\fR
+force remove projects with uncommitted modifications
+if projects no longer exist in the manifest. WARNING:
+this may cause loss of data
+.TP
+\fB\-l\fR, \fB\-\-local\-only\fR
+only update working tree, don't fetch
+.TP
+\fB\-\-no\-manifest\-update\fR, \fB\-\-nmu\fR
+use the existing manifest checkout as\-is. (do not
+update to the latest revision)
+.TP
+\fB\-n\fR, \fB\-\-network\-only\fR
+fetch only, don't update working tree
+.TP
+\fB\-d\fR, \fB\-\-detach\fR
+detach projects back to manifest revision
+.TP
+\fB\-c\fR, \fB\-\-current\-branch\fR
+fetch only current branch from server
+.TP
+\fB\-\-no\-current\-branch\fR
+fetch all branches from server
+.TP
+\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
+temporary manifest to use for this sync
+.TP
+\fB\-\-clone\-bundle\fR
+enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
+.TP
+\fB\-\-no\-clone\-bundle\fR
+disable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
+.TP
+\fB\-u\fR MANIFEST_SERVER_USERNAME, \fB\-\-manifest\-server\-username\fR=\fI\,MANIFEST_SERVER_USERNAME\/\fR
+username to authenticate with the manifest server
+.TP
+\fB\-p\fR MANIFEST_SERVER_PASSWORD, \fB\-\-manifest\-server\-password\fR=\fI\,MANIFEST_SERVER_PASSWORD\/\fR
+password to authenticate with the manifest server
+.TP
+\fB\-\-fetch\-submodules\fR
+fetch submodules from server
+.TP
+\fB\-\-use\-superproject\fR
+use the manifest superproject to sync projects
+.TP
+\fB\-\-no\-use\-superproject\fR
+disable use of manifest superprojects
+.TP
+\fB\-\-tags\fR
+fetch tags
+.TP
+\fB\-\-no\-tags\fR
+don't fetch tags
+.TP
+\fB\-\-optimized\-fetch\fR
+only fetch projects fixed to sha1 if revision does not
+exist locally
+.TP
+\fB\-\-retry\-fetches\fR=\fI\,RETRY_FETCHES\/\fR
+number of times to retry fetches on transient errors
+.TP
+\fB\-\-prune\fR
+delete refs that no longer exist on the remote
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.SS repo Version options:
+.TP
+\fB\-\-no\-repo\-verify\fR
+do not verify repo source code
+.PP
+Run `repo help smartsync` to view the detailed manual.
+.SH DETAILS
+.PP
+The 'repo smartsync' command is a shortcut for sync \fB\-s\fR.
diff --git a/man/repo-stage.1 b/man/repo-stage.1
new file mode 100644
index 0000000..07e1cac
--- /dev/null
+++ b/man/repo-stage.1
@@ -0,0 +1,30 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo stage" "Repo Manual"
+.SH NAME
+repo \- repo stage - manual page for repo stage
+.SH SYNOPSIS
+.B repo
+\fI\,stage -i \/\fR[\fI\,<project>\/\fR...]
+.SH DESCRIPTION
+Summary
+.PP
+Stage file(s) for commit
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.TP
+\fB\-i\fR, \fB\-\-interactive\fR
+use interactive staging
+.PP
+Run `repo help stage` to view the detailed manual.
+.SH DETAILS
+.PP
+The 'repo stage' command stages files to prepare the next commit.
diff --git a/man/repo-start.1 b/man/repo-start.1
new file mode 100644
index 0000000..b00a31f
--- /dev/null
+++ b/man/repo-start.1
@@ -0,0 +1,41 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo start" "Repo Manual"
+.SH NAME
+repo \- repo start - manual page for repo start
+.SH SYNOPSIS
+.B repo
+\fI\,start <newbranchname> \/\fR[\fI\,--all | <project>\/\fR...]
+.SH DESCRIPTION
+Summary
+.PP
+Start a new branch for development
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
+number of jobs to run in parallel (default: based on
+number of CPU cores)
+.TP
+\fB\-\-all\fR
+begin branch in all projects
+.TP
+\fB\-r\fR REVISION, \fB\-\-rev\fR=\fI\,REVISION\/\fR, \fB\-\-revision\fR=\fI\,REVISION\/\fR
+point branch at this revision instead of upstream
+.TP
+\fB\-\-head\fR, \fB\-\-HEAD\fR
+abbreviation for \fB\-\-rev\fR HEAD
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.PP
+Run `repo help start` to view the detailed manual.
+.SH DETAILS
+.PP
+\&'repo start' begins a new branch of development, starting from the revision
+specified in the manifest.
diff --git a/man/repo-status.1 b/man/repo-status.1
new file mode 100644
index 0000000..fbae2c5
--- /dev/null
+++ b/man/repo-status.1
@@ -0,0 +1,98 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo status" "Repo Manual"
+.SH NAME
+repo \- repo status - manual page for repo status
+.SH SYNOPSIS
+.B repo
+\fI\,status \/\fR[\fI\,<project>\/\fR...]
+.SH DESCRIPTION
+Summary
+.PP
+Show the working tree status
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
+number of jobs to run in parallel (default: based on
+number of CPU cores)
+.TP
+\fB\-o\fR, \fB\-\-orphans\fR
+include objects in working directory outside of repo
+projects
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.PP
+Run `repo help status` to view the detailed manual.
+.SH DETAILS
+.PP
+\&'repo status' compares the working tree to the staging area (aka index), and the
+most recent commit on this branch (HEAD), in each project specified. A summary
+is displayed, one line per file where there is a difference between these three
+states.
+.PP
+The \fB\-j\fR/\-\-jobs option can be used to run multiple status queries in parallel.
+.PP
+The \fB\-o\fR/\-\-orphans option can be used to show objects that are in the working
+directory, but not associated with a repo project. This includes unmanaged
+top\-level files and directories, but also includes deeper items. For example, if
+dir/subdir/proj1 and dir/subdir/proj2 are repo projects, dir/subdir/proj3 will
+be shown if it is not known to repo.
+.PP
+Status Display
+.PP
+The status display is organized into three columns of information, for example
+if the file 'subcmds/status.py' is modified in the project 'repo' on branch
+\&'devwork':
+.TP
+project repo/
+branch devwork
+.TP
+\fB\-m\fR
+subcmds/status.py
+.PP
+The first column explains how the staging area (index) differs from the last
+commit (HEAD). Its values are always displayed in upper case and have the
+following meanings:
+.TP
+\-:
+no difference
+.TP
+A:
+added (not in HEAD, in index )
+.TP
+M:
+modified ( in HEAD, in index, different content )
+.TP
+D:
+deleted ( in HEAD, not in index )
+.TP
+R:
+renamed (not in HEAD, in index, path changed )
+.TP
+C:
+copied (not in HEAD, in index, copied from another)
+.TP
+T:
+mode changed ( in HEAD, in index, same content )
+.TP
+U:
+unmerged; conflict resolution required
+.PP
+The second column explains how the working directory differs from the index. Its
+values are always displayed in lower case and have the following meanings:
+.TP
+\-:
+new / unknown (not in index, in work tree )
+.TP
+m:
+modified ( in index, in work tree, modified )
+.TP
+d:
+deleted ( in index, not in work tree )
diff --git a/man/repo-sync.1 b/man/repo-sync.1
new file mode 100644
index 0000000..c87c970
--- /dev/null
+++ b/man/repo-sync.1
@@ -0,0 +1,209 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo sync" "Repo Manual"
+.SH NAME
+repo \- repo sync - manual page for repo sync
+.SH SYNOPSIS
+.B repo
+\fI\,sync \/\fR[\fI\,<project>\/\fR...]
+.SH DESCRIPTION
+Summary
+.PP
+Update working tree to the latest revision
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
+number of jobs to run in parallel (default: based on
+number of CPU cores)
+.TP
+\fB\-\-jobs\-network\fR=\fI\,JOBS\/\fR
+number of network jobs to run in parallel (defaults to
+\fB\-\-jobs\fR)
+.TP
+\fB\-\-jobs\-checkout\fR=\fI\,JOBS\/\fR
+number of local checkout jobs to run in parallel
+(defaults to \fB\-\-jobs\fR)
+.TP
+\fB\-f\fR, \fB\-\-force\-broken\fR
+obsolete option (to be deleted in the future)
+.TP
+\fB\-\-fail\-fast\fR
+stop syncing after first error is hit
+.TP
+\fB\-\-force\-sync\fR
+overwrite an existing git directory if it needs to
+point to a different object directory. WARNING: this
+may cause loss of data
+.TP
+\fB\-\-force\-remove\-dirty\fR
+force remove projects with uncommitted modifications
+if projects no longer exist in the manifest. WARNING:
+this may cause loss of data
+.TP
+\fB\-l\fR, \fB\-\-local\-only\fR
+only update working tree, don't fetch
+.TP
+\fB\-\-no\-manifest\-update\fR, \fB\-\-nmu\fR
+use the existing manifest checkout as\-is. (do not
+update to the latest revision)
+.TP
+\fB\-n\fR, \fB\-\-network\-only\fR
+fetch only, don't update working tree
+.TP
+\fB\-d\fR, \fB\-\-detach\fR
+detach projects back to manifest revision
+.TP
+\fB\-c\fR, \fB\-\-current\-branch\fR
+fetch only current branch from server
+.TP
+\fB\-\-no\-current\-branch\fR
+fetch all branches from server
+.TP
+\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
+temporary manifest to use for this sync
+.TP
+\fB\-\-clone\-bundle\fR
+enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
+.TP
+\fB\-\-no\-clone\-bundle\fR
+disable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
+.TP
+\fB\-u\fR MANIFEST_SERVER_USERNAME, \fB\-\-manifest\-server\-username\fR=\fI\,MANIFEST_SERVER_USERNAME\/\fR
+username to authenticate with the manifest server
+.TP
+\fB\-p\fR MANIFEST_SERVER_PASSWORD, \fB\-\-manifest\-server\-password\fR=\fI\,MANIFEST_SERVER_PASSWORD\/\fR
+password to authenticate with the manifest server
+.TP
+\fB\-\-fetch\-submodules\fR
+fetch submodules from server
+.TP
+\fB\-\-use\-superproject\fR
+use the manifest superproject to sync projects
+.TP
+\fB\-\-no\-use\-superproject\fR
+disable use of manifest superprojects
+.TP
+\fB\-\-tags\fR
+fetch tags
+.TP
+\fB\-\-no\-tags\fR
+don't fetch tags
+.TP
+\fB\-\-optimized\-fetch\fR
+only fetch projects fixed to sha1 if revision does not
+exist locally
+.TP
+\fB\-\-retry\-fetches\fR=\fI\,RETRY_FETCHES\/\fR
+number of times to retry fetches on transient errors
+.TP
+\fB\-\-prune\fR
+delete refs that no longer exist on the remote
+.TP
+\fB\-s\fR, \fB\-\-smart\-sync\fR
+smart sync using manifest from the latest known good
+build
+.TP
+\fB\-t\fR SMART_TAG, \fB\-\-smart\-tag\fR=\fI\,SMART_TAG\/\fR
+smart sync using manifest from a known tag
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.SS repo Version options:
+.TP
+\fB\-\-no\-repo\-verify\fR
+do not verify repo source code
+.PP
+Run `repo help sync` to view the detailed manual.
+.SH DETAILS
+.PP
+The 'repo sync' command synchronizes local project directories with the remote
+repositories specified in the manifest. If a local project does not yet exist,
+it will clone a new local directory from the remote repository and set up
+tracking branches as specified in the manifest. If the local project already
+exists, 'repo sync' will update the remote branches and rebase any new local
+changes on top of the new remote changes.
+.PP
+\&'repo sync' will synchronize all projects listed at the command line. Projects
+can be specified either by name, or by a relative or absolute path to the
+project's local directory. If no projects are specified, 'repo sync' will
+synchronize all projects listed in the manifest.
+.PP
+The \fB\-d\fR/\-\-detach option can be used to switch specified projects back to the
+manifest revision. This option is especially helpful if the project is currently
+on a topic branch, but the manifest revision is temporarily needed.
+.PP
+The \fB\-s\fR/\-\-smart\-sync option can be used to sync to a known good build as
+specified by the manifest\-server element in the current manifest. The
+\fB\-t\fR/\-\-smart\-tag option is similar and allows you to specify a custom tag/label.
+.PP
+The \fB\-u\fR/\-\-manifest\-server\-username and \fB\-p\fR/\-\-manifest\-server\-password options can
+be used to specify a username and password to authenticate with the manifest
+server when using the \fB\-s\fR or \fB\-t\fR option.
+.PP
+If \fB\-u\fR and \fB\-p\fR are not specified when using the \fB\-s\fR or \fB\-t\fR option, 'repo sync' will
+attempt to read authentication credentials for the manifest server from the
+user's .netrc file.
+.PP
+\&'repo sync' will not use authentication credentials from \fB\-u\fR/\-p or .netrc if the
+manifest server specified in the manifest file already includes credentials.
+.PP
+By default, all projects will be synced. The \fB\-\-fail\-fast\fR option can be used to
+halt syncing as soon as possible when the first project fails to sync.
+.PP
+The \fB\-\-force\-sync\fR option can be used to overwrite existing git directories if
+they have previously been linked to a different object directory. WARNING: This
+may cause data to be lost since refs may be removed when overwriting.
+.PP
+The \fB\-\-force\-remove\-dirty\fR option can be used to remove previously used projects
+with uncommitted changes. WARNING: This may cause data to be lost since
+uncommitted changes may be removed with projects that no longer exist in the
+manifest.
+.PP
+The \fB\-\-no\-clone\-bundle\fR option disables any attempt to use \fI\,$URL/clone.bundle\/\fP to
+bootstrap a new Git repository from a resumeable bundle file on a content
+delivery network. This may be necessary if there are problems with the local
+Python HTTP client or proxy configuration, but the Git binary works.
+.PP
+The \fB\-\-fetch\-submodules\fR option enables fetching Git submodules of a project from
+server.
+.PP
+The \fB\-c\fR/\-\-current\-branch option can be used to only fetch objects that are on the
+branch specified by a project's revision.
+.PP
+The \fB\-\-optimized\-fetch\fR option can be used to only fetch projects that are fixed
+to a sha1 revision if the sha1 revision does not already exist locally.
+.PP
+The \fB\-\-prune\fR option can be used to remove any refs that no longer exist on the
+remote.
+.PP
+SSH Connections
+.PP
+If at least one project remote URL uses an SSH connection (ssh://, git+ssh://,
+or user@host:path syntax) repo will automatically enable the SSH ControlMaster
+option when connecting to that host. This feature permits other projects in the
+same 'repo sync' session to reuse the same SSH tunnel, saving connection setup
+overheads.
+.PP
+To disable this behavior on UNIX platforms, set the GIT_SSH environment variable
+to 'ssh'. For example:
+.IP
+export GIT_SSH=ssh
+repo sync
+.PP
+Compatibility
+.PP
+This feature is automatically disabled on Windows, due to the lack of UNIX
+domain socket support.
+.PP
+This feature is not compatible with url.insteadof rewrites in the user's
+~/.gitconfig. 'repo sync' is currently not able to perform the rewrite early
+enough to establish the ControlMaster tunnel.
+.PP
+If the remote SSH daemon is Gerrit Code Review, version 2.0.10 or later is
+required to fix a server side protocol bug.
diff --git a/man/repo-upload.1 b/man/repo-upload.1
new file mode 100644
index 0000000..36a0dac
--- /dev/null
+++ b/man/repo-upload.1
@@ -0,0 +1,175 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo upload" "Repo Manual"
+.SH NAME
+repo \- repo upload - manual page for repo upload
+.SH SYNOPSIS
+.B repo
+\fI\,upload \/\fR[\fI\,--re --cc\/\fR] [\fI\,<project>\/\fR]...
+.SH DESCRIPTION
+Summary
+.PP
+Upload changes for code review
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-j\fR JOBS, \fB\-\-jobs\fR=\fI\,JOBS\/\fR
+number of jobs to run in parallel (default: based on
+number of CPU cores)
+.TP
+\fB\-t\fR
+send local branch name to Gerrit Code Review
+.TP
+\fB\-\-hashtag\fR=\fI\,HASHTAGS\/\fR, \fB\-\-ht\fR=\fI\,HASHTAGS\/\fR
+add hashtags (comma delimited) to the review
+.TP
+\fB\-\-hashtag\-branch\fR, \fB\-\-htb\fR
+add local branch name as a hashtag
+.TP
+\fB\-l\fR LABELS, \fB\-\-label\fR=\fI\,LABELS\/\fR
+add a label when uploading
+.TP
+\fB\-\-re\fR=\fI\,REVIEWERS\/\fR, \fB\-\-reviewers\fR=\fI\,REVIEWERS\/\fR
+request reviews from these people
+.TP
+\fB\-\-cc\fR=\fI\,CC\/\fR
+also send email to these email addresses
+.TP
+\fB\-\-br\fR=\fI\,BRANCH\/\fR, \fB\-\-branch\fR=\fI\,BRANCH\/\fR
+(local) branch to upload
+.TP
+\fB\-c\fR, \fB\-\-current\-branch\fR
+upload current git branch
+.TP
+\fB\-\-no\-current\-branch\fR
+upload all git branches
+.TP
+\fB\-\-ne\fR, \fB\-\-no\-emails\fR
+do not send e\-mails on upload
+.TP
+\fB\-p\fR, \fB\-\-private\fR
+upload as a private change (deprecated; use \fB\-\-wip\fR)
+.TP
+\fB\-w\fR, \fB\-\-wip\fR
+upload as a work\-in\-progress change
+.TP
+\fB\-o\fR PUSH_OPTIONS, \fB\-\-push\-option\fR=\fI\,PUSH_OPTIONS\/\fR
+additional push options to transmit
+.TP
+\fB\-D\fR BRANCH, \fB\-\-destination\fR=\fI\,BRANCH\/\fR, \fB\-\-dest\fR=\fI\,BRANCH\/\fR
+submit for review on this target branch
+.TP
+\fB\-n\fR, \fB\-\-dry\-run\fR
+do everything except actually upload the CL
+.TP
+\fB\-y\fR, \fB\-\-yes\fR
+answer yes to all safe prompts
+.TP
+\fB\-\-no\-cert\-checks\fR
+disable verifying ssl certs (unsafe)
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.SS pre\-upload hooks:
+.TP
+\fB\-\-no\-verify\fR
+Do not run the pre\-upload hook.
+.TP
+\fB\-\-verify\fR
+Run the pre\-upload hook without prompting.
+.TP
+\fB\-\-ignore\-hooks\fR
+Do not abort if pre\-upload hooks fail.
+.PP
+Run `repo help upload` to view the detailed manual.
+.SH DETAILS
+.PP
+The 'repo upload' command is used to send changes to the Gerrit Code Review
+system. It searches for topic branches in local projects that have not yet been
+published for review. If multiple topic branches are found, 'repo upload' opens
+an editor to allow the user to select which branches to upload.
+.PP
+\&'repo upload' searches for uploadable changes in all projects listed at the
+command line. Projects can be specified either by name, or by a relative or
+absolute path to the project's local directory. If no projects are specified,
+\&'repo upload' will search for uploadable changes in all projects listed in the
+manifest.
+.PP
+If the \fB\-\-reviewers\fR or \fB\-\-cc\fR options are passed, those emails are added to the
+respective list of users, and emails are sent to any new users. Users passed as
+\fB\-\-reviewers\fR must already be registered with the code review system, or the
+upload will fail.
+.PP
+Configuration
+.PP
+review.URL.autoupload:
+.PP
+To disable the "Upload ... (y/N)?" prompt, you can set a per\-project or global
+Git configuration option. If review.URL.autoupload is set to "true" then repo
+will assume you always answer "y" at the prompt, and will not prompt you
+further. If it is set to "false" then repo will assume you always answer "n",
+and will abort.
+.PP
+review.URL.autoreviewer:
+.PP
+To automatically append a user or mailing list to reviews, you can set a
+per\-project or global Git option to do so.
+.PP
+review.URL.autocopy:
+.PP
+To automatically copy a user or mailing list to all uploaded reviews, you can
+set a per\-project or global Git option to do so. Specifically,
+review.URL.autocopy can be set to a comma separated list of reviewers who you
+always want copied on all uploads with a non\-empty \fB\-\-re\fR argument.
+.PP
+review.URL.username:
+.PP
+Override the username used to connect to Gerrit Code Review. By default the
+local part of the email address is used.
+.PP
+The URL must match the review URL listed in the manifest XML file, or in the
+\&.git/config within the project. For example:
+.IP
+[remote "origin"]
+.IP
+url = git://git.example.com/project.git
+review = http://review.example.com/
+.IP
+[review "http://review.example.com/"]
+.IP
+autoupload = true
+autocopy = johndoe@company.com,my\-team\-alias@company.com
+.PP
+review.URL.uploadtopic:
+.PP
+To add a topic branch whenever uploading a commit, you can set a per\-project or
+global Git option to do so. If review.URL.uploadtopic is set to "true" then repo
+will assume you always want the equivalent of the \fB\-t\fR option to the repo command.
+If unset or set to "false" then repo will make use of only the command line
+option.
+.PP
+review.URL.uploadhashtags:
+.PP
+To add hashtags whenever uploading a commit, you can set a per\-project or global
+Git option to do so. The value of review.URL.uploadhashtags will be used as
+comma delimited hashtags like the \fB\-\-hashtag\fR option.
+.PP
+review.URL.uploadlabels:
+.PP
+To add labels whenever uploading a commit, you can set a per\-project or global
+Git option to do so. The value of review.URL.uploadlabels will be used as comma
+delimited labels like the \fB\-\-label\fR option.
+.PP
+review.URL.uploadnotify:
+.PP
+Control e\-mail notifications when uploading.
+https://gerrit\-review.googlesource.com/Documentation/user\-upload.html#notify
+.PP
+References
+.PP
+Gerrit Code Review: https://www.gerritcodereview.com/
diff --git a/man/repo-version.1 b/man/repo-version.1
new file mode 100644
index 0000000..cc703f6
--- /dev/null
+++ b/man/repo-version.1
@@ -0,0 +1,24 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo version" "Repo Manual"
+.SH NAME
+repo \- repo version - manual page for repo version
+.SH SYNOPSIS
+.B repo
+\fI\,version\/\fR
+.SH DESCRIPTION
+Summary
+.PP
+Display the version of repo
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.SS Logging options:
+.TP
+\fB\-v\fR, \fB\-\-verbose\fR
+show all output
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+only show errors
+.PP
+Run `repo help version` to view the detailed manual.
diff --git a/man/repo.1 b/man/repo.1
new file mode 100644
index 0000000..4aa7638
--- /dev/null
+++ b/man/repo.1
@@ -0,0 +1,133 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
+.TH REPO "1" "July 2021" "repo" "Repo Manual"
+.SH NAME
+repo \- repository management tool built on top of git
+.SH SYNOPSIS
+.B repo
+[\fI\,-p|--paginate|--no-pager\/\fR] \fI\,COMMAND \/\fR[\fI\,ARGS\/\fR]
+.SH OPTIONS
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-\-help\-all\fR
+show this help message with all subcommands and exit
+.TP
+\fB\-p\fR, \fB\-\-paginate\fR
+display command output in the pager
+.TP
+\fB\-\-no\-pager\fR
+disable the pager
+.TP
+\fB\-\-color\fR=\fI\,COLOR\/\fR
+control color usage: auto, always, never
+.TP
+\fB\-\-trace\fR
+trace git command execution (REPO_TRACE=1)
+.TP
+\fB\-\-trace\-python\fR
+trace python command execution
+.TP
+\fB\-\-time\fR
+time repo command execution
+.TP
+\fB\-\-version\fR
+display this version of repo
+.TP
+\fB\-\-show\-toplevel\fR
+display the path of the top\-level directory of the
+repo client checkout
+.TP
+\fB\-\-event\-log\fR=\fI\,EVENT_LOG\/\fR
+filename of event log to append timeline to
+.TP
+\fB\-\-git\-trace2\-event\-log\fR=\fI\,GIT_TRACE2_EVENT_LOG\/\fR
+directory to write git trace2 event log to
+.SS "The complete list of recognized repo commands are:"
+.TP
+abandon
+Permanently abandon a development branch
+.TP
+branch
+View current topic branches
+.TP
+branches
+View current topic branches
+.TP
+checkout
+Checkout a branch for development
+.TP
+cherry\-pick
+Cherry\-pick a change.
+.TP
+diff
+Show changes between commit and working tree
+.TP
+diffmanifests
+Manifest diff utility
+.TP
+download
+Download and checkout a change
+.TP
+forall
+Run a shell command in each project
+.TP
+gitc\-delete
+Delete a GITC Client.
+.TP
+gitc\-init
+Initialize a GITC Client.
+.TP
+grep
+Print lines matching a pattern
+.TP
+help
+Display detailed help on a command
+.TP
+info
+Get info on the manifest branch, current branch or unmerged branches
+.TP
+init
+Initialize a repo client checkout in the current directory
+.TP
+list
+List projects and their associated directories
+.TP
+manifest
+Manifest inspection utility
+.TP
+overview
+Display overview of unmerged project branches
+.TP
+prune
+Prune (delete) already merged topics
+.TP
+rebase
+Rebase local branches on upstream branch
+.TP
+selfupdate
+Update repo to the latest version
+.TP
+smartsync
+Update working tree to the latest known good revision
+.TP
+stage
+Stage file(s) for commit
+.TP
+start
+Start a new branch for development
+.TP
+status
+Show the working tree status
+.TP
+sync
+Update working tree to the latest revision
+.TP
+upload
+Upload changes for code review
+.TP
+version
+Display the version of repo
+.PP
+See 'repo help <command>' for more information on a specific command.
+Bug reports: https://bugs.chromium.org/p/gerrit/issues/entry?template=Repo+tool+issue
diff --git a/manifest_xml.py b/manifest_xml.py
index 0c2b45e..68ead53 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import collections
import itertools
import os
import platform
@@ -24,14 +25,21 @@
from git_config import GitConfig, IsId
from git_refs import R_HEADS, HEAD
import platform_utils
-from project import RemoteSpec, Project, MetaProject
+from project import Annotation, RemoteSpec, Project, MetaProject
from error import (ManifestParseError, ManifestInvalidPathError,
ManifestInvalidRevisionError)
+from wrapper import Wrapper
MANIFEST_FILE_NAME = 'manifest.xml'
LOCAL_MANIFEST_NAME = 'local_manifest.xml'
LOCAL_MANIFESTS_DIR_NAME = 'local_manifests'
+# Add all projects from local manifest into a group.
+LOCAL_MANIFEST_GROUP_PREFIX = 'local:'
+
+# ContactInfo has the self-registered bug url, supplied by the manifest authors.
+ContactInfo = collections.namedtuple('ContactInfo', 'bugurl')
+
# urljoin gets confused if the scheme is not known.
urllib.parse.uses_relative.extend([
'ssh',
@@ -114,9 +122,13 @@
sync_tags = True
def __eq__(self, other):
+ if not isinstance(other, _Default):
+ return False
return self.__dict__ == other.__dict__
def __ne__(self, other):
+ if not isinstance(other, _Default):
+ return True
return self.__dict__ != other.__dict__
@@ -137,14 +149,22 @@
self.reviewUrl = review
self.revision = revision
self.resolvedFetchUrl = self._resolveFetchUrl()
+ self.annotations = []
def __eq__(self, other):
- return self.__dict__ == other.__dict__
+ if not isinstance(other, _XmlRemote):
+ return False
+ return (sorted(self.annotations) == sorted(other.annotations) and
+ self.name == other.name and self.fetchUrl == other.fetchUrl and
+ self.pushUrl == other.pushUrl and self.remoteAlias == other.remoteAlias
+ and self.reviewUrl == other.reviewUrl and self.revision == other.revision)
def __ne__(self, other):
- return self.__dict__ != other.__dict__
+ return not self.__eq__(other)
def _resolveFetchUrl(self):
+ if self.fetchUrl is None:
+ return ''
url = self.fetchUrl.rstrip('/')
manifestUrl = self.manifestUrl.rstrip('/')
# urljoin will gets confused over quite a few things. The ones we care
@@ -173,6 +193,9 @@
orig_name=self.name,
fetchUrl=self.fetchUrl)
+ def AddAnnotation(self, name, value, keep):
+ self.annotations.append(Annotation(name, value, keep))
+
class XmlManifest(object):
"""manages the repo configuration file"""
@@ -247,8 +270,7 @@
self.Override(name)
# Old versions of repo would generate symlinks we need to clean up.
- if os.path.lexists(self.manifestFile):
- platform_utils.remove(self.manifestFile)
+ platform_utils.remove(self.manifestFile, missing_ok=True)
# This file is interpreted as if it existed inside the manifest repo.
# That allows us to use <include> with the relative file name.
with open(self.manifestFile, 'w') as fp:
@@ -282,6 +304,13 @@
if r.revision is not None:
e.setAttribute('revision', r.revision)
+ for a in r.annotations:
+ if a.keep == 'true':
+ ae = doc.createElement('annotation')
+ ae.setAttribute('name', a.name)
+ ae.setAttribute('value', a.value)
+ e.appendChild(ae)
+
def _ParseList(self, field):
"""Parse fields that contain flattened lists.
@@ -477,6 +506,15 @@
if not d.remote or remote.orig_name != remoteName:
remoteName = remote.orig_name
e.setAttribute('remote', remoteName)
+ revision = remote.revision or d.revisionExpr
+ if not revision or revision != self._superproject['revision']:
+ e.setAttribute('revision', self._superproject['revision'])
+ root.appendChild(e)
+
+ if self._contactinfo.bugurl != Wrapper().BUG_URL:
+ root.appendChild(doc.createTextNode(''))
+ e = doc.createElement('contactinfo')
+ e.setAttribute('bugurl', self._contactinfo.bugurl)
root.appendChild(e)
return doc
@@ -490,6 +528,7 @@
'manifest-server',
'repo-hooks',
'superproject',
+ 'contactinfo',
}
# Elements that may be repeated.
MULTI_ELEMENTS = {
@@ -566,6 +605,11 @@
return self._superproject
@property
+ def contactinfo(self):
+ self._Load()
+ return self._contactinfo
+
+ @property
def notice(self):
self._Load()
return self._notice
@@ -596,6 +640,17 @@
return set(x.strip() for x in exclude.split(','))
@property
+ def UseLocalManifests(self):
+ return self._load_local_manifests
+
+ def SetUseLocalManifests(self, value):
+ self._load_local_manifests = value
+
+ @property
+ def HasLocalManifests(self):
+ return self._load_local_manifests and self.local_manifests
+
+ @property
def IsMirror(self):
return self.manifestProject.config.GetBoolean('repo.mirror')
@@ -630,6 +685,7 @@
self._default = None
self._repo_hooks_project = None
self._superproject = {}
+ self._contactinfo = ContactInfo(Wrapper().BUG_URL)
self._notice = None
self.branch = None
self._manifest_server = None
@@ -657,7 +713,9 @@
# Since local manifests are entirely managed by the user, allow
# them to point anywhere the user wants.
nodes.append(self._ParseManifestXml(
- local, self.repodir, restrict_includes=False))
+ local, self.repodir,
+ parent_groups=f'{LOCAL_MANIFEST_GROUP_PREFIX}:{local_file[:-4]}',
+ restrict_includes=False))
except OSError:
pass
@@ -754,9 +812,10 @@
for node in itertools.chain(*node_list):
if node.nodeName == 'default':
new_default = self._ParseDefault(node)
+ emptyDefault = not node.hasAttributes() and not node.hasChildNodes()
if self._default is None:
self._default = new_default
- elif new_default != self._default:
+ elif not emptyDefault and new_default != self._default:
raise ManifestParseError('duplicate default in %s' %
(self.manifestFile))
@@ -795,6 +854,8 @@
for subproject in project.subprojects:
recursively_add_projects(subproject)
+ repo_hooks_project = None
+ enabled_repo_hooks = None
for node in itertools.chain(*node_list):
if node.nodeName == 'project':
project = self._ParseProject(node)
@@ -807,6 +868,7 @@
'project: %s' % name)
path = node.getAttribute('path')
+ dest_path = node.getAttribute('dest-path')
groups = node.getAttribute('groups')
if groups:
groups = self._ParseList(groups)
@@ -815,46 +877,37 @@
if remote:
remote = self._get_remote(node)
+ named_projects = self._projects[name]
+ if dest_path and not path and len(named_projects) > 1:
+ raise ManifestParseError('extend-project cannot use dest-path when '
+ 'matching multiple projects: %s' % name)
for p in self._projects[name]:
if path and p.relpath != path:
continue
if groups:
p.groups.extend(groups)
if revision:
- p.revisionExpr = revision
- if IsId(revision):
- p.revisionId = revision
- else:
- p.revisionId = None
+ p.SetRevision(revision)
+
if remote:
p.remote = remote.ToRemoteSpec(name)
- if node.nodeName == 'repo-hooks':
- # Get the name of the project and the (space-separated) list of enabled.
- repo_hooks_project = self._reqatt(node, 'in-project')
- enabled_repo_hooks = self._ParseList(self._reqatt(node, 'enabled-list'))
+ if dest_path:
+ del self._paths[p.relpath]
+ relpath, worktree, gitdir, objdir, _ = self.GetProjectPaths(name, dest_path)
+ p.UpdatePaths(relpath, worktree, gitdir, objdir)
+ self._paths[p.relpath] = p
+
+ if node.nodeName == 'repo-hooks':
# Only one project can be the hooks project
- if self._repo_hooks_project is not None:
+ if repo_hooks_project is not None:
raise ManifestParseError(
'duplicate repo-hooks in %s' %
(self.manifestFile))
- # Store a reference to the Project.
- try:
- repo_hooks_projects = self._projects[repo_hooks_project]
- except KeyError:
- raise ManifestParseError(
- 'project %s not found for repo-hooks' %
- (repo_hooks_project))
-
- if len(repo_hooks_projects) != 1:
- raise ManifestParseError(
- 'internal error parsing repo-hooks in %s' %
- (self.manifestFile))
- self._repo_hooks_project = repo_hooks_projects[0]
-
- # Store the enabled hooks in the Project object.
- self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks
+ # Get the name of the project and the (space-separated) list of enabled.
+ repo_hooks_project = self._reqatt(node, 'in-project')
+ enabled_repo_hooks = self._ParseList(self._reqatt(node, 'enabled-list'))
if node.nodeName == 'superproject':
name = self._reqatt(node, 'name')
# There can only be one superproject.
@@ -872,21 +925,51 @@
raise ManifestParseError("no remote for superproject %s within %s" %
(name, self.manifestFile))
self._superproject['remote'] = remote.ToRemoteSpec(name)
+ revision = node.getAttribute('revision') or remote.revision
+ if not revision:
+ revision = self._default.revisionExpr
+ if not revision:
+ raise ManifestParseError('no revision for superproject %s within %s' %
+ (name, self.manifestFile))
+ self._superproject['revision'] = revision
+ if node.nodeName == 'contactinfo':
+ bugurl = self._reqatt(node, 'bugurl')
+ # This element can be repeated, later entries will clobber earlier ones.
+ self._contactinfo = ContactInfo(bugurl)
+
if node.nodeName == 'remove-project':
name = self._reqatt(node, 'name')
- if name not in self._projects:
+ if name in self._projects:
+ for p in self._projects[name]:
+ del self._paths[p.relpath]
+ del self._projects[name]
+
+ # If the manifest removes the hooks project, treat it as if it deleted
+ # the repo-hooks element too.
+ if repo_hooks_project == name:
+ repo_hooks_project = None
+ elif not XmlBool(node, 'optional', False):
raise ManifestParseError('remove-project element specifies non-existent '
'project: %s' % name)
- for p in self._projects[name]:
- del self._paths[p.relpath]
- del self._projects[name]
+ # Store repo hooks project information.
+ if repo_hooks_project:
+ # Store a reference to the Project.
+ try:
+ repo_hooks_projects = self._projects[repo_hooks_project]
+ except KeyError:
+ raise ManifestParseError(
+ 'project %s not found for repo-hooks' %
+ (repo_hooks_project))
- # If the manifest removes the hooks project, treat it as if it deleted
- # the repo-hooks element too.
- if self._repo_hooks_project and (self._repo_hooks_project.name == name):
- self._repo_hooks_project = None
+ if len(repo_hooks_projects) != 1:
+ raise ManifestParseError(
+ 'internal error parsing repo-hooks in %s' %
+ (self.manifestFile))
+ self._repo_hooks_project = repo_hooks_projects[0]
+ # Store the enabled hooks in the Project object.
+ self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks
def _AddMetaProjectMirror(self, m):
name = None
@@ -945,7 +1028,14 @@
if revision == '':
revision = None
manifestUrl = self.manifestProject.config.GetString('remote.origin.url')
- return _XmlRemote(name, alias, fetch, pushUrl, manifestUrl, review, revision)
+
+ remote = _XmlRemote(name, alias, fetch, pushUrl, manifestUrl, review, revision)
+
+ for n in node.childNodes:
+ if n.nodeName == 'annotation':
+ self._ParseAnnotation(remote, n)
+
+ return remote
def _ParseDefault(self, node):
"""
@@ -1199,6 +1289,8 @@
if '~' in path:
return '~ not allowed (due to 8.3 filenames on Windows filesystems)'
+ path_codepoints = set(path)
+
# Some filesystems (like Apple's HFS+) try to normalize Unicode codepoints
# which means there are alternative names for ".git". Reject paths with
# these in it as there shouldn't be any reasonable need for them here.
@@ -1222,10 +1314,17 @@
u'\u206F', # NOMINAL DIGIT SHAPES
u'\uFEFF', # ZERO WIDTH NO-BREAK SPACE
}
- if BAD_CODEPOINTS & set(path):
+ if BAD_CODEPOINTS & path_codepoints:
# This message is more expansive than reality, but should be fine.
return 'Unicode combining characters not allowed'
+ # Reject newlines as there shouldn't be any legitmate use for them, they'll
+ # be confusing to users, and they can easily break tools that expect to be
+ # able to iterate over newline delimited lists. This even applies to our
+ # own code like .repo/project.list.
+ if {'\r', '\n'} & path_codepoints:
+ return 'Newlines not allowed'
+
# Assume paths might be used on case-insensitive filesystems.
path = path.lower()
@@ -1303,7 +1402,7 @@
self._ValidateFilePaths('linkfile', src, dest)
project.AddLinkFile(src, dest, self.topdir)
- def _ParseAnnotation(self, project, node):
+ def _ParseAnnotation(self, element, node):
name = self._reqatt(node, 'name')
value = self._reqatt(node, 'value')
try:
@@ -1313,7 +1412,7 @@
if keep != "true" and keep != "false":
raise ManifestParseError('optional "keep" attribute must be '
'"true" or "false"')
- project.AddAnnotation(name, value, keep)
+ element.AddAnnotation(name, value, keep)
def _get_remote(self, node):
name = node.getAttribute('remote')
diff --git a/platform_utils.py b/platform_utils.py
index 00c51d9..0203249 100644
--- a/platform_utils.py
+++ b/platform_utils.py
@@ -124,31 +124,30 @@
else:
raise
else:
- os.rename(src, dst)
+ shutil.move(src, dst)
-def remove(path):
+def remove(path, missing_ok=False):
"""Remove (delete) the file path. This is a replacement for os.remove that
allows deleting read-only files on Windows, with support for long paths and
for deleting directory symbolic links.
Availability: Unix, Windows."""
- if isWindows():
- longpath = _makelongpath(path)
- try:
- os.remove(longpath)
- except OSError as e:
- if e.errno == errno.EACCES:
- os.chmod(longpath, stat.S_IWRITE)
- # Directory symbolic links must be deleted with 'rmdir'.
- if islink(longpath) and isdir(longpath):
- os.rmdir(longpath)
- else:
- os.remove(longpath)
+ longpath = _makelongpath(path) if isWindows() else path
+ try:
+ os.remove(longpath)
+ except OSError as e:
+ if e.errno == errno.EACCES:
+ os.chmod(longpath, stat.S_IWRITE)
+ # Directory symbolic links must be deleted with 'rmdir'.
+ if islink(longpath) and isdir(longpath):
+ os.rmdir(longpath)
else:
- raise
- else:
- os.remove(path)
+ os.remove(longpath)
+ elif missing_ok and e.errno == errno.ENOENT:
+ pass
+ else:
+ raise
def walk(top, topdown=True, onerror=None, followlinks=False):
diff --git a/project.py b/project.py
index 992a0c0..5b26b64 100644
--- a/project.py
+++ b/project.py
@@ -251,13 +251,29 @@
self.fail = self.printer('fail', fg='red')
-class _Annotation(object):
+class Annotation(object):
def __init__(self, name, value, keep):
self.name = name
self.value = value
self.keep = keep
+ def __eq__(self, other):
+ if not isinstance(other, Annotation):
+ return False
+ return self.__dict__ == other.__dict__
+
+ def __lt__(self, other):
+ # This exists just so that lists of Annotation objects can be sorted, for
+ # use in comparisons.
+ if not isinstance(other, Annotation):
+ raise ValueError('comparison is not between two Annotation objects')
+ if self.name == other.name:
+ if self.value == other.value:
+ return self.keep < other.keep
+ return self.value < other.value
+ return self.name < other.name
+
def _SafeExpandPath(base, subpath, skipfinal=False):
"""Make sure |subpath| is completely safe under |base|.
@@ -503,21 +519,8 @@
self.client = self.manifest = manifest
self.name = name
self.remote = remote
- self.gitdir = gitdir.replace('\\', '/')
- self.objdir = objdir.replace('\\', '/')
- if worktree:
- self.worktree = os.path.normpath(worktree).replace('\\', '/')
- else:
- self.worktree = None
- self.relpath = relpath
- self.revisionExpr = revisionExpr
-
- if revisionId is None \
- and revisionExpr \
- and IsId(revisionExpr):
- self.revisionId = revisionExpr
- else:
- self.revisionId = revisionId
+ self.UpdatePaths(relpath, worktree, gitdir, objdir)
+ self.SetRevision(revisionExpr, revisionId=revisionId)
self.rebase = rebase
self.groups = groups
@@ -540,16 +543,6 @@
self.copyfiles = []
self.linkfiles = []
self.annotations = []
- self.config = GitConfig.ForRepository(gitdir=self.gitdir,
- defaults=self.client.globalConfig)
-
- if self.worktree:
- self.work_git = self._GitGetByExec(self, bare=False, gitdir=gitdir)
- else:
- self.work_git = None
- self.bare_git = self._GitGetByExec(self, bare=True, gitdir=gitdir)
- self.bare_ref = GitRefs(gitdir)
- self.bare_objdir = self._GitGetByExec(self, bare=True, gitdir=objdir)
self.dest_branch = dest_branch
self.old_revision = old_revision
@@ -557,6 +550,35 @@
# project containing repo hooks.
self.enabled_repo_hooks = []
+ def SetRevision(self, revisionExpr, revisionId=None):
+ """Set revisionId based on revision expression and id"""
+ self.revisionExpr = revisionExpr
+ if revisionId is None and revisionExpr and IsId(revisionExpr):
+ self.revisionId = self.revisionExpr
+ else:
+ self.revisionId = revisionId
+
+ def UpdatePaths(self, relpath, worktree, gitdir, objdir):
+ """Update paths used by this project"""
+ self.gitdir = gitdir.replace('\\', '/')
+ self.objdir = objdir.replace('\\', '/')
+ if worktree:
+ self.worktree = os.path.normpath(worktree).replace('\\', '/')
+ else:
+ self.worktree = None
+ self.relpath = relpath
+
+ self.config = GitConfig.ForRepository(gitdir=self.gitdir,
+ defaults=self.manifest.globalConfig)
+
+ if self.worktree:
+ self.work_git = self._GitGetByExec(self, bare=False, gitdir=self.gitdir)
+ else:
+ self.work_git = None
+ self.bare_git = self._GitGetByExec(self, bare=True, gitdir=self.gitdir)
+ self.bare_ref = GitRefs(self.gitdir)
+ self.bare_objdir = self._GitGetByExec(self, bare=True, gitdir=self.objdir)
+
@property
def Derived(self):
return self.is_derived
@@ -1041,15 +1063,16 @@
verbose=False,
output_redir=None,
is_new=None,
- current_branch_only=False,
+ current_branch_only=None,
force_sync=False,
clone_bundle=True,
- tags=True,
+ tags=None,
archive=False,
optimized_fetch=False,
retry_fetches=0,
prune=False,
submodules=False,
+ ssh_proxy=None,
clone_filter=None,
partial_clone_exclude=set()):
"""Perform only the network IO portion of the sync process.
@@ -1116,7 +1139,7 @@
and self._ApplyCloneBundle(initial=is_new, quiet=quiet, verbose=verbose)):
is_new = False
- if not current_branch_only:
+ if current_branch_only is None:
if self.sync_c:
current_branch_only = True
elif not self.manifest._loaded:
@@ -1125,8 +1148,8 @@
elif self.manifest.default.sync_c:
current_branch_only = True
- if not self.sync_tags:
- tags = False
+ if tags is None:
+ tags = self.sync_tags
if self.clone_depth:
depth = self.clone_depth
@@ -1143,6 +1166,7 @@
alt_dir=alt_dir, current_branch_only=current_branch_only,
tags=tags, prune=prune, depth=depth,
submodules=submodules, force_sync=force_sync,
+ ssh_proxy=ssh_proxy,
clone_filter=clone_filter, retry_fetches=retry_fetches):
return False
@@ -1164,10 +1188,8 @@
self._InitMRef()
else:
self._InitMirrorHead()
- try:
- platform_utils.remove(os.path.join(self.gitdir, 'FETCH_HEAD'))
- except OSError:
- pass
+ platform_utils.remove(os.path.join(self.gitdir, 'FETCH_HEAD'),
+ missing_ok=True)
return True
def PostRepoUpgrade(self):
@@ -1214,6 +1236,9 @@
(self.revisionExpr, self.name))
def SetRevisionId(self, revisionId):
+ if self.revisionExpr:
+ self.upstream = self.revisionExpr
+
self.revisionId = revisionId
def Sync_LocalHalf(self, syncbuf, force_sync=False, submodules=False):
@@ -1443,7 +1468,7 @@
self.linkfiles.append(_LinkFile(self.worktree, src, topdir, dest))
def AddAnnotation(self, name, value, keep):
- self.annotations.append(_Annotation(name, value, keep))
+ self.annotations.append(Annotation(name, value, keep))
def DownloadPatchSet(self, change_id, patch_id):
"""Download a single patch set of a single change to FETCH_HEAD.
@@ -1962,6 +1987,11 @@
# throws an error.
self.bare_git.rev_list('-1', '--missing=allow-any',
'%s^0' % self.revisionExpr, '--')
+ if self.upstream:
+ rev = self.GetRemote(self.remote.name).ToLocal(self.upstream)
+ self.bare_git.rev_list('-1', '--missing=allow-any',
+ '%s^0' % rev, '--')
+ self.bare_git.merge_base('--is-ancestor', self.revisionExpr, rev)
return True
except GitError:
# There is no such persistent revision. We have to fetch it.
@@ -1991,6 +2021,7 @@
prune=False,
depth=None,
submodules=False,
+ ssh_proxy=None,
force_sync=False,
clone_filter=None,
retry_fetches=2,
@@ -2038,16 +2069,14 @@
if not name:
name = self.remote.name
- ssh_proxy = False
remote = self.GetRemote(name)
- if remote.PreConnectFetch():
- ssh_proxy = True
+ if not remote.PreConnectFetch(ssh_proxy):
+ ssh_proxy = None
if initial:
if alt_dir and 'objects' == os.path.basename(alt_dir):
ref_dir = os.path.dirname(alt_dir)
packed_refs = os.path.join(self.gitdir, 'packed-refs')
- remote = self.GetRemote(name)
all_refs = self.bare_ref.all
ids = set(all_refs.values())
@@ -2134,6 +2163,8 @@
# Shallow checkout of a specific commit, fetch from that commit and not
# the heads only as the commit might be deeper in the history.
spec.append(branch)
+ if self.upstream:
+ spec.append(self.upstream)
else:
if is_sha1:
branch = self.upstream
@@ -2191,7 +2222,7 @@
ret = prunecmd.Wait()
if ret:
break
- output_redir.write('retrying fetch after pruning remote branches')
+ print('retrying fetch after pruning remote branches', file=output_redir)
# Continue right away so we don't sleep as we shouldn't need to.
continue
elif current_branch_only and is_sha1 and ret == 128:
@@ -2204,10 +2235,11 @@
break
# Figure out how long to sleep before the next attempt, if there is one.
- if not verbose:
- output_redir.write('\n%s:\n%s' % (self.name, gitcmd.stdout))
+ if not verbose and gitcmd.stdout:
+ print('\n%s:\n%s' % (self.name, gitcmd.stdout), end='', file=output_redir)
if try_n < retry_fetches - 1:
- output_redir.write('sleeping %s seconds before retrying' % retry_cur_sleep)
+ print('%s: sleeping %s seconds before retrying' % (self.name, retry_cur_sleep),
+ file=output_redir)
time.sleep(retry_cur_sleep)
retry_cur_sleep = min(retry_exp_factor * retry_cur_sleep,
MAXIMUM_RETRY_SLEEP_SEC)
@@ -2233,7 +2265,7 @@
name=name, quiet=quiet, verbose=verbose, output_redir=output_redir,
current_branch_only=current_branch_only and depth,
initial=False, alt_dir=alt_dir,
- depth=None, clone_filter=clone_filter)
+ depth=None, ssh_proxy=ssh_proxy, clone_filter=clone_filter)
return ok
@@ -2279,15 +2311,12 @@
cmd.append('+refs/tags/*:refs/tags/*')
ok = GitCommand(self, cmd, bare=True).Wait() == 0
- if os.path.exists(bundle_dst):
- platform_utils.remove(bundle_dst)
- if os.path.exists(bundle_tmp):
- platform_utils.remove(bundle_tmp)
+ platform_utils.remove(bundle_dst, missing_ok=True)
+ platform_utils.remove(bundle_tmp, missing_ok=True)
return ok
def _FetchBundle(self, srcUrl, tmpPath, dstPath, quiet, verbose):
- if os.path.exists(dstPath):
- platform_utils.remove(dstPath)
+ platform_utils.remove(dstPath, missing_ok=True)
cmd = ['curl', '--fail', '--output', tmpPath, '--netrc', '--location']
if quiet:
@@ -2438,14 +2467,6 @@
self.bare_objdir.init()
if self.use_git_worktrees:
- # Set up the m/ space to point to the worktree-specific ref space.
- # We'll update the worktree-specific ref space on each checkout.
- if self.manifest.branch:
- self.bare_git.symbolic_ref(
- '-m', 'redirecting to worktree scope',
- R_M + self.manifest.branch,
- R_WORKTREE_M + self.manifest.branch)
-
# Enable per-worktree config file support if possible. This is more a
# nice-to-have feature for users rather than a hard requirement.
if git_require((2, 20, 0)):
@@ -2582,6 +2603,14 @@
def _InitMRef(self):
if self.manifest.branch:
if self.use_git_worktrees:
+ # Set up the m/ space to point to the worktree-specific ref space.
+ # We'll update the worktree-specific ref space on each checkout.
+ ref = R_M + self.manifest.branch
+ if not self.bare_ref.symref(ref):
+ self.bare_git.symbolic_ref(
+ '-m', 'redirecting to worktree scope',
+ ref, R_WORKTREE_M + self.manifest.branch)
+
# We can't update this ref with git worktrees until it exists.
# We'll wait until the initial checkout to set it.
if not os.path.exists(self.worktree):
@@ -2711,10 +2740,7 @@
# If the source file doesn't exist, ensure the destination
# file doesn't either.
if name in symlink_files and not os.path.lexists(src):
- try:
- platform_utils.remove(dst)
- except OSError:
- pass
+ platform_utils.remove(dst, missing_ok=True)
except OSError as e:
if e.errno == errno.EPERM:
diff --git a/release/update-manpages b/release/update-manpages
new file mode 100755
index 0000000..ddbce0c
--- /dev/null
+++ b/release/update-manpages
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+# Copyright (C) 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Helper tool for generating manual page for all repo commands.
+
+This is intended to be run before every official Repo release.
+"""
+
+from pathlib import Path
+from functools import partial
+import argparse
+import multiprocessing
+import os
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+
+TOPDIR = Path(__file__).resolve().parent.parent
+MANDIR = TOPDIR.joinpath('man')
+
+# Load repo local modules.
+sys.path.insert(0, str(TOPDIR))
+from git_command import RepoSourceVersion
+import subcmds
+
+def worker(cmd, **kwargs):
+ subprocess.run(cmd, **kwargs)
+
+def main(argv):
+ parser = argparse.ArgumentParser(description=__doc__)
+ opts = parser.parse_args(argv)
+
+ if not shutil.which('help2man'):
+ sys.exit('Please install help2man to continue.')
+
+ # Let repo know we're generating man pages so it can avoid some dynamic
+ # behavior (like probing active number of CPUs). We use a weird name &
+ # value to make it less likely for users to set this var themselves.
+ os.environ['_REPO_GENERATE_MANPAGES_'] = ' indeed! '
+
+ # "repo branch" is an alias for "repo branches".
+ del subcmds.all_commands['branch']
+ (MANDIR / 'repo-branch.1').write_text('.so man1/repo-branches.1')
+
+ version = RepoSourceVersion()
+ cmdlist = [['help2man', '-N', '-n', f'repo {cmd} - manual page for repo {cmd}',
+ '-S', f'repo {cmd}', '-m', 'Repo Manual', f'--version-string={version}',
+ '-o', MANDIR.joinpath(f'repo-{cmd}.1.tmp'), TOPDIR.joinpath('repo'),
+ '-h', f'help {cmd}'] for cmd in subcmds.all_commands]
+ cmdlist.append(['help2man', '-N', '-n', 'repository management tool built on top of git',
+ '-S', 'repo', '-m', 'Repo Manual', f'--version-string={version}',
+ '-o', MANDIR.joinpath('repo.1.tmp'), TOPDIR.joinpath('repo'),
+ '-h', '--help-all'])
+
+ with tempfile.TemporaryDirectory() as tempdir:
+ repo_dir = Path(tempdir) / '.repo'
+ repo_dir.mkdir()
+ (repo_dir / 'repo').symlink_to(TOPDIR)
+
+ # Run all cmd in parallel, and wait for them to finish.
+ with multiprocessing.Pool() as pool:
+ pool.map(partial(worker, cwd=tempdir, check=True), cmdlist)
+
+ regex = (
+ (r'(It was generated by help2man) [0-9.]+', '\g<1>.'),
+ (r'^\.IP\n(.*:)\n', '.SS \g<1>\n'),
+ (r'^\.PP\nDescription', '.SH DETAILS'),
+ )
+ for tmp_path in MANDIR.glob('*.1.tmp'):
+ path = tmp_path.parent / tmp_path.stem
+ old_data = path.read_text() if path.exists() else ''
+
+ data = tmp_path.read_text()
+ tmp_path.unlink()
+
+ for pattern, replacement in regex:
+ data = re.sub(pattern, replacement, data, flags=re.M)
+
+ # If the only thing that changed was the date, don't refresh. This avoids
+ # a lot of noise when only one file actually updates.
+ old_data = re.sub(r'^(\.TH REPO "1" ")([^"]+)', r'\1', old_data, flags=re.M)
+ new_data = re.sub(r'^(\.TH REPO "1" ")([^"]+)', r'\1', data, flags=re.M)
+ if old_data != new_data:
+ path.write_text(data)
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
diff --git a/repo b/repo
index d9c97de..4cddbf1 100755
--- a/repo
+++ b/repo
@@ -117,7 +117,7 @@
# If the python3 version looks like it's new enough, give it a try.
if (python3_ver and python3_ver >= MIN_PYTHON_VERSION_HARD
- and python3_ver != (major, minor)):
+ and python3_ver != (major, minor)):
reexec('python3')
# We're still here, so diagnose things for the user.
@@ -145,9 +145,11 @@
REPO_REV = os.environ.get('REPO_REV')
if not REPO_REV:
REPO_REV = 'stable'
+# URL to file bug reports for repo tool issues.
+BUG_URL = 'https://bugs.chromium.org/p/gerrit/issues/entry?template=Repo+tool+issue'
# increment this whenever we make important changes to this script
-VERSION = (2, 14)
+VERSION = (2, 17)
# increment this if the MAINTAINER_KEYS block is modified
KEYRING_VERSION = (2, 3)
@@ -310,6 +312,10 @@
metavar='PLATFORM')
group.add_option('--submodules', action='store_true',
help='sync any submodules associated with the manifest repo')
+ group.add_option('--standalone-manifest', action='store_true',
+ help='download the manifest as a static file '
+ 'rather then create a git checkout of '
+ 'the manifest repo')
# Options that only affect manifest project, and not any of the projects
# specified in the manifest itself.
@@ -322,8 +328,14 @@
group.add_option(*cbr_opts,
dest='current_branch_only', action='store_true',
help='fetch only current manifest branch from server')
+ group.add_option('--no-current-branch',
+ dest='current_branch_only', action='store_false',
+ help='fetch all manifest branches from server')
+ group.add_option('--tags',
+ action='store_true',
+ help='fetch tags in the manifest')
group.add_option('--no-tags',
- dest='tags', default=True, action='store_false',
+ dest='tags', action='store_false',
help="don't fetch tags in the manifest")
# These are fundamentally different ways of structuring the checkout.
@@ -851,11 +863,10 @@
try:
r = urllib.request.urlopen(url)
except urllib.error.HTTPError as e:
- if e.code in [401, 403, 404, 501]:
- return False
- print('fatal: Cannot get %s' % url, file=sys.stderr)
- print('fatal: HTTP error %s' % e.code, file=sys.stderr)
- raise CloneFailure()
+ if e.code not in [400, 401, 403, 404, 501]:
+ print('warning: Cannot get %s' % url, file=sys.stderr)
+ print('warning: HTTP error %s' % e.code, file=sys.stderr)
+ return False
except urllib.error.URLError as e:
print('fatal: Cannot get %s' % url, file=sys.stderr)
print('fatal: error %s' % e.reason, file=sys.stderr)
@@ -1171,6 +1182,7 @@
For access to the full online help, install repo ("repo init").
""")
+ print('Bug reports:', BUG_URL)
sys.exit(0)
@@ -1204,6 +1216,7 @@
print('OS %s %s (%s)' % (uname.system, uname.release, uname.version))
print('CPU %s (%s)' %
(uname.machine, uname.processor if uname.processor else 'unknown'))
+ print('Bug reports:', BUG_URL)
sys.exit(0)
diff --git a/requirements.json b/requirements.json
index 86b9a46..cb55cd2 100644
--- a/requirements.json
+++ b/requirements.json
@@ -38,9 +38,9 @@
# Supported Python versions.
#
# python-3.6 is in Ubuntu Bionic.
- # python-3.5 is in Debian Stretch.
+ # python-3.7 is in Debian Buster.
"python": {
- "hard": [3, 5],
+ "hard": [3, 6],
"soft": [3, 6]
},
diff --git a/run_tests b/run_tests
index 6c6f859..573dd44 100755
--- a/run_tests
+++ b/run_tests
@@ -24,6 +24,10 @@
def find_pytest():
"""Try to locate a good version of pytest."""
+ # If we're in a virtualenv, assume that it's provided the right pytest.
+ if 'VIRTUAL_ENV' in os.environ:
+ return 'pytest'
+
# Use the Python 3 version if available.
ret = shutil.which('pytest-3')
if ret:
diff --git a/setup.py b/setup.py
index 9d0ff5f..17aeae2 100755
--- a/setup.py
+++ b/setup.py
@@ -56,6 +56,6 @@
'Programming Language :: Python :: 3 :: Only',
'Topic :: Software Development :: Version Control :: Git',
],
- python_requires='>=3.5',
+ python_requires='>=3.6',
packages=['subcmds'],
)
diff --git a/ssh.py b/ssh.py
new file mode 100644
index 0000000..0ae8d12
--- /dev/null
+++ b/ssh.py
@@ -0,0 +1,277 @@
+# Copyright (C) 2008 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.
+
+"""Common SSH management logic."""
+
+import functools
+import multiprocessing
+import os
+import re
+import signal
+import subprocess
+import sys
+import tempfile
+import time
+
+import platform_utils
+from repo_trace import Trace
+
+
+PROXY_PATH = os.path.join(os.path.dirname(__file__), 'git_ssh')
+
+
+def _run_ssh_version():
+ """run ssh -V to display the version number"""
+ return subprocess.check_output(['ssh', '-V'], stderr=subprocess.STDOUT).decode()
+
+
+def _parse_ssh_version(ver_str=None):
+ """parse a ssh version string into a tuple"""
+ if ver_str is None:
+ ver_str = _run_ssh_version()
+ m = re.match(r'^OpenSSH_([0-9.]+)(p[0-9]+)?\s', ver_str)
+ if m:
+ return tuple(int(x) for x in m.group(1).split('.'))
+ else:
+ return ()
+
+
+@functools.lru_cache(maxsize=None)
+def version():
+ """return ssh version as a tuple"""
+ try:
+ return _parse_ssh_version()
+ except subprocess.CalledProcessError:
+ print('fatal: unable to detect ssh version', file=sys.stderr)
+ sys.exit(1)
+
+
+URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):')
+URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/')
+
+
+class ProxyManager:
+ """Manage various ssh clients & masters that we spawn.
+
+ This will take care of sharing state between multiprocessing children, and
+ make sure that if we crash, we don't leak any of the ssh sessions.
+
+ The code should work with a single-process scenario too, and not add too much
+ overhead due to the manager.
+ """
+
+ # Path to the ssh program to run which will pass our master settings along.
+ # Set here more as a convenience API.
+ proxy = PROXY_PATH
+
+ def __init__(self, manager):
+ # Protect access to the list of active masters.
+ self._lock = multiprocessing.Lock()
+ # List of active masters (pid). These will be spawned on demand, and we are
+ # responsible for shutting them all down at the end.
+ self._masters = manager.list()
+ # Set of active masters indexed by "host:port" information.
+ # The value isn't used, but multiprocessing doesn't provide a set class.
+ self._master_keys = manager.dict()
+ # Whether ssh masters are known to be broken, so we give up entirely.
+ self._master_broken = manager.Value('b', False)
+ # List of active ssh sesssions. Clients will be added & removed as
+ # connections finish, so this list is just for safety & cleanup if we crash.
+ self._clients = manager.list()
+ # Path to directory for holding master sockets.
+ self._sock_path = None
+
+ def __enter__(self):
+ """Enter a new context."""
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ """Exit a context & clean up all resources."""
+ self.close()
+
+ def add_client(self, proc):
+ """Track a new ssh session."""
+ self._clients.append(proc.pid)
+
+ def remove_client(self, proc):
+ """Remove a completed ssh session."""
+ try:
+ self._clients.remove(proc.pid)
+ except ValueError:
+ pass
+
+ def add_master(self, proc):
+ """Track a new master connection."""
+ self._masters.append(proc.pid)
+
+ def _terminate(self, procs):
+ """Kill all |procs|."""
+ for pid in procs:
+ try:
+ os.kill(pid, signal.SIGTERM)
+ os.waitpid(pid, 0)
+ except OSError:
+ pass
+
+ # The multiprocessing.list() API doesn't provide many standard list()
+ # methods, so we have to manually clear the list.
+ while True:
+ try:
+ procs.pop(0)
+ except:
+ break
+
+ def close(self):
+ """Close this active ssh session.
+
+ Kill all ssh clients & masters we created, and nuke the socket dir.
+ """
+ self._terminate(self._clients)
+ self._terminate(self._masters)
+
+ d = self.sock(create=False)
+ if d:
+ try:
+ platform_utils.rmdir(os.path.dirname(d))
+ except OSError:
+ pass
+
+ def _open_unlocked(self, host, port=None):
+ """Make sure a ssh master session exists for |host| & |port|.
+
+ If one doesn't exist already, we'll create it.
+
+ We won't grab any locks, so the caller has to do that. This helps keep the
+ business logic of actually creating the master separate from grabbing locks.
+ """
+ # Check to see whether we already think that the master is running; if we
+ # think it's already running, return right away.
+ if port is not None:
+ key = '%s:%s' % (host, port)
+ else:
+ key = host
+
+ if key in self._master_keys:
+ return True
+
+ if self._master_broken.value or 'GIT_SSH' in os.environ:
+ # Failed earlier, so don't retry.
+ return False
+
+ # We will make two calls to ssh; this is the common part of both calls.
+ command_base = ['ssh', '-o', 'ControlPath %s' % self.sock(), host]
+ if port is not None:
+ command_base[1:1] = ['-p', str(port)]
+
+ # Since the key wasn't in _master_keys, we think that master isn't running.
+ # ...but before actually starting a master, we'll double-check. This can
+ # be important because we can't tell that that 'git@myhost.com' is the same
+ # as 'myhost.com' where "User git" is setup in the user's ~/.ssh/config file.
+ check_command = command_base + ['-O', 'check']
+ try:
+ Trace(': %s', ' '.join(check_command))
+ check_process = subprocess.Popen(check_command,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ check_process.communicate() # read output, but ignore it...
+ isnt_running = check_process.wait()
+
+ if not isnt_running:
+ # Our double-check found that the master _was_ infact running. Add to
+ # the list of keys.
+ self._master_keys[key] = True
+ return True
+ except Exception:
+ # Ignore excpetions. We we will fall back to the normal command and print
+ # to the log there.
+ pass
+
+ command = command_base[:1] + ['-M', '-N'] + command_base[1:]
+ try:
+ Trace(': %s', ' '.join(command))
+ p = subprocess.Popen(command)
+ except Exception as e:
+ self._master_broken.value = True
+ print('\nwarn: cannot enable ssh control master for %s:%s\n%s'
+ % (host, port, str(e)), file=sys.stderr)
+ return False
+
+ time.sleep(1)
+ ssh_died = (p.poll() is not None)
+ if ssh_died:
+ return False
+
+ self.add_master(p)
+ self._master_keys[key] = True
+ return True
+
+ def _open(self, host, port=None):
+ """Make sure a ssh master session exists for |host| & |port|.
+
+ If one doesn't exist already, we'll create it.
+
+ This will obtain any necessary locks to avoid inter-process races.
+ """
+ # Bail before grabbing the lock if we already know that we aren't going to
+ # try creating new masters below.
+ if sys.platform in ('win32', 'cygwin'):
+ return False
+
+ # Acquire the lock. This is needed to prevent opening multiple masters for
+ # the same host when we're running "repo sync -jN" (for N > 1) _and_ the
+ # manifest <remote fetch="ssh://xyz"> specifies a different host from the
+ # one that was passed to repo init.
+ with self._lock:
+ return self._open_unlocked(host, port)
+
+ def preconnect(self, url):
+ """If |uri| will create a ssh connection, setup the ssh master for it."""
+ m = URI_ALL.match(url)
+ if m:
+ scheme = m.group(1)
+ host = m.group(2)
+ if ':' in host:
+ host, port = host.split(':')
+ else:
+ port = None
+ if scheme in ('ssh', 'git+ssh', 'ssh+git'):
+ return self._open(host, port)
+ return False
+
+ m = URI_SCP.match(url)
+ if m:
+ host = m.group(1)
+ return self._open(host)
+
+ return False
+
+ def sock(self, create=True):
+ """Return the path to the ssh socket dir.
+
+ This has all the master sockets so clients can talk to them.
+ """
+ if self._sock_path is None:
+ if not create:
+ return None
+ tmp_dir = '/tmp'
+ if not os.path.exists(tmp_dir):
+ tmp_dir = tempfile.gettempdir()
+ if version() < (6, 7):
+ tokens = '%r@%h:%p'
+ else:
+ tokens = '%C' # hash of %l%h%p%r
+ self._sock_path = os.path.join(
+ tempfile.mkdtemp('', 'ssh-', tmp_dir),
+ 'master-' + tokens)
+ return self._sock_path
diff --git a/subcmds/abandon.py b/subcmds/abandon.py
index c7c127d..85d85f5 100644
--- a/subcmds/abandon.py
+++ b/subcmds/abandon.py
@@ -23,7 +23,7 @@
class Abandon(Command):
- common = True
+ COMMON = True
helpSummary = "Permanently abandon a development branch"
helpUsage = """
%prog [--all | <branchname>] [<project>...]
diff --git a/subcmds/branches.py b/subcmds/branches.py
index 2dc102b..6d975ed 100644
--- a/subcmds/branches.py
+++ b/subcmds/branches.py
@@ -62,7 +62,7 @@
class Branches(Command):
- common = True
+ COMMON = True
helpSummary = "View current topic branches"
helpUsage = """
%prog [<project>...]
diff --git a/subcmds/checkout.py b/subcmds/checkout.py
index 4d8009b..9b42948 100644
--- a/subcmds/checkout.py
+++ b/subcmds/checkout.py
@@ -20,7 +20,7 @@
class Checkout(Command):
- common = True
+ COMMON = True
helpSummary = "Checkout a branch for development"
helpUsage = """
%prog <branchname> [<project>...]
diff --git a/subcmds/cherry_pick.py b/subcmds/cherry_pick.py
index fc4998c..7bd858b 100644
--- a/subcmds/cherry_pick.py
+++ b/subcmds/cherry_pick.py
@@ -21,7 +21,7 @@
class CherryPick(Command):
- common = True
+ COMMON = True
helpSummary = "Cherry-pick a change."
helpUsage = """
%prog <sha1>
diff --git a/subcmds/diff.py b/subcmds/diff.py
index 4966bb1..00a7ec2 100644
--- a/subcmds/diff.py
+++ b/subcmds/diff.py
@@ -19,7 +19,7 @@
class Diff(PagedCommand):
- common = True
+ COMMON = True
helpSummary = "Show changes between commit and working tree"
helpUsage = """
%prog [<project>...]
@@ -33,7 +33,7 @@
def _Options(self, p):
p.add_option('-u', '--absolute',
dest='absolute', action='store_true',
- help='Paths are relative to the repository root')
+ help='paths are relative to the repository root')
def _ExecuteOne(self, absolute, project):
"""Obtains the diff for a specific project.
diff --git a/subcmds/diffmanifests.py b/subcmds/diffmanifests.py
index 392e597..f6cc30a 100644
--- a/subcmds/diffmanifests.py
+++ b/subcmds/diffmanifests.py
@@ -31,7 +31,7 @@
deeper level.
"""
- common = True
+ COMMON = True
helpSummary = "Manifest diff utility"
helpUsage = """%prog manifest1.xml [manifest2.xml] [options]"""
@@ -68,10 +68,10 @@
def _Options(self, p):
p.add_option('--raw',
dest='raw', action='store_true',
- help='Display raw diff.')
+ help='display raw diff')
p.add_option('--no-color',
dest='color', action='store_false', default=True,
- help='does not display the diff in color.')
+ help='does not display the diff in color')
p.add_option('--pretty-format',
dest='pretty_format', action='store',
metavar='<FORMAT>',
diff --git a/subcmds/download.py b/subcmds/download.py
index 81d997e..523f25e 100644
--- a/subcmds/download.py
+++ b/subcmds/download.py
@@ -22,7 +22,7 @@
class Download(Command):
- common = True
+ COMMON = True
helpSummary = "Download and checkout a change"
helpUsage = """
%prog {[project] change[/patchset]}...
diff --git a/subcmds/forall.py b/subcmds/forall.py
index 4a631fb..7c1dea9 100644
--- a/subcmds/forall.py
+++ b/subcmds/forall.py
@@ -41,7 +41,7 @@
class Forall(Command, MirrorSafeCommand):
- common = False
+ COMMON = False
helpSummary = "Run a shell command in each project"
helpUsage = """
%prog [<project>...] -c <command> [<arg>...]
@@ -131,30 +131,30 @@
def _Options(self, p):
p.add_option('-r', '--regex',
dest='regex', action='store_true',
- help="Execute the command only on projects matching regex or wildcard expression")
+ help='execute the command only on projects matching regex or wildcard expression')
p.add_option('-i', '--inverse-regex',
dest='inverse_regex', action='store_true',
- help="Execute the command only on projects not matching regex or "
- "wildcard expression")
+ help='execute the command only on projects not matching regex or '
+ 'wildcard expression')
p.add_option('-g', '--groups',
dest='groups',
- help="Execute the command only on projects matching the specified groups")
+ help='execute the command only on projects matching the specified groups')
p.add_option('-c', '--command',
- help='Command (and arguments) to execute',
+ help='command (and arguments) to execute',
dest='command',
action='callback',
callback=self._cmd_option)
p.add_option('-e', '--abort-on-errors',
dest='abort_on_errors', action='store_true',
- help='Abort if a command exits unsuccessfully')
+ help='abort if a command exits unsuccessfully')
p.add_option('--ignore-missing', action='store_true',
- help='Silently skip & do not exit non-zero due missing '
+ help='silently skip & do not exit non-zero due missing '
'checkouts')
g = p.get_option_group('--quiet')
g.add_option('-p',
dest='project_header', action='store_true',
- help='Show project headers before output')
+ help='show project headers before output')
p.add_option('--interactive',
action='store_true',
help='force interactive usage')
diff --git a/subcmds/gitc_delete.py b/subcmds/gitc_delete.py
index 56e0eab..df74946 100644
--- a/subcmds/gitc_delete.py
+++ b/subcmds/gitc_delete.py
@@ -19,7 +19,7 @@
class GitcDelete(Command, GitcClientCommand):
- common = True
+ COMMON = True
visible_everywhere = False
helpSummary = "Delete a GITC Client."
helpUsage = """
@@ -33,7 +33,7 @@
def _Options(self, p):
p.add_option('-f', '--force',
dest='force', action='store_true',
- help='Force the deletion (no prompt).')
+ help='force the deletion (no prompt)')
def Execute(self, opt, args):
if not opt.force:
diff --git a/subcmds/gitc_init.py b/subcmds/gitc_init.py
index 23a4ebb..e705b61 100644
--- a/subcmds/gitc_init.py
+++ b/subcmds/gitc_init.py
@@ -23,7 +23,7 @@
class GitcInit(init.Init, GitcAvailableCommand):
- common = True
+ COMMON = True
helpSummary = "Initialize a GITC Client."
helpUsage = """
%prog [options] [client name]
diff --git a/subcmds/grep.py b/subcmds/grep.py
index 6cb1445..8ac4ba1 100644
--- a/subcmds/grep.py
+++ b/subcmds/grep.py
@@ -29,7 +29,7 @@
class Grep(PagedCommand):
- common = True
+ COMMON = True
helpSummary = "Print lines matching a pattern"
helpUsage = """
%prog {pattern | -e pattern} [<project>...]
diff --git a/subcmds/help.py b/subcmds/help.py
index 6a767e6..1a60ef4 100644
--- a/subcmds/help.py
+++ b/subcmds/help.py
@@ -20,10 +20,11 @@
from color import Coloring
from command import PagedCommand, MirrorSafeCommand, GitcAvailableCommand, GitcClientCommand
import gitc_utils
+from wrapper import Wrapper
class Help(PagedCommand, MirrorSafeCommand):
- common = False
+ COMMON = False
helpSummary = "Display detailed help on a command"
helpUsage = """
%prog [--all|command]
@@ -49,14 +50,21 @@
def _PrintAllCommands(self):
print('usage: repo COMMAND [ARGS]')
+ self.PrintAllCommandsBody()
+
+ def PrintAllCommandsBody(self):
print('The complete list of recognized repo commands are:')
commandNames = list(sorted(all_commands))
self._PrintCommands(commandNames)
print("See 'repo help <command>' for more information on a "
'specific command.')
+ print('Bug reports:', Wrapper().BUG_URL)
def _PrintCommonCommands(self):
print('usage: repo COMMAND [ARGS]')
+ self.PrintCommonCommandsBody()
+
+ def PrintCommonCommandsBody(self):
print('The most commonly used repo commands are:')
def gitc_supported(cmd):
@@ -72,12 +80,13 @@
commandNames = list(sorted([name
for name, command in all_commands.items()
- if command.common and gitc_supported(command)]))
+ if command.COMMON and gitc_supported(command)]))
self._PrintCommands(commandNames)
print(
"See 'repo help <command>' for more information on a specific command.\n"
"See 'repo help --all' for a complete list of recognized commands.")
+ print('Bug reports:', Wrapper().BUG_URL)
def _PrintCommandHelp(self, cmd, header_prefix=''):
class _Out(Coloring):
@@ -136,8 +145,7 @@
def _PrintAllCommandHelp(self):
for name in sorted(all_commands):
- cmd = all_commands[name]()
- cmd.manifest = self.manifest
+ cmd = all_commands[name](manifest=self.manifest)
self._PrintCommandHelp(cmd, header_prefix='[%s] ' % (name,))
def _Options(self, p):
@@ -161,12 +169,11 @@
name = args[0]
try:
- cmd = all_commands[name]()
+ cmd = all_commands[name](manifest=self.manifest)
except KeyError:
print("repo: '%s' is not a repo command." % name, file=sys.stderr)
sys.exit(1)
- cmd.manifest = self.manifest
self._PrintCommandHelp(cmd)
else:
diff --git a/subcmds/info.py b/subcmds/info.py
index 6381fa8..6c1246e 100644
--- a/subcmds/info.py
+++ b/subcmds/info.py
@@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import optparse
+
from command import PagedCommand
from color import Coloring
from git_refs import R_M, R_HEADS
@@ -23,9 +25,9 @@
class Info(PagedCommand):
- common = True
+ COMMON = True
helpSummary = "Get info on the manifest branch, current branch or unmerged branches"
- helpUsage = "%prog [-dl] [-o [-b]] [<project>...]"
+ helpUsage = "%prog [-dl] [-o [-c]] [<project>...]"
def _Options(self, p):
p.add_option('-d', '--diff',
@@ -34,12 +36,19 @@
p.add_option('-o', '--overview',
dest='overview', action='store_true',
help='show overview of all local commits')
- p.add_option('-b', '--current-branch',
+ p.add_option('-c', '--current-branch',
dest="current_branch", action="store_true",
help="consider only checked out branches")
+ p.add_option('--no-current-branch',
+ dest='current_branch', action='store_false',
+ help='consider all local branches')
+ # Turn this into a warning & remove this someday.
+ p.add_option('-b',
+ dest='current_branch', action='store_true',
+ help=optparse.SUPPRESS_HELP)
p.add_option('-l', '--local-only',
dest="local", action="store_true",
- help="Disable all remote operations")
+ help="disable all remote operations")
def Execute(self, opt, args):
self.out = _Coloring(self.client.globalConfig)
diff --git a/subcmds/init.py b/subcmds/init.py
index 4182262..9c6b2ad 100644
--- a/subcmds/init.py
+++ b/subcmds/init.py
@@ -12,10 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-import optparse
import os
import platform
import re
+import subprocess
import sys
import urllib.parse
@@ -25,13 +25,14 @@
from project import SyncBuffer
from git_config import GitConfig
from git_command import git_require, MIN_GIT_VERSION_SOFT, MIN_GIT_VERSION_HARD
+import fetch
import git_superproject
import platform_utils
from wrapper import Wrapper
class Init(InteractiveCommand, MirrorSafeCommand):
- common = True
+ COMMON = True
helpSummary = "Initialize a repo client checkout in the current directory"
helpUsage = """
%prog [options] [manifest url]
@@ -54,6 +55,12 @@
to be used. If no manifest is specified, the manifest default.xml
will be used.
+If the --standalone-manifest argument is set, the manifest will be downloaded
+directly from the specified --manifest-url as a static file (rather than
+setting up a manifest git checkout). With --standalone-manifest, the manifest
+will be fully static and will not be re-downloaded during subsesquent
+`repo init` and `repo sync` calls.
+
The --reference option can be used to point to a directory that
has the content of a --mirror sync. This will make the working
directory use as much data as possible from the local reference
@@ -97,15 +104,38 @@
"""
superproject = git_superproject.Superproject(self.manifest,
self.repodir,
+ self.git_event_log,
quiet=opt.quiet)
- if not superproject.Sync():
- print('error: git update of superproject failed', file=sys.stderr)
- sys.exit(1)
+ sync_result = superproject.Sync()
+ if not sync_result.success:
+ print('warning: git update of superproject failed, repo sync will not '
+ 'use superproject to fetch source; while this error is not fatal, '
+ 'and you can continue to run repo sync, please run repo init with '
+ 'the --no-use-superproject option to stop seeing this warning',
+ file=sys.stderr)
+ if sync_result.fatal and opt.use_superproject is not None:
+ sys.exit(1)
def _SyncManifest(self, opt):
m = self.manifest.manifestProject
is_new = not m.Exists
+ # If repo has already been initialized, we take -u with the absence of
+ # --standalone-manifest to mean "transition to a standard repo set up",
+ # which necessitates starting fresh.
+ # If --standalone-manifest is set, we always tear everything down and start
+ # anew.
+ if not is_new:
+ was_standalone_manifest = m.config.GetString('manifest.standalone')
+ if opt.standalone_manifest or (
+ was_standalone_manifest and opt.manifest_url):
+ m.config.ClearCache()
+ if m.gitdir and os.path.exists(m.gitdir):
+ platform_utils.rmtree(m.gitdir)
+ if m.worktree and os.path.exists(m.worktree):
+ platform_utils.rmtree(m.worktree)
+
+ is_new = not m.Exists
if is_new:
if not opt.manifest_url:
print('fatal: manifest url is required.', file=sys.stderr)
@@ -130,6 +160,19 @@
m._InitGitDir(mirror_git=mirrored_manifest_git)
+ # If standalone_manifest is set, mark the project as "standalone" -- we'll
+ # still do much of the manifests.git set up, but will avoid actual syncs to
+ # a remote.
+ standalone_manifest = False
+ if opt.standalone_manifest:
+ standalone_manifest = True
+ elif not opt.manifest_url:
+ # If -u is set and --standalone-manifest is not, then we're not in
+ # standalone mode. Otherwise, use config to infer what we were in the last
+ # init.
+ standalone_manifest = bool(m.config.GetString('manifest.standalone'))
+ m.config.SetString('manifest.standalone', opt.manifest_url)
+
self._ConfigureDepth(opt)
# Set the remote URL before the remote branch as we might need it below.
@@ -139,22 +182,23 @@
r.ResetFetch()
r.Save()
- if opt.manifest_branch:
- if opt.manifest_branch == 'HEAD':
- opt.manifest_branch = m.ResolveRemoteHead()
- if opt.manifest_branch is None:
- print('fatal: unable to resolve HEAD', file=sys.stderr)
- sys.exit(1)
- m.revisionExpr = opt.manifest_branch
- else:
- if is_new:
- default_branch = m.ResolveRemoteHead()
- if default_branch is None:
- # If the remote doesn't have HEAD configured, default to master.
- default_branch = 'refs/heads/master'
- m.revisionExpr = default_branch
+ if not standalone_manifest:
+ if opt.manifest_branch:
+ if opt.manifest_branch == 'HEAD':
+ opt.manifest_branch = m.ResolveRemoteHead()
+ if opt.manifest_branch is None:
+ print('fatal: unable to resolve HEAD', file=sys.stderr)
+ sys.exit(1)
+ m.revisionExpr = opt.manifest_branch
else:
- m.PreSync()
+ if is_new:
+ default_branch = m.ResolveRemoteHead()
+ if default_branch is None:
+ # If the remote doesn't have HEAD configured, default to master.
+ default_branch = 'refs/heads/master'
+ m.revisionExpr = default_branch
+ else:
+ m.PreSync()
groups = re.split(r'[,\s]+', opt.groups)
all_platforms = ['linux', 'darwin', 'windows']
@@ -244,6 +288,16 @@
if opt.use_superproject is not None:
m.config.SetBoolean('repo.superproject', opt.use_superproject)
+ if standalone_manifest:
+ if is_new:
+ manifest_name = 'default.xml'
+ manifest_data = fetch.fetch_file(opt.manifest_url)
+ dest = os.path.join(m.worktree, manifest_name)
+ os.makedirs(os.path.dirname(dest), exist_ok=True)
+ with open(dest, 'wb') as f:
+ f.write(manifest_data)
+ return
+
if not m.Sync_NetworkHalf(is_new=is_new, quiet=opt.quiet, verbose=opt.verbose,
clone_bundle=opt.clone_bundle,
current_branch_only=opt.current_branch_only,
@@ -420,6 +474,11 @@
if opt.archive and opt.mirror:
self.OptionParser.error('--mirror and --archive cannot be used together.')
+ if opt.standalone_manifest and (
+ opt.manifest_branch or opt.manifest_name != 'default.xml'):
+ self.OptionParser.error('--manifest-branch and --manifest-name cannot'
+ ' be used with --standalone-manifest.')
+
if args:
if opt.manifest_url:
self.OptionParser.error(
diff --git a/subcmds/list.py b/subcmds/list.py
index 5cbc0c2..6adf85b 100644
--- a/subcmds/list.py
+++ b/subcmds/list.py
@@ -12,11 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import os
+
from command import Command, MirrorSafeCommand
class List(Command, MirrorSafeCommand):
- common = True
+ COMMON = True
helpSummary = "List projects and their associated directories"
helpUsage = """
%prog [-f] [<project>...]
@@ -36,27 +38,33 @@
def _Options(self, p):
p.add_option('-r', '--regex',
dest='regex', action='store_true',
- help="Filter the project list based on regex or wildcard matching of strings")
+ help='filter the project list based on regex or wildcard matching of strings')
p.add_option('-g', '--groups',
dest='groups',
- help="Filter the project list based on the groups the project is in")
+ help='filter the project list based on the groups the project is in')
p.add_option('-a', '--all',
action='store_true',
- help='Show projects regardless of checkout state')
- p.add_option('-f', '--fullpath',
- dest='fullpath', action='store_true',
- help="Display the full work tree path instead of the relative path")
+ help='show projects regardless of checkout state')
p.add_option('-n', '--name-only',
dest='name_only', action='store_true',
- help="Display only the name of the repository")
+ help='display only the name of the repository')
p.add_option('-p', '--path-only',
dest='path_only', action='store_true',
- help="Display only the path of the repository")
+ help='display only the path of the repository')
+ p.add_option('-f', '--fullpath',
+ dest='fullpath', action='store_true',
+ help='display the full work tree path instead of the relative path')
+ p.add_option('--relative-to', metavar='PATH',
+ help='display paths relative to this one (default: top of repo client checkout)')
def ValidateOptions(self, opt, args):
if opt.fullpath and opt.name_only:
self.OptionParser.error('cannot combine -f and -n')
+ # Resolve any symlinks so the output is stable.
+ if opt.relative_to:
+ opt.relative_to = os.path.realpath(opt.relative_to)
+
def Execute(self, opt, args):
"""List all projects and the associated directories.
@@ -76,6 +84,8 @@
def _getpath(x):
if opt.fullpath:
return x.worktree
+ if opt.relative_to:
+ return os.path.relpath(x.worktree, opt.relative_to)
return x.relpath
lines = []
diff --git a/subcmds/manifest.py b/subcmds/manifest.py
index e33e683..0fbdeac 100644
--- a/subcmds/manifest.py
+++ b/subcmds/manifest.py
@@ -20,7 +20,7 @@
class Manifest(PagedCommand):
- common = False
+ COMMON = False
helpSummary = "Manifest inspection utility"
helpUsage = """
%prog [-o {-|NAME.xml}] [-m MANIFEST.xml] [-r]
@@ -53,27 +53,29 @@
def _Options(self, p):
p.add_option('-r', '--revision-as-HEAD',
dest='peg_rev', action='store_true',
- help='Save revisions as current HEAD')
+ help='save revisions as current HEAD')
p.add_option('-m', '--manifest-name',
help='temporary manifest to use for this sync', metavar='NAME.xml')
p.add_option('--suppress-upstream-revision', dest='peg_rev_upstream',
default=True, action='store_false',
- help='If in -r mode, do not write the upstream field. '
- 'Only of use if the branch names for a sha1 manifest are '
- 'sensitive.')
+ help='if in -r mode, do not write the upstream field '
+ '(only of use if the branch names for a sha1 manifest are '
+ 'sensitive)')
p.add_option('--suppress-dest-branch', dest='peg_rev_dest_branch',
default=True, action='store_false',
- help='If in -r mode, do not write the dest-branch field. '
- 'Only of use if the branch names for a sha1 manifest are '
- 'sensitive.')
+ help='if in -r mode, do not write the dest-branch field '
+ '(only of use if the branch names for a sha1 manifest are '
+ 'sensitive)')
p.add_option('--json', default=False, action='store_true',
- help='Output manifest in JSON format (experimental).')
+ help='output manifest in JSON format (experimental)')
p.add_option('--pretty', default=False, action='store_true',
- help='Format output for humans to read.')
+ help='format output for humans to read')
+ p.add_option('--no-local-manifests', default=False, action='store_true',
+ dest='ignore_local_manifests', help='ignore local manifests')
p.add_option('-o', '--output-file',
dest='output_file',
default='-',
- help='File to save the manifest to',
+ help='file to save the manifest to',
metavar='-|NAME.xml')
def _Output(self, opt):
@@ -85,6 +87,9 @@
fd = sys.stdout
else:
fd = open(opt.output_file, 'w')
+
+ self.manifest.SetUseLocalManifests(not opt.ignore_local_manifests)
+
if opt.json:
print('warning: --json is experimental!', file=sys.stderr)
doc = self.manifest.ToDict(peg_rev=opt.peg_rev,
diff --git a/subcmds/overview.py b/subcmds/overview.py
index 004a847..63f5a79 100644
--- a/subcmds/overview.py
+++ b/subcmds/overview.py
@@ -12,12 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import optparse
+
from color import Coloring
from command import PagedCommand
class Overview(PagedCommand):
- common = True
+ COMMON = True
helpSummary = "Display overview of unmerged project branches"
helpUsage = """
%prog [--current-branch] [<project>...]
@@ -26,15 +28,22 @@
The '%prog' command is used to display an overview of the projects branches,
and list any local commits that have not yet been merged into the project.
-The -b/--current-branch option can be used to restrict the output to only
+The -c/--current-branch option can be used to restrict the output to only
branches currently checked out in each project. By default, all branches
are displayed.
"""
def _Options(self, p):
- p.add_option('-b', '--current-branch',
+ p.add_option('-c', '--current-branch',
dest="current_branch", action="store_true",
- help="Consider only checked out branches")
+ help="consider only checked out branches")
+ p.add_option('--no-current-branch',
+ dest='current_branch', action='store_false',
+ help='consider all local branches')
+ # Turn this into a warning & remove this someday.
+ p.add_option('-b',
+ dest='current_branch', action='store_true',
+ help=optparse.SUPPRESS_HELP)
def Execute(self, opt, args):
all_branches = []
diff --git a/subcmds/prune.py b/subcmds/prune.py
index 236b647..584ee7e 100644
--- a/subcmds/prune.py
+++ b/subcmds/prune.py
@@ -19,7 +19,7 @@
class Prune(PagedCommand):
- common = True
+ COMMON = True
helpSummary = "Prune (delete) already merged topics"
helpUsage = """
%prog [<project>...]
diff --git a/subcmds/rebase.py b/subcmds/rebase.py
index e0186d4..7c53eb7 100644
--- a/subcmds/rebase.py
+++ b/subcmds/rebase.py
@@ -27,7 +27,7 @@
class Rebase(Command):
- common = True
+ COMMON = True
helpSummary = "Rebase local branches on upstream branch"
helpUsage = """
%prog {[<project>...] | -i <project>...}
@@ -46,27 +46,27 @@
p.add_option('--fail-fast',
dest='fail_fast', action='store_true',
- help='Stop rebasing after first error is hit')
+ help='stop rebasing after first error is hit')
p.add_option('-f', '--force-rebase',
dest='force_rebase', action='store_true',
- help='Pass --force-rebase to git rebase')
+ help='pass --force-rebase to git rebase')
p.add_option('--no-ff',
dest='ff', default=True, action='store_false',
- help='Pass --no-ff to git rebase')
+ help='pass --no-ff to git rebase')
p.add_option('--autosquash',
dest='autosquash', action='store_true',
- help='Pass --autosquash to git rebase')
+ help='pass --autosquash to git rebase')
p.add_option('--whitespace',
dest='whitespace', action='store', metavar='WS',
- help='Pass --whitespace to git rebase')
+ help='pass --whitespace to git rebase')
p.add_option('--auto-stash',
dest='auto_stash', action='store_true',
- help='Stash local modifications before starting')
+ help='stash local modifications before starting')
p.add_option('-m', '--onto-manifest',
dest='onto_manifest', action='store_true',
- help='Rebase onto the manifest version instead of upstream '
- 'HEAD. This helps to make sure the local tree stays '
- 'consistent if you previously synced to a manifest.')
+ help='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)')
def Execute(self, opt, args):
all_projects = self.GetProjects(args)
diff --git a/subcmds/selfupdate.py b/subcmds/selfupdate.py
index 388881d..282f518 100644
--- a/subcmds/selfupdate.py
+++ b/subcmds/selfupdate.py
@@ -21,7 +21,7 @@
class Selfupdate(Command, MirrorSafeCommand):
- common = False
+ COMMON = False
helpSummary = "Update repo to the latest version"
helpUsage = """
%prog
diff --git a/subcmds/smartsync.py b/subcmds/smartsync.py
index c7d1d4d..d91d59c 100644
--- a/subcmds/smartsync.py
+++ b/subcmds/smartsync.py
@@ -16,7 +16,7 @@
class Smartsync(Sync):
- common = True
+ COMMON = True
helpSummary = "Update working tree to the latest known good revision"
helpUsage = """
%prog [<project>...]
diff --git a/subcmds/stage.py b/subcmds/stage.py
index ff0f173..0389a4f 100644
--- a/subcmds/stage.py
+++ b/subcmds/stage.py
@@ -28,7 +28,7 @@
class Stage(InteractiveCommand):
- common = True
+ COMMON = True
helpSummary = "Stage file(s) for commit"
helpUsage = """
%prog -i [<project>...]
diff --git a/subcmds/start.py b/subcmds/start.py
index ff2bae5..2addaf2 100644
--- a/subcmds/start.py
+++ b/subcmds/start.py
@@ -25,7 +25,7 @@
class Start(Command):
- common = True
+ COMMON = True
helpSummary = "Start a new branch for development"
helpUsage = """
%prog <newbranchname> [--all | <project>...]
diff --git a/subcmds/status.py b/subcmds/status.py
index 1b48dce..5b66954 100644
--- a/subcmds/status.py
+++ b/subcmds/status.py
@@ -24,7 +24,7 @@
class Status(PagedCommand):
- common = True
+ COMMON = True
helpSummary = "Show the working tree status"
helpUsage = """
%prog [<project>...]
diff --git a/subcmds/sync.py b/subcmds/sync.py
index d41052d..3211cbb 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import errno
import functools
import http.cookiejar as cookielib
import io
@@ -56,6 +57,7 @@
import platform_utils
from project import SyncBuffer
from progress import Progress
+import ssh
from wrapper import Wrapper
from manifest_xml import GitcManifest
@@ -64,7 +66,7 @@
class Sync(Command, MirrorSafeCommand):
jobs = 1
- common = True
+ COMMON = True
helpSummary = "Update working tree to the latest revision"
helpUsage = """
%prog [<project>...]
@@ -168,10 +170,11 @@
PARALLEL_JOBS = 1
def _CommonOptions(self, p):
- try:
- self.PARALLEL_JOBS = self.manifest.default.sync_j
- except ManifestParseError:
- pass
+ if self.manifest:
+ try:
+ self.PARALLEL_JOBS = self.manifest.default.sync_j
+ except ManifestParseError:
+ pass
super()._CommonOptions(p)
def _Options(self, p, show_smart=True):
@@ -212,6 +215,9 @@
p.add_option('-c', '--current-branch',
dest='current_branch_only', action='store_true',
help='fetch only current branch from server')
+ p.add_option('--no-current-branch',
+ dest='current_branch_only', action='store_false',
+ help='fetch all branches from server')
p.add_option('-m', '--manifest-name',
dest='manifest_name',
help='temporary manifest to use for this sync', metavar='NAME.xml')
@@ -230,8 +236,14 @@
help='fetch submodules from server')
p.add_option('--use-superproject', action='store_true',
help='use the manifest superproject to sync projects')
+ p.add_option('--no-use-superproject', action='store_false',
+ dest='use_superproject',
+ help='disable use of manifest superprojects')
+ p.add_option('--tags',
+ action='store_false',
+ help='fetch tags')
p.add_option('--no-tags',
- dest='tags', default=True, action='store_false',
+ dest='tags', action='store_false',
help="don't fetch tags")
p.add_option('--optimized-fetch',
dest='optimized_fetch', action='store_true',
@@ -266,17 +278,11 @@
branch = branch[len(R_HEADS):]
return branch
- def _UseSuperproject(self, opt):
- """Returns True if use-superproject option is enabled"""
- return (opt.use_superproject or
- self.manifest.manifestProject.config.GetBoolean(
- 'repo.superproject'))
-
def _GetCurrentBranchOnly(self, opt):
"""Returns True if current-branch or use-superproject options are enabled."""
- return opt.current_branch_only or self._UseSuperproject(opt)
+ return opt.current_branch_only or git_superproject.UseSuperproject(opt, self.manifest)
- def _UpdateProjectsRevisionId(self, opt, args):
+ def _UpdateProjectsRevisionId(self, opt, args, load_local_manifests, superproject_logging_data):
"""Update revisionId of every project with the SHA from superproject.
This function updates each project's revisionId with SHA from superproject.
@@ -286,22 +292,40 @@
opt: Program options returned from optparse. See _Options().
args: Arguments to pass to GetProjects. See the GetProjects
docstring for details.
+ load_local_manifests: Whether to load local manifests.
+ superproject_logging_data: A dictionary of superproject data that is to be logged.
Returns:
- Returns path to the overriding manifest file.
+ Returns path to the overriding manifest file instead of None.
"""
+ print_messages = git_superproject.PrintMessages(opt, self.manifest)
superproject = git_superproject.Superproject(self.manifest,
self.repodir,
- quiet=opt.quiet)
+ self.git_event_log,
+ quiet=opt.quiet,
+ print_messages=print_messages)
+ if opt.local_only:
+ manifest_path = superproject.manifest_path
+ if manifest_path:
+ self._ReloadManifest(manifest_path, load_local_manifests)
+ return manifest_path
+
all_projects = self.GetProjects(args,
missing_ok=True,
submodules_ok=opt.fetch_submodules)
- manifest_path = superproject.UpdateProjectsRevisionId(all_projects)
- if not manifest_path:
- print('error: Update of revsionId from superproject has failed',
- file=sys.stderr)
- sys.exit(1)
- self._ReloadManifest(manifest_path)
+ update_result = superproject.UpdateProjectsRevisionId(all_projects)
+ manifest_path = update_result.manifest_path
+ superproject_logging_data['updatedrevisionid'] = bool(manifest_path)
+ if manifest_path:
+ self._ReloadManifest(manifest_path, load_local_manifests)
+ else:
+ if print_messages:
+ print('warning: Update of revisionId from superproject has failed, '
+ 'repo sync will not use superproject to fetch the source. ',
+ 'Please resync with the --no-use-superproject option to avoid this repo warning.',
+ file=sys.stderr)
+ if update_result.fatal and opt.use_superproject is not None:
+ sys.exit(1)
return manifest_path
def _FetchProjectList(self, opt, projects):
@@ -343,11 +367,12 @@
optimized_fetch=opt.optimized_fetch,
retry_fetches=opt.retry_fetches,
prune=opt.prune,
+ ssh_proxy=self.ssh_proxy,
clone_filter=self.manifest.CloneFilter,
partial_clone_exclude=self.manifest.PartialCloneExclude)
output = buf.getvalue()
- if opt.verbose and output:
+ if (opt.verbose or not success) and output:
print('\n' + output.rstrip())
if not success:
@@ -364,7 +389,11 @@
finish = time.time()
return (success, project, start, finish)
- def _Fetch(self, projects, opt, err_event):
+ @classmethod
+ def _FetchInitChild(cls, ssh_proxy):
+ cls.ssh_proxy = ssh_proxy
+
+ def _Fetch(self, projects, opt, err_event, ssh_proxy):
ret = True
jobs = opt.jobs_network if opt.jobs_network else self.jobs
@@ -394,8 +423,14 @@
break
return ret
+ # We pass the ssh proxy settings via the class. This allows multiprocessing
+ # to pickle it up when spawning children. We can't pass it as an argument
+ # to _FetchProjectList below as multiprocessing is unable to pickle those.
+ Sync.ssh_proxy = None
+
# NB: Multiprocessing is heavy, so don't spin it up for one job.
if len(projects_list) == 1 or jobs == 1:
+ self._FetchInitChild(ssh_proxy)
if not _ProcessResults(self._FetchProjectList(opt, x) for x in projects_list):
ret = False
else:
@@ -413,7 +448,8 @@
else:
pm.update(inc=0, msg='warming up')
chunksize = 4
- with multiprocessing.Pool(jobs) as pool:
+ with multiprocessing.Pool(
+ jobs, initializer=self._FetchInitChild, initargs=(ssh_proxy,)) as pool:
results = pool.imap_unordered(
functools.partial(self._FetchProjectList, opt),
projects_list,
@@ -422,6 +458,11 @@
ret = False
pool.close()
+ # Cleanup the reference now that we're done with it, and we're going to
+ # release any resources it points to. If we don't, later multiprocessing
+ # usage (e.g. checkouts) will try to pickle and then crash.
+ del Sync.ssh_proxy
+
pm.end()
self._fetch_times.Save()
@@ -430,6 +471,69 @@
return (ret, fetched)
+ def _FetchMain(self, opt, args, all_projects, err_event, manifest_name,
+ load_local_manifests, ssh_proxy):
+ """The main network fetch loop.
+
+ Args:
+ opt: Program options returned from optparse. See _Options().
+ args: Command line args used to filter out projects.
+ all_projects: List of all projects that should be fetched.
+ err_event: Whether an error was hit while processing.
+ manifest_name: Manifest file to be reloaded.
+ load_local_manifests: Whether to load local manifests.
+ ssh_proxy: SSH manager for clients & masters.
+
+ Returns:
+ List of all projects that should be checked out.
+ """
+ rp = self.manifest.repoProject
+
+ to_fetch = []
+ now = time.time()
+ if _ONE_DAY_S <= (now - rp.LastFetch):
+ to_fetch.append(rp)
+ to_fetch.extend(all_projects)
+ to_fetch.sort(key=self._fetch_times.Get, reverse=True)
+
+ success, fetched = self._Fetch(to_fetch, opt, err_event, ssh_proxy)
+ if not success:
+ err_event.set()
+
+ _PostRepoFetch(rp, opt.repo_verify)
+ if opt.network_only:
+ # bail out now; the rest touches the working tree
+ if err_event.is_set():
+ print('\nerror: Exited sync due to fetch errors.\n', file=sys.stderr)
+ sys.exit(1)
+ return
+
+ # Iteratively fetch missing and/or nested unregistered submodules
+ previously_missing_set = set()
+ while True:
+ self._ReloadManifest(manifest_name, load_local_manifests)
+ all_projects = self.GetProjects(args,
+ missing_ok=True,
+ submodules_ok=opt.fetch_submodules)
+ missing = []
+ for project in all_projects:
+ if project.gitdir not in fetched:
+ missing.append(project)
+ if not missing:
+ break
+ # Stop us from non-stopped fetching actually-missing repos: If set of
+ # missing repos has not been changed from last fetch, we break.
+ missing_set = set(p.name for p in missing)
+ if previously_missing_set == missing_set:
+ break
+ previously_missing_set = missing_set
+ success, new_fetched = self._Fetch(missing, opt, err_event, ssh_proxy)
+ if not success:
+ err_event.set()
+ fetched.update(new_fetched)
+
+ return all_projects
+
def _CheckoutOne(self, detach_head, force_sync, project):
"""Checkout work tree for one project
@@ -564,10 +668,18 @@
t.join()
pm.end()
- def _ReloadManifest(self, manifest_name=None):
+ def _ReloadManifest(self, manifest_name=None, load_local_manifests=True):
+ """Reload the manfiest from the file specified by the |manifest_name|.
+
+ It unloads the manifest if |manifest_name| is None.
+
+ Args:
+ manifest_name: Manifest file to be reloaded.
+ load_local_manifests: Whether to load local manifests.
+ """
if manifest_name:
# Override calls _Unload already
- self.manifest.Override(manifest_name)
+ self.manifest.Override(manifest_name, load_local_manifests=load_local_manifests)
else:
self.manifest._Unload()
@@ -614,6 +726,56 @@
fd.write('\n')
return 0
+ def UpdateCopyLinkfileList(self):
+ """Save all dests of copyfile and linkfile, and update them if needed.
+
+ Returns:
+ Whether update was successful.
+ """
+ new_paths = {}
+ new_linkfile_paths = []
+ new_copyfile_paths = []
+ for project in self.GetProjects(None, missing_ok=True):
+ new_linkfile_paths.extend(x.dest for x in project.linkfiles)
+ new_copyfile_paths.extend(x.dest for x in project.copyfiles)
+
+ new_paths = {
+ 'linkfile': new_linkfile_paths,
+ 'copyfile': new_copyfile_paths,
+ }
+
+ copylinkfile_name = 'copy-link-files.json'
+ copylinkfile_path = os.path.join(self.manifest.repodir, copylinkfile_name)
+ old_copylinkfile_paths = {}
+
+ if os.path.exists(copylinkfile_path):
+ with open(copylinkfile_path, 'rb') as fp:
+ try:
+ old_copylinkfile_paths = json.load(fp)
+ except:
+ print('error: %s is not a json formatted file.' %
+ copylinkfile_path, file=sys.stderr)
+ platform_utils.remove(copylinkfile_path)
+ return False
+
+ need_remove_files = []
+ need_remove_files.extend(
+ set(old_copylinkfile_paths.get('linkfile', [])) -
+ set(new_linkfile_paths))
+ need_remove_files.extend(
+ set(old_copylinkfile_paths.get('copyfile', [])) -
+ set(new_copyfile_paths))
+
+ for need_remove_file in need_remove_files:
+ # Try to remove the updated copyfile or linkfile.
+ # So, if the file is not exist, nothing need to do.
+ platform_utils.remove(need_remove_file, missing_ok=True)
+
+ # Create copy-link-files.json, save dest path of "copyfile" and "linkfile".
+ with open(copylinkfile_path, 'w', encoding='utf-8') as fp:
+ json.dump(new_paths, fp)
+ return True
+
def _SmartSyncSetup(self, opt, smart_sync_manifest_path):
if not self.manifest.manifest_server:
print('error: cannot smart sync: no manifest server defined in '
@@ -730,7 +892,7 @@
start, time.time(), clean)
if not clean:
sys.exit(1)
- self._ReloadManifest(opt.manifest_name)
+ self._ReloadManifest(manifest_name)
if opt.jobs is None:
self.jobs = self.manifest.default.sync_j
@@ -779,7 +941,7 @@
print('error: failed to remove existing smart sync override manifest: %s' %
e, file=sys.stderr)
- err_event = _threading.Event()
+ err_event = multiprocessing.Event()
rp = self.manifest.repoProject
rp.PreSync()
@@ -802,8 +964,16 @@
else:
self._UpdateManifestProject(opt, mp, manifest_name)
- if self._UseSuperproject(opt):
- manifest_name = self._UpdateProjectsRevisionId(opt, args)
+ load_local_manifests = not self.manifest.HasLocalManifests
+ use_superproject = git_superproject.UseSuperproject(opt, self.manifest)
+ superproject_logging_data = {
+ 'superproject': use_superproject,
+ 'haslocalmanifests': bool(self.manifest.HasLocalManifests),
+ 'hassuperprojecttag': bool(self.manifest.superproject),
+ }
+ if use_superproject:
+ manifest_name = self._UpdateProjectsRevisionId(
+ opt, args, load_local_manifests, superproject_logging_data) or opt.manifest_name
if self.gitc_manifest:
gitc_manifest_projects = self.GetProjects(args,
@@ -849,49 +1019,17 @@
self._fetch_times = _FetchTimes(self.manifest)
if not opt.local_only:
- to_fetch = []
- now = time.time()
- if _ONE_DAY_S <= (now - rp.LastFetch):
- to_fetch.append(rp)
- to_fetch.extend(all_projects)
- to_fetch.sort(key=self._fetch_times.Get, reverse=True)
+ with multiprocessing.Manager() as manager:
+ with ssh.ProxyManager(manager) as ssh_proxy:
+ # Initialize the socket dir once in the parent.
+ ssh_proxy.sock()
+ all_projects = self._FetchMain(opt, args, all_projects, err_event,
+ manifest_name, load_local_manifests,
+ ssh_proxy)
- success, fetched = self._Fetch(to_fetch, opt, err_event)
- if not success:
- err_event.set()
-
- _PostRepoFetch(rp, opt.repo_verify)
if opt.network_only:
- # bail out now; the rest touches the working tree
- if err_event.is_set():
- print('\nerror: Exited sync due to fetch errors.\n', file=sys.stderr)
- sys.exit(1)
return
- # Iteratively fetch missing and/or nested unregistered submodules
- previously_missing_set = set()
- while True:
- self._ReloadManifest(manifest_name)
- all_projects = self.GetProjects(args,
- missing_ok=True,
- submodules_ok=opt.fetch_submodules)
- missing = []
- for project in all_projects:
- if project.gitdir not in fetched:
- missing.append(project)
- if not missing:
- break
- # Stop us from non-stopped fetching actually-missing repos: If set of
- # missing repos has not been changed from last fetch, we break.
- missing_set = set(p.name for p in missing)
- if previously_missing_set == missing_set:
- break
- previously_missing_set = missing_set
- success, new_fetched = self._Fetch(missing, opt, err_event)
- if not success:
- err_event.set()
- fetched.update(new_fetched)
-
# If we saw an error, exit with code 1 so that other scripts can check.
if err_event.is_set():
err_network_sync = True
@@ -914,6 +1052,13 @@
print('\nerror: Local checkouts *not* updated.', file=sys.stderr)
sys.exit(1)
+ err_update_linkfiles = not self.UpdateCopyLinkfileList()
+ if err_update_linkfiles:
+ err_event.set()
+ if opt.fail_fast:
+ print('\nerror: Local update copyfile or linkfile failed.', file=sys.stderr)
+ sys.exit(1)
+
err_results = []
# NB: We don't exit here because this is the last step.
err_checkout = not self._Checkout(all_projects, opt, err_results)
@@ -932,6 +1077,8 @@
print('error: Downloading network changes failed.', file=sys.stderr)
if err_update_projects:
print('error: Updating local project lists failed.', file=sys.stderr)
+ if err_update_linkfiles:
+ print('error: Updating copyfiles or linkfiles failed.', file=sys.stderr)
if err_checkout:
print('error: Checking out local projects failed.', file=sys.stderr)
if err_results:
@@ -940,6 +1087,15 @@
file=sys.stderr)
sys.exit(1)
+ # Log the previous sync analysis state from the config.
+ self.git_event_log.LogDataConfigEvents(mp.config.GetSyncAnalysisStateData(),
+ 'previous_sync_state')
+
+ # Update and log with the new sync analysis state.
+ mp.config.UpdateSyncAnalysisState(opt, superproject_logging_data)
+ self.git_event_log.LogDataConfigEvents(mp.config.GetSyncAnalysisStateData(),
+ 'current_sync_state')
+
if not opt.quiet:
print('repo sync has finished successfully.')
@@ -1011,10 +1167,7 @@
with open(self._path) as f:
self._times = json.load(f)
except (IOError, ValueError):
- try:
- platform_utils.remove(self._path)
- except OSError:
- pass
+ platform_utils.remove(self._path, missing_ok=True)
self._times = {}
def Save(self):
@@ -1032,10 +1185,7 @@
with open(self._path, 'w') as f:
json.dump(self._times, f, indent=2)
except (IOError, TypeError):
- try:
- platform_utils.remove(self._path)
- except OSError:
- pass
+ platform_utils.remove(self._path, missing_ok=True)
# This is a replacement for xmlrpc.client.Transport using urllib2
# and supporting persistent-http[s]. It cannot change hosts from
diff --git a/subcmds/upload.py b/subcmds/upload.py
index 50dccc5..c48deab 100644
--- a/subcmds/upload.py
+++ b/subcmds/upload.py
@@ -13,10 +13,12 @@
# limitations under the License.
import copy
+import functools
+import optparse
import re
import sys
-from command import InteractiveCommand
+from command import DEFAULT_LOCAL_JOBS, InteractiveCommand
from editor import Editor
from error import UploadError
from git_command import GitCommand
@@ -53,7 +55,7 @@
class Upload(InteractiveCommand):
- common = True
+ COMMON = True
helpSummary = "Upload changes for code review"
helpUsage = """
%prog [--re --cc] [<project>]...
@@ -145,58 +147,66 @@
Gerrit Code Review: https://www.gerritcodereview.com/
"""
+ PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
def _Options(self, p):
p.add_option('-t',
dest='auto_topic', action='store_true',
- help='Send local branch name to Gerrit Code Review')
+ help='send local branch name to Gerrit Code Review')
p.add_option('--hashtag', '--ht',
dest='hashtags', action='append', default=[],
- help='Add hashtags (comma delimited) to the review.')
+ help='add hashtags (comma delimited) to the review')
p.add_option('--hashtag-branch', '--htb',
action='store_true',
- help='Add local branch name as a hashtag.')
+ help='add local branch name as a hashtag')
p.add_option('-l', '--label',
dest='labels', action='append', default=[],
- help='Add a label when uploading.')
+ help='add a label when uploading')
p.add_option('--re', '--reviewers',
type='string', action='append', dest='reviewers',
- help='Request reviews from these people.')
+ help='request reviews from these people')
p.add_option('--cc',
type='string', action='append', dest='cc',
- help='Also send email to these email addresses.')
- p.add_option('--br',
+ help='also send email to these email addresses')
+ p.add_option('--br', '--branch',
type='string', action='store', dest='branch',
- help='Branch to upload.')
- p.add_option('--cbr', '--current-branch',
+ help='(local) branch to upload')
+ p.add_option('-c', '--current-branch',
dest='current_branch', action='store_true',
- help='Upload current git branch.')
+ help='upload current git branch')
+ p.add_option('--no-current-branch',
+ dest='current_branch', action='store_false',
+ help='upload all git branches')
+ # Turn this into a warning & remove this someday.
+ p.add_option('--cbr',
+ dest='current_branch', action='store_true',
+ help=optparse.SUPPRESS_HELP)
p.add_option('--ne', '--no-emails',
action='store_false', dest='notify', default=True,
- help='If specified, do not send emails on upload.')
+ help='do not send e-mails on upload')
p.add_option('-p', '--private',
action='store_true', dest='private', default=False,
- help='If specified, upload as a private change.')
+ help='upload as a private change (deprecated; use --wip)')
p.add_option('-w', '--wip',
action='store_true', dest='wip', default=False,
- help='If specified, upload as a work-in-progress change.')
+ help='upload as a work-in-progress change')
p.add_option('-o', '--push-option',
type='string', action='append', dest='push_options',
default=[],
- help='Additional push options to transmit')
+ help='additional push options to transmit')
p.add_option('-D', '--destination', '--dest',
type='string', action='store', dest='dest_branch',
metavar='BRANCH',
- help='Submit for review on this target branch.')
+ help='submit for review on this target branch')
p.add_option('-n', '--dry-run',
dest='dryrun', default=False, action='store_true',
- help='Do everything except actually upload the CL.')
+ help='do everything except actually upload the CL')
p.add_option('-y', '--yes',
default=False, action='store_true',
- help='Answer yes to all safe prompts.')
+ help='answer yes to all safe prompts')
p.add_option('--no-cert-checks',
dest='validate_certs', action='store_false', default=True,
- help='Disable verifying ssl certs (unsafe).')
+ help='disable verifying ssl certs (unsafe)')
RepoHook.AddOptionGroup(p, 'pre-upload')
def _SingleBranch(self, opt, branch, people):
@@ -502,40 +512,46 @@
merge_branch = p.stdout.strip()
return merge_branch
+ @staticmethod
+ def _GatherOne(opt, project):
+ """Figure out the upload status for |project|."""
+ if opt.current_branch:
+ cbr = project.CurrentBranch
+ up_branch = project.GetUploadableBranch(cbr)
+ avail = [up_branch] if up_branch else None
+ else:
+ avail = project.GetUploadableBranches(opt.branch)
+ return (project, avail)
+
def Execute(self, opt, args):
- project_list = self.GetProjects(args)
- pending = []
- reviewers = []
- cc = []
- branch = None
+ projects = self.GetProjects(args)
- if opt.branch:
- branch = opt.branch
-
- for project in project_list:
- if opt.current_branch:
- cbr = project.CurrentBranch
- up_branch = project.GetUploadableBranch(cbr)
- if up_branch:
- avail = [up_branch]
- else:
- avail = None
- print('repo: error: Unable to upload branch "%s". '
+ def _ProcessResults(_pool, _out, results):
+ pending = []
+ for result in results:
+ project, avail = result
+ if avail is None:
+ print('repo: error: %s: Unable to upload branch "%s". '
'You might be able to fix the branch by running:\n'
' git branch --set-upstream-to m/%s' %
- (str(cbr), self.manifest.branch),
+ (project.relpath, project.CurrentBranch, self.manifest.branch),
file=sys.stderr)
- else:
- avail = project.GetUploadableBranches(branch)
- if avail:
- pending.append((project, avail))
+ elif avail:
+ pending.append(result)
+ return pending
+
+ pending = self.ExecuteInParallel(
+ opt.jobs,
+ functools.partial(self._GatherOne, opt),
+ projects,
+ callback=_ProcessResults)
if not pending:
- if branch is None:
+ if opt.branch is None:
print('repo: error: no branches ready for upload', file=sys.stderr)
else:
print('repo: error: no branches named "%s" ready for upload' %
- (branch,), file=sys.stderr)
+ (opt.branch,), file=sys.stderr)
return 1
pending_proj_names = [project.name for (project, available) in pending]
@@ -548,10 +564,8 @@
worktree_list=pending_worktrees):
return 1
- if opt.reviewers:
- reviewers = _SplitEmails(opt.reviewers)
- if opt.cc:
- cc = _SplitEmails(opt.cc)
+ reviewers = _SplitEmails(opt.reviewers) if opt.reviewers else []
+ cc = _SplitEmails(opt.cc) if opt.cc else []
people = (reviewers, cc)
if len(pending) == 1 and len(pending[0][1]) == 1:
diff --git a/subcmds/version.py b/subcmds/version.py
index e95a86d..09b053e 100644
--- a/subcmds/version.py
+++ b/subcmds/version.py
@@ -18,13 +18,14 @@
from command import Command, MirrorSafeCommand
from git_command import git, RepoSourceVersion, user_agent
from git_refs import HEAD
+from wrapper import Wrapper
class Version(Command, MirrorSafeCommand):
wrapper_version = None
wrapper_path = None
- common = False
+ COMMON = False
helpSummary = "Display the version of repo"
helpUsage = """
%prog
@@ -62,3 +63,4 @@
print('OS %s %s (%s)' % (uname.system, uname.release, uname.version))
print('CPU %s (%s)' %
(uname.machine, uname.processor if uname.processor else 'unknown'))
+ print('Bug reports:', Wrapper().BUG_URL)
diff --git a/tests/fixtures/test.gitconfig b/tests/fixtures/test.gitconfig
index 9b3f257..b178cf6 100644
--- a/tests/fixtures/test.gitconfig
+++ b/tests/fixtures/test.gitconfig
@@ -11,3 +11,13 @@
intk = 10k
intm = 10m
intg = 10g
+[repo "syncstate.main"]
+ synctime = 2021-09-14T17:23:43.537338Z
+ version = 1
+[repo "syncstate.sys"]
+ argv = ['/usr/bin/pytest-3']
+[repo "syncstate.superproject"]
+ test = false
+[repo "syncstate.options"]
+ verbose = true
+ mpupdate = false
diff --git a/tests/test_git_command.py b/tests/test_git_command.py
index 912a9db..93300a6 100644
--- a/tests/test_git_command.py
+++ b/tests/test_git_command.py
@@ -26,33 +26,6 @@
import wrapper
-class SSHUnitTest(unittest.TestCase):
- """Tests the ssh functions."""
-
- def test_ssh_version(self):
- """Check ssh_version() handling."""
- ver = git_command._parse_ssh_version('Unknown\n')
- self.assertEqual(ver, ())
- ver = git_command._parse_ssh_version('OpenSSH_1.0\n')
- self.assertEqual(ver, (1, 0))
- ver = git_command._parse_ssh_version('OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.13, OpenSSL 1.0.1f 6 Jan 2014\n')
- self.assertEqual(ver, (6, 6, 1))
- ver = git_command._parse_ssh_version('OpenSSH_7.6p1 Ubuntu-4ubuntu0.3, OpenSSL 1.0.2n 7 Dec 2017\n')
- self.assertEqual(ver, (7, 6))
-
- def test_ssh_sock(self):
- """Check ssh_sock() function."""
- with mock.patch('tempfile.mkdtemp', return_value='/tmp/foo'):
- # old ssh version uses port
- with mock.patch('git_command.ssh_version', return_value=(6, 6)):
- self.assertTrue(git_command.ssh_sock().endswith('%p'))
- git_command._ssh_sock_path = None
- # new ssh version uses hash
- with mock.patch('git_command.ssh_version', return_value=(6, 7)):
- self.assertTrue(git_command.ssh_sock().endswith('%C'))
- git_command._ssh_sock_path = None
-
-
class GitCallUnitTest(unittest.TestCase):
"""Tests the _GitCall class (via git_command.git)."""
diff --git a/tests/test_git_config.py b/tests/test_git_config.py
index 3300c12..faf12a2 100644
--- a/tests/test_git_config.py
+++ b/tests/test_git_config.py
@@ -104,6 +104,25 @@
for key, value in TESTS:
self.assertEqual(value, self.config.GetInt('section.%s' % (key,)))
+ def test_GetSyncAnalysisStateData(self):
+ """Test config entries with a sync state analysis data."""
+ superproject_logging_data = {}
+ superproject_logging_data['test'] = False
+ options = type('options', (object,), {})()
+ options.verbose = 'true'
+ options.mp_update = 'false'
+ TESTS = (
+ ('superproject.test', 'false'),
+ ('options.verbose', 'true'),
+ ('options.mpupdate', 'false'),
+ ('main.version', '1'),
+ )
+ self.config.UpdateSyncAnalysisState(options, superproject_logging_data)
+ sync_data = self.config.GetSyncAnalysisStateData()
+ for key, value in TESTS:
+ self.assertEqual(sync_data[f'{git_config.SYNC_STATE_PREFIX}{key}'], value)
+ self.assertTrue(sync_data[f'{git_config.SYNC_STATE_PREFIX}main.synctime'])
+
class GitConfigReadWriteTests(unittest.TestCase):
"""Read/write tests of the GitConfig class."""
diff --git a/tests/test_git_superproject.py b/tests/test_git_superproject.py
index 9550949..a24fc7f 100644
--- a/tests/test_git_superproject.py
+++ b/tests/test_git_superproject.py
@@ -14,6 +14,7 @@
"""Unittests for the git_superproject.py module."""
+import json
import os
import platform
import tempfile
@@ -21,13 +22,20 @@
from unittest import mock
import git_superproject
+import git_trace2_event_log
import manifest_xml
import platform_utils
+from test_manifest_xml import sort_attributes
class SuperprojectTestCase(unittest.TestCase):
"""TestCase for the Superproject module."""
+ PARENT_SID_KEY = 'GIT_TRACE2_PARENT_SID'
+ PARENT_SID_VALUE = 'parent_sid'
+ SELF_SID_REGEX = r'repo-\d+T\d+Z-.*'
+ FULL_SID_REGEX = r'^%s/%s' % (PARENT_SID_VALUE, SELF_SID_REGEX)
+
def setUp(self):
"""Set up superproject every time."""
self.tempdir = tempfile.mkdtemp(prefix='repo_tests')
@@ -37,6 +45,13 @@
os.mkdir(self.repodir)
self.platform = platform.system().lower()
+ # By default we initialize with the expected case where
+ # repo launches us (so GIT_TRACE2_PARENT_SID is set).
+ env = {
+ self.PARENT_SID_KEY: self.PARENT_SID_VALUE,
+ }
+ self.git_event_log = git_trace2_event_log.EventLog(env=env)
+
# The manifest parsing really wants a git repo currently.
gitdir = os.path.join(self.repodir, 'manifests.git')
os.mkdir(gitdir)
@@ -53,7 +68,8 @@
<project path="art" name="platform/art" groups="notdefault,platform-""" + self.platform + """
" /></manifest>
""")
- self._superproject = git_superproject.Superproject(manifest, self.repodir)
+ self._superproject = git_superproject.Superproject(manifest, self.repodir,
+ self.git_event_log)
def tearDown(self):
"""Tear down superproject every time."""
@@ -65,14 +81,56 @@
fp.write(data)
return manifest_xml.XmlManifest(self.repodir, self.manifest_file)
+ def verifyCommonKeys(self, log_entry, expected_event_name, full_sid=True):
+ """Helper function to verify common event log keys."""
+ self.assertIn('event', log_entry)
+ self.assertIn('sid', log_entry)
+ self.assertIn('thread', log_entry)
+ self.assertIn('time', log_entry)
+
+ # Do basic data format validation.
+ self.assertEqual(expected_event_name, log_entry['event'])
+ if full_sid:
+ self.assertRegex(log_entry['sid'], self.FULL_SID_REGEX)
+ else:
+ self.assertRegex(log_entry['sid'], self.SELF_SID_REGEX)
+ self.assertRegex(log_entry['time'], r'^\d+-\d+-\d+T\d+:\d+:\d+\.\d+Z$')
+
+ def readLog(self, log_path):
+ """Helper function to read log data into a list."""
+ log_data = []
+ with open(log_path, mode='rb') as f:
+ for line in f:
+ log_data.append(json.loads(line))
+ return log_data
+
+ def verifyErrorEvent(self):
+ """Helper to verify that error event is written."""
+
+ with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
+ log_path = self.git_event_log.Write(path=tempdir)
+ self.log_data = self.readLog(log_path)
+
+ self.assertEqual(len(self.log_data), 2)
+ error_event = self.log_data[1]
+ self.verifyCommonKeys(self.log_data[0], expected_event_name='version')
+ self.verifyCommonKeys(error_event, expected_event_name='error')
+ # Check for 'error' event specific fields.
+ self.assertIn('msg', error_event)
+ self.assertIn('fmt', error_event)
+
def test_superproject_get_superproject_no_superproject(self):
"""Test with no url."""
manifest = self.getXmlManifest("""
<manifest>
</manifest>
""")
- superproject = git_superproject.Superproject(manifest, self.repodir)
- self.assertFalse(superproject.Sync())
+ superproject = git_superproject.Superproject(manifest, self.repodir, self.git_event_log)
+ # Test that exit condition is false when there is no superproject tag.
+ sync_result = superproject.Sync()
+ self.assertFalse(sync_result.success)
+ self.assertFalse(sync_result.fatal)
+ self.verifyErrorEvent()
def test_superproject_get_superproject_invalid_url(self):
"""Test with an invalid url."""
@@ -83,8 +141,10 @@
<superproject name="superproject"/>
</manifest>
""")
- superproject = git_superproject.Superproject(manifest, self.repodir)
- self.assertFalse(superproject.Sync())
+ superproject = git_superproject.Superproject(manifest, self.repodir, self.git_event_log)
+ sync_result = superproject.Sync()
+ self.assertFalse(sync_result.success)
+ self.assertTrue(sync_result.fatal)
def test_superproject_get_superproject_invalid_branch(self):
"""Test with an invalid branch."""
@@ -95,21 +155,28 @@
<superproject name="superproject"/>
</manifest>
""")
- superproject = git_superproject.Superproject(manifest, self.repodir)
- with mock.patch.object(self._superproject, '_GetBranch', return_value='junk'):
- self.assertFalse(superproject.Sync())
+ self._superproject = git_superproject.Superproject(manifest, self.repodir,
+ self.git_event_log)
+ with mock.patch.object(self._superproject, '_branch', 'junk'):
+ sync_result = self._superproject.Sync()
+ self.assertFalse(sync_result.success)
+ self.assertTrue(sync_result.fatal)
def test_superproject_get_superproject_mock_init(self):
"""Test with _Init failing."""
with mock.patch.object(self._superproject, '_Init', return_value=False):
- self.assertFalse(self._superproject.Sync())
+ sync_result = self._superproject.Sync()
+ self.assertFalse(sync_result.success)
+ self.assertTrue(sync_result.fatal)
def test_superproject_get_superproject_mock_fetch(self):
"""Test with _Fetch failing."""
with mock.patch.object(self._superproject, '_Init', return_value=True):
os.mkdir(self._superproject._superproject_path)
with mock.patch.object(self._superproject, '_Fetch', return_value=False):
- self.assertFalse(self._superproject.Sync())
+ sync_result = self._superproject.Sync()
+ self.assertFalse(sync_result.success)
+ self.assertTrue(sync_result.fatal)
def test_superproject_get_all_project_commit_ids_mock_ls_tree(self):
"""Test with LsTree being a mock."""
@@ -121,12 +188,13 @@
with mock.patch.object(self._superproject, '_Init', return_value=True):
with mock.patch.object(self._superproject, '_Fetch', return_value=True):
with mock.patch.object(self._superproject, '_LsTree', return_value=data):
- commit_ids = self._superproject._GetAllProjectsCommitIds()
- self.assertEqual(commit_ids, {
+ commit_ids_result = self._superproject._GetAllProjectsCommitIds()
+ self.assertEqual(commit_ids_result.commit_ids, {
'art': '2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea',
'bootable/recovery': 'e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06',
'build/bazel': 'ade9b7a0d874e25fff4bf2552488825c6f111928'
})
+ self.assertFalse(commit_ids_result.fatal)
def test_superproject_write_manifest_file(self):
"""Test with writing manifest to a file after setting revisionId."""
@@ -135,18 +203,18 @@
project.SetRevisionId('ABCDEF')
# Create temporary directory so that it can write the file.
os.mkdir(self._superproject._superproject_path)
- manifest_path = self._superproject._WriteManfiestFile()
+ manifest_path = self._superproject._WriteManifestFile()
self.assertIsNotNone(manifest_path)
with open(manifest_path, 'r') as fp:
- manifest_xml = fp.read()
+ manifest_xml_data = fp.read()
self.assertEqual(
- manifest_xml,
- '<?xml version="1.0" ?><manifest>' +
- '<remote name="default-remote" fetch="http://localhost"/>' +
- '<default remote="default-remote" revision="refs/heads/main"/>' +
- '<project name="platform/art" path="art" revision="ABCDEF" ' +
- 'groups="notdefault,platform-' + self.platform + '"/>' +
- '<superproject name="superproject"/>' +
+ sort_attributes(manifest_xml_data),
+ '<?xml version="1.0" ?><manifest>'
+ '<remote fetch="http://localhost" name="default-remote"/>'
+ '<default remote="default-remote" revision="refs/heads/main"/>'
+ '<project groups="notdefault,platform-' + self.platform + '" '
+ 'name="platform/art" path="art" revision="ABCDEF" upstream="refs/heads/main"/>'
+ '<superproject name="superproject"/>'
'</manifest>')
def test_superproject_update_project_revision_id(self):
@@ -162,19 +230,145 @@
return_value=data):
# Create temporary directory so that it can write the file.
os.mkdir(self._superproject._superproject_path)
- manifest_path = self._superproject.UpdateProjectsRevisionId(projects)
- self.assertIsNotNone(manifest_path)
- with open(manifest_path, 'r') as fp:
- manifest_xml = fp.read()
+ update_result = self._superproject.UpdateProjectsRevisionId(projects)
+ self.assertIsNotNone(update_result.manifest_path)
+ self.assertFalse(update_result.fatal)
+ with open(update_result.manifest_path, 'r') as fp:
+ manifest_xml_data = fp.read()
self.assertEqual(
- manifest_xml,
- '<?xml version="1.0" ?><manifest>' +
- '<remote name="default-remote" fetch="http://localhost"/>' +
- '<default remote="default-remote" revision="refs/heads/main"/>' +
- '<project name="platform/art" path="art" ' +
- 'revision="2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea" ' +
- 'groups="notdefault,platform-' + self.platform + '"/>' +
- '<superproject name="superproject"/>' +
+ sort_attributes(manifest_xml_data),
+ '<?xml version="1.0" ?><manifest>'
+ '<remote fetch="http://localhost" name="default-remote"/>'
+ '<default remote="default-remote" revision="refs/heads/main"/>'
+ '<project groups="notdefault,platform-' + self.platform + '" '
+ 'name="platform/art" path="art" '
+ 'revision="2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea" upstream="refs/heads/main"/>'
+ '<superproject name="superproject"/>'
+ '</manifest>')
+
+ def test_superproject_update_project_revision_id_no_superproject_tag(self):
+ """Test update of commit ids of a manifest without superproject tag."""
+ manifest = self.getXmlManifest("""
+<manifest>
+ <remote name="default-remote" fetch="http://localhost" />
+ <default remote="default-remote" revision="refs/heads/main" />
+ <project name="test-name"/>
+</manifest>
+""")
+ self.maxDiff = None
+ self._superproject = git_superproject.Superproject(manifest, self.repodir,
+ self.git_event_log)
+ self.assertEqual(len(self._superproject._manifest.projects), 1)
+ projects = self._superproject._manifest.projects
+ project = projects[0]
+ project.SetRevisionId('ABCDEF')
+ update_result = self._superproject.UpdateProjectsRevisionId(projects)
+ self.assertIsNone(update_result.manifest_path)
+ self.assertFalse(update_result.fatal)
+ self.verifyErrorEvent()
+ self.assertEqual(
+ sort_attributes(manifest.ToXml().toxml()),
+ '<?xml version="1.0" ?><manifest>'
+ '<remote fetch="http://localhost" name="default-remote"/>'
+ '<default remote="default-remote" revision="refs/heads/main"/>'
+ '<project name="test-name" revision="ABCDEF" upstream="refs/heads/main"/>'
+ '</manifest>')
+
+ def test_superproject_update_project_revision_id_from_local_manifest_group(self):
+ """Test update of commit ids of a manifest that have local manifest no superproject group."""
+ local_group = manifest_xml.LOCAL_MANIFEST_GROUP_PREFIX + ':local'
+ manifest = self.getXmlManifest("""
+<manifest>
+ <remote name="default-remote" fetch="http://localhost" />
+ <remote name="goog" fetch="http://localhost2" />
+ <default remote="default-remote" revision="refs/heads/main" />
+ <superproject name="superproject"/>
+ <project path="vendor/x" name="platform/vendor/x" remote="goog"
+ groups=\"""" + local_group + """
+ " revision="master-with-vendor" clone-depth="1" />
+ <project path="art" name="platform/art" groups="notdefault,platform-""" + self.platform + """
+ " /></manifest>
+""")
+ self.maxDiff = None
+ self._superproject = git_superproject.Superproject(manifest, self.repodir,
+ self.git_event_log)
+ self.assertEqual(len(self._superproject._manifest.projects), 2)
+ projects = self._superproject._manifest.projects
+ data = ('160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00')
+ with mock.patch.object(self._superproject, '_Init', return_value=True):
+ with mock.patch.object(self._superproject, '_Fetch', return_value=True):
+ with mock.patch.object(self._superproject,
+ '_LsTree',
+ return_value=data):
+ # Create temporary directory so that it can write the file.
+ os.mkdir(self._superproject._superproject_path)
+ update_result = self._superproject.UpdateProjectsRevisionId(projects)
+ self.assertIsNotNone(update_result.manifest_path)
+ self.assertFalse(update_result.fatal)
+ with open(update_result.manifest_path, 'r') as fp:
+ manifest_xml_data = fp.read()
+ # Verify platform/vendor/x's project revision hasn't changed.
+ self.assertEqual(
+ sort_attributes(manifest_xml_data),
+ '<?xml version="1.0" ?><manifest>'
+ '<remote fetch="http://localhost" name="default-remote"/>'
+ '<remote fetch="http://localhost2" name="goog"/>'
+ '<default remote="default-remote" revision="refs/heads/main"/>'
+ '<project groups="notdefault,platform-' + self.platform + '" '
+ 'name="platform/art" path="art" '
+ 'revision="2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea" upstream="refs/heads/main"/>'
+ '<project clone-depth="1" groups="' + local_group + '" '
+ 'name="platform/vendor/x" path="vendor/x" remote="goog" '
+ 'revision="master-with-vendor"/>'
+ '<superproject name="superproject"/>'
+ '</manifest>')
+
+ def test_superproject_update_project_revision_id_with_pinned_manifest(self):
+ """Test update of commit ids of a pinned manifest."""
+ manifest = self.getXmlManifest("""
+<manifest>
+ <remote name="default-remote" fetch="http://localhost" />
+ <default remote="default-remote" revision="refs/heads/main" />
+ <superproject name="superproject"/>
+ <project path="vendor/x" name="platform/vendor/x" revision="" />
+ <project path="vendor/y" name="platform/vendor/y"
+ revision="52d3c9f7c107839ece2319d077de0cd922aa9d8f" />
+ <project path="art" name="platform/art" groups="notdefault,platform-""" + self.platform + """
+ " /></manifest>
+""")
+ self.maxDiff = None
+ self._superproject = git_superproject.Superproject(manifest, self.repodir,
+ self.git_event_log)
+ self.assertEqual(len(self._superproject._manifest.projects), 3)
+ projects = self._superproject._manifest.projects
+ data = ('160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00'
+ '160000 commit e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06\tvendor/x\x00')
+ with mock.patch.object(self._superproject, '_Init', return_value=True):
+ with mock.patch.object(self._superproject, '_Fetch', return_value=True):
+ with mock.patch.object(self._superproject,
+ '_LsTree',
+ return_value=data):
+ # Create temporary directory so that it can write the file.
+ os.mkdir(self._superproject._superproject_path)
+ update_result = self._superproject.UpdateProjectsRevisionId(projects)
+ self.assertIsNotNone(update_result.manifest_path)
+ self.assertFalse(update_result.fatal)
+ with open(update_result.manifest_path, 'r') as fp:
+ manifest_xml_data = fp.read()
+ # Verify platform/vendor/x's project revision hasn't changed.
+ self.assertEqual(
+ sort_attributes(manifest_xml_data),
+ '<?xml version="1.0" ?><manifest>'
+ '<remote fetch="http://localhost" name="default-remote"/>'
+ '<default remote="default-remote" revision="refs/heads/main"/>'
+ '<project groups="notdefault,platform-' + self.platform + '" '
+ 'name="platform/art" path="art" '
+ 'revision="2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea" upstream="refs/heads/main"/>'
+ '<project name="platform/vendor/x" path="vendor/x" '
+ 'revision="e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06" upstream="refs/heads/main"/>'
+ '<project name="platform/vendor/y" path="vendor/y" '
+ 'revision="52d3c9f7c107839ece2319d077de0cd922aa9d8f"/>'
+ '<superproject name="superproject"/>'
'</manifest>')
diff --git a/tests/test_git_trace2_event_log.py b/tests/test_git_trace2_event_log.py
index 4a3a4c4..89dcfb9 100644
--- a/tests/test_git_trace2_event_log.py
+++ b/tests/test_git_trace2_event_log.py
@@ -42,7 +42,7 @@
self._event_log_module = git_trace2_event_log.EventLog(env=env)
self._log_data = None
- def verifyCommonKeys(self, log_entry, expected_event_name, full_sid=True):
+ def verifyCommonKeys(self, log_entry, expected_event_name=None, full_sid=True):
"""Helper function to verify common event log keys."""
self.assertIn('event', log_entry)
self.assertIn('sid', log_entry)
@@ -50,7 +50,8 @@
self.assertIn('time', log_entry)
# Do basic data format validation.
- self.assertEqual(expected_event_name, log_entry['event'])
+ if expected_event_name:
+ self.assertEqual(expected_event_name, log_entry['event'])
if full_sid:
self.assertRegex(log_entry['sid'], self.FULL_SID_REGEX)
else:
@@ -65,6 +66,13 @@
log_data.append(json.loads(line))
return log_data
+ def remove_prefix(self, s, prefix):
+ """Return a copy string after removing |prefix| from |s|, if present or the original string."""
+ if s.startswith(prefix):
+ return s[len(prefix):]
+ else:
+ return s
+
def test_initial_state_with_parent_sid(self):
"""Test initial state when 'GIT_TRACE2_PARENT_SID' is set by parent."""
self.assertRegex(self._event_log_module.full_sid, self.FULL_SID_REGEX)
@@ -234,6 +242,66 @@
self.assertEqual(len(self._log_data), 1)
self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
+ def test_data_event_config(self):
+ """Test 'data' event data outputs all config keys.
+
+ Expected event log:
+ <version event>
+ <data event>
+ <data event>
+ """
+ config = {
+ 'git.foo': 'bar',
+ 'repo.partialclone': 'false',
+ 'repo.syncstate.superproject.hassuperprojecttag': 'true',
+ 'repo.syncstate.superproject.sys.argv': ['--', 'sync', 'protobuf'],
+ }
+ prefix_value = 'prefix'
+ self._event_log_module.LogDataConfigEvents(config, prefix_value)
+
+ with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
+ log_path = self._event_log_module.Write(path=tempdir)
+ self._log_data = self.readLog(log_path)
+
+ self.assertEqual(len(self._log_data), 5)
+ data_events = self._log_data[1:]
+ self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
+
+ for event in data_events:
+ self.verifyCommonKeys(event)
+ # Check for 'data' event specific fields.
+ self.assertIn('key', event)
+ self.assertIn('value', event)
+ key = event['key']
+ key = self.remove_prefix(key, f'{prefix_value}/')
+ value = event['value']
+ self.assertEqual(self._event_log_module.GetDataEventName(value), event['event'])
+ self.assertTrue(key in config and value == config[key])
+
+ def test_error_event(self):
+ """Test and validate 'error' event data is valid.
+
+ Expected event log:
+ <version event>
+ <error event>
+ """
+ msg = 'invalid option: --cahced'
+ fmt = 'invalid option: %s'
+ self._event_log_module.ErrorEvent(msg, fmt)
+ with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
+ log_path = self._event_log_module.Write(path=tempdir)
+ self._log_data = self.readLog(log_path)
+
+ self.assertEqual(len(self._log_data), 2)
+ error_event = self._log_data[1]
+ self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
+ self.verifyCommonKeys(error_event, expected_event_name='error')
+ # Check for 'error' event specific fields.
+ self.assertIn('msg', error_event)
+ self.assertIn('fmt', error_event)
+ self.assertEqual(error_event['msg'], msg)
+ self.assertEqual(error_event['fmt'], fmt)
+
def test_write_with_filename(self):
"""Test Write() with a path to a file exits with None."""
self.assertIsNone(self._event_log_module.Write(path='path/to/file'))
diff --git a/tests/test_manifest_xml.py b/tests/test_manifest_xml.py
index eda0696..cb3eb85 100644
--- a/tests/test_manifest_xml.py
+++ b/tests/test_manifest_xml.py
@@ -16,6 +16,7 @@
import os
import platform
+import re
import shutil
import tempfile
import unittest
@@ -52,6 +53,9 @@
'blah/foo~',
# Block Unicode characters that get normalized out by filesystems.
u'foo\u200Cbar',
+ # Block newlines.
+ 'f\n/bar',
+ 'f\r/bar',
)
# Make sure platforms that use path separators (e.g. Windows) are also
@@ -60,6 +64,30 @@
INVALID_FS_PATHS += tuple(x.replace('/', os.path.sep) for x in INVALID_FS_PATHS)
+def sort_attributes(manifest):
+ """Sort the attributes of all elements alphabetically.
+
+ This is needed because different versions of the toxml() function from
+ xml.dom.minidom outputs the attributes of elements in different orders.
+ Before Python 3.8 they were output alphabetically, later versions preserve
+ the order specified by the user.
+
+ Args:
+ manifest: String containing an XML manifest.
+
+ Returns:
+ The XML manifest with the attributes of all elements sorted alphabetically.
+ """
+ new_manifest = ''
+ # This will find every element in the XML manifest, whether they have
+ # attributes or not. This simplifies recreating the manifest below.
+ matches = re.findall(r'(<[/?]?[a-z-]+\s*)((?:\S+?="[^"]+"\s*?)*)(\s*[/?]?>)', manifest)
+ for head, attrs, tail in matches:
+ m = re.findall(r'\S+?="[^"]+"', attrs)
+ new_manifest += head + ' '.join(sorted(m)) + tail
+ return new_manifest
+
+
class ManifestParseTestCase(unittest.TestCase):
"""TestCase for parsing manifests."""
@@ -91,6 +119,11 @@
fp.write(data)
return manifest_xml.XmlManifest(self.repodir, self.manifest_file)
+ @staticmethod
+ def encodeXmlAttr(attr):
+ """Encode |attr| using XML escape rules."""
+ return attr.replace('\r', '
').replace('\n', '
')
+
class ManifestValidateFilePaths(unittest.TestCase):
"""Check _ValidateFilePaths helper.
@@ -232,6 +265,19 @@
self.assertEqual(manifest.repo_hooks_project.name, 'repohooks')
self.assertEqual(manifest.repo_hooks_project.enabled_repo_hooks, ['a', 'b'])
+ def test_repo_hooks_unordered(self):
+ """Check repo-hooks settings work even if the project def comes second."""
+ manifest = self.getXmlManifest("""
+<manifest>
+ <remote name="test-remote" fetch="http://localhost" />
+ <default remote="test-remote" revision="refs/heads/main" />
+ <repo-hooks in-project="repohooks" enabled-list="a, b"/>
+ <project name="repohooks" path="src/repohooks"/>
+</manifest>
+""")
+ self.assertEqual(manifest.repo_hooks_project.name, 'repohooks')
+ self.assertEqual(manifest.repo_hooks_project.enabled_repo_hooks, ['a', 'b'])
+
def test_unknown_tags(self):
"""Check superproject settings."""
manifest = self.getXmlManifest("""
@@ -246,11 +292,30 @@
self.assertEqual(manifest.superproject['name'], 'superproject')
self.assertEqual(manifest.superproject['remote'].name, 'test-remote')
self.assertEqual(
- manifest.ToXml().toxml(),
- '<?xml version="1.0" ?><manifest>' +
- '<remote name="test-remote" fetch="http://localhost"/>' +
- '<default remote="test-remote" revision="refs/heads/main"/>' +
- '<superproject name="superproject"/>' +
+ sort_attributes(manifest.ToXml().toxml()),
+ '<?xml version="1.0" ?><manifest>'
+ '<remote fetch="http://localhost" name="test-remote"/>'
+ '<default remote="test-remote" revision="refs/heads/main"/>'
+ '<superproject name="superproject"/>'
+ '</manifest>')
+
+ def test_remote_annotations(self):
+ """Check remote settings."""
+ manifest = self.getXmlManifest("""
+<manifest>
+ <remote name="test-remote" fetch="http://localhost">
+ <annotation name="foo" value="bar"/>
+ </remote>
+</manifest>
+""")
+ self.assertEqual(manifest.remotes['test-remote'].annotations[0].name, 'foo')
+ self.assertEqual(manifest.remotes['test-remote'].annotations[0].value, 'bar')
+ self.assertEqual(
+ sort_attributes(manifest.ToXml().toxml()),
+ '<?xml version="1.0" ?><manifest>'
+ '<remote fetch="http://localhost" name="test-remote">'
+ '<annotation name="foo" value="bar"/>'
+ '</remote>'
'</manifest>')
@@ -303,6 +368,7 @@
def test_allow_bad_name_from_user(self):
"""Check handling of bad name attribute from the user's input."""
def parse(name):
+ name = self.encodeXmlAttr(name)
manifest = self.getXmlManifest(f"""
<manifest>
<remote name="default-remote" fetch="http://localhost" />
@@ -327,6 +393,7 @@
def test_bad_name_checks(self):
"""Check handling of bad name attribute."""
def parse(name):
+ name = self.encodeXmlAttr(name)
# Setup target of the include.
with open(os.path.join(self.manifest_dir, 'target.xml'), 'w') as fp:
fp.write(f'<manifest><include name="{name}"/></manifest>')
@@ -398,16 +465,18 @@
project = manifest.projects[0]
project.SetRevisionId('ABCDEF')
self.assertEqual(
- manifest.ToXml().toxml(),
- '<?xml version="1.0" ?><manifest>' +
- '<remote name="default-remote" fetch="http://localhost"/>' +
- '<default remote="default-remote" revision="refs/heads/main"/>' +
- '<project name="test-name" revision="ABCDEF"/>' +
+ sort_attributes(manifest.ToXml().toxml()),
+ '<?xml version="1.0" ?><manifest>'
+ '<remote fetch="http://localhost" name="default-remote"/>'
+ '<default remote="default-remote" revision="refs/heads/main"/>'
+ '<project name="test-name" revision="ABCDEF" upstream="refs/heads/main"/>'
'</manifest>')
def test_trailing_slash(self):
"""Check handling of trailing slashes in attributes."""
def parse(name, path):
+ name = self.encodeXmlAttr(name)
+ path = self.encodeXmlAttr(path)
return self.getXmlManifest(f"""
<manifest>
<remote name="default-remote" fetch="http://localhost" />
@@ -437,6 +506,8 @@
def test_toplevel_path(self):
"""Check handling of path=. specially."""
def parse(name, path):
+ name = self.encodeXmlAttr(name)
+ path = self.encodeXmlAttr(path)
return self.getXmlManifest(f"""
<manifest>
<remote name="default-remote" fetch="http://localhost" />
@@ -453,6 +524,8 @@
def test_bad_path_name_checks(self):
"""Check handling of bad path & name attributes."""
def parse(name, path):
+ name = self.encodeXmlAttr(name)
+ path = self.encodeXmlAttr(path)
manifest = self.getXmlManifest(f"""
<manifest>
<remote name="default-remote" fetch="http://localhost" />
@@ -499,12 +572,79 @@
self.assertEqual(manifest.superproject['name'], 'superproject')
self.assertEqual(manifest.superproject['remote'].name, 'test-remote')
self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject')
+ self.assertEqual(manifest.superproject['revision'], 'refs/heads/main')
self.assertEqual(
- manifest.ToXml().toxml(),
- '<?xml version="1.0" ?><manifest>' +
- '<remote name="test-remote" fetch="http://localhost"/>' +
- '<default remote="test-remote" revision="refs/heads/main"/>' +
- '<superproject name="superproject"/>' +
+ sort_attributes(manifest.ToXml().toxml()),
+ '<?xml version="1.0" ?><manifest>'
+ '<remote fetch="http://localhost" name="test-remote"/>'
+ '<default remote="test-remote" revision="refs/heads/main"/>'
+ '<superproject name="superproject"/>'
+ '</manifest>')
+
+ def test_superproject_revision(self):
+ """Check superproject settings with a different revision attribute"""
+ self.maxDiff = None
+ manifest = self.getXmlManifest("""
+<manifest>
+ <remote name="test-remote" fetch="http://localhost" />
+ <default remote="test-remote" revision="refs/heads/main" />
+ <superproject name="superproject" revision="refs/heads/stable" />
+</manifest>
+""")
+ self.assertEqual(manifest.superproject['name'], 'superproject')
+ self.assertEqual(manifest.superproject['remote'].name, 'test-remote')
+ self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject')
+ self.assertEqual(manifest.superproject['revision'], 'refs/heads/stable')
+ self.assertEqual(
+ sort_attributes(manifest.ToXml().toxml()),
+ '<?xml version="1.0" ?><manifest>'
+ '<remote fetch="http://localhost" name="test-remote"/>'
+ '<default remote="test-remote" revision="refs/heads/main"/>'
+ '<superproject name="superproject" revision="refs/heads/stable"/>'
+ '</manifest>')
+
+ def test_superproject_revision_default_negative(self):
+ """Check superproject settings with a same revision attribute"""
+ self.maxDiff = None
+ manifest = self.getXmlManifest("""
+<manifest>
+ <remote name="test-remote" fetch="http://localhost" />
+ <default remote="test-remote" revision="refs/heads/stable" />
+ <superproject name="superproject" revision="refs/heads/stable" />
+</manifest>
+""")
+ self.assertEqual(manifest.superproject['name'], 'superproject')
+ self.assertEqual(manifest.superproject['remote'].name, 'test-remote')
+ self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject')
+ self.assertEqual(manifest.superproject['revision'], 'refs/heads/stable')
+ self.assertEqual(
+ sort_attributes(manifest.ToXml().toxml()),
+ '<?xml version="1.0" ?><manifest>'
+ '<remote fetch="http://localhost" name="test-remote"/>'
+ '<default remote="test-remote" revision="refs/heads/stable"/>'
+ '<superproject name="superproject"/>'
+ '</manifest>')
+
+ def test_superproject_revision_remote(self):
+ """Check superproject settings with a same revision attribute"""
+ self.maxDiff = None
+ manifest = self.getXmlManifest("""
+<manifest>
+ <remote name="test-remote" fetch="http://localhost" revision="refs/heads/main" />
+ <default remote="test-remote" />
+ <superproject name="superproject" revision="refs/heads/stable" />
+</manifest>
+""")
+ self.assertEqual(manifest.superproject['name'], 'superproject')
+ self.assertEqual(manifest.superproject['remote'].name, 'test-remote')
+ self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject')
+ self.assertEqual(manifest.superproject['revision'], 'refs/heads/stable')
+ self.assertEqual(
+ sort_attributes(manifest.ToXml().toxml()),
+ '<?xml version="1.0" ?><manifest>'
+ '<remote fetch="http://localhost" name="test-remote" revision="refs/heads/main"/>'
+ '<default remote="test-remote"/>'
+ '<superproject name="superproject" revision="refs/heads/stable"/>'
'</manifest>')
def test_remote(self):
@@ -520,13 +660,14 @@
self.assertEqual(manifest.superproject['name'], 'platform/superproject')
self.assertEqual(manifest.superproject['remote'].name, 'superproject-remote')
self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/platform/superproject')
+ self.assertEqual(manifest.superproject['revision'], 'refs/heads/main')
self.assertEqual(
- manifest.ToXml().toxml(),
- '<?xml version="1.0" ?><manifest>' +
- '<remote name="default-remote" fetch="http://localhost"/>' +
- '<remote name="superproject-remote" fetch="http://localhost"/>' +
- '<default remote="default-remote" revision="refs/heads/main"/>' +
- '<superproject name="platform/superproject" remote="superproject-remote"/>' +
+ sort_attributes(manifest.ToXml().toxml()),
+ '<?xml version="1.0" ?><manifest>'
+ '<remote fetch="http://localhost" name="default-remote"/>'
+ '<remote fetch="http://localhost" name="superproject-remote"/>'
+ '<default remote="default-remote" revision="refs/heads/main"/>'
+ '<superproject name="platform/superproject" remote="superproject-remote"/>'
'</manifest>')
def test_defalut_remote(self):
@@ -540,10 +681,165 @@
""")
self.assertEqual(manifest.superproject['name'], 'superproject')
self.assertEqual(manifest.superproject['remote'].name, 'default-remote')
+ self.assertEqual(manifest.superproject['revision'], 'refs/heads/main')
+ self.assertEqual(
+ sort_attributes(manifest.ToXml().toxml()),
+ '<?xml version="1.0" ?><manifest>'
+ '<remote fetch="http://localhost" name="default-remote"/>'
+ '<default remote="default-remote" revision="refs/heads/main"/>'
+ '<superproject name="superproject"/>'
+ '</manifest>')
+
+
+class ContactinfoElementTests(ManifestParseTestCase):
+ """Tests for <contactinfo>."""
+
+ def test_contactinfo(self):
+ """Check contactinfo settings."""
+ bugurl = 'http://localhost/contactinfo'
+ manifest = self.getXmlManifest(f"""
+<manifest>
+ <contactinfo bugurl="{bugurl}"/>
+</manifest>
+""")
+ self.assertEqual(manifest.contactinfo.bugurl, bugurl)
self.assertEqual(
manifest.ToXml().toxml(),
- '<?xml version="1.0" ?><manifest>' +
- '<remote name="default-remote" fetch="http://localhost"/>' +
- '<default remote="default-remote" revision="refs/heads/main"/>' +
- '<superproject name="superproject"/>' +
+ '<?xml version="1.0" ?><manifest>'
+ f'<contactinfo bugurl="{bugurl}"/>'
'</manifest>')
+
+
+class DefaultElementTests(ManifestParseTestCase):
+ """Tests for <default>."""
+
+ def test_default(self):
+ """Check default settings."""
+ a = manifest_xml._Default()
+ a.revisionExpr = 'foo'
+ a.remote = manifest_xml._XmlRemote(name='remote')
+ b = manifest_xml._Default()
+ b.revisionExpr = 'bar'
+ self.assertEqual(a, a)
+ self.assertNotEqual(a, b)
+ self.assertNotEqual(b, a.remote)
+ self.assertNotEqual(a, 123)
+ self.assertNotEqual(a, None)
+
+
+class RemoteElementTests(ManifestParseTestCase):
+ """Tests for <remote>."""
+
+ def test_remote(self):
+ """Check remote settings."""
+ a = manifest_xml._XmlRemote(name='foo')
+ a.AddAnnotation('key1', 'value1', 'true')
+ b = manifest_xml._XmlRemote(name='foo')
+ b.AddAnnotation('key2', 'value1', 'true')
+ c = manifest_xml._XmlRemote(name='foo')
+ c.AddAnnotation('key1', 'value2', 'true')
+ d = manifest_xml._XmlRemote(name='foo')
+ d.AddAnnotation('key1', 'value1', 'false')
+ self.assertEqual(a, a)
+ self.assertNotEqual(a, b)
+ self.assertNotEqual(a, c)
+ self.assertNotEqual(a, d)
+ self.assertNotEqual(a, manifest_xml._Default())
+ self.assertNotEqual(a, 123)
+ self.assertNotEqual(a, None)
+
+
+class RemoveProjectElementTests(ManifestParseTestCase):
+ """Tests for <remove-project>."""
+
+ def test_remove_one_project(self):
+ manifest = self.getXmlManifest("""
+<manifest>
+ <remote name="default-remote" fetch="http://localhost" />
+ <default remote="default-remote" revision="refs/heads/main" />
+ <project name="myproject" />
+ <remove-project name="myproject" />
+</manifest>
+""")
+ self.assertEqual(manifest.projects, [])
+
+ def test_remove_one_project_one_remains(self):
+ manifest = self.getXmlManifest("""
+<manifest>
+ <remote name="default-remote" fetch="http://localhost" />
+ <default remote="default-remote" revision="refs/heads/main" />
+ <project name="myproject" />
+ <project name="yourproject" />
+ <remove-project name="myproject" />
+</manifest>
+""")
+
+ self.assertEqual(len(manifest.projects), 1)
+ self.assertEqual(manifest.projects[0].name, 'yourproject')
+
+ def test_remove_one_project_doesnt_exist(self):
+ with self.assertRaises(manifest_xml.ManifestParseError):
+ manifest = self.getXmlManifest("""
+<manifest>
+ <remote name="default-remote" fetch="http://localhost" />
+ <default remote="default-remote" revision="refs/heads/main" />
+ <remove-project name="myproject" />
+</manifest>
+""")
+ manifest.projects
+
+ def test_remove_one_optional_project_doesnt_exist(self):
+ manifest = self.getXmlManifest("""
+<manifest>
+ <remote name="default-remote" fetch="http://localhost" />
+ <default remote="default-remote" revision="refs/heads/main" />
+ <remove-project name="myproject" optional="true" />
+</manifest>
+""")
+ self.assertEqual(manifest.projects, [])
+
+
+class ExtendProjectElementTests(ManifestParseTestCase):
+ """Tests for <extend-project>."""
+
+ def test_extend_project_dest_path_single_match(self):
+ manifest = self.getXmlManifest("""
+<manifest>
+ <remote name="default-remote" fetch="http://localhost" />
+ <default remote="default-remote" revision="refs/heads/main" />
+ <project name="myproject" />
+ <extend-project name="myproject" dest-path="bar" />
+</manifest>
+""")
+ self.assertEqual(len(manifest.projects), 1)
+ self.assertEqual(manifest.projects[0].relpath, 'bar')
+
+ def test_extend_project_dest_path_multi_match(self):
+ with self.assertRaises(manifest_xml.ManifestParseError):
+ manifest = self.getXmlManifest("""
+<manifest>
+ <remote name="default-remote" fetch="http://localhost" />
+ <default remote="default-remote" revision="refs/heads/main" />
+ <project name="myproject" path="x" />
+ <project name="myproject" path="y" />
+ <extend-project name="myproject" dest-path="bar" />
+</manifest>
+""")
+ manifest.projects
+
+ def test_extend_project_dest_path_multi_match_path_specified(self):
+ manifest = self.getXmlManifest("""
+<manifest>
+ <remote name="default-remote" fetch="http://localhost" />
+ <default remote="default-remote" revision="refs/heads/main" />
+ <project name="myproject" path="x" />
+ <project name="myproject" path="y" />
+ <extend-project name="myproject" path="x" dest-path="bar" />
+</manifest>
+""")
+ self.assertEqual(len(manifest.projects), 2)
+ if manifest.projects[0].relpath == 'y':
+ self.assertEqual(manifest.projects[1].relpath, 'bar')
+ else:
+ self.assertEqual(manifest.projects[0].relpath, 'bar')
+ self.assertEqual(manifest.projects[1].relpath, 'y')
diff --git a/tests/test_platform_utils.py b/tests/test_platform_utils.py
new file mode 100644
index 0000000..55b7805
--- /dev/null
+++ b/tests/test_platform_utils.py
@@ -0,0 +1,50 @@
+# Copyright 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# 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 platform_utils.py module."""
+
+import os
+import tempfile
+import unittest
+
+import platform_utils
+
+
+class RemoveTests(unittest.TestCase):
+ """Check remove() helper."""
+
+ def testMissingOk(self):
+ """Check missing_ok handling."""
+ with tempfile.TemporaryDirectory() as tmpdir:
+ path = os.path.join(tmpdir, 'test')
+
+ # Should not fail.
+ platform_utils.remove(path, missing_ok=True)
+
+ # Should fail.
+ self.assertRaises(OSError, platform_utils.remove, path)
+ self.assertRaises(OSError, platform_utils.remove, path, missing_ok=False)
+
+ # Should not fail if it exists.
+ open(path, 'w').close()
+ platform_utils.remove(path, missing_ok=True)
+ self.assertFalse(os.path.exists(path))
+
+ open(path, 'w').close()
+ platform_utils.remove(path)
+ self.assertFalse(os.path.exists(path))
+
+ open(path, 'w').close()
+ platform_utils.remove(path, missing_ok=False)
+ self.assertFalse(os.path.exists(path))
diff --git a/tests/test_ssh.py b/tests/test_ssh.py
new file mode 100644
index 0000000..ffb5cb9
--- /dev/null
+++ b/tests/test_ssh.py
@@ -0,0 +1,74 @@
+# Copyright 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# 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 ssh.py module."""
+
+import multiprocessing
+import subprocess
+import unittest
+from unittest import mock
+
+import ssh
+
+
+class SshTests(unittest.TestCase):
+ """Tests the ssh functions."""
+
+ def test_parse_ssh_version(self):
+ """Check _parse_ssh_version() handling."""
+ ver = ssh._parse_ssh_version('Unknown\n')
+ self.assertEqual(ver, ())
+ ver = ssh._parse_ssh_version('OpenSSH_1.0\n')
+ self.assertEqual(ver, (1, 0))
+ ver = ssh._parse_ssh_version('OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.13, OpenSSL 1.0.1f 6 Jan 2014\n')
+ self.assertEqual(ver, (6, 6, 1))
+ ver = ssh._parse_ssh_version('OpenSSH_7.6p1 Ubuntu-4ubuntu0.3, OpenSSL 1.0.2n 7 Dec 2017\n')
+ self.assertEqual(ver, (7, 6))
+
+ def test_version(self):
+ """Check version() handling."""
+ with mock.patch('ssh._run_ssh_version', return_value='OpenSSH_1.2\n'):
+ self.assertEqual(ssh.version(), (1, 2))
+
+ def test_context_manager_empty(self):
+ """Verify context manager with no clients works correctly."""
+ with multiprocessing.Manager() as manager:
+ with ssh.ProxyManager(manager):
+ pass
+
+ def test_context_manager_child_cleanup(self):
+ """Verify orphaned clients & masters get cleaned up."""
+ with multiprocessing.Manager() as manager:
+ with ssh.ProxyManager(manager) as ssh_proxy:
+ client = subprocess.Popen(['sleep', '964853320'])
+ ssh_proxy.add_client(client)
+ master = subprocess.Popen(['sleep', '964853321'])
+ ssh_proxy.add_master(master)
+ # If the process still exists, these will throw timeout errors.
+ client.wait(0)
+ master.wait(0)
+
+ def test_ssh_sock(self):
+ """Check sock() function."""
+ manager = multiprocessing.Manager()
+ proxy = ssh.ProxyManager(manager)
+ with mock.patch('tempfile.mkdtemp', return_value='/tmp/foo'):
+ # old ssh version uses port
+ with mock.patch('ssh.version', return_value=(6, 6)):
+ self.assertTrue(proxy.sock().endswith('%p'))
+
+ proxy._sock_path = None
+ # new ssh version uses hash
+ with mock.patch('ssh.version', return_value=(6, 7)):
+ self.assertTrue(proxy.sock().endswith('%C'))
diff --git a/tests/test_subcmds.py b/tests/test_subcmds.py
index 2234e64..bc53051 100644
--- a/tests/test_subcmds.py
+++ b/tests/test_subcmds.py
@@ -14,6 +14,7 @@
"""Unittests for the subcmds module (mostly __init__.py than subcommands)."""
+import optparse
import unittest
import subcmds
@@ -41,3 +42,32 @@
# Reject internal python paths like "__init__".
self.assertFalse(cmd.startswith('__'))
+
+ def test_help_desc_style(self):
+ """Force some consistency in option descriptions.
+
+ Python's optparse & argparse has a few default options like --help. Their
+ option description text uses lowercase sentence fragments, so enforce our
+ options follow the same style so UI is consistent.
+
+ We enforce:
+ * Text starts with lowercase.
+ * Text doesn't end with period.
+ """
+ for name, cls in subcmds.all_commands.items():
+ cmd = cls()
+ parser = cmd.OptionParser
+ for option in parser.option_list:
+ if option.help == optparse.SUPPRESS_HELP:
+ continue
+
+ c = option.help[0]
+ self.assertEqual(
+ c.lower(), c,
+ msg=f'subcmds/{name}.py: {option.get_opt_string()}: help text '
+ f'should start with lowercase: "{option.help}"')
+
+ self.assertNotEqual(
+ option.help[-1], '.',
+ msg=f'subcmds/{name}.py: {option.get_opt_string()}: help text '
+ f'should not end in a period: "{option.help}"')
diff --git a/tox.ini b/tox.ini
index 3282de1..aa4e297 100644
--- a/tox.ini
+++ b/tox.ini
@@ -15,11 +15,10 @@
# https://tox.readthedocs.io/
[tox]
-envlist = py35, py36, py37, py38, py39
+envlist = py36, py37, py38, py39
[gh-actions]
python =
- 3.5: py35
3.6: py36
3.7: py37
3.8: py38