mirror of
https://gerrit.googlesource.com/git-repo
synced 2025-06-28 20:17:26 +00:00
Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
8d5f032611 | |||
99eca45eb2 | |||
66685f07ec | |||
cf9a2a2a76 | |||
5ae8292fea | |||
dfdf577e98 | |||
747ec83f58 | |||
1711bc23c0 | |||
db111d3924 | |||
3405446a4e | |||
41a27eb854 | |||
d93fe60e89 | |||
61224d01fa | |||
13d6588bf6 |
@ -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)`.
|
@ -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
|
||||||
|
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
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
115
project.py
115
project.py
@ -642,6 +642,10 @@ class Project:
|
|||||||
# 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.
|
||||||
|
|
||||||
@ -1560,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)
|
||||||
@ -2347,6 +2356,8 @@ class Project:
|
|||||||
)
|
)
|
||||||
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):
|
||||||
@ -2997,6 +3008,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:
|
||||||
@ -3415,6 +3437,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)
|
||||||
@ -3425,34 +3452,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]
|
||||||
if GitCommand(self, cmd).Wait() != 0:
|
try:
|
||||||
raise GitError(
|
if GitCommand(self, cmd).Wait() != 0:
|
||||||
"Cannot initialize work tree for " + self.name,
|
raise GitError(
|
||||||
project=self.name,
|
"Cannot initialize work tree for " + 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.
|
||||||
@ -3541,6 +3610,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 (
|
||||||
|
@ -5,61 +5,55 @@
|
|||||||
# 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
|
|
||||||
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:23.1.0"
|
||||||
|
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)
|
@ -1058,6 +1058,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
|
||||||
@ -1500,6 +1502,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:
|
||||||
|
Reference in New Issue
Block a user