Compare commits

...

26 Commits
v2.53 ... v2.55

Author SHA1 Message Date
Gavin Mak
08815ad3eb upload: Add rev to rootRepo push option
Bug: b/401147338
Change-Id: Iac19af5aadd250538702920d9beaeef9250c78fe
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/478801
Tested-by: Gavin Mak <gavinmak@google.com>
Reviewed-by: Scott Lee <ddoman@google.com>
2025-05-28 11:44:55 -07:00
Scott Lee
3c8bae27ec info: print superproject revision
Bug: 416589884
Change-Id: I5d1c709518d76d777a7f07c4c774569773c5a265
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/478205
Lint: Scott Lee <ddoman@google.com>
Tested-by: Scott Lee <ddoman@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Commit-Queue: Scott Lee <ddoman@google.com>
2025-05-27 11:49:32 -07:00
Mike Frysinger
06338abe79 subcmds: delete redundant dest= settings
Add a test to enforce this too.

Change-Id: I80b5cf567aa33db9c24b53428c66d69f9c1d8d74
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/478481
Commit-Queue: Mike Frysinger <vapier@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Scott Lee <ddoman@google.com>
2025-05-27 09:26:43 -07:00
Gavin Mak
8d37f61471 upload: Add superproject identifier as push option
When uploading, add the root superproject repo as a push option in the
format `-o custom-keyed-value=rootRepo:$HOST/$PROJECT`.

Bug: b/401147338
Change-Id: I00230256eb7ae307b03840bb4090c28dc8a0505e
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/472601
Reviewed-by: Josip Sokcevic <sokcevic@chromium.org>
Tested-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
2025-05-05 15:10:49 -07:00
Mike Frysinger
1acbc14c34 manifest: generalize --json as --format=<format>
This will make it easier to add more formats without exploding the
common --xxx space and checking a large set of boolean flags.

Also fill out the test coverage while we're here.

Bug: b/412725063
Change-Id: I754013dc6cb3445f8a0979cefec599d55dafdcff
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/471941
Reviewed-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Mike Frysinger <vapier@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2025-04-30 12:25:15 -07:00
Mike Frysinger
c448ba9cc7 run_tests: only allow help2man skipping in CI
Make sure we run this for local devs.

Change-Id: I472b7c347086d54649dd9d5778eea4737447b353
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/471921
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Scott Lee <ddoman@google.com>
Commit-Queue: Mike Frysinger <vapier@google.com>
2025-04-30 11:47:18 -07:00
Mike Frysinger
21cbcc54e9 update-manpages: include in unittests
People often forget to regen when making interface changes.

We skip the test if help2man isn't installed since it's not common,
and it's not available on our CI bots currently.

Change-Id: Ib4911a0e3fa1294ad90e4ac8afc047a0b7c2b66d
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/469741
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Mike Frysinger <vapier@google.com>
2025-04-28 10:24:33 -07:00
Erik Elmeke
0f200bb3a1 flake8: Ignore .venv directory
.venv is by convention a very common place for venvs and
is the default in some tools, for example like "Astral uv".
The third-party packages installed there should not be linted.

Change-Id: I3278d90c2fdfc8a34a2488e82d4df8e836111ce1
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/469941
Tested-by: Erik Elmeke <erik@haleytek.corp-partner.google.com>
Commit-Queue: Josip Sokcevic <sokcevic@chromium.org>
Reviewed-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Josip Sokcevic <sokcevic@chromium.org>
2025-04-23 08:33:37 -07:00
Mike Frysinger
c8da28c3ed man: regenerate man pages
Change-Id: Ie348f7a29523655bf1d6247af8302ff885420d75
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/469742
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Mike Frysinger <vapier@google.com>
2025-04-22 11:37:45 -07:00
Erik Elmeke
c061593a12 manifest: Remove redundant re-raise of BaseExceptions
This change should be a noop from a functional point of view.
Exceptions inheriting directly from BaseException (KeyboardInterrupt,
SystemExit) are not caught by "except Exception", they will instead
continue raising upwards the stack, so there is no need to explicitly
catch and re-raise them.

Change-Id: Ic10764af4a6c05d1162f8b21651e7864ed742286
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/469601
Reviewed-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Josip Sokcevic <sokcevic@chromium.org>
Tested-by: Erik Elmeke <erik@haleytek.corp-partner.google.com>
2025-04-22 10:20:08 -07:00
Kaushik Lingarkar
a94457d1ce Fallback to full sync when depth enabled fetch of a sha1 fails
In sha1 mode, when depth is enabled, syncing the revision from
upstream may not work because some servers only allow fetching
named refs. Fetching a specific sha1 may result in an error like
'server does not allow request for unadvertised object'. In this
case, attempt a full sync with depth disabled.

Bug: 410825502
Change-Id: If51bcf18b877cd9491706f5bc3d6fd13c0c3d4f3
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/468282
Commit-Queue: Kaushik Lingarkar <kaushikl@qti.qualcomm.com>
Tested-by: Kaushik Lingarkar <kaushikl@qti.qualcomm.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
2025-04-17 11:46:11 -07:00
Gavin Mak
97dc5c1bd9 project: use --netrc-optional instead of --netrc
Some users are reporting a "curl: (26) .netrc error: no such file"
message on sync caused by an change to curl behavior.
See https://github.com/curl/curl/issues/16163.

Use --netrc-optional which was introduced in curl version 7.9.8
released in 2002.

Bug: 409354839
Change-Id: I8365c6e806968a4ee765a7e023b4bced30489c20
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/467026
Reviewed-by: Mike Frysinger <vapier@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
Reviewed-by: Josip Sokcevic <sokcevic@chromium.org>
2025-04-10 11:30:42 -07:00
Mike Frysinger
0214730c9a launcher: switch command quoting to shlex.quote
Minor fix, but just in case, provides properly quoted commands for
people to copy & paste.

Change-Id: Ia9fce5c0df9f51cbed9d49861adcf6821251e46f
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/466821
Tested-by: Mike Frysinger <vapier@google.com>
Commit-Queue: Mike Frysinger <vapier@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
2025-04-10 10:23:08 -07:00
Gavin Mak
daebd6cbc2 sync: Warn about excessive job counts
Warn users if the effective job count specified via `-j`,
`--jobs-network`, or `--jobs-checkout` exceeds a threshold
(currently 100). This encourages users to use more reasonable
values.

Bug: 406868778
Bug: 254914814
Change-Id: I116e2bbaf3dc824c04d1b2fbe52cf9ca5be77b9a
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/466801
Reviewed-by: Mike Frysinger <vapier@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
2025-04-09 14:52:22 -07:00
Mike Frysinger
3667de1d0f run_tests: fix running when cwd is not the root
If you try running this from a subdir, then most of the tests fail
because they assume they're running from the top of the source tree.
Change all the tests to actually run there.

For example: cd docs && ../run_tests

Change-Id: I92e17476393a108e56b58e049193b9fd72c5b7ba
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/464841
Reviewed-by: Gavin Mak <gavinmak@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
Commit-Queue: Mike Frysinger <vapier@google.com>
2025-04-03 11:11:04 -07:00
Mike Frysinger
85ee1738e6 run_tests: enable Python 3.8 CI coverage
Change-Id: I507da20d3b7234e9f2a22d7654a6405b362eebaf
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/464541
Reviewed-by: Gavin Mak <gavinmak@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
Commit-Queue: Mike Frysinger <vapier@google.com>
2025-04-02 13:50:29 -07:00
Egor Duda
f070331a4c Fix EROFS error when root fs is mounted read-only
repo attempts to create /etc/.repo_gitconfig.json file, and fails if
root file system is mounted read-only. Removing non-existing file on
read-only filesystem results in EROFS instead of ENOENT.

