Compare commits

...

9 Commits

Author SHA1 Message Date
68d69635c7 Fix Projects.shareable_dirs
If this tree is not using alternates for object sharing, then we need to
continue to call it a shared directory.

Bug: https://bugs.chromium.org/p/gerrit/issues/detail?id=15982
Test: manual
Change-Id: I1750f10b192504ac67f552222f8ddb9809d344fe
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/338974
Tested-by: LaMont Jones <lamontjones@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
2022-06-08 16:49:08 +00:00
ff6b1dae1e Only sync superproject if it will be used.
If the user says `--no-use-superproject`, then do not bother syncing the
superproject.

Also add/update docstrings and comments throughout.

Change-Id: I9cdad706130501bab9a22d3099a1dae605e9c194
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/338975
Tested-by: LaMont Jones <lamontjones@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
2022-06-08 16:49:08 +00:00
bdcba7dc36 sync: add multi-manifest support
With this change, partial syncs (sync with a project list) are again
supported.

If the updated manifest includes new sub manifests, download them
inheriting options from the parent manifestProject.

Change-Id: Id952f85df2e26d34e38b251973be26434443ff56
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/334819
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: LaMont Jones <lamontjones@google.com>
2022-05-26 00:03:37 +00:00
1d00a7e2ae project: initial separation of shared project objects
For now, this is opt-in via environment variables:
  - export REPO_USE_ALTERNATES=1

The shared project logic that shares the internal .git/objects/ dir
directly between multiple projects via the project-objects/ tree has
a lot of KI with random corruption.  It all boils down to projects
sharing objects/ but not refs/.  Git operations that use refs to see
what objects are reachable and discard the rest can easily discard
objects that are used by other projects.

Consider this project layout:
<show fs layout>

There are unique refs in each of these trees that are not visible in
the others.  This means it's not safe to run basic operations like
git prune or git gc.

Since we can't share refs (each project needs to have unique refs
like HEAD in order to function), let's change how we share objects.
The old way involved symlinking .git/objects/ to the project-objects
tree.  The new way shares objects using git's info/alternates.

This means project-objects/ will only contain objects that exist in
the remote project.  Local per-project objects (like when creating
branches and making changes) will never be shared.  When running a
prune or gc operation in the per-project state, it will only ever
repack or discard those per-project objects.  The common shared
objects would only be cleaned up when running a common operation
(i.e. by repo itself).

One downside to this for users is if they try blending unrelated
upstream projects.  For example, in CrOS we have multiple kernel
projects (for diff versions) checked out.  If a dev fetched the
upstream Linus tree into one of them, the objects & tags would
not be shared with the others, so they would have to fetch the
upstream state for each project.  Annoying, but better than the
current corruption situation we're in now.

Also if the dev runs a manual `git fetch` in the per-project to
sync it up to newer state than the last `repo sync` they ran,
the objects would get duplicated.  However, git operations later
on should eventually dedupe this.

Bug: https://crbug.com/gerrit/15553
Change-Id: I313a9b8962f9d439ef98ac0ed37ecfb9e0b3864e
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/328101
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: LaMont Jones <lamontjones@google.com>
2022-05-26 00:02:18 +00:00
3a0a145b0e upload: move label validation to core function
This way we know we don't need to encode the labels.

Change-Id: Ib83ed8f4ed05f00b9d2d06a9dd3f304e4443430e
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/337518
Tested-by: Mike Frysinger <vapier@google.com>
Reviewed-by: LaMont Jones <lamontjones@google.com>
2022-05-21 19:19:44 +00:00
74737da1ab tests: switch to tempfile.TemporaryDirectory
Now that we don't need to support Python 2, we can switch to this
API for better contextmanager logic.

Change-Id: I2d03e391121886547e7808a3b5c3b470c411533f
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/337515
Reviewed-by: LaMont Jones <lamontjones@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2022-05-20 11:38:10 +00:00
0ddb677611 project: fix --use-superproject logic for init.
If init was run with --use-superproject, init failed.

If init was run without --{no,}use-superproject option then manifests
with <superproject/> elements were mishandled.

Bug: b/233226285
Test: manual
Change-Id: I737e71c89d2d7c324114f58bf2dc82b40e5beba7
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/337534
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: LaMont Jones <lamontjones@google.com>
2022-05-20 11:01:28 +00:00
501733c2ab manifest: add submanifest.default_groups attribute
When the user does not specify any manifest groups, this allows the
parent manifest to indicate which manifest groups should be used for
syncing the submanifest.

Change-Id: I88806ed35013d13dd2ab3cd245fcd4f9061112c4
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/335474
Tested-by: LaMont Jones <lamontjones@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
2022-04-29 18:42:23 +00:00
0165e20fcc project: Do not exit early on --standalone-manifest.
After we successfully download the standalone manifest file, we cannot
exit early.

Bug: https://bugs.chromium.org/p/gerrit/issues/detail?id=15861
Change-Id: Ic47c9f7e9921851f94c6f24fd82b896eff524037
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/335974
Reviewed-by: Raman Tenneti <rtenneti@google.com>
Tested-by: LaMont Jones <lamontjones@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
2022-04-29 17:13:49 +00:00
14 changed files with 456 additions and 228 deletions

View File

