Compare commits

..

7 Commits

Author SHA1 Message Date
6a872c9dae sync: Added --use-superproject option and support for superproject.
Added "--use-superporject" option to sync.py to fetch project SHAs from
superproject. If there are any missing projects in superprojects, it
prints the missing entries and exits. If there are no missing entries,
it will use SHAs from superproject to fetch the projects from git.

Tested the code with the following commands.

$ ./run_tests tests/test_manifest_xml.py
$ ./run_tests -v tests/test_git_superproject.py
$ ./run_tests -v

Tested the sync code by copying all the repo changes into my Android
AOSP checkout and adding <superporject> tag to default.xml. With
local modification to the code to print the status,

.../WORKING_DIRECTORY$ repo sync --use-superproject
repo: executing 'git clone' url: sso://android/platform/superproject
repo: executing 'git ls-tree'
Success: []

Bug: https://crbug.com/gerrit/13709
Tested-by: Raman Tenneti <rtenneti@google.com>
Change-Id: Id18665992428dd684c04b0e0b3a52f46316873a0
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/293822
Reviewed-by: Mike Frysinger <vapier@google.com>
2021-01-21 19:41:52 +00:00
df6c506a8a launcher: bump version for new release
Change-Id: I8a39630d482fc389cf497399102f795d7e576ff9
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/294122
Reviewed-by: Michael Mortensen <mmortensen@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2021-01-21 16:17:19 +00:00
febe73ff16 Update "evt" field to be logged as a string type.
Testing:
- Unit tests
- Verified git trace log has "evt": "2" (vs "evt": 2 previously)

Bug: https://crbug.com/gerrit/13966
Change-Id: I2e0c98dda0cccdd5cb6328105c11b93cd42676eb
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/294123
Reviewed-by: Jonathan Nieder <jrn@google.com>
Tested-by: Ian Kasprzak <iankaz@google.com>
2021-01-19 20:35:56 +00:00
e5670c8812 launcher: add a requirements framework to declare version dependencies
Currently we don't have a way for the checked out repo version to
declare the version of tools it needs before we start running it.
For somethings, like git, it's not a big deal as it can handle all
the asserts itself.  But for things like Python, it's impossible
to reliably check before executing.

We're in this state now:
- we've been allowing Python 3.4, so the launcher accepts it
- the repo codebase starts using Python 3.6 features
- launcher tries to import us but hits syntax errors
- user is left confused and assuming new repo is broken because
  they're seeing syntax errors

This scenario is playing out with old launchers that still accept
Python 2, and will continue to play out as time goes on and we want
to require newer versions of Python 3.

Lets create a JSON file to declare all these system requirements.
That file format is extremely stable, so loading & parsing from
even ancient versions of Python shouldn't be a problem.  Then the
launcher can read these settings and check the system state before
attempting to execute any code.  If the tools are too old, it can
clearly diagnose & display information to the user as to the real
problem (and not emit tracebacks or syntax errors).

We have a couple of different tool version checks already (git,
python, ssh) and can harmonize them in a single place.

This also allows us to assert a reverse dependency if the need
ever comes up: force the user to upgrade their `repo` launcher
before we'll let them run us.  Even though the launcher warns
whenever a newer release is available, some users seem to ignore
that, or they don't use repo that often (on the scale of years),
and their upgrade jump is so dramatic that they fall back into
the syntax error pit.

Hopefully by the end of the year we can assume enough people
have upgraded their launcher such that we can delete all of the
duplicate version checks in the codebase.  But until then, we'll
keep them to maintain coverage.

Change-Id: I5c12bbffdfd0a8ce978f39aa7f4674026fe9f4f8
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/293003
Reviewed-by: Michael Mortensen <mmortensen@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2021-01-19 16:48:21 +00:00
48b2d10d8f manifest_xml: - Added doc and testing of unknown tags/elements.
Added this test to verify that older versions of repo can handle
"<superproject" element. Tested by adding "<iankaz" unknown element.

Tested the code with the following commands.

$ ./run_tests tests/test_manifest_xml.py
$ ./run_tests -v

Bug: https://crbug.com/gerrit/13709
Tested-by: Raman Tenneti <rtenneti@google.com>
Change-Id: I858d56f38cefcfcd14474efdd631a5a940c3ce47
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/293482
Reviewed-by: Mike Frysinger <vapier@google.com>
2021-01-12 21:49:13 +00:00
0588f3dc52 version: add remote tracking information
This tells us what --repo-rev the user is using.

Bug: https://crbug.com/1164415
Change-Id: Idb6c48e6ca5a4783c529717e6be38266bf7038b0
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/293143
Reviewed-by: Michael Mortensen <mmortensen@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
2021-01-08 20:41:27 +00:00
1bb4fb222d manifest_xml: initial support for <superproject>
At most one superproject may be specified. It will be used
to specify the URL of superproject.