Bug: 401018409
Change-Id: I64edc0567fb88649f3fd8cacb65a8780744640d4
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/458821
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Egor Duda <egor.duda@gmail.com>
Commit-Queue: Egor Duda <egor.duda@gmail.com>
2025-04-02 06:43:06 -07:00
Mike Frysinger
9ecb80ba26 pager: drop unused global vars
We use global when we need to write to a variable, not read it.
This function only reads, so drop the keyword.

Change-Id: Iee91998fba67fd3e8ebaf2f4a79f95032f70b1c0
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/464501
Reviewed-by: Scott Lee <ddoman@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
Commit-Queue: Mike Frysinger <vapier@google.com>
2025-04-01 20:59:10 -07:00
Mike Frysinger
dc8185f2a9 launcher: change RunError to subprocess.CalledProcessError
Since we require Python 3.6 now in the launcher, swap out our custom
RunError class for the standard subprocess one.

Change-Id: Id0ca17c40e22ece03e06366a263ad340963f979d
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/464401
Commit-Queue: Mike Frysinger <vapier@google.com>
Reviewed-by: Scott Lee <ddoman@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2025-04-01 17:28:26 -07:00
Mike Frysinger
59b81c84de launcher: change collections.namedtuple to typing.NamedTuple
Since we require Python 3.6 now in the launcher, switch to NamedTuple
so we get better documentation & typing information.

Change-Id: Ic58fdc07db02fc49166eccbbc3e527f474973424
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/463721
Reviewed-by: Gavin Mak <gavinmak@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
Commit-Queue: Mike Frysinger <vapier@google.com>
2025-03-28 19:13:49 -07:00
Mike Frysinger
507d463600 tox: sync black settings with run_tests
We updated run_tests to use black-25, so update tox too.

Change-Id: I7ee6471fbc78825bd2dbc8c1f8dab9dc10460852
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/463601
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Scott Lee <ddoman@google.com>
Commit-Queue: Mike Frysinger <vapier@google.com>
2025-03-27 14:20:01 -07:00
Mike Frysinger
cd391e77d0 black: update to v25
Requires a little reformatting in the tree.

Change-Id: Iaa40fe0dfca372c49c04cc26edccb5f7b0c2a8ad
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/462883
Commit-Queue: Mike Frysinger <vapier@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2025-03-25 11:20:35 -07:00
Mike Frysinger
8310436be0 run_tests: move test filtering to pytest markers
Move the test disable logic even closer to the exact test that's
disabled.  This way people updating tests have a better chance of
seeing they'll get reduced coverage in the CQ.

Change-Id: I57c1a073a844019798b27e14d742fd32925d9ae8
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/462882
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Mike Frysinger <vapier@google.com>
2025-03-25 11:19:49 -07:00
Mike Frysinger
d5087392ed run_tests: move CQ test skips here
Our recipes have been disabling a bunch of tests.  To increase
visibility, and to make it easier to test changes, move that
logic to this script.

Change-Id: I3894f047715177c0f1d27a2fe4c3490972dab204
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/462881
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
2025-03-25 10:08:54 -07:00
Mike Frysinger
91f428058d run_tests: run all tests all the time
Using a generator w/all() causes the code to exit on the first error.
We really want to see all errors all the time, so use sum() instead.

Change-Id: Ib1adb8de199db9fe727d4b49c890b4d5061e9e6b
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/462901
Tested-by: Mike Frysinger <vapier@google.com>
Commit-Queue: Mike Frysinger <vapier@google.com>
Reviewed-by: Scott Lee <ddoman@google.com>
2025-03-25 10:07:42 -07:00
Mike Frysinger
243df2042e launcher: change RunResult to subprocess.CompletedProcess
Since we require Python 3.6 now in the launcher, swap out our custom
RunResult class for the standard subprocess one.

Change-Id: Idd8598df37c0a952d3ef828df6e250cab03c6589
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/462341
Reviewed-by: Gavin Mak <gavinmak@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
Commit-Queue: Mike Frysinger <vapier@google.com>
2025-03-24 11:49:00 -07:00
41 changed files with 608 additions and 178 deletions

View File

@@ -12,5 +12,6 @@ extend-ignore =
# E731: do not assign a lambda expression, use a def
E731,
exclude =
.venv,
venv,
.tox,

View File

@@ -1 +1,2 @@
black<24
# NB: Keep in sync with run_tests.vpython3.
black<26

View File

@@ -238,9 +238,9 @@ def _build_env(
s = p + " " + s
env["GIT_CONFIG_PARAMETERS"] = s
if "GIT_ALLOW_PROTOCOL" not in env:
env[
"GIT_ALLOW_PROTOCOL"
] = "file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc"
env["GIT_ALLOW_PROTOCOL"] = (
"file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc"
)
env["GIT_HTTP_USER_AGENT"] = user_agent.git
if objdir:
@@ -350,9 +350,9 @@ class GitCommand:
"Project": e.project,
"CommandName": command_name,
"Message": str(e),
"ReturnCode": str(e.git_rc)
if e.git_rc is not None
else None,
"ReturnCode": (
str(e.git_rc) if e.git_rc is not None else None
),
"IsError": log_as_error,
}
)

View File