@ -144,11 +144,10 @@ class Command(object):
help=f'number of jobs to run in parallel (default: {default})') help=f'number of jobs to run in parallel (default: {default})')
m = p.add_option_group('Multi-manifest options') m = p.add_option_group('Multi-manifest options')
m.add_option('--outer-manifest', action='store_true', m.add_option('--outer-manifest', action='store_true', default=None,
help='operate starting at the outermost manifest') help='operate starting at the outermost manifest')
m.add_option('--no-outer-manifest', dest='outer_manifest', m.add_option('--no-outer-manifest', dest='outer_manifest',
action='store_false', default=None, action='store_false', help='do not operate on outer manifests')
help='do not operate on outer manifests')
m.add_option('--this-manifest-only', action='store_true', default=None, m.add_option('--this-manifest-only', action='store_true', default=None,
help='only operate on this (sub)manifest') help='only operate on this (sub)manifest')
m.add_option('--no-this-manifest-only', '--all-manifests', m.add_option('--no-this-manifest-only', '--all-manifests',
@ -186,6 +185,10 @@ class Command(object):
"""Validate common options.""" """Validate common options."""
opt.quiet = opt.output_mode is False opt.quiet = opt.output_mode is False
opt.verbose = opt.output_mode is True opt.verbose = opt.output_mode is True
if opt.outer_manifest is None:
# By default, treat multi-manifest instances as a single manifest from
# the user's perspective.
opt.outer_manifest = True
def ValidateOptions(self, opt, args): def ValidateOptions(self, opt, args):
"""Validate the user options & arguments before executing. """Validate the user options & arguments before executing.
@ -274,6 +277,18 @@ class Command(object):
def GetProjects(self, args, manifest=None, groups='', missing_ok=False, def GetProjects(self, args, manifest=None, groups='', missing_ok=False,
submodules_ok=False, all_manifests=False): submodules_ok=False, all_manifests=False):
"""A list of projects that match the arguments. """A list of projects that match the arguments.
Args:
args: a list of (case-insensitive) strings, projects to search for.
manifest: an XmlManifest, the manifest to use, or None for default.
groups: a string, the manifest groups in use.
missing_ok: a boolean, whether to allow missing projects.
submodules_ok: a boolean, whether to allow submodules.
all_manifests: a boolean, if True then all manifests and submanifests are
used. If False, then only the local (sub)manifest is used.
Returns:
A list of matching Project instances.
""" """
if all_manifests: if all_manifests:
if not manifest: if not manifest:
@ -385,7 +400,7 @@ class Command(object):
opt: The command options. opt: The command options.
""" """
top = self.outer_manifest top = self.outer_manifest
if opt.outer_manifest is False or opt.this_manifest_only: if not opt.outer_manifest or opt.this_manifest_only:
top = self.manifest top = self.manifest
yield top yield top
if not opt.this_manifest_only: if not opt.this_manifest_only:

View File

@ -66,6 +66,7 @@ following DTD:
<!ATTLIST submanifest revision CDATA #IMPLIED> <!ATTLIST submanifest revision CDATA #IMPLIED>
<!ATTLIST submanifest path CDATA #IMPLIED> <!ATTLIST submanifest path CDATA #IMPLIED>
<!ATTLIST submanifest groups CDATA #IMPLIED> <!ATTLIST submanifest groups CDATA #IMPLIED>
<!ATTLIST submanifest default-groups CDATA #IMPLIED>
<!ELEMENT project (annotation*, <!ELEMENT project (annotation*,
project*, project*,
@ -302,6 +303,9 @@ in the included submanifest belong. This appends and recurses, meaning
all projects in submanifests carry all parent submanifest groups. all projects in submanifests carry all parent submanifest groups.
Same syntax as the corresponding element of `project`. Same syntax as the corresponding element of `project`.
Attribute `default-groups`: The list of manifest groups to sync if no
`--groups=` parameter was specified at init. When that list is empty, use this
list instead of "default" as the list of groups to sync.
### Element project ### Element project

View File

@ -18,7 +18,7 @@ For more information on superproject, check out:
https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects
Examples: Examples:
superproject = Superproject() superproject = Superproject(manifest, name, remote, revision)
UpdateProjectsResult = superproject.UpdateProjectsRevisionId(projects) UpdateProjectsResult = superproject.UpdateProjectsRevisionId(projects)
""" """
@ -99,8 +99,8 @@ class Superproject(object):
self._work_git_name = git_name + _SUPERPROJECT_GIT_NAME self._work_git_name = git_name + _SUPERPROJECT_GIT_NAME
self._work_git = os.path.join(self._superproject_path, self._work_git_name) self._work_git = os.path.join(self._superproject_path, self._work_git_name)
# The following are command arguemnts, rather then superproject attributes, # The following are command arguemnts, rather than superproject attributes,
# and where included here originally. They should eventually become # and were included here originally. They should eventually become
# arguments that are passed down from the public methods, instead of being # arguments that are passed down from the public methods, instead of being
# treated as attributes. # treated as attributes.
self._git_event_log = None self._git_event_log = None
@ -329,7 +329,8 @@ class Superproject(object):
"""Update revisionId of every project in projects with the commit id. """Update revisionId of every project in projects with the commit id.
Args: Args:
projects: List of projects whose revisionId needs to be updated. projects: a list of projects whose revisionId needs to be updated.
git_event_log: an EventLog, for git tracing.
Returns: Returns:
UpdateProjectsResult UpdateProjectsResult
@ -431,9 +432,15 @@ def UseSuperproject(use_superproject, manifest):
Args: Args:
use_superproject: option value from optparse. use_superproject: option value from optparse.
manifest: manifest to use. manifest: manifest to use.
Returns:
Whether the superproject should be used.
""" """
if use_superproject is not None: if not manifest.superproject:
# This (sub) manifest does not have a superproject definition.
return False
elif use_superproject is not None:
return use_superproject return use_superproject
else: else:
client_value = manifest.manifestProject.use_superproject client_value = manifest.manifestProject.use_superproject

View File

@ -294,8 +294,7 @@ class _Repo(object):
cmd.ValidateOptions(copts, cargs) cmd.ValidateOptions(copts, cargs)
this_manifest_only = copts.this_manifest_only this_manifest_only = copts.this_manifest_only
# If not specified, default to using the outer manifest. outer_manifest = copts.outer_manifest
outer_manifest = copts.outer_manifest is not False
if cmd.MULTI_MANIFEST_SUPPORT or this_manifest_only: if cmd.MULTI_MANIFEST_SUPPORT or this_manifest_only:
result = cmd.Execute(copts, cargs) result = cmd.Execute(copts, cargs)
elif outer_manifest and repo_client.manifest.is_submanifest: elif outer_manifest and repo_client.manifest.is_submanifest:

View File

@ -214,6 +214,7 @@ class _XmlSubmanifest:
revision: a string, the commitish. revision: a string, the commitish.
manifestName: a string, the submanifest file name. manifestName: a string, the submanifest file name.
groups: a list of strings, the groups to add to all projects in the submanifest. groups: a list of strings, the groups to add to all projects in the submanifest.
default_groups: a list of strings, the default groups to sync.
path: a string, the relative path for the submanifest checkout. path: a string, the relative path for the submanifest checkout.
parent: an XmlManifest, the parent manifest. parent: an XmlManifest, the parent manifest.
annotations: (derived) a list of annotations. annotations: (derived) a list of annotations.
@ -226,6 +227,7 @@ class _XmlSubmanifest:
revision=None, revision=None,
manifestName=None, manifestName=None,
groups=None, groups=None,
default_groups=None,
path=None, path=None,
parent=None): parent=None):
self.name = name self.name = name
@ -234,6 +236,7 @@ class _XmlSubmanifest:
self.revision = revision self.revision = revision
self.manifestName = manifestName self.manifestName = manifestName
self.groups = groups self.groups = groups
self.default_groups = default_groups
self.path = path self.path = path
self.parent = parent self.parent = parent
self.annotations = [] self.annotations = []
@ -250,7 +253,8 @@ class _XmlSubmanifest:
os.path.join(parent.path_prefix, self.relpath), MANIFEST_FILE_NAME) os.path.join(parent.path_prefix, self.relpath), MANIFEST_FILE_NAME)
rc = self.repo_client = RepoClient( rc = self.repo_client = RepoClient(
parent.repodir, linkFile, parent_groups=','.join(groups) or '', parent.repodir, linkFile, parent_groups=','.join(groups) or '',
submanifest_path=self.relpath, outer_client=outer_client) submanifest_path=self.relpath, outer_client=outer_client,
default_groups=default_groups)
self.present = os.path.exists(manifestFile) self.present = os.path.exists(manifestFile)
@ -264,6 +268,7 @@ class _XmlSubmanifest:
self.revision == other.revision and self.revision == other.revision and
self.manifestName == other.manifestName and self.manifestName == other.manifestName and
self.groups == other.groups and self.groups == other.groups and
self.default_groups == other.default_groups and
self.path == other.path and self.path == other.path and
sorted(self.annotations) == sorted(other.annotations)) sorted(self.annotations) == sorted(other.annotations))
@ -284,6 +289,7 @@ class _XmlSubmanifest:
revision = self.revision or self.name revision = self.revision or self.name
path = self.path or revision.split('/')[-1] path = self.path or revision.split('/')[-1]
groups = self.groups or [] groups = self.groups or []
default_groups = self.default_groups or []
return SubmanifestSpec(self.name, manifestUrl, manifestName, revision, path, return SubmanifestSpec(self.name, manifestUrl, manifestName, revision, path,
groups) groups)
@ -300,6 +306,10 @@ class _XmlSubmanifest:
return ','.join(self.groups) return ','.join(self.groups)
return '' return ''
def GetDefaultGroupsStr(self):
"""Returns the `default-groups` given for this submanifest."""
return ','.join(self.default_groups or [])
def AddAnnotation(self, name, value, keep): def AddAnnotation(self, name, value, keep):
"""Add annotations to the submanifest.""" """Add annotations to the submanifest."""
self.annotations.append(Annotation(name, value, keep)) self.annotations.append(Annotation(name, value, keep))
@ -327,7 +337,8 @@ class XmlManifest(object):
"""manages the repo configuration file""" """manages the repo configuration file"""
def __init__(self, repodir, manifest_file, local_manifests=None, def __init__(self, repodir, manifest_file, local_manifests=None,
outer_client=None, parent_groups='', submanifest_path=''): outer_client=None, parent_groups='', submanifest_path='',
default_groups=None):
"""Initialize. """Initialize.
Args: Args:
@ -337,9 +348,10 @@ class XmlManifest(object):
be |repodir|/|MANIFEST_FILE_NAME|. be |repodir|/|MANIFEST_FILE_NAME|.
local_manifests: Full path to the directory of local override manifests. local_manifests: Full path to the directory of local override manifests.
This will usually be |repodir|/|LOCAL_MANIFESTS_DIR_NAME|. This will usually be |repodir|/|LOCAL_MANIFESTS_DIR_NAME|.
outer_client: RepoClient of the outertree. outer_client: RepoClient of the outer manifest.
parent_groups: a string, the groups to apply to this projects. parent_groups: a string, the groups to apply to this projects.
submanifest_path: The submanifest root relative to the repo root. submanifest_path: The submanifest root relative to the repo root.
default_groups: a string, the default manifest groups to use.
""" """
# TODO(vapier): Move this out of this class. # TODO(vapier): Move this out of this class.
self.globalConfig = GitConfig.ForUser() self.globalConfig = GitConfig.ForUser()
@ -358,6 +370,7 @@ class XmlManifest(object):
self.local_manifests = local_manifests self.local_manifests = local_manifests
self._load_local_manifests = True self._load_local_manifests = True
self.parent_groups = parent_groups self.parent_groups = parent_groups
self.default_groups = default_groups
if outer_client and self.isGitcClient: if outer_client and self.isGitcClient:
raise ManifestParseError('Multi-manifest is incompatible with `gitc-init`') raise ManifestParseError('Multi-manifest is incompatible with `gitc-init`')
@ -472,6 +485,8 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
e.setAttribute('path', r.path) e.setAttribute('path', r.path)
if r.groups: if r.groups:
e.setAttribute('groups', r.GetGroupsStr()) e.setAttribute('groups', r.GetGroupsStr())
if r.default_groups:
e.setAttribute('default-groups', r.GetDefaultGroupsStr())
for a in r.annotations: for a in r.annotations:
if a.keep == 'true': if a.keep == 'true':
@ -753,23 +768,29 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
@property @property
def is_multimanifest(self): def is_multimanifest(self):
"""Whether this is a multimanifest checkout""" """Whether this is a multimanifest checkout.
return bool(self.outer_client.submanifests)
This is safe to use as long as the outermost manifest XML has been parsed.
"""
return bool(self._outer_client._submanifests)
@property @property
def is_submanifest(self): def is_submanifest(self):
"""Whether this manifest is a submanifest""" """Whether this manifest is a submanifest.
This is safe to use as long as the outermost manifest XML has been parsed.
"""
return self._outer_client and self._outer_client != self return self._outer_client and self._outer_client != self
@property @property
def outer_client(self): def outer_client(self):
"""The instance of the outermost manifest client""" """The instance of the outermost manifest client."""
self._Load() self._Load()
return self._outer_client return self._outer_client
@property @property
def all_manifests(self): def all_manifests(self):
"""Generator yielding all (sub)manifests.""" """Generator yielding all (sub)manifests, in depth-first order."""
self._Load() self._Load()
outer = self._outer_client outer = self._outer_client
yield outer yield outer
@ -778,7 +799,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
@property @property
def all_children(self): def all_children(self):
"""Generator yielding all child submanifests.""" """Generator yielding all (present) child submanifests."""
self._Load() self._Load()
for child in self._submanifests.values(): for child in self._submanifests.values():
if child.repo_client: if child.repo_client:
@ -795,7 +816,14 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
@property @property
def all_paths(self): def all_paths(self):
"""All project paths for all (sub)manifests. See `paths`.""" """All project paths for all (sub)manifests.
See also `paths`.
Returns:
A dictionary of {path: Project()}. `path` is relative to the outer
manifest.
"""
ret = {} ret = {}
for tree in self.all_manifests: for tree in self.all_manifests:
prefix = tree.path_prefix prefix = tree.path_prefix
@ -811,7 +839,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
def paths(self): def paths(self):
"""Return all paths for this manifest. """Return all paths for this manifest.
Return: Returns:
A dictionary of {path: Project()}. `path` is relative to this manifest. A dictionary of {path: Project()}. `path` is relative to this manifest.
""" """
self._Load() self._Load()
@ -825,11 +853,13 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
@property @property
def remotes(self): def remotes(self):
"""Return a list of remotes for this manifest."""
self._Load() self._Load()
return self._remotes return self._remotes
@property @property
def default(self): def default(self):
"""Return default values for this manifest."""
self._Load() self._Load()
return self._default return self._default
@ -967,16 +997,21 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
worktree=os.path.join(subdir, 'manifests')) worktree=os.path.join(subdir, 'manifests'))
return mp return mp
def GetDefaultGroupsStr(self): def GetDefaultGroupsStr(self, with_platform=True):
"""Returns the default group string for the platform.""" """Returns the default group string to use.
return 'default,platform-' + platform.system().lower()
Args:
with_platform: a boolean, whether to include the group for the
underlying platform.
"""
groups = ','.join(self.default_groups or ['default'])
if with_platform:
groups += f',platform-{platform.system().lower()}'
return groups
def GetGroupsStr(self): def GetGroupsStr(self):
"""Returns the manifest group string that should be synced.""" """Returns the manifest group string that should be synced."""
groups = self.manifestProject.manifest_groups return self.manifestProject.manifest_groups or self.GetDefaultGroupsStr()
if not groups:
groups = self.GetDefaultGroupsStr()
return groups
def Unload(self): def Unload(self):
"""Unload the manifest. """Unload the manifest.
@ -1067,8 +1102,8 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
if override: if override:
self.manifestFile = savedManifestFile self.manifestFile = savedManifestFile
# Now that we have loaded this manifest, load any submanifest manifests # Now that we have loaded this manifest, load any submanifests as well.
# as well. We need to do this after self._loaded is set to avoid looping. # We need to do this after self._loaded is set to avoid looping.
for name in self._submanifests: for name in self._submanifests:
tree = self._submanifests[name] tree = self._submanifests[name]
spec = tree.ToSubmanifestSpec() spec = tree.ToSubmanifestSpec()
@ -1491,6 +1526,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
if node.hasAttribute('groups'): if node.hasAttribute('groups'):
groups = node.getAttribute('groups') groups = node.getAttribute('groups')
groups = self._ParseList(groups) groups = self._ParseList(groups)
default_groups = self._ParseList(node.getAttribute('default-groups'))
path = node.getAttribute('path') path = node.getAttribute('path')
if path == '': if path == '':
path = None path = None
@ -1511,7 +1547,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
'<submanifest> invalid "path": %s: %s' % (path, msg)) '<submanifest> invalid "path": %s: %s' % (path, msg))
submanifest = _XmlSubmanifest(name, remote, project, revision, manifestName, submanifest = _XmlSubmanifest(name, remote, project, revision, manifestName,
groups, path, self) groups, default_groups, path, self)
for n in node.childNodes: for n in node.childNodes:
if n.nodeName == 'annotation': if n.nodeName == 'annotation':
@ -1635,6 +1671,10 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
name: a string, the name of the project. name: a string, the name of the project.
path: a string, the path of the project. path: a string, the path of the project.
remote: a string, the remote.name of the project. remote: a string, the remote.name of the project.
Returns:
A tuple of (relpath, worktree, gitdir, objdir, use_git_worktrees) for the
project with |name| and |path|.
""" """
# The manifest entries might have trailing slashes. Normalize them to avoid # The manifest entries might have trailing slashes. Normalize them to avoid
# unexpected filesystem behavior since we do string concatenation below. # unexpected filesystem behavior since we do string concatenation below.
@ -1642,7 +1682,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
name = name.rstrip('/') name = name.rstrip('/')
remote = remote.rstrip('/') remote = remote.rstrip('/')
use_git_worktrees = False use_git_worktrees = False
use_remote_name = bool(self._outer_client._submanifests) use_remote_name = self.is_multimanifest
relpath = path relpath = path
if self.IsMirror: if self.IsMirror:
worktree = None worktree = None
@ -1658,7 +1698,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
# We allow people to mix git worktrees & non-git worktrees for now. # We allow people to mix git worktrees & non-git worktrees for now.
# This allows for in situ migration of repo clients. # This allows for in situ migration of repo clients.
if os.path.exists(gitdir) or not self.UseGitWorktrees: if os.path.exists(gitdir) or not self.UseGitWorktrees:
objdir = os.path.join(self.subdir, 'project-objects', namepath) objdir = os.path.join(self.repodir, 'project-objects', namepath)
else: else:
use_git_worktrees = True use_git_worktrees = True
gitdir = os.path.join(self.repodir, 'worktrees', namepath) gitdir = os.path.join(self.repodir, 'worktrees', namepath)
@ -1672,6 +1712,9 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
name: a string, the name of the project. name: a string, the name of the project.
all_manifests: a boolean, if True, then all manifests are searched. If all_manifests: a boolean, if True, then all manifests are searched. If
False, then only this manifest is searched. False, then only this manifest is searched.
Returns:
A list of Project instances with name |name|.
""" """
if all_manifests: if all_manifests:
return list(itertools.chain.from_iterable( return list(itertools.chain.from_iterable(
@ -1932,6 +1975,16 @@ class RepoClient(XmlManifest):
"""Manages a repo client checkout.""" """Manages a repo client checkout."""
def __init__(self, repodir, manifest_file=None, submanifest_path='', **kwargs): def __init__(self, repodir, manifest_file=None, submanifest_path='', **kwargs):
"""Initialize.
Args:
repodir: Path to the .repo/ dir for holding all internal checkout state.
It must be in the top directory of the repo client checkout.
manifest_file: Full path to the manifest file to parse. This will usually
be |repodir|/|MANIFEST_FILE_NAME|.
submanifest_path: The submanifest root relative to the repo root.
**kwargs: Additional keyword arguments, passed to XmlManifest.
"""
self.isGitcClient = False self.isGitcClient = False
submanifest_path = submanifest_path or '' submanifest_path = submanifest_path or ''
if submanifest_path: if submanifest_path:

View File

@ -33,6 +33,7 @@ import fetch
from git_command import GitCommand, git_require from git_command import GitCommand, git_require
from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, \ from git_config import GitConfig, IsId, GetSchemeFromUrl, GetUrlCookieFile, \
ID_RE ID_RE
import git_superproject
from git_trace2_event_log import EventLog from git_trace2_event_log import EventLog
from error import GitError, UploadError, DownloadError from error import GitError, UploadError, DownloadError
from error import ManifestInvalidRevisionError, ManifestInvalidPathError from error import ManifestInvalidRevisionError, ManifestInvalidPathError
@ -49,6 +50,9 @@ MAXIMUM_RETRY_SLEEP_SEC = 3600.0
# +-10% random jitter is added to each Fetches retry sleep duration. # +-10% random jitter is added to each Fetches retry sleep duration.
RETRY_JITTER_PERCENT = 0.1 RETRY_JITTER_PERCENT = 0.1
# Whether to use alternates.
# TODO(vapier): Remove knob once behavior is verified.
_ALTERNATES = os.environ.get('REPO_USE_ALTERNATES') == '1'
def _lwrite(path, content): def _lwrite(path, content):
lock = '%s.lock' % path lock = '%s.lock' % path
@ -460,7 +464,13 @@ class RemoteSpec(object):
class Project(object): class Project(object):
# These objects can be shared between several working trees. # These objects can be shared between several working trees.
shareable_dirs = ['hooks', 'objects', 'rr-cache'] @property
def shareable_dirs(self):
"""Return the shareable directories"""
if self.UseAlternates:
return ['hooks', 'rr-cache']
else:
return ['hooks', 'objects', 'rr-cache']
def __init__(self, def __init__(self,
manifest, manifest,
@ -590,6 +600,14 @@ class Project(object):
self.bare_ref = GitRefs(self.gitdir) self.bare_ref = GitRefs(self.gitdir)
self.bare_objdir = self._GitGetByExec(self, bare=True, gitdir=self.objdir) self.bare_objdir = self._GitGetByExec(self, bare=True, gitdir=self.objdir)
@property
def UseAlternates(self):
"""Whether git alternates are in use.
This will be removed once migration to alternates is complete.
"""
return _ALTERNATES or self.manifest.is_multimanifest
@property @property
def Derived(self): def Derived(self):
return self.is_derived return self.is_derived
@ -715,7 +733,8 @@ class Project(object):
The special manifest group "default" will match any project that The special manifest group "default" will match any project that
does not have the special project group "notdefault" does not have the special project group "notdefault"
""" """
expanded_manifest_groups = manifest_groups or ['default'] default_groups = self.manifest.default_groups or ['default']
expanded_manifest_groups = manifest_groups or default_groups
expanded_project_groups = ['all'] + (self.groups or []) expanded_project_groups = ['all'] + (self.groups or [])
if 'notdefault' not in expanded_project_groups: if 'notdefault' not in expanded_project_groups:
expanded_project_groups += ['default'] expanded_project_groups += ['default']
@ -995,6 +1014,13 @@ class Project(object):
if not branch.remote.review: if not branch.remote.review:
raise GitError('remote %s has no review url' % branch.remote.name) raise GitError('remote %s has no review url' % branch.remote.name)
# Basic validity check on label syntax.
for label in labels:
if not re.match(r'^.+[+-][0-9]+$', label):
raise UploadError(
f'invalid label syntax "{label}": labels use forms like '
'CodeReview+1 or Verified-1')
if dest_branch is None: if dest_branch is None:
dest_branch = self.dest_branch dest_branch = self.dest_branch
if dest_branch is None: if dest_branch is None:
@ -1030,6 +1056,7 @@ class Project(object):
if auto_topic: if auto_topic:
opts += ['topic=' + branch.name] opts += ['topic=' + branch.name]
opts += ['t=%s' % p for p in hashtags] opts += ['t=%s' % p for p in hashtags]
# NB: No need to encode labels as they've been validated above.
opts += ['l=%s' % p for p in labels] opts += ['l=%s' % p for p in labels]
opts += ['r=%s' % p for p in people[0]] opts += ['r=%s' % p for p in people[0]]
@ -1134,6 +1161,17 @@ class Project(object):
self._UpdateHooks(quiet=quiet) self._UpdateHooks(quiet=quiet)
self._InitRemote() self._InitRemote()
if self.UseAlternates:
# If gitdir/objects is a symlink, migrate it from the old layout.
gitdir_objects = os.path.join(self.gitdir, 'objects')
if platform_utils.islink(gitdir_objects):
platform_utils.remove(gitdir_objects, missing_ok=True)
gitdir_alt = os.path.join(self.gitdir, 'objects/info/alternates')
if not os.path.exists(gitdir_alt):
os.makedirs(os.path.dirname(gitdir_alt), exist_ok=True)
_lwrite(gitdir_alt, os.path.join(
os.path.relpath(self.objdir, gitdir_objects), 'objects') + '\n')
if is_new: if is_new:
alt = os.path.join(self.objdir, 'objects/info/alternates') alt = os.path.join(self.objdir, 'objects/info/alternates')
try: try:
@ -2157,6 +2195,8 @@ class Project(object):
if prune: if prune:
cmd.append('--prune') cmd.append('--prune')
# Always pass something for --recurse-submodules, git with GIT_DIR behaves
# incorrectly when not given `--recurse-submodules=no`. (b/218891912)
cmd.append(f'--recurse-submodules={"on-demand" if submodules else "no"}') cmd.append(f'--recurse-submodules={"on-demand" if submodules else "no"}')
spec = [] spec = []
@ -3444,6 +3484,67 @@ class ManifestProject(MetaProject):
"""Return the name of the platform.""" """Return the name of the platform."""
return platform.system().lower() return platform.system().lower()
def SyncWithPossibleInit(self, submanifest, verbose=False,
current_branch_only=False, tags='', git_event_log=None):
"""Sync a manifestProject, possibly for the first time.
Call Sync() with arguments from the most recent `repo init`. If this is a
new sub manifest, then inherit options from the parent's manifestProject.
This is used by subcmds.Sync() to do an initial download of new sub
manifests.
Args:
submanifest: an XmlSubmanifest, the submanifest to re-sync.
verbose: a boolean, whether to show all output, rather than only errors.
current_branch_only: a boolean, whether to only fetch the current manifest
branch from the server.
tags: a boolean, whether to fetch tags.
git_event_log: an EventLog, for git tracing.
"""
# TODO(lamontjones): when refactoring sync (and init?) consider how to
# better get the init options that we should use for new submanifests that
# are added when syncing an existing workspace.
git_event_log = git_event_log or EventLog()
spec = submanifest.ToSubmanifestSpec()
# Use the init options from the existing manifestProject, or the parent if
# it doesn't exist.
#
# Today, we only support changing manifest_groups on the sub-manifest, with
# no supported-for-the-user way to change the other arguments from those
# specified by the outermost manifest.
#
# TODO(lamontjones): determine which of these should come from the outermost
# manifest and which should come from the parent manifest.
mp = self if self.Exists else submanifest.parent.manifestProject
return self.Sync(
manifest_url=spec.manifestUrl,
manifest_branch=spec.revision,
standalone_manifest=mp.standalone_manifest_url,
groups=mp.manifest_groups,
platform=mp.manifest_platform,
mirror=mp.mirror,
dissociate=mp.dissociate,
reference=mp.reference,
worktree=mp.use_worktree,
submodules=mp.submodules,
archive=mp.archive,
partial_clone=mp.partial_clone,
clone_filter=mp.clone_filter,
partial_clone_exclude=mp.partial_clone_exclude,
clone_bundle=mp.clone_bundle,
git_lfs=mp.git_lfs,
use_superproject=mp.use_superproject,
verbose=verbose,
current_branch_only=current_branch_only,
tags=tags,
depth=mp.depth,
git_event_log=git_event_log,
manifest_name=spec.manifestName,
this_manifest_only=True,
outer_manifest=False,
)
def Sync(self, _kwargs_only=(), manifest_url='', manifest_branch=None, def Sync(self, _kwargs_only=(), manifest_url='', manifest_branch=None,
standalone_manifest=False, groups='', mirror=False, reference='', standalone_manifest=False, groups='', mirror=False, reference='',
dissociate=False, worktree=False, submodules=False, archive=False, dissociate=False, worktree=False, submodules=False, archive=False,
@ -3496,7 +3597,7 @@ class ManifestProject(MetaProject):
""" """
assert _kwargs_only == (), 'Sync only accepts keyword arguments.' assert _kwargs_only == (), 'Sync only accepts keyword arguments.'
groups = groups or 'default' groups = groups or self.manifest.GetDefaultGroupsStr(with_platform=False)
platform = platform or 'auto' platform = platform or 'auto'
git_event_log = git_event_log or EventLog() git_event_log = git_event_log or EventLog()
if outer_manifest and self.manifest.is_submanifest: if outer_manifest and self.manifest.is_submanifest:
@ -3710,46 +3811,45 @@ class ManifestProject(MetaProject):
if use_superproject is not None: if use_superproject is not None:
self.config.SetBoolean('repo.superproject', use_superproject) self.config.SetBoolean('repo.superproject', use_superproject)
if standalone_manifest: if not standalone_manifest:
if is_new: if not self.Sync_NetworkHalf(
manifest_name = 'default.xml' is_new=is_new, quiet=not verbose, verbose=verbose,
manifest_data = fetch.fetch_file(manifest_url, verbose=verbose) clone_bundle=clone_bundle, current_branch_only=current_branch_only,
dest = os.path.join(self.worktree, manifest_name) tags=tags, submodules=submodules, clone_filter=clone_filter,
os.makedirs(os.path.dirname(dest), exist_ok=True) partial_clone_exclude=self.manifest.PartialCloneExclude):
with open(dest, 'wb') as f: r = self.GetRemote(self.remote.name)
f.write(manifest_data) print('fatal: cannot obtain manifest %s' % r.url, file=sys.stderr)
return
if not self.Sync_NetworkHalf(is_new=is_new, quiet=not verbose, verbose=verbose, # Better delete the manifest git dir if we created it; otherwise next
clone_bundle=clone_bundle, # time (when user fixes problems) we won't go through the "is_new" logic.
current_branch_only=current_branch_only, if is_new:
tags=tags, submodules=submodules, platform_utils.rmtree(self.gitdir)
clone_filter=clone_filter,
partial_clone_exclude=self.manifest.PartialCloneExclude):
r = self.GetRemote(self.remote.name)
print('fatal: cannot obtain manifest %s' % r.url, file=sys.stderr)
# Better delete the manifest git dir if we created it; otherwise next
# time (when user fixes problems) we won't go through the "is_new" logic.
if is_new:
platform_utils.rmtree(self.gitdir)
return False
if manifest_branch:
self.MetaBranchSwitch(submodules=submodules)
syncbuf = SyncBuffer(self.config)
self.Sync_LocalHalf(syncbuf, submodules=submodules)
syncbuf.Finish()
if is_new or self.CurrentBranch is None:
if not self.StartBranch('default'):
print('fatal: cannot create default in manifest', file=sys.stderr)
return False return False
if not manifest_name: if manifest_branch:
print('fatal: manifest name (-m) is required.', file=sys.stderr) self.MetaBranchSwitch(submodules=submodules)
return False
syncbuf = SyncBuffer(self.config)
self.Sync_LocalHalf(syncbuf, submodules=submodules)
syncbuf.Finish()
if is_new or self.CurrentBranch is None:
if not self.StartBranch('default'):
print('fatal: cannot create default in manifest', file=sys.stderr)
return False
if not manifest_name:
print('fatal: manifest name (-m) is required.', file=sys.stderr)
return False
elif is_new:
# This is a new standalone manifest.
manifest_name = 'default.xml'
manifest_data = fetch.fetch_file(manifest_url, verbose=verbose)
dest = os.path.join(self.worktree, manifest_name)
os.makedirs(os.path.dirname(dest), exist_ok=True)
with open(dest, 'wb') as f:
f.write(manifest_data)
try: try:
self.manifest.Link(manifest_name) self.manifest.Link(manifest_name)
@ -3790,10 +3890,10 @@ class ManifestProject(MetaProject):
outer_manifest=False, outer_manifest=False,
) )
# Lastly, clone the superproject(s). # Lastly, if the manifest has a <superproject> then have the superproject
if self.manifest.manifestProject.use_superproject: # sync it (if it will be used).
sync_result = Superproject( if git_superproject.UseSuperproject(use_superproject, self.manifest):
self.manifest, self.manifest.repodir, git_event_log, quiet=not verbose).Sync() sync_result = self.manifest.superproject.Sync(git_event_log)
if not sync_result.success: if not sync_result.success:
print('warning: git update of superproject for ' print('warning: git update of superproject for '
f'{self.manifest.path_prefix} failed, repo sync will not use ' f'{self.manifest.path_prefix} failed, repo sync will not use '

View File

@ -65,8 +65,7 @@ class Info(PagedCommand):
self.manifest = self.manifest.outer_client self.manifest = self.manifest.outer_client
manifestConfig = self.manifest.manifestProject.config manifestConfig = self.manifest.manifestProject.config
mergeBranch = manifestConfig.GetBranch("default").merge mergeBranch = manifestConfig.GetBranch("default").merge
manifestGroups = (manifestConfig.GetString('manifest.groups') manifestGroups = self.manifest.GetGroupsStr()
or 'all,-notdefault')
self.heading("Manifest branch: ") self.heading("Manifest branch: ")
if self.manifest.default.revisionExpr: if self.manifest.default.revisionExpr:

View File

@ -89,11 +89,10 @@ to update the working directory files.
def _Options(self, p, gitc_init=False): def _Options(self, p, gitc_init=False):
Wrapper().InitParser(p, gitc_init=gitc_init) Wrapper().InitParser(p, gitc_init=gitc_init)
m = p.add_option_group('Multi-manifest') m = p.add_option_group('Multi-manifest')
m.add_option('--outer-manifest', action='store_true', m.add_option('--outer-manifest', action='store_true', default=True,
help='operate starting at the outermost manifest') help='operate starting at the outermost manifest')
m.add_option('--no-outer-manifest', dest='outer_manifest', m.add_option('--no-outer-manifest', dest='outer_manifest',
action='store_false', default=None, action='store_false', help='do not operate on outer manifests')
help='do not operate on outer manifests')
m.add_option('--this-manifest-only', action='store_true', default=None, m.add_option('--this-manifest-only', action='store_true', default=None,
help='only operate on this (sub)manifest') help='only operate on this (sub)manifest')
m.add_option('--no-this-manifest-only', '--all-manifests', m.add_option('--no-this-manifest-only', '--all-manifests',

View File

@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import collections
import functools import functools
import http.cookiejar as cookielib import http.cookiejar as cookielib
import io import io
@ -66,7 +67,7 @@ _ONE_DAY_S = 24 * 60 * 60
class Sync(Command, MirrorSafeCommand): class Sync(Command, MirrorSafeCommand):
jobs = 1 jobs = 1
COMMON = True COMMON = True
MULTI_MANIFEST_SUPPORT = False MULTI_MANIFEST_SUPPORT = True
helpSummary = "Update working tree to the latest revision" helpSummary = "Update working tree to the latest revision"
helpUsage = """ helpUsage = """
%prog [<project>...] %prog [<project>...]
@ -295,52 +296,92 @@ later is required to fix a server side protocol bug.
""" """
return git_superproject.UseSuperproject(opt.use_superproject, manifest) or opt.current_branch_only return git_superproject.UseSuperproject(opt.use_superproject, manifest) or opt.current_branch_only
def _UpdateProjectsRevisionId(self, opt, args, load_local_manifests, superproject_logging_data, manifest): def _UpdateProjectsRevisionId(self, opt, args, superproject_logging_data,
"""Update revisionId of every project with the SHA from superproject. manifest):
"""Update revisionId of projects with the commit hash from the superproject.
This function updates each project's revisionId with SHA from superproject. This function updates each project's revisionId with the commit hash from
It writes the updated manifest into a file and reloads the manifest from it. the superproject. It writes the updated manifest into a file and reloads
the manifest from it. When appropriate, sub manifests are also processed.
Args: Args:
opt: Program options returned from optparse. See _Options(). opt: Program options returned from optparse. See _Options().
args: Arguments to pass to GetProjects. See the GetProjects args: Arguments to pass to GetProjects. See the GetProjects
docstring for details. docstring for details.
load_local_manifests: Whether to load local manifests. superproject_logging_data: A dictionary of superproject data to log.
superproject_logging_data: A dictionary of superproject data that is to be logged.
manifest: The manifest to use. manifest: The manifest to use.
Returns:
Returns path to the overriding manifest file instead of None.
""" """
superproject = self.manifest.superproject have_superproject = manifest.superproject or any(
superproject.SetQuiet(opt.quiet) m.superproject for m in manifest.all_children)
print_messages = git_superproject.PrintMessages(opt.use_superproject, if not have_superproject:
self.manifest) return
superproject.SetPrintMessages(print_messages)
if opt.local_only: if opt.local_only and manifest.superproject:
manifest_path = superproject.manifest_path manifest_path = manifest.superproject.manifest_path
if manifest_path: if manifest_path:
self._ReloadManifest(manifest_path, manifest, load_local_manifests) self._ReloadManifest(manifest_path, manifest)
return manifest_path return
all_projects = self.GetProjects(args, all_projects = self.GetProjects(args,
missing_ok=True, missing_ok=True,
submodules_ok=opt.fetch_submodules) submodules_ok=opt.fetch_submodules,
update_result = superproject.UpdateProjectsRevisionId( manifest=manifest,
all_projects, git_event_log=self.git_event_log) all_manifests=not opt.this_manifest_only)
manifest_path = update_result.manifest_path
superproject_logging_data['updatedrevisionid'] = bool(manifest_path) per_manifest = collections.defaultdict(list)
if manifest_path: manifest_paths = {}
self._ReloadManifest(manifest_path, manifest, load_local_manifests) if opt.this_manifest_only:
per_manifest[manifest.path_prefix] = all_projects
else: else:
if print_messages: for p in all_projects:
print('warning: Update of revisionId from superproject has failed, ' per_manifest[p.manifest.path_prefix].append(p)
'repo sync will not use superproject to fetch the source. ',
'Please resync with the --no-use-superproject option to avoid this repo warning.', superproject_logging_data = {}
file=sys.stderr) need_unload = False
if update_result.fatal and opt.use_superproject is not None: for m in self.ManifestList(opt):
sys.exit(1) if not m.path_prefix in per_manifest:
return manifest_path continue
use_super = git_superproject.UseSuperproject(opt.use_superproject, m)
if superproject_logging_data:
superproject_logging_data['multimanifest'] = True
superproject_logging_data.update(
superproject=use_super,
haslocalmanifests=bool(m.HasLocalManifests),
hassuperprojecttag=bool(m.superproject),
)
if use_super and (m.IsMirror or m.IsArchive):
# Don't use superproject, because we have no working tree.
use_super = False
superproject_logging_data['superproject'] = False
superproject_logging_data['noworktree'] = True
if opt.use_superproject is not False:
print(f'{m.path_prefix}: not using superproject because there is no '
'working tree.')
if not use_super:
continue
m.superproject.SetQuiet(opt.quiet)
print_messages = git_superproject.PrintMessages(opt.use_superproject, m)
m.superproject.SetPrintMessages(print_messages)
update_result = m.superproject.UpdateProjectsRevisionId(
per_manifest[m.path_prefix], git_event_log=self.git_event_log)
manifest_path = update_result.manifest_path
superproject_logging_data['updatedrevisionid'] = bool(manifest_path)
if manifest_path:
m.SetManifestOverride(manifest_path)
need_unload = True
else:
if print_messages:
print(f'{m.path_prefix}: warning: Update of revisionId from '
'superproject has failed, repo sync will not use superproject '
'to fetch the source. ',
'Please resync with the --no-use-superproject option to avoid '
'this repo warning.',
file=sys.stderr)
if update_result.fatal and opt.use_superproject is not None:
sys.exit(1)
if need_unload:
m.outer_client.manifest.Unload()
def _FetchProjectList(self, opt, projects): def _FetchProjectList(self, opt, projects):
"""Main function of the fetch worker. """Main function of the fetch worker.
@ -485,8 +526,8 @@ later is required to fix a server side protocol bug.
return (ret, fetched) return (ret, fetched)
def _FetchMain(self, opt, args, all_projects, err_event, manifest_name, def _FetchMain(self, opt, args, all_projects, err_event,
load_local_manifests, ssh_proxy, manifest): ssh_proxy, manifest):
"""The main network fetch loop. """The main network fetch loop.
Args: Args:
@ -494,8 +535,6 @@ later is required to fix a server side protocol bug.
args: Command line args used to filter out projects. args: Command line args used to filter out projects.
all_projects: List of all projects that should be fetched. all_projects: List of all projects that should be fetched.
err_event: Whether an error was hit while processing. err_event: Whether an error was hit while processing.
manifest_name: Manifest file to be reloaded.
load_local_manifests: Whether to load local manifests.
ssh_proxy: SSH manager for clients & masters. ssh_proxy: SSH manager for clients & masters.
manifest: The manifest to use. manifest: The manifest to use.
@ -526,10 +565,12 @@ later is required to fix a server side protocol bug.
# Iteratively fetch missing and/or nested unregistered submodules # Iteratively fetch missing and/or nested unregistered submodules
previously_missing_set = set() previously_missing_set = set()
while True: while True:
self._ReloadManifest(manifest_name, self.manifest, load_local_manifests) self._ReloadManifest(None, manifest)
all_projects = self.GetProjects(args, all_projects = self.GetProjects(args,
missing_ok=True, missing_ok=True,
submodules_ok=opt.fetch_submodules) submodules_ok=opt.fetch_submodules,
manifest=manifest,
all_manifests=not opt.this_manifest_only)
missing = [] missing = []
for project in all_projects: for project in all_projects:
if project.gitdir not in fetched: if project.gitdir not in fetched:
@ -624,7 +665,7 @@ later is required to fix a server side protocol bug.
for project in projects: for project in projects:
# Make sure pruning never kicks in with shared projects. # Make sure pruning never kicks in with shared projects.
if (not project.use_git_worktrees and if (not project.use_git_worktrees and
len(project.manifest.GetProjectsWithName(project.name)) > 1): len(project.manifest.GetProjectsWithName(project.name, all_manifests=True)) > 1):
if not opt.quiet: if not opt.quiet:
print('\r%s: Shared project %s found, disabling pruning.' % print('\r%s: Shared project %s found, disabling pruning.' %
(project.relpath, project.name)) (project.relpath, project.name))
@ -698,7 +739,7 @@ later is required to fix a server side protocol bug.
t.join() t.join()
pm.end() pm.end()
def _ReloadManifest(self, manifest_name, manifest, load_local_manifests=True): def _ReloadManifest(self, manifest_name, manifest):
"""Reload the manfiest from the file specified by the |manifest_name|. """Reload the manfiest from the file specified by the |manifest_name|.
It unloads the manifest if |manifest_name| is None. It unloads the manifest if |manifest_name| is None.
@ -706,17 +747,29 @@ later is required to fix a server side protocol bug.
Args: Args:
manifest_name: Manifest file to be reloaded. manifest_name: Manifest file to be reloaded.
manifest: The manifest to use. manifest: The manifest to use.
load_local_manifests: Whether to load local manifests.
""" """
if manifest_name: if manifest_name:
# Override calls Unload already # Override calls Unload already
manifest.Override(manifest_name, load_local_manifests=load_local_manifests) manifest.Override(manifest_name)
else: else:
manifest.Unload() manifest.Unload()
def UpdateProjectList(self, opt, manifest): def UpdateProjectList(self, opt, manifest):
"""Update the cached projects list for |manifest|
In a multi-manifest checkout, each manifest has its own project.list.
Args:
opt: Program options returned from optparse. See _Options().
manifest: The manifest to use.
Returns:
0: success
1: failure
"""
new_project_paths = [] new_project_paths = []
for project in self.GetProjects(None, missing_ok=True): for project in self.GetProjects(None, missing_ok=True, manifest=manifest,
all_manifests=False):
if project.relpath: if project.relpath:
new_project_paths.append(project.relpath) new_project_paths.append(project.relpath)
file_name = 'project.list' file_name = 'project.list'
@ -766,7 +819,8 @@ later is required to fix a server side protocol bug.
new_paths = {} new_paths = {}
new_linkfile_paths = [] new_linkfile_paths = []
new_copyfile_paths = [] new_copyfile_paths = []
for project in self.GetProjects(None, missing_ok=True): for project in self.GetProjects(None, missing_ok=True,
manifest=manifest, all_manifests=False):
new_linkfile_paths.extend(x.dest for x in project.linkfiles) new_linkfile_paths.extend(x.dest for x in project.linkfiles)
new_copyfile_paths.extend(x.dest for x in project.copyfiles) new_copyfile_paths.extend(x.dest for x in project.copyfiles)
@ -897,8 +951,40 @@ later is required to fix a server side protocol bug.
return manifest_name return manifest_name
def _UpdateAllManifestProjects(self, opt, mp, manifest_name):
"""Fetch & update the local manifest project.
After syncing the manifest project, if the manifest has any sub manifests,
those are recursively processed.
Args:
opt: Program options returned from optparse. See _Options().
mp: the manifestProject to query.
manifest_name: Manifest file to be reloaded.
"""
if not mp.standalone_manifest_url:
self._UpdateManifestProject(opt, mp, manifest_name)
if mp.manifest.submanifests:
for submanifest in mp.manifest.submanifests.values():
child = submanifest.repo_client.manifest
child.manifestProject.SyncWithPossibleInit(
submanifest,
current_branch_only=self._GetCurrentBranchOnly(opt, child),
verbose=opt.verbose,
tags=opt.tags,
git_event_log=self.git_event_log,
)
self._UpdateAllManifestProjects(opt, child.manifestProject, None)
def _UpdateManifestProject(self, opt, mp, manifest_name): def _UpdateManifestProject(self, opt, mp, manifest_name):
"""Fetch & update the local manifest project.""" """Fetch & update the local manifest project.
Args:
opt: Program options returned from optparse. See _Options().
mp: the manifestProject to query.
manifest_name: Manifest file to be reloaded.
"""
if not opt.local_only: if not opt.local_only:
start = time.time() start = time.time()
success = mp.Sync_NetworkHalf(quiet=opt.quiet, verbose=opt.verbose, success = mp.Sync_NetworkHalf(quiet=opt.quiet, verbose=opt.verbose,
@ -924,6 +1010,7 @@ later is required to fix a server side protocol bug.
if not clean: if not clean:
sys.exit(1) sys.exit(1)
self._ReloadManifest(manifest_name, mp.manifest) self._ReloadManifest(manifest_name, mp.manifest)
if opt.jobs is None: if opt.jobs is None:
self.jobs = mp.manifest.default.sync_j self.jobs = mp.manifest.default.sync_j
@ -948,9 +1035,6 @@ later is required to fix a server side protocol bug.
if opt.prune is None: if opt.prune is None:
opt.prune = True opt.prune = True
if self.outer_client.manifest.is_multimanifest and not opt.this_manifest_only and args:
self.OptionParser.error('partial syncs must use --this-manifest-only')
def Execute(self, opt, args): def Execute(self, opt, args):
if opt.jobs: if opt.jobs:
self.jobs = opt.jobs self.jobs = opt.jobs
@ -959,7 +1043,7 @@ later is required to fix a server side protocol bug.
self.jobs = min(self.jobs, (soft_limit - 5) // 3) self.jobs = min(self.jobs, (soft_limit - 5) // 3)
manifest = self.outer_manifest manifest = self.outer_manifest
if opt.this_manifest_only or not opt.outer_manifest: if not opt.outer_manifest:
manifest = self.manifest manifest = self.manifest
if opt.manifest_name: if opt.manifest_name:
@ -994,39 +1078,26 @@ later is required to fix a server side protocol bug.
'receive updates; run `repo init --repo-rev=stable` to fix.', 'receive updates; run `repo init --repo-rev=stable` to fix.',
file=sys.stderr) file=sys.stderr)
mp = manifest.manifestProject for m in self.ManifestList(opt):
is_standalone_manifest = bool(mp.standalone_manifest_url) mp = m.manifestProject
if not is_standalone_manifest: is_standalone_manifest = bool(mp.standalone_manifest_url)
mp.PreSync() if not is_standalone_manifest:
mp.PreSync()
if opt.repo_upgraded: if opt.repo_upgraded:
_PostRepoUpgrade(manifest, quiet=opt.quiet) _PostRepoUpgrade(m, quiet=opt.quiet)
if not opt.mp_update: if opt.mp_update:
self._UpdateAllManifestProjects(opt, mp, manifest_name)
else:
print('Skipping update of local manifest project.') print('Skipping update of local manifest project.')
elif not is_standalone_manifest:
self._UpdateManifestProject(opt, mp, manifest_name)
load_local_manifests = not manifest.HasLocalManifests superproject_logging_data = {}
use_superproject = git_superproject.UseSuperproject(opt.use_superproject, manifest) self._UpdateProjectsRevisionId(opt, args, superproject_logging_data,
if use_superproject and (manifest.IsMirror or manifest.IsArchive): manifest)
# Don't use superproject, because we have no working tree.
use_superproject = False
if opt.use_superproject is not None:
print('Defaulting to no-use-superproject because there is no working tree.')
superproject_logging_data = {
'superproject': use_superproject,
'haslocalmanifests': bool(manifest.HasLocalManifests),
'hassuperprojecttag': bool(manifest.superproject),
}
if use_superproject:
manifest_name = self._UpdateProjectsRevisionId(
opt, args, load_local_manifests, superproject_logging_data,
manifest) or opt.manifest_name
if self.gitc_manifest: if self.gitc_manifest:
gitc_manifest_projects = self.GetProjects(args, gitc_manifest_projects = self.GetProjects(args, missing_ok=True)
missing_ok=True)
gitc_projects = [] gitc_projects = []
opened_projects = [] opened_projects = []
for project in gitc_manifest_projects: for project in gitc_manifest_projects:
@ -1059,9 +1130,12 @@ later is required to fix a server side protocol bug.
for path in opened_projects] for path in opened_projects]
if not args: if not args:
return return
all_projects = self.GetProjects(args, all_projects = self.GetProjects(args,
missing_ok=True, missing_ok=True,
submodules_ok=opt.fetch_submodules) submodules_ok=opt.fetch_submodules,
manifest=manifest,
all_manifests=not opt.this_manifest_only)
err_network_sync = False err_network_sync = False
err_update_projects = False err_update_projects = False
@ -1073,7 +1147,6 @@ later is required to fix a server side protocol bug.
# Initialize the socket dir once in the parent. # Initialize the socket dir once in the parent.
ssh_proxy.sock() ssh_proxy.sock()
all_projects = self._FetchMain(opt, args, all_projects, err_event, all_projects = self._FetchMain(opt, args, all_projects, err_event,
manifest_name, load_local_manifests,
ssh_proxy, manifest) ssh_proxy, manifest)
if opt.network_only: if opt.network_only:
@ -1090,23 +1163,24 @@ later is required to fix a server side protocol bug.
file=sys.stderr) file=sys.stderr)
sys.exit(1) sys.exit(1)
if manifest.IsMirror or manifest.IsArchive: for m in self.ManifestList(opt):
# bail out now, we have no working tree if m.IsMirror or m.IsArchive:
return # bail out now, we have no working tree
continue
if self.UpdateProjectList(opt, manifest): if self.UpdateProjectList(opt, m):
err_event.set() err_event.set()
err_update_projects = True err_update_projects = True
if opt.fail_fast: if opt.fail_fast:
print('\nerror: Local checkouts *not* updated.', file=sys.stderr) print('\nerror: Local checkouts *not* updated.', file=sys.stderr)
sys.exit(1) sys.exit(1)
err_update_linkfiles = not self.UpdateCopyLinkfileList(manifest) err_update_linkfiles = not self.UpdateCopyLinkfileList(m)
if err_update_linkfiles: if err_update_linkfiles:
err_event.set() err_event.set()
if opt.fail_fast: if opt.fail_fast:
print('\nerror: Local update copyfile or linkfile failed.', file=sys.stderr) print('\nerror: Local update copyfile or linkfile failed.', file=sys.stderr)
sys.exit(1) sys.exit(1)
err_results = [] err_results = []
# NB: We don't exit here because this is the last step. # NB: We don't exit here because this is the last step.
@ -1114,10 +1188,14 @@ later is required to fix a server side protocol bug.
if err_checkout: if err_checkout:
err_event.set() err_event.set()
# If there's a notice that's supposed to print at the end of the sync, print printed_notices = set()
# it now... # If there's a notice that's supposed to print at the end of the sync,
if manifest.notice: # print it now... But avoid printing duplicate messages, and preserve
print(manifest.notice) # order.
for m in sorted(self.ManifestList(opt), key=lambda x: x.path_prefix):
if m.notice and m.notice not in printed_notices:
print(m.notice)
printed_notices.add(m.notice)
# If we saw an error, exit with code 1 so that other scripts can check. # If we saw an error, exit with code 1 so that other scripts can check.
if err_event.is_set(): if err_event.is_set():

View File

@ -421,12 +421,6 @@ Gerrit Code Review: https://www.gerritcodereview.com/
labels = set(_ExpandCommaList(branch.project.config.GetString(key))) labels = set(_ExpandCommaList(branch.project.config.GetString(key)))
for label in opt.labels: for label in opt.labels:
labels.update(_ExpandCommaList(label)) labels.update(_ExpandCommaList(label))
# Basic sanity check on label syntax.
for label in labels:
if not re.match(r'^.+[+-][0-9]+$', label):
print('repo: error: invalid label syntax "%s": labels use forms '
'like CodeReview+1 or Verified-1' % (label,), file=sys.stderr)
sys.exit(1)
# Handle e-mail notifications. # Handle e-mail notifications.
if opt.notify is False: if opt.notify is False:

View File

@ -24,7 +24,6 @@ from unittest import mock
import git_superproject import git_superproject
import git_trace2_event_log import git_trace2_event_log
import manifest_xml import manifest_xml
import platform_utils
from test_manifest_xml import sort_attributes from test_manifest_xml import sort_attributes
@ -38,7 +37,8 @@ class SuperprojectTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
"""Set up superproject every time.""" """Set up superproject every time."""
self.tempdir = tempfile.mkdtemp(prefix='repo_tests') self.tempdirobj = tempfile.TemporaryDirectory(prefix='repo_tests')
self.tempdir = self.tempdirobj.name
self.repodir = os.path.join(self.tempdir, '.repo') self.repodir = os.path.join(self.tempdir, '.repo')
self.manifest_file = os.path.join( self.manifest_file = os.path.join(
self.repodir, manifest_xml.MANIFEST_FILE_NAME) self.repodir, manifest_xml.MANIFEST_FILE_NAME)
@ -75,7 +75,7 @@ class SuperprojectTestCase(unittest.TestCase):
def tearDown(self): def tearDown(self):
"""Tear down superproject every time.""" """Tear down superproject every time."""
platform_utils.rmtree(self.tempdir) self.tempdirobj.cleanup()
def getXmlManifest(self, data): def getXmlManifest(self, data):
"""Helper to initialize a manifest for testing.""" """Helper to initialize a manifest for testing."""

View File

@ -17,7 +17,6 @@
import os import os
import platform import platform
import re import re
import shutil
import tempfile import tempfile
import unittest import unittest
import xml.dom.minidom import xml.dom.minidom
@ -92,7 +91,8 @@ class ManifestParseTestCase(unittest.TestCase):
"""TestCase for parsing manifests.""" """TestCase for parsing manifests."""
def setUp(self): def setUp(self):
self.tempdir = tempfile.mkdtemp(prefix='repo_tests') self.tempdirobj = tempfile.TemporaryDirectory(prefix='repo_tests')
self.tempdir = self.tempdirobj.name
self.repodir = os.path.join(self.tempdir, '.repo') self.repodir = os.path.join(self.tempdir, '.repo')
self.manifest_dir = os.path.join(self.repodir, 'manifests') self.manifest_dir = os.path.join(self.repodir, 'manifests')
self.manifest_file = os.path.join( self.manifest_file = os.path.join(
@ -111,7 +111,7 @@ class ManifestParseTestCase(unittest.TestCase):
""") """)
def tearDown(self): def tearDown(self):
shutil.rmtree(self.tempdir, ignore_errors=True) self.tempdirobj.cleanup()
def getXmlManifest(self, data): def getXmlManifest(self, data):
"""Helper to initialize a manifest for testing.""" """Helper to initialize a manifest for testing."""

View File

@ -17,7 +17,6 @@
import contextlib import contextlib
import os import os
from pathlib import Path from pathlib import Path
import shutil
import subprocess import subprocess
import tempfile import tempfile
import unittest import unittest
@ -32,11 +31,7 @@ import project
@contextlib.contextmanager @contextlib.contextmanager
def TempGitTree(): def TempGitTree():
"""Create a new empty git checkout for testing.""" """Create a new empty git checkout for testing."""
# TODO(vapier): Convert this to tempfile.TemporaryDirectory once we drop with tempfile.TemporaryDirectory(prefix='repo-tests') as tempdir:
# Python 2 support entirely.
try:
tempdir = tempfile.mkdtemp(prefix='repo-tests')
# Tests need to assume, that main is default branch at init, # Tests need to assume, that main is default branch at init,
# which is not supported in config until 2.28. # which is not supported in config until 2.28.
cmd = ['git', 'init'] cmd = ['git', 'init']
@ -50,8 +45,6 @@ def TempGitTree():
cmd += ['--template', templatedir] cmd += ['--template', templatedir]
subprocess.check_call(cmd, cwd=tempdir) subprocess.check_call(cmd, cwd=tempdir)
yield tempdir yield tempdir
finally:
platform_utils.rmtree(tempdir)
class FakeProject(object): class FakeProject(object):
@ -124,14 +117,15 @@ class CopyLinkTestCase(unittest.TestCase):
""" """
def setUp(self): def setUp(self):
self.tempdir = tempfile.mkdtemp(prefix='repo_tests') self.tempdirobj = tempfile.TemporaryDirectory(prefix='repo_tests')
self.tempdir = self.tempdirobj.name
self.topdir = os.path.join(self.tempdir, 'checkout') self.topdir = os.path.join(self.tempdir, 'checkout')
self.worktree = os.path.join(self.topdir, 'git-project') self.worktree = os.path.join(self.topdir, 'git-project')
os.makedirs(self.topdir) os.makedirs(self.topdir)
os.makedirs(self.worktree) os.makedirs(self.worktree)
def tearDown(self): def tearDown(self):
shutil.rmtree(self.tempdir, ignore_errors=True) self.tempdirobj.cleanup()
@staticmethod @staticmethod
def touch(path): def touch(path):

View File

@ -14,11 +14,9 @@
"""Unittests for the wrapper.py module.""" """Unittests for the wrapper.py module."""
import contextlib
from io import StringIO from io import StringIO
import os import os
import re import re
import shutil
import sys import sys
import tempfile import tempfile
import unittest import unittest
@ -26,22 +24,9 @@ from unittest import mock
import git_command import git_command
import main import main
import platform_utils
import wrapper import wrapper
@contextlib.contextmanager
def TemporaryDirectory():
"""Create a new empty git checkout for testing."""
# TODO(vapier): Convert this to tempfile.TemporaryDirectory once we drop
# Python 2 support entirely.
try:
tempdir = tempfile.mkdtemp(prefix='repo-tests')
yield tempdir
finally:
platform_utils.rmtree(tempdir)
def fixture(*paths): def fixture(*paths):
"""Return a path relative to tests/fixtures. """Return a path relative to tests/fixtures.
""" """
@ -336,19 +321,19 @@ class NeedSetupGnuPG(RepoWrapperTestCase):
def test_missing_dir(self): def test_missing_dir(self):
"""The ~/.repoconfig tree doesn't exist yet.""" """The ~/.repoconfig tree doesn't exist yet."""
with TemporaryDirectory() as tempdir: with tempfile.TemporaryDirectory(prefix='repo-tests') as tempdir:
self.wrapper.home_dot_repo = os.path.join(tempdir, 'foo') self.wrapper.home_dot_repo = os.path.join(tempdir, 'foo')
self.assertTrue(self.wrapper.NeedSetupGnuPG()) self.assertTrue(self.wrapper.NeedSetupGnuPG())
def test_missing_keyring(self): def test_missing_keyring(self):
"""The keyring-version file doesn't exist yet.""" """The keyring-version file doesn't exist yet."""
with TemporaryDirectory() as tempdir: with tempfile.TemporaryDirectory(prefix='repo-tests') as tempdir:
self.wrapper.home_dot_repo = tempdir self.wrapper.home_dot_repo = tempdir
self.assertTrue(self.wrapper.NeedSetupGnuPG()) self.assertTrue(self.wrapper.NeedSetupGnuPG())
def test_empty_keyring(self): def test_empty_keyring(self):
"""The keyring-version file exists, but is empty.""" """The keyring-version file exists, but is empty."""
with TemporaryDirectory() as tempdir: with tempfile.TemporaryDirectory(prefix='repo-tests') as tempdir:
self.wrapper.home_dot_repo = tempdir self.wrapper.home_dot_repo = tempdir
with open(os.path.join(tempdir, 'keyring-version'), 'w'): with open(os.path.join(tempdir, 'keyring-version'), 'w'):
pass pass
@ -356,7 +341,7 @@ class NeedSetupGnuPG(RepoWrapperTestCase):
def test_old_keyring(self): def test_old_keyring(self):
"""The keyring-version file exists, but it's old.""" """The keyring-version file exists, but it's old."""
with TemporaryDirectory() as tempdir: with tempfile.TemporaryDirectory(prefix='repo-tests') as tempdir:
self.wrapper.home_dot_repo = tempdir self.wrapper.home_dot_repo = tempdir
with open(os.path.join(tempdir, 'keyring-version'), 'w') as fp: with open(os.path.join(tempdir, 'keyring-version'), 'w') as fp:
fp.write('1.0\n') fp.write('1.0\n')
@ -364,7 +349,7 @@ class NeedSetupGnuPG(RepoWrapperTestCase):
def test_new_keyring(self): def test_new_keyring(self):
"""The keyring-version file exists, and is up-to-date.""" """The keyring-version file exists, and is up-to-date."""
with TemporaryDirectory() as tempdir: with tempfile.TemporaryDirectory(prefix='repo-tests') as tempdir:
self.wrapper.home_dot_repo = tempdir self.wrapper.home_dot_repo = tempdir
with open(os.path.join(tempdir, 'keyring-version'), 'w') as fp: with open(os.path.join(tempdir, 'keyring-version'), 'w') as fp:
fp.write('1000.0\n') fp.write('1000.0\n')
@ -376,7 +361,7 @@ class SetupGnuPG(RepoWrapperTestCase):
def test_full(self): def test_full(self):
"""Make sure it works completely.""" """Make sure it works completely."""
with TemporaryDirectory() as tempdir: with tempfile.TemporaryDirectory(prefix='repo-tests') as tempdir:
self.wrapper.home_dot_repo = tempdir self.wrapper.home_dot_repo = tempdir
self.wrapper.gpg_dir = os.path.join(self.wrapper.home_dot_repo, 'gnupg') self.wrapper.gpg_dir = os.path.join(self.wrapper.home_dot_repo, 'gnupg')
self.assertTrue(self.wrapper.SetupGnuPG(True)) self.assertTrue(self.wrapper.SetupGnuPG(True))
@ -426,7 +411,8 @@ class GitCheckoutTestCase(RepoWrapperTestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
# Create a repo to operate on, but do it once per-class. # Create a repo to operate on, but do it once per-class.
cls.GIT_DIR = tempfile.mkdtemp(prefix='repo-rev-tests') cls.tempdirobj = tempfile.TemporaryDirectory(prefix='repo-rev-tests')
cls.GIT_DIR = cls.tempdirobj.name
run_git = wrapper.Wrapper().run_git run_git = wrapper.Wrapper().run_git
remote = os.path.join(cls.GIT_DIR, 'remote') remote = os.path.join(cls.GIT_DIR, 'remote')
@ -455,10 +441,10 @@ class GitCheckoutTestCase(RepoWrapperTestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
if not cls.GIT_DIR: if not cls.tempdirobj:
return return
shutil.rmtree(cls.GIT_DIR) cls.tempdirobj.cleanup()
class ResolveRepoRev(GitCheckoutTestCase): class ResolveRepoRev(GitCheckoutTestCase):