It would have 3 attributes: remote, name, and default.
Only "name" is required while the others have reasonable defaults.

<remote name="superproject-url" review="<url>" />
<superproject remote="superproject-url" name="platform/superproject"/>

TODO: This CL only implements the parsing logic and further work
will be in followup CLs.

Tested the code with the following commands.

$ ./run_tests tests/test_manifest_xml.py
$ ./run_tests -v

Bug: https://crbug.com/gerrit/13709
Tested-by: Raman Tenneti <rtenneti@google.com>
Change-Id: I5b4bba02c8b59601c754cf6b5e4d07a1e16ce167
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/292982
Reviewed-by: Mike Frysinger <vapier@google.com>
2021-01-08 19:49:52 +00:00
14 changed files with 657 additions and 3 deletions

View File

@ -29,6 +29,7 @@ following DTD:
project*, project*,
extend-project*, extend-project*,
repo-hooks?, repo-hooks?,
superproject?,
include*)> include*)>
<!ELEMENT notice (#PCDATA)> <!ELEMENT notice (#PCDATA)>
@ -98,12 +99,22 @@ following DTD:
<!ATTLIST repo-hooks in-project CDATA #REQUIRED> <!ATTLIST repo-hooks in-project CDATA #REQUIRED>
<!ATTLIST repo-hooks enabled-list CDATA #REQUIRED> <!ATTLIST repo-hooks enabled-list CDATA #REQUIRED>
<!ELEMENT superproject (EMPTY)>
<!ATTLIST superproject name CDATA #REQUIRED>
<!ATTLIST superproject remote IDREF #IMPLIED>
<!ELEMENT include EMPTY> <!ELEMENT include EMPTY>
<!ATTLIST include name CDATA #REQUIRED> <!ATTLIST include name CDATA #REQUIRED>
<!ATTLIST include groups CDATA #IMPLIED> <!ATTLIST include groups CDATA #IMPLIED>
]> ]>
``` ```
For compatibility purposes across repo releases, all unknown elements are
silently ignored. However, repo reserves all possible names for itself for
future use. If you want to use custom elements, the `x-*` namespace is
reserved for that purpose, and repo guarantees to never allocate any
corresponding names.
A description of the elements and their attributes follows. A description of the elements and their attributes follows.
@ -377,6 +388,28 @@ defined `project` element.
Attribute `enabled-list`: List of hooks to use, whitespace or comma separated. Attribute `enabled-list`: List of hooks to use, whitespace or comma separated.
### Element superproject
***
*Note*: This is currently a WIP.
***
NB: See the [git superprojects documentation](
https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects) for background
information.
This element is used to specify the URL of the superproject. It has "name" and
"remote" as atrributes. Only "name" is required while the others have
reasonable defaults. At most one superproject may be specified.
Attempting to redefine it will fail to parse.
Attribute `name`: A unique name for the superproject. This attribute has the
same meaning as project's name attribute. See the
[element project](#element-project) for more information.
Attribute `remote`: Name of a previously defined remote element.
If not supplied the remote given by the default element is used.
### Element include ### Element include
This element provides the capability of including another manifest This element provides the capability of including another manifest

View File

@ -13,6 +13,10 @@
# limitations under the License. # limitations under the License.
# URL to file bug reports for repo tool issues.
BUG_REPORT_URL = 'https://bugs.chromium.org/p/gerrit/issues/entry?template=Repo+tool+issue'
class ManifestParseError(Exception): class ManifestParseError(Exception):
"""Failed to parse the manifest file. """Failed to parse the manifest file.
""" """

149
git_superproject.py Normal file
View File

@ -0,0 +1,149 @@
# 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.
"""Provide functionality to get all projects and their SHAs from Superproject.
For more information on superproject, check out:
https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects
Examples:
superproject = Superproject()
project_shas = superproject.GetAllProjectsSHAs()
"""
import os
import sys
from error import GitError
from git_command import GitCommand
import platform_utils
class Superproject(object):
"""Get SHAs from superproject.
It does a 'git clone' of superproject and 'git ls-tree' to get list of SHAs for all projects.
It contains project_shas which is a dictionary with project/sha entries.
"""
def __init__(self, repodir, superproject_dir='exp-superproject'):
"""Initializes superproject.
Args:
repodir: Path to the .repo/ dir for holding all internal checkout state.
superproject_dir: Relative path under |repodir| to checkout superproject.
"""
self._project_shas = None
self._repodir = os.path.abspath(repodir)
self._superproject_dir = superproject_dir
self._superproject_path = os.path.join(self._repodir, superproject_dir)
@property
def project_shas(self):
"""Returns a dictionary of projects and their SHAs."""
return self._project_shas
def _Clone(self, url, branch=None):
"""Do a 'git clone' for the given url and branch.
Args:
url: superproject's url to be passed to git clone.
branch: the branchname to be passed as argument to git clone.
Returns:
True if 'git clone <url> <branch>' is successful, or False.
"""
cmd = ['clone', url, '--depth', '1']
if branch:
cmd += ['--branch', branch]
p = GitCommand(None,
cmd,
cwd=self._superproject_path,
capture_stdout=True,
capture_stderr=True)
retval = p.Wait()
if retval:
# `git clone` is documented to produce an exit status of `128` if
# the requested url or branch are not present in the configuration.
print('repo: error: git clone call failed with return code: %r, stderr: %r' %
(retval, p.stderr), file=sys.stderr)
return False
return True
def _LsTree(self):
"""Returns the data from 'git ls-tree -r HEAD'.
Works only in git repositories.
Returns:
data: data returned from 'git ls-tree -r HEAD' instead of None.
"""
git_dir = os.path.join(self._superproject_path, 'superproject')
if not os.path.exists(git_dir):
raise GitError('git ls-tree. Missing drectory: %s' % git_dir)
data = None
cmd = ['ls-tree', '-z', '-r', 'HEAD']
p = GitCommand(None,
cmd,
cwd=git_dir,
capture_stdout=True,
capture_stderr=True)
retval = p.Wait()
if retval == 0:
data = p.stdout
else:
# `git clone` is documented to produce an exit status of `128` if
# the requested url or branch are not present in the configuration.
print('repo: error: git ls-tree call failed with return code: %r, stderr: %r' % (
retval, p.stderr), file=sys.stderr)
return data
def GetAllProjectsSHAs(self, url, branch=None):
"""Get SHAs for all projects from superproject and save them in _project_shas.
Args:
url: superproject's url to be passed to git clone.
branch: the branchname to be passed as argument to git clone.
Returns:
A dictionary with the projects/SHAs instead of None.
"""
if not url:
raise ValueError('url argument is not supplied.')
if os.path.exists(self._superproject_path):
platform_utils.rmtree(self._superproject_path)
os.mkdir(self._superproject_path)
# TODO(rtenneti): we shouldn't be cloning the repo from scratch every time.
if not self._Clone(url, branch):
raise GitError('git clone failed for url: %s' % url)
data = self._LsTree()
if not data:
raise GitError('git ls-tree failed for url: %s' % url)
# Parse lines like the following to select lines starting with '160000' and
# build a dictionary with project path (last element) and its SHA (3rd element).
#
# 160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00
# 120000 blob acc2cbdf438f9d2141f0ae424cec1d8fc4b5d97f\tbootstrap.bash\x00
shas = {}
for line in data.split('\x00'):
ls_data = line.split(None, 3)
if not ls_data:
break
if ls_data[0] == '160000':
shas[ls_data[3]] = ls_data[2]
self._project_shas = shas
return shas

View File

@ -92,7 +92,7 @@ class EventLog(object):
def _AddVersionEvent(self): def _AddVersionEvent(self):
"""Adds a 'version' event at the beginning of current log.""" """Adds a 'version' event at the beginning of current log."""
version_event = self._CreateEventDict('version') version_event = self._CreateEventDict('version')
version_event['evt'] = 2 version_event['evt'] = "2"
version_event['exe'] = RepoSourceVersion() version_event['exe'] = RepoSourceVersion()
self._log.insert(0, version_event) self._log.insert(0, version_event)

View File

@ -463,6 +463,19 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
' '.join(self._repo_hooks_project.enabled_repo_hooks)) ' '.join(self._repo_hooks_project.enabled_repo_hooks))
root.appendChild(e) root.appendChild(e)
if self._superproject:
root.appendChild(doc.createTextNode(''))
e = doc.createElement('superproject')
e.setAttribute('name', self._superproject['name'])
remoteName = None
if d.remote:
remoteName = d.remote.name
remote = self._superproject.get('remote')
if not d.remote or remote.orig_name != remoteName:
remoteName = remote.orig_name
e.setAttribute('remote', remoteName)
root.appendChild(e)
return doc return doc
def ToDict(self, **kwargs): def ToDict(self, **kwargs):
@ -473,6 +486,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
'default', 'default',
'manifest-server', 'manifest-server',
'repo-hooks', 'repo-hooks',
'superproject',
} }
# Elements that may be repeated. # Elements that may be repeated.
MULTI_ELEMENTS = { MULTI_ELEMENTS = {
@ -544,6 +558,11 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
self._Load() self._Load()
return self._repo_hooks_project return self._repo_hooks_project
@property
def superproject(self):
self._Load()
return self._superproject
@property @property
def notice(self): def notice(self):
self._Load() self._Load()
@ -591,6 +610,7 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
self._remotes = {} self._remotes = {}
self._default = None self._default = None
self._repo_hooks_project = None self._repo_hooks_project = None
self._superproject = {}
self._notice = None self._notice = None
self.branch = None self.branch = None
self._manifest_server = None self._manifest_server = None
@ -793,6 +813,23 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
# Store the enabled hooks in the Project object. # Store the enabled hooks in the Project object.
self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks self._repo_hooks_project.enabled_repo_hooks = enabled_repo_hooks
if node.nodeName == 'superproject':
name = self._reqatt(node, 'name')
# There can only be one superproject.
if self._superproject.get('name'):
raise ManifestParseError(
'duplicate superproject in %s' %
(self.manifestFile))
self._superproject['name'] = name
remote_name = node.getAttribute('remote')
if not remote_name:
remote = self._default.remote
else:
remote = self._get_remote(node)
if remote is None:
raise ManifestParseError("no remote for superproject %s within %s" %
(name, self.manifestFile))
self._superproject['remote'] = remote.ToRemoteSpec(name)
if node.nodeName == 'remove-project': if node.nodeName == 'remove-project':
name = self._reqatt(node, 'name') name = self._reqatt(node, 'name')

View File

@ -1197,6 +1197,9 @@ class Project(object):
raise ManifestInvalidRevisionError('revision %s in %s not found' % raise ManifestInvalidRevisionError('revision %s in %s not found' %
(self.revisionExpr, self.name)) (self.revisionExpr, self.name))
def SetRevisionId(self, revisionId):
self.revisionId = revisionId
def Sync_LocalHalf(self, syncbuf, force_sync=False, submodules=False): def Sync_LocalHalf(self, syncbuf, force_sync=False, submodules=False):
"""Perform only the local IO portion of the sync process. """Perform only the local IO portion of the sync process.
Network access is not required. Network access is not required.

91
repo
View File

@ -147,7 +147,7 @@ if not REPO_REV:
REPO_REV = 'stable' REPO_REV = 'stable'
# increment this whenever we make important changes to this script # increment this whenever we make important changes to this script
VERSION = (2, 11) VERSION = (2, 12)
# increment this if the MAINTAINER_KEYS block is modified # increment this if the MAINTAINER_KEYS block is modified
KEYRING_VERSION = (2, 3) KEYRING_VERSION = (2, 3)
@ -246,6 +246,7 @@ GITC_FS_ROOT_DIR = '/gitc/manifest-rw/'
import collections import collections
import errno import errno
import json
import optparse import optparse
import re import re
import shutil import shutil
@ -1035,6 +1036,90 @@ def _ParseArguments(args):
return cmd, opt, arg return cmd, opt, arg
class Requirements(object):
"""Helper for checking repo's system requirements."""
REQUIREMENTS_NAME = 'requirements.json'
def __init__(self, requirements):
"""Initialize.
Args:
requirements: A dictionary of settings.
"""
self.requirements = requirements
@classmethod
def from_dir(cls, path):
return cls.from_file(os.path.join(path, cls.REQUIREMENTS_NAME))
@classmethod
def from_file(cls, path):
try:
with open(path, 'rb') as f:
data = f.read()
except EnvironmentError:
# NB: EnvironmentError is used for Python 2 & 3 compatibility.
# If we couldn't open the file, assume it's an old source tree.
return None
return cls.from_data(data)
@classmethod
def from_data(cls, data):
comment_line = re.compile(br'^ *#')
strip_data = b''.join(x for x in data.splitlines() if not comment_line.match(x))
try:
json_data = json.loads(strip_data)
except Exception: # pylint: disable=broad-except
# If we couldn't parse it, assume it's incompatible.
return None
return cls(json_data)
def _get_soft_ver(self, pkg):
"""Return the soft version for |pkg| if it exists."""
return self.requirements.get(pkg, {}).get('soft', ())
def _get_hard_ver(self, pkg):
"""Return the hard version for |pkg| if it exists."""
return self.requirements.get(pkg, {}).get('hard', ())
@staticmethod
def _format_ver(ver):
"""Return a dotted version from |ver|."""
return '.'.join(str(x) for x in ver)
def assert_ver(self, pkg, curr_ver):
"""Verify |pkg|'s |curr_ver| is new enough."""
curr_ver = tuple(curr_ver)
soft_ver = tuple(self._get_soft_ver(pkg))
hard_ver = tuple(self._get_hard_ver(pkg))
if curr_ver < hard_ver:
print('repo: error: Your version of "%s" (%s) is unsupported; '
'Please upgrade to at least version %s to continue.' %
(pkg, self._format_ver(curr_ver), self._format_ver(soft_ver)),
file=sys.stderr)
sys.exit(1)
if curr_ver < soft_ver:
print('repo: warning: Your version of "%s" (%s) is no longer supported; '
'Please upgrade to at least version %s to avoid breakage.' %
(pkg, self._format_ver(curr_ver), self._format_ver(soft_ver)),
file=sys.stderr)
def assert_all(self):
"""Assert all of the requirements are satisified."""
# See if we need a repo launcher upgrade first.
self.assert_ver('repo', VERSION)
# Check python before we try to import the repo code.
self.assert_ver('python', sys.version_info)
# Check git while we're at it.
self.assert_ver('git', ParseGitVersion())
def _Usage(): def _Usage():
gitc_usage = "" gitc_usage = ""
if get_gitc_manifest_dir(): if get_gitc_manifest_dir():
@ -1192,6 +1277,10 @@ def main(orig_args):
print("fatal: unable to find repo entry point", file=sys.stderr) print("fatal: unable to find repo entry point", file=sys.stderr)
sys.exit(1) sys.exit(1)
reqs = Requirements.from_dir(os.path.dirname(repo_main))
if reqs:
reqs.assert_all()
ver_str = '.'.join(map(str, VERSION)) ver_str = '.'.join(map(str, VERSION))
me = [sys.executable, repo_main, me = [sys.executable, repo_main,
'--repo-dir=%s' % rel_repo_dir, '--repo-dir=%s' % rel_repo_dir,

57
requirements.json Normal file
View File

@ -0,0 +1,57 @@
# This file declares various requirements for this version of repo. The
# launcher script will load it and check the constraints before trying to run
# us. This avoids issues of the launcher using an old version of Python (e.g.
# 3.5) while the codebase has moved on to requiring something much newer (e.g.
# 3.8). If the launcher tried to import us, it would fail with syntax errors.
# This is a JSON file with line-level comments allowed.
# Always keep backwards compatibility in mine. The launcher script is robust
# against missing values, but when a field is renamed/removed, it means older
# versions of the launcher script won't be able to enforce the constraint.
# When requiring versions, always use lists as they are easy to parse & compare
# in Python. Strings would require futher processing to turn into a list.
# Version constraints should be expressed in pairs: soft & hard. Soft versions
# are when we start warning users that their software too old and we're planning
# on dropping support for it, so they need to start planning system upgrades.
# Hard versions are when we refuse to work the tool. Users will be shown an
# error message before we abort entirely.
# When deciding whether to upgrade a version requirement, check out the distro
# lists to see who will be impacted:
# https://gerrit.googlesource.com/git-repo/+/HEAD/docs/release-process.md#Project-References
{
# The repo launcher itself. This allows us to force people to upgrade as some
# ignore the warnings about it being out of date, or install ancient versions
# to start with for whatever reason.
#
# NB: Repo launchers started checking this file with repo-2.12, so listing
# versions older than that won't make a difference.
"repo": {
"hard": [2, 11],
"soft": [2, 11]
},
# Supported Python versions.
#
# python-3.6 is in Ubuntu Bionic.
# python-3.5 is in Debian Stretch.
"python": {
"hard": [3, 5],
"soft": [3, 6]
},
# Supported git versions.
#
# git-1.7.2 is in Debian Squeeze.
# git-1.7.9 is in Ubuntu Precise.
# git-1.9.1 is in Ubuntu Trusty.
# git-1.7.10 is in Debian Wheezy.
"git": {
"hard": [1, 7, 2],
"soft": [1, 9, 1]
}
}

View File

@ -51,11 +51,12 @@ import event_log
from git_command import GIT, git_require from git_command import GIT, git_require
from git_config import GetUrlCookieFile from git_config import GetUrlCookieFile
from git_refs import R_HEADS, HEAD from git_refs import R_HEADS, HEAD
import git_superproject
import gitc_utils import gitc_utils
from project import Project from project import Project
from project import RemoteSpec from project import RemoteSpec
from command import Command, MirrorSafeCommand from command import Command, MirrorSafeCommand
from error import RepoChangedException, GitError, ManifestParseError from error import BUG_REPORT_URL, RepoChangedException, GitError, ManifestParseError
import platform_utils import platform_utils
from project import SyncBuffer from project import SyncBuffer
from progress import Progress from progress import Progress
@ -241,6 +242,8 @@ later is required to fix a server side protocol bug.
p.add_option('--fetch-submodules', p.add_option('--fetch-submodules',
dest='fetch_submodules', action='store_true', dest='fetch_submodules', action='store_true',
help='fetch submodules from server') help='fetch submodules from server')
p.add_option('--use-superproject', action='store_true',
help='use the manifest superproject to sync projects')
p.add_option('--no-tags', p.add_option('--no-tags',
dest='tags', default=True, action='store_false', dest='tags', default=True, action='store_false',
help="don't fetch tags") help="don't fetch tags")
@ -894,6 +897,41 @@ later is required to fix a server side protocol bug.
missing_ok=True, missing_ok=True,
submodules_ok=opt.fetch_submodules) submodules_ok=opt.fetch_submodules)
if opt.use_superproject:
if not self.manifest.superproject:
print('error: superproject tag is not defined in manifest.xml',
file=sys.stderr)
sys.exit(1)
print('WARNING: --use-superproject is experimental and not '
'for general use', file=sys.stderr)
superproject_url = self.manifest.superproject['remote'].url
if not superproject_url:
print('error: superproject URL is not defined in manifest.xml',
file=sys.stderr)
sys.exit(1)
superproject = git_superproject.Superproject(self.manifest.repodir)
try:
superproject_shas = superproject.GetAllProjectsSHAs(url=superproject_url)
except Exception as e:
print('error: Cannot get project SHAs for %s: %s: %s' %
(superproject_url, type(e).__name__, str(e)),
file=sys.stderr)
sys.exit(1)
projects_missing_shas = []
for project in all_projects:
path = project.relpath
if not path:
continue
sha = superproject_shas.get(path)
if sha:
project.SetRevisionId(sha)
else:
projects_missing_shas.append(path)
if projects_missing_shas:
print('error: please file a bug using %s to report missing shas for: %s' %
(BUG_REPORT_URL, projects_missing_shas), file=sys.stderr)
sys.exit(1)
err_network_sync = False err_network_sync = False
err_update_projects = False err_update_projects = False
err_checkout = False err_checkout = False

View File

@ -33,12 +33,14 @@ class Version(Command, MirrorSafeCommand):
def Execute(self, opt, args): def Execute(self, opt, args):
rp = self.manifest.repoProject rp = self.manifest.repoProject
rem = rp.GetRemote(rp.remote.name) rem = rp.GetRemote(rp.remote.name)
branch = rp.GetBranch('default')
# These might not be the same. Report them both. # These might not be the same. Report them both.
src_ver = RepoSourceVersion() src_ver = RepoSourceVersion()
rp_ver = rp.bare_git.describe(HEAD) rp_ver = rp.bare_git.describe(HEAD)
print('repo version %s' % rp_ver) print('repo version %s' % rp_ver)
print(' (from %s)' % rem.url) print(' (from %s)' % rem.url)
print(' (tracking %s)' % branch.merge)
print(' (%s)' % rp.bare_git.log('-1', '--format=%cD', HEAD)) print(' (%s)' % rp.bare_git.log('-1', '--format=%cD', HEAD))
if self.wrapper_path is not None: if self.wrapper_path is not None:

View File

@ -0,0 +1,82 @@
# 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.
"""Unittests for the git_superproject.py module."""
import os
import tempfile
import unittest
from unittest import mock
from error import GitError
import git_superproject
import platform_utils
class SuperprojectTestCase(unittest.TestCase):
"""TestCase for the Superproject module."""
def setUp(self):
"""Set up superproject every time."""
self.tempdir = tempfile.mkdtemp(prefix='repo_tests')
self.repodir = os.path.join(self.tempdir, '.repo')
os.mkdir(self.repodir)
self._superproject = git_superproject.Superproject(self.repodir)
def tearDown(self):
"""Tear down superproject every time."""
platform_utils.rmtree(self.tempdir)
def test_superproject_get_project_shas_no_url(self):
"""Test with no url."""
with self.assertRaises(ValueError):
self._superproject.GetAllProjectsSHAs(url=None)
def test_superproject_get_project_shas_invalid_url(self):
"""Test with an invalid url."""
with self.assertRaises(GitError):
self._superproject.GetAllProjectsSHAs(url='localhost')
def test_superproject_get_project_shas_invalid_branch(self):
"""Test with an invalid branch."""
with self.assertRaises(GitError):
self._superproject.GetAllProjectsSHAs(
url='sso://android/platform/superproject',
branch='junk')
def test_superproject_get_project_shas_mock_clone(self):
"""Test with _Clone failing."""
with self.assertRaises(GitError):
with mock.patch.object(self._superproject, '_Clone', return_value=False):
self._superproject.GetAllProjectsSHAs(url='localhost')
def test_superproject_get_project_shas_mock_ls_tree(self):
"""Test with LsTree being a mock."""
data = ('120000 blob 158258bdf146f159218e2b90f8b699c4d85b5804\tAndroid.bp\x00'
'160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00'
'160000 commit e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06\tbootable/recovery\x00'
'120000 blob acc2cbdf438f9d2141f0ae424cec1d8fc4b5d97f\tbootstrap.bash\x00'
'160000 commit ade9b7a0d874e25fff4bf2552488825c6f111928\tbuild/bazel\x00')
with mock.patch.object(self._superproject, '_Clone', return_value=True):
with mock.patch.object(self._superproject, '_LsTree', return_value=data):
shas = self._superproject.GetAllProjectsSHAs(url='localhost', branch='junk')
self.assertEqual(shas, {
'art': '2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea',
'bootable/recovery': 'e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06',
'build/bazel': 'ade9b7a0d874e25fff4bf2552488825c6f111928'
})
if __name__ == '__main__':
unittest.main()

View File

@ -95,6 +95,8 @@ class EventLogTestCase(unittest.TestCase):
# Check for 'version' event specific fields. # Check for 'version' event specific fields.
self.assertIn('evt', version_event) self.assertIn('evt', version_event)
self.assertIn('exe', version_event) self.assertIn('exe', version_event)
# Verify "evt" version field is a string.
self.assertIsInstance(version_event['evt'], str)
def test_start_event(self): def test_start_event(self):
"""Test and validate 'start' event data is valid. """Test and validate 'start' event data is valid.

View File

@ -221,6 +221,88 @@ class XmlManifestTests(unittest.TestCase):
self.assertEqual(manifest.repo_hooks_project.name, 'repohooks') self.assertEqual(manifest.repo_hooks_project.name, 'repohooks')
self.assertEqual(manifest.repo_hooks_project.enabled_repo_hooks, ['a', 'b']) self.assertEqual(manifest.repo_hooks_project.enabled_repo_hooks, ['a', 'b'])
def test_superproject(self):
"""Check superproject settings."""
manifest = self.getXmlManifest("""
<manifest>
<remote name="test-remote" fetch="http://localhost" />
<default remote="test-remote" revision="refs/heads/main" />
<superproject name="superproject"/>
</manifest>
""")
self.assertEqual(manifest.superproject['name'], 'superproject')
self.assertEqual(manifest.superproject['remote'].name, 'test-remote')
self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/superproject')
self.assertEqual(
manifest.ToXml().toxml(),
'<?xml version="1.0" ?><manifest>' +
'<remote name="test-remote" fetch="http://localhost"/>' +
'<default remote="test-remote" revision="refs/heads/main"/>' +
'<superproject name="superproject"/>' +
'</manifest>')
def test_superproject_with_remote(self):
"""Check superproject settings."""
manifest = self.getXmlManifest("""
<manifest>
<remote name="default-remote" fetch="http://localhost" />
<remote name="superproject-remote" fetch="http://localhost" />
<default remote="default-remote" revision="refs/heads/main" />
<superproject name="platform/superproject" remote="superproject-remote"/>
</manifest>
""")
self.assertEqual(manifest.superproject['name'], 'platform/superproject')
self.assertEqual(manifest.superproject['remote'].name, 'superproject-remote')
self.assertEqual(manifest.superproject['remote'].url, 'http://localhost/platform/superproject')
self.assertEqual(
manifest.ToXml().toxml(),
'<?xml version="1.0" ?><manifest>' +
'<remote name="default-remote" fetch="http://localhost"/>' +
'<remote name="superproject-remote" fetch="http://localhost"/>' +
'<default remote="default-remote" revision="refs/heads/main"/>' +
'<superproject name="platform/superproject" remote="superproject-remote"/>' +
'</manifest>')
def test_superproject_with_defalut_remote(self):
"""Check superproject settings."""
manifest = self.getXmlManifest("""
<manifest>
<remote name="default-remote" fetch="http://localhost" />
<default remote="default-remote" revision="refs/heads/main" />
<superproject name="superproject" remote="default-remote"/>
</manifest>
""")
self.assertEqual(manifest.superproject['name'], 'superproject')
self.assertEqual(manifest.superproject['remote'].name, 'default-remote')
self.assertEqual(
manifest.ToXml().toxml(),
'<?xml version="1.0" ?><manifest>' +
'<remote name="default-remote" fetch="http://localhost"/>' +
'<default remote="default-remote" revision="refs/heads/main"/>' +
'<superproject name="superproject"/>' +
'</manifest>')
def test_unknown_tags(self):
"""Check superproject settings."""
manifest = self.getXmlManifest("""
<manifest>
<remote name="test-remote" fetch="http://localhost" />
<default remote="test-remote" revision="refs/heads/main" />
<superproject name="superproject"/>
<iankaz value="unknown (possible) future tags are ignored"/>
<x-custom-tag>X tags are always ignored</x-custom-tag>
</manifest>
""")
self.assertEqual(manifest.superproject['name'], 'superproject')
self.assertEqual(manifest.superproject['remote'].name, 'test-remote')
self.assertEqual(
manifest.ToXml().toxml(),
'<?xml version="1.0" ?><manifest>' +
'<remote name="test-remote" fetch="http://localhost"/>' +
'<default remote="test-remote" revision="refs/heads/main"/>' +
'<superproject name="superproject"/>' +
'</manifest>')
def test_project_group(self): def test_project_group(self):
"""Check project group settings.""" """Check project group settings."""
manifest = self.getXmlManifest(""" manifest = self.getXmlManifest("""

View File

@ -19,6 +19,7 @@ from io import StringIO
import os import os
import re import re
import shutil import shutil
import sys
import tempfile import tempfile
import unittest import unittest
from unittest import mock from unittest import mock
@ -255,6 +256,81 @@ class CheckGitVersion(RepoWrapperTestCase):
self.wrapper._CheckGitVersion() self.wrapper._CheckGitVersion()
class Requirements(RepoWrapperTestCase):
"""Check Requirements handling."""
def test_missing_file(self):
"""Don't crash if the file is missing (old version)."""
testdir = os.path.dirname(os.path.realpath(__file__))
self.assertIsNone(self.wrapper.Requirements.from_dir(testdir))
self.assertIsNone(self.wrapper.Requirements.from_file(
os.path.join(testdir, 'xxxxxxxxxxxxxxxxxxxxxxxx')))
def test_corrupt_data(self):
"""If the file can't be parsed, don't blow up."""
self.assertIsNone(self.wrapper.Requirements.from_file(__file__))
self.assertIsNone(self.wrapper.Requirements.from_data(b'x'))
def test_valid_data(self):
"""Make sure we can parse the file we ship."""
self.assertIsNotNone(self.wrapper.Requirements.from_data(b'{}'))
rootdir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
self.assertIsNotNone(self.wrapper.Requirements.from_dir(rootdir))
self.assertIsNotNone(self.wrapper.Requirements.from_file(os.path.join(
rootdir, 'requirements.json')))
def test_format_ver(self):
"""Check format_ver can format."""
self.assertEqual('1.2.3', self.wrapper.Requirements._format_ver((1, 2, 3)))
self.assertEqual('1', self.wrapper.Requirements._format_ver([1]))
def test_assert_all_unknown(self):
"""Check assert_all works with incompatible file."""
reqs = self.wrapper.Requirements({})
reqs.assert_all()
def test_assert_all_new_repo(self):
"""Check assert_all accepts new enough repo."""
reqs = self.wrapper.Requirements({'repo': {'hard': [1, 0]}})
reqs.assert_all()
def test_assert_all_old_repo(self):
"""Check assert_all rejects old repo."""
reqs = self.wrapper.Requirements({'repo': {'hard': [99999, 0]}})
with self.assertRaises(SystemExit):
reqs.assert_all()
def test_assert_all_new_python(self):
"""Check assert_all accepts new enough python."""
reqs = self.wrapper.Requirements({'python': {'hard': sys.version_info}})
reqs.assert_all()
def test_assert_all_old_repo(self):
"""Check assert_all rejects old repo."""
reqs = self.wrapper.Requirements({'python': {'hard': [99999, 0]}})
with self.assertRaises(SystemExit):
reqs.assert_all()
def test_assert_ver_unknown(self):
"""Check assert_ver works with incompatible file."""
reqs = self.wrapper.Requirements({})
reqs.assert_ver('xxx', (1, 0))
def test_assert_ver_new(self):
"""Check assert_ver allows new enough versions."""
reqs = self.wrapper.Requirements({'git': {'hard': [1, 0], 'soft': [2, 0]}})
reqs.assert_ver('git', (1, 0))
reqs.assert_ver('git', (1, 5))
reqs.assert_ver('git', (2, 0))
reqs.assert_ver('git', (2, 5))
def test_assert_ver_old(self):
"""Check assert_ver rejects old versions."""
reqs = self.wrapper.Requirements({'git': {'hard': [1, 0], 'soft': [2, 0]}})
with self.assertRaises(SystemExit):
reqs.assert_ver('git', (0, 5))
class NeedSetupGnuPG(RepoWrapperTestCase): class NeedSetupGnuPG(RepoWrapperTestCase):
"""Check NeedSetupGnuPG behavior.""" """Check NeedSetupGnuPG behavior."""