@@ -28,6 +28,7 @@ import os
import sys
import time
from typing import NamedTuple
import urllib.parse
from git_command import git_require
from git_command import GitCommand
@@ -128,6 +129,29 @@ class Superproject:
"""Set the _print_messages attribute."""
self._print_messages = value
@property
def commit_id(self):
"""Returns the commit ID of the superproject checkout."""
cmd = ["rev-parse", self.revision]
p = GitCommand(
None, # project
cmd,
gitdir=self._work_git,
bare=True,
capture_stdout=True,
capture_stderr=True,
)
retval = p.Wait()
if retval != 0:
self._LogWarning(
"git rev-parse call failed, command: git {}, "
"return code: {}, stderr: {}",
cmd,
p.stdwerr,
)
return None
return p.stdout
@property
def project_commit_ids(self):
"""Returns a dictionary of projects and their commit ids."""
@@ -140,6 +164,24 @@ class Superproject:
self._manifest_path if os.path.exists(self._manifest_path) else None
)
@property
def repo_id(self):
"""Returns the repo ID for the superproject.
For example, if the superproject points to:
https://android-review.googlesource.com/platform/superproject/
Then the repo_id would be:
android/platform/superproject
"""
if review_url := self.remote.review:
parsed_url = urllib.parse.urlparse(review_url)
if netloc := parsed_url.netloc:
parts = netloc.split("-review", 1)
host = parts[0]
rev = GitRefs(self._work_git).get("HEAD")
return f"{host}/{self.name}@{rev}"
return None
def _LogMessage(self, fmt, *inputs):
"""Logs message to stderr and _git_event_log."""
message = f"{self._LogMessagePrefix()} {fmt.format(*inputs)}"
@@ -258,7 +300,7 @@ class Superproject:
Works only in git repositories.
Returns:
data: data returned from 'git ls-tree ...' instead of None.
data: data returned from 'git ls-tree ...'. None on error.
"""
if not os.path.exists(self._work_git):
self._LogWarning(
@@ -288,6 +330,7 @@ class Superproject:
retval,
p.stderr,
)
return None
return data
def Sync(self, git_event_log):

View File

@@ -1,5 +1,5 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
.TH REPO "1" "December 2024" "repo gc" "Repo Manual"
.TH REPO "1" "April 2025" "repo gc" "Repo Manual"
.SH NAME
repo \- repo gc - manual page for repo gc
.SH SYNOPSIS
@@ -8,7 +8,7 @@ repo \- repo gc - manual page for repo gc
.SH DESCRIPTION
Summary
.PP
Cleaning up internal repo state.
Cleaning up internal repo and Git state.
.SH OPTIONS
.TP
\fB\-h\fR, \fB\-\-help\fR
@@ -19,6 +19,10 @@ do everything except actually delete
.TP
\fB\-y\fR, \fB\-\-yes\fR
answer yes to all safe prompts
.TP
\fB\-\-repack\fR
repack all projects that use partial clone with
filter=blob:none
.SS Logging options:
.TP
\fB\-v\fR, \fB\-\-verbose\fR

View File

@@ -1,5 +1,5 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
.TH REPO "1" "December 2024" "repo manifest" "Repo Manual"
.TH REPO "1" "April 2025" "repo manifest" "Repo Manual"
.SH NAME
repo \- repo manifest - manual page for repo manifest
.SH SYNOPSIS
@@ -30,8 +30,8 @@ if in \fB\-r\fR mode, do not write the dest\-branch field
(only of use if the branch names for a sha1 manifest
are sensitive)
.TP
\fB\-\-json\fR
output manifest in JSON format (experimental)
\fB\-\-format\fR=\fI\,FORMAT\/\fR
output format: xml, json (default: xml)
.TP
\fB\-\-pretty\fR
format output for humans to read
@@ -78,6 +78,10 @@ set to the ref we were on when the manifest was generated. The 'dest\-branch'
attribute is set to indicate the remote ref to push changes to via 'repo
upload'.
.PP
Multiple output formats are supported via \fB\-\-format\fR. The default output is XML,
and formats are generally "condensed". Use \fB\-\-pretty\fR for more human\-readable
variations.
.PP
repo Manifest Format
.PP
A repo manifest describes the structure of a repo client; that is the
@@ -306,25 +310,7 @@ Element manifest\-server
At most one manifest\-server may be specified. The url attribute is used to
specify the URL of a manifest server, which is an XML RPC service.
.PP
The manifest server should implement the following RPC methods:
.IP
GetApprovedManifest(branch, target)
.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.
See the [smart sync documentation](./smart\-sync.md) for more details.
.PP
Element submanifest
.PP

View File

@@ -1,5 +1,5 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
.TH REPO "1" "December 2024" "repo" "Repo Manual"
.TH REPO "1" "April 2025" "repo" "Repo Manual"
.SH NAME
repo \- repository management tool built on top of git
.SH SYNOPSIS
@@ -80,7 +80,7 @@ forall
Run a shell command in each project
.TP
gc
Cleaning up internal repo state.
Cleaning up internal repo and Git state.
.TP
grep
Print lines matching a pattern

View File

@@ -1014,9 +1014,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
def SetManifestOverride(self, path):
"""Override manifestFile. The caller must call Unload()"""
self._outer_client.manifest.manifestFileOverrides[
self.path_prefix
] = path
self._outer_client.manifest.manifestFileOverrides[self.path_prefix] = (
path
)
@property
def UseLocalManifests(self):
@@ -1328,12 +1328,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
)
# should isolate this to the exact exception, but that's
# tricky. actual parsing implementation may vary.
except (
KeyboardInterrupt,
RuntimeError,
SystemExit,
ManifestParseError,
):
except (RuntimeError, ManifestParseError):
raise
except Exception as e:
raise ManifestParseError(
@@ -2112,22 +2107,22 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
# implementation:
# https://eclipse.googlesource.com/jgit/jgit/+/9110037e3e9461ff4dac22fee84ef3694ed57648/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java#884
BAD_CODEPOINTS = {
"\u200C", # ZERO WIDTH NON-JOINER
"\u200D", # ZERO WIDTH JOINER
"\u200E", # LEFT-TO-RIGHT MARK
"\u200F", # RIGHT-TO-LEFT MARK
"\u202A", # LEFT-TO-RIGHT EMBEDDING
"\u202B", # RIGHT-TO-LEFT EMBEDDING
"\u202C", # POP DIRECTIONAL FORMATTING
"\u202D", # LEFT-TO-RIGHT OVERRIDE
"\u202E", # RIGHT-TO-LEFT OVERRIDE
"\u206A", # INHIBIT SYMMETRIC SWAPPING
"\u206B", # ACTIVATE SYMMETRIC SWAPPING
"\u206C", # INHIBIT ARABIC FORM SHAPING
"\u206D", # ACTIVATE ARABIC FORM SHAPING
"\u206E", # NATIONAL DIGIT SHAPES
"\u206F", # NOMINAL DIGIT SHAPES
"\uFEFF", # ZERO WIDTH NO-BREAK SPACE
"\u200c", # ZERO WIDTH NON-JOINER
"\u200d", # ZERO WIDTH JOINER
"\u200e", # LEFT-TO-RIGHT MARK
"\u200f", # RIGHT-TO-LEFT MARK
"\u202a", # LEFT-TO-RIGHT EMBEDDING
"\u202b", # RIGHT-TO-LEFT EMBEDDING
"\u202c", # POP DIRECTIONAL FORMATTING
"\u202d", # LEFT-TO-RIGHT OVERRIDE
"\u202e", # RIGHT-TO-LEFT OVERRIDE
"\u206a", # INHIBIT SYMMETRIC SWAPPING
"\u206b", # ACTIVATE SYMMETRIC SWAPPING
"\u206c", # INHIBIT ARABIC FORM SHAPING
"\u206d", # ACTIVATE ARABIC FORM SHAPING
"\u206e", # NATIONAL DIGIT SHAPES
"\u206f", # NOMINAL DIGIT SHAPES
"\ufeff", # ZERO WIDTH NO-BREAK SPACE
}
if BAD_CODEPOINTS & path_codepoints:
# This message is more expansive than reality, but should be fine.

View File

@@ -40,7 +40,7 @@ def RunPager(globalConfig):
def TerminatePager():
global pager_process, old_stdout, old_stderr
global pager_process
if pager_process:
sys.stdout.flush()
sys.stderr.flush()

View File

@@ -156,6 +156,12 @@ def remove(path, missing_ok=False):
os.rmdir(longpath)
else:
os.remove(longpath)
elif (
e.errno == errno.EROFS
and missing_ok
and not os.path.exists(longpath)
):
pass
elif missing_ok and e.errno == errno.ENOENT:
pass
else:

View File

@@ -2755,6 +2755,14 @@ class Project:
# field; it doesn't exist, thus abort the optimization attempt
# and do a full sync.
break
elif depth and is_sha1 and ret == 1:
# In sha1 mode, when depth is enabled, syncing the revision
# from upstream may not work because some servers only allow
# fetching named refs. Fetching a specific sha1 may result
# in an error like 'server does not allow request for
# unadvertised object'. In this case, attempt a full sync
# without depth.
break
elif ret < 0:
# Git died with a signal, exit immediately.
break
@@ -2875,7 +2883,14 @@ class Project:
# We do not use curl's --retry option since it generally doesn't
# actually retry anything; code 18 for example, it will not retry on.
cmd = ["curl", "--fail", "--output", tmpPath, "--netrc", "--location"]
cmd = [
"curl",
"--fail",
"--output",
tmpPath,
"--netrc-optional",
"--location",
]
if quiet:
cmd += ["--silent", "--show-error"]
if os.path.exists(tmpPath):

View File

@@ -16,3 +16,8 @@
line-length = 80
# NB: Keep in sync with tox.ini.
target-version = ['py36', 'py37', 'py38', 'py39', 'py310', 'py311'] #, 'py312'
[tool.pytest.ini_options]
markers = """
skip_cq: Skip tests in the CQ. Should be rarely used!
"""

View File

@@ -27,9 +27,11 @@ import shutil
import subprocess
import sys
import tempfile
from typing import List
TOPDIR = Path(__file__).resolve().parent.parent
THIS_FILE = Path(__file__).resolve()
TOPDIR = THIS_FILE.parent.parent
MANDIR = TOPDIR.joinpath("man")
# Load repo local modules.
@@ -42,9 +44,23 @@ def worker(cmd, **kwargs):
subprocess.run(cmd, **kwargs)
def main(argv):
def get_parser() -> argparse.ArgumentParser:
"""Get argument parser."""
parser = argparse.ArgumentParser(description=__doc__)
parser.parse_args(argv)
parser.add_argument(
"-n",
"--check",
"--dry-run",
action="store_const",
const=True,
help="Check if changes are necessary; don't actually change files",
)
return parser
def main(argv: List[str]) -> int:
parser = get_parser()
opts = parser.parse_args(argv)
if not shutil.which("help2man"):
sys.exit("Please install help2man to continue.")
@@ -117,6 +133,7 @@ def main(argv):
functools.partial(worker, cwd=tempdir, check=True), cmdlist
)
ret = 0
for tmp_path in MANDIR.glob("*.1.tmp"):
path = tmp_path.parent / tmp_path.stem
old_data = path.read_text() if path.exists() else ""
@@ -133,7 +150,17 @@ def main(argv):
)
new_data = re.sub(r'^(\.TH REPO "1" ")([^"]+)', r"\1", data, flags=re.M)
if old_data != new_data:
path.write_text(data)
if opts.check:
ret = 1
print(
f"{THIS_FILE.name}: {path.name}: "
"man page needs regenerating",
file=sys.stderr,
)
else:
path.write_text(data)
return ret
def replace_regex(data):

View File

@@ -16,6 +16,7 @@
import os
import re
import shlex
import subprocess
import sys
@@ -35,12 +36,7 @@ KEYID_ECC = "E1F9040D7A3F6DAFAC897CD3D3B95DA243E48A39"
def cmdstr(cmd):
"""Get a nicely quoted shell command."""
ret = []
for arg in cmd:
if not re.match(r"^[a-zA-Z0-9/_.=-]+$", arg):
arg = f'"{arg}"'
ret.append(arg)
return " ".join(ret)
return " ".join(shlex.quote(x) for x in cmd)
def run(opts, cmd, check=True, **kwargs):

53
repo
View File

@@ -27,6 +27,7 @@ import platform
import shlex
import subprocess
import sys
from typing import NamedTuple
# These should never be newer than the main.py version since this needs to be a
@@ -56,9 +57,14 @@ class Trace:
trace = Trace()
def cmdstr(cmd):
"""Get a nicely quoted shell command."""
return " ".join(shlex.quote(x) for x in cmd)
def exec_command(cmd):
"""Execute |cmd| or return None on failure."""
trace.print(":", " ".join(cmd))
trace.print(":", cmdstr(cmd))
try:
if platform.system() == "Windows":
ret = subprocess.call(cmd)
@@ -124,7 +130,7 @@ if not REPO_REV:
BUG_URL = "https://issues.gerritcodereview.com/issues/new?component=1370071"
# increment this whenever we make important changes to this script
VERSION = (2, 50)
VERSION = (2, 54)
# increment this if the MAINTAINER_KEYS block is modified
KEYRING_VERSION = (2, 3)
@@ -217,7 +223,6 @@ S_manifests = "manifests" # special manifest repository
REPO_MAIN = S_repo + "/main.py" # main script
import collections
import errno
import json
import optparse
@@ -482,16 +487,6 @@ def InitParser(parser):
return parser
# This is a poor replacement for subprocess.run until we require Python 3.6+.
RunResult = collections.namedtuple(
"RunResult", ("returncode", "stdout", "stderr")
)
class RunError(Exception):
"""Error when running a command failed."""
def run_command(cmd, **kwargs):
"""Run |cmd| and return its output."""
check = kwargs.pop("check", False)
@@ -516,7 +511,7 @@ def run_command(cmd, **kwargs):
# Run & package the results.
proc = subprocess.Popen(cmd, **kwargs)
(stdout, stderr) = proc.communicate(input=cmd_input)
dbg = ": " + " ".join(cmd)
dbg = ": " + cmdstr(cmd)
if cmd_input is not None:
dbg += " 0<|"
if stdout == subprocess.PIPE:
@@ -526,7 +521,9 @@ def run_command(cmd, **kwargs):
elif stderr == subprocess.STDOUT:
dbg += " 2>&1"
trace.print(dbg)
ret = RunResult(proc.returncode, decode(stdout), decode(stderr))
ret = subprocess.CompletedProcess(
cmd, proc.returncode, decode(stdout), decode(stderr)
)
# If things failed, print useful debugging output.
if check and ret.returncode:
@@ -547,13 +544,13 @@ def run_command(cmd, **kwargs):
_print_output("stdout", ret.stdout)
_print_output("stderr", ret.stderr)
raise RunError(ret)
# This will raise subprocess.CalledProcessError for us.
ret.check_returncode()
return ret
class CloneFailure(Exception):
"""Indicate the remote clone of repo itself failed."""
@@ -672,15 +669,20 @@ def run_git(*args, **kwargs):
file=sys.stderr,
)
sys.exit(1)
except RunError:
except subprocess.CalledProcessError:
raise CloneFailure()
# The git version info broken down into components for easy analysis.
# Similar to Python's sys.version_info.
GitVersion = collections.namedtuple(
"GitVersion", ("major", "minor", "micro", "full")
)
class GitVersion(NamedTuple):
"""The git version info broken down into components for easy analysis.
Similar to Python's sys.version_info.
"""
major: int
minor: int
micro: int
full: int
def ParseGitVersion(ver_str=None):
@@ -846,10 +848,11 @@ def _GetRepoConfig(name):
return None
else:
print(
f"repo: error: git {' '.join(cmd)} failed:\n{ret.stderr}",
f"repo: error: git {cmdstr(cmd)} failed:\n{ret.stderr}",
file=sys.stderr,
)
raise RunError()
# This will raise subprocess.CalledProcessError for us.
ret.check_returncode()
def _InitHttp():

View File

@@ -15,16 +15,58 @@
"""Wrapper to run linters and pytest with the right settings."""
import functools
import os
import shutil
import subprocess
import sys
import pytest
from typing import List
ROOT_DIR = os.path.dirname(os.path.realpath(__file__))
@functools.lru_cache()
def is_ci() -> bool:
"""Whether we're running in our CI system."""
return os.getenv("LUCI_CQ") == "yes"
def run_pytest(argv: List[str]) -> int:
"""Returns the exit code from pytest."""
if is_ci():
argv = ["-m", "not skip_cq"] + argv
return subprocess.run(
[sys.executable, "-m", "pytest"] + argv,
check=False,
cwd=ROOT_DIR,
).returncode
def run_pytest_py38(argv: List[str]) -> int:
"""Returns the exit code from pytest under Python 3.8."""
if is_ci():
argv = ["-m", "not skip_cq"] + argv
try:
return subprocess.run(
[
"vpython3",
"-vpython-spec",
"run_tests.vpython3.8",
"-m",
"pytest",
]
+ argv,
check=False,
cwd=ROOT_DIR,
).returncode
except FileNotFoundError:
# Skip if the user doesn't have vpython from depot_tools.
return 0
def run_black():
"""Returns the exit code from black."""
# Black by default only matches .py files. We have to list standalone
@@ -38,32 +80,55 @@ def run_black():
return subprocess.run(
[sys.executable, "-m", "black", "--check", ROOT_DIR] + extra_programs,
check=False,
cwd=ROOT_DIR,
).returncode
def run_flake8():
"""Returns the exit code from flake8."""
return subprocess.run(
[sys.executable, "-m", "flake8", ROOT_DIR], check=False
[sys.executable, "-m", "flake8", ROOT_DIR],
check=False,
cwd=ROOT_DIR,
).returncode
def run_isort():
"""Returns the exit code from isort."""
return subprocess.run(
[sys.executable, "-m", "isort", "--check", ROOT_DIR], check=False
[sys.executable, "-m", "isort", "--check", ROOT_DIR],
check=False,
cwd=ROOT_DIR,
).returncode
def run_update_manpages() -> int:
"""Returns the exit code from release/update-manpages."""
# Allow this to fail on CI, but not local devs.
if is_ci() and not shutil.which("help2man"):
print("update-manpages: help2man not found; skipping test")
return 0
return subprocess.run(
[sys.executable, "release/update-manpages", "--check"],
check=False,
cwd=ROOT_DIR,
).returncode
def main(argv):
"""The main entry."""
checks = (
lambda: pytest.main(argv),
functools.partial(run_pytest, argv),
functools.partial(run_pytest_py38, argv),
run_black,
run_flake8,
run_isort,
run_update_manpages,
)
return 0 if all(not c() for c in checks) else 1
# Run all the tests all the time to get full feedback. Don't exit on the
# first error as that makes it more difficult to iterate in the CQ.
return 1 if sum(c() for c in checks) else 0
if __name__ == "__main__":

View File

@@ -54,42 +54,43 @@ wheel: <
version: "version:21.4.0"
>
# NB: Keep in sync with constraints.txt.
wheel: <
name: "infra/python/wheels/black-py3"
version: "version:23.1.0"
version: "version:25.1.0"
>
# Required by black==23.1.0
# Required by black==25.1.0
wheel: <
name: "infra/python/wheels/mypy-extensions-py3"
version: "version:0.4.3"
>
# Required by black==23.1.0
# Required by black==25.1.0
wheel: <
name: "infra/python/wheels/tomli-py3"
version: "version:2.0.1"
>
# Required by black==23.1.0
# Required by black==25.1.0
wheel: <
name: "infra/python/wheels/platformdirs-py3"
version: "version:2.5.2"
>
# Required by black==23.1.0
# Required by black==25.1.0
wheel: <
name: "infra/python/wheels/pathspec-py3"
version: "version:0.9.0"
>
# Required by black==23.1.0
# Required by black==25.1.0
wheel: <
name: "infra/python/wheels/typing-extensions-py3"
version: "version:4.3.0"
>
# Required by black==23.1.0
# Required by black==25.1.0
wheel: <
name: "infra/python/wheels/click-py3"
version: "version:8.0.3"

67
run_tests.vpython3.8 Normal file
View File

