repo: Add support for standalone manifests
Added --standalone_manifest to repo tool. If set, the
manifest is downloaded directly from the appropriate source
(currently, we only support GS) and used instead of creating
a manifest git checkout. The manifests.git repo is still created to
keep track of various config but is marked as being for a standalone
manifest so that the repo tool doesn't try to run networked git
commands in it.
BUG=b:192664812
TEST=existing tests (no coverage), manual runs
Change-Id: I84378cbc7f8e515eabeccdde9665efc8cd2a9d21
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/312942
Tested-by: Jack Neus <jackneus@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
diff --git a/docs/internal-fs-layout.md b/docs/internal-fs-layout.md
index e3be173..af6a452 100644
--- a/docs/internal-fs-layout.md
+++ b/docs/internal-fs-layout.md
@@ -157,6 +157,7 @@
| Setting | `repo init` Option | Use/Meaning |
|------------------- |---------------------------|-------------|
| manifest.groups | `--groups` & `--platform` | The manifest groups to sync |
+| manifest.standalone | `--standalone-manifest` | Download manifest as static file instead of creating checkout |
| repo.archive | `--archive` | Use `git archive` for checkouts |
| repo.clonebundle | `--clone-bundle` | Whether the initial sync used clone.bundle explicitly |
| repo.clonefilter | `--clone-filter` | Filter setting when using [partial git clones] |
diff --git a/fetch.py b/fetch.py
new file mode 100644
index 0000000..5b9997a
--- /dev/null
+++ b/fetch.py
@@ -0,0 +1,38 @@
+# Copyright (C) 2021 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This module contains functions used to fetch files from various sources."""
+
+import subprocess
+import sys
+from urllib.parse import urlparse
+
+def fetch_file(url):
+ """Fetch a file from the specified source using the appropriate protocol.
+
+ Returns:
+ The contents of the file as bytes.
+ """
+ scheme = urlparse(url).scheme
+ if scheme == 'gs':
+ cmd = ['gsutil', 'cat', url]
+ try:
+ result = subprocess.run(
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ return result.stdout
+ except subprocess.CalledProcessError as e:
+ print('fatal: error running "gsutil": %s' % e.output,
+ file=sys.stderr)
+ sys.exit(1)
+ raise ValueError('unsupported url %s' % url)
diff --git a/git_config.py b/git_config.py
index d882239..778e81a 100644
--- a/git_config.py
+++ b/git_config.py
@@ -104,6 +104,10 @@
os.path.dirname(self.file),
'.repo_' + os.path.basename(self.file) + '.json')
+ def ClearCache(self):
+ """Clear the in-memory cache of config."""
+ self._cache_dict = None
+
def Has(self, name, include_defaults=True):
"""Return true if this configuration file has the key.
"""
@@ -399,7 +403,7 @@
if p.Wait() == 0:
return p.stdout
else:
- GitError('git config %s: %s' % (str(args), p.stderr))
+ raise GitError('git config %s: %s' % (str(args), p.stderr))
class RepoConfig(GitConfig):
diff --git a/man/repo-gitc-init.1 b/man/repo-gitc-init.1
index 1d1b23a..9b61866 100644
--- a/man/repo-gitc-init.1
+++ b/man/repo-gitc-init.1
@@ -1,5 +1,5 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
-.TH REPO "1" "July 2021" "repo gitc-init" "Repo Manual"
+.TH REPO "1" "September 2021" "repo gitc-init" "Repo Manual"
.SH NAME
repo \- repo gitc-init - manual page for repo gitc-init
.SH SYNOPSIS
@@ -31,6 +31,10 @@
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
initial manifest file
.TP
+\fB\-\-standalone\-manifest\fR
+download the manifest as a static file rather then
+create a git checkout of the manifest repo
+.TP
\fB\-g\fR GROUP, \fB\-\-groups\fR=\fI\,GROUP\/\fR
restrict manifest projects to ones with specified
group(s) [default|all|G1,G2,G3|G4,\-G5,\-G6]
diff --git a/man/repo-init.1 b/man/repo-init.1
index e860f95..9957b64 100644
--- a/man/repo-init.1
+++ b/man/repo-init.1
@@ -1,5 +1,5 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man.
-.TH REPO "1" "July 2021" "repo init" "Repo Manual"
+.TH REPO "1" "September 2021" "repo init" "Repo Manual"
.SH NAME
repo \- repo init - manual page for repo init
.SH SYNOPSIS
@@ -31,6 +31,10 @@
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
initial manifest file
.TP
+\fB\-\-standalone\-manifest\fR
+download the manifest as a static file rather then
+create a git checkout of the manifest repo
+.TP
\fB\-g\fR GROUP, \fB\-\-groups\fR=\fI\,GROUP\/\fR
restrict manifest projects to ones with specified
group(s) [default|all|G1,G2,G3|G4,\-G5,\-G6]
@@ -137,6 +141,12 @@
The optional \fB\-m\fR argument can be used to specify an alternate manifest to be
used. If no manifest is specified, the manifest default.xml will be used.
.PP
+If the \fB\-\-standalone\-manifest\fR argument is set, the manifest will be downloaded
+directly from the specified \fB\-\-manifest\-url\fR as a static file (rather than setting
+up a manifest git checkout). With \fB\-\-standalone\-manifest\fR, the manifest will be
+fully static and will not be re\-downloaded during subsesquent `repo init` and
+`repo sync` calls.
+.PP
The \fB\-\-reference\fR option can be used to point to a directory that has the content
of a \fB\-\-mirror\fR sync. This will make the working directory use as much data as
possible from the local reference directory when fetching from the server. This
diff --git a/repo b/repo
index 3b244c1..f61639f 100755
--- a/repo
+++ b/repo
@@ -312,6 +312,10 @@
metavar='PLATFORM')
group.add_option('--submodules', action='store_true',
help='sync any submodules associated with the manifest repo')
+ group.add_option('--standalone-manifest', action='store_true',
+ help='download the manifest as a static file '
+ 'rather then create a git checkout of '
+ 'the manifest repo')
# Options that only affect manifest project, and not any of the projects
# specified in the manifest itself.
diff --git a/subcmds/init.py b/subcmds/init.py
index 5671fc2..9c6b2ad 100644
--- a/subcmds/init.py
+++ b/subcmds/init.py
@@ -15,6 +15,7 @@
import os
import platform
import re
+import subprocess
import sys
import urllib.parse
@@ -24,6 +25,7 @@
from project import SyncBuffer
from git_config import GitConfig
from git_command import git_require, MIN_GIT_VERSION_SOFT, MIN_GIT_VERSION_HARD
+import fetch
import git_superproject
import platform_utils
from wrapper import Wrapper
@@ -53,6 +55,12 @@
to be used. If no manifest is specified, the manifest default.xml
will be used.
+If the --standalone-manifest argument is set, the manifest will be downloaded
+directly from the specified --manifest-url as a static file (rather than
+setting up a manifest git checkout). With --standalone-manifest, the manifest
+will be fully static and will not be re-downloaded during subsesquent
+`repo init` and `repo sync` calls.
+
The --reference option can be used to point to a directory that
has the content of a --mirror sync. This will make the working
directory use as much data as possible from the local reference
@@ -112,6 +120,22 @@
m = self.manifest.manifestProject
is_new = not m.Exists
+ # If repo has already been initialized, we take -u with the absence of
+ # --standalone-manifest to mean "transition to a standard repo set up",
+ # which necessitates starting fresh.
+ # If --standalone-manifest is set, we always tear everything down and start
+ # anew.
+ if not is_new:
+ was_standalone_manifest = m.config.GetString('manifest.standalone')
+ if opt.standalone_manifest or (
+ was_standalone_manifest and opt.manifest_url):
+ m.config.ClearCache()
+ if m.gitdir and os.path.exists(m.gitdir):
+ platform_utils.rmtree(m.gitdir)
+ if m.worktree and os.path.exists(m.worktree):
+ platform_utils.rmtree(m.worktree)
+
+ is_new = not m.Exists
if is_new:
if not opt.manifest_url:
print('fatal: manifest url is required.', file=sys.stderr)
@@ -136,6 +160,19 @@
m._InitGitDir(mirror_git=mirrored_manifest_git)
+ # If standalone_manifest is set, mark the project as "standalone" -- we'll
+ # still do much of the manifests.git set up, but will avoid actual syncs to
+ # a remote.
+ standalone_manifest = False
+ if opt.standalone_manifest:
+ standalone_manifest = True
+ elif not opt.manifest_url:
+ # If -u is set and --standalone-manifest is not, then we're not in
+ # standalone mode. Otherwise, use config to infer what we were in the last
+ # init.
+ standalone_manifest = bool(m.config.GetString('manifest.standalone'))
+ m.config.SetString('manifest.standalone', opt.manifest_url)
+
self._ConfigureDepth(opt)
# Set the remote URL before the remote branch as we might need it below.
@@ -145,22 +182,23 @@
r.ResetFetch()
r.Save()
- if opt.manifest_branch:
- if opt.manifest_branch == 'HEAD':
- opt.manifest_branch = m.ResolveRemoteHead()
- if opt.manifest_branch is None:
- print('fatal: unable to resolve HEAD', file=sys.stderr)
- sys.exit(1)
- m.revisionExpr = opt.manifest_branch
- else:
- if is_new:
- default_branch = m.ResolveRemoteHead()
- if default_branch is None:
- # If the remote doesn't have HEAD configured, default to master.
- default_branch = 'refs/heads/master'
- m.revisionExpr = default_branch
+ if not standalone_manifest:
+ if opt.manifest_branch:
+ if opt.manifest_branch == 'HEAD':
+ opt.manifest_branch = m.ResolveRemoteHead()
+ if opt.manifest_branch is None:
+ print('fatal: unable to resolve HEAD', file=sys.stderr)
+ sys.exit(1)
+ m.revisionExpr = opt.manifest_branch
else:
- m.PreSync()
+ if is_new:
+ default_branch = m.ResolveRemoteHead()
+ if default_branch is None:
+ # If the remote doesn't have HEAD configured, default to master.
+ default_branch = 'refs/heads/master'
+ m.revisionExpr = default_branch
+ else:
+ m.PreSync()
groups = re.split(r'[,\s]+', opt.groups)
all_platforms = ['linux', 'darwin', 'windows']
@@ -250,6 +288,16 @@
if opt.use_superproject is not None:
m.config.SetBoolean('repo.superproject', opt.use_superproject)
+ if standalone_manifest:
+ if is_new:
+ manifest_name = 'default.xml'
+ manifest_data = fetch.fetch_file(opt.manifest_url)
+ dest = os.path.join(m.worktree, manifest_name)
+ os.makedirs(os.path.dirname(dest), exist_ok=True)
+ with open(dest, 'wb') as f:
+ f.write(manifest_data)
+ return
+
if not m.Sync_NetworkHalf(is_new=is_new, quiet=opt.quiet, verbose=opt.verbose,
clone_bundle=opt.clone_bundle,
current_branch_only=opt.current_branch_only,
@@ -426,6 +474,11 @@
if opt.archive and opt.mirror:
self.OptionParser.error('--mirror and --archive cannot be used together.')
+ if opt.standalone_manifest and (
+ opt.manifest_branch or opt.manifest_name != 'default.xml'):
+ self.OptionParser.error('--manifest-branch and --manifest-name cannot'
+ ' be used with --standalone-manifest.')
+
if args:
if opt.manifest_url:
self.OptionParser.error(