mirror of
https://gerrit.googlesource.com/git-repo
synced 2025-04-20 14:09:30 +00:00
Compare commits
43 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a94457d1ce | ||
|
97dc5c1bd9 | ||
|
0214730c9a | ||
|
daebd6cbc2 | ||
|
3667de1d0f | ||
|
85ee1738e6 | ||
|
f070331a4c | ||
|
9ecb80ba26 | ||
|
dc8185f2a9 | ||
|
59b81c84de | ||
|
507d463600 | ||
|
cd391e77d0 | ||
|
8310436be0 | ||
|
d5087392ed | ||
|
91f428058d | ||
|
243df2042e | ||
|
4b94e773ef | ||
|
fc901b92bb | ||
|
8d5f032611 | ||
|
99eca45eb2 | ||
|
66685f07ec | ||
|
cf9a2a2a76 | ||
|
5ae8292fea | ||
|
dfdf577e98 | ||
|
747ec83f58 | ||
|
1711bc23c0 | ||
|
db111d3924 | ||
|
3405446a4e | ||
|
41a27eb854 | ||
|
d93fe60e89 | ||
|
61224d01fa | ||
|
13d6588bf6 | ||
|
9500aca754 | ||
|
e8a7b9d596 | ||
|
cf411b3f03 | ||
|
1feecbd91e | ||
|
616e314902 | ||
|
fafd1ec23e | ||
|
b1613d741e | ||
|
ab2d321104 | ||
|
aada468916 | ||
|
1d5098617e | ||
|
e219c78fe5 |
@ -547,7 +547,3 @@ class MirrorSafeCommand:
|
|||||||
"""Command permits itself to run within a mirror, and does not require a
|
"""Command permits itself to run within a mirror, and does not require a
|
||||||
working directory.
|
working directory.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class GitcClientCommand:
|
|
||||||
"""Command that requires the local client to be a GITC client."""
|
|
||||||
|
@ -1 +1,2 @@
|
|||||||
black<24
|
# NB: Keep in sync with run_tests.vpython3.
|
||||||
|
black<26
|
||||||
|
@ -141,7 +141,7 @@ Instead, you should use standard Git workflows like [git worktree] or
|
|||||||
(e.g. a local mirror & a public review server) while avoiding duplicating
|
(e.g. a local mirror & a public review server) while avoiding duplicating
|
||||||
the content. However, this can run into problems if different remotes use
|
the content. However, this can run into problems if different remotes use
|
||||||
the same path on their respective servers. Best to avoid that.
|
the same path on their respective servers. Best to avoid that.
|
||||||
* `subprojects/`: Like `projects/`, but for git submodules.
|
* `modules/`: Like `projects/`, but for git submodules.
|
||||||
* `subproject-objects/`: Like `project-objects/`, but for git submodules.
|
* `subproject-objects/`: Like `project-objects/`, but for git submodules.
|
||||||
* `worktrees/`: Bare checkouts of every project synced by the manifest. The
|
* `worktrees/`: Bare checkouts of every project synced by the manifest. The
|
||||||
filesystem layout matches the `<project name=...` setting in the manifest
|
filesystem layout matches the `<project name=...` setting in the manifest
|
||||||
|
@ -231,26 +231,7 @@ At most one manifest-server may be specified. The url attribute
|
|||||||
is used to specify the URL of a manifest server, which is an
|
is used to specify the URL of a manifest server, which is an
|
||||||
XML RPC service.
|
XML RPC service.
|
||||||
|
|
||||||
The manifest server should implement the following RPC methods:
|
See the [smart sync documentation](./smart-sync.md) for more details.
|
||||||
|
|
||||||
GetApprovedManifest(branch, target)
|
|
||||||
|
|
||||||
Return a manifest in which each project is pegged to a known good revision
|
|
||||||
for the current branch and target. This is used by repo sync when the
|
|
||||||
--smart-sync option is given.
|
|
||||||
|
|
||||||
The target to use is defined by environment variables TARGET_PRODUCT
|
|
||||||
and TARGET_BUILD_VARIANT. These variables are used to create a string
|
|
||||||
of the form $TARGET_PRODUCT-$TARGET_BUILD_VARIANT, e.g. passion-userdebug.
|
|
||||||
If one of those variables or both are not present, the program will call
|
|
||||||
GetApprovedManifest without the target parameter and the manifest server
|
|
||||||
should choose a reasonable default target.
|
|
||||||
|
|
||||||
GetManifest(tag)
|
|
||||||
|
|
||||||
Return a manifest in which each project is pegged to the revision at
|
|
||||||
the specified tag. This is used by repo sync when the --smart-tag option
|
|
||||||
is given.
|
|
||||||
|
|
||||||
|
|
||||||
### Element submanifest
|
### Element submanifest
|
||||||
|
129
docs/smart-sync.md
Normal file
129
docs/smart-sync.md
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# repo Smart Syncing
|
||||||
|
|
||||||
|
Repo normally fetches & syncs manifests from the same URL specified during
|
||||||
|
`repo init`, and that often fetches the latest revisions of all projects in
|
||||||
|
the manifest. This flow works well for tracking and developing with the
|
||||||
|
latest code, but often it's desirable to sync to other points. For example,
|
||||||
|
to get a local build matching a specific release or build to reproduce bugs
|
||||||
|
reported by other people.
|
||||||
|
|
||||||
|
Repo's sync subcommand has support for fetching manifests from a server over
|
||||||
|
an XML-RPC connection. The local configuration and network API are defined by
|
||||||
|
repo, but individual projects have to host their own server for the client to
|
||||||
|
communicate with.
|
||||||
|
|
||||||
|
This process is called "smart syncing" -- instead of blindly fetching the latest
|
||||||
|
revision of all projects and getting an unknown state to develop against, the
|
||||||
|
client passes a request to the server and is given a matching manifest that
|
||||||
|
typically specifies specific commits for every project to fetch a known source
|
||||||
|
state.
|
||||||
|
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
## Manifest Configuration
|
||||||
|
|
||||||
|
The manifest specifies the server to communicate with via the
|
||||||
|
the [`<manifest-server>` element](manifest-format.md#Element-manifest_server)
|
||||||
|
element. This is how the client knows what service to talk to.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<manifest-server url="https://example.com/your/manifest/server/url" />
|
||||||
|
```
|
||||||
|
|
||||||
|
If the URL starts with `persistent-`, then the
|
||||||
|
[`git-remote-persistent-https` helper](https://github.com/git/git/blob/HEAD/contrib/persistent-https/README)
|
||||||
|
is used to communicate with the server.
|
||||||
|
|
||||||
|
## Credentials
|
||||||
|
|
||||||
|
Credentials may be specified directly in typical `username:password`
|
||||||
|
[URI syntax](https://en.wikipedia.org/wiki/URI#Syntax) in the
|
||||||
|
`<manifest-server>` element directly in the manifest.
|
||||||
|
|
||||||
|
If they are not specified, `repo sync` has `--manifest-server-username=USERNAME`
|
||||||
|
and `--manifest-server-password=PASSWORD` options.
|
||||||
|
|
||||||
|
If those are not used, then repo will look up the host in your
|
||||||
|
[`~/.netrc`](https://docs.python.org/3/library/netrc.html) database.
|
||||||
|
|
||||||
|
When making the connection, cookies matching the host are automatically loaded
|
||||||
|
from the cookiejar specified in
|
||||||
|
[Git's `http.cookiefile` setting](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpcookieFile).
|
||||||
|
|
||||||
|
## Manifest Server
|
||||||
|
|
||||||
|
Unfortunately, there are no public reference implementations. Google has an
|
||||||
|
internal one for Android, but it is written using Google's internal systems,
|
||||||
|
so wouldn't be that helpful as a reference.
|
||||||
|
|
||||||
|
That said, the XML-RPC API is pretty simple, so any standard XML-RPC server
|
||||||
|
example would do. Google's internal server uses Python's
|
||||||
|
[xmlrpc.server.SimpleXMLRPCDispatcher](https://docs.python.org/3/library/xmlrpc.server.html).
|
||||||
|
|
||||||
|
## Network API
|
||||||
|
|
||||||
|
The manifest server should implement the following RPC methods.
|
||||||
|
|
||||||
|
### GetApprovedManifest
|
||||||
|
|
||||||
|
> `GetApprovedManifest(branch: str, target: Optional[str]) -> str`
|
||||||
|
|
||||||
|
The meaning of `branch` and `target` is not strictly defined. The server may
|
||||||
|
interpret them however it wants. The recommended interpretation is that the
|
||||||
|
`branch` matches the manifest branch, and `target` is an identifier for your
|
||||||
|
project that matches something users would build.
|
||||||
|
|
||||||
|
See the client section below for how repo typically generates these values.
|
||||||
|
|
||||||
|
The server will return a manifest or an error. If it's an error, repo will
|
||||||
|
show the output directly to the user to provide a limited feedback channel.
|
||||||
|
|
||||||
|
If the user's request is ambiguous and could match multiple manifests, the
|
||||||
|
server has to decide whether to pick one automatically (and silently such that
|
||||||
|
the user won't know there were multiple matches), or return an error and force
|
||||||
|
the user to be more specific.
|
||||||
|
|
||||||
|
### GetManifest
|
||||||
|
|
||||||
|
> `GetManifest(tag: str) -> str`
|
||||||
|
|
||||||
|
The meaning of `tag` is not strictly defined. Projects are encouraged to use
|
||||||
|
a system where the tag matches a unique source state.
|
||||||
|
|
||||||
|
See the client section below for how repo typically generates these values.
|
||||||
|
|
||||||
|
The server will return a manifest or an error. If it's an error, repo will
|
||||||
|
show the output directly to the user to provide a limited feedback channel.
|
||||||
|
|
||||||
|
If the user's request is ambiguous and could match multiple manifests, the
|
||||||
|
server has to decide whether to pick one automatically (and silently such that
|
||||||
|
the user won't know there were multiple matches), or return an error and force
|
||||||
|
the user to be more specific.
|
||||||
|
|
||||||
|
## Client Options
|
||||||
|
|
||||||
|
Once repo has successfully downloaded the manifest from the server, it saves a
|
||||||
|
copy into `.repo/manifests/smart_sync_override.xml` so users can examine it.
|
||||||
|
The next time `repo sync` is run, this file is automatically replaced or removed
|
||||||
|
based on the current set of options.
|
||||||
|
|
||||||
|
### --smart-sync
|
||||||
|
|
||||||
|
Repo will call `GetApprovedManifest(branch[, target])`.
|
||||||
|
|
||||||
|
The `branch` is determined by the current manifest branch as specified by
|
||||||
|
`--manifest-branch=BRANCH` when running `repo init`.
|
||||||
|
|
||||||
|
The `target` is defined by environment variables in the order below. If none
|
||||||
|
of them match, then `target` is omitted. These variables were decided as they
|
||||||
|
match the settings Android build environments automatically setup.
|
||||||
|
|
||||||
|
1. `${SYNC_TARGET}`: If defined, the value is used directly.
|
||||||
|
2. `${TARGET_PRODUCT}-${TARGET_RELEASE}-${TARGET_BUILD_VARIANT}`: If these
|
||||||
|
variables are all defined, then they are merged with `-` and used.
|
||||||
|
3. `${TARGET_PRODUCT}-${TARGET_BUILD_VARIANT}`: If these variables are all
|
||||||
|
defined, then they are merged with `-` and used.
|
||||||
|
|
||||||
|
### --smart-tag=TAG
|
||||||
|
|
||||||
|
Repo will call `GetManifest(TAG)`.
|
4
error.py
4
error.py
@ -111,10 +111,6 @@ class GitAuthError(RepoExitError):
|
|||||||
"""Cannot talk to remote due to auth issue."""
|
"""Cannot talk to remote due to auth issue."""
|
||||||
|
|
||||||
|
|
||||||
class GitcUnsupportedError(RepoExitError):
|
|
||||||
"""Gitc no longer supported."""
|
|
||||||
|
|
||||||
|
|
||||||
class UploadError(RepoError):
|
class UploadError(RepoError):
|
||||||
"""A bundle upload to Gerrit did not succeed."""
|
"""A bundle upload to Gerrit did not succeed."""
|
||||||
|
|
||||||
|
@ -238,9 +238,9 @@ def _build_env(
|
|||||||
s = p + " " + s
|
s = p + " " + s
|
||||||
env["GIT_CONFIG_PARAMETERS"] = s
|
env["GIT_CONFIG_PARAMETERS"] = s
|
||||||
if "GIT_ALLOW_PROTOCOL" not in env:
|
if "GIT_ALLOW_PROTOCOL" not in env:
|
||||||
env[
|
env["GIT_ALLOW_PROTOCOL"] = (
|
||||||
"GIT_ALLOW_PROTOCOL"
|
"file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc"
|
||||||
] = "file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc"
|
)
|
||||||
env["GIT_HTTP_USER_AGENT"] = user_agent.git
|
env["GIT_HTTP_USER_AGENT"] = user_agent.git
|
||||||
|
|
||||||
if objdir:
|
if objdir:
|
||||||
@ -350,9 +350,9 @@ class GitCommand:
|
|||||||
"Project": e.project,
|
"Project": e.project,
|
||||||
"CommandName": command_name,
|
"CommandName": command_name,
|
||||||
"Message": str(e),
|
"Message": str(e),
|
||||||
"ReturnCode": str(e.git_rc)
|
"ReturnCode": (
|
||||||
if e.git_rc is not None
|
str(e.git_rc) if e.git_rc is not None else None
|
||||||
else None,
|
),
|
||||||
"IsError": log_as_error,
|
"IsError": log_as_error,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -90,6 +90,20 @@ class GitConfig:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _getUserConfig():
|
def _getUserConfig():
|
||||||
|
"""Get the user-specific config file.
|
||||||
|
|
||||||
|
Prefers the XDG config location if available, with fallback to
|
||||||
|
~/.gitconfig
|
||||||
|
|
||||||
|
This matches git behavior:
|
||||||
|
https://git-scm.com/docs/git-config#FILES
|
||||||
|
"""
|
||||||
|
xdg_config_home = os.getenv(
|
||||||
|
"XDG_CONFIG_HOME", os.path.expanduser("~/.config")
|
||||||
|
)
|
||||||
|
xdg_config_file = os.path.join(xdg_config_home, "git", "config")
|
||||||
|
if os.path.exists(xdg_config_file):
|
||||||
|
return xdg_config_file
|
||||||
return os.path.expanduser("~/.gitconfig")
|
return os.path.expanduser("~/.gitconfig")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -130,10 +130,10 @@ class BaseEventLog:
|
|||||||
"time": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
"time": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def StartEvent(self):
|
def StartEvent(self, argv):
|
||||||
"""Append a 'start' event to the current log."""
|
"""Append a 'start' event to the current log."""
|
||||||
start_event = self._CreateEventDict("start")
|
start_event = self._CreateEventDict("start")
|
||||||
start_event["argv"] = sys.argv
|
start_event["argv"] = argv
|
||||||
self._log.append(start_event)
|
self._log.append(start_event)
|
||||||
|
|
||||||
def ExitEvent(self, result):
|
def ExitEvent(self, result):
|
||||||
@ -159,9 +159,11 @@ class BaseEventLog:
|
|||||||
name: Name of the primary command (ex: repo, git)
|
name: Name of the primary command (ex: repo, git)
|
||||||
subcommands: List of the sub-commands (ex: version, init, sync)
|
subcommands: List of the sub-commands (ex: version, init, sync)
|
||||||
"""
|
"""
|
||||||
command_event = self._CreateEventDict("command")
|
command_event = self._CreateEventDict("cmd_name")
|
||||||
|
name = f"{name}-"
|
||||||
|
name += "-".join(subcommands)
|
||||||
command_event["name"] = name
|
command_event["name"] = name
|
||||||
command_event["subcommands"] = subcommands
|
command_event["hierarchy"] = name
|
||||||
self._log.append(command_event)
|
self._log.append(command_event)
|
||||||
|
|
||||||
def LogConfigEvents(self, config, event_dict_name):
|
def LogConfigEvents(self, config, event_dict_name):
|
||||||
|
7
main.py
7
main.py
@ -45,7 +45,6 @@ from command import InteractiveCommand
|
|||||||
from command import MirrorSafeCommand
|
from command import MirrorSafeCommand
|
||||||
from editor import Editor
|
from editor import Editor
|
||||||
from error import DownloadError
|
from error import DownloadError
|
||||||
from error import GitcUnsupportedError
|
|
||||||
from error import InvalidProjectGroupsError
|
from error import InvalidProjectGroupsError
|
||||||
from error import ManifestInvalidRevisionError
|
from error import ManifestInvalidRevisionError
|
||||||
from error import ManifestParseError
|
from error import ManifestParseError
|
||||||
@ -308,10 +307,6 @@ class _Repo:
|
|||||||
outer_client=outer_client,
|
outer_client=outer_client,
|
||||||
)
|
)
|
||||||
|
|
||||||
if Wrapper().gitc_parse_clientdir(os.getcwd()):
|
|
||||||
logger.error("GITC is not supported.")
|
|
||||||
raise GitcUnsupportedError()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cmd = self.commands[name](
|
cmd = self.commands[name](
|
||||||
repodir=self.repodir,
|
repodir=self.repodir,
|
||||||
@ -357,7 +352,7 @@ class _Repo:
|
|||||||
start = time.time()
|
start = time.time()
|
||||||
cmd_event = cmd.event_log.Add(name, event_log.TASK_COMMAND, start)
|
cmd_event = cmd.event_log.Add(name, event_log.TASK_COMMAND, start)
|
||||||
cmd.event_log.SetParent(cmd_event)
|
cmd.event_log.SetParent(cmd_event)
|
||||||
git_trace2_event_log.StartEvent()
|
git_trace2_event_log.StartEvent(["repo", name] + argv)
|
||||||
git_trace2_event_log.CommandEvent(name="repo", subcommands=[name])
|
git_trace2_event_log.CommandEvent(name="repo", subcommands=[name])
|
||||||
|
|
||||||
def execute_command_helper():
|
def execute_command_helper():
|
||||||
|
43
man/repo-gc.1
Normal file
43
man/repo-gc.1
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
|
.TH REPO "1" "December 2024" "repo gc" "Repo Manual"
|
||||||
|
.SH NAME
|
||||||
|
repo \- repo gc - manual page for repo gc
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B repo
|
||||||
|
\fI\,gc\/\fR
|
||||||
|
.SH DESCRIPTION
|
||||||
|
Summary
|
||||||
|
.PP
|
||||||
|
Cleaning up internal repo state.
|
||||||
|
.SH OPTIONS
|
||||||
|
.TP
|
||||||
|
\fB\-h\fR, \fB\-\-help\fR
|
||||||
|
show this help message and exit
|
||||||
|
.TP
|
||||||
|
\fB\-n\fR, \fB\-\-dry\-run\fR
|
||||||
|
do everything except actually delete
|
||||||
|
.TP
|
||||||
|
\fB\-y\fR, \fB\-\-yes\fR
|
||||||
|
answer yes to all safe prompts
|
||||||
|
.SS Logging options:
|
||||||
|
.TP
|
||||||
|
\fB\-v\fR, \fB\-\-verbose\fR
|
||||||
|
show all output
|
||||||
|
.TP
|
||||||
|
\fB\-q\fR, \fB\-\-quiet\fR
|
||||||
|
only show errors
|
||||||
|
.SS Multi\-manifest options:
|
||||||
|
.TP
|
||||||
|
\fB\-\-outer\-manifest\fR
|
||||||
|
operate starting at the outermost manifest
|
||||||
|
.TP
|
||||||
|
\fB\-\-no\-outer\-manifest\fR
|
||||||
|
do not operate on outer manifests
|
||||||
|
.TP
|
||||||
|
\fB\-\-this\-manifest\-only\fR
|
||||||
|
only operate on this (sub)manifest
|
||||||
|
.TP
|
||||||
|
\fB\-\-no\-this\-manifest\-only\fR, \fB\-\-all\-manifests\fR
|
||||||
|
operate on this manifest and its submanifests
|
||||||
|
.PP
|
||||||
|
Run `repo help gc` to view the detailed manual.
|
@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "April 2024" "repo manifest" "Repo Manual"
|
.TH REPO "1" "December 2024" "repo manifest" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repo manifest - manual page for repo manifest
|
repo \- repo manifest - manual page for repo manifest
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@ -192,11 +192,13 @@ CDATA #IMPLIED>
|
|||||||
<!ATTLIST extend\-project remote CDATA #IMPLIED>
|
<!ATTLIST extend\-project remote CDATA #IMPLIED>
|
||||||
<!ATTLIST extend\-project dest\-branch CDATA #IMPLIED>
|
<!ATTLIST extend\-project dest\-branch CDATA #IMPLIED>
|
||||||
<!ATTLIST extend\-project upstream CDATA #IMPLIED>
|
<!ATTLIST extend\-project upstream CDATA #IMPLIED>
|
||||||
|
<!ATTLIST extend\-project base\-rev CDATA #IMPLIED>
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT remove\-project EMPTY>
|
<!ELEMENT remove\-project EMPTY>
|
||||||
<!ATTLIST remove\-project name CDATA #IMPLIED>
|
<!ATTLIST remove\-project name CDATA #IMPLIED>
|
||||||
<!ATTLIST remove\-project path CDATA #IMPLIED>
|
<!ATTLIST remove\-project path CDATA #IMPLIED>
|
||||||
<!ATTLIST remove\-project optional CDATA #IMPLIED>
|
<!ATTLIST remove\-project optional CDATA #IMPLIED>
|
||||||
|
<!ATTLIST remove\-project base\-rev CDATA #IMPLIED>
|
||||||
.IP
|
.IP
|
||||||
<!ELEMENT repo\-hooks EMPTY>
|
<!ELEMENT repo\-hooks EMPTY>
|
||||||
<!ATTLIST repo\-hooks in\-project CDATA #REQUIRED>
|
<!ATTLIST repo\-hooks in\-project CDATA #REQUIRED>
|
||||||
@ -495,6 +497,14 @@ project. Same syntax as the corresponding element of `project`.
|
|||||||
Attribute `upstream`: If specified, overrides the upstream of the original
|
Attribute `upstream`: If specified, overrides the upstream of the original
|
||||||
project. Same syntax as the corresponding element of `project`.
|
project. Same syntax as the corresponding element of `project`.
|
||||||
.PP
|
.PP
|
||||||
|
Attribute `base\-rev`: If specified, adds a check against the revision to be
|
||||||
|
extended. Manifest parse will fail and give a list of mismatch extends if the
|
||||||
|
revisions being extended have changed since base\-rev was set. Intended for use
|
||||||
|
with layered manifests using hash revisions to prevent patch branches hiding
|
||||||
|
newer upstream revisions. Also compares named refs like branches or tags but is
|
||||||
|
misleading if branches are used as base\-rev. Same syntax as the corresponding
|
||||||
|
element of `project`.
|
||||||
|
.PP
|
||||||
Element annotation
|
Element annotation
|
||||||
.PP
|
.PP
|
||||||
Zero or more annotation elements may be specified as children of a project or
|
Zero or more annotation elements may be specified as children of a project or
|
||||||
@ -556,6 +566,14 @@ Logic otherwise behaves like both are specified.
|
|||||||
Attribute `optional`: Set to true to ignore remove\-project elements with no
|
Attribute `optional`: Set to true to ignore remove\-project elements with no
|
||||||
matching `project` element.
|
matching `project` element.
|
||||||
.PP
|
.PP
|
||||||
|
Attribute `base\-rev`: If specified, adds a check against the revision to be
|
||||||
|
removed. Manifest parse will fail and give a list of mismatch removes if the
|
||||||
|
revisions being removed have changed since base\-rev was set. Intended for use
|
||||||
|
with layered manifests using hash revisions to prevent patch branches hiding
|
||||||
|
newer upstream revisions. Also compares named refs like branches or tags but is
|
||||||
|
misleading if branches are used as base\-rev. Same syntax as the corresponding
|
||||||
|
element of `project`.
|
||||||
|
.PP
|
||||||
Element repo\-hooks
|
Element repo\-hooks
|
||||||
.PP
|
.PP
|
||||||
NB: See the [practical documentation](./repo\-hooks.md) for using repo hooks.
|
NB: See the [practical documentation](./repo\-hooks.md) for using repo hooks.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
|
||||||
.TH REPO "1" "April 2024" "repo" "Repo Manual"
|
.TH REPO "1" "December 2024" "repo" "Repo Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
repo \- repository management tool built on top of git
|
repo \- repository management tool built on top of git
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@ -79,6 +79,9 @@ Download and checkout a change
|
|||||||
forall
|
forall
|
||||||
Run a shell command in each project
|
Run a shell command in each project
|
||||||
.TP
|
.TP
|
||||||
|
gc
|
||||||
|
Cleaning up internal repo state.
|
||||||
|
.TP
|
||||||
grep
|
grep
|
||||||
Print lines matching a pattern
|
Print lines matching a pattern
|
||||||
.TP
|
.TP
|
||||||
|
@ -1014,9 +1014,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
|
|
||||||
def SetManifestOverride(self, path):
|
def SetManifestOverride(self, path):
|
||||||
"""Override manifestFile. The caller must call Unload()"""
|
"""Override manifestFile. The caller must call Unload()"""
|
||||||
self._outer_client.manifest.manifestFileOverrides[
|
self._outer_client.manifest.manifestFileOverrides[self.path_prefix] = (
|
||||||
self.path_prefix
|
path
|
||||||
] = path
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def UseLocalManifests(self):
|
def UseLocalManifests(self):
|
||||||
@ -2056,7 +2056,12 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
path = path.rstrip("/")
|
path = path.rstrip("/")
|
||||||
name = name.rstrip("/")
|
name = name.rstrip("/")
|
||||||
relpath = self._JoinRelpath(parent.relpath, path)
|
relpath = self._JoinRelpath(parent.relpath, path)
|
||||||
gitdir = os.path.join(parent.gitdir, "subprojects", "%s.git" % path)
|
subprojects = os.path.join(parent.gitdir, "subprojects", f"{path}.git")
|
||||||
|
modules = os.path.join(parent.gitdir, "modules", path)
|
||||||
|
if platform_utils.isdir(subprojects):
|
||||||
|
gitdir = subprojects
|
||||||
|
else:
|
||||||
|
gitdir = modules
|
||||||
objdir = os.path.join(
|
objdir = os.path.join(
|
||||||
parent.gitdir, "subproject-objects", "%s.git" % name
|
parent.gitdir, "subproject-objects", "%s.git" % name
|
||||||
)
|
)
|
||||||
@ -2107,22 +2112,22 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
|
|||||||
# implementation:
|
# implementation:
|
||||||
# https://eclipse.googlesource.com/jgit/jgit/+/9110037e3e9461ff4dac22fee84ef3694ed57648/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java#884
|
# https://eclipse.googlesource.com/jgit/jgit/+/9110037e3e9461ff4dac22fee84ef3694ed57648/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java#884
|
||||||
BAD_CODEPOINTS = {
|
BAD_CODEPOINTS = {
|
||||||
"\u200C", # ZERO WIDTH NON-JOINER
|
"\u200c", # ZERO WIDTH NON-JOINER
|
||||||
"\u200D", # ZERO WIDTH JOINER
|
"\u200d", # ZERO WIDTH JOINER
|
||||||
"\u200E", # LEFT-TO-RIGHT MARK
|
"\u200e", # LEFT-TO-RIGHT MARK
|
||||||
"\u200F", # RIGHT-TO-LEFT MARK
|
"\u200f", # RIGHT-TO-LEFT MARK
|
||||||
"\u202A", # LEFT-TO-RIGHT EMBEDDING
|
"\u202a", # LEFT-TO-RIGHT EMBEDDING
|
||||||
"\u202B", # RIGHT-TO-LEFT EMBEDDING
|
"\u202b", # RIGHT-TO-LEFT EMBEDDING
|
||||||
"\u202C", # POP DIRECTIONAL FORMATTING
|
"\u202c", # POP DIRECTIONAL FORMATTING
|
||||||
"\u202D", # LEFT-TO-RIGHT OVERRIDE
|
"\u202d", # LEFT-TO-RIGHT OVERRIDE
|
||||||
"\u202E", # RIGHT-TO-LEFT OVERRIDE
|
"\u202e", # RIGHT-TO-LEFT OVERRIDE
|
||||||
"\u206A", # INHIBIT SYMMETRIC SWAPPING
|
"\u206a", # INHIBIT SYMMETRIC SWAPPING
|
||||||
"\u206B", # ACTIVATE SYMMETRIC SWAPPING
|
"\u206b", # ACTIVATE SYMMETRIC SWAPPING
|
||||||
"\u206C", # INHIBIT ARABIC FORM SHAPING
|
"\u206c", # INHIBIT ARABIC FORM SHAPING
|
||||||
"\u206D", # ACTIVATE ARABIC FORM SHAPING
|
"\u206d", # ACTIVATE ARABIC FORM SHAPING
|
||||||
"\u206E", # NATIONAL DIGIT SHAPES
|
"\u206e", # NATIONAL DIGIT SHAPES
|
||||||
"\u206F", # NOMINAL DIGIT SHAPES
|
"\u206f", # NOMINAL DIGIT SHAPES
|
||||||
"\uFEFF", # ZERO WIDTH NO-BREAK SPACE
|
"\ufeff", # ZERO WIDTH NO-BREAK SPACE
|
||||||
}
|
}
|
||||||
if BAD_CODEPOINTS & path_codepoints:
|
if BAD_CODEPOINTS & path_codepoints:
|
||||||
# This message is more expansive than reality, but should be fine.
|
# This message is more expansive than reality, but should be fine.
|
||||||
|
2
pager.py
2
pager.py
@ -40,7 +40,7 @@ def RunPager(globalConfig):
|
|||||||
|
|
||||||
|
|
||||||
def TerminatePager():
|
def TerminatePager():
|
||||||
global pager_process, old_stdout, old_stderr
|
global pager_process
|
||||||
if pager_process:
|
if pager_process:
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
|
@ -156,6 +156,12 @@ def remove(path, missing_ok=False):
|
|||||||
os.rmdir(longpath)
|
os.rmdir(longpath)
|
||||||
else:
|
else:
|
||||||
os.remove(longpath)
|
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:
|
elif missing_ok and e.errno == errno.ENOENT:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
170
project.py
170
project.py
@ -576,7 +576,6 @@ class Project:
|
|||||||
dest_branch=None,
|
dest_branch=None,
|
||||||
optimized_fetch=False,
|
optimized_fetch=False,
|
||||||
retry_fetches=0,
|
retry_fetches=0,
|
||||||
old_revision=None,
|
|
||||||
):
|
):
|
||||||
"""Init a Project object.
|
"""Init a Project object.
|
||||||
|
|
||||||
@ -609,7 +608,6 @@ class Project:
|
|||||||
only fetch from the remote if the sha1 is not present locally.
|
only fetch from the remote if the sha1 is not present locally.
|
||||||
retry_fetches: Retry remote fetches n times upon receiving transient
|
retry_fetches: Retry remote fetches n times upon receiving transient
|
||||||
error with exponential backoff and jitter.
|
error with exponential backoff and jitter.
|
||||||
old_revision: saved git commit id for open GITC projects.
|
|
||||||
"""
|
"""
|
||||||
self.client = self.manifest = manifest
|
self.client = self.manifest = manifest
|
||||||
self.name = name
|
self.name = name
|
||||||
@ -639,12 +637,15 @@ class Project:
|
|||||||
self.linkfiles = []
|
self.linkfiles = []
|
||||||
self.annotations = []
|
self.annotations = []
|
||||||
self.dest_branch = dest_branch
|
self.dest_branch = dest_branch
|
||||||
self.old_revision = old_revision
|
|
||||||
|
|
||||||
# This will be filled in if a project is later identified to be the
|
# This will be filled in if a project is later identified to be the
|
||||||
# project containing repo hooks.
|
# project containing repo hooks.
|
||||||
self.enabled_repo_hooks = []
|
self.enabled_repo_hooks = []
|
||||||
|
|
||||||
|
# This will be updated later if the project has submodules and
|
||||||
|
# if they will be synced.
|
||||||
|
self.has_subprojects = False
|
||||||
|
|
||||||
def RelPath(self, local=True):
|
def RelPath(self, local=True):
|
||||||
"""Return the path for the project relative to a manifest.
|
"""Return the path for the project relative to a manifest.
|
||||||
|
|
||||||
@ -1563,6 +1564,11 @@ class Project:
|
|||||||
return
|
return
|
||||||
|
|
||||||
self._InitWorkTree(force_sync=force_sync, submodules=submodules)
|
self._InitWorkTree(force_sync=force_sync, submodules=submodules)
|
||||||
|
# TODO(https://git-scm.com/docs/git-worktree#_bugs): Re-evaluate if
|
||||||
|
# submodules can be init when using worktrees once its support is
|
||||||
|
# complete.
|
||||||
|
if self.has_subprojects and not self.use_git_worktrees:
|
||||||
|
self._InitSubmodules()
|
||||||
all_refs = self.bare_ref.all
|
all_refs = self.bare_ref.all
|
||||||
self.CleanPublishedCache(all_refs)
|
self.CleanPublishedCache(all_refs)
|
||||||
revid = self.GetRevisionId(all_refs)
|
revid = self.GetRevisionId(all_refs)
|
||||||
@ -2191,24 +2197,27 @@ class Project:
|
|||||||
|
|
||||||
def get_submodules(gitdir, rev):
|
def get_submodules(gitdir, rev):
|
||||||
# Parse .gitmodules for submodule sub_paths and sub_urls.
|
# Parse .gitmodules for submodule sub_paths and sub_urls.
|
||||||
sub_paths, sub_urls = parse_gitmodules(gitdir, rev)
|
sub_paths, sub_urls, sub_shallows = parse_gitmodules(gitdir, rev)
|
||||||
if not sub_paths:
|
if not sub_paths:
|
||||||
return []
|
return []
|
||||||
# Run `git ls-tree` to read SHAs of submodule object, which happen
|
# Run `git ls-tree` to read SHAs of submodule object, which happen
|
||||||
# to be revision of submodule repository.
|
# to be revision of submodule repository.
|
||||||
sub_revs = git_ls_tree(gitdir, rev, sub_paths)
|
sub_revs = git_ls_tree(gitdir, rev, sub_paths)
|
||||||
submodules = []
|
submodules = []
|
||||||
for sub_path, sub_url in zip(sub_paths, sub_urls):
|
for sub_path, sub_url, sub_shallow in zip(
|
||||||
|
sub_paths, sub_urls, sub_shallows
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
sub_rev = sub_revs[sub_path]
|
sub_rev = sub_revs[sub_path]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# Ignore non-exist submodules.
|
# Ignore non-exist submodules.
|
||||||
continue
|
continue
|
||||||
submodules.append((sub_rev, sub_path, sub_url))
|
submodules.append((sub_rev, sub_path, sub_url, sub_shallow))
|
||||||
return submodules
|
return submodules
|
||||||
|
|
||||||
re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
|
re_path = re.compile(r"^submodule\.(.+)\.path=(.*)$")
|
||||||
re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
|
re_url = re.compile(r"^submodule\.(.+)\.url=(.*)$")
|
||||||
|
re_shallow = re.compile(r"^submodule\.(.+)\.shallow=(.*)$")
|
||||||
|
|
||||||
def parse_gitmodules(gitdir, rev):
|
def parse_gitmodules(gitdir, rev):
|
||||||
cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
|
cmd = ["cat-file", "blob", "%s:.gitmodules" % rev]
|
||||||
@ -2222,9 +2231,9 @@ class Project:
|
|||||||
gitdir=gitdir,
|
gitdir=gitdir,
|
||||||
)
|
)
|
||||||
except GitError:
|
except GitError:
|
||||||
return [], []
|
return [], [], []
|
||||||
if p.Wait() != 0:
|
if p.Wait() != 0:
|
||||||
return [], []
|
return [], [], []
|
||||||
|
|
||||||
gitmodules_lines = []
|
gitmodules_lines = []
|
||||||
fd, temp_gitmodules_path = tempfile.mkstemp()
|
fd, temp_gitmodules_path = tempfile.mkstemp()
|
||||||
@ -2241,16 +2250,17 @@ class Project:
|
|||||||
gitdir=gitdir,
|
gitdir=gitdir,
|
||||||
)
|
)
|
||||||
if p.Wait() != 0:
|
if p.Wait() != 0:
|
||||||
return [], []
|
return [], [], []
|
||||||
gitmodules_lines = p.stdout.split("\n")
|
gitmodules_lines = p.stdout.split("\n")
|
||||||
except GitError:
|
except GitError:
|
||||||
return [], []
|
return [], [], []
|
||||||
finally:
|
finally:
|
||||||
platform_utils.remove(temp_gitmodules_path)
|
platform_utils.remove(temp_gitmodules_path)
|
||||||
|
|
||||||
names = set()
|
names = set()
|
||||||
paths = {}
|
paths = {}
|
||||||
urls = {}
|
urls = {}
|
||||||
|
shallows = {}
|
||||||
for line in gitmodules_lines:
|
for line in gitmodules_lines:
|
||||||
if not line:
|
if not line:
|
||||||
continue
|
continue
|
||||||
@ -2264,10 +2274,16 @@ class Project:
|
|||||||
names.add(m.group(1))
|
names.add(m.group(1))
|
||||||
urls[m.group(1)] = m.group(2)
|
urls[m.group(1)] = m.group(2)
|
||||||
continue
|
continue
|
||||||
|
m = re_shallow.match(line)
|
||||||
|
if m:
|
||||||
|
names.add(m.group(1))
|
||||||
|
shallows[m.group(1)] = m.group(2)
|
||||||
|
continue
|
||||||
names = sorted(names)
|
names = sorted(names)
|
||||||
return (
|
return (
|
||||||
[paths.get(name, "") for name in names],
|
[paths.get(name, "") for name in names],
|
||||||
[urls.get(name, "") for name in names],
|
[urls.get(name, "") for name in names],
|
||||||
|
[shallows.get(name, "") for name in names],
|
||||||
)
|
)
|
||||||
|
|
||||||
def git_ls_tree(gitdir, rev, paths):
|
def git_ls_tree(gitdir, rev, paths):
|
||||||
@ -2308,7 +2324,7 @@ class Project:
|
|||||||
# If git repo does not exist yet, querying its submodules will
|
# If git repo does not exist yet, querying its submodules will
|
||||||
# mess up its states; so return here.
|
# mess up its states; so return here.
|
||||||
return result
|
return result
|
||||||
for rev, path, url in self._GetSubmodules():
|
for rev, path, url, shallow in self._GetSubmodules():
|
||||||
name = self.manifest.GetSubprojectName(self, path)
|
name = self.manifest.GetSubprojectName(self, path)
|
||||||
(
|
(
|
||||||
relpath,
|
relpath,
|
||||||
@ -2330,6 +2346,7 @@ class Project:
|
|||||||
review=self.remote.review,
|
review=self.remote.review,
|
||||||
revision=self.remote.revision,
|
revision=self.remote.revision,
|
||||||
)
|
)
|
||||||
|
clone_depth = 1 if shallow.lower() == "true" else None
|
||||||
subproject = Project(
|
subproject = Project(
|
||||||
manifest=self.manifest,
|
manifest=self.manifest,
|
||||||
name=name,
|
name=name,
|
||||||
@ -2346,10 +2363,13 @@ class Project:
|
|||||||
sync_s=self.sync_s,
|
sync_s=self.sync_s,
|
||||||
sync_tags=self.sync_tags,
|
sync_tags=self.sync_tags,
|
||||||
parent=self,
|
parent=self,
|
||||||
|
clone_depth=clone_depth,
|
||||||
is_derived=True,
|
is_derived=True,
|
||||||
)
|
)
|
||||||
result.append(subproject)
|
result.append(subproject)
|
||||||
result.extend(subproject.GetDerivedSubprojects())
|
result.extend(subproject.GetDerivedSubprojects())
|
||||||
|
if result:
|
||||||
|
self.has_subprojects = True
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def EnableRepositoryExtension(self, key, value="true", version=1):
|
def EnableRepositoryExtension(self, key, value="true", version=1):
|
||||||
@ -2735,6 +2755,14 @@ class Project:
|
|||||||
# field; it doesn't exist, thus abort the optimization attempt
|
# field; it doesn't exist, thus abort the optimization attempt
|
||||||
# and do a full sync.
|
# and do a full sync.
|
||||||
break
|
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:
|
elif ret < 0:
|
||||||
# Git died with a signal, exit immediately.
|
# Git died with a signal, exit immediately.
|
||||||
break
|
break
|
||||||
@ -2855,7 +2883,14 @@ class Project:
|
|||||||
|
|
||||||
# We do not use curl's --retry option since it generally doesn't
|
# 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.
|
# 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:
|
if quiet:
|
||||||
cmd += ["--silent", "--show-error"]
|
cmd += ["--silent", "--show-error"]
|
||||||
if os.path.exists(tmpPath):
|
if os.path.exists(tmpPath):
|
||||||
@ -3000,6 +3035,17 @@ class Project:
|
|||||||
project=self.name,
|
project=self.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _InitSubmodules(self, quiet=True):
|
||||||
|
"""Initialize the submodules for the project."""
|
||||||
|
cmd = ["submodule", "init"]
|
||||||
|
if quiet:
|
||||||
|
cmd.append("-q")
|
||||||
|
if GitCommand(self, cmd).Wait() != 0:
|
||||||
|
raise GitError(
|
||||||
|
f"{self.name} submodule init",
|
||||||
|
project=self.name,
|
||||||
|
)
|
||||||
|
|
||||||
def _Rebase(self, upstream, onto=None):
|
def _Rebase(self, upstream, onto=None):
|
||||||
cmd = ["rebase"]
|
cmd = ["rebase"]
|
||||||
if onto is not None:
|
if onto is not None:
|
||||||
@ -3375,20 +3421,25 @@ class Project:
|
|||||||
setting = fp.read()
|
setting = fp.read()
|
||||||
assert setting.startswith("gitdir:")
|
assert setting.startswith("gitdir:")
|
||||||
git_worktree_path = setting.split(":", 1)[1].strip()
|
git_worktree_path = setting.split(":", 1)[1].strip()
|
||||||
|
|
||||||
|
# `gitdir` maybe be either relative or absolute depending on the
|
||||||
|
# behavior of the local copy of git, so only convert the path to
|
||||||
|
# relative if it needs to be converted.
|
||||||
|
if os.path.isabs(git_worktree_path):
|
||||||
# Some platforms (e.g. Windows) won't let us update dotgit in situ
|
# Some platforms (e.g. Windows) won't let us update dotgit in situ
|
||||||
# because of file permissions. Delete it and recreate it from scratch
|
# because of file permissions. Delete it and recreate it from
|
||||||
# to avoid.
|
# scratch to avoid.
|
||||||
platform_utils.remove(dotgit)
|
platform_utils.remove(dotgit)
|
||||||
# Use relative path from checkout->worktree & maintain Unix line endings
|
# Use relative path from checkout->worktree & maintain Unix line
|
||||||
# on all OS's to match git behavior.
|
# endings on all OS's to match git behavior.
|
||||||
with open(dotgit, "w", newline="\n") as fp:
|
with open(dotgit, "w", newline="\n") as fp:
|
||||||
print(
|
print(
|
||||||
"gitdir:",
|
"gitdir:",
|
||||||
os.path.relpath(git_worktree_path, self.worktree),
|
os.path.relpath(git_worktree_path, self.worktree),
|
||||||
file=fp,
|
file=fp,
|
||||||
)
|
)
|
||||||
# Use relative path from worktree->checkout & maintain Unix line endings
|
# Use relative path from worktree->checkout & maintain Unix line
|
||||||
# on all OS's to match git behavior.
|
# endings on all OS's to match git behavior.
|
||||||
with open(
|
with open(
|
||||||
os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
|
os.path.join(git_worktree_path, "gitdir"), "w", newline="\n"
|
||||||
) as fp:
|
) as fp:
|
||||||
@ -3413,6 +3464,11 @@ class Project:
|
|||||||
"""
|
"""
|
||||||
dotgit = os.path.join(self.worktree, ".git")
|
dotgit = os.path.join(self.worktree, ".git")
|
||||||
|
|
||||||
|
# If bare checkout of the submodule is stored under the subproject dir,
|
||||||
|
# migrate it.
|
||||||
|
if self.parent:
|
||||||
|
self._MigrateOldSubmoduleDir()
|
||||||
|
|
||||||
# If using an old layout style (a directory), migrate it.
|
# If using an old layout style (a directory), migrate it.
|
||||||
if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
|
if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit):
|
||||||
self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
|
self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
|
||||||
@ -3423,34 +3479,76 @@ class Project:
|
|||||||
self._InitGitWorktree()
|
self._InitGitWorktree()
|
||||||
self._CopyAndLinkFiles()
|
self._CopyAndLinkFiles()
|
||||||
else:
|
else:
|
||||||
|
# Remove old directory symbolic links for submodules.
|
||||||
|
if self.parent and platform_utils.islink(dotgit):
|
||||||
|
platform_utils.remove(dotgit)
|
||||||
|
init_dotgit = True
|
||||||
|
|
||||||
if not init_dotgit:
|
if not init_dotgit:
|
||||||
# See if the project has changed.
|
# See if the project has changed.
|
||||||
if os.path.realpath(self.gitdir) != os.path.realpath(dotgit):
|
self._removeBadGitDirLink(dotgit)
|
||||||
platform_utils.remove(dotgit)
|
|
||||||
|
|
||||||
if init_dotgit or not os.path.exists(dotgit):
|
if init_dotgit or not os.path.exists(dotgit):
|
||||||
os.makedirs(self.worktree, exist_ok=True)
|
self._createDotGit(dotgit)
|
||||||
platform_utils.symlink(
|
|
||||||
os.path.relpath(self.gitdir, self.worktree), dotgit
|
|
||||||
)
|
|
||||||
|
|
||||||
if init_dotgit:
|
if init_dotgit:
|
||||||
_lwrite(
|
_lwrite(
|
||||||
os.path.join(dotgit, HEAD), "%s\n" % self.GetRevisionId()
|
os.path.join(self.gitdir, HEAD), f"{self.GetRevisionId()}\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Finish checking out the worktree.
|
# Finish checking out the worktree.
|
||||||
cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
|
cmd = ["read-tree", "--reset", "-u", "-v", HEAD]
|
||||||
|
try:
|
||||||
if GitCommand(self, cmd).Wait() != 0:
|
if GitCommand(self, cmd).Wait() != 0:
|
||||||
raise GitError(
|
raise GitError(
|
||||||
"Cannot initialize work tree for " + self.name,
|
"Cannot initialize work tree for " + self.name,
|
||||||
project=self.name,
|
project=self.name,
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# Something went wrong with read-tree (perhaps fetching
|
||||||
|
# missing blobs), so remove .git to avoid half initialized
|
||||||
|
# workspace from which repo can't recover on its own.
|
||||||
|
platform_utils.remove(dotgit)
|
||||||
|
raise e
|
||||||
|
|
||||||
if submodules:
|
if submodules:
|
||||||
self._SyncSubmodules(quiet=True)
|
self._SyncSubmodules(quiet=True)
|
||||||
self._CopyAndLinkFiles()
|
self._CopyAndLinkFiles()
|
||||||
|
|
||||||
|
def _createDotGit(self, dotgit):
|
||||||
|
"""Initialize .git path.
|
||||||
|
|
||||||
|
For submodule projects, create a '.git' file using the gitfile
|
||||||
|
mechanism, and for the rest, create a symbolic link.
|
||||||
|
"""
|
||||||
|
os.makedirs(self.worktree, exist_ok=True)
|
||||||
|
if self.parent:
|
||||||
|
_lwrite(
|
||||||
|
dotgit,
|
||||||
|
f"gitdir: {os.path.relpath(self.gitdir, self.worktree)}\n",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
platform_utils.symlink(
|
||||||
|
os.path.relpath(self.gitdir, self.worktree), dotgit
|
||||||
|
)
|
||||||
|
|
||||||
|
def _removeBadGitDirLink(self, dotgit):
|
||||||
|
"""Verify .git is initialized correctly, otherwise delete it."""
|
||||||
|
if self.parent and os.path.isfile(dotgit):
|
||||||
|
with open(dotgit) as fp:
|
||||||
|
setting = fp.read()
|
||||||
|
if not setting.startswith("gitdir:"):
|
||||||
|
raise GitError(
|
||||||
|
f"'.git' in {self.worktree} must start with 'gitdir:'",
|
||||||
|
project=self.name,
|
||||||
|
)
|
||||||
|
gitdir = setting.split(":", 1)[1].strip()
|
||||||
|
dotgit_path = os.path.normpath(os.path.join(self.worktree, gitdir))
|
||||||
|
else:
|
||||||
|
dotgit_path = os.path.realpath(dotgit)
|
||||||
|
if os.path.realpath(self.gitdir) != dotgit_path:
|
||||||
|
platform_utils.remove(dotgit)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
|
def _MigrateOldWorkTreeGitDir(cls, dotgit, project=None):
|
||||||
"""Migrate the old worktree .git/ dir style to a symlink.
|
"""Migrate the old worktree .git/ dir style to a symlink.
|
||||||
@ -3539,6 +3637,28 @@ class Project:
|
|||||||
dotgit,
|
dotgit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _MigrateOldSubmoduleDir(self):
|
||||||
|
"""Move the old bare checkout in 'subprojects' to 'modules'
|
||||||
|
as bare checkouts of submodules are now in 'modules' dir.
|
||||||
|
"""
|
||||||
|
subprojects = os.path.join(self.parent.gitdir, "subprojects")
|
||||||
|
if not platform_utils.isdir(subprojects):
|
||||||
|
return
|
||||||
|
|
||||||
|
modules = os.path.join(self.parent.gitdir, "modules")
|
||||||
|
old = self.gitdir
|
||||||
|
new = os.path.splitext(self.gitdir.replace(subprojects, modules))[0]
|
||||||
|
|
||||||
|
if all(map(platform_utils.isdir, [old, new])):
|
||||||
|
platform_utils.rmtree(old, ignore_errors=True)
|
||||||
|
else:
|
||||||
|
os.makedirs(modules, exist_ok=True)
|
||||||
|
platform_utils.rename(old, new)
|
||||||
|
self.gitdir = new
|
||||||
|
self.UpdatePaths(self.relpath, self.worktree, self.gitdir, self.objdir)
|
||||||
|
if platform_utils.isdir(subprojects) and not os.listdir(subprojects):
|
||||||
|
platform_utils.rmtree(subprojects, ignore_errors=True)
|
||||||
|
|
||||||
def _get_symlink_error_message(self):
|
def _get_symlink_error_message(self):
|
||||||
if platform_utils.isWindows():
|
if platform_utils.isWindows():
|
||||||
return (
|
return (
|
||||||
|
@ -16,3 +16,8 @@
|
|||||||
line-length = 80
|
line-length = 80
|
||||||
# NB: Keep in sync with tox.ini.
|
# NB: Keep in sync with tox.ini.
|
||||||
target-version = ['py36', 'py37', 'py38', 'py39', 'py310', 'py311'] #, 'py312'
|
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!
|
||||||
|
"""
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@ -35,12 +36,7 @@ KEYID_ECC = "E1F9040D7A3F6DAFAC897CD3D3B95DA243E48A39"
|
|||||||
|
|
||||||
def cmdstr(cmd):
|
def cmdstr(cmd):
|
||||||
"""Get a nicely quoted shell command."""
|
"""Get a nicely quoted shell command."""
|
||||||
ret = []
|
return " ".join(shlex.quote(x) for x in cmd)
|
||||||
for arg in cmd:
|
|
||||||
if not re.match(r"^[a-zA-Z0-9/_.=-]+$", arg):
|
|
||||||
arg = f'"{arg}"'
|
|
||||||
ret.append(arg)
|
|
||||||
return " ".join(ret)
|
|
||||||
|
|
||||||
|
|
||||||
def run(opts, cmd, check=True, **kwargs):
|
def run(opts, cmd, check=True, **kwargs):
|
||||||
|
144
repo
144
repo
@ -27,6 +27,7 @@ import platform
|
|||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
|
||||||
# These should never be newer than the main.py version since this needs to be a
|
# These should never be newer than the main.py version since this needs to be a
|
||||||
@ -56,9 +57,14 @@ class Trace:
|
|||||||
trace = 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):
|
def exec_command(cmd):
|
||||||
"""Execute |cmd| or return None on failure."""
|
"""Execute |cmd| or return None on failure."""
|
||||||
trace.print(":", " ".join(cmd))
|
trace.print(":", cmdstr(cmd))
|
||||||
try:
|
try:
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
ret = subprocess.call(cmd)
|
ret = subprocess.call(cmd)
|
||||||
@ -124,7 +130,7 @@ if not REPO_REV:
|
|||||||
BUG_URL = "https://issues.gerritcodereview.com/issues/new?component=1370071"
|
BUG_URL = "https://issues.gerritcodereview.com/issues/new?component=1370071"
|
||||||
|
|
||||||
# increment this whenever we make important changes to this script
|
# increment this whenever we make important changes to this script
|
||||||
VERSION = (2, 48)
|
VERSION = (2, 54)
|
||||||
|
|
||||||
# increment this if the MAINTAINER_KEYS block is modified
|
# increment this if the MAINTAINER_KEYS block is modified
|
||||||
KEYRING_VERSION = (2, 3)
|
KEYRING_VERSION = (2, 3)
|
||||||
@ -215,11 +221,8 @@ repodir = ".repo" # name of repo's private directory
|
|||||||
S_repo = "repo" # special repo repository
|
S_repo = "repo" # special repo repository
|
||||||
S_manifests = "manifests" # special manifest repository
|
S_manifests = "manifests" # special manifest repository
|
||||||
REPO_MAIN = S_repo + "/main.py" # main script
|
REPO_MAIN = S_repo + "/main.py" # main script
|
||||||
GITC_CONFIG_FILE = "/gitc/.config"
|
|
||||||
GITC_FS_ROOT_DIR = "/gitc/manifest-rw/"
|
|
||||||
|
|
||||||
|
|
||||||
import collections
|
|
||||||
import errno
|
import errno
|
||||||
import json
|
import json
|
||||||
import optparse
|
import optparse
|
||||||
@ -235,11 +238,8 @@ home_dot_repo = os.path.join(repo_config_dir, ".repoconfig")
|
|||||||
gpg_dir = os.path.join(home_dot_repo, "gnupg")
|
gpg_dir = os.path.join(home_dot_repo, "gnupg")
|
||||||
|
|
||||||
|
|
||||||
def GetParser(gitc_init=False):
|
def GetParser():
|
||||||
"""Setup the CLI parser."""
|
"""Setup the CLI parser."""
|
||||||
if gitc_init:
|
|
||||||
sys.exit("repo: fatal: GITC not supported.")
|
|
||||||
else:
|
|
||||||
usage = "repo init [options] [-u] url"
|
usage = "repo init [options] [-u] url"
|
||||||
|
|
||||||
parser = optparse.OptionParser(usage=usage)
|
parser = optparse.OptionParser(usage=usage)
|
||||||
@ -487,16 +487,6 @@ def InitParser(parser):
|
|||||||
return 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):
|
def run_command(cmd, **kwargs):
|
||||||
"""Run |cmd| and return its output."""
|
"""Run |cmd| and return its output."""
|
||||||
check = kwargs.pop("check", False)
|
check = kwargs.pop("check", False)
|
||||||
@ -521,7 +511,7 @@ def run_command(cmd, **kwargs):
|
|||||||
# Run & package the results.
|
# Run & package the results.
|
||||||
proc = subprocess.Popen(cmd, **kwargs)
|
proc = subprocess.Popen(cmd, **kwargs)
|
||||||
(stdout, stderr) = proc.communicate(input=cmd_input)
|
(stdout, stderr) = proc.communicate(input=cmd_input)
|
||||||
dbg = ": " + " ".join(cmd)
|
dbg = ": " + cmdstr(cmd)
|
||||||
if cmd_input is not None:
|
if cmd_input is not None:
|
||||||
dbg += " 0<|"
|
dbg += " 0<|"
|
||||||
if stdout == subprocess.PIPE:
|
if stdout == subprocess.PIPE:
|
||||||
@ -531,7 +521,9 @@ def run_command(cmd, **kwargs):
|
|||||||
elif stderr == subprocess.STDOUT:
|
elif stderr == subprocess.STDOUT:
|
||||||
dbg += " 2>&1"
|
dbg += " 2>&1"
|
||||||
trace.print(dbg)
|
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 things failed, print useful debugging output.
|
||||||
if check and ret.returncode:
|
if check and ret.returncode:
|
||||||
@ -552,56 +544,13 @@ def run_command(cmd, **kwargs):
|
|||||||
|
|
||||||
_print_output("stdout", ret.stdout)
|
_print_output("stdout", ret.stdout)
|
||||||
_print_output("stderr", ret.stderr)
|
_print_output("stderr", ret.stderr)
|
||||||
raise RunError(ret)
|
# This will raise subprocess.CalledProcessError for us.
|
||||||
|
ret.check_returncode()
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
_gitc_manifest_dir = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_gitc_manifest_dir():
|
|
||||||
global _gitc_manifest_dir
|
|
||||||
if _gitc_manifest_dir is None:
|
|
||||||
_gitc_manifest_dir = ""
|
|
||||||
try:
|
|
||||||
with open(GITC_CONFIG_FILE) as gitc_config:
|
|
||||||
for line in gitc_config:
|
|
||||||
match = re.match("gitc_dir=(?P<gitc_manifest_dir>.*)", line)
|
|
||||||
if match:
|
|
||||||
_gitc_manifest_dir = match.group("gitc_manifest_dir")
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
return _gitc_manifest_dir
|
|
||||||
|
|
||||||
|
|
||||||
def gitc_parse_clientdir(gitc_fs_path):
|
|
||||||
"""Parse a path in the GITC FS and return its client name.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
gitc_fs_path: A subdirectory path within the GITC_FS_ROOT_DIR.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The GITC client name.
|
|
||||||
"""
|
|
||||||
if gitc_fs_path == GITC_FS_ROOT_DIR:
|
|
||||||
return None
|
|
||||||
if not gitc_fs_path.startswith(GITC_FS_ROOT_DIR):
|
|
||||||
manifest_dir = get_gitc_manifest_dir()
|
|
||||||
if manifest_dir == "":
|
|
||||||
return None
|
|
||||||
if manifest_dir[-1] != "/":
|
|
||||||
manifest_dir += "/"
|
|
||||||
if gitc_fs_path == manifest_dir:
|
|
||||||
return None
|
|
||||||
if not gitc_fs_path.startswith(manifest_dir):
|
|
||||||
return None
|
|
||||||
return gitc_fs_path.split(manifest_dir)[1].split("/")[0]
|
|
||||||
return gitc_fs_path.split(GITC_FS_ROOT_DIR)[1].split("/")[0]
|
|
||||||
|
|
||||||
|
|
||||||
class CloneFailure(Exception):
|
class CloneFailure(Exception):
|
||||||
|
|
||||||
"""Indicate the remote clone of repo itself failed."""
|
"""Indicate the remote clone of repo itself failed."""
|
||||||
|
|
||||||
|
|
||||||
@ -638,9 +587,9 @@ def check_repo_rev(dst, rev, repo_verify=True, quiet=False):
|
|||||||
return (remote_ref, rev)
|
return (remote_ref, rev)
|
||||||
|
|
||||||
|
|
||||||
def _Init(args, gitc_init=False):
|
def _Init(args):
|
||||||
"""Installs repo by cloning it over the network."""
|
"""Installs repo by cloning it over the network."""
|
||||||
parser = GetParser(gitc_init=gitc_init)
|
parser = GetParser()
|
||||||
opt, args = parser.parse_args(args)
|
opt, args = parser.parse_args(args)
|
||||||
if args:
|
if args:
|
||||||
if not opt.manifest_url:
|
if not opt.manifest_url:
|
||||||
@ -720,15 +669,20 @@ def run_git(*args, **kwargs):
|
|||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except RunError:
|
except subprocess.CalledProcessError:
|
||||||
raise CloneFailure()
|
raise CloneFailure()
|
||||||
|
|
||||||
|
|
||||||
# The git version info broken down into components for easy analysis.
|
class GitVersion(NamedTuple):
|
||||||
# Similar to Python's sys.version_info.
|
"""The git version info broken down into components for easy analysis.
|
||||||
GitVersion = collections.namedtuple(
|
|
||||||
"GitVersion", ("major", "minor", "micro", "full")
|
Similar to Python's sys.version_info.
|
||||||
)
|
"""
|
||||||
|
|
||||||
|
major: int
|
||||||
|
minor: int
|
||||||
|
micro: int
|
||||||
|
full: int
|
||||||
|
|
||||||
|
|
||||||
def ParseGitVersion(ver_str=None):
|
def ParseGitVersion(ver_str=None):
|
||||||
@ -894,10 +848,11 @@ def _GetRepoConfig(name):
|
|||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
f"repo: error: git {' '.join(cmd)} failed:\n{ret.stderr}",
|
f"repo: error: git {cmdstr(cmd)} failed:\n{ret.stderr}",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
raise RunError()
|
# This will raise subprocess.CalledProcessError for us.
|
||||||
|
ret.check_returncode()
|
||||||
|
|
||||||
|
|
||||||
def _InitHttp():
|
def _InitHttp():
|
||||||
@ -1164,7 +1119,7 @@ class _Options:
|
|||||||
def _ExpandAlias(name):
|
def _ExpandAlias(name):
|
||||||
"""Look up user registered aliases."""
|
"""Look up user registered aliases."""
|
||||||
# We don't resolve aliases for existing subcommands. This matches git.
|
# We don't resolve aliases for existing subcommands. This matches git.
|
||||||
if name in {"gitc-init", "help", "init"}:
|
if name in {"help", "init"}:
|
||||||
return name, []
|
return name, []
|
||||||
|
|
||||||
alias = _GetRepoConfig(f"alias.{name}")
|
alias = _GetRepoConfig(f"alias.{name}")
|
||||||
@ -1292,10 +1247,6 @@ class Requirements:
|
|||||||
|
|
||||||
|
|
||||||
def _Usage():
|
def _Usage():
|
||||||
gitc_usage = ""
|
|
||||||
if get_gitc_manifest_dir():
|
|
||||||
gitc_usage = " gitc-init Initialize a GITC Client.\n"
|
|
||||||
|
|
||||||
print(
|
print(
|
||||||
"""usage: repo COMMAND [ARGS]
|
"""usage: repo COMMAND [ARGS]
|
||||||
|
|
||||||
@ -1304,9 +1255,7 @@ repo is not yet installed. Use "repo init" to install it here.
|
|||||||
The most commonly used repo commands are:
|
The most commonly used repo commands are:
|
||||||
|
|
||||||
init Install repo in the current working directory
|
init Install repo in the current working directory
|
||||||
"""
|
help Display detailed help on a command
|
||||||
+ gitc_usage
|
|
||||||
+ """ help Display detailed help on a command
|
|
||||||
|
|
||||||
For access to the full online help, install repo ("repo init").
|
For access to the full online help, install repo ("repo init").
|
||||||
"""
|
"""
|
||||||
@ -1317,8 +1266,8 @@ For access to the full online help, install repo ("repo init").
|
|||||||
|
|
||||||
def _Help(args):
|
def _Help(args):
|
||||||
if args:
|
if args:
|
||||||
if args[0] in {"init", "gitc-init"}:
|
if args[0] in {"init"}:
|
||||||
parser = GetParser(gitc_init=args[0] == "gitc-init")
|
parser = GetParser()
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
else:
|
else:
|
||||||
@ -1335,10 +1284,11 @@ def _Help(args):
|
|||||||
|
|
||||||
def _Version():
|
def _Version():
|
||||||
"""Show version information."""
|
"""Show version information."""
|
||||||
|
git_version = ParseGitVersion()
|
||||||
print("<repo not installed>")
|
print("<repo not installed>")
|
||||||
print(f"repo launcher version {'.'.join(str(x) for x in VERSION)}")
|
print(f"repo launcher version {'.'.join(str(x) for x in VERSION)}")
|
||||||
print(f" (from {__file__})")
|
print(f" (from {__file__})")
|
||||||
print(f"git {ParseGitVersion().full}")
|
print(f"git {git_version.full}" if git_version else "git not installed")
|
||||||
print(f"Python {sys.version}")
|
print(f"Python {sys.version}")
|
||||||
uname = platform.uname()
|
uname = platform.uname()
|
||||||
print(f"OS {uname.system} {uname.release} ({uname.version})")
|
print(f"OS {uname.system} {uname.release} ({uname.version})")
|
||||||
@ -1371,11 +1321,11 @@ def _RunSelf(wrapper_path):
|
|||||||
my_main = os.path.join(my_dir, "main.py")
|
my_main = os.path.join(my_dir, "main.py")
|
||||||
my_git = os.path.join(my_dir, ".git")
|
my_git = os.path.join(my_dir, ".git")
|
||||||
|
|
||||||
if os.path.isfile(my_main) and os.path.isdir(my_git):
|
if os.path.isfile(my_main):
|
||||||
for name in ["git_config.py", "project.py", "subcmds"]:
|
for name in ["git_config.py", "project.py", "subcmds"]:
|
||||||
if not os.path.exists(os.path.join(my_dir, name)):
|
if not os.path.exists(os.path.join(my_dir, name)):
|
||||||
return None, None
|
return None, None
|
||||||
return my_main, my_git
|
return my_main, my_git if os.path.isdir(my_git) else None
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
@ -1406,23 +1356,11 @@ def main(orig_args):
|
|||||||
# We run this early as we run some git commands ourselves.
|
# We run this early as we run some git commands ourselves.
|
||||||
SetGitTrace2ParentSid()
|
SetGitTrace2ParentSid()
|
||||||
|
|
||||||
repo_main, rel_repo_dir = None, None
|
|
||||||
# Don't use the local repo copy, make sure to switch to the gitc client first.
|
|
||||||
if cmd != "gitc-init":
|
|
||||||
repo_main, rel_repo_dir = _FindRepo()
|
repo_main, rel_repo_dir = _FindRepo()
|
||||||
|
|
||||||
wrapper_path = os.path.abspath(__file__)
|
wrapper_path = os.path.abspath(__file__)
|
||||||
my_main, my_git = _RunSelf(wrapper_path)
|
my_main, my_git = _RunSelf(wrapper_path)
|
||||||
|
|
||||||
cwd = os.getcwd()
|
|
||||||
if get_gitc_manifest_dir() and cwd.startswith(get_gitc_manifest_dir()):
|
|
||||||
print(
|
|
||||||
"error: repo cannot be used in the GITC local manifest directory."
|
|
||||||
"\nIf you want to work on this GITC client please rerun this "
|
|
||||||
"command from the corresponding client under /gitc/",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
if not repo_main:
|
if not repo_main:
|
||||||
# Only expand aliases here since we'll be parsing the CLI ourselves.
|
# Only expand aliases here since we'll be parsing the CLI ourselves.
|
||||||
# If we had repo_main, alias expansion would happen in main.py.
|
# If we had repo_main, alias expansion would happen in main.py.
|
||||||
@ -1437,11 +1375,11 @@ def main(orig_args):
|
|||||||
_Version()
|
_Version()
|
||||||
if not cmd:
|
if not cmd:
|
||||||
_NotInstalled()
|
_NotInstalled()
|
||||||
if cmd == "init" or cmd == "gitc-init":
|
if cmd == "init":
|
||||||
if my_git:
|
if my_git:
|
||||||
_SetDefaultsTo(my_git)
|
_SetDefaultsTo(my_git)
|
||||||
try:
|
try:
|
||||||
_Init(args, gitc_init=(cmd == "gitc-init"))
|
_Init(args)
|
||||||
except CloneFailure:
|
except CloneFailure:
|
||||||
path = os.path.join(repodir, S_repo)
|
path = os.path.join(repodir, S_repo)
|
||||||
print(
|
print(
|
||||||
|
61
run_tests
61
run_tests
@ -15,16 +15,57 @@
|
|||||||
|
|
||||||
"""Wrapper to run linters and pytest with the right settings."""
|
"""Wrapper to run linters and pytest with the right settings."""
|
||||||
|
|
||||||
|
import functools
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from typing import List
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
ROOT_DIR = os.path.dirname(os.path.realpath(__file__))
|
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():
|
def run_black():
|
||||||
"""Returns the exit code from black."""
|
"""Returns the exit code from black."""
|
||||||
# Black by default only matches .py files. We have to list standalone
|
# Black by default only matches .py files. We have to list standalone
|
||||||
@ -38,32 +79,40 @@ def run_black():
|
|||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
[sys.executable, "-m", "black", "--check", ROOT_DIR] + extra_programs,
|
[sys.executable, "-m", "black", "--check", ROOT_DIR] + extra_programs,
|
||||||
check=False,
|
check=False,
|
||||||
|
cwd=ROOT_DIR,
|
||||||
).returncode
|
).returncode
|
||||||
|
|
||||||
|
|
||||||
def run_flake8():
|
def run_flake8():
|
||||||
"""Returns the exit code from flake8."""
|
"""Returns the exit code from flake8."""
|
||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
[sys.executable, "-m", "flake8", ROOT_DIR], check=False
|
[sys.executable, "-m", "flake8", ROOT_DIR],
|
||||||
|
check=False,
|
||||||
|
cwd=ROOT_DIR,
|
||||||
).returncode
|
).returncode
|
||||||
|
|
||||||
|
|
||||||
def run_isort():
|
def run_isort():
|
||||||
"""Returns the exit code from isort."""
|
"""Returns the exit code from isort."""
|
||||||
return subprocess.run(
|
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
|
).returncode
|
||||||
|
|
||||||
|
|
||||||
def main(argv):
|
def main(argv):
|
||||||
"""The main entry."""
|
"""The main entry."""
|
||||||
checks = (
|
checks = (
|
||||||
lambda: pytest.main(argv),
|
functools.partial(run_pytest, argv),
|
||||||
|
functools.partial(run_pytest_py38, argv),
|
||||||
run_black,
|
run_black,
|
||||||
run_flake8,
|
run_flake8,
|
||||||
run_isort,
|
run_isort,
|
||||||
)
|
)
|
||||||
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__":
|
if __name__ == "__main__":
|
||||||
|
@ -5,97 +5,92 @@
|
|||||||
# List of available wheels:
|
# List of available wheels:
|
||||||
# https://chromium.googlesource.com/infra/infra/+/main/infra/tools/dockerbuild/wheels.md
|
# https://chromium.googlesource.com/infra/infra/+/main/infra/tools/dockerbuild/wheels.md
|
||||||
|
|
||||||
python_version: "3.8"
|
python_version: "3.11"
|
||||||
|
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/pytest-py3"
|
name: "infra/python/wheels/pytest-py3"
|
||||||
version: "version:6.2.2"
|
version: "version:8.3.4"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by pytest==6.2.2
|
# Required by pytest==8.3.4
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/py-py2_py3"
|
name: "infra/python/wheels/py-py2_py3"
|
||||||
version: "version:1.10.0"
|
version: "version:1.11.0"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by pytest==6.2.2
|
# Required by pytest==8.3.4
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/iniconfig-py3"
|
name: "infra/python/wheels/iniconfig-py3"
|
||||||
version: "version:1.1.1"
|
version: "version:1.1.1"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by pytest==6.2.2
|
# Required by pytest==8.3.4
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/packaging-py3"
|
name: "infra/python/wheels/packaging-py3"
|
||||||
version: "version:23.0"
|
version: "version:23.0"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by pytest==6.2.2
|
# Required by pytest==8.3.4
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/pluggy-py3"
|
name: "infra/python/wheels/pluggy-py3"
|
||||||
version: "version:0.13.1"
|
version: "version:1.5.0"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by pytest==6.2.2
|
# Required by pytest==8.3.4
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/toml-py3"
|
name: "infra/python/wheels/toml-py3"
|
||||||
version: "version:0.10.1"
|
version: "version:0.10.1"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by pytest==6.2.2
|
# Required by pytest==8.3.4
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/pyparsing-py3"
|
name: "infra/python/wheels/pyparsing-py3"
|
||||||
version: "version:3.0.7"
|
version: "version:3.0.7"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by pytest==6.2.2
|
# Required by pytest==8.3.4
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/attrs-py2_py3"
|
name: "infra/python/wheels/attrs-py2_py3"
|
||||||
version: "version:21.4.0"
|
version: "version:21.4.0"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by packaging==16.8
|
# NB: Keep in sync with constraints.txt.
|
||||||
wheel: <
|
|
||||||
name: "infra/python/wheels/six-py2_py3"
|
|
||||||
version: "version:1.16.0"
|
|
||||||
>
|
|
||||||
|
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/black-py3"
|
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: <
|
wheel: <
|
||||||
name: "infra/python/wheels/mypy-extensions-py3"
|
name: "infra/python/wheels/mypy-extensions-py3"
|
||||||
version: "version:0.4.3"
|
version: "version:0.4.3"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by black==23.1.0
|
# Required by black==25.1.0
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/tomli-py3"
|
name: "infra/python/wheels/tomli-py3"
|
||||||
version: "version:2.0.1"
|
version: "version:2.0.1"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by black==23.1.0
|
# Required by black==25.1.0
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/platformdirs-py3"
|
name: "infra/python/wheels/platformdirs-py3"
|
||||||
version: "version:2.5.2"
|
version: "version:2.5.2"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by black==23.1.0
|
# Required by black==25.1.0
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/pathspec-py3"
|
name: "infra/python/wheels/pathspec-py3"
|
||||||
version: "version:0.9.0"
|
version: "version:0.9.0"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by black==23.1.0
|
# Required by black==25.1.0
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/typing-extensions-py3"
|
name: "infra/python/wheels/typing-extensions-py3"
|
||||||
version: "version:4.3.0"
|
version: "version:4.3.0"
|
||||||
>
|
>
|
||||||
|
|
||||||
# Required by black==23.1.0
|
# Required by black==25.1.0
|
||||||
wheel: <
|
wheel: <
|
||||||
name: "infra/python/wheels/click-py3"
|
name: "infra/python/wheels/click-py3"
|
||||||
version: "version:8.0.3"
|
version: "version:8.0.3"
|
||||||
|
67
run_tests.vpython3.8
Normal file
67
run_tests.vpython3.8
Normal 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"
|
||||||
|
>
|
@ -167,7 +167,10 @@ is shown, then the branch appears in all projects.
|
|||||||
else:
|
else:
|
||||||
published = " "
|
published = " "
|
||||||
|
|
||||||
hdr("%c%c %-*s" % (current, published, width, name))
|
# A branch name can contain a percent sign, so we need to escape it.
|
||||||
|
# Escape after f-string formatting to properly account for leading
|
||||||
|
# spaces.
|
||||||
|
hdr(f"{current}{published} {name:{width}}".replace("%", "%%"))
|
||||||
out.write(" |")
|
out.write(" |")
|
||||||
|
|
||||||
_RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
|
_RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
|
||||||
|
@ -233,9 +233,9 @@ synced and their revisions won't be found.
|
|||||||
)
|
)
|
||||||
self.printRevision = self.out.nofmt_printer("revision", fg="yellow")
|
self.printRevision = self.out.nofmt_printer("revision", fg="yellow")
|
||||||
else:
|
else:
|
||||||
self.printProject = (
|
self.printProject = self.printAdded = self.printRemoved = (
|
||||||
self.printAdded
|
self.printRevision
|
||||||
) = self.printRemoved = self.printRevision = self.printText
|
) = self.printText
|
||||||
|
|
||||||
manifest1 = RepoClient(self.repodir)
|
manifest1 = RepoClient(self.repodir)
|
||||||
manifest1.Override(args[0], load_local_manifests=False)
|
manifest1.Override(args[0], load_local_manifests=False)
|
||||||
|
@ -298,7 +298,7 @@ without iterating through the remaining projects.
|
|||||||
)
|
)
|
||||||
except (KeyboardInterrupt, WorkerKeyboardInterrupt):
|
except (KeyboardInterrupt, WorkerKeyboardInterrupt):
|
||||||
# Catch KeyboardInterrupt raised inside and outside of workers
|
# Catch KeyboardInterrupt raised inside and outside of workers
|
||||||
rc = rc or errno.EINTR
|
rc = errno.EINTR
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Catch any other exceptions raised
|
# Catch any other exceptions raised
|
||||||
logger.error(
|
logger.error(
|
||||||
@ -306,7 +306,7 @@ without iterating through the remaining projects.
|
|||||||
type(e).__name__,
|
type(e).__name__,
|
||||||
e,
|
e,
|
||||||
)
|
)
|
||||||
rc = rc or getattr(e, "errno", 1)
|
rc = getattr(e, "errno", 1)
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
sys.exit(rc)
|
sys.exit(rc)
|
||||||
|
|
||||||
|
294
subcmds/gc.py
Normal file
294
subcmds/gc.py
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
# Copyright (C) 2024 The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import List, Set
|
||||||
|
|
||||||
|
from command import Command
|
||||||
|
from git_command import GitCommand
|
||||||
|
import platform_utils
|
||||||
|
from progress import Progress
|
||||||
|
from project import Project
|
||||||
|
|
||||||
|
|
||||||
|
class Gc(Command):
|
||||||
|
COMMON = True
|
||||||
|
helpSummary = "Cleaning up internal repo and Git state."
|
||||||
|
helpUsage = """
|
||||||
|
%prog
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _Options(self, p):
|
||||||
|
p.add_option(
|
||||||
|
"-n",
|
||||||
|
"--dry-run",
|
||||||
|
dest="dryrun",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="do everything except actually delete",
|
||||||
|
)
|
||||||
|
p.add_option(
|
||||||
|
"-y",
|
||||||
|
"--yes",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="answer yes to all safe prompts",
|
||||||
|
)
|
||||||
|
p.add_option(
|
||||||
|
"--repack",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="repack all projects that use partial clone with "
|
||||||
|
"filter=blob:none",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _find_git_to_delete(
|
||||||
|
self, to_keep: Set[str], start_dir: str
|
||||||
|
) -> Set[str]:
|
||||||
|
"""Searches no longer needed ".git" directories.
|
||||||
|
|
||||||
|
Scans the file system starting from `start_dir` and removes all
|
||||||
|
directories that end with ".git" that are not in the `to_keep` set.
|
||||||
|
"""
|
||||||
|
to_delete = set()
|
||||||
|
for root, dirs, _ in platform_utils.walk(start_dir):
|
||||||
|
for directory in dirs:
|
||||||
|
if not directory.endswith(".git"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
path = os.path.join(root, directory)
|
||||||
|
if path not in to_keep:
|
||||||
|
to_delete.add(path)
|
||||||
|
|
||||||
|
return to_delete
|
||||||
|
|
||||||
|
def delete_unused_projects(self, projects: List[Project], opt):
|
||||||
|
print(f"Scanning filesystem under {self.repodir}...")
|
||||||
|
|
||||||
|
project_paths = set()
|
||||||
|
project_object_paths = set()
|
||||||
|
|
||||||
|
for project in projects:
|
||||||
|
project_paths.add(project.gitdir)
|
||||||
|
project_object_paths.add(project.objdir)
|
||||||
|
|
||||||
|
to_delete = self._find_git_to_delete(
|
||||||
|
project_paths, os.path.join(self.repodir, "projects")
|
||||||
|
)
|
||||||
|
|
||||||
|
to_delete.update(
|
||||||
|
self._find_git_to_delete(
|
||||||
|
project_object_paths,
|
||||||
|
os.path.join(self.repodir, "project-objects"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not to_delete:
|
||||||
|
print("Nothing to clean up.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print("Identified the following projects are no longer used:")
|
||||||
|
print("\n".join(to_delete))
|
||||||
|
print("")
|
||||||
|
if not opt.yes:
|
||||||
|
print(
|
||||||
|
"If you proceed, any local commits in those projects will be "
|
||||||
|
"destroyed!"
|
||||||
|
)
|
||||||
|
ask = input("Proceed? [y/N] ")
|
||||||
|
if ask.lower() != "y":
|
||||||
|
return 1
|
||||||
|
|
||||||
|
pm = Progress(
|
||||||
|
"Deleting",
|
||||||
|
len(to_delete),
|
||||||
|
delay=False,
|
||||||
|
quiet=opt.quiet,
|
||||||
|
show_elapsed=True,
|
||||||
|
elide=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
for path in to_delete:
|
||||||
|
if opt.dryrun:
|
||||||
|
print(f"\nWould have deleted ${path}")
|
||||||
|
else:
|
||||||
|
tmp_path = os.path.join(
|
||||||
|
os.path.dirname(path),
|
||||||
|
f"to_be_deleted_{os.path.basename(path)}",
|
||||||
|
)
|
||||||
|
platform_utils.rename(path, tmp_path)
|
||||||
|
platform_utils.rmtree(tmp_path)
|
||||||
|
pm.update(msg=path)
|
||||||
|
pm.end()
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _generate_promisor_files(self, pack_dir: str):
|
||||||
|
"""Generates promisor files for all pack files in the given directory.
|
||||||
|
|
||||||
|
Promisor files are empty files with the same name as the corresponding
|
||||||
|
pack file but with the ".promisor" extension. They are used by Git.
|
||||||
|
"""
|
||||||
|
for root, _, files in platform_utils.walk(pack_dir):
|
||||||
|
for file in files:
|
||||||
|
if not file.endswith(".pack"):
|
||||||
|
continue
|
||||||
|
with open(os.path.join(root, f"{file[:-4]}promisor"), "w"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def repack_projects(self, projects: List[Project], opt):
|
||||||
|
repack_projects = []
|
||||||
|
# Find all projects eligible for repacking:
|
||||||
|
# - can't be shared
|
||||||
|
# - have a specific fetch filter
|
||||||
|
for project in projects:
|
||||||
|
if project.config.GetBoolean("extensions.preciousObjects"):
|
||||||
|
continue
|
||||||
|
if not project.clone_depth:
|
||||||
|
continue
|
||||||
|
if project.manifest.CloneFilterForDepth != "blob:none":
|
||||||
|
continue
|
||||||
|
|
||||||
|
repack_projects.append(project)
|
||||||
|
|
||||||
|
if opt.dryrun:
|
||||||
|
print(f"Would have repacked {len(repack_projects)} projects.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
pm = Progress(
|
||||||
|
"Repacking (this will take a while)",
|
||||||
|
len(repack_projects),
|
||||||
|
delay=False,
|
||||||
|
quiet=opt.quiet,
|
||||||
|
show_elapsed=True,
|
||||||
|
elide=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
for project in repack_projects:
|
||||||
|
pm.update(msg=f"{project.name}")
|
||||||
|
|
||||||
|
pack_dir = os.path.join(project.gitdir, "tmp_repo_repack")
|
||||||
|
if os.path.isdir(pack_dir):
|
||||||
|
platform_utils.rmtree(pack_dir)
|
||||||
|
os.mkdir(pack_dir)
|
||||||
|
|
||||||
|
# Prepare workspace for repacking - remove all unreachable refs and
|
||||||
|
# their objects.
|
||||||
|
GitCommand(
|
||||||
|
project,
|
||||||
|
["reflog", "expire", "--expire-unreachable=all"],
|
||||||
|
verify_command=True,
|
||||||
|
).Wait()
|
||||||
|
pm.update(msg=f"{project.name} | gc", inc=0)
|
||||||
|
GitCommand(
|
||||||
|
project,
|
||||||
|
["gc"],
|
||||||
|
verify_command=True,
|
||||||
|
).Wait()
|
||||||
|
|
||||||
|
# Get all objects that are reachable from the remote, and pack them.
|
||||||
|
pm.update(msg=f"{project.name} | generating list of objects", inc=0)
|
||||||
|
remote_objects_cmd = GitCommand(
|
||||||
|
project,
|
||||||
|
[
|
||||||
|
"rev-list",
|
||||||
|
"--objects",
|
||||||
|
f"--remotes={project.remote.name}",
|
||||||
|
"--filter=blob:none",
|
||||||
|
"--tags",
|
||||||
|
],
|
||||||
|
capture_stdout=True,
|
||||||
|
verify_command=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get all local objects and pack them.
|
||||||
|
local_head_objects_cmd = GitCommand(
|
||||||
|
project,
|
||||||
|
["rev-list", "--objects", "HEAD^{tree}"],
|
||||||
|
capture_stdout=True,
|
||||||
|
verify_command=True,
|
||||||
|
)
|
||||||
|
local_objects_cmd = GitCommand(
|
||||||
|
project,
|
||||||
|
[
|
||||||
|
"rev-list",
|
||||||
|
"--objects",
|
||||||
|
"--all",
|
||||||
|
"--reflog",
|
||||||
|
"--indexed-objects",
|
||||||
|
"--not",
|
||||||
|
f"--remotes={project.remote.name}",
|
||||||
|
"--tags",
|
||||||
|
],
|
||||||
|
capture_stdout=True,
|
||||||
|
verify_command=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
remote_objects_cmd.Wait()
|
||||||
|
|
||||||
|
pm.update(msg=f"{project.name} | remote repack", inc=0)
|
||||||
|
GitCommand(
|
||||||
|
project,
|
||||||
|
["pack-objects", os.path.join(pack_dir, "pack")],
|
||||||
|
input=remote_objects_cmd.stdout,
|
||||||
|
capture_stderr=True,
|
||||||
|
capture_stdout=True,
|
||||||
|
verify_command=True,
|
||||||
|
).Wait()
|
||||||
|
|
||||||
|
# create promisor file for each pack file
|
||||||
|
self._generate_promisor_files(pack_dir)
|
||||||
|
|
||||||
|
local_head_objects_cmd.Wait()
|
||||||
|
local_objects_cmd.Wait()
|
||||||
|
|
||||||
|
pm.update(msg=f"{project.name} | local repack", inc=0)
|
||||||
|
GitCommand(
|
||||||
|
project,
|
||||||
|
["pack-objects", os.path.join(pack_dir, "pack")],
|
||||||
|
input=local_head_objects_cmd.stdout + local_objects_cmd.stdout,
|
||||||
|
capture_stderr=True,
|
||||||
|
capture_stdout=True,
|
||||||
|
verify_command=True,
|
||||||
|
).Wait()
|
||||||
|
|
||||||
|
# Swap the old pack directory with the new one.
|
||||||
|
platform_utils.rename(
|
||||||
|
os.path.join(project.objdir, "objects", "pack"),
|
||||||
|
os.path.join(project.objdir, "objects", "pack_old"),
|
||||||
|
)
|
||||||
|
platform_utils.rename(
|
||||||
|
pack_dir,
|
||||||
|
os.path.join(project.objdir, "objects", "pack"),
|
||||||
|
)
|
||||||
|
platform_utils.rmtree(
|
||||||
|
os.path.join(project.objdir, "objects", "pack_old")
|
||||||
|
)
|
||||||
|
|
||||||
|
pm.end()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def Execute(self, opt, args):
|
||||||
|
projects: List[Project] = self.GetProjects(
|
||||||
|
args, all_manifests=not opt.this_manifest_only
|
||||||
|
)
|
||||||
|
|
||||||
|
ret = self.delete_unused_projects(projects, opt)
|
||||||
|
if ret != 0:
|
||||||
|
return ret
|
||||||
|
|
||||||
|
if not opt.repack:
|
||||||
|
return
|
||||||
|
|
||||||
|
return self.repack_projects(projects, opt)
|
@ -350,6 +350,8 @@ later is required to fix a server side protocol bug.
|
|||||||
# value later on.
|
# value later on.
|
||||||
PARALLEL_JOBS = 0
|
PARALLEL_JOBS = 0
|
||||||
|
|
||||||
|
_JOBS_WARN_THRESHOLD = 100
|
||||||
|
|
||||||
def _Options(self, p, show_smart=True):
|
def _Options(self, p, show_smart=True):
|
||||||
p.add_option(
|
p.add_option(
|
||||||
"--jobs-network",
|
"--jobs-network",
|
||||||
@ -821,6 +823,16 @@ later is required to fix a server side protocol bug.
|
|||||||
jobs = jobs_str(len(items))
|
jobs = jobs_str(len(items))
|
||||||
return f"{jobs} | {elapsed_str(elapsed)} {earliest_proj}"
|
return f"{jobs} | {elapsed_str(elapsed)} {earliest_proj}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def InitWorker(cls):
|
||||||
|
# Force connect to the manager server now.
|
||||||
|
# This is good because workers are initialized one by one. Without this,
|
||||||
|
# multiple workers may connect to the manager when handling the first
|
||||||
|
# job at the same time. Then the connection may fail if too many
|
||||||
|
# connections are pending and execeeded the socket listening backlog,
|
||||||
|
# especially on MacOS.
|
||||||
|
len(cls.get_parallel_context()["sync_dict"])
|
||||||
|
|
||||||
def _Fetch(self, projects, opt, err_event, ssh_proxy, errors):
|
def _Fetch(self, projects, opt, err_event, ssh_proxy, errors):
|
||||||
ret = True
|
ret = True
|
||||||
|
|
||||||
@ -891,7 +903,7 @@ later is required to fix a server side protocol bug.
|
|||||||
objdir_project_map.setdefault(project.objdir, []).append(index)
|
objdir_project_map.setdefault(project.objdir, []).append(index)
|
||||||
projects_list = list(objdir_project_map.values())
|
projects_list = list(objdir_project_map.values())
|
||||||
|
|
||||||
jobs = min(opt.jobs_network, len(projects_list))
|
jobs = max(1, min(opt.jobs_network, len(projects_list)))
|
||||||
|
|
||||||
# We pass the ssh proxy settings via the class. This allows
|
# We pass the ssh proxy settings via the class. This allows
|
||||||
# multiprocessing to pickle it up when spawning children. We can't
|
# multiprocessing to pickle it up when spawning children. We can't
|
||||||
@ -913,6 +925,7 @@ later is required to fix a server side protocol bug.
|
|||||||
# idle while other workers still have more than one job in
|
# idle while other workers still have more than one job in
|
||||||
# their chunk queue.
|
# their chunk queue.
|
||||||
chunksize=1,
|
chunksize=1,
|
||||||
|
initializer=self.InitWorker,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
sync_event.set()
|
sync_event.set()
|
||||||
@ -1047,6 +1060,8 @@ later is required to fix a server side protocol bug.
|
|||||||
verbose=verbose,
|
verbose=verbose,
|
||||||
)
|
)
|
||||||
success = syncbuf.Finish()
|
success = syncbuf.Finish()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.error("Keyboard interrupt while processing %s", project.name)
|
||||||
except GitError as e:
|
except GitError as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
"error.GitError: Cannot checkout %s: %s", project.name, e
|
"error.GitError: Cannot checkout %s: %s", project.name, e
|
||||||
@ -1431,7 +1446,10 @@ later is required to fix a server side protocol bug.
|
|||||||
for need_remove_file in need_remove_files:
|
for need_remove_file in need_remove_files:
|
||||||
# Try to remove the updated copyfile or linkfile.
|
# Try to remove the updated copyfile or linkfile.
|
||||||
# So, if the file is not exist, nothing need to do.
|
# So, if the file is not exist, nothing need to do.
|
||||||
platform_utils.remove(need_remove_file, missing_ok=True)
|
platform_utils.remove(
|
||||||
|
os.path.join(self.client.topdir, need_remove_file),
|
||||||
|
missing_ok=True,
|
||||||
|
)
|
||||||
|
|
||||||
# Create copy-link-files.json, save dest path of "copyfile" and
|
# Create copy-link-files.json, save dest path of "copyfile" and
|
||||||
# "linkfile".
|
# "linkfile".
|
||||||
@ -1486,6 +1504,7 @@ later is required to fix a server side protocol bug.
|
|||||||
if manifest_server.startswith("persistent-"):
|
if manifest_server.startswith("persistent-"):
|
||||||
manifest_server = manifest_server[len("persistent-") :]
|
manifest_server = manifest_server[len("persistent-") :]
|
||||||
|
|
||||||
|
# Changes in behavior should update docs/smart-sync.md accordingly.
|
||||||
try:
|
try:
|
||||||
server = xmlrpc.client.Server(manifest_server, transport=transport)
|
server = xmlrpc.client.Server(manifest_server, transport=transport)
|
||||||
if opt.smart_sync:
|
if opt.smart_sync:
|
||||||
@ -1711,6 +1730,24 @@ later is required to fix a server side protocol bug.
|
|||||||
opt.jobs_network = min(opt.jobs_network, jobs_soft_limit)
|
opt.jobs_network = min(opt.jobs_network, jobs_soft_limit)
|
||||||
opt.jobs_checkout = min(opt.jobs_checkout, 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):
|
def Execute(self, opt, args):
|
||||||
errors = []
|
errors = []
|
||||||
try:
|
try:
|
||||||
@ -1982,6 +2019,8 @@ def _PostRepoFetch(rp, repo_verify=True, verbose=False):
|
|||||||
# We also have to make sure this will switch to an older commit if
|
# We also have to make sure this will switch to an older commit if
|
||||||
# that's the latest tag in order to support release rollback.
|
# that's the latest tag in order to support release rollback.
|
||||||
try:
|
try:
|
||||||
|
# Refresh index since reset --keep won't do it.
|
||||||
|
rp.work_git.update_index("-q", "--refresh")
|
||||||
rp.work_git.reset("--keep", new_rev)
|
rp.work_git.reset("--keep", new_rev)
|
||||||
except GitError as e:
|
except GitError as e:
|
||||||
raise RepoUnhandledExceptionError(e)
|
raise RepoUnhandledExceptionError(e)
|
||||||
|
@ -737,8 +737,8 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
pending = []
|
pending = []
|
||||||
for result in results:
|
for result in results:
|
||||||
project_idx, avail = result
|
project_idx, avail = result
|
||||||
if avail is None:
|
|
||||||
project = projects[project_idx]
|
project = projects[project_idx]
|
||||||
|
if avail is None:
|
||||||
logger.error(
|
logger.error(
|
||||||
'repo: error: %s: Unable to upload branch "%s". '
|
'repo: error: %s: Unable to upload branch "%s". '
|
||||||
"You might be able to fix the branch by running:\n"
|
"You might be able to fix the branch by running:\n"
|
||||||
@ -748,7 +748,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
|
|||||||
project.manifest.branch,
|
project.manifest.branch,
|
||||||
)
|
)
|
||||||
elif avail:
|
elif avail:
|
||||||
pending.append(result)
|
pending.append((project, avail))
|
||||||
return pending
|
return pending
|
||||||
|
|
||||||
with self.ParallelContext():
|
with self.ParallelContext():
|
||||||
|
1
tests/fixtures/gitc_config
vendored
1
tests/fixtures/gitc_config
vendored
@ -1 +0,0 @@
|
|||||||
gitc_dir=/test/usr/local/google/gitc
|
|
@ -21,6 +21,8 @@ import subprocess
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
import git_command
|
import git_command
|
||||||
import wrapper
|
import wrapper
|
||||||
|
|
||||||
@ -263,6 +265,7 @@ class UserAgentUnitTest(unittest.TestCase):
|
|||||||
m = re.match(r"^[^ ]+$", os_name)
|
m = re.match(r"^[^ ]+$", os_name)
|
||||||
self.assertIsNotNone(m)
|
self.assertIsNotNone(m)
|
||||||
|
|
||||||
|
@pytest.mark.skip_cq("TODO(b/266734831): Find out why this fails in CQ")
|
||||||
def test_smoke_repo(self):
|
def test_smoke_repo(self):
|
||||||
"""Make sure repo UA returns something useful."""
|
"""Make sure repo UA returns something useful."""
|
||||||
ua = git_command.user_agent.repo
|
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)
|
m = re.match(r"^git-repo/[^ ]+ ([^ ]+) git/[^ ]+ Python/[0-9.]+", ua)
|
||||||
self.assertIsNotNone(m)
|
self.assertIsNotNone(m)
|
||||||
|
|
||||||
|
@pytest.mark.skip_cq("TODO(b/266734831): Find out why this fails in CQ")
|
||||||
def test_smoke_git(self):
|
def test_smoke_git(self):
|
||||||
"""Make sure git UA returns something useful."""
|
"""Make sure git UA returns something useful."""
|
||||||
ua = git_command.user_agent.git
|
ua = git_command.user_agent.git
|
||||||
|
@ -21,6 +21,7 @@ import tempfile
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
from test_manifest_xml import sort_attributes
|
from test_manifest_xml import sort_attributes
|
||||||
|
|
||||||
import git_superproject
|
import git_superproject
|
||||||
@ -145,6 +146,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertIsNone(manifest.superproject)
|
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):
|
def test_superproject_get_superproject_invalid_url(self):
|
||||||
"""Test with an invalid url."""
|
"""Test with an invalid url."""
|
||||||
manifest = self.getXmlManifest(
|
manifest = self.getXmlManifest(
|
||||||
@ -168,6 +170,7 @@ class SuperprojectTestCase(unittest.TestCase):
|
|||||||
self.assertFalse(sync_result.success)
|
self.assertFalse(sync_result.success)
|
||||||
self.assertTrue(sync_result.fatal)
|
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):
|
def test_superproject_get_superproject_invalid_branch(self):
|
||||||
"""Test with an invalid branch."""
|
"""Test with an invalid branch."""
|
||||||
manifest = self.getXmlManifest(
|
manifest = self.getXmlManifest(
|
||||||
|
@ -150,7 +150,7 @@ class EventLogTestCase(unittest.TestCase):
|
|||||||
<version event>
|
<version event>
|
||||||
<start event>
|
<start event>
|
||||||
"""
|
"""
|
||||||
self._event_log_module.StartEvent()
|
self._event_log_module.StartEvent([])
|
||||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||||
log_path = self._event_log_module.Write(path=tempdir)
|
log_path = self._event_log_module.Write(path=tempdir)
|
||||||
self._log_data = self.readLog(log_path)
|
self._log_data = self.readLog(log_path)
|
||||||
@ -213,10 +213,8 @@ class EventLogTestCase(unittest.TestCase):
|
|||||||
<version event>
|
<version event>
|
||||||
<command event>
|
<command event>
|
||||||
"""
|
"""
|
||||||
name = "repo"
|
|
||||||
subcommands = ["init" "this"]
|
|
||||||
self._event_log_module.CommandEvent(
|
self._event_log_module.CommandEvent(
|
||||||
name="repo", subcommands=subcommands
|
name="repo", subcommands=["init", "this"]
|
||||||
)
|
)
|
||||||
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
|
||||||
log_path = self._event_log_module.Write(path=tempdir)
|
log_path = self._event_log_module.Write(path=tempdir)
|
||||||
@ -225,12 +223,10 @@ class EventLogTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(len(self._log_data), 2)
|
self.assertEqual(len(self._log_data), 2)
|
||||||
command_event = self._log_data[1]
|
command_event = self._log_data[1]
|
||||||
self.verifyCommonKeys(self._log_data[0], expected_event_name="version")
|
self.verifyCommonKeys(self._log_data[0], expected_event_name="version")
|
||||||
self.verifyCommonKeys(command_event, expected_event_name="command")
|
self.verifyCommonKeys(command_event, expected_event_name="cmd_name")
|
||||||
# Check for 'command' event specific fields.
|
# Check for 'command' event specific fields.
|
||||||
self.assertIn("name", command_event)
|
self.assertIn("name", command_event)
|
||||||
self.assertIn("subcommands", command_event)
|
self.assertEqual(command_event["name"], "repo-init-this")
|
||||||
self.assertEqual(command_event["name"], name)
|
|
||||||
self.assertEqual(command_event["subcommands"], subcommands)
|
|
||||||
|
|
||||||
def test_def_params_event_repo_config(self):
|
def test_def_params_event_repo_config(self):
|
||||||
"""Test 'def_params' event data outputs only repo config keys.
|
"""Test 'def_params' event data outputs only repo config keys.
|
||||||
@ -382,17 +378,17 @@ class EventLogTestCase(unittest.TestCase):
|
|||||||
socket_path = os.path.join(tempdir, "server.sock")
|
socket_path = os.path.join(tempdir, "server.sock")
|
||||||
server_ready = threading.Condition()
|
server_ready = threading.Condition()
|
||||||
# Start "server" listening on Unix domain socket at socket_path.
|
# Start "server" listening on Unix domain socket at socket_path.
|
||||||
try:
|
|
||||||
server_thread = threading.Thread(
|
server_thread = threading.Thread(
|
||||||
target=serverLoggingThread,
|
target=serverLoggingThread,
|
||||||
args=(socket_path, server_ready, received_traces),
|
args=(socket_path, server_ready, received_traces),
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
server_thread.start()
|
server_thread.start()
|
||||||
|
|
||||||
with server_ready:
|
with server_ready:
|
||||||
server_ready.wait(timeout=120)
|
server_ready.wait(timeout=120)
|
||||||
|
|
||||||
self._event_log_module.StartEvent()
|
self._event_log_module.StartEvent([])
|
||||||
path = self._event_log_module.Write(
|
path = self._event_log_module.Write(
|
||||||
path=f"af_unix:{socket_path}"
|
path=f"af_unix:{socket_path}"
|
||||||
)
|
)
|
||||||
|
@ -51,7 +51,7 @@ INVALID_FS_PATHS = (
|
|||||||
"foo~",
|
"foo~",
|
||||||
"blah/foo~",
|
"blah/foo~",
|
||||||
# Block Unicode characters that get normalized out by filesystems.
|
# Block Unicode characters that get normalized out by filesystems.
|
||||||
"foo\u200Cbar",
|
"foo\u200cbar",
|
||||||
# Block newlines.
|
# Block newlines.
|
||||||
"f\n/bar",
|
"f\n/bar",
|
||||||
"f\r/bar",
|
"f\r/bar",
|
||||||
|
156
tests/test_subcmds_forall.py
Normal file
156
tests/test_subcmds_forall.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
# Copyright (C) 2024 The Android Open Source Project
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
"""Unittests for the forall subcmd."""
|
||||||
|
|
||||||
|
from io import StringIO
|
||||||
|
import os
|
||||||
|
from shutil import rmtree
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import git_command
|
||||||
|
import manifest_xml
|
||||||
|
import project
|
||||||
|
import subcmds
|
||||||
|
|
||||||
|
|
||||||
|
class AllCommands(unittest.TestCase):
|
||||||
|
"""Check registered all_commands."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Common setup."""
|
||||||
|
self.tempdirobj = tempfile.TemporaryDirectory(prefix="forall_tests")
|
||||||
|
self.tempdir = self.tempdirobj.name
|
||||||
|
self.repodir = os.path.join(self.tempdir, ".repo")
|
||||||
|
self.manifest_dir = os.path.join(self.repodir, "manifests")
|
||||||
|
self.manifest_file = os.path.join(
|
||||||
|
self.repodir, manifest_xml.MANIFEST_FILE_NAME
|
||||||
|
)
|
||||||
|
self.local_manifest_dir = os.path.join(
|
||||||
|
self.repodir, manifest_xml.LOCAL_MANIFESTS_DIR_NAME
|
||||||
|
)
|
||||||
|
os.mkdir(self.repodir)
|
||||||
|
os.mkdir(self.manifest_dir)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Common teardown."""
|
||||||
|
rmtree(self.tempdir, ignore_errors=True)
|
||||||
|
|
||||||
|
def initTempGitTree(self, git_dir):
|
||||||
|
"""Create a new empty git checkout for testing."""
|
||||||
|
|
||||||
|
# Tests need to assume, that main is default branch at init,
|
||||||
|
# which is not supported in config until 2.28.
|
||||||
|
cmd = ["git", "init", "-q"]
|
||||||
|
if git_command.git_require((2, 28, 0)):
|
||||||
|
cmd += ["--initial-branch=main"]
|
||||||
|
else:
|
||||||
|
# Use template dir for init
|
||||||
|
templatedir = os.path.join(self.tempdirobj.name, ".test-template")
|
||||||
|
os.makedirs(templatedir)
|
||||||
|
with open(os.path.join(templatedir, "HEAD"), "w") as fp:
|
||||||
|
fp.write("ref: refs/heads/main\n")
|
||||||
|
cmd += ["--template", templatedir]
|
||||||
|
cmd += [git_dir]
|
||||||
|
subprocess.check_call(cmd)
|
||||||
|
|
||||||
|
def getXmlManifestWith8Projects(self):
|
||||||
|
"""Create and return a setup of 8 projects with enough dummy
|
||||||
|
files and setup to execute forall."""
|
||||||
|
|
||||||
|
# Set up a manifest git dir for parsing to work
|
||||||
|
gitdir = os.path.join(self.repodir, "manifests.git")
|
||||||
|
os.mkdir(gitdir)
|
||||||
|
with open(os.path.join(gitdir, "config"), "w") as fp:
|
||||||
|
fp.write(
|
||||||
|
"""[remote "origin"]
|
||||||
|
url = https://localhost:0/manifest
|
||||||
|
verbose = false
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add the manifest data
|
||||||
|
manifest_data = """
|
||||||
|
<manifest>
|
||||||
|
<remote name="origin" fetch="http://localhost" />
|
||||||
|
<default remote="origin" revision="refs/heads/main" />
|
||||||
|
<project name="project1" path="tests/path1" />
|
||||||
|
<project name="project2" path="tests/path2" />
|
||||||
|
<project name="project3" path="tests/path3" />
|
||||||
|
<project name="project4" path="tests/path4" />
|
||||||
|
<project name="project5" path="tests/path5" />
|
||||||
|
<project name="project6" path="tests/path6" />
|
||||||
|
<project name="project7" path="tests/path7" />
|
||||||
|
<project name="project8" path="tests/path8" />
|
||||||
|
</manifest>
|
||||||
|
"""
|
||||||
|
with open(self.manifest_file, "w", encoding="utf-8") as fp:
|
||||||
|
fp.write(manifest_data)
|
||||||
|
|
||||||
|
# Set up 8 empty projects to match the manifest
|
||||||
|
for x in range(1, 9):
|
||||||
|
os.makedirs(
|
||||||
|
os.path.join(
|
||||||
|
self.repodir, "projects/tests/path" + str(x) + ".git"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
os.makedirs(
|
||||||
|
os.path.join(
|
||||||
|
self.repodir, "project-objects/project" + str(x) + ".git"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
git_path = os.path.join(self.tempdir, "tests/path" + str(x))
|
||||||
|
self.initTempGitTree(git_path)
|
||||||
|
|
||||||
|
return manifest_xml.XmlManifest(self.repodir, self.manifest_file)
|
||||||
|
|
||||||
|
# Use mock to capture stdout from the forall run
|
||||||
|
@unittest.mock.patch("sys.stdout", new_callable=StringIO)
|
||||||
|
def test_forall_all_projects_called_once(self, mock_stdout):
|
||||||
|
"""Test that all projects get a command run once each."""
|
||||||
|
|
||||||
|
manifest_with_8_projects = self.getXmlManifestWith8Projects()
|
||||||
|
|
||||||
|
cmd = subcmds.forall.Forall()
|
||||||
|
cmd.manifest = manifest_with_8_projects
|
||||||
|
|
||||||
|
# Use echo project names as the test of forall
|
||||||
|
opts, args = cmd.OptionParser.parse_args(["-c", "echo $REPO_PROJECT"])
|
||||||
|
opts.verbose = False
|
||||||
|
|
||||||
|
# Mock to not have the Execute fail on remote check
|
||||||
|
with mock.patch.object(
|
||||||
|
project.Project, "GetRevisionId", return_value="refs/heads/main"
|
||||||
|
):
|
||||||
|
# Run the forall command
|
||||||
|
cmd.Execute(opts, args)
|
||||||
|
|
||||||
|
# Verify that we got every project name in the prints
|
||||||
|
for x in range(1, 9):
|
||||||
|
self.assertIn("project" + str(x), mock_stdout.getvalue())
|
||||||
|
|
||||||
|
# Split the captured output into lines to count them
|
||||||
|
line_count = 0
|
||||||
|
for line in mock_stdout.getvalue().split("\n"):
|
||||||
|
# A commented out print to stderr as a reminder
|
||||||
|
# that stdout is mocked, include sys and uncomment if needed
|
||||||
|
# print(line, file=sys.stderr)
|
||||||
|
if len(line) > 0:
|
||||||
|
line_count += 1
|
||||||
|
|
||||||
|
# Verify that we didn't get more lines than expected
|
||||||
|
assert line_count == 8
|
@ -17,6 +17,7 @@
|
|||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
@ -72,84 +73,11 @@ class RepoWrapperUnitTest(RepoWrapperTestCase):
|
|||||||
|
|
||||||
def test_init_parser(self):
|
def test_init_parser(self):
|
||||||
"""Make sure 'init' GetParser works."""
|
"""Make sure 'init' GetParser works."""
|
||||||
parser = self.wrapper.GetParser(gitc_init=False)
|
parser = self.wrapper.GetParser()
|
||||||
opts, args = parser.parse_args([])
|
opts, args = parser.parse_args([])
|
||||||
self.assertEqual([], args)
|
self.assertEqual([], args)
|
||||||
self.assertIsNone(opts.manifest_url)
|
self.assertIsNone(opts.manifest_url)
|
||||||
|
|
||||||
def test_gitc_init_parser(self):
|
|
||||||
"""Make sure 'gitc-init' GetParser raises."""
|
|
||||||
with self.assertRaises(SystemExit):
|
|
||||||
self.wrapper.GetParser(gitc_init=True)
|
|
||||||
|
|
||||||
def test_get_gitc_manifest_dir_no_gitc(self):
|
|
||||||
"""
|
|
||||||
Test reading a missing gitc config file
|
|
||||||
"""
|
|
||||||
self.wrapper.GITC_CONFIG_FILE = fixture("missing_gitc_config")
|
|
||||||
val = self.wrapper.get_gitc_manifest_dir()
|
|
||||||
self.assertEqual(val, "")
|
|
||||||
|
|
||||||
def test_get_gitc_manifest_dir(self):
|
|
||||||
"""
|
|
||||||
Test reading the gitc config file and parsing the directory
|
|
||||||
"""
|
|
||||||
self.wrapper.GITC_CONFIG_FILE = fixture("gitc_config")
|
|
||||||
val = self.wrapper.get_gitc_manifest_dir()
|
|
||||||
self.assertEqual(val, "/test/usr/local/google/gitc")
|
|
||||||
|
|
||||||
def test_gitc_parse_clientdir_no_gitc(self):
|
|
||||||
"""
|
|
||||||
Test parsing the gitc clientdir without gitc running
|
|
||||||
"""
|
|
||||||
self.wrapper.GITC_CONFIG_FILE = fixture("missing_gitc_config")
|
|
||||||
self.assertEqual(self.wrapper.gitc_parse_clientdir("/something"), None)
|
|
||||||
self.assertEqual(
|
|
||||||
self.wrapper.gitc_parse_clientdir("/gitc/manifest-rw/test"), "test"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_gitc_parse_clientdir(self):
|
|
||||||
"""
|
|
||||||
Test parsing the gitc clientdir
|
|
||||||
"""
|
|
||||||
self.wrapper.GITC_CONFIG_FILE = fixture("gitc_config")
|
|
||||||
self.assertEqual(self.wrapper.gitc_parse_clientdir("/something"), None)
|
|
||||||
self.assertEqual(
|
|
||||||
self.wrapper.gitc_parse_clientdir("/gitc/manifest-rw/test"), "test"
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.wrapper.gitc_parse_clientdir("/gitc/manifest-rw/test/"), "test"
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.wrapper.gitc_parse_clientdir("/gitc/manifest-rw/test/extra"),
|
|
||||||
"test",
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.wrapper.gitc_parse_clientdir(
|
|
||||||
"/test/usr/local/google/gitc/test"
|
|
||||||
),
|
|
||||||
"test",
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.wrapper.gitc_parse_clientdir(
|
|
||||||
"/test/usr/local/google/gitc/test/"
|
|
||||||
),
|
|
||||||
"test",
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.wrapper.gitc_parse_clientdir(
|
|
||||||
"/test/usr/local/google/gitc/test/extra"
|
|
||||||
),
|
|
||||||
"test",
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.wrapper.gitc_parse_clientdir("/gitc/manifest-rw/"), None
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.wrapper.gitc_parse_clientdir("/test/usr/local/google/gitc/"),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SetGitTrace2ParentSid(RepoWrapperTestCase):
|
class SetGitTrace2ParentSid(RepoWrapperTestCase):
|
||||||
"""Check SetGitTrace2ParentSid behavior."""
|
"""Check SetGitTrace2ParentSid behavior."""
|
||||||
@ -198,7 +126,7 @@ class RunCommand(RepoWrapperTestCase):
|
|||||||
self.wrapper.run_command(["true"], check=False)
|
self.wrapper.run_command(["true"], check=False)
|
||||||
self.wrapper.run_command(["true"], check=True)
|
self.wrapper.run_command(["true"], check=True)
|
||||||
self.wrapper.run_command(["false"], check=False)
|
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)
|
self.wrapper.run_command(["false"], check=True)
|
||||||
|
|
||||||
|
|
||||||
@ -431,8 +359,8 @@ class VerifyRev(RepoWrapperTestCase):
|
|||||||
|
|
||||||
def test_verify_passes(self):
|
def test_verify_passes(self):
|
||||||
"""Check when we have a valid signed tag."""
|
"""Check when we have a valid signed tag."""
|
||||||
desc_result = self.wrapper.RunResult(0, "v1.0\n", "")
|
desc_result = subprocess.CompletedProcess([], 0, "v1.0\n", "")
|
||||||
gpg_result = self.wrapper.RunResult(0, "", "")
|
gpg_result = subprocess.CompletedProcess([], 0, "", "")
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
self.wrapper, "run_git", side_effect=(desc_result, gpg_result)
|
self.wrapper, "run_git", side_effect=(desc_result, gpg_result)
|
||||||
):
|
):
|
||||||
@ -443,8 +371,8 @@ class VerifyRev(RepoWrapperTestCase):
|
|||||||
|
|
||||||
def test_unsigned_commit(self):
|
def test_unsigned_commit(self):
|
||||||
"""Check we fall back to signed tag when we have an unsigned commit."""
|
"""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 = self.wrapper.RunResult(0, "", "")
|
gpg_result = subprocess.CompletedProcess([], 0, "", "")
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
self.wrapper, "run_git", side_effect=(desc_result, gpg_result)
|
self.wrapper, "run_git", side_effect=(desc_result, gpg_result)
|
||||||
):
|
):
|
||||||
@ -455,7 +383,7 @@ class VerifyRev(RepoWrapperTestCase):
|
|||||||
|
|
||||||
def test_verify_fails(self):
|
def test_verify_fails(self):
|
||||||
"""Check we fall back to signed tag when we have an unsigned commit."""
|
"""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
|
gpg_result = Exception
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
self.wrapper, "run_git", side_effect=(desc_result, gpg_result)
|
self.wrapper, "run_git", side_effect=(desc_result, gpg_result)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user