@@ -0,0 +1,67 @@
# This is a vpython "spec" file.
#
# Read more about `vpython` and how to modify this file here:
# https://chromium.googlesource.com/infra/infra/+/main/doc/users/vpython.md
# List of available wheels:
# https://chromium.googlesource.com/infra/infra/+/main/infra/tools/dockerbuild/wheels.md
python_version: "3.8"
wheel: <
name: "infra/python/wheels/pytest-py3"
version: "version:8.3.4"
>
# Required by pytest==8.3.4
wheel: <
name: "infra/python/wheels/py-py2_py3"
version: "version:1.11.0"
>
# Required by pytest==8.3.4
wheel: <
name: "infra/python/wheels/iniconfig-py3"
version: "version:1.1.1"
>
# Required by pytest==8.3.4
wheel: <
name: "infra/python/wheels/packaging-py3"
version: "version:23.0"
>
# Required by pytest==8.3.4
wheel: <
name: "infra/python/wheels/pluggy-py3"
version: "version:1.5.0"
>
# Required by pytest==8.3.4
wheel: <
name: "infra/python/wheels/toml-py3"
version: "version:0.10.1"
>
# Required by pytest==8.3.4
wheel: <
name: "infra/python/wheels/tomli-py3"
version: "version:2.1.0"
>
# Required by pytest==8.3.4
wheel: <
name: "infra/python/wheels/pyparsing-py3"
version: "version:3.0.7"
>
# Required by pytest==8.3.4
wheel: <
name: "infra/python/wheels/attrs-py2_py3"
version: "version:21.4.0"
>
# Required by pytest==8.3.4
wheel: <
name: "infra/python/wheels/exceptiongroup-py3"
version: "version:1.1.2"
>

View File

@@ -48,7 +48,6 @@ It is equivalent to "git branch -D <branchname>".
def _Options(self, p):
p.add_option(
"--all",
dest="all",
action="store_true",
help="delete all branches in all projects",
)

View File

@@ -35,7 +35,6 @@ to the Unix 'patch' command.
p.add_option(
"-u",
"--absolute",
dest="absolute",
action="store_true",
help="paths are relative to the repository root",
)

View File

@@ -67,7 +67,9 @@ synced and their revisions won't be found.
def _Options(self, p):
p.add_option(
"--raw", dest="raw", action="store_true", help="display raw diff"
"--raw",
action="store_true",
help="display raw diff",
)
p.add_option(
"--no-color",
@@ -78,7 +80,6 @@ synced and their revisions won't be found.
)
p.add_option(
"--pretty-format",
dest="pretty_format",
action="store",
metavar="<FORMAT>",
help="print the log using a custom git pretty format string",
@@ -233,9 +234,9 @@ synced and their revisions won't be found.
)
self.printRevision = self.out.nofmt_printer("revision", fg="yellow")
else:
self.printProject = (
self.printAdded
) = self.printRemoved = self.printRevision = self.printText
self.printProject = self.printAdded = self.printRemoved = (
self.printRevision
) = self.printText
manifest1 = RepoClient(self.repodir)
manifest1.Override(args[0], load_local_manifests=False)

View File

@@ -60,7 +60,6 @@ If no project is specified try to use current directory as a project.
p.add_option(
"-r",
"--revert",
dest="revert",
action="store_true",
help="revert instead of checkout",
)

View File

@@ -133,7 +133,7 @@ without iterating through the remaining projects.
@staticmethod
def _cmd_option(option, _opt_str, _value, parser):
setattr(parser.values, option.dest, list(parser.rargs))
setattr(parser.values, option.dest or "command", list(parser.rargs))
while parser.rargs:
del parser.rargs[0]
@@ -141,7 +141,6 @@ without iterating through the remaining projects.
p.add_option(
"-r",
"--regex",
dest="regex",
action="store_true",
help="execute the command only on projects matching regex or "
"wildcard expression",
@@ -149,7 +148,6 @@ without iterating through the remaining projects.
p.add_option(
"-i",
"--inverse-regex",
dest="inverse_regex",
action="store_true",
help="execute the command only on projects not matching regex or "
"wildcard expression",
@@ -157,7 +155,6 @@ without iterating through the remaining projects.
p.add_option(
"-g",
"--groups",
dest="groups",
help="execute the command only on projects matching the specified "
"groups",
)
@@ -165,14 +162,12 @@ without iterating through the remaining projects.
"-c",
"--command",
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",
)

View File

@@ -120,7 +120,6 @@ contain a line that matches both expressions:
g.add_option(
"-r",
"--revision",
dest="revision",
action="append",
metavar="TREEish",
help="Search TREEish, instead of the work tree",

View File

@@ -43,14 +43,12 @@ class Info(PagedCommand):
p.add_option(
"-o",
"--overview",
dest="overview",
action="store_true",
help="show overview of all local commits",
)
p.add_option(
"-c",
"--current-branch",
dest="current_branch",
action="store_true",
help="consider only checked out branches",
)
@@ -104,6 +102,10 @@ class Info(PagedCommand):
self.heading("Manifest groups: ")
self.headtext(manifestGroups)
self.out.nl()
sp = self.manifest.superproject
srev = sp.commit_id if sp and sp.commit_id else "None"
self.heading("Superproject revision: ")
self.headtext(srev)
self.printSeparator()

View File

@@ -40,7 +40,6 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
p.add_option(
"-r",
"--regex",
dest="regex",
action="store_true",
help="filter the project list based on regex or wildcard matching "
"of strings",
@@ -48,7 +47,6 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
p.add_option(
"-g",
"--groups",
dest="groups",
help="filter the project list based on the groups the project is "
"in",
)
@@ -61,21 +59,18 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
p.add_option(
"-n",
"--name-only",
dest="name_only",
action="store_true",
help="display only the name of the repository",
)
p.add_option(
"-p",
"--path-only",
dest="path_only",
action="store_true",
help="display only the path of the repository",
)
p.add_option(
"-f",
"--fullpath",
dest="fullpath",
action="store_true",
help="display the full work tree path instead of the relative path",
)

View File

@@ -12,7 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import enum
import json
import optparse
import os
import sys
@@ -23,6 +25,16 @@ from repo_logging import RepoLogger
logger = RepoLogger(__file__)
class OutputFormat(enum.Enum):
"""Type for the requested output format."""
# Canonicalized manifest in XML format.
XML = enum.auto()
# Canonicalized manifest in JSON format.
JSON = enum.auto()
class Manifest(PagedCommand):
COMMON = False
helpSummary = "Manifest inspection utility"
@@ -42,6 +54,10 @@ revisions set to the current commit hash. These are known as
In this case, the 'upstream' attribute is set to the ref we were on
when the manifest was generated. The 'dest-branch' attribute is set
to indicate the remote ref to push changes to via 'repo upload'.
Multiple output formats are supported via --format. The default output
is XML, and formats are generally "condensed". Use --pretty for more
human-readable variations.
"""
@property
@@ -86,11 +102,21 @@ to indicate the remote ref to push changes to via 'repo upload'.
"(only of use if the branch names for a sha1 manifest are "
"sensitive)",
)
# Replaced with --format=json. Kept for backwards compatibility.
# Can delete in Jun 2026 or later.
p.add_option(
"--json",
default=False,
action="store_true",
help="output manifest in JSON format (experimental)",
action="store_const",
dest="format",
const=OutputFormat.JSON.name.lower(),
help=optparse.SUPPRESS_HELP,
)
formats = tuple(x.lower() for x in OutputFormat.__members__.keys())
p.add_option(
"--format",
default=OutputFormat.XML.name.lower(),
choices=formats,
help=f"output format: {', '.join(formats)} (default: %default)",
)
p.add_option(
"--pretty",
@@ -108,7 +134,6 @@ to indicate the remote ref to push changes to via 'repo upload'.
p.add_option(
"-o",
"--output-file",
dest="output_file",
default="-",
help="file to save the manifest to. (Filename prefix for "
"multi-tree.)",
@@ -121,6 +146,8 @@ to indicate the remote ref to push changes to via 'repo upload'.
if opt.manifest_name:
self.manifest.Override(opt.manifest_name, False)
output_format = OutputFormat[opt.format.upper()]
for manifest in self.ManifestList(opt):
output_file = opt.output_file
if output_file == "-":
@@ -135,8 +162,7 @@ to indicate the remote ref to push changes to via 'repo upload'.
manifest.SetUseLocalManifests(not opt.ignore_local_manifests)
if opt.json:
logger.warning("warning: --json is experimental!")
if output_format == OutputFormat.JSON:
doc = manifest.ToDict(
peg_rev=opt.peg_rev,
peg_rev_upstream=opt.peg_rev_upstream,
@@ -152,7 +178,7 @@ to indicate the remote ref to push changes to via 'repo upload'.
"separators": (",", ": ") if opt.pretty else (",", ":"),
"sort_keys": True,
}
fd.write(json.dumps(doc, **json_settings))
fd.write(json.dumps(doc, **json_settings) + "\n")
else:
manifest.Save(
fd,

View File

@@ -37,7 +37,6 @@ are displayed.
p.add_option(
"-c",
"--current-branch",
dest="current_branch",
action="store_true",
help="consider only checked out branches",
)

View File

@@ -47,21 +47,18 @@ branch but need to incorporate new upstream changes "underneath" them.
g.add_option(
"-i",
"--interactive",
dest="interactive",
action="store_true",
help="interactive rebase (single project only)",
)
p.add_option(
"--fail-fast",
dest="fail_fast",
action="store_true",
help="stop rebasing after first error is hit",
)
p.add_option(
"-f",
"--force-rebase",
dest="force_rebase",
action="store_true",
help="pass --force-rebase to git rebase",
)
@@ -74,27 +71,23 @@ branch but need to incorporate new upstream changes "underneath" them.
)
p.add_option(
"--autosquash",
dest="autosquash",
action="store_true",
help="pass --autosquash to git rebase",
)
p.add_option(
"--whitespace",
dest="whitespace",
action="store",
metavar="WS",
help="pass --whitespace to git rebase",
)
p.add_option(
"--auto-stash",
dest="auto_stash",
action="store_true",
help="stash local modifications before starting",
)
p.add_option(
"-m",
"--onto-manifest",
dest="onto_manifest",
action="store_true",
help="rebase onto the manifest version instead of upstream "
"HEAD (this helps to make sure the local tree stays "

View File

@@ -54,7 +54,6 @@ need to be performed by an end-user.
)
g.add_option(
"--repo-upgraded",
dest="repo_upgraded",
action="store_true",
help=optparse.SUPPRESS_HELP,
)

View File

@@ -46,7 +46,6 @@ The '%prog' command stages files to prepare the next commit.
g.add_option(
"-i",
"--interactive",
dest="interactive",
action="store_true",
help="use interactive staging",
)

View File

@@ -51,7 +51,6 @@ revision specified in the manifest.
def _Options(self, p):
p.add_option(
"--all",
dest="all",
action="store_true",
help="begin branch in all projects",
)

View File

@@ -82,7 +82,6 @@ the following meanings:
p.add_option(
"-o",
"--orphans",
dest="orphans",
action="store_true",
help="include objects in working directory outside of repo "
"projects",

View File

@@ -350,6 +350,8 @@ later is required to fix a server side protocol bug.
# value later on.
PARALLEL_JOBS = 0
_JOBS_WARN_THRESHOLD = 100
def _Options(self, p, show_smart=True):
p.add_option(
"--jobs-network",
@@ -371,19 +373,16 @@ later is required to fix a server side protocol bug.
p.add_option(
"-f",
"--force-broken",
dest="force_broken",
action="store_true",
help="obsolete option (to be deleted in the future)",
)
p.add_option(
"--fail-fast",
dest="fail_fast",
action="store_true",
help="stop syncing after first error is hit",
)
p.add_option(
"--force-sync",
dest="force_sync",
action="store_true",
help="overwrite an existing git directory if it needs to "
"point to a different object directory. WARNING: this "
@@ -391,7 +390,6 @@ later is required to fix a server side protocol bug.
)
p.add_option(
"--force-checkout",
dest="force_checkout",
action="store_true",
help="force checkout even if it results in throwing away "
"uncommitted modifications. "
@@ -399,7 +397,6 @@ later is required to fix a server side protocol bug.
)
p.add_option(
"--force-remove-dirty",
dest="force_remove_dirty",
action="store_true",
help="force remove projects with uncommitted modifications if "
"projects no longer exist in the manifest. "
@@ -407,7 +404,6 @@ later is required to fix a server side protocol bug.
)
p.add_option(
"--rebase",
dest="rebase",
action="store_true",
help="rebase local commits regardless of whether they are "
"published",
@@ -415,7 +411,6 @@ later is required to fix a server side protocol bug.
p.add_option(
"-l",
"--local-only",
dest="local_only",
action="store_true",
help="only update working tree, don't fetch",
)
@@ -431,7 +426,6 @@ later is required to fix a server side protocol bug.
p.add_option(
"-n",
"--network-only",
dest="network_only",
action="store_true",
help="fetch only, don't update working tree",
)
@@ -458,7 +452,6 @@ later is required to fix a server side protocol bug.
p.add_option(
"-m",
"--manifest-name",
dest="manifest_name",
help="temporary manifest to use for this sync",
metavar="NAME.xml",
)
@@ -477,19 +470,16 @@ later is required to fix a server side protocol bug.
"-u",
"--manifest-server-username",
action="store",
dest="manifest_server_username",
help="username to authenticate with the manifest server",
)
p.add_option(
"-p",
"--manifest-server-password",
action="store",
dest="manifest_server_password",
help="password to authenticate with the manifest server",
)
p.add_option(
"--fetch-submodules",
dest="fetch_submodules",
action="store_true",
help="fetch submodules from server",
)
@@ -513,7 +503,6 @@ later is required to fix a server side protocol bug.
)
p.add_option(
"--optimized-fetch",
dest="optimized_fetch",
action="store_true",
help="only fetch projects fixed to sha1 if revision does not exist "
"locally",
@@ -552,7 +541,6 @@ later is required to fix a server side protocol bug.
p.add_option(
"-s",
"--smart-sync",
dest="smart_sync",
action="store_true",
help="smart sync using manifest from the latest known good "
"build",
@@ -560,7 +548,6 @@ later is required to fix a server side protocol bug.
p.add_option(
"-t",
"--smart-tag",
dest="smart_tag",
action="store",
help="smart sync using manifest from a known tag",
)
@@ -575,7 +562,6 @@ later is required to fix a server side protocol bug.
)
g.add_option(
"--repo-upgraded",
dest="repo_upgraded",
action="store_true",
help=optparse.SUPPRESS_HELP,
)
@@ -1728,6 +1714,24 @@ later is required to fix a server side protocol bug.
opt.jobs_network = min(opt.jobs_network, jobs_soft_limit)
opt.jobs_checkout = min(opt.jobs_checkout, jobs_soft_limit)
# Warn once if effective job counts seem excessively high.
# Prioritize --jobs, then --jobs-network, then --jobs-checkout.
job_options_to_check = (
("--jobs", opt.jobs),
("--jobs-network", opt.jobs_network),
("--jobs-checkout", opt.jobs_checkout),
)
for name, value in job_options_to_check:
if value > self._JOBS_WARN_THRESHOLD:
logger.warning(
"High job count (%d > %d) specified for %s; this may "
"lead to excessive resource usage or diminishing returns.",
value,
self._JOBS_WARN_THRESHOLD,
name,
)
break
def Execute(self, opt, args):
errors = []
try:

View File

@@ -267,7 +267,6 @@ Gerrit Code Review: https://www.gerritcodereview.com/
"--cc",
type="string",
action="append",
dest="cc",
help="also send email to these email addresses",
)
p.add_option(
@@ -281,7 +280,6 @@ Gerrit Code Review: https://www.gerritcodereview.com/
p.add_option(
"-c",
"--current-branch",
dest="current_branch",
action="store_true",
help="upload current git branch",
)
@@ -310,7 +308,6 @@ Gerrit Code Review: https://www.gerritcodereview.com/
"-p",
"--private",
action="store_true",
dest="private",
default=False,
help="upload as a private change (deprecated; use --wip)",
)
@@ -318,7 +315,6 @@ Gerrit Code Review: https://www.gerritcodereview.com/
"-w",
"--wip",
action="store_true",
dest="wip",
default=False,
help="upload as a work-in-progress change",
)
@@ -628,6 +624,13 @@ Gerrit Code Review: https://www.gerritcodereview.com/
branch.uploaded = False
return
# If using superproject, add the root repo as a push option.
manifest = branch.project.manifest
push_options = list(opt.push_options)
sp = manifest.superproject
if sp and sp.repo_id and manifest.manifestProject.use_superproject:
push_options.append(f"custom-keyed-value=rootRepo:{sp.repo_id}")
branch.UploadForReview(
people,
dryrun=opt.dryrun,
@@ -640,7 +643,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
ready=opt.ready,
dest_branch=destination,
validate_certs=opt.validate_certs,
push_options=opt.push_options,
push_options=push_options,
patchset_description=opt.patchset_description,
)

View File

@@ -21,6 +21,8 @@ import subprocess
import unittest
from unittest import mock
import pytest
import git_command
import wrapper
@@ -263,6 +265,7 @@ class UserAgentUnitTest(unittest.TestCase):
m = re.match(r"^[^ ]+$", os_name)
self.assertIsNotNone(m)
@pytest.mark.skip_cq("TODO(b/266734831): Find out why this fails in CQ")
def test_smoke_repo(self):
"""Make sure repo UA returns something useful."""
ua = git_command.user_agent.repo
@@ -271,6 +274,7 @@ class UserAgentUnitTest(unittest.TestCase):
m = re.match(r"^git-repo/[^ ]+ ([^ ]+) git/[^ ]+ Python/[0-9.]+", ua)
self.assertIsNotNone(m)
@pytest.mark.skip_cq("TODO(b/266734831): Find out why this fails in CQ")
def test_smoke_git(self):
"""Make sure git UA returns something useful."""
ua = git_command.user_agent.git

View File

@@ -21,6 +21,7 @@ import tempfile
import unittest
from unittest import mock
import pytest
from test_manifest_xml import sort_attributes
import git_superproject
@@ -145,6 +146,7 @@ class SuperprojectTestCase(unittest.TestCase):
)
self.assertIsNone(manifest.superproject)
@pytest.mark.skip_cq("TODO(b/266734831): Find out why this takes 8m+ in CQ")
def test_superproject_get_superproject_invalid_url(self):
"""Test with an invalid url."""
manifest = self.getXmlManifest(
@@ -168,6 +170,7 @@ class SuperprojectTestCase(unittest.TestCase):
self.assertFalse(sync_result.success)
self.assertTrue(sync_result.fatal)
@pytest.mark.skip_cq("TODO(b/266734831): Find out why this takes 8m+ in CQ")
def test_superproject_get_superproject_invalid_branch(self):
"""Test with an invalid branch."""
manifest = self.getXmlManifest(

View File

@@ -51,7 +51,7 @@ INVALID_FS_PATHS = (
"foo~",
"blah/foo~",
# Block Unicode characters that get normalized out by filesystems.
"foo\u200Cbar",
"foo\u200cbar",
# Block newlines.
"f\n/bar",
"f\r/bar",

View File

@@ -89,3 +89,44 @@ class AllCommands(unittest.TestCase):
msg=f"subcmds/{name}.py: {opt}: only use dashes in "
"options, not underscores",
)
def test_cli_option_dest(self):
"""Block redundant dest= arguments."""
def _check_dest(opt):
if opt.dest is None or not opt._long_opts:
return
long = opt._long_opts[0]
assert long.startswith("--")
# This matches optparse's behavior.
implicit_dest = long[2:].replace("-", "_")
if implicit_dest == opt.dest:
bad_opts.append((str(opt), opt.dest))
# Hook the option check list.
optparse.Option.CHECK_METHODS.insert(0, _check_dest)
# Gather all the bad options up front so people can see all bad options
# instead of failing at the first one.
all_bad_opts = {}
for name, cls in subcmds.all_commands.items():
bad_opts = all_bad_opts[name] = []
cmd = cls()
# Trigger construction of parser.
cmd.OptionParser
errmsg = None
for name, bad_opts in sorted(all_bad_opts.items()):
if bad_opts:
if not errmsg:
errmsg = "Omit redundant dest= when defining options.\n"
errmsg += f"\nSubcommand {name} (subcmds/{name}.py):\n"
errmsg += "".join(
f" {opt}: dest='{dest}'\n" for opt, dest in bad_opts
)
if errmsg:
self.fail(errmsg)
# Make sure we aren't popping the wrong stuff.
assert optparse.Option.CHECK_METHODS.pop(0) is _check_dest

View File

@@ -0,0 +1,156 @@
# Copyright (C) 2025 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unittests for the subcmds/manifest.py module."""
import json
from pathlib import Path
from unittest import mock
import manifest_xml
from subcmds import manifest
_EXAMPLE_MANIFEST = """\
<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<remote name="test-remote" fetch="http://localhost" />
<default remote="test-remote" revision="refs/heads/main" />
<project name="repohooks" path="src/repohooks"/>
<repo-hooks in-project="repohooks" enabled-list="a, b"/>
</manifest>
"""
def _get_cmd(repodir: Path) -> manifest.Manifest:
"""Instantiate a manifest command object to test."""
manifests_git = repodir / "manifests.git"
manifests_git.mkdir()
(manifests_git / "config").write_text(
"""
[remote "origin"]
\turl = http://localhost/manifest
"""
)
client = manifest_xml.RepoClient(repodir=str(repodir))
git_event_log = mock.MagicMock(ErrorEvent=mock.Mock(return_value=None))
return manifest.Manifest(
repodir=client.repodir,
client=client,
manifest=client.manifest,
outer_client=client,
outer_manifest=client.manifest,
git_event_log=git_event_log,
)
def test_output_format_xml_file(tmp_path):
"""Test writing XML to a file."""
path = tmp_path / "manifest.xml"
path.write_text(_EXAMPLE_MANIFEST)
outpath = tmp_path / "output.xml"
cmd = _get_cmd(tmp_path)
opt, args = cmd.OptionParser.parse_args(["--output-file", str(outpath)])
cmd.Execute(opt, args)
# Normalize the output a bit as we don't exactly care.
normalize = lambda data: "\n".join(
x.strip() for x in data.splitlines() if x.strip()
)
assert (
normalize(outpath.read_text())
== """<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<remote name="test-remote" fetch="http://localhost"/>
<default remote="test-remote" revision="refs/heads/main"/>
<project name="repohooks" path="src/repohooks"/>
<repo-hooks in-project="repohooks" enabled-list="a b"/>
</manifest>"""
)
def test_output_format_xml_stdout(tmp_path, capsys):
"""Test writing XML to stdout."""
path = tmp_path / "manifest.xml"
path.write_text(_EXAMPLE_MANIFEST)
cmd = _get_cmd(tmp_path)
opt, args = cmd.OptionParser.parse_args(["--format", "xml"])
cmd.Execute(opt, args)
# Normalize the output a bit as we don't exactly care.
normalize = lambda data: "\n".join(
x.strip() for x in data.splitlines() if x.strip()
)
stdout = capsys.readouterr().out
assert (
normalize(stdout)
== """<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<remote name="test-remote" fetch="http://localhost"/>
<default remote="test-remote" revision="refs/heads/main"/>
<project name="repohooks" path="src/repohooks"/>
<repo-hooks in-project="repohooks" enabled-list="a b"/>
</manifest>"""
)
def test_output_format_json(tmp_path, capsys):
"""Test writing JSON."""
path = tmp_path / "manifest.xml"
path.write_text(_EXAMPLE_MANIFEST)
cmd = _get_cmd(tmp_path)
opt, args = cmd.OptionParser.parse_args(["--format", "json"])
cmd.Execute(opt, args)
obj = json.loads(capsys.readouterr().out)
assert obj == {
"default": {"remote": "test-remote", "revision": "refs/heads/main"},
"project": [{"name": "repohooks", "path": "src/repohooks"}],
"remote": [{"fetch": "http://localhost", "name": "test-remote"}],
"repo-hooks": {"enabled-list": "a b", "in-project": "repohooks"},
}
def test_output_format_json_pretty(tmp_path, capsys):
"""Test writing pretty JSON."""
path = tmp_path / "manifest.xml"
path.write_text(_EXAMPLE_MANIFEST)
cmd = _get_cmd(tmp_path)
opt, args = cmd.OptionParser.parse_args(["--format", "json", "--pretty"])
cmd.Execute(opt, args)
stdout = capsys.readouterr().out
assert (
stdout
== """\
{
"default": {
"remote": "test-remote",
"revision": "refs/heads/main"
},
"project": [
{
"name": "repohooks",
"path": "src/repohooks"
}
],
"remote": [
{
"fetch": "http://localhost",
"name": "test-remote"
}
],
"repo-hooks": {
"enabled-list": "a b",
"in-project": "repohooks"
}
}
"""
)

View File

@@ -17,6 +17,7 @@
import io
import os
import re
import subprocess
import sys
import tempfile
import unittest
@@ -125,7 +126,7 @@ class RunCommand(RepoWrapperTestCase):
self.wrapper.run_command(["true"], check=False)
self.wrapper.run_command(["true"], check=True)
self.wrapper.run_command(["false"], check=False)
with self.assertRaises(self.wrapper.RunError):
with self.assertRaises(subprocess.CalledProcessError):
self.wrapper.run_command(["false"], check=True)
@@ -358,8 +359,8 @@ class VerifyRev(RepoWrapperTestCase):
def test_verify_passes(self):
"""Check when we have a valid signed tag."""
desc_result = self.wrapper.RunResult(0, "v1.0\n", "")
gpg_result = self.wrapper.RunResult(0, "", "")
desc_result = subprocess.CompletedProcess([], 0, "v1.0\n", "")
gpg_result = subprocess.CompletedProcess([], 0, "", "")
with mock.patch.object(
self.wrapper, "run_git", side_effect=(desc_result, gpg_result)
):
@@ -370,8 +371,8 @@ class VerifyRev(RepoWrapperTestCase):
def test_unsigned_commit(self):
"""Check we fall back to signed tag when we have an unsigned commit."""
desc_result = self.wrapper.RunResult(0, "v1.0-10-g1234\n", "")
gpg_result = self.wrapper.RunResult(0, "", "")
desc_result = subprocess.CompletedProcess([], 0, "v1.0-10-g1234\n", "")
gpg_result = subprocess.CompletedProcess([], 0, "", "")
with mock.patch.object(
self.wrapper, "run_git", side_effect=(desc_result, gpg_result)
):
@@ -382,7 +383,7 @@ class VerifyRev(RepoWrapperTestCase):
def test_verify_fails(self):
"""Check we fall back to signed tag when we have an unsigned commit."""
desc_result = self.wrapper.RunResult(0, "v1.0-10-g1234\n", "")
desc_result = subprocess.CompletedProcess([], 0, "v1.0-10-g1234\n", "")
gpg_result = Exception
with mock.patch.object(
self.wrapper, "run_git", side_effect=(desc_result, gpg_result)