Format codebase with black and check formatting in CQ

Apply rules set by https://gerrit-review.googlesource.com/c/git-repo/+/362954/ across the codebase and fix any lingering errors caught
by flake8. Also check black formatting in run_tests (and CQ).

Bug: b/267675342
Change-Id: I972d77649dac351150dcfeb1cd1ad0ea2efc1956
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/363474
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
This commit is contained in:
Gavin Mak 2023-03-11 06:46:20 +00:00 committed by LUCI
parent 1604cf255f
commit ea2e330e43
79 changed files with 19698 additions and 16679 deletions

View File

@ -1,5 +1,8 @@
[flake8]
max-line-length = 80
per-file-ignores =
# E501: line too long
tests/test_git_superproject.py: E501
extend-ignore =
# E203: Whitespace before ':'
# See https://github.com/PyCQA/pycodestyle/issues/373

View File

@ -17,23 +17,20 @@ import sys
import pager
COLORS = {None: -1,
'normal': -1,
'black': 0,
'red': 1,
'green': 2,
'yellow': 3,
'blue': 4,
'magenta': 5,
'cyan': 6,
'white': 7}
COLORS = {
None: -1,
"normal": -1,
"black": 0,
"red": 1,
"green": 2,
"yellow": 3,
"blue": 4,
"magenta": 5,
"cyan": 6,
"white": 7,
}
ATTRS = {None: -1,
'bold': 1,
'dim': 2,
'ul': 4,
'blink': 5,
'reverse': 7}
ATTRS = {None: -1, "bold": 1, "dim": 2, "ul": 4, "blink": 5, "reverse": 7}
RESET = "\033[m"
@ -56,30 +53,30 @@ def _Color(fg=None, bg=None, attr=None):
code = "\033["
if attr >= 0:
code += chr(ord('0') + attr)
code += chr(ord("0") + attr)
need_sep = True
if fg >= 0:
if need_sep:
code += ';'
code += ";"
need_sep = True
if fg < 8:
code += '3%c' % (ord('0') + fg)
code += "3%c" % (ord("0") + fg)
else:
code += '38;5;%d' % fg
code += "38;5;%d" % fg
if bg >= 0:
if need_sep:
code += ';'
code += ";"
if bg < 8:
code += '4%c' % (ord('0') + bg)
code += "4%c" % (ord("0") + bg)
else:
code += '48;5;%d' % bg
code += 'm'
code += "48;5;%d" % bg
code += "m"
else:
code = ''
code = ""
return code
@ -97,17 +94,17 @@ def SetDefaultColoring(state):
global DEFAULT
state = state.lower()
if state in ('auto',):
if state in ("auto",):
DEFAULT = state
elif state in ('always', 'yes', 'true', True):
DEFAULT = 'always'
elif state in ('never', 'no', 'false', False):
DEFAULT = 'never'
elif state in ("always", "yes", "true", True):
DEFAULT = "always"
elif state in ("never", "no", "false", False):
DEFAULT = "never"
class Coloring(object):
def __init__(self, config, section_type):
self._section = 'color.%s' % section_type
self._section = "color.%s" % section_type
self._config = config
self._out = sys.stdout
@ -115,14 +112,14 @@ class Coloring(object):
if on is None:
on = self._config.GetString(self._section)
if on is None:
on = self._config.GetString('color.ui')
on = self._config.GetString("color.ui")
if on == 'auto':
if on == "auto":
if pager.active or os.isatty(1):
self._on = True
else:
self._on = False
elif on in ('true', 'always'):
elif on in ("true", "always"):
self._on = True
else:
self._on = False
@ -141,7 +138,7 @@ class Coloring(object):
self._out.flush()
def nl(self):
self._out.write('\n')
self._out.write("\n")
def printer(self, opt=None, fg=None, bg=None, attr=None):
s = self
@ -149,6 +146,7 @@ class Coloring(object):
def f(fmt, *args):
s._out.write(c(fmt, *args))
return f
def nofmt_printer(self, opt=None, fg=None, bg=None, attr=None):
@ -157,6 +155,7 @@ class Coloring(object):
def f(fmt):
s._out.write(c(fmt))
return f
def colorer(self, opt=None, fg=None, bg=None, attr=None):
@ -165,12 +164,14 @@ class Coloring(object):
def f(fmt, *args):
output = fmt % args
return ''.join([c, output, RESET])
return "".join([c, output, RESET])
return f
else:
def f(fmt, *args):
return fmt % args
return f
def nofmt_colorer(self, opt=None, fg=None, bg=None, attr=None):
@ -178,29 +179,32 @@ class Coloring(object):
c = self._parse(opt, fg, bg, attr)
def f(fmt):
return ''.join([c, fmt, RESET])
return "".join([c, fmt, RESET])
return f
else:
def f(fmt):
return fmt
return f
def _parse(self, opt, fg, bg, attr):
if not opt:
return _Color(fg, bg, attr)
v = self._config.GetString('%s.%s' % (self._section, opt))
v = self._config.GetString("%s.%s" % (self._section, opt))
if v is None:
return _Color(fg, bg, attr)
v = v.strip().lower()
if v == "reset":
return RESET
elif v == '':
elif v == "":
return _Color(fg, bg, attr)
have_fg = False
for a in v.split(' '):
for a in v.split(" "):
if is_color(a):
if have_fg:
bg = a

View File

@ -25,7 +25,7 @@ import progress
# Are we generating man-pages?
GENERATE_MANPAGES = os.environ.get('_REPO_GENERATE_MANPAGES_') == ' indeed! '
GENERATE_MANPAGES = os.environ.get("_REPO_GENERATE_MANPAGES_") == " indeed! "
# Number of projects to submit to a single worker process at a time.
@ -43,8 +43,7 @@ DEFAULT_LOCAL_JOBS = min(os.cpu_count(), 8)
class Command(object):
"""Base class for any command line action in repo.
"""
"""Base class for any command line action in repo."""
# Singleton for all commands to track overall repo command execution and
# provide event summary to callers. Only used by sync subcommand currently.
@ -52,9 +51,9 @@ class Command(object):
# NB: This is being replaced by git trace2 events. See git_trace2_event_log.
event_log = EventLog()
# Whether this command is a "common" one, i.e. whether the user would commonly
# use it or it's a more uncommon command. This is used by the help command to
# show short-vs-full summaries.
# Whether this command is a "common" one, i.e. whether the user would
# commonly use it or it's a more uncommon command. This is used by the help
# command to show short-vs-full summaries.
COMMON = False
# Whether this command supports running in parallel. If greater than 0,
@ -67,8 +66,16 @@ class Command(object):
# migrated subcommands can set it to False.
MULTI_MANIFEST_SUPPORT = True
def __init__(self, repodir=None, client=None, manifest=None, gitc_manifest=None,
git_event_log=None, outer_client=None, outer_manifest=None):
def __init__(
self,
repodir=None,
client=None,
manifest=None,
gitc_manifest=None,
git_event_log=None,
outer_client=None,
outer_manifest=None,
):
self.repodir = repodir
self.client = client
self.outer_client = outer_client or client
@ -93,8 +100,8 @@ class Command(object):
opt_value = getattr(opts, opt_key)
# If the value is set, it means the user has passed it as a command
# line option, and we should use that. Otherwise we can try to set it
# with the value from the corresponding environment variable.
# line option, and we should use that. Otherwise we can try to set
# it with the value from the corresponding environment variable.
if opt_value is not None:
continue
@ -108,11 +115,13 @@ class Command(object):
def OptionParser(self):
if self._optparse is None:
try:
me = 'repo %s' % self.NAME
usage = self.helpUsage.strip().replace('%prog', me)
me = "repo %s" % self.NAME
usage = self.helpUsage.strip().replace("%prog", me)
except AttributeError:
usage = 'repo %s' % self.NAME
epilog = 'Run `repo help %s` to view the detailed manual.' % self.NAME
usage = "repo %s" % self.NAME
epilog = (
"Run `repo help %s` to view the detailed manual." % self.NAME
)
self._optparse = optparse.OptionParser(usage=usage, epilog=epilog)
self._CommonOptions(self._optparse)
self._Options(self._optparse)
@ -124,35 +133,63 @@ class Command(object):
These will show up for *all* subcommands, so use sparingly.
NB: Keep in sync with repo:InitParser().
"""
g = p.add_option_group('Logging options')
opts = ['-v'] if opt_v else []
g.add_option(*opts, '--verbose',
dest='output_mode', action='store_true',
help='show all output')
g.add_option('-q', '--quiet',
dest='output_mode', action='store_false',
help='only show errors')
g = p.add_option_group("Logging options")
opts = ["-v"] if opt_v else []
g.add_option(
*opts,
"--verbose",
dest="output_mode",
action="store_true",
help="show all output",
)
g.add_option(
"-q",
"--quiet",
dest="output_mode",
action="store_false",
help="only show errors",
)
if self.PARALLEL_JOBS is not None:
default = 'based on number of CPU cores'
default = "based on number of CPU cores"
if not GENERATE_MANPAGES:
# Only include active cpu count if we aren't generating man pages.
default = f'%default; {default}'
# Only include active cpu count if we aren't generating man
# pages.
default = f"%default; {default}"
p.add_option(
'-j', '--jobs',
type=int, default=self.PARALLEL_JOBS,
help=f'number of jobs to run in parallel (default: {default})')
"-j",
"--jobs",
type=int,
default=self.PARALLEL_JOBS,
help=f"number of jobs to run in parallel (default: {default})",
)
m = p.add_option_group('Multi-manifest options')
m.add_option('--outer-manifest', action='store_true', default=None,
help='operate starting at the outermost manifest')
m.add_option('--no-outer-manifest', dest='outer_manifest',
action='store_false', help='do not operate on outer manifests')
m.add_option('--this-manifest-only', action='store_true', default=None,
help='only operate on this (sub)manifest')
m.add_option('--no-this-manifest-only', '--all-manifests',
dest='this_manifest_only', action='store_false',
help='operate on this manifest and its submanifests')
m = p.add_option_group("Multi-manifest options")
m.add_option(
"--outer-manifest",
action="store_true",
default=None,
help="operate starting at the outermost manifest",
)
m.add_option(
"--no-outer-manifest",
dest="outer_manifest",
action="store_false",
help="do not operate on outer manifests",
)
m.add_option(
"--this-manifest-only",
action="store_true",
default=None,
help="only operate on this (sub)manifest",
)
m.add_option(
"--no-this-manifest-only",
"--all-manifests",
dest="this_manifest_only",
action="store_false",
help="operate on this manifest and its submanifests",
)
def _Options(self, p):
"""Initialize the option parser with subcommand-specific options."""
@ -176,8 +213,7 @@ class Command(object):
return {}
def Usage(self):
"""Display usage and terminate.
"""
"""Display usage and terminate."""
self.OptionParser.print_usage()
sys.exit(1)
@ -186,8 +222,8 @@ class Command(object):
opt.quiet = opt.output_mode is False
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.
# By default, treat multi-manifest instances as a single manifest
# from the user's perspective.
opt.outer_manifest = True
def ValidateOptions(self, opt, args):
@ -201,12 +237,13 @@ class Command(object):
"""
def Execute(self, opt, args):
"""Perform the action, after option parsing is complete.
"""
"""Perform the action, after option parsing is complete."""
raise NotImplementedError
@staticmethod
def ExecuteInParallel(jobs, func, inputs, callback, output=None, ordered=False):
def ExecuteInParallel(
jobs, func, inputs, callback, output=None, ordered=False
):
"""Helper for managing parallel execution boiler plate.
For subcommands that can easily split their work up.
@ -214,18 +251,21 @@ class Command(object):
Args:
jobs: How many parallel processes to use.
func: The function to apply to each of the |inputs|. Usually a
functools.partial for wrapping additional arguments. It will be run
in a separate process, so it must be pickalable, so nested functions
won't work. Methods on the subcommand Command class should work.
functools.partial for wrapping additional arguments. It will be
run in a separate process, so it must be pickalable, so nested
functions won't work. Methods on the subcommand Command class
should work.
inputs: The list of items to process. Must be a list.
callback: The function to pass the results to for processing. It will be
executed in the main thread and process the results of |func| as they
become available. Thus it may be a local nested function. Its return
value is passed back directly. It takes three arguments:
callback: The function to pass the results to for processing. It
will be executed in the main thread and process the results of
|func| as they become available. Thus it may be a local nested
function. Its return value is passed back directly. It takes
three arguments:
- The processing pool (or None with one job).
- The |output| argument.
- An iterator for the results.
output: An output manager. May be progress.Progess or color.Coloring.
output: An output manager. May be progress.Progess or
color.Coloring.
ordered: Whether the jobs should be processed in order.
Returns:
@ -238,7 +278,11 @@ class Command(object):
else:
with multiprocessing.Pool(jobs) as pool:
submit = pool.imap if ordered else pool.imap_unordered
return callback(pool, output, submit(func, inputs, chunksize=WORKER_BATCH_SIZE))
return callback(
pool,
output,
submit(func, inputs, chunksize=WORKER_BATCH_SIZE),
)
finally:
if isinstance(output, progress.Progress):
output.end()
@ -253,9 +297,7 @@ class Command(object):
project = None
if os.path.exists(path):
oldpath = None
while (path and
path != oldpath and
path != manifest.topdir):
while path and path != oldpath and path != manifest.topdir:
try:
project = self._by_path[path]
break
@ -274,8 +316,15 @@ class Command(object):
pass
return project
def GetProjects(self, args, manifest=None, groups='', missing_ok=False,
submodules_ok=False, all_manifests=False):
def GetProjects(
self,
args,
manifest=None,
groups="",
missing_ok=False,
submodules_ok=False,
all_manifests=False,
):
"""A list of projects that match the arguments.
Args:
@ -284,8 +333,9 @@ class Command(object):
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.
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.
@ -302,31 +352,38 @@ class Command(object):
if not groups:
groups = manifest.GetGroupsStr()
groups = [x for x in re.split(r'[,\s]+', groups) if x]
groups = [x for x in re.split(r"[,\s]+", groups) if x]
if not args:
derived_projects = {}
for project in all_projects_list:
if submodules_ok or project.sync_s:
derived_projects.update((p.name, p)
for p in project.GetDerivedSubprojects())
derived_projects.update(
(p.name, p) for p in project.GetDerivedSubprojects()
)
all_projects_list.extend(derived_projects.values())
for project in all_projects_list:
if (missing_ok or project.Exists) and project.MatchesGroups(groups):
if (missing_ok or project.Exists) and project.MatchesGroups(
groups
):
result.append(project)
else:
self._ResetPathToProjectMap(all_projects_list)
for arg in args:
# We have to filter by manifest groups in case the requested project is
# checked out multiple times or differently based on them.
projects = [project
# We have to filter by manifest groups in case the requested
# project is checked out multiple times or differently based on
# them.
projects = [
project
for project in manifest.GetProjectsWithName(
arg, all_manifests=all_manifests)
if project.MatchesGroups(groups)]
arg, all_manifests=all_manifests
)
if project.MatchesGroups(groups)
]
if not projects:
path = os.path.abspath(arg).replace('\\', '/')
path = os.path.abspath(arg).replace("\\", "/")
tree = manifest
if all_manifests:
# Look for the deepest matching submanifest.
@ -335,16 +392,23 @@ class Command(object):
break
project = self._GetProjectByPath(tree, path)
# If it's not a derived project, update path->project mapping and
# search again, as arg might actually point to a derived subproject.
if (project and not project.Derived and (submodules_ok or
project.sync_s)):
# If it's not a derived project, update path->project
# mapping and search again, as arg might actually point to
# a derived subproject.
if (
project
and not project.Derived
and (submodules_ok or project.sync_s)
):
search_again = False
for subproject in project.GetDerivedSubprojects():
self._UpdatePathToProjectMap(subproject)
search_again = True
if search_again:
project = self._GetProjectByPath(manifest, path) or project
project = (
self._GetProjectByPath(manifest, path)
or project
)
if project:
projects = [project]
@ -354,8 +418,10 @@ class Command(object):
for project in projects:
if not missing_ok and not project.Exists:
raise NoSuchProjectError('%s (%s)' % (
arg, project.RelPath(local=not all_manifests)))
raise NoSuchProjectError(
"%s (%s)"
% (arg, project.RelPath(local=not all_manifests))
)
if not project.MatchesGroups(groups):
raise InvalidProjectGroupsError(arg)
@ -363,6 +429,7 @@ class Command(object):
def _getpath(x):
return x.relpath
result.sort(key=_getpath)
return result
@ -371,14 +438,15 @@ class Command(object):
Args:
args: a list of (case-insensitive) strings, projects to search for.
inverse: a boolean, if True, then projects not matching any |args| are
returned.
all_manifests: a boolean, if True then all manifests and submanifests are
used. If False, then only the local (sub)manifest is used.
inverse: a boolean, if True, then projects not matching any |args|
are returned.
all_manifests: a boolean, if True then all manifests and
submanifests are used. If False, then only the local
(sub)manifest is used.
"""
result = []
patterns = [re.compile(r'%s' % a, re.IGNORECASE) for a in args]
for project in self.GetProjects('', all_manifests=all_manifests):
patterns = [re.compile(r"%s" % a, re.IGNORECASE) for a in args]
for project in self.GetProjects("", all_manifests=all_manifests):
paths = [project.name, project.RelPath(local=not all_manifests)]
for pattern in patterns:
match = any(pattern.search(x) for x in paths)
@ -390,8 +458,9 @@ class Command(object):
else:
if inverse:
result.append(project)
result.sort(key=lambda project: (project.manifest.path_prefix,
project.relpath))
result.sort(
key=lambda project: (project.manifest.path_prefix, project.relpath)
)
return result
def ManifestList(self, opt):
@ -410,8 +479,8 @@ class Command(object):
class InteractiveCommand(Command):
"""Command which requires user interaction on the tty and
must not run within a pager, even if the user asks to.
"""Command which requires user interaction on the tty and must not run
within a pager, even if the user asks to.
"""
def WantPager(self, _opt):
@ -419,8 +488,8 @@ class InteractiveCommand(Command):
class PagedCommand(Command):
"""Command which defaults to output in a pager, as its
display tends to be larger than one screen full.
"""Command which defaults to output in a pager, as its display tends to be
larger than one screen full.
"""
def WantPager(self, _opt):
@ -428,18 +497,16 @@ class PagedCommand(Command):
class MirrorSafeCommand(object):
"""Command permits itself to run within a mirror,
and does not require a working directory.
"""Command permits itself to run within a mirror, and does not require a
working directory.
"""
class GitcAvailableCommand(object):
"""Command that requires GITC to be available, but does
not require the local client to be a GITC client.
"""Command that requires GITC to be available, but does not require the
local client to be a GITC client.
"""
class GitcClientCommand(object):
"""Command that requires the local client to be a GITC
client.
"""
"""Command that requires the local client to be a GITC client."""

View File

@ -36,31 +36,33 @@ class Editor(object):
@classmethod
def _SelectEditor(cls):
e = os.getenv('GIT_EDITOR')
e = os.getenv("GIT_EDITOR")
if e:
return e
if cls.globalConfig:
e = cls.globalConfig.GetString('core.editor')
e = cls.globalConfig.GetString("core.editor")
if e:
return e
e = os.getenv('VISUAL')
e = os.getenv("VISUAL")
if e:
return e
e = os.getenv('EDITOR')
e = os.getenv("EDITOR")
if e:
return e
if os.getenv('TERM') == 'dumb':
if os.getenv("TERM") == "dumb":
print(
"""No editor specified in GIT_EDITOR, core.editor, VISUAL or EDITOR.
Tried to fall back to vi but terminal is dumb. Please configure at
least one of these before using this command.""", file=sys.stderr)
least one of these before using this command.""", # noqa: E501
file=sys.stderr,
)
sys.exit(1)
return 'vi'
return "vi"
@classmethod
def EditString(cls, data):
@ -76,22 +78,23 @@ least one of these before using this command.""", file=sys.stderr)
EditorError: The editor failed to run.
"""
editor = cls._GetEditor()
if editor == ':':
if editor == ":":
return data
fd, path = tempfile.mkstemp()
try:
os.write(fd, data.encode('utf-8'))
os.write(fd, data.encode("utf-8"))
os.close(fd)
fd = None
if platform_utils.isWindows():
# Split on spaces, respecting quoted strings
import shlex
args = shlex.split(editor)
shell = False
elif re.compile("^.*[$ \t'].*$").match(editor):
args = [editor + ' "$@"', 'sh']
args = [editor + ' "$@"', "sh"]
shell = True
else:
args = [editor]
@ -101,14 +104,17 @@ least one of these before using this command.""", file=sys.stderr)
try:
rc = subprocess.Popen(args, shell=shell).wait()
except OSError as e:
raise EditorError('editor failed, %s: %s %s'
% (str(e), editor, path))
raise EditorError(
"editor failed, %s: %s %s" % (str(e), editor, path)
)
if rc != 0:
raise EditorError('editor failed with exit status %d: %s %s'
% (rc, editor, path))
raise EditorError(
"editor failed with exit status %d: %s %s"
% (rc, editor, path)
)
with open(path, mode='rb') as fd2:
return fd2.read().decode('utf-8')
with open(path, mode="rb") as fd2:
return fd2.read().decode("utf-8")
finally:
if fd:
os.close(fd)

View File

@ -14,23 +14,19 @@
class ManifestParseError(Exception):
"""Failed to parse the manifest file.
"""
"""Failed to parse the manifest file."""
class ManifestInvalidRevisionError(ManifestParseError):
"""The revision value in a project is incorrect.
"""
"""The revision value in a project is incorrect."""
class ManifestInvalidPathError(ManifestParseError):
"""A path used in <copyfile> or <linkfile> is incorrect.
"""
"""A path used in <copyfile> or <linkfile> is incorrect."""
class NoManifestException(Exception):
"""The required manifest does not exist.
"""
"""The required manifest does not exist."""
def __init__(self, path, reason):
super().__init__(path, reason)
@ -42,8 +38,7 @@ class NoManifestException(Exception):
class EditorError(Exception):
"""Unspecified error from the user's text editor.
"""
"""Unspecified error from the user's text editor."""
def __init__(self, reason):
super().__init__(reason)
@ -54,8 +49,7 @@ class EditorError(Exception):
class GitError(Exception):
"""Unspecified internal error from git.
"""
"""Unspecified internal error from git."""
def __init__(self, command):
super().__init__(command)
@ -66,8 +60,7 @@ class GitError(Exception):
class UploadError(Exception):
"""A bundle upload to Gerrit did not succeed.
"""
"""A bundle upload to Gerrit did not succeed."""
def __init__(self, reason):
super().__init__(reason)
@ -78,8 +71,7 @@ class UploadError(Exception):
class DownloadError(Exception):
"""Cannot download a repository.
"""
"""Cannot download a repository."""
def __init__(self, reason):
super().__init__(reason)
@ -90,8 +82,7 @@ class DownloadError(Exception):
class NoSuchProjectError(Exception):
"""A specified project does not exist in the work tree.
"""
"""A specified project does not exist in the work tree."""
def __init__(self, name=None):
super().__init__(name)
@ -99,13 +90,12 @@ class NoSuchProjectError(Exception):
def __str__(self):
if self.name is None:
return 'in current directory'
return "in current directory"
return self.name
class InvalidProjectGroupsError(Exception):
"""A specified project is not suitable for the specified groups
"""
"""A specified project is not suitable for the specified groups"""
def __init__(self, name=None):
super().__init__(name)
@ -113,7 +103,7 @@ class InvalidProjectGroupsError(Exception):
def __str__(self):
if self.name is None:
return 'in current directory'
return "in current directory"
return self.name

View File

@ -15,9 +15,9 @@
import json
import multiprocessing
TASK_COMMAND = 'command'
TASK_SYNC_NETWORK = 'sync-network'
TASK_SYNC_LOCAL = 'sync-local'
TASK_COMMAND = "command"
TASK_SYNC_NETWORK = "sync-network"
TASK_SYNC_LOCAL = "sync-local"
class EventLog(object):
@ -51,8 +51,16 @@ class EventLog(object):
self._log = []
self._parent = None
def Add(self, name, task_name, start, finish=None, success=None,
try_count=1, kind='RepoOp'):
def Add(
self,
name,
task_name,
start,
finish=None,
success=None,
try_count=1,
kind="RepoOp",
):
"""Add an event to the log.
Args:
@ -68,15 +76,15 @@ class EventLog(object):
A dictionary of the event added to the log.
"""
event = {
'id': (kind, _NextEventId()),
'name': name,
'task_name': task_name,
'start_time': start,
'try': try_count,
"id": (kind, _NextEventId()),
"name": name,
"task_name": task_name,
"start_time": start,
"try": try_count,
}
if self._parent:
event['parent'] = self._parent['id']
event["parent"] = self._parent["id"]
if success is not None or finish is not None:
self.FinishEvent(event, finish, success)
@ -100,15 +108,15 @@ class EventLog(object):
"""
event = self.Add(project.relpath, task_name, start, finish, success)
if event is not None:
event['project'] = project.name
event["project"] = project.name
if project.revisionExpr:
event['revision'] = project.revisionExpr
event["revision"] = project.revisionExpr
if project.remote.url:
event['project_url'] = project.remote.url
event["project_url"] = project.remote.url
if project.remote.fetchUrl:
event['remote_url'] = project.remote.fetchUrl
event["remote_url"] = project.remote.fetchUrl
try:
event['git_hash'] = project.GetCommitRevisionId()
event["git_hash"] = project.GetCommitRevisionId()
except Exception:
pass
return event
@ -122,7 +130,7 @@ class EventLog(object):
Returns:
status string.
"""
return 'pass' if success else 'fail'
return "pass" if success else "fail"
def FinishEvent(self, event, finish, success):
"""Finishes an incomplete event.
@ -135,8 +143,8 @@ class EventLog(object):
Returns:
A dictionary of the event added to the log.
"""
event['status'] = self.GetStatusString(success)
event['finish_time'] = finish
event["status"] = self.GetStatusString(success)
event["finish_time"] = finish
return event
def SetParent(self, event):
@ -153,14 +161,14 @@ class EventLog(object):
Args:
filename: The file to write the log to.
"""
with open(filename, 'w+') as f:
with open(filename, "w+") as f:
for e in self._log:
json.dump(e, f, sort_keys=True)
f.write('\n')
f.write("\n")
# An integer id that is unique across this invocation of the program.
_EVENT_ID = multiprocessing.Value('i', 1)
_EVENT_ID = multiprocessing.Value("i", 1)
def _NextEventId():

View File

@ -27,19 +27,23 @@ def fetch_file(url, verbose=False):
The contents of the file as bytes.
"""
scheme = urlparse(url).scheme
if scheme == 'gs':
cmd = ['gsutil', 'cat', url]
if scheme == "gs":
cmd = ["gsutil", "cat", url]
try:
result = subprocess.run(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
check=True)
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True
)
if result.stderr and verbose:
print('warning: non-fatal error running "gsutil": %s' % result.stderr,
file=sys.stderr)
print(
'warning: non-fatal error running "gsutil": %s'
% result.stderr,
file=sys.stderr,
)
return result.stdout
except subprocess.CalledProcessError as e:
print('fatal: error running "gsutil": %s' % e.stderr,
file=sys.stderr)
print(
'fatal: error running "gsutil": %s' % e.stderr, file=sys.stderr
)
sys.exit(1)
with urlopen(url) as f:
return f.read()

View File

@ -24,7 +24,7 @@ import platform_utils
from repo_trace import REPO_TRACE, IsTrace, Trace
from wrapper import Wrapper
GIT = 'git'
GIT = "git"
# NB: These do not need to be kept in sync with the repo launcher script.
# These may be much newer as it allows the repo launcher to roll between
# different repo releases while source versions might require a newer git.
@ -36,7 +36,7 @@ GIT = 'git'
# git-1.7 is in (EOL) Ubuntu Precise. git-1.9 is in Ubuntu Trusty.
MIN_GIT_VERSION_SOFT = (1, 9, 1)
MIN_GIT_VERSION_HARD = (1, 7, 2)
GIT_DIR = 'GIT_DIR'
GIT_DIR = "GIT_DIR"
LAST_GITDIR = None
LAST_CWD = None
@ -47,17 +47,18 @@ class _GitCall(object):
def version_tuple(self):
ret = Wrapper().ParseGitVersion()
if ret is None:
print('fatal: unable to detect git version', file=sys.stderr)
print("fatal: unable to detect git version", file=sys.stderr)
sys.exit(1)
return ret
def __getattr__(self, name):
name = name.replace('_', '-')
name = name.replace("_", "-")
def fun(*cmdv):
command = [name]
command.extend(cmdv)
return GitCommand(None, command).Wait() == 0
return fun
@ -66,7 +67,7 @@ git = _GitCall()
def RepoSourceVersion():
"""Return the version of the repo.git tree."""
ver = getattr(RepoSourceVersion, 'version', None)
ver = getattr(RepoSourceVersion, "version", None)
# We avoid GitCommand so we don't run into circular deps -- GitCommand needs
# to initialize version info we provide.
@ -74,17 +75,22 @@ def RepoSourceVersion():
env = GitCommand._GetBasicEnv()
proj = os.path.dirname(os.path.abspath(__file__))
env[GIT_DIR] = os.path.join(proj, '.git')
result = subprocess.run([GIT, 'describe', HEAD], stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, encoding='utf-8',
env=env, check=False)
env[GIT_DIR] = os.path.join(proj, ".git")
result = subprocess.run(
[GIT, "describe", HEAD],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
encoding="utf-8",
env=env,
check=False,
)
if result.returncode == 0:
ver = result.stdout.strip()
if ver.startswith('v'):
if ver.startswith("v"):
ver = ver[1:]
else:
ver = 'unknown'
setattr(RepoSourceVersion, 'version', ver)
ver = "unknown"
setattr(RepoSourceVersion, "version", ver)
return ver
@ -105,14 +111,14 @@ class UserAgent(object):
"""The operating system name."""
if self._os is None:
os_name = sys.platform
if os_name.lower().startswith('linux'):
os_name = 'Linux'
elif os_name == 'win32':
os_name = 'Win32'
elif os_name == 'cygwin':
os_name = 'Cygwin'
elif os_name == 'darwin':
os_name = 'Darwin'
if os_name.lower().startswith("linux"):
os_name = "Linux"
elif os_name == "win32":
os_name = "Win32"
elif os_name == "cygwin":
os_name = "Cygwin"
elif os_name == "darwin":
os_name = "Darwin"
self._os = os_name
return self._os
@ -122,11 +128,14 @@ class UserAgent(object):
"""The UA when connecting directly from repo."""
if self._repo_ua is None:
py_version = sys.version_info
self._repo_ua = 'git-repo/%s (%s) git/%s Python/%d.%d.%d' % (
self._repo_ua = "git-repo/%s (%s) git/%s Python/%d.%d.%d" % (
RepoSourceVersion(),
self.os,
git.version_tuple().full,
py_version.major, py_version.minor, py_version.micro)
py_version.major,
py_version.minor,
py_version.micro,
)
return self._repo_ua
@ -134,10 +143,11 @@ class UserAgent(object):
def git(self):
"""The UA when running git."""
if self._git_ua is None:
self._git_ua = 'git/%s (%s) git-repo/%s' % (
self._git_ua = "git/%s (%s) git-repo/%s" % (
git.version_tuple().full,
self.os,
RepoSourceVersion())
RepoSourceVersion(),
)
return self._git_ua
@ -145,15 +155,17 @@ class UserAgent(object):
user_agent = UserAgent()
def git_require(min_version, fail=False, msg=''):
def git_require(min_version, fail=False, msg=""):
git_version = git.version_tuple()
if min_version <= git_version:
return True
if fail:
need = '.'.join(map(str, min_version))
need = ".".join(map(str, min_version))
if msg:
msg = ' for ' + msg
print('fatal: git %s or later required%s' % (need, msg), file=sys.stderr)
msg = " for " + msg
print(
"fatal: git %s or later required%s" % (need, msg), file=sys.stderr
)
sys.exit(1)
return False
@ -164,40 +176,44 @@ def _build_env(
disable_editor: Optional[bool] = False,
ssh_proxy: Optional[Any] = None,
gitdir: Optional[str] = None,
objdir: Optional[str] = None
objdir: Optional[str] = None,
):
"""Constucts an env dict for command execution."""
assert _kwargs_only == (), '_build_env only accepts keyword arguments.'
assert _kwargs_only == (), "_build_env only accepts keyword arguments."
env = GitCommand._GetBasicEnv()
if disable_editor:
env['GIT_EDITOR'] = ':'
env["GIT_EDITOR"] = ":"
if ssh_proxy:
env['REPO_SSH_SOCK'] = ssh_proxy.sock()
env['GIT_SSH'] = ssh_proxy.proxy
env['GIT_SSH_VARIANT'] = 'ssh'
if 'http_proxy' in env and 'darwin' == sys.platform:
s = "'http.proxy=%s'" % (env['http_proxy'],)
p = env.get('GIT_CONFIG_PARAMETERS')
env["REPO_SSH_SOCK"] = ssh_proxy.sock()
env["GIT_SSH"] = ssh_proxy.proxy
env["GIT_SSH_VARIANT"] = "ssh"
if "http_proxy" in env and "darwin" == sys.platform:
s = "'http.proxy=%s'" % (env["http_proxy"],)
p = env.get("GIT_CONFIG_PARAMETERS")
if p is not None:
s = p + ' ' + s
env['GIT_CONFIG_PARAMETERS'] = s
if 'GIT_ALLOW_PROTOCOL' not in env:
env['GIT_ALLOW_PROTOCOL'] = (
'file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc')
env['GIT_HTTP_USER_AGENT'] = user_agent.git
s = p + " " + s
env["GIT_CONFIG_PARAMETERS"] = s
if "GIT_ALLOW_PROTOCOL" not in env:
env[
"GIT_ALLOW_PROTOCOL"
] = "file:git:http:https:ssh:persistent-http:persistent-https:sso:rpc"
env["GIT_HTTP_USER_AGENT"] = user_agent.git
if objdir:
# Set to the place we want to save the objects.
env['GIT_OBJECT_DIRECTORY'] = objdir
env["GIT_OBJECT_DIRECTORY"] = objdir
alt_objects = os.path.join(gitdir, 'objects') if gitdir else None
if alt_objects and os.path.realpath(alt_objects) != os.path.realpath(objdir):
# Allow git to search the original place in case of local or unique refs
# that git will attempt to resolve even if we aren't fetching them.
env['GIT_ALTERNATE_OBJECT_DIRECTORIES'] = alt_objects
alt_objects = os.path.join(gitdir, "objects") if gitdir else None
if alt_objects and os.path.realpath(alt_objects) != os.path.realpath(
objdir
):
# Allow git to search the original place in case of local or unique
# refs that git will attempt to resolve even if we aren't fetching
# them.
env["GIT_ALTERNATE_OBJECT_DIRECTORIES"] = alt_objects
if bare and gitdir is not None:
env[GIT_DIR] = gitdir
@ -207,7 +223,8 @@ def _build_env(
class GitCommand(object):
"""Wrapper around a single git invocation."""
def __init__(self,
def __init__(
self,
project,
cmdv,
bare=False,
@ -219,8 +236,8 @@ class GitCommand(object):
ssh_proxy=None,
cwd=None,
gitdir=None,
objdir=None):
objdir=None,
):
if project:
if not cwd:
cwd = project.worktree
@ -230,9 +247,9 @@ class GitCommand(object):
# Git on Windows wants its paths only using / for reliability.
if platform_utils.isWindows():
if objdir:
objdir = objdir.replace('\\', '/')
objdir = objdir.replace("\\", "/")
if gitdir:
gitdir = gitdir.replace('\\', '/')
gitdir = gitdir.replace("\\", "/")
env = _build_env(
disable_editor=disable_editor,
@ -247,63 +264,75 @@ class GitCommand(object):
cwd = None
command.append(cmdv[0])
# Need to use the --progress flag for fetch/clone so output will be
# displayed as by default git only does progress output if stderr is a TTY.
if sys.stderr.isatty() and cmdv[0] in ('fetch', 'clone'):
if '--progress' not in cmdv and '--quiet' not in cmdv:
command.append('--progress')
# displayed as by default git only does progress output if stderr is a
# TTY.
if sys.stderr.isatty() and cmdv[0] in ("fetch", "clone"):
if "--progress" not in cmdv and "--quiet" not in cmdv:
command.append("--progress")
command.extend(cmdv[1:])
stdin = subprocess.PIPE if input else None
stdout = subprocess.PIPE if capture_stdout else None
stderr = (subprocess.STDOUT if merge_output else
(subprocess.PIPE if capture_stderr else None))
stderr = (
subprocess.STDOUT
if merge_output
else (subprocess.PIPE if capture_stderr else None)
)
dbg = ''
dbg = ""
if IsTrace():
global LAST_CWD
global LAST_GITDIR
if cwd and LAST_CWD != cwd:
if LAST_GITDIR or LAST_CWD:
dbg += '\n'
dbg += ': cd %s\n' % cwd
dbg += "\n"
dbg += ": cd %s\n" % cwd
LAST_CWD = cwd
if GIT_DIR in env and LAST_GITDIR != env[GIT_DIR]:
if LAST_GITDIR or LAST_CWD:
dbg += '\n'
dbg += ': export GIT_DIR=%s\n' % env[GIT_DIR]
dbg += "\n"
dbg += ": export GIT_DIR=%s\n" % env[GIT_DIR]
LAST_GITDIR = env[GIT_DIR]
if 'GIT_OBJECT_DIRECTORY' in env:
dbg += ': export GIT_OBJECT_DIRECTORY=%s\n' % env['GIT_OBJECT_DIRECTORY']
if 'GIT_ALTERNATE_OBJECT_DIRECTORIES' in env:
dbg += ': export GIT_ALTERNATE_OBJECT_DIRECTORIES=%s\n' % (
env['GIT_ALTERNATE_OBJECT_DIRECTORIES'])
if "GIT_OBJECT_DIRECTORY" in env:
dbg += (
": export GIT_OBJECT_DIRECTORY=%s\n"
% env["GIT_OBJECT_DIRECTORY"]
)
if "GIT_ALTERNATE_OBJECT_DIRECTORIES" in env:
dbg += ": export GIT_ALTERNATE_OBJECT_DIRECTORIES=%s\n" % (
env["GIT_ALTERNATE_OBJECT_DIRECTORIES"]
)
dbg += ': '
dbg += ' '.join(command)
dbg += ": "
dbg += " ".join(command)
if stdin == subprocess.PIPE:
dbg += ' 0<|'
dbg += " 0<|"
if stdout == subprocess.PIPE:
dbg += ' 1>|'
dbg += " 1>|"
if stderr == subprocess.PIPE:
dbg += ' 2>|'
dbg += " 2>|"
elif stderr == subprocess.STDOUT:
dbg += ' 2>&1'
dbg += " 2>&1"
with Trace('git command %s %s with debug: %s', LAST_GITDIR, command, dbg):
with Trace(
"git command %s %s with debug: %s", LAST_GITDIR, command, dbg
):
try:
p = subprocess.Popen(command,
p = subprocess.Popen(
command,
cwd=cwd,
env=env,
encoding='utf-8',
errors='backslashreplace',
encoding="utf-8",
errors="backslashreplace",
stdin=stdin,
stdout=stdout,
stderr=stderr)
stderr=stderr,
)
except Exception as e:
raise GitError('%s: %s' % (command[1], e))
raise GitError("%s: %s" % (command[1], e))
if ssh_proxy:
ssh_proxy.add_client(p)
@ -324,13 +353,15 @@ class GitCommand(object):
This is guaranteed to be side-effect free.
"""
env = os.environ.copy()
for key in (REPO_TRACE,
for key in (
REPO_TRACE,
GIT_DIR,
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
'GIT_OBJECT_DIRECTORY',
'GIT_WORK_TREE',
'GIT_GRAFT_FILE',
'GIT_INDEX_FILE'):
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_OBJECT_DIRECTORY",
"GIT_WORK_TREE",
"GIT_GRAFT_FILE",
"GIT_INDEX_FILE",
):
env.pop(key, None)
return env

View File

@ -34,9 +34,9 @@ from git_refs import R_CHANGES, R_HEADS, R_TAGS
# Prefix that is prepended to all the keys of SyncAnalysisState's data
# that is saved in the config.
SYNC_STATE_PREFIX = 'repo.syncstate.'
SYNC_STATE_PREFIX = "repo.syncstate."
ID_RE = re.compile(r'^[0-9a-f]{40}$')
ID_RE = re.compile(r"^[0-9a-f]{40}$")
REVIEW_CACHE = dict()
@ -58,19 +58,19 @@ def IsImmutable(rev):
def _key(name):
parts = name.split('.')
parts = name.split(".")
if len(parts) < 2:
return name.lower()
parts[0] = parts[0].lower()
parts[-1] = parts[-1].lower()
return '.'.join(parts)
return ".".join(parts)
class GitConfig(object):
_ForUser = None
_ForSystem = None
_SYSTEM_CONFIG = '/etc/gitconfig'
_SYSTEM_CONFIG = "/etc/gitconfig"
@classmethod
def ForSystem(cls):
@ -86,12 +86,11 @@ class GitConfig(object):
@staticmethod
def _getUserConfig():
return os.path.expanduser('~/.gitconfig')
return os.path.expanduser("~/.gitconfig")
@classmethod
def ForRepository(cls, gitdir, defaults=None):
return cls(configfile=os.path.join(gitdir, 'config'),
defaults=defaults)
return cls(configfile=os.path.join(gitdir, "config"), defaults=defaults)
def __init__(self, configfile, defaults=None, jsonFile=None):
self.file = configfile
@ -105,15 +104,15 @@ class GitConfig(object):
if self._json is None:
self._json = os.path.join(
os.path.dirname(self.file),
'.repo_' + os.path.basename(self.file) + '.json')
".repo_" + os.path.basename(self.file) + ".json",
)
def ClearCache(self):
"""Clear the in-memory cache of config."""
self._cache_dict = None
def Has(self, name, include_defaults=True):
"""Return true if this configuration file has the key.
"""
"""Return true if this configuration file has the key."""
if _key(name) in self._cache:
return True
if include_defaults and self.defaults:
@ -138,26 +137,28 @@ class GitConfig(object):
v = v.strip()
mult = 1
if v.endswith('k'):
if v.endswith("k"):
v = v[:-1]
mult = 1024
elif v.endswith('m'):
elif v.endswith("m"):
v = v[:-1]
mult = 1024 * 1024
elif v.endswith('g'):
elif v.endswith("g"):
v = v[:-1]
mult = 1024 * 1024 * 1024
base = 10
if v.startswith('0x'):
if v.startswith("0x"):
base = 16
try:
return int(v, base=base) * mult
except ValueError:
print(
f"warning: expected {name} to represent an integer, got {v} instead",
file=sys.stderr)
f"warning: expected {name} to represent an integer, got {v} "
"instead",
file=sys.stderr,
)
return None
def DumpConfigDict(self):
@ -177,6 +178,8 @@ class GitConfig(object):
def GetBoolean(self, name: str) -> Union[str, None]:
"""Returns a boolean from the configuration file.
Returns:
None: The value was not defined, or is not a boolean.
True: The value was set to true or yes.
False: The value was set to false or no.
@ -185,18 +188,20 @@ class GitConfig(object):
if v is None:
return None
v = v.lower()
if v in ('true', 'yes'):
if v in ("true", "yes"):
return True
if v in ('false', 'no'):
if v in ("false", "no"):
return False
print(f"warning: expected {name} to represent a boolean, got {v} instead",
file=sys.stderr)
print(
f"warning: expected {name} to represent a boolean, got {v} instead",
file=sys.stderr,
)
return None
def SetBoolean(self, name, value):
"""Set the truthy value for a key."""
if value is not None:
value = 'true' if value else 'false'
value = "true" if value else "false"
self.SetString(name, value)
def GetString(self, name: str, all_keys: bool = False) -> Union[str, None]:
@ -240,7 +245,7 @@ class GitConfig(object):
if value is None:
if old:
del self._cache[key]
self._do('--unset-all', name)
self._do("--unset-all", name)
elif isinstance(value, list):
if len(value) == 0:
@ -251,17 +256,16 @@ class GitConfig(object):
elif old != value:
self._cache[key] = list(value)
self._do('--replace-all', name, value[0])
self._do("--replace-all", name, value[0])
for i in range(1, len(value)):
self._do('--add', name, value[i])
self._do("--add", name, value[i])
elif len(old) != 1 or old[0] != value:
self._cache[key] = [value]
self._do('--replace-all', name, value)
self._do("--replace-all", name, value)
def GetRemote(self, name):
"""Get the remote.$name.* configuration values as an object.
"""
"""Get the remote.$name.* configuration values as an object."""
try:
r = self._remotes[name]
except KeyError:
@ -270,8 +274,7 @@ class GitConfig(object):
return r
def GetBranch(self, name):
"""Get the branch.$name.* configuration values as an object.
"""
"""Get the branch.$name.* configuration values as an object."""
try:
b = self._branches[name]
except KeyError:
@ -281,14 +284,20 @@ class GitConfig(object):
def GetSyncAnalysisStateData(self):
"""Returns data to be logged for the analysis of sync performance."""
return {k: v for k, v in self.DumpConfigDict().items() if k.startswith(SYNC_STATE_PREFIX)}
return {
k: v
for k, v in self.DumpConfigDict().items()
if k.startswith(SYNC_STATE_PREFIX)
}
def UpdateSyncAnalysisState(self, options, superproject_logging_data):
"""Update Config's SYNC_STATE_PREFIX* data with the latest sync data.
Args:
options: Options passed to sync returned from optparse. See _Options().
superproject_logging_data: A dictionary of superproject data that is to be logged.
options: Options passed to sync returned from optparse. See
_Options().
superproject_logging_data: A dictionary of superproject data that is
to be logged.
Returns:
SyncAnalysisState object.
@ -296,23 +305,20 @@ class GitConfig(object):
return SyncAnalysisState(self, options, superproject_logging_data)
def GetSubSections(self, section):
"""List all subsection names matching $section.*.*
"""
"""List all subsection names matching $section.*.*"""
return self._sections.get(section, set())
def HasSection(self, section, subsection=''):
"""Does at least one key in section.subsection exist?
"""
def HasSection(self, section, subsection=""):
"""Does at least one key in section.subsection exist?"""
try:
return subsection in self._sections[section]
except KeyError:
return False
def UrlInsteadOf(self, url):
"""Resolve any url.*.insteadof references.
"""
for new_url in self.GetSubSections('url'):
for old_url in self.GetString('url.%s.insteadof' % new_url, True):
"""Resolve any url.*.insteadof references."""
for new_url in self.GetSubSections("url"):
for old_url in self.GetString("url.%s.insteadof" % new_url, True):
if old_url is not None and url.startswith(old_url):
return new_url + url[len(old_url) :]
return url
@ -323,13 +329,13 @@ class GitConfig(object):
if d is None:
d = {}
for name in self._cache.keys():
p = name.split('.')
p = name.split(".")
if 2 == len(p):
section = p[0]
subsect = ''
subsect = ""
else:
section = p[0]
subsect = '.'.join(p[1:-1])
subsect = ".".join(p[1:-1])
if section not in d:
d[section] = set()
d[section].add(subsect)
@ -357,7 +363,7 @@ class GitConfig(object):
except OSError:
return None
try:
with Trace(': parsing %s', self.file):
with Trace(": parsing %s", self.file):
with open(self._json) as fd:
return json.load(fd)
except (IOError, ValueError):
@ -366,7 +372,7 @@ class GitConfig(object):
def _SaveJson(self, cache):
try:
with open(self._json, 'w') as fd:
with open(self._json, "w") as fd:
json.dump(cache, fd, indent=2)
except (IOError, TypeError):
platform_utils.remove(self._json, missing_ok=True)
@ -382,10 +388,10 @@ class GitConfig(object):
if not os.path.exists(self.file):
return c
d = self._do('--null', '--list')
for line in d.rstrip('\0').split('\0'):
if '\n' in line:
key, val = line.split('\n', 1)
d = self._do("--null", "--list")
for line in d.rstrip("\0").split("\0"):
if "\n" in line:
key, val = line.split("\n", 1)
else:
key = line
val = None
@ -399,19 +405,16 @@ class GitConfig(object):
def _do(self, *args):
if self.file == self._SYSTEM_CONFIG:
command = ['config', '--system', '--includes']
command = ["config", "--system", "--includes"]
else:
command = ['config', '--file', self.file, '--includes']
command = ["config", "--file", self.file, "--includes"]
command.extend(args)
p = GitCommand(None,
command,
capture_stdout=True,
capture_stderr=True)
p = GitCommand(None, command, capture_stdout=True, capture_stderr=True)
if p.Wait() == 0:
return p.stdout
else:
raise GitError('git config %s: %s' % (str(args), p.stderr))
raise GitError("git config %s: %s" % (str(args), p.stderr))
class RepoConfig(GitConfig):
@ -419,8 +422,8 @@ class RepoConfig(GitConfig):
@staticmethod
def _getUserConfig():
repo_config_dir = os.getenv('REPO_CONFIG_DIR', os.path.expanduser('~'))
return os.path.join(repo_config_dir, '.repoconfig/config')
repo_config_dir = os.getenv("REPO_CONFIG_DIR", os.path.expanduser("~"))
return os.path.join(repo_config_dir, ".repoconfig/config")
class RefSpec(object):
@ -433,8 +436,8 @@ class RefSpec(object):
@classmethod
def FromString(cls, rs):
lhs, rhs = rs.split(':', 2)
if lhs.startswith('+'):
lhs, rhs = rs.split(":", 2)
if lhs.startswith("+"):
lhs = lhs[1:]
forced = True
else:
@ -450,7 +453,7 @@ class RefSpec(object):
if self.src:
if rev == self.src:
return True
if self.src.endswith('/*') and rev.startswith(self.src[:-1]):
if self.src.endswith("/*") and rev.startswith(self.src[:-1]):
return True
return False
@ -458,28 +461,28 @@ class RefSpec(object):
if self.dst:
if ref == self.dst:
return True
if self.dst.endswith('/*') and ref.startswith(self.dst[:-1]):
if self.dst.endswith("/*") and ref.startswith(self.dst[:-1]):
return True
return False
def MapSource(self, rev):
if self.src.endswith('/*'):
if self.src.endswith("/*"):
return self.dst[:-1] + rev[len(self.src) - 1 :]
return self.dst
def __str__(self):
s = ''
s = ""
if self.forced:
s += '+'
s += "+"
if self.src:
s += self.src
if self.dst:
s += ':'
s += ":"
s += self.dst
return s
URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/')
URI_ALL = re.compile(r"^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/")
def GetSchemeFromUrl(url):
@ -491,21 +494,25 @@ def GetSchemeFromUrl(url):
@contextlib.contextmanager
def GetUrlCookieFile(url, quiet):
if url.startswith('persistent-'):
if url.startswith("persistent-"):
try:
p = subprocess.Popen(
['git-remote-persistent-https', '-print_config', url],
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
["git-remote-persistent-https", "-print_config", url],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
try:
cookieprefix = 'http.cookiefile='
proxyprefix = 'http.proxy='
cookieprefix = "http.cookiefile="
proxyprefix = "http.proxy="
cookiefile = None
proxy = None
for line in p.stdout:
line = line.strip().decode('utf-8')
line = line.strip().decode("utf-8")
if line.startswith(cookieprefix):
cookiefile = os.path.expanduser(line[len(cookieprefix):])
cookiefile = os.path.expanduser(
line[len(cookieprefix) :]
)
if line.startswith(proxyprefix):
proxy = line[len(proxyprefix) :]
# Leave subprocess open, as cookie file may be transient.
@ -515,8 +522,8 @@ def GetUrlCookieFile(url, quiet):
finally:
p.stdin.close()
if p.wait():
err_msg = p.stderr.read().decode('utf-8')
if ' -print_config' in err_msg:
err_msg = p.stderr.read().decode("utf-8")
if " -print_config" in err_msg:
pass # Persistent proxy doesn't support -print_config.
elif not quiet:
print(err_msg, file=sys.stderr)
@ -524,30 +531,30 @@ def GetUrlCookieFile(url, quiet):
if e.errno == errno.ENOENT:
pass # No persistent proxy.
raise
cookiefile = GitConfig.ForUser().GetString('http.cookiefile')
cookiefile = GitConfig.ForUser().GetString("http.cookiefile")
if cookiefile:
cookiefile = os.path.expanduser(cookiefile)
yield cookiefile, None
class Remote(object):
"""Configuration options related to a remote.
"""
"""Configuration options related to a remote."""
def __init__(self, config, name):
self._config = config
self.name = name
self.url = self._Get('url')
self.pushUrl = self._Get('pushurl')
self.review = self._Get('review')
self.projectname = self._Get('projectname')
self.fetch = list(map(RefSpec.FromString,
self._Get('fetch', all_keys=True)))
self.url = self._Get("url")
self.pushUrl = self._Get("pushurl")
self.review = self._Get("review")
self.projectname = self._Get("projectname")
self.fetch = list(
map(RefSpec.FromString, self._Get("fetch", all_keys=True))
)
self._review_url = None
def _InsteadOf(self):
globCfg = GitConfig.ForUser()
urlList = globCfg.GetSubSections('url')
urlList = globCfg.GetSubSections("url")
longest = ""
longestUrl = ""
@ -556,8 +563,9 @@ class Remote(object):
insteadOfList = globCfg.GetString(key, all_keys=True)
for insteadOf in insteadOfList:
if (self.url.startswith(insteadOf)
and len(insteadOf) > len(longest)):
if self.url.startswith(insteadOf) and len(insteadOf) > len(
longest
):
longest = insteadOf
longestUrl = url
@ -590,71 +598,78 @@ class Remote(object):
return None
u = self.review
if u.startswith('persistent-'):
u = u[len('persistent-'):]
if u.split(':')[0] not in ('http', 'https', 'sso', 'ssh'):
u = 'http://%s' % u
if u.endswith('/Gerrit'):
u = u[:len(u) - len('/Gerrit')]
if u.endswith('/ssh_info'):
u = u[:len(u) - len('/ssh_info')]
if not u.endswith('/'):
u += '/'
if u.startswith("persistent-"):
u = u[len("persistent-") :]
if u.split(":")[0] not in ("http", "https", "sso", "ssh"):
u = "http://%s" % u
if u.endswith("/Gerrit"):
u = u[: len(u) - len("/Gerrit")]
if u.endswith("/ssh_info"):
u = u[: len(u) - len("/ssh_info")]
if not u.endswith("/"):
u += "/"
http_url = u
if u in REVIEW_CACHE:
self._review_url = REVIEW_CACHE[u]
elif 'REPO_HOST_PORT_INFO' in os.environ:
host, port = os.environ['REPO_HOST_PORT_INFO'].split()
elif "REPO_HOST_PORT_INFO" in os.environ:
host, port = os.environ["REPO_HOST_PORT_INFO"].split()
self._review_url = self._SshReviewUrl(userEmail, host, port)
REVIEW_CACHE[u] = self._review_url
elif u.startswith('sso:') or u.startswith('ssh:'):
elif u.startswith("sso:") or u.startswith("ssh:"):
self._review_url = u # Assume it's right
REVIEW_CACHE[u] = self._review_url
elif 'REPO_IGNORE_SSH_INFO' in os.environ:
elif "REPO_IGNORE_SSH_INFO" in os.environ:
self._review_url = http_url
REVIEW_CACHE[u] = self._review_url
else:
try:
info_url = u + 'ssh_info'
info_url = u + "ssh_info"
if not validate_certs:
context = ssl._create_unverified_context()
info = urllib.request.urlopen(info_url, context=context).read()
info = urllib.request.urlopen(
info_url, context=context
).read()
else:
info = urllib.request.urlopen(info_url).read()
if info == b'NOT_AVAILABLE' or b'<' in info:
# If `info` contains '<', we assume the server gave us some sort
# of HTML response back, like maybe a login page.
if info == b"NOT_AVAILABLE" or b"<" in info:
# If `info` contains '<', we assume the server gave us
# some sort of HTML response back, like maybe a login
# page.
#
# Assume HTTP if SSH is not enabled or ssh_info doesn't look right.
# Assume HTTP if SSH is not enabled or ssh_info doesn't
# look right.
self._review_url = http_url
else:
info = info.decode('utf-8')
info = info.decode("utf-8")
host, port = info.split()
self._review_url = self._SshReviewUrl(userEmail, host, port)
self._review_url = self._SshReviewUrl(
userEmail, host, port
)
except urllib.error.HTTPError as e:
raise UploadError('%s: %s' % (self.review, str(e)))
raise UploadError("%s: %s" % (self.review, str(e)))
except urllib.error.URLError as e:
raise UploadError('%s: %s' % (self.review, str(e)))
raise UploadError("%s: %s" % (self.review, str(e)))
except HTTPException as e:
raise UploadError('%s: %s' % (self.review, e.__class__.__name__))
raise UploadError(
"%s: %s" % (self.review, e.__class__.__name__)
)
REVIEW_CACHE[u] = self._review_url
return self._review_url + self.projectname
def _SshReviewUrl(self, userEmail, host, port):
username = self._config.GetString('review.%s.username' % self.review)
username = self._config.GetString("review.%s.username" % self.review)
if username is None:
username = userEmail.split('@')[0]
return 'ssh://%s@%s:%s/' % (username, host, port)
username = userEmail.split("@")[0]
return "ssh://%s@%s:%s/" % (username, host, port)
def ToLocal(self, rev):
"""Convert a remote revision string to something we have locally.
"""
if self.name == '.' or IsId(rev):
"""Convert a remote revision string to something we have locally."""
if self.name == "." or IsId(rev):
return rev
if not rev.startswith('refs/'):
if not rev.startswith("refs/"):
rev = R_HEADS + rev
for spec in self.fetch:
@ -664,57 +679,55 @@ class Remote(object):
if not rev.startswith(R_HEADS):
return rev
raise GitError('%s: remote %s does not have %s' %
(self.projectname, self.name, rev))
raise GitError(
"%s: remote %s does not have %s"
% (self.projectname, self.name, rev)
)
def WritesTo(self, ref):
"""True if the remote stores to the tracking ref.
"""
"""True if the remote stores to the tracking ref."""
for spec in self.fetch:
if spec.DestMatches(ref):
return True
return False
def ResetFetch(self, mirror=False):
"""Set the fetch refspec to its default value.
"""
"""Set the fetch refspec to its default value."""
if mirror:
dst = 'refs/heads/*'
dst = "refs/heads/*"
else:
dst = 'refs/remotes/%s/*' % self.name
self.fetch = [RefSpec(True, 'refs/heads/*', dst)]
dst = "refs/remotes/%s/*" % self.name
self.fetch = [RefSpec(True, "refs/heads/*", dst)]
def Save(self):
"""Save this remote to the configuration.
"""
self._Set('url', self.url)
"""Save this remote to the configuration."""
self._Set("url", self.url)
if self.pushUrl is not None:
self._Set('pushurl', self.pushUrl + '/' + self.projectname)
self._Set("pushurl", self.pushUrl + "/" + self.projectname)
else:
self._Set('pushurl', self.pushUrl)
self._Set('review', self.review)
self._Set('projectname', self.projectname)
self._Set('fetch', list(map(str, self.fetch)))
self._Set("pushurl", self.pushUrl)
self._Set("review", self.review)
self._Set("projectname", self.projectname)
self._Set("fetch", list(map(str, self.fetch)))
def _Set(self, key, value):
key = 'remote.%s.%s' % (self.name, key)
key = "remote.%s.%s" % (self.name, key)
return self._config.SetString(key, value)
def _Get(self, key, all_keys=False):
key = 'remote.%s.%s' % (self.name, key)
key = "remote.%s.%s" % (self.name, key)
return self._config.GetString(key, all_keys=all_keys)
class Branch(object):
"""Configuration options related to a single branch.
"""
"""Configuration options related to a single branch."""
def __init__(self, config, name):
self._config = config
self.name = name
self.merge = self._Get('merge')
self.merge = self._Get("merge")
r = self._Get('remote')
r = self._Get("remote")
if r:
self.remote = self._config.GetRemote(r)
else:
@ -722,36 +735,34 @@ class Branch(object):
@property
def LocalMerge(self):
"""Convert the merge spec to a local name.
"""
"""Convert the merge spec to a local name."""
if self.remote and self.merge:
return self.remote.ToLocal(self.merge)
return None
def Save(self):
"""Save this branch back into the configuration.
"""
if self._config.HasSection('branch', self.name):
"""Save this branch back into the configuration."""
if self._config.HasSection("branch", self.name):
if self.remote:
self._Set('remote', self.remote.name)
self._Set("remote", self.remote.name)
else:
self._Set('remote', None)
self._Set('merge', self.merge)
self._Set("remote", None)
self._Set("merge", self.merge)
else:
with open(self._config.file, 'a') as fd:
with open(self._config.file, "a") as fd:
fd.write('[branch "%s"]\n' % self.name)
if self.remote:
fd.write('\tremote = %s\n' % self.remote.name)
fd.write("\tremote = %s\n" % self.remote.name)
if self.merge:
fd.write('\tmerge = %s\n' % self.merge)
fd.write("\tmerge = %s\n" % self.merge)
def _Set(self, key, value):
key = 'branch.%s.%s' % (self.name, key)
key = "branch.%s.%s" % (self.name, key)
return self._config.SetString(key, value)
def _Get(self, key, all_keys=False):
key = 'branch.%s.%s' % (self.name, key)
key = "branch.%s.%s" % (self.name, key)
return self._config.GetString(key, all_keys=all_keys)
@ -760,6 +771,7 @@ class SyncAnalysisState:
This object is versioned.
"""
def __init__(self, config, options, superproject_logging_data):
"""Initializes SyncAnalysisState.
@ -773,23 +785,30 @@ class SyncAnalysisState:
Args:
config: GitConfig object to store all options.
options: Options passed to sync returned from optparse. See _Options().
superproject_logging_data: A dictionary of superproject data that is to be logged.
options: Options passed to sync returned from optparse. See
_Options().
superproject_logging_data: A dictionary of superproject data that is
to be logged.
"""
self._config = config
now = datetime.datetime.utcnow()
self._Set('main.synctime', now.isoformat() + 'Z')
self._Set('main.version', '1')
self._Set('sys.argv', sys.argv)
self._Set("main.synctime", now.isoformat() + "Z")
self._Set("main.version", "1")
self._Set("sys.argv", sys.argv)
for key, value in superproject_logging_data.items():
self._Set(f'superproject.{key}', value)
self._Set(f"superproject.{key}", value)
for key, value in options.__dict__.items():
self._Set(f'options.{key}', value)
self._Set(f"options.{key}", value)
config_items = config.DumpConfigDict().items()
EXTRACT_NAMESPACES = {'repo', 'branch', 'remote'}
self._SetDictionary({k: v for k, v in config_items
if not k.startswith(SYNC_STATE_PREFIX) and
k.split('.', 1)[0] in EXTRACT_NAMESPACES})
EXTRACT_NAMESPACES = {"repo", "branch", "remote"}
self._SetDictionary(
{
k: v
for k, v in config_items
if not k.startswith(SYNC_STATE_PREFIX)
and k.split(".", 1)[0] in EXTRACT_NAMESPACES
}
)
def _SetDictionary(self, data):
"""Save all key/value pairs of |data| dictionary.
@ -807,13 +826,14 @@ class SyncAnalysisState:
Args:
key: Name of the key.
value: |value| could be of any type. If it is 'bool', it will be saved
as a Boolean and for all other types, it will be saved as a String.
value: |value| could be of any type. If it is 'bool', it will be
saved as a Boolean and for all other types, it will be saved as
a String.
"""
if value is None:
return
sync_key = f'{SYNC_STATE_PREFIX}{key}'
sync_key = sync_key.replace('_', '')
sync_key = f"{SYNC_STATE_PREFIX}{key}"
sync_key = sync_key.replace("_", "")
if isinstance(value, str):
self._config.SetString(sync_key, value)
elif isinstance(value, bool):

View File

@ -16,14 +16,14 @@ import os
from repo_trace import Trace
import platform_utils
HEAD = 'HEAD'
R_CHANGES = 'refs/changes/'
R_HEADS = 'refs/heads/'
R_TAGS = 'refs/tags/'
R_PUB = 'refs/published/'
R_WORKTREE = 'refs/worktree/'
R_WORKTREE_M = R_WORKTREE + 'm/'
R_M = 'refs/remotes/m/'
HEAD = "HEAD"
R_CHANGES = "refs/changes/"
R_HEADS = "refs/heads/"
R_TAGS = "refs/tags/"
R_PUB = "refs/published/"
R_WORKTREE = "refs/worktree/"
R_WORKTREE_M = R_WORKTREE + "m/"
R_M = "refs/remotes/m/"
class GitRefs(object):
@ -42,7 +42,7 @@ class GitRefs(object):
try:
return self.all[name]
except KeyError:
return ''
return ""
def deleted(self, name):
if self._phyref is not None:
@ -60,31 +60,32 @@ class GitRefs(object):
self._EnsureLoaded()
return self._symref[name]
except KeyError:
return ''
return ""
def _EnsureLoaded(self):
if self._phyref is None or self._NeedUpdate():
self._LoadAll()
def _NeedUpdate(self):
with Trace(': scan refs %s', self._gitdir):
with Trace(": scan refs %s", self._gitdir):
for name, mtime in self._mtime.items():
try:
if mtime != os.path.getmtime(os.path.join(self._gitdir, name)):
if mtime != os.path.getmtime(
os.path.join(self._gitdir, name)
):
return True
except OSError:
return True
return False
def _LoadAll(self):
with Trace(': load refs %s', self._gitdir):
with Trace(": load refs %s", self._gitdir):
self._phyref = {}
self._symref = {}
self._mtime = {}
self._ReadPackedRefs()
self._ReadLoose('refs/')
self._ReadLoose("refs/")
self._ReadLoose1(os.path.join(self._gitdir, HEAD), HEAD)
scan = self._symref
@ -100,9 +101,9 @@ class GitRefs(object):
attempts += 1
def _ReadPackedRefs(self):
path = os.path.join(self._gitdir, 'packed-refs')
path = os.path.join(self._gitdir, "packed-refs")
try:
fd = open(path, 'r')
fd = open(path, "r")
mtime = os.path.getmtime(path)
except IOError:
return
@ -111,33 +112,33 @@ class GitRefs(object):
try:
for line in fd:
line = str(line)
if line[0] == '#':
if line[0] == "#":
continue
if line[0] == '^':
if line[0] == "^":
continue
line = line[:-1]
p = line.split(' ')
p = line.split(" ")
ref_id = p[0]
name = p[1]
self._phyref[name] = ref_id
finally:
fd.close()
self._mtime['packed-refs'] = mtime
self._mtime["packed-refs"] = mtime
def _ReadLoose(self, prefix):
base = os.path.join(self._gitdir, prefix)
for name in platform_utils.listdir(base):
p = os.path.join(base, name)
# We don't implement the full ref validation algorithm, just the simple
# rules that would show up in local filesystems.
# We don't implement the full ref validation algorithm, just the
# simple rules that would show up in local filesystems.
# https://git-scm.com/docs/git-check-ref-format
if name.startswith('.') or name.endswith('.lock'):
if name.startswith(".") or name.endswith(".lock"):
pass
elif platform_utils.isdir(p):
self._mtime[prefix] = os.path.getmtime(base)
self._ReadLoose(prefix + name + '/')
self._ReadLoose(prefix + name + "/")
else:
self._ReadLoose1(p, prefix + name)
@ -157,7 +158,7 @@ class GitRefs(object):
return
ref_id = ref_id[:-1]
if ref_id.startswith('ref: '):
if ref_id.startswith("ref: "):
self._symref[name] = ref_id[5:]
else:
self._phyref[name] = ref_id

View File

@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Provide functionality to get all projects and their commit ids from Superproject.
"""Provide functionality to get projects and their commit ids from Superproject.
For more information on superproject, check out:
https://en.wikibooks.org/wiki/Git/Submodules_and_Superprojects
@ -33,8 +33,8 @@ from git_command import git_require, GitCommand
from git_config import RepoConfig
from git_refs import GitRefs
_SUPERPROJECT_GIT_NAME = 'superproject.git'
_SUPERPROJECT_MANIFEST_NAME = 'superproject_override.xml'
_SUPERPROJECT_GIT_NAME = "superproject.git"
_SUPERPROJECT_MANIFEST_NAME = "superproject_override.xml"
class SyncResult(NamedTuple):
@ -71,8 +71,15 @@ class Superproject(object):
lookup of commit ids for all projects. It contains _project_commit_ids which
is a dictionary with project/commit id entries.
"""
def __init__(self, manifest, name, remote, revision,
superproject_dir='exp-superproject'):
def __init__(
self,
manifest,
name,
remote,
revision,
superproject_dir="exp-superproject",
):
"""Initializes superproject.
Args:
@ -90,19 +97,23 @@ class Superproject(object):
self.revision = self._branch = revision
self._repodir = manifest.repodir
self._superproject_dir = superproject_dir
self._superproject_path = manifest.SubmanifestInfoDir(manifest.path_prefix,
superproject_dir)
self._manifest_path = os.path.join(self._superproject_path,
_SUPERPROJECT_MANIFEST_NAME)
git_name = hashlib.md5(remote.name.encode('utf8')).hexdigest() + '-'
self._superproject_path = manifest.SubmanifestInfoDir(
manifest.path_prefix, superproject_dir
)
self._manifest_path = os.path.join(
self._superproject_path, _SUPERPROJECT_MANIFEST_NAME
)
git_name = hashlib.md5(remote.name.encode("utf8")).hexdigest() + "-"
self._remote_url = remote.url
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 than superproject attributes,
# and were included here originally. They should eventually become
# arguments that are passed down from the public methods, instead of being
# treated as attributes.
# The following are command arguemnts, rather than superproject
# attributes, and were included here originally. They should eventually
# become arguments that are passed down from the public methods, instead
# of being treated as attributes.
self._git_event_log = None
self._quiet = False
self._print_messages = False
@ -123,26 +134,30 @@ class Superproject(object):
@property
def manifest_path(self):
"""Returns the manifest path if the path exists or None."""
return self._manifest_path if os.path.exists(self._manifest_path) else None
return (
self._manifest_path if os.path.exists(self._manifest_path) else None
)
def _LogMessage(self, fmt, *inputs):
"""Logs message to stderr and _git_event_log."""
message = f'{self._LogMessagePrefix()} {fmt.format(*inputs)}'
message = f"{self._LogMessagePrefix()} {fmt.format(*inputs)}"
if self._print_messages:
print(message, file=sys.stderr)
self._git_event_log.ErrorEvent(message, fmt)
def _LogMessagePrefix(self):
"""Returns the prefix string to be logged in each log message"""
return f'repo superproject branch: {self._branch} url: {self._remote_url}'
return (
f"repo superproject branch: {self._branch} url: {self._remote_url}"
)
def _LogError(self, fmt, *inputs):
"""Logs error message to stderr and _git_event_log."""
self._LogMessage(f'error: {fmt}', *inputs)
self._LogMessage(f"error: {fmt}", *inputs)
def _LogWarning(self, fmt, *inputs):
"""Logs warning message to stderr and _git_event_log."""
self._LogMessage(f'warning: {fmt}', *inputs)
self._LogMessage(f"warning: {fmt}", *inputs)
def _Init(self):
"""Sets up a local Git repository to get a copy of a superproject.
@ -153,56 +168,84 @@ class Superproject(object):
if not os.path.exists(self._superproject_path):
os.mkdir(self._superproject_path)
if not self._quiet and not os.path.exists(self._work_git):
print('%s: Performing initial setup for superproject; this might take '
'several minutes.' % self._work_git)
cmd = ['init', '--bare', self._work_git_name]
p = GitCommand(None,
print(
"%s: Performing initial setup for superproject; this might "
"take several minutes." % self._work_git
)
cmd = ["init", "--bare", self._work_git_name]
p = GitCommand(
None,
cmd,
cwd=self._superproject_path,
capture_stdout=True,
capture_stderr=True)
capture_stderr=True,
)
retval = p.Wait()
if retval:
self._LogWarning('git init call failed, command: git {}, '
'return code: {}, stderr: {}', cmd, retval, p.stderr)
self._LogWarning(
"git init call failed, command: git {}, "
"return code: {}, stderr: {}",
cmd,
retval,
p.stderr,
)
return False
return True
def _Fetch(self):
"""Fetches a local copy of a superproject for the manifest based on |_remote_url|.
"""Fetches a superproject for the manifest based on |_remote_url|.
This runs git fetch which stores a local copy the superproject.
Returns:
True if fetch is successful, or False.
"""
if not os.path.exists(self._work_git):
self._LogWarning('git fetch missing directory: {}', self._work_git)
self._LogWarning("git fetch missing directory: {}", self._work_git)
return False
if not git_require((2, 28, 0)):
self._LogWarning('superproject requires a git version 2.28 or later')
self._LogWarning(
"superproject requires a git version 2.28 or later"
)
return False
cmd = ['fetch', self._remote_url, '--depth', '1', '--force', '--no-tags',
'--filter', 'blob:none']
cmd = [
"fetch",
self._remote_url,
"--depth",
"1",
"--force",
"--no-tags",
"--filter",
"blob:none",
]
# Check if there is a local ref that we can pass to --negotiation-tip.
# If this is the first fetch, it does not exist yet.
# We use --negotiation-tip to speed up the fetch. Superproject branches do
# not share commits. So this lets git know it only needs to send commits
# reachable from the specified local refs.
rev_commit = GitRefs(self._work_git).get(f'refs/heads/{self.revision}')
# We use --negotiation-tip to speed up the fetch. Superproject branches
# do not share commits. So this lets git know it only needs to send
# commits reachable from the specified local refs.
rev_commit = GitRefs(self._work_git).get(f"refs/heads/{self.revision}")
if rev_commit:
cmd.extend(['--negotiation-tip', rev_commit])
cmd.extend(["--negotiation-tip", rev_commit])
if self._branch:
cmd += [self._branch + ':' + self._branch]
p = GitCommand(None,
cmd += [self._branch + ":" + self._branch]
p = GitCommand(
None,
cmd,
cwd=self._work_git,
capture_stdout=True,
capture_stderr=True)
capture_stderr=True,
)
retval = p.Wait()
if retval:
self._LogWarning('git fetch call failed, command: git {}, '
'return code: {}, stderr: {}', cmd, retval, p.stderr)
self._LogWarning(
"git fetch call failed, command: git {}, "
"return code: {}, stderr: {}",
cmd,
retval,
p.stderr,
)
return False
return True
@ -215,23 +258,32 @@ class Superproject(object):
data: data returned from 'git ls-tree ...' instead of None.
"""
if not os.path.exists(self._work_git):
self._LogWarning('git ls-tree missing directory: {}', self._work_git)
self._LogWarning(
"git ls-tree missing directory: {}", self._work_git
)
return None
data = None
branch = 'HEAD' if not self._branch else self._branch
cmd = ['ls-tree', '-z', '-r', branch]
branch = "HEAD" if not self._branch else self._branch
cmd = ["ls-tree", "-z", "-r", branch]
p = GitCommand(None,
p = GitCommand(
None,
cmd,
cwd=self._work_git,
capture_stdout=True,
capture_stderr=True)
capture_stderr=True,
)
retval = p.Wait()
if retval == 0:
data = p.stdout
else:
self._LogWarning('git ls-tree call failed, command: git {}, '
'return code: {}, stderr: {}', cmd, retval, p.stderr)
self._LogWarning(
"git ls-tree call failed, command: git {}, "
"return code: {}, stderr: {}",
cmd,
retval,
p.stderr,
)
return data
def Sync(self, git_event_log):
@ -245,16 +297,20 @@ class Superproject(object):
"""
self._git_event_log = git_event_log
if not self._manifest.superproject:
self._LogWarning('superproject tag is not defined in manifest: {}',
self._manifest.manifestFile)
self._LogWarning(
"superproject tag is not defined in manifest: {}",
self._manifest.manifestFile,
)
return SyncResult(False, False)
_PrintBetaNotice()
should_exit = True
if not self._remote_url:
self._LogWarning('superproject URL is not defined in manifest: {}',
self._manifest.manifestFile)
self._LogWarning(
"superproject URL is not defined in manifest: {}",
self._manifest.manifestFile,
)
return SyncResult(False, should_exit)
if not self._Init():
@ -262,11 +318,15 @@ class Superproject(object):
if not self._Fetch():
return SyncResult(False, should_exit)
if not self._quiet:
print('%s: Initial setup for superproject completed.' % self._work_git)
print(
"%s: Initial setup for superproject completed." % self._work_git
)
return SyncResult(True, False)
def _GetAllProjectsCommitIds(self):
"""Get commit ids for all projects from superproject and save them in _project_commit_ids.
"""Get commit ids for all projects from superproject and save them.
Commit ids are saved in _project_commit_ids.
Returns:
CommitIdsResult
@ -277,21 +337,24 @@ class Superproject(object):
data = self._LsTree()
if not data:
self._LogWarning('git ls-tree failed to return data for manifest: {}',
self._manifest.manifestFile)
self._LogWarning(
"git ls-tree failed to return data for manifest: {}",
self._manifest.manifestFile,
)
return CommitIdsResult(None, True)
# Parse lines like the following to select lines starting with '160000' and
# build a dictionary with project path (last element) and its commit id (3rd element).
# Parse lines like the following to select lines starting with '160000'
# and build a dictionary with project path (last element) and its commit
# id (3rd element).
#
# 160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00
# 120000 blob acc2cbdf438f9d2141f0ae424cec1d8fc4b5d97f\tbootstrap.bash\x00
# 120000 blob acc2cbdf438f9d2141f0ae424cec1d8fc4b5d97f\tbootstrap.bash\x00 # noqa: E501
commit_ids = {}
for line in data.split('\x00'):
for line in data.split("\x00"):
ls_data = line.split(None, 3)
if not ls_data:
break
if ls_data[0] == '160000':
if ls_data[0] == "160000":
commit_ids[ls_data[3]] = ls_data[2]
self._project_commit_ids = commit_ids
@ -301,20 +364,23 @@ class Superproject(object):
"""Writes manifest to a file.
Returns:
manifest_path: Path name of the file into which manifest is written instead of None.
manifest_path: Path name of the file into which manifest is written
instead of None.
"""
if not os.path.exists(self._superproject_path):
self._LogWarning('missing superproject directory: {}', self._superproject_path)
self._LogWarning(
"missing superproject directory: {}", self._superproject_path
)
return None
manifest_str = self._manifest.ToXml(groups=self._manifest.GetGroupsStr(),
omit_local=True).toxml()
manifest_str = self._manifest.ToXml(
groups=self._manifest.GetGroupsStr(), omit_local=True
).toxml()
manifest_path = self._manifest_path
try:
with open(manifest_path, 'w', encoding='utf-8') as fp:
with open(manifest_path, "w", encoding="utf-8") as fp:
fp.write(manifest_str)
except IOError as e:
self._LogError('cannot write manifest to : {} {}',
manifest_path, e)
self._LogError("cannot write manifest to : {} {}", manifest_path, e)
return None
return manifest_path
@ -366,9 +432,12 @@ class Superproject(object):
# If superproject doesn't have a commit id for a project, then report an
# error event and continue as if do not use superproject is specified.
if projects_missing_commit_ids:
self._LogWarning('please file a bug using {} to report missing '
'commit_ids for: {}', self._manifest.contactinfo.bugurl,
projects_missing_commit_ids)
self._LogWarning(
"please file a bug using {} to report missing "
"commit_ids for: {}",
self._manifest.contactinfo.bugurl,
projects_missing_commit_ids,
)
return UpdateProjectsResult(None, False)
for project in projects:
@ -382,8 +451,11 @@ class Superproject(object):
@functools.lru_cache(maxsize=10)
def _PrintBetaNotice():
"""Print the notice of beta status."""
print('NOTICE: --use-superproject is in beta; report any issues to the '
'address described in `repo version`', file=sys.stderr)
print(
"NOTICE: --use-superproject is in beta; report any issues to the "
"address described in `repo version`",
file=sys.stderr,
)
@functools.lru_cache(maxsize=None)
@ -392,25 +464,39 @@ def _UseSuperprojectFromConfiguration():
user_cfg = RepoConfig.ForUser()
time_now = int(time.time())
user_value = user_cfg.GetBoolean('repo.superprojectChoice')
user_value = user_cfg.GetBoolean("repo.superprojectChoice")
if user_value is not None:
user_expiration = user_cfg.GetInt('repo.superprojectChoiceExpire')
if user_expiration is None or user_expiration <= 0 or user_expiration >= time_now:
# TODO(b/190688390) - Remove prompt when we are comfortable with the new
# default value.
user_expiration = user_cfg.GetInt("repo.superprojectChoiceExpire")
if (
user_expiration is None
or user_expiration <= 0
or user_expiration >= time_now
):
# TODO(b/190688390) - Remove prompt when we are comfortable with the
# new default value.
if user_value:
print(('You are currently enrolled in Git submodules experiment '
'(go/android-submodules-quickstart). Use --no-use-superproject '
'to override.\n'), file=sys.stderr)
print(
(
"You are currently enrolled in Git submodules "
"experiment (go/android-submodules-quickstart). Use "
"--no-use-superproject to override.\n"
),
file=sys.stderr,
)
else:
print(('You are not currently enrolled in Git submodules experiment '
'(go/android-submodules-quickstart). Use --use-superproject '
'to override.\n'), file=sys.stderr)
print(
(
"You are not currently enrolled in Git submodules "
"experiment (go/android-submodules-quickstart). Use "
"--use-superproject to override.\n"
),
file=sys.stderr,
)
return user_value
# We don't have an unexpired choice, ask for one.
system_cfg = RepoConfig.ForSystem()
system_value = system_cfg.GetBoolean('repo.superprojectChoice')
system_value = system_cfg.GetBoolean("repo.superprojectChoice")
if system_value:
# The system configuration is proposing that we should enable the
# use of superproject. Treat the user as enrolled for two weeks.
@ -419,11 +505,15 @@ def _UseSuperprojectFromConfiguration():
# default value.
userchoice = True
time_choiceexpire = time_now + (86400 * 14)
user_cfg.SetString('repo.superprojectChoiceExpire', str(time_choiceexpire))
user_cfg.SetBoolean('repo.superprojectChoice', userchoice)
print('You are automatically enrolled in Git submodules experiment '
'(go/android-submodules-quickstart) for another two weeks.\n',
file=sys.stderr)
user_cfg.SetString(
"repo.superprojectChoiceExpire", str(time_choiceexpire)
)
user_cfg.SetBoolean("repo.superprojectChoice", userchoice)
print(
"You are automatically enrolled in Git submodules experiment "
"(go/android-submodules-quickstart) for another two weeks.\n",
file=sys.stderr,
)
return True
# For all other cases, we would not use superproject by default.

View File

@ -48,7 +48,8 @@ class EventLog(object):
Each entry contains the following common keys:
- event: The event name
- sid: session-id - Unique string to allow process instance to be identified.
- sid: session-id - Unique string to allow process instance to be
identified.
- thread: The thread name.
- time: is the UTC time of the event.
@ -60,7 +61,7 @@ class EventLog(object):
"""Initializes the event log."""
self._log = []
# Try to get session-id (sid) from environment (setup in repo launcher).
KEY = 'GIT_TRACE2_PARENT_SID'
KEY = "GIT_TRACE2_PARENT_SID"
if env is None:
env = os.environ
@ -69,11 +70,14 @@ class EventLog(object):
# Save both our sid component and the complete sid.
# We use our sid component (self._sid) as the unique filename prefix and
# the full sid (self._full_sid) in the log itself.
self._sid = 'repo-%s-P%08x' % (now.strftime('%Y%m%dT%H%M%SZ'), os.getpid())
self._sid = "repo-%s-P%08x" % (
now.strftime("%Y%m%dT%H%M%SZ"),
os.getpid(),
)
parent_sid = env.get(KEY)
# Append our sid component to the parent sid (if it exists).
if parent_sid is not None:
self._full_sid = parent_sid + '/' + self._sid
self._full_sid = parent_sid + "/" + self._sid
else:
self._full_sid = self._sid
@ -93,13 +97,13 @@ class EventLog(object):
def _AddVersionEvent(self):
"""Adds a 'version' event at the beginning of current log."""
version_event = self._CreateEventDict('version')
version_event['evt'] = "2"
version_event['exe'] = RepoSourceVersion()
version_event = self._CreateEventDict("version")
version_event["evt"] = "2"
version_event["exe"] = RepoSourceVersion()
self._log.insert(0, version_event)
def _CreateEventDict(self, event_name):
"""Returns a dictionary with the common keys/values for git trace2 events.
"""Returns a dictionary with common keys/values for git trace2 events.
Args:
event_name: The event name.
@ -108,16 +112,16 @@ class EventLog(object):
Dictionary with the common event fields populated.
"""
return {
'event': event_name,
'sid': self._full_sid,
'thread': threading.current_thread().name,
'time': datetime.datetime.utcnow().isoformat() + 'Z',
"event": event_name,
"sid": self._full_sid,
"thread": threading.current_thread().name,
"time": datetime.datetime.utcnow().isoformat() + "Z",
}
def StartEvent(self):
"""Append a 'start' event to the current log."""
start_event = self._CreateEventDict('start')
start_event['argv'] = sys.argv
start_event = self._CreateEventDict("start")
start_event["argv"] = sys.argv
self._log.append(start_event)
def ExitEvent(self, result):
@ -126,12 +130,12 @@ class EventLog(object):
Args:
result: Exit code of the event
"""
exit_event = self._CreateEventDict('exit')
exit_event = self._CreateEventDict("exit")
# Consider 'None' success (consistent with event_log result handling).
if result is None:
result = 0
exit_event['code'] = result
exit_event["code"] = result
self._log.append(exit_event)
def CommandEvent(self, name, subcommands):
@ -141,9 +145,9 @@ class EventLog(object):
name: Name of the primary command (ex: repo, git)
subcommands: List of the sub-commands (ex: version, init, sync)
"""
command_event = self._CreateEventDict('command')
command_event['name'] = name
command_event['subcommands'] = subcommands
command_event = self._CreateEventDict("command")
command_event["name"] = name
command_event["subcommands"] = subcommands
self._log.append(command_event)
def LogConfigEvents(self, config, event_dict_name):
@ -151,33 +155,36 @@ class EventLog(object):
Args:
config: Configuration dictionary.
event_dict_name: Name of the event dictionary for items to be logged under.
event_dict_name: Name of the event dictionary for items to be logged
under.
"""
for param, value in config.items():
event = self._CreateEventDict(event_dict_name)
event['param'] = param
event['value'] = value
event["param"] = param
event["value"] = value
self._log.append(event)
def DefParamRepoEvents(self, config):
"""Append a 'def_param' event for each repo.* config key to the current log.
"""Append 'def_param' events for repo config keys to the current log.
This appends one event for each repo.* config key.
Args:
config: Repo configuration dictionary
"""
# Only output the repo.* config parameters.
repo_config = {k: v for k, v in config.items() if k.startswith('repo.')}
self.LogConfigEvents(repo_config, 'def_param')
repo_config = {k: v for k, v in config.items() if k.startswith("repo.")}
self.LogConfigEvents(repo_config, "def_param")
def GetDataEventName(self, value):
"""Returns 'data-json' if the value is an array else returns 'data'."""
return 'data-json' if value[0] == '[' and value[-1] == ']' else 'data'
return "data-json" if value[0] == "[" and value[-1] == "]" else "data"
def LogDataConfigEvents(self, config, prefix):
"""Append a 'data' event for each config key/value in |config| to the current log.
"""Append a 'data' event for each entry in |config| to the current log.
For each keyX and valueX of the config, "key" field of the event is '|prefix|/keyX'
and the "value" of the "key" field is valueX.
For each keyX and valueX of the config, "key" field of the event is
'|prefix|/keyX' and the "value" of the "key" field is valueX.
Args:
config: Configuration dictionary.
@ -185,15 +192,15 @@ class EventLog(object):
"""
for key, value in config.items():
event = self._CreateEventDict(self.GetDataEventName(value))
event['key'] = f'{prefix}/{key}'
event['value'] = value
event["key"] = f"{prefix}/{key}"
event["value"] = value
self._log.append(event)
def ErrorEvent(self, msg, fmt):
"""Append a 'error' event to the current log."""
error_event = self._CreateEventDict('error')
error_event['msg'] = msg
error_event['fmt'] = fmt
error_event = self._CreateEventDict("error")
error_event["msg"] = msg
error_event["fmt"] = fmt
self._log.append(error_event)
def _GetEventTargetPath(self):
@ -203,38 +210,48 @@ class EventLog(object):
path: git config's 'trace2.eventtarget' path if it exists, or None
"""
path = None
cmd = ['config', '--get', 'trace2.eventtarget']
cmd = ["config", "--get", "trace2.eventtarget"]
# TODO(https://crbug.com/gerrit/13706): Use GitConfig when it supports
# system git config variables.
p = GitCommand(None, cmd, capture_stdout=True, capture_stderr=True,
bare=True)
p = GitCommand(
None, cmd, capture_stdout=True, capture_stderr=True, bare=True
)
retval = p.Wait()
if retval == 0:
# Strip trailing carriage-return in path.
path = p.stdout.rstrip('\n')
path = p.stdout.rstrip("\n")
elif retval != 1:
# `git config --get` is documented to produce an exit status of `1` if
# the requested variable is not present in the configuration. Report any
# other return value as an error.
print("repo: error: 'git config --get' call failed with return code: %r, stderr: %r" % (
retval, p.stderr), file=sys.stderr)
# `git config --get` is documented to produce an exit status of `1`
# if the requested variable is not present in the configuration.
# Report any other return value as an error.
print(
"repo: error: 'git config --get' call failed with return code: "
"%r, stderr: %r" % (retval, p.stderr),
file=sys.stderr,
)
return path
def _WriteLog(self, write_fn):
"""Writes the log out using a provided writer function.
Generate compact JSON output for each item in the log, and write it using
write_fn.
Generate compact JSON output for each item in the log, and write it
using write_fn.
Args:
write_fn: A function that accepts byts and writes them to a destination.
write_fn: A function that accepts byts and writes them to a
destination.
"""
for e in self._log:
# Dump in compact encoding mode.
# See 'Compact encoding' in Python docs:
# https://docs.python.org/3/library/json.html#module-json
write_fn(json.dumps(e, indent=None, separators=(',', ':')).encode('utf-8') + b'\n')
write_fn(
json.dumps(e, indent=None, separators=(",", ":")).encode(
"utf-8"
)
+ b"\n"
)
def Write(self, path=None):
"""Writes the log out to a file or socket.
@ -246,16 +263,19 @@ class EventLog(object):
(exclusive writable) file.
Args:
path: Path to where logs should be written. The path may have a prefix of
the form "af_unix:[{stream|dgram}:]", in which case the path is
treated as a Unix domain socket. See
https://git-scm.com/docs/api-trace2#_enabling_a_target for details.
path: Path to where logs should be written. The path may have a
prefix of the form "af_unix:[{stream|dgram}:]", in which case
the path is treated as a Unix domain socket. See
https://git-scm.com/docs/api-trace2#_enabling_a_target for
details.
Returns:
log_path: Path to the log file or socket if log is written, otherwise None
log_path: Path to the log file or socket if log is written,
otherwise None
"""
log_path = None
# If no logging path is specified, get the path from 'trace2.eventtarget'.
# If no logging path is specified, get the path from
# 'trace2.eventtarget'.
if path is None:
path = self._GetEventTargetPath()
@ -266,22 +286,22 @@ class EventLog(object):
path_is_socket = False
socket_type = None
if isinstance(path, str):
parts = path.split(':', 1)
if parts[0] == 'af_unix' and len(parts) == 2:
parts = path.split(":", 1)
if parts[0] == "af_unix" and len(parts) == 2:
path_is_socket = True
path = parts[1]
parts = path.split(':', 1)
if parts[0] == 'stream' and len(parts) == 2:
parts = path.split(":", 1)
if parts[0] == "stream" and len(parts) == 2:
socket_type = socket.SOCK_STREAM
path = parts[1]
elif parts[0] == 'dgram' and len(parts) == 2:
elif parts[0] == "dgram" and len(parts) == 2:
socket_type = socket.SOCK_DGRAM
path = parts[1]
else:
# Get absolute path.
path = os.path.abspath(os.path.expanduser(path))
else:
raise TypeError('path: str required but got %s.' % type(path))
raise TypeError("path: str required but got %s." % type(path))
# Git trace2 requires a directory to write log to.
@ -292,40 +312,59 @@ class EventLog(object):
if path_is_socket:
if socket_type == socket.SOCK_STREAM or socket_type is None:
try:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
with socket.socket(
socket.AF_UNIX, socket.SOCK_STREAM
) as sock:
sock.connect(path)
self._WriteLog(sock.sendall)
return f'af_unix:stream:{path}'
return f"af_unix:stream:{path}"
except OSError as err:
# If we tried to connect to a DGRAM socket using STREAM, ignore the
# attempt and continue to DGRAM below. Otherwise, issue a warning.
# If we tried to connect to a DGRAM socket using STREAM,
# ignore the attempt and continue to DGRAM below. Otherwise,
# issue a warning.
if err.errno != errno.EPROTOTYPE:
print(f'repo: warning: git trace2 logging failed: {err}', file=sys.stderr)
print(
f"repo: warning: git trace2 logging failed: {err}",
file=sys.stderr,
)
return None
if socket_type == socket.SOCK_DGRAM or socket_type is None:
try:
with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as sock:
with socket.socket(
socket.AF_UNIX, socket.SOCK_DGRAM
) as sock:
self._WriteLog(lambda bs: sock.sendto(bs, path))
return f'af_unix:dgram:{path}'
return f"af_unix:dgram:{path}"
except OSError as err:
print(f'repo: warning: git trace2 logging failed: {err}', file=sys.stderr)
print(
f"repo: warning: git trace2 logging failed: {err}",
file=sys.stderr,
)
return None
# Tried to open a socket but couldn't connect (SOCK_STREAM) or write
# (SOCK_DGRAM).
print('repo: warning: git trace2 logging failed: could not write to socket', file=sys.stderr)
print(
"repo: warning: git trace2 logging failed: could not write to "
"socket",
file=sys.stderr,
)
return None
# Path is an absolute path
# Use NamedTemporaryFile to generate a unique filename as required by git trace2.
# Use NamedTemporaryFile to generate a unique filename as required by
# git trace2.
try:
with tempfile.NamedTemporaryFile(mode='xb', prefix=self._sid, dir=path,
delete=False) as f:
# TODO(https://crbug.com/gerrit/13706): Support writing events as they
# occur.
with tempfile.NamedTemporaryFile(
mode="xb", prefix=self._sid, dir=path, delete=False
) as f:
# TODO(https://crbug.com/gerrit/13706): Support writing events
# as they occur.
self._WriteLog(f.write)
log_path = f.name
except FileExistsError as err:
print('repo: warning: git trace2 logging failed: %r' % err,
file=sys.stderr)
print(
"repo: warning: git trace2 logging failed: %r" % err,
file=sys.stderr,
)
return None
return log_path

View File

@ -39,9 +39,10 @@ def _get_project_revision(args):
"""Worker for _set_project_revisions to lookup one project remote."""
(i, url, expr) = args
gitcmd = git_command.GitCommand(
None, ['ls-remote', url, expr], capture_stdout=True, cwd='/tmp')
None, ["ls-remote", url, expr], capture_stdout=True, cwd="/tmp"
)
rc = gitcmd.Wait()
return (i, rc, gitcmd.stdout.split('\t', 1)[0])
return (i, rc, gitcmd.stdout.split("\t", 1)[0])
def _set_project_revisions(projects):
@ -54,25 +55,33 @@ def _set_project_revisions(projects):
Args:
projects: List of project objects to set the revionExpr for.
"""
# Retrieve the commit id for each project based off of it's current
# Retrieve the commit id for each project based off of its current
# revisionExpr and it is not already a commit id.
with multiprocessing.Pool(NUM_BATCH_RETRIEVE_REVISIONID) as pool:
results_iter = pool.imap_unordered(
_get_project_revision,
((i, project.remote.url, project.revisionExpr)
(
(i, project.remote.url, project.revisionExpr)
for i, project in enumerate(projects)
if not git_config.IsId(project.revisionExpr)),
chunksize=8)
for (i, rc, revisionExpr) in results_iter:
if not git_config.IsId(project.revisionExpr)
),
chunksize=8,
)
for i, rc, revisionExpr in results_iter:
project = projects[i]
if rc:
print('FATAL: Failed to retrieve revisionExpr for %s' % project.name)
print(
"FATAL: Failed to retrieve revisionExpr for %s"
% project.name
)
pool.terminate()
sys.exit(1)
if not revisionExpr:
pool.terminate()
raise ManifestParseError('Invalid SHA-1 revision project %s (%s)' %
(project.remote.url, project.revisionExpr))
raise ManifestParseError(
"Invalid SHA-1 revision project %s (%s)"
% (project.remote.url, project.revisionExpr)
)
project.revisionExpr = revisionExpr
@ -85,12 +94,14 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
paths: List of project paths we want to update.
"""
print('Generating GITC Manifest by fetching revision SHAs for each '
'project.')
print(
"Generating GITC Manifest by fetching revision SHAs for each "
"project."
)
if paths is None:
paths = list(manifest.paths.keys())
groups = [x for x in re.split(r'[,\s]+', manifest.GetGroupsStr()) if x]
groups = [x for x in re.split(r"[,\s]+", manifest.GetGroupsStr()) if x]
# Convert the paths to projects, and filter them to the matched groups.
projects = [manifest.paths[p] for p in paths]
@ -105,12 +116,12 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
proj.upstream = proj.revisionExpr
if path not in gitc_manifest.paths:
# Any new projects need their first revision, even if we weren't asked
# for them.
# Any new projects need their first revision, even if we weren't
# asked for them.
projects.append(proj)
elif path not in paths:
# And copy revisions from the previous manifest if we're not updating
# them now.
# And copy revisions from the previous manifest if we're not
# updating them now.
gitc_proj = gitc_manifest.paths[path]
if gitc_proj.old_revision:
proj.revisionExpr = None
@ -123,8 +134,8 @@ def generate_gitc_manifest(gitc_manifest, manifest, paths=None):
if gitc_manifest is not None:
for path, proj in gitc_manifest.paths.items():
if proj.old_revision and path in paths:
# If we updated a project that has been started, keep the old-revision
# updated.
# If we updated a project that has been started, keep the
# old-revision updated.
repo_proj = manifest.paths[path]
repo_proj.old_revision = repo_proj.revisionExpr
repo_proj.revisionExpr = None
@ -147,8 +158,8 @@ def save_manifest(manifest, client_dir=None):
if not client_dir:
manifest_file = manifest.manifestFile
else:
manifest_file = os.path.join(client_dir, '.manifest')
with open(manifest_file, 'w') as f:
manifest_file = os.path.join(client_dir, ".manifest")
with open(manifest_file, "w") as f:
manifest.Save(f, groups=manifest.GetGroupsStr())
# TODO(sbasi/jorg): Come up with a solution to remove the sleep below.
# Give the GITC filesystem time to register the manifest changes.

296
hooks.py
View File

@ -28,8 +28,9 @@ from git_refs import HEAD
class RepoHook(object):
"""A RepoHook contains information about a script to run as a hook.
Hooks are used to run a python script before running an upload (for instance,
to run presubmit checks). Eventually, we may have hooks for other actions.
Hooks are used to run a python script before running an upload (for
instance, to run presubmit checks). Eventually, we may have hooks for other
actions.
This shouldn't be confused with files in the 'repo/hooks' directory. Those
files are copied into each '.git/hooks' folder for each project. Repo-level
@ -52,7 +53,8 @@ class RepoHook(object):
Invalid
"""
def __init__(self,
def __init__(
self,
hook_type,
hooks_project,
repo_topdir,
@ -60,7 +62,8 @@ class RepoHook(object):
bypass_hooks=False,
allow_all_hooks=False,
ignore_hooks=False,
abort_if_user_denies=False):
abort_if_user_denies=False,
):
"""RepoHook constructor.
Params:
@ -78,8 +81,8 @@ class RepoHook(object):
bypass_hooks: If True, then 'Do not run the hook'.
allow_all_hooks: If True, then 'Run the hook without prompting'.
ignore_hooks: If True, then 'Do not abort action if hooks fail'.
abort_if_user_denies: If True, we'll abort running the hook if the user
doesn't allow us to run the hook.
abort_if_user_denies: If True, we'll abort running the hook if the
user doesn't allow us to run the hook.
"""
self._hook_type = hook_type
self._hooks_project = hooks_project
@ -92,77 +95,82 @@ class RepoHook(object):
# Store the full path to the script for convenience.
if self._hooks_project:
self._script_fullpath = os.path.join(self._hooks_project.worktree,
self._hook_type + '.py')
self._script_fullpath = os.path.join(
self._hooks_project.worktree, self._hook_type + ".py"
)
else:
self._script_fullpath = None
def _GetHash(self):
"""Return a hash of the contents of the hooks directory.
We'll just use git to do this. This hash has the property that if anything
changes in the directory we will return a different has.
We'll just use git to do this. This hash has the property that if
anything changes in the directory we will return a different has.
SECURITY CONSIDERATION:
This hash only represents the contents of files in the hook directory, not
any other files imported or called by hooks. Changes to imported files
can change the script behavior without affecting the hash.
This hash only represents the contents of files in the hook
directory, not any other files imported or called by hooks. Changes
to imported files can change the script behavior without affecting
the hash.
Returns:
A string representing the hash. This will always be ASCII so that it can
be printed to the user easily.
A string representing the hash. This will always be ASCII so that
it can be printed to the user easily.
"""
assert self._hooks_project, "Must have hooks to calculate their hash."
# We will use the work_git object rather than just calling GetRevisionId().
# That gives us a hash of the latest checked in version of the files that
# the user will actually be executing. Specifically, GetRevisionId()
# doesn't appear to change even if a user checks out a different version
# of the hooks repo (via git checkout) nor if a user commits their own revs.
# We will use the work_git object rather than just calling
# GetRevisionId(). That gives us a hash of the latest checked in version
# of the files that the user will actually be executing. Specifically,
# GetRevisionId() doesn't appear to change even if a user checks out a
# different version of the hooks repo (via git checkout) nor if a user
# commits their own revs.
#
# NOTE: Local (non-committed) changes will not be factored into this hash.
# I think this is OK, since we're really only worried about warning the user
# about upstream changes.
# NOTE: Local (non-committed) changes will not be factored into this
# hash. I think this is OK, since we're really only worried about
# warning the user about upstream changes.
return self._hooks_project.work_git.rev_parse(HEAD)
def _GetMustVerb(self):
"""Return 'must' if the hook is required; 'should' if not."""
if self._abort_if_user_denies:
return 'must'
return "must"
else:
return 'should'
return "should"
def _CheckForHookApproval(self):
"""Check to see whether this hook has been approved.
We'll accept approval of manifest URLs if they're using secure transports.
This way the user can say they trust the manifest hoster. For insecure
hosts, we fall back to checking the hash of the hooks repo.
We'll accept approval of manifest URLs if they're using secure
transports. This way the user can say they trust the manifest hoster.
For insecure hosts, we fall back to checking the hash of the hooks repo.
Note that we ask permission for each individual hook even though we use
the hash of all hooks when detecting changes. We'd like the user to be
able to approve / deny each hook individually. We only use the hash of all
hooks because there is no other easy way to detect changes to local imports.
able to approve / deny each hook individually. We only use the hash of
all hooks because there is no other easy way to detect changes to local
imports.
Returns:
True if this hook is approved to run; False otherwise.
Raises:
HookError: Raised if the user doesn't approve and abort_if_user_denies
was passed to the consturctor.
HookError: Raised if the user doesn't approve and
abort_if_user_denies was passed to the consturctor.
"""
if self._ManifestUrlHasSecureScheme():
return self._CheckForHookApprovalManifest()
else:
return self._CheckForHookApprovalHash()
def _CheckForHookApprovalHelper(self, subkey, new_val, main_prompt,
changed_prompt):
def _CheckForHookApprovalHelper(
self, subkey, new_val, main_prompt, changed_prompt
):
"""Check for approval for a particular attribute and hook.
Args:
subkey: The git config key under [repo.hooks.<hook_type>] to store the
last approved string.
subkey: The git config key under [repo.hooks.<hook_type>] to store
the last approved string.
new_val: The new value to compare against the last approved one.
main_prompt: Message to display to the user to ask for approval.
changed_prompt: Message explaining why we're re-asking for approval.
@ -171,11 +179,11 @@ class RepoHook(object):
True if this hook is approved to run; False otherwise.
Raises:
HookError: Raised if the user doesn't approve and abort_if_user_denies
was passed to the consturctor.
HookError: Raised if the user doesn't approve and
abort_if_user_denies was passed to the consturctor.
"""
hooks_config = self._hooks_project.config
git_approval_key = 'repo.hooks.%s.%s' % (self._hook_type, subkey)
git_approval_key = "repo.hooks.%s.%s" % (self._hook_type, subkey)
# Get the last value that the user approved for this hook; may be None.
old_val = hooks_config.GetString(git_approval_key)
@ -186,35 +194,44 @@ class RepoHook(object):
# Approval matched. We're done.
return True
else:
# Give the user a reason why we're prompting, since they last told
# us to "never ask again".
prompt = 'WARNING: %s\n\n' % (changed_prompt,)
# Give the user a reason why we're prompting, since they last
# told us to "never ask again".
prompt = "WARNING: %s\n\n" % (changed_prompt,)
else:
prompt = ''
prompt = ""
# Prompt the user if we're not on a tty; on a tty we'll assume "no".
if sys.stdout.isatty():
prompt += main_prompt + ' (yes/always/NO)? '
prompt += main_prompt + " (yes/always/NO)? "
response = input(prompt).lower()
print()
# User is doing a one-time approval.
if response in ('y', 'yes'):
if response in ("y", "yes"):
return True
elif response == 'always':
elif response == "always":
hooks_config.SetString(git_approval_key, new_val)
return True
# For anything else, we'll assume no approval.
if self._abort_if_user_denies:
raise HookError('You must allow the %s hook or use --no-verify.' %
self._hook_type)
raise HookError(
"You must allow the %s hook or use --no-verify."
% self._hook_type
)
return False
def _ManifestUrlHasSecureScheme(self):
"""Check if the URI for the manifest is a secure transport."""
secure_schemes = ('file', 'https', 'ssh', 'persistent-https', 'sso', 'rpc')
secure_schemes = (
"file",
"https",
"ssh",
"persistent-https",
"sso",
"rpc",
)
parse_results = urllib.parse.urlparse(self._manifest_url)
return parse_results.scheme in secure_schemes
@ -225,10 +242,12 @@ class RepoHook(object):
True if this hook is approved to run; False otherwise.
"""
return self._CheckForHookApprovalHelper(
'approvedmanifest',
"approvedmanifest",
self._manifest_url,
'Run hook scripts from %s' % (self._manifest_url,),
'Manifest URL has changed since %s was allowed.' % (self._hook_type,))
"Run hook scripts from %s" % (self._manifest_url,),
"Manifest URL has changed since %s was allowed."
% (self._hook_type,),
)
def _CheckForHookApprovalHash(self):
"""Check whether the user has approved the hooks repo.
@ -236,15 +255,18 @@ class RepoHook(object):
Returns:
True if this hook is approved to run; False otherwise.
"""
prompt = ('Repo %s run the script:\n'
' %s\n'
'\n'
'Do you want to allow this script to run')
prompt = (
"Repo %s run the script:\n"
" %s\n"
"\n"
"Do you want to allow this script to run"
)
return self._CheckForHookApprovalHelper(
'approvedhash',
"approvedhash",
self._GetHash(),
prompt % (self._GetMustVerb(), self._script_fullpath),
'Scripts have changed since %s was allowed.' % (self._hook_type,))
"Scripts have changed since %s was allowed." % (self._hook_type,),
)
@staticmethod
def _ExtractInterpFromShebang(data):
@ -256,8 +278,8 @@ class RepoHook(object):
data: The file content of the script.
Returns:
The basename of the main script interpreter, or None if a shebang is not
used or could not be parsed out.
The basename of the main script interpreter, or None if a shebang is
not used or could not be parsed out.
"""
firstline = data.splitlines()[:1]
if not firstline:
@ -265,13 +287,13 @@ class RepoHook(object):
# The format here can be tricky.
shebang = firstline[0].strip()
m = re.match(r'^#!\s*([^\s]+)(?:\s+([^\s]+))?', shebang)
m = re.match(r"^#!\s*([^\s]+)(?:\s+([^\s]+))?", shebang)
if not m:
return None
# If the using `env`, find the target program.
interp = m.group(1)
if os.path.basename(interp) == 'env':
if os.path.basename(interp) == "env":
interp = m.group(2)
return interp
@ -300,18 +322,18 @@ data = open(path).read()
exec(compile(data, path, 'exec'), context)
context['main'](**kwargs)
""" % {
'path': self._script_fullpath,
'kwargs': json.dumps(kwargs),
'context': json.dumps(context),
"path": self._script_fullpath,
"kwargs": json.dumps(kwargs),
"context": json.dumps(context),
}
# We pass the script via stdin to avoid OS argv limits. It also makes
# unhandled exception tracebacks less verbose/confusing for users.
cmd = [interp, '-c', 'import sys; exec(sys.stdin.read())']
cmd = [interp, "-c", "import sys; exec(sys.stdin.read())"]
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
proc.communicate(input=script.encode('utf-8'))
proc.communicate(input=script.encode("utf-8"))
if proc.returncode:
raise HookError('Failed to run %s hook.' % (self._hook_type,))
raise HookError("Failed to run %s hook." % (self._hook_type,))
def _ExecuteHookViaImport(self, data, context, **kwargs):
"""Execute the hook code in |data| directly.
@ -327,23 +349,27 @@ context['main'](**kwargs)
# Exec, storing global context in the context dict. We catch exceptions
# and convert to a HookError w/ just the failing traceback.
try:
exec(compile(data, self._script_fullpath, 'exec'), context)
exec(compile(data, self._script_fullpath, "exec"), context)
except Exception:
raise HookError('%s\nFailed to import %s hook; see traceback above.' %
(traceback.format_exc(), self._hook_type))
raise HookError(
"%s\nFailed to import %s hook; see traceback above."
% (traceback.format_exc(), self._hook_type)
)
# Running the script should have defined a main() function.
if 'main' not in context:
if "main" not in context:
raise HookError('Missing main() in: "%s"' % self._script_fullpath)
# Call the main function in the hook. If the hook should cause the
# build to fail, it will raise an Exception. We'll catch that convert
# to a HookError w/ just the failing traceback.
try:
context['main'](**kwargs)
context["main"](**kwargs)
except Exception:
raise HookError('%s\nFailed to run main() for %s hook; see traceback '
'above.' % (traceback.format_exc(), self._hook_type))
raise HookError(
"%s\nFailed to run main() for %s hook; see traceback "
"above." % (traceback.format_exc(), self._hook_type)
)
def _ExecuteHook(self, **kwargs):
"""Actually execute the given hook.
@ -351,9 +377,9 @@ context['main'](**kwargs)
This will run the hook's 'main' function in our python interpreter.
Args:
kwargs: Keyword arguments to pass to the hook. These are often specific
to the hook type. For instance, pre-upload hooks will contain
a project_list.
kwargs: Keyword arguments to pass to the hook. These are often
specific to the hook type. For instance, pre-upload hooks will
contain a project_list.
"""
# Keep sys.path and CWD stashed away so that we can always restore them
# upon function exit.
@ -370,17 +396,18 @@ context['main'](**kwargs)
sys.path = [os.path.dirname(self._script_fullpath)] + sys.path[1:]
# Initial global context for the hook to run within.
context = {'__file__': self._script_fullpath}
context = {"__file__": self._script_fullpath}
# Add 'hook_should_take_kwargs' to the arguments to be passed to main.
# We don't actually want hooks to define their main with this argument--
# it's there to remind them that their hook should always take **kwargs.
# Add 'hook_should_take_kwargs' to the arguments to be passed to
# main. We don't actually want hooks to define their main with this
# argument--it's there to remind them that their hook should always
# take **kwargs.
# For instance, a pre-upload hook should be defined like:
# def main(project_list, **kwargs):
#
# This allows us to later expand the API without breaking old hooks.
kwargs = kwargs.copy()
kwargs['hook_should_take_kwargs'] = True
kwargs["hook_should_take_kwargs"] = True
# See what version of python the hook has been written against.
data = open(self._script_fullpath).read()
@ -388,18 +415,20 @@ context['main'](**kwargs)
reexec = False
if interp:
prog = os.path.basename(interp)
if prog.startswith('python2') and sys.version_info.major != 2:
if prog.startswith("python2") and sys.version_info.major != 2:
reexec = True
elif prog.startswith('python3') and sys.version_info.major == 2:
elif prog.startswith("python3") and sys.version_info.major == 2:
reexec = True
# Attempt to execute the hooks through the requested version of Python.
# Attempt to execute the hooks through the requested version of
# Python.
if reexec:
try:
self._ExecuteHookViaReexec(interp, context, **kwargs)
except OSError as e:
if e.errno == errno.ENOENT:
# We couldn't find the interpreter, so fallback to importing.
# We couldn't find the interpreter, so fallback to
# importing.
reexec = False
else:
raise
@ -415,7 +444,9 @@ context['main'](**kwargs)
def _CheckHook(self):
# Bail with a nice error if we can't find the hook.
if not os.path.isfile(self._script_fullpath):
raise HookError('Couldn\'t find repo hook: %s' % self._script_fullpath)
raise HookError(
"Couldn't find repo hook: %s" % self._script_fullpath
)
def Run(self, **kwargs):
"""Run the hook.
@ -424,27 +455,30 @@ context['main'](**kwargs)
this particular hook is not enabled), this is a no-op.
Args:
user_allows_all_hooks: If True, we will never prompt about running the
hook--we'll just assume it's OK to run it.
kwargs: Keyword arguments to pass to the hook. These are often specific
to the hook type. For instance, pre-upload hooks will contain
a project_list.
user_allows_all_hooks: If True, we will never prompt about running
the hook--we'll just assume it's OK to run it.
kwargs: Keyword arguments to pass to the hook. These are often
specific to the hook type. For instance, pre-upload hooks will
contain a project_list.
Returns:
True: On success or ignore hooks by user-request
False: The hook failed. The caller should respond with aborting the action.
Some examples in which False is returned:
False: The hook failed. The caller should respond with aborting the
action. Some examples in which False is returned:
* Finding the hook failed while it was enabled, or
* the user declined to run a required hook (from _CheckForHookApproval)
* the user declined to run a required hook (from
_CheckForHookApproval)
In all these cases the user did not pass the proper arguments to
ignore the result through the option combinations as listed in
AddHookOptionGroup().
"""
# Do not do anything in case bypass_hooks is set, or
# no-op if there is no hooks project or if hook is disabled.
if (self._bypass_hooks or
not self._hooks_project or
self._hook_type not in self._hooks_project.enabled_repo_hooks):
if (
self._bypass_hooks
or not self._hooks_project
or self._hook_type not in self._hooks_project.enabled_repo_hooks
):
return True
passed = True
@ -457,15 +491,21 @@ context['main'](**kwargs)
self._ExecuteHook(**kwargs)
except SystemExit as e:
passed = False
print('ERROR: %s hooks exited with exit code: %s' % (self._hook_type, str(e)),
file=sys.stderr)
print(
"ERROR: %s hooks exited with exit code: %s"
% (self._hook_type, str(e)),
file=sys.stderr,
)
except HookError as e:
passed = False
print('ERROR: %s' % str(e), file=sys.stderr)
print("ERROR: %s" % str(e), file=sys.stderr)
if not passed and self._ignore_hooks:
print('\nWARNING: %s hooks failed, but continuing anyways.' % self._hook_type,
file=sys.stderr)
print(
"\nWARNING: %s hooks failed, but continuing anyways."
% self._hook_type,
file=sys.stderr,
)
passed = True
return passed
@ -478,16 +518,20 @@ context['main'](**kwargs)
manifest: The current active manifest for this command from which we
extract a couple of fields.
opt: Contains the commandline options for the action of this hook.
It should contain the options added by AddHookOptionGroup() in which
we are interested in RepoHook execution.
It should contain the options added by AddHookOptionGroup() in
which we are interested in RepoHook execution.
"""
for key in ('bypass_hooks', 'allow_all_hooks', 'ignore_hooks'):
for key in ("bypass_hooks", "allow_all_hooks", "ignore_hooks"):
kwargs.setdefault(key, getattr(opt, key))
kwargs.update({
'hooks_project': manifest.repo_hooks_project,
'repo_topdir': manifest.topdir,
'manifest_url': manifest.manifestProject.GetRemote('origin').url,
})
kwargs.update(
{
"hooks_project": manifest.repo_hooks_project,
"repo_topdir": manifest.topdir,
"manifest_url": manifest.manifestProject.GetRemote(
"origin"
).url,
}
)
return cls(*args, **kwargs)
@staticmethod
@ -497,13 +541,21 @@ context['main'](**kwargs)
# Note that verify and no-verify are NOT opposites of each other, which
# is why they store to different locations. We are using them to match
# 'git commit' syntax.
group = parser.add_option_group(name + ' hooks')
group.add_option('--no-verify',
dest='bypass_hooks', action='store_true',
help='Do not run the %s hook.' % name)
group.add_option('--verify',
dest='allow_all_hooks', action='store_true',
help='Run the %s hook without prompting.' % name)
group.add_option('--ignore-hooks',
action='store_true',
help='Do not abort if %s hooks fail.' % name)
group = parser.add_option_group(name + " hooks")
group.add_option(
"--no-verify",
dest="bypass_hooks",
action="store_true",
help="Do not run the %s hook." % name,
)
group.add_option(
"--verify",
dest="allow_all_hooks",
action="store_true",
help="Run the %s hook without prompting." % name,
)
group.add_option(
"--ignore-hooks",
action="store_true",
help="Do not abort if %s hooks fail." % name,
)

466
main.py
View File

@ -74,64 +74,109 @@ MIN_PYTHON_VERSION_SOFT = (3, 6)
MIN_PYTHON_VERSION_HARD = (3, 6)
if sys.version_info.major < 3:
print('repo: error: Python 2 is no longer supported; '
'Please upgrade to Python {}.{}+.'.format(*MIN_PYTHON_VERSION_SOFT),
file=sys.stderr)
print(
"repo: error: Python 2 is no longer supported; "
"Please upgrade to Python {}.{}+.".format(*MIN_PYTHON_VERSION_SOFT),
file=sys.stderr,
)
sys.exit(1)
else:
if sys.version_info < MIN_PYTHON_VERSION_HARD:
print('repo: error: Python 3 version is too old; '
'Please upgrade to Python {}.{}+.'.format(*MIN_PYTHON_VERSION_SOFT),
file=sys.stderr)
print(
"repo: error: Python 3 version is too old; "
"Please upgrade to Python {}.{}+.".format(*MIN_PYTHON_VERSION_SOFT),
file=sys.stderr,
)
sys.exit(1)
elif sys.version_info < MIN_PYTHON_VERSION_SOFT:
print('repo: warning: your Python 3 version is no longer supported; '
'Please upgrade to Python {}.{}+.'.format(*MIN_PYTHON_VERSION_SOFT),
file=sys.stderr)
print(
"repo: warning: your Python 3 version is no longer supported; "
"Please upgrade to Python {}.{}+.".format(*MIN_PYTHON_VERSION_SOFT),
file=sys.stderr,
)
global_options = optparse.OptionParser(
usage='repo [-p|--paginate|--no-pager] COMMAND [ARGS]',
add_help_option=False)
global_options.add_option('-h', '--help', action='store_true',
help='show this help message and exit')
global_options.add_option('--help-all', action='store_true',
help='show this help message with all subcommands and exit')
global_options.add_option('-p', '--paginate',
dest='pager', action='store_true',
help='display command output in the pager')
global_options.add_option('--no-pager',
dest='pager', action='store_false',
help='disable the pager')
global_options.add_option('--color',
choices=('auto', 'always', 'never'), default=None,
help='control color usage: auto, always, never')
global_options.add_option('--trace',
dest='trace', action='store_true',
help='trace git command execution (REPO_TRACE=1)')
global_options.add_option('--trace-to-stderr',
dest='trace_to_stderr', action='store_true',
help='trace outputs go to stderr in addition to .repo/TRACE_FILE')
global_options.add_option('--trace-python',
dest='trace_python', action='store_true',
help='trace python command execution')
global_options.add_option('--time',
dest='time', action='store_true',
help='time repo command execution')
global_options.add_option('--version',
dest='show_version', action='store_true',
help='display this version of repo')
global_options.add_option('--show-toplevel',
action='store_true',
help='display the path of the top-level directory of '
'the repo client checkout')
global_options.add_option('--event-log',
dest='event_log', action='store',
help='filename of event log to append timeline to')
global_options.add_option('--git-trace2-event-log', action='store',
help='directory to write git trace2 event log to')
global_options.add_option('--submanifest-path', action='store',
metavar='REL_PATH', help='submanifest path')
usage="repo [-p|--paginate|--no-pager] COMMAND [ARGS]",
add_help_option=False,
)
global_options.add_option(
"-h", "--help", action="store_true", help="show this help message and exit"
)
global_options.add_option(
"--help-all",
action="store_true",
help="show this help message with all subcommands and exit",
)
global_options.add_option(
"-p",
"--paginate",
dest="pager",
action="store_true",
help="display command output in the pager",
)
global_options.add_option(
"--no-pager", dest="pager", action="store_false", help="disable the pager"
)
global_options.add_option(
"--color",
choices=("auto", "always", "never"),
default=None,
help="control color usage: auto, always, never",
)
global_options.add_option(
"--trace",
dest="trace",
action="store_true",
help="trace git command execution (REPO_TRACE=1)",
)
global_options.add_option(
"--trace-to-stderr",
dest="trace_to_stderr",
action="store_true",
help="trace outputs go to stderr in addition to .repo/TRACE_FILE",
)
global_options.add_option(
"--trace-python",
dest="trace_python",
action="store_true",
help="trace python command execution",
)
global_options.add_option(
"--time",
dest="time",
action="store_true",
help="time repo command execution",
)
global_options.add_option(
"--version",
dest="show_version",
action="store_true",
help="display this version of repo",
)
global_options.add_option(
"--show-toplevel",
action="store_true",
help="display the path of the top-level directory of "
"the repo client checkout",
)
global_options.add_option(
"--event-log",
dest="event_log",
action="store",
help="filename of event log to append timeline to",
)
global_options.add_option(
"--git-trace2-event-log",
action="store",
help="directory to write git trace2 event log to",
)
global_options.add_option(
"--submanifest-path",
action="store",
metavar="REL_PATH",
help="submanifest path",
)
class _Repo(object):
@ -144,13 +189,15 @@ class _Repo(object):
global_options.print_help()
print()
if short:
commands = ' '.join(sorted(self.commands))
commands = " ".join(sorted(self.commands))
wrapped_commands = textwrap.wrap(commands, width=77)
print('Available commands:\n %s' % ('\n '.join(wrapped_commands),))
print('\nRun `repo help <command>` for command-specific details.')
print('Bug reports:', Wrapper().BUG_URL)
print(
"Available commands:\n %s" % ("\n ".join(wrapped_commands),)
)
print("\nRun `repo help <command>` for command-specific details.")
print("Bug reports:", Wrapper().BUG_URL)
else:
cmd = self.commands['help']()
cmd = self.commands["help"]()
if all_commands:
cmd.PrintAllCommandsBody()
else:
@ -159,7 +206,7 @@ class _Repo(object):
def _ParseArgs(self, argv):
"""Parse the main `repo` command line options."""
for i, arg in enumerate(argv):
if not arg.startswith('-'):
if not arg.startswith("-"):
name = arg
glob = argv[:i]
argv = argv[i + 1 :]
@ -182,14 +229,14 @@ class _Repo(object):
if name in self.commands:
return name, []
key = 'alias.%s' % (name,)
key = "alias.%s" % (name,)
alias = RepoConfig.ForRepository(self.repodir).GetString(key)
if alias is None:
alias = RepoConfig.ForUser().GetString(key)
if alias is None:
return name, []
args = alias.strip().split(' ', 1)
args = alias.strip().split(" ", 1)
name = args[0]
if len(args) == 2:
args = shlex.split(args[1])
@ -207,7 +254,7 @@ class _Repo(object):
return 0
elif gopts.show_version:
# Always allow global --version regardless of subcommand validity.
name = 'version'
name = "version"
elif gopts.show_toplevel:
print(os.path.dirname(self.repodir))
return 0
@ -217,12 +264,20 @@ class _Repo(object):
return 1
run = lambda: self._RunLong(name, gopts, argv) or 0
with Trace('starting new command: %s', ', '.join([name] + argv),
first_trace=True):
with Trace(
"starting new command: %s",
", ".join([name] + argv),
first_trace=True,
):
if gopts.trace_python:
import trace
tracer = trace.Trace(count=False, trace=True, timing=True,
ignoredirs=set(sys.path[1:]))
tracer = trace.Trace(
count=False,
trace=True,
timing=True,
ignoredirs=set(sys.path[1:]),
)
result = tracer.runfunc(run)
else:
result = run()
@ -237,9 +292,11 @@ class _Repo(object):
outer_client = RepoClient(self.repodir)
repo_client = outer_client
if gopts.submanifest_path:
repo_client = RepoClient(self.repodir,
repo_client = RepoClient(
self.repodir,
submanifest_path=gopts.submanifest_path,
outer_client=outer_client)
outer_client=outer_client,
)
gitc_manifest = None
gitc_client_name = gitc_utils.parse_clientdir(os.getcwd())
if gitc_client_name:
@ -254,37 +311,50 @@ class _Repo(object):
outer_client=outer_client,
outer_manifest=outer_client.manifest,
gitc_manifest=gitc_manifest,
git_event_log=git_trace2_event_log)
git_event_log=git_trace2_event_log,
)
except KeyError:
print("repo: '%s' is not a repo command. See 'repo help'." % name,
file=sys.stderr)
print(
"repo: '%s' is not a repo command. See 'repo help'." % name,
file=sys.stderr,
)
return 1
Editor.globalConfig = cmd.client.globalConfig
if not isinstance(cmd, MirrorSafeCommand) and cmd.manifest.IsMirror:
print("fatal: '%s' requires a working directory" % name,
file=sys.stderr)
print(
"fatal: '%s' requires a working directory" % name,
file=sys.stderr,
)
return 1
if isinstance(cmd, GitcAvailableCommand) and not gitc_utils.get_gitc_manifest_dir():
print("fatal: '%s' requires GITC to be available" % name,
file=sys.stderr)
if (
isinstance(cmd, GitcAvailableCommand)
and not gitc_utils.get_gitc_manifest_dir()
):
print(
"fatal: '%s' requires GITC to be available" % name,
file=sys.stderr,
)
return 1
if isinstance(cmd, GitcClientCommand) and not gitc_client_name:
print("fatal: '%s' requires a GITC client" % name,
file=sys.stderr)
print("fatal: '%s' requires a GITC client" % name, file=sys.stderr)
return 1
try:
copts, cargs = cmd.OptionParser.parse_args(argv)
copts = cmd.ReadEnvironmentOptions(copts)
except NoManifestException as e:
print('error: in `%s`: %s' % (' '.join([name] + argv), str(e)),
file=sys.stderr)
print('error: manifest missing or unreadable -- please run init',
file=sys.stderr)
print(
"error: in `%s`: %s" % (" ".join([name] + argv), str(e)),
file=sys.stderr,
)
print(
"error: manifest missing or unreadable -- please run init",
file=sys.stderr,
)
return 1
if gopts.pager is not False and not isinstance(cmd, InteractiveCommand):
@ -292,7 +362,7 @@ class _Repo(object):
if gopts.pager:
use_pager = True
else:
use_pager = config.GetBoolean('pager.%s' % name)
use_pager = config.GetBoolean("pager.%s" % name)
if use_pager is None:
use_pager = cmd.WantPager(copts)
if use_pager:
@ -302,7 +372,7 @@ class _Repo(object):
cmd_event = cmd.event_log.Add(name, event_log.TASK_COMMAND, start)
cmd.event_log.SetParent(cmd_event)
git_trace2_event_log.StartEvent()
git_trace2_event_log.CommandEvent(name='repo', subcommands=[name])
git_trace2_event_log.CommandEvent(name="repo", subcommands=[name])
try:
cmd.CommonValidateOptions(copts, cargs)
@ -314,10 +384,10 @@ class _Repo(object):
result = cmd.Execute(copts, cargs)
elif outer_manifest and repo_client.manifest.is_submanifest:
# The command does not support multi-manifest, we are using a
# submanifest, and the command line is for the outermost manifest.
# Re-run using the outermost manifest, which will recurse through the
# submanifests.
gopts.submanifest_path = ''
# submanifest, and the command line is for the outermost
# manifest. Re-run using the outermost manifest, which will
# recurse through the submanifests.
gopts.submanifest_path = ""
result = self._Run(name, gopts, argv)
else:
# No multi-manifest support. Run the command in the current
@ -327,36 +397,52 @@ class _Repo(object):
spec = submanifest.ToSubmanifestSpec()
gopts.submanifest_path = submanifest.repo_client.path_prefix
child_argv = argv[:]
child_argv.append('--no-outer-manifest')
# Not all subcommands support the 3 manifest options, so only add them
# if the original command includes them.
if hasattr(copts, 'manifest_url'):
child_argv.extend(['--manifest-url', spec.manifestUrl])
if hasattr(copts, 'manifest_name'):
child_argv.extend(['--manifest-name', spec.manifestName])
if hasattr(copts, 'manifest_branch'):
child_argv.extend(['--manifest-branch', spec.revision])
child_argv.append("--no-outer-manifest")
# Not all subcommands support the 3 manifest options, so
# only add them if the original command includes them.
if hasattr(copts, "manifest_url"):
child_argv.extend(["--manifest-url", spec.manifestUrl])
if hasattr(copts, "manifest_name"):
child_argv.extend(
["--manifest-name", spec.manifestName]
)
if hasattr(copts, "manifest_branch"):
child_argv.extend(["--manifest-branch", spec.revision])
result = self._Run(name, gopts, child_argv) or result
except (DownloadError, ManifestInvalidRevisionError,
NoManifestException) as e:
print('error: in `%s`: %s' % (' '.join([name] + argv), str(e)),
file=sys.stderr)
except (
DownloadError,
ManifestInvalidRevisionError,
NoManifestException,
) as e:
print(
"error: in `%s`: %s" % (" ".join([name] + argv), str(e)),
file=sys.stderr,
)
if isinstance(e, NoManifestException):
print('error: manifest missing or unreadable -- please run init',
file=sys.stderr)
print(
"error: manifest missing or unreadable -- please run init",
file=sys.stderr,
)
result = 1
except NoSuchProjectError as e:
if e.name:
print('error: project %s not found' % e.name, file=sys.stderr)
print("error: project %s not found" % e.name, file=sys.stderr)
else:
print('error: no project in current directory', file=sys.stderr)
print("error: no project in current directory", file=sys.stderr)
result = 1
except InvalidProjectGroupsError as e:
if e.name:
print('error: project group must be enabled for project %s' % e.name, file=sys.stderr)
print(
"error: project group must be enabled for project %s"
% e.name,
file=sys.stderr,
)
else:
print('error: project group must be enabled for the project in the current directory',
file=sys.stderr)
print(
"error: project group must be enabled for the project in "
"the current directory",
file=sys.stderr,
)
result = 1
except SystemExit as e:
if e.code:
@ -369,20 +455,27 @@ class _Repo(object):
minutes, seconds = divmod(remainder, 60)
if gopts.time:
if hours == 0:
print('real\t%dm%.3fs' % (minutes, seconds), file=sys.stderr)
print(
"real\t%dm%.3fs" % (minutes, seconds), file=sys.stderr
)
else:
print('real\t%dh%dm%.3fs' % (hours, minutes, seconds),
file=sys.stderr)
print(
"real\t%dh%dm%.3fs" % (hours, minutes, seconds),
file=sys.stderr,
)
cmd.event_log.FinishEvent(cmd_event, finish,
result is None or result == 0)
cmd.event_log.FinishEvent(
cmd_event, finish, result is None or result == 0
)
git_trace2_event_log.DefParamRepoEvents(
cmd.manifest.manifestProject.config.DumpConfigDict())
cmd.manifest.manifestProject.config.DumpConfigDict()
)
git_trace2_event_log.ExitEvent(result)
if gopts.event_log:
cmd.event_log.Write(os.path.abspath(
os.path.expanduser(gopts.event_log)))
cmd.event_log.Write(
os.path.abspath(os.path.expanduser(gopts.event_log))
)
git_trace2_event_log.Write(gopts.git_trace2_event_log)
return result
@ -392,29 +485,31 @@ def _CheckWrapperVersion(ver_str, repo_path):
"""Verify the repo launcher is new enough for this checkout.
Args:
ver_str: The version string passed from the repo launcher when it ran us.
ver_str: The version string passed from the repo launcher when it ran
us.
repo_path: The path to the repo launcher that loaded us.
"""
# Refuse to work with really old wrapper versions. We don't test these,
# so might as well require a somewhat recent sane version.
# v1.15 of the repo launcher was released in ~Mar 2012.
MIN_REPO_VERSION = (1, 15)
min_str = '.'.join(str(x) for x in MIN_REPO_VERSION)
min_str = ".".join(str(x) for x in MIN_REPO_VERSION)
if not repo_path:
repo_path = '~/bin/repo'
repo_path = "~/bin/repo"
if not ver_str:
print('no --wrapper-version argument', file=sys.stderr)
print("no --wrapper-version argument", file=sys.stderr)
sys.exit(1)
# Pull out the version of the repo launcher we know about to compare.
exp = Wrapper().VERSION
ver = tuple(map(int, ver_str.split('.')))
ver = tuple(map(int, ver_str.split(".")))
exp_str = '.'.join(map(str, exp))
exp_str = ".".join(map(str, exp))
if ver < MIN_REPO_VERSION:
print("""
print(
"""
repo: error:
!!! Your version of repo %s is too old.
!!! We need at least version %s.
@ -422,29 +517,42 @@ repo: error:
!!! You must upgrade before you can continue:
cp %s %s
""" % (ver_str, min_str, exp_str, WrapperPath(), repo_path), file=sys.stderr)
"""
% (ver_str, min_str, exp_str, WrapperPath(), repo_path),
file=sys.stderr,
)
sys.exit(1)
if exp > ver:
print('\n... A new version of repo (%s) is available.' % (exp_str,),
file=sys.stderr)
print(
"\n... A new version of repo (%s) is available." % (exp_str,),
file=sys.stderr,
)
if os.access(repo_path, os.W_OK):
print("""\
print(
"""\
... You should upgrade soon:
cp %s %s
""" % (WrapperPath(), repo_path), file=sys.stderr)
"""
% (WrapperPath(), repo_path),
file=sys.stderr,
)
else:
print("""\
print(
"""\
... New version is available at: %s
... The launcher is run from: %s
!!! The launcher is not writable. Please talk to your sysadmin or distro
!!! to get an update installed.
""" % (WrapperPath(), repo_path), file=sys.stderr)
"""
% (WrapperPath(), repo_path),
file=sys.stderr,
)
def _CheckRepoDir(repo_dir):
if not repo_dir:
print('no --repo-dir argument', file=sys.stderr)
print("no --repo-dir argument", file=sys.stderr)
sys.exit(1)
@ -452,10 +560,10 @@ def _PruneOptions(argv, opt):
i = 0
while i < len(argv):
a = argv[i]
if a == '--':
if a == "--":
break
if a.startswith('--'):
eq = a.find('=')
if a.startswith("--"):
eq = a.find("=")
if eq > 0:
a = a[0:eq]
if not opt.has_option(a):
@ -466,11 +574,11 @@ def _PruneOptions(argv, opt):
class _UserAgentHandler(urllib.request.BaseHandler):
def http_request(self, req):
req.add_header('User-Agent', user_agent.repo)
req.add_header("User-Agent", user_agent.repo)
return req
def https_request(self, req):
req.add_header('User-Agent', user_agent.repo)
req.add_header("User-Agent", user_agent.repo)
return req
@ -481,7 +589,7 @@ def _AddPasswordFromUserInput(handler, msg, req):
if user is None:
print(msg)
try:
user = input('User: ')
user = input("User: ")
password = getpass.getpass()
except KeyboardInterrupt:
return
@ -492,23 +600,28 @@ class _BasicAuthHandler(urllib.request.HTTPBasicAuthHandler):
def http_error_401(self, req, fp, code, msg, headers):
_AddPasswordFromUserInput(self, msg, req)
return urllib.request.HTTPBasicAuthHandler.http_error_401(
self, req, fp, code, msg, headers)
self, req, fp, code, msg, headers
)
def http_error_auth_reqed(self, authreq, host, req, headers):
try:
old_add_header = req.add_header
def _add_header(name, val):
val = val.replace('\n', '')
val = val.replace("\n", "")
old_add_header(name, val)
req.add_header = _add_header
return urllib.request.AbstractBasicAuthHandler.http_error_auth_reqed(
self, authreq, host, req, headers)
return (
urllib.request.AbstractBasicAuthHandler.http_error_auth_reqed(
self, authreq, host, req, headers
)
)
except Exception:
reset = getattr(self, 'reset_retry_count', None)
reset = getattr(self, "reset_retry_count", None)
if reset is not None:
reset()
elif getattr(self, 'retried', None):
elif getattr(self, "retried", None):
self.retried = 0
raise
@ -517,23 +630,28 @@ class _DigestAuthHandler(urllib.request.HTTPDigestAuthHandler):
def http_error_401(self, req, fp, code, msg, headers):
_AddPasswordFromUserInput(self, msg, req)
return urllib.request.HTTPDigestAuthHandler.http_error_401(
self, req, fp, code, msg, headers)
self, req, fp, code, msg, headers
)
def http_error_auth_reqed(self, auth_header, host, req, headers):
try:
old_add_header = req.add_header
def _add_header(name, val):
val = val.replace('\n', '')
val = val.replace("\n", "")
old_add_header(name, val)
req.add_header = _add_header
return urllib.request.AbstractDigestAuthHandler.http_error_auth_reqed(
self, auth_header, host, req, headers)
return (
urllib.request.AbstractDigestAuthHandler.http_error_auth_reqed(
self, auth_header, host, req, headers
)
)
except Exception:
reset = getattr(self, 'reset_retry_count', None)
reset = getattr(self, "reset_retry_count", None)
if reset is not None:
reset()
elif getattr(self, 'retried', None):
elif getattr(self, "retried", None):
self.retried = 0
raise
@ -546,7 +664,9 @@ class _KerberosAuthHandler(urllib.request.BaseHandler):
def http_error_401(self, req, fp, code, msg, headers):
host = req.get_host()
retry = self.http_error_auth_reqed('www-authenticate', host, req, headers)
retry = self.http_error_auth_reqed(
"www-authenticate", host, req, headers
)
return retry
def http_error_auth_reqed(self, auth_header, host, req, headers):
@ -555,8 +675,13 @@ class _KerberosAuthHandler(urllib.request.BaseHandler):
authdata = self._negotiate_get_authdata(auth_header, headers)
if self.retried > 3:
raise urllib.request.HTTPError(req.get_full_url(), 401,
"Negotiate auth failed", headers, None)
raise urllib.request.HTTPError(
req.get_full_url(),
401,
"Negotiate auth failed",
headers,
None,
)
else:
self.retried += 1
@ -564,7 +689,7 @@ class _KerberosAuthHandler(urllib.request.BaseHandler):
if neghdr is None:
return None
req.add_unredirected_header('Authorization', neghdr)
req.add_unredirected_header("Authorization", neghdr)
response = self.parent.open(req)
srvauth = self._negotiate_get_authdata(auth_header, response.info())
@ -627,8 +752,8 @@ def init_http():
n = netrc.netrc()
for host in n.hosts:
p = n.hosts[host]
mgr.add_password(p[1], 'http://%s/' % host, p[0], p[2])
mgr.add_password(p[1], 'https://%s/' % host, p[0], p[2])
mgr.add_password(p[1], "http://%s/" % host, p[0], p[2])
mgr.add_password(p[1], "https://%s/" % host, p[0], p[2])
except netrc.NetrcParseError:
pass
except IOError:
@ -638,10 +763,12 @@ def init_http():
if kerberos:
handlers.append(_KerberosAuthHandler())
if 'http_proxy' in os.environ:
url = os.environ['http_proxy']
handlers.append(urllib.request.ProxyHandler({'http': url, 'https': url}))
if 'REPO_CURL_VERBOSE' in os.environ:
if "http_proxy" in os.environ:
url = os.environ["http_proxy"]
handlers.append(
urllib.request.ProxyHandler({"http": url, "https": url})
)
if "REPO_CURL_VERBOSE" in os.environ:
handlers.append(urllib.request.HTTPHandler(debuglevel=1))
handlers.append(urllib.request.HTTPSHandler(debuglevel=1))
urllib.request.install_opener(urllib.request.build_opener(*handlers))
@ -651,12 +778,17 @@ def _Main(argv):
result = 0
opt = optparse.OptionParser(usage="repo wrapperinfo -- ...")
opt.add_option("--repo-dir", dest="repodir",
help="path to .repo/")
opt.add_option("--wrapper-version", dest="wrapper_version",
help="version of the wrapper script")
opt.add_option("--wrapper-path", dest="wrapper_path",
help="location of the wrapper script")
opt.add_option("--repo-dir", dest="repodir", help="path to .repo/")
opt.add_option(
"--wrapper-version",
dest="wrapper_version",
help="version of the wrapper script",
)
opt.add_option(
"--wrapper-path",
dest="wrapper_path",
help="location of the wrapper script",
)
_PruneOptions(argv, opt)
opt, argv = opt.parse_args(argv)
@ -680,10 +812,10 @@ def _Main(argv):
result = repo._Run(name, gopts, argv) or 0
except KeyboardInterrupt:
print('aborted by user', file=sys.stderr)
print("aborted by user", file=sys.stderr)
result = 1
except ManifestParseError as mpe:
print('fatal: %s' % mpe, file=sys.stderr)
print("fatal: %s" % mpe, file=sys.stderr)
result = 1
except RepoChangedException as rce:
# If repo changed, re-exec ourselves.
@ -693,13 +825,13 @@ def _Main(argv):
try:
os.execv(sys.executable, [__file__] + argv)
except OSError as e:
print('fatal: cannot restart repo after upgrade', file=sys.stderr)
print('fatal: %s' % e, file=sys.stderr)
print("fatal: cannot restart repo after upgrade", file=sys.stderr)
print("fatal: %s" % e, file=sys.stderr)
result = 128
TerminatePager()
sys.exit(result)
if __name__ == '__main__':
if __name__ == "__main__":
_Main(sys.argv[1:])

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,7 @@ def RunPager(globalConfig):
if not os.isatty(0) or not os.isatty(1):
return
pager = _SelectPager(globalConfig)
if pager == '' or pager == 'cat':
if pager == "" or pager == "cat":
return
if platform_utils.isWindows():
@ -46,8 +46,8 @@ def TerminatePager():
pager_process.stdin.close()
pager_process.wait()
pager_process = None
# Restore initial stdout/err in case there is more output in this process
# after shutting down the pager process
# Restore initial stdout/err in case there is more output in this
# process after shutting down the pager process.
sys.stdout = old_stdout
sys.stderr = old_stderr
@ -55,10 +55,11 @@ def TerminatePager():
def _PipePager(pager):
global pager_process, old_stdout, old_stderr
assert pager_process is None, "Only one active pager process at a time"
# Create pager process, piping stdout/err into its stdin
# Create pager process, piping stdout/err into its stdin.
try:
pager_process = subprocess.Popen([pager], stdin=subprocess.PIPE, stdout=sys.stdout,
stderr=sys.stderr)
pager_process = subprocess.Popen(
[pager], stdin=subprocess.PIPE, stdout=sys.stdout, stderr=sys.stderr
)
except FileNotFoundError:
sys.exit(f'fatal: cannot start pager "{pager}"')
old_stdout = sys.stdout
@ -72,7 +73,6 @@ def _ForkPager(pager):
# This process turns into the pager; a child it forks will
# do the real processing and output back to the pager. This
# is necessary to keep the pager in control of the tty.
#
try:
r, w = os.pipe()
pid = os.fork()
@ -96,32 +96,31 @@ def _ForkPager(pager):
def _SelectPager(globalConfig):
try:
return os.environ['GIT_PAGER']
return os.environ["GIT_PAGER"]
except KeyError:
pass
pager = globalConfig.GetString('core.pager')
pager = globalConfig.GetString("core.pager")
if pager:
return pager
try:
return os.environ['PAGER']
return os.environ["PAGER"]
except KeyError:
pass
return 'less'
return "less"
def _BecomePager(pager):
# Delaying execution of the pager until we have output
# ready works around a long-standing bug in popularly
# available versions of 'less', a better 'more'.
#
_a, _b, _c = select.select([0], [], [0])
os.environ['LESS'] = 'FRSX'
os.environ["LESS"] = "FRSX"
try:
os.execvp(pager, [pager])
except OSError:
os.execv('/bin/sh', ['sh', '-c', pager])
os.execv("/bin/sh", ["sh", "-c", pager])

View File

@ -30,18 +30,24 @@ def isWindows():
def symlink(source, link_name):
"""Creates a symbolic link pointing to source named link_name.
Note: On Windows, source must exist on disk, as the implementation needs
to know whether to create a "File" or a "Directory" symbolic link.
"""
if isWindows():
import platform_utils_win32
source = _validate_winpath(source)
link_name = _validate_winpath(link_name)
target = os.path.join(os.path.dirname(link_name), source)
if isdir(target):
platform_utils_win32.create_dirsymlink(_makelongpath(source), link_name)
platform_utils_win32.create_dirsymlink(
_makelongpath(source), link_name
)
else:
platform_utils_win32.create_filesymlink(_makelongpath(source), link_name)
platform_utils_win32.create_filesymlink(
_makelongpath(source), link_name
)
else:
return os.symlink(source, link_name)
@ -50,8 +56,10 @@ def _validate_winpath(path):
path = os.path.normpath(path)
if _winpath_is_valid(path):
return path
raise ValueError("Path \"%s\" must be a relative path or an absolute "
"path starting with a drive letter".format(path))
raise ValueError(
'Path "{}" must be a relative path or an absolute '
"path starting with a drive letter".format(path)
)
def _winpath_is_valid(path):
@ -77,16 +85,17 @@ def _makelongpath(path):
MAX_PATH limit.
"""
if isWindows():
# Note: MAX_PATH is 260, but, for directories, the maximum value is actually 246.
# Note: MAX_PATH is 260, but, for directories, the maximum value is
# actually 246.
if len(path) < 246:
return path
if path.startswith(u"\\\\?\\"):
if path.startswith("\\\\?\\"):
return path
if not os.path.isabs(path):
return path
# Append prefix and ensure unicode so that the special longpath syntax
# is supported by underlying Win32 API calls
return u"\\\\?\\" + os.path.normpath(path)
return "\\\\?\\" + os.path.normpath(path)
else:
return path
@ -94,7 +103,8 @@ def _makelongpath(path):
def rmtree(path, ignore_errors=False):
"""shutil.rmtree(path) wrapper with support for long paths on Windows.
Availability: Unix, Windows."""
Availability: Unix, Windows.
"""
onerror = None
if isWindows():
path = _makelongpath(path)
@ -103,7 +113,7 @@ def rmtree(path, ignore_errors=False):
def handle_rmtree_error(function, path, excinfo):
# Allow deleting read-only files
# Allow deleting read-only files.
os.chmod(path, stat.S_IWRITE)
function(path)
@ -111,7 +121,8 @@ def handle_rmtree_error(function, path, excinfo):
def rename(src, dst):
"""os.rename(src, dst) wrapper with support for long paths on Windows.
Availability: Unix, Windows."""
Availability: Unix, Windows.
"""
if isWindows():
# On Windows, rename fails if destination exists, see
# https://docs.python.org/2/library/os.html#os.rename
@ -132,7 +143,8 @@ def remove(path, missing_ok=False):
allows deleting read-only files on Windows, with support for long paths and
for deleting directory symbolic links.
Availability: Unix, Windows."""
Availability: Unix, Windows.
"""
longpath = _makelongpath(path) if isWindows() else path
try:
os.remove(longpath)
@ -181,7 +193,9 @@ def _walk_windows_impl(top, topdown, onerror, followlinks):
for name in dirs:
new_path = os.path.join(top, name)
if followlinks or not islink(new_path):
for x in _walk_windows_impl(new_path, topdown, onerror, followlinks):
for x in _walk_windows_impl(
new_path, topdown, onerror, followlinks
):
yield x
if not topdown:
yield top, dirs, nondirs
@ -218,6 +232,7 @@ def islink(path):
"""
if isWindows():
import platform_utils_win32
return platform_utils_win32.islink(_makelongpath(path))
else:
return os.path.islink(path)
@ -233,6 +248,7 @@ def readlink(path):
"""
if isWindows():
import platform_utils_win32
return platform_utils_win32.readlink(_makelongpath(path))
else:
return os.readlink(path)
@ -250,10 +266,12 @@ def realpath(path):
for c in range(0, 100): # Avoid cycles
if islink(current_path):
target = readlink(current_path)
current_path = os.path.join(os.path.dirname(current_path), target)
current_path = os.path.join(
os.path.dirname(current_path), target
)
else:
basename = os.path.basename(current_path)
if basename == '':
if basename == "":
path_tail.append(current_path)
break
path_tail.append(basename)

View File

@ -19,7 +19,7 @@ from ctypes import c_buffer, c_ubyte, Structure, Union, byref
from ctypes.wintypes import BOOL, BOOLEAN, LPCWSTR, DWORD, HANDLE
from ctypes.wintypes import WCHAR, USHORT, LPVOID, ULONG, LPDWORD
kernel32 = WinDLL('kernel32', use_last_error=True)
kernel32 = WinDLL("kernel32", use_last_error=True)
UCHAR = c_ubyte
@ -31,14 +31,17 @@ ERROR_PRIVILEGE_NOT_HELD = 1314
# Win32 API entry points
CreateSymbolicLinkW = kernel32.CreateSymbolicLinkW
CreateSymbolicLinkW.restype = BOOLEAN
CreateSymbolicLinkW.argtypes = (LPCWSTR, # lpSymlinkFileName In
CreateSymbolicLinkW.argtypes = (
LPCWSTR, # lpSymlinkFileName In
LPCWSTR, # lpTargetFileName In
DWORD) # dwFlags In
DWORD, # dwFlags In
)
# Symbolic link creation flags
SYMBOLIC_LINK_FLAG_FILE = 0x00
SYMBOLIC_LINK_FLAG_DIRECTORY = 0x01
# symlink support for CreateSymbolicLink() starting with Windows 10 (1703, v10.0.14972)
# symlink support for CreateSymbolicLink() starting with Windows 10 (1703,
# v10.0.14972)
SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE = 0x02
GetFileAttributesW = kernel32.GetFileAttributesW
@ -50,13 +53,15 @@ FILE_ATTRIBUTE_REPARSE_POINT = 0x00400
CreateFileW = kernel32.CreateFileW
CreateFileW.restype = HANDLE
CreateFileW.argtypes = (LPCWSTR, # lpFileName In
CreateFileW.argtypes = (
LPCWSTR, # lpFileName In
DWORD, # dwDesiredAccess In
DWORD, # dwShareMode In
LPVOID, # lpSecurityAttributes In_opt
DWORD, # dwCreationDisposition In
DWORD, # dwFlagsAndAttributes In
HANDLE) # hTemplateFile In_opt
HANDLE, # hTemplateFile In_opt
)
CloseHandle = kernel32.CloseHandle
CloseHandle.restype = BOOL
@ -69,14 +74,16 @@ FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
DeviceIoControl = kernel32.DeviceIoControl
DeviceIoControl.restype = BOOL
DeviceIoControl.argtypes = (HANDLE, # hDevice In
DeviceIoControl.argtypes = (
HANDLE, # hDevice In
DWORD, # dwIoControlCode In
LPVOID, # lpInBuffer In_opt
DWORD, # nInBufferSize In
LPVOID, # lpOutBuffer Out_opt
DWORD, # nOutBufferSize In
LPDWORD, # lpBytesReturned Out_opt
LPVOID) # lpOverlapped Inout_opt
LPVOID, # lpOverlapped Inout_opt
)
# Device I/O control flags and options
FSCTL_GET_REPARSE_POINT = 0x000900A8
@ -86,16 +93,18 @@ MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 0x4000
class GENERIC_REPARSE_BUFFER(Structure):
_fields_ = (('DataBuffer', UCHAR * 1),)
_fields_ = (("DataBuffer", UCHAR * 1),)
class SYMBOLIC_LINK_REPARSE_BUFFER(Structure):
_fields_ = (('SubstituteNameOffset', USHORT),
('SubstituteNameLength', USHORT),
('PrintNameOffset', USHORT),
('PrintNameLength', USHORT),
('Flags', ULONG),
('PathBuffer', WCHAR * 1))
_fields_ = (
("SubstituteNameOffset", USHORT),
("SubstituteNameLength", USHORT),
("PrintNameOffset", USHORT),
("PrintNameLength", USHORT),
("Flags", ULONG),
("PathBuffer", WCHAR * 1),
)
@property
def PrintName(self):
@ -105,11 +114,13 @@ class SYMBOLIC_LINK_REPARSE_BUFFER(Structure):
class MOUNT_POINT_REPARSE_BUFFER(Structure):
_fields_ = (('SubstituteNameOffset', USHORT),
('SubstituteNameLength', USHORT),
('PrintNameOffset', USHORT),
('PrintNameLength', USHORT),
('PathBuffer', WCHAR * 1))
_fields_ = (
("SubstituteNameOffset", USHORT),
("SubstituteNameLength", USHORT),
("PrintNameOffset", USHORT),
("PrintNameLength", USHORT),
("PathBuffer", WCHAR * 1),
)
@property
def PrintName(self):
@ -120,14 +131,19 @@ class MOUNT_POINT_REPARSE_BUFFER(Structure):
class REPARSE_DATA_BUFFER(Structure):
class REPARSE_BUFFER(Union):
_fields_ = (('SymbolicLinkReparseBuffer', SYMBOLIC_LINK_REPARSE_BUFFER),
('MountPointReparseBuffer', MOUNT_POINT_REPARSE_BUFFER),
('GenericReparseBuffer', GENERIC_REPARSE_BUFFER))
_fields_ = (('ReparseTag', ULONG),
('ReparseDataLength', USHORT),
('Reserved', USHORT),
('ReparseBuffer', REPARSE_BUFFER))
_anonymous_ = ('ReparseBuffer',)
_fields_ = (
("SymbolicLinkReparseBuffer", SYMBOLIC_LINK_REPARSE_BUFFER),
("MountPointReparseBuffer", MOUNT_POINT_REPARSE_BUFFER),
("GenericReparseBuffer", GENERIC_REPARSE_BUFFER),
)
_fields_ = (
("ReparseTag", ULONG),
("ReparseDataLength", USHORT),
("Reserved", USHORT),
("ReparseBuffer", REPARSE_BUFFER),
)
_anonymous_ = ("ReparseBuffer",)
def create_filesymlink(source, link_name):
@ -136,25 +152,27 @@ def create_filesymlink(source, link_name):
def create_dirsymlink(source, link_name):
"""Creates a Windows directory symbolic link source pointing to link_name.
"""
"""Creates a Windows directory symbolic link source pointing to link_name.""" # noqa: E501
_create_symlink(source, link_name, SYMBOLIC_LINK_FLAG_DIRECTORY)
def _create_symlink(source, link_name, dwFlags):
if not CreateSymbolicLinkW(link_name, source,
dwFlags | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE):
# See https://github.com/golang/go/pull/24307/files#diff-b87bc12e4da2497308f9ef746086e4f0
# "the unprivileged create flag is unsupported below Windows 10 (1703, v10.0.14972).
# retry without it."
if not CreateSymbolicLinkW(
link_name,
source,
dwFlags | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE,
):
# See https://github.com/golang/go/pull/24307/files#diff-b87bc12e4da2497308f9ef746086e4f0 # noqa: E501
# "the unprivileged create flag is unsupported below Windows 10 (1703,
# v10.0.14972). retry without it."
if not CreateSymbolicLinkW(link_name, source, dwFlags):
code = get_last_error()
error_desc = FormatError(code).strip()
if code == ERROR_PRIVILEGE_NOT_HELD:
raise OSError(errno.EPERM, error_desc, link_name)
_raise_winerror(
code,
'Error creating symbolic link \"%s\"'.format(link_name))
code, 'Error creating symbolic link "{}"'.format(link_name)
)
def islink(path):
@ -165,45 +183,48 @@ def islink(path):
def readlink(path):
reparse_point_handle = CreateFileW(path,
reparse_point_handle = CreateFileW(
path,
0,
0,
None,
OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT |
FILE_FLAG_BACKUP_SEMANTICS,
None)
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
None,
)
if reparse_point_handle == INVALID_HANDLE_VALUE:
_raise_winerror(
get_last_error(),
'Error opening symbolic link \"%s\"'.format(path))
get_last_error(), 'Error opening symbolic link "{}"'.format(path)
)
target_buffer = c_buffer(MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
n_bytes_returned = DWORD()
io_result = DeviceIoControl(reparse_point_handle,
io_result = DeviceIoControl(
reparse_point_handle,
FSCTL_GET_REPARSE_POINT,
None,
0,
target_buffer,
len(target_buffer),
byref(n_bytes_returned),
None)
None,
)
CloseHandle(reparse_point_handle)
if not io_result:
_raise_winerror(
get_last_error(),
'Error reading symbolic link \"%s\"'.format(path))
get_last_error(), 'Error reading symbolic link "{}"'.format(path)
)
rdb = REPARSE_DATA_BUFFER.from_buffer(target_buffer)
if rdb.ReparseTag == IO_REPARSE_TAG_SYMLINK:
return rdb.SymbolicLinkReparseBuffer.PrintName
elif rdb.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT:
return rdb.MountPointReparseBuffer.PrintName
# Unsupported reparse point type
# Unsupported reparse point type.
_raise_winerror(
ERROR_NOT_SUPPORTED,
'Error reading symbolic link \"%s\"'.format(path))
ERROR_NOT_SUPPORTED, 'Error reading symbolic link "{}"'.format(path)
)
def _raise_winerror(code, error_desc):
win_error_desc = FormatError(code).strip()
error_desc = "%s: %s".format(error_desc, win_error_desc)
error_desc = "{0}: {1}".format(error_desc, win_error_desc)
raise WinError(code, error_desc)

View File

@ -22,12 +22,12 @@ _NOT_TTY = not os.isatty(2)
# This will erase all content in the current line (wherever the cursor is).
# It does not move the cursor, so this is usually followed by \r to move to
# column 0.
CSI_ERASE_LINE = '\x1b[2K'
CSI_ERASE_LINE = "\x1b[2K"
# This will erase all content in the current line after the cursor. This is
# useful for partial updates & progress messages as the terminal can display
# it better.
CSI_ERASE_LINE_AFTER = '\x1b[K'
CSI_ERASE_LINE_AFTER = "\x1b[K"
def duration_str(total):
@ -38,17 +38,24 @@ def duration_str(total):
"""
hours, rem = divmod(total, 3600)
mins, secs = divmod(rem, 60)
ret = '%.3fs' % (secs,)
ret = "%.3fs" % (secs,)
if mins:
ret = '%im%s' % (mins, ret)
ret = "%im%s" % (mins, ret)
if hours:
ret = '%ih%s' % (hours, ret)
ret = "%ih%s" % (hours, ret)
return ret
class Progress(object):
def __init__(self, title, total=0, units='', print_newline=False, delay=True,
quiet=False):
def __init__(
self,
title,
total=0,
units="",
print_newline=False,
delay=True,
quiet=False,
):
self._title = title
self._total = total
self._done = 0
@ -71,13 +78,13 @@ class Progress(object):
self._active += 1
if not self._show_jobs:
self._show_jobs = self._active > 1
self.update(inc=0, msg='started ' + name)
self.update(inc=0, msg="started " + name)
def finish(self, name):
self.update(msg='finished ' + name)
self.update(msg="finished " + name)
self._active -= 1
def update(self, inc=1, msg=''):
def update(self, inc=1, msg=""):
self._done += inc
if _NOT_TTY or IsTraceToStderr():
@ -90,26 +97,35 @@ class Progress(object):
return
if self._total <= 0:
sys.stderr.write('\r%s: %d,%s' % (
self._title,
self._done,
CSI_ERASE_LINE_AFTER))
sys.stderr.write(
"\r%s: %d,%s" % (self._title, self._done, CSI_ERASE_LINE_AFTER)
)
sys.stderr.flush()
else:
p = (100 * self._done) / self._total
if self._show_jobs:
jobs = '[%d job%s] ' % (self._active, 's' if self._active > 1 else '')
jobs = "[%d job%s] " % (
self._active,
"s" if self._active > 1 else "",
)
else:
jobs = ''
sys.stderr.write('\r%s: %2d%% %s(%d%s/%d%s)%s%s%s%s' % (
jobs = ""
sys.stderr.write(
"\r%s: %2d%% %s(%d%s/%d%s)%s%s%s%s"
% (
self._title,
p,
jobs,
self._done, self._units,
self._total, self._units,
' ' if msg else '', msg,
self._done,
self._units,
self._total,
self._units,
" " if msg else "",
msg,
CSI_ERASE_LINE_AFTER,
'\n' if self._print_newline else ''))
"\n" if self._print_newline else "",
)
)
sys.stderr.flush()
def end(self):
@ -118,19 +134,24 @@ class Progress(object):
duration = duration_str(time() - self._start)
if self._total <= 0:
sys.stderr.write('\r%s: %d, done in %s%s\n' % (
self._title,
self._done,
duration,
CSI_ERASE_LINE_AFTER))
sys.stderr.write(
"\r%s: %d, done in %s%s\n"
% (self._title, self._done, duration, CSI_ERASE_LINE_AFTER)
)
sys.stderr.flush()
else:
p = (100 * self._done) / self._total
sys.stderr.write('\r%s: %3d%% (%d%s/%d%s), done in %s%s\n' % (
sys.stderr.write(
"\r%s: %3d%% (%d%s/%d%s), done in %s%s\n"
% (
self._title,
p,
self._done, self._units,
self._total, self._units,
self._done,
self._units,
self._total,
self._units,
duration,
CSI_ERASE_LINE_AFTER))
CSI_ERASE_LINE_AFTER,
)
)
sys.stderr.flush()

2402
project.py

File diff suppressed because it is too large Load Diff

View File

@ -29,42 +29,55 @@ import util
def sign(opts):
"""Sign the launcher!"""
output = ''
output = ""
for key in opts.keys:
# We use ! at the end of the key so that gpg uses this specific key.
# Otherwise it uses the key as a lookup into the overall key and uses the
# default signing key. i.e. It will see that KEYID_RSA is a subkey of
# another key, and use the primary key to sign instead of the subkey.
cmd = ['gpg', '--homedir', opts.gpgdir, '-u', f'{key}!', '--batch', '--yes',
'--armor', '--detach-sign', '--output', '-', opts.launcher]
ret = util.run(opts, cmd, encoding='utf-8', stdout=subprocess.PIPE)
# Otherwise it uses the key as a lookup into the overall key and uses
# the default signing key. i.e. It will see that KEYID_RSA is a subkey
# of another key, and use the primary key to sign instead of the subkey.
cmd = [
"gpg",
"--homedir",
opts.gpgdir,
"-u",
f"{key}!",
"--batch",
"--yes",
"--armor",
"--detach-sign",
"--output",
"-",
opts.launcher,
]
ret = util.run(opts, cmd, encoding="utf-8", stdout=subprocess.PIPE)
output += ret.stdout
# Save the combined signatures into one file.
with open(f'{opts.launcher}.asc', 'w', encoding='utf-8') as fp:
with open(f"{opts.launcher}.asc", "w", encoding="utf-8") as fp:
fp.write(output)
def check(opts):
"""Check the signature."""
util.run(opts, ['gpg', '--verify', f'{opts.launcher}.asc'])
util.run(opts, ["gpg", "--verify", f"{opts.launcher}.asc"])
def get_version(opts):
"""Get the version from |launcher|."""
# Make sure we don't search $PATH when signing the "repo" file in the cwd.
launcher = os.path.join('.', opts.launcher)
cmd = [launcher, '--version']
ret = util.run(opts, cmd, encoding='utf-8', stdout=subprocess.PIPE)
m = re.search(r'repo launcher version ([0-9.]+)', ret.stdout)
launcher = os.path.join(".", opts.launcher)
cmd = [launcher, "--version"]
ret = util.run(opts, cmd, encoding="utf-8", stdout=subprocess.PIPE)
m = re.search(r"repo launcher version ([0-9.]+)", ret.stdout)
if not m:
sys.exit(f'{opts.launcher}: unable to detect repo version')
sys.exit(f"{opts.launcher}: unable to detect repo version")
return m.group(1)
def postmsg(opts, version):
"""Helpful info to show at the end for release manager."""
print(f"""
print(
f"""
Repo launcher bucket:
gs://git-repo-downloads/
@ -81,24 +94,39 @@ NB: If a rollback is necessary, the GS bucket archives old versions, and may be
gsutil ls -la gs://git-repo-downloads/repo gs://git-repo-downloads/repo.asc
gsutil cp -a public-read gs://git-repo-downloads/repo#<unique id> gs://git-repo-downloads/repo
gsutil cp -a public-read gs://git-repo-downloads/repo.asc#<unique id> gs://git-repo-downloads/repo.asc
""")
""" # noqa: E501
)
def get_parser():
"""Get a CLI parser."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('-n', '--dry-run',
dest='dryrun', action='store_true',
help='show everything that would be done')
parser.add_argument('--gpgdir',
default=os.path.join(util.HOMEDIR, '.gnupg', 'repo'),
help='path to dedicated gpg dir with release keys '
'(default: ~/.gnupg/repo/)')
parser.add_argument('--keyid', dest='keys', default=[], action='append',
help='alternative signing keys to use')
parser.add_argument('launcher',
default=os.path.join(util.TOPDIR, 'repo'), nargs='?',
help='the launcher script to sign')
parser.add_argument(
"-n",
"--dry-run",
dest="dryrun",
action="store_true",
help="show everything that would be done",
)
parser.add_argument(
"--gpgdir",
default=os.path.join(util.HOMEDIR, ".gnupg", "repo"),
help="path to dedicated gpg dir with release keys "
"(default: ~/.gnupg/repo/)",
)
parser.add_argument(
"--keyid",
dest="keys",
default=[],
action="append",
help="alternative signing keys to use",
)
parser.add_argument(
"launcher",
default=os.path.join(util.TOPDIR, "repo"),
nargs="?",
help="the launcher script to sign",
)
return parser
@ -108,18 +136,20 @@ def main(argv):
opts = parser.parse_args(argv)
if not os.path.exists(opts.gpgdir):
parser.error(f'--gpgdir does not exist: {opts.gpgdir}')
parser.error(f"--gpgdir does not exist: {opts.gpgdir}")
if not os.path.exists(opts.launcher):
parser.error(f'launcher does not exist: {opts.launcher}')
parser.error(f"launcher does not exist: {opts.launcher}")
opts.launcher = os.path.relpath(opts.launcher)
print(f'Signing "{opts.launcher}" launcher script and saving to '
f'"{opts.launcher}.asc"')
print(
f'Signing "{opts.launcher}" launcher script and saving to '
f'"{opts.launcher}.asc"'
)
if opts.keys:
print(f'Using custom keys to sign: {" ".join(opts.keys)}')
else:
print('Using official Repo release keys to sign')
print("Using official Repo release keys to sign")
opts.keys = [util.KEYID_DSA, util.KEYID_RSA, util.KEYID_ECC]
util.import_release_key(opts)
@ -131,5 +161,5 @@ def main(argv):
return 0
if __name__ == '__main__':
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))

View File

@ -35,7 +35,7 @@ import util
KEYID = util.KEYID_DSA
# Regular expression to validate tag names.
RE_VALID_TAG = r'^v([0-9]+[.])+[0-9]+$'
RE_VALID_TAG = r"^v([0-9]+[.])+[0-9]+$"
def sign(opts):
@ -44,11 +44,20 @@ def sign(opts):
# Otherwise it uses the key as a lookup into the overall key and uses the
# default signing key. i.e. It will see that KEYID_RSA is a subkey of
# another key, and use the primary key to sign instead of the subkey.
cmd = ['git', 'tag', '-s', opts.tag, '-u', f'{opts.key}!',
'-m', f'repo {opts.tag}', opts.commit]
cmd = [
"git",
"tag",
"-s",
opts.tag,
"-u",
f"{opts.key}!",
"-m",
f"repo {opts.tag}",
opts.commit,
]
key = 'GNUPGHOME'
print('+', f'export {key}="{opts.gpgdir}"')
key = "GNUPGHOME"
print("+", f'export {key}="{opts.gpgdir}"')
oldvalue = os.getenv(key)
os.putenv(key, opts.gpgdir)
util.run(opts, cmd)
@ -60,21 +69,27 @@ def sign(opts):
def check(opts):
"""Check the signature."""
util.run(opts, ['git', 'tag', '--verify', opts.tag])
util.run(opts, ["git", "tag", "--verify", opts.tag])
def postmsg(opts):
"""Helpful info to show at the end for release manager."""
cmd = ['git', 'rev-parse', 'remotes/origin/stable']
ret = util.run(opts, cmd, encoding='utf-8', stdout=subprocess.PIPE)
cmd = ["git", "rev-parse", "remotes/origin/stable"]
ret = util.run(opts, cmd, encoding="utf-8", stdout=subprocess.PIPE)
current_release = ret.stdout.strip()
cmd = ['git', 'log', '--format=%h (%aN) %s', '--no-merges',
f'remotes/origin/stable..{opts.tag}']
ret = util.run(opts, cmd, encoding='utf-8', stdout=subprocess.PIPE)
cmd = [
"git",
"log",
"--format=%h (%aN) %s",
"--no-merges",
f"remotes/origin/stable..{opts.tag}",
]
ret = util.run(opts, cmd, encoding="utf-8", stdout=subprocess.PIPE)
shortlog = ret.stdout.strip()
print(f"""
print(
f"""
Here's the short log since the last release.
{shortlog}
@ -84,29 +99,39 @@ NB: People will start upgrading to this version immediately.
To roll back a release:
git push origin --force {current_release}:stable -n
""")
"""
)
def get_parser():
"""Get a CLI parser."""
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('-n', '--dry-run',
dest='dryrun', action='store_true',
help='show everything that would be done')
parser.add_argument('--gpgdir',
default=os.path.join(util.HOMEDIR, '.gnupg', 'repo'),
help='path to dedicated gpg dir with release keys '
'(default: ~/.gnupg/repo/)')
parser.add_argument('-f', '--force', action='store_true',
help='force signing of any tag')
parser.add_argument('--keyid', dest='key',
help='alternative signing key to use')
parser.add_argument('tag',
help='the tag to create (e.g. "v2.0")')
parser.add_argument('commit', default='HEAD', nargs='?',
help='the commit to tag')
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"-n",
"--dry-run",
dest="dryrun",
action="store_true",
help="show everything that would be done",
)
parser.add_argument(
"--gpgdir",
default=os.path.join(util.HOMEDIR, ".gnupg", "repo"),
help="path to dedicated gpg dir with release keys "
"(default: ~/.gnupg/repo/)",
)
parser.add_argument(
"-f", "--force", action="store_true", help="force signing of any tag"
)
parser.add_argument(
"--keyid", dest="key", help="alternative signing key to use"
)
parser.add_argument("tag", help='the tag to create (e.g. "v2.0")')
parser.add_argument(
"commit", default="HEAD", nargs="?", help="the commit to tag"
)
return parser
@ -116,16 +141,18 @@ def main(argv):
opts = parser.parse_args(argv)
if not os.path.exists(opts.gpgdir):
parser.error(f'--gpgdir does not exist: {opts.gpgdir}')
parser.error(f"--gpgdir does not exist: {opts.gpgdir}")
if not opts.force and not re.match(RE_VALID_TAG, opts.tag):
parser.error(f'tag "{opts.tag}" does not match regex "{RE_VALID_TAG}"; '
'use --force to sign anyways')
parser.error(
f'tag "{opts.tag}" does not match regex "{RE_VALID_TAG}"; '
"use --force to sign anyways"
)
if opts.key:
print(f'Using custom key to sign: {opts.key}')
print(f"Using custom key to sign: {opts.key}")
else:
print('Using official Repo release key to sign')
print("Using official Repo release key to sign")
opts.key = KEYID
util.import_release_key(opts)
@ -136,5 +163,5 @@ def main(argv):
return 0
if __name__ == '__main__':
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))

View File

@ -29,72 +29,106 @@ import sys
import tempfile
TOPDIR = Path(__file__).resolve().parent.parent
MANDIR = TOPDIR.joinpath('man')
MANDIR = TOPDIR.joinpath("man")
# Load repo local modules.
sys.path.insert(0, str(TOPDIR))
from git_command import RepoSourceVersion
import subcmds
def worker(cmd, **kwargs):
subprocess.run(cmd, **kwargs)
def main(argv):
parser = argparse.ArgumentParser(description=__doc__)
opts = parser.parse_args(argv)
parser.parse_args(argv)
if not shutil.which('help2man'):
sys.exit('Please install help2man to continue.')
if not shutil.which("help2man"):
sys.exit("Please install help2man to continue.")
# Let repo know we're generating man pages so it can avoid some dynamic
# behavior (like probing active number of CPUs). We use a weird name &
# value to make it less likely for users to set this var themselves.
os.environ['_REPO_GENERATE_MANPAGES_'] = ' indeed! '
os.environ["_REPO_GENERATE_MANPAGES_"] = " indeed! "
# "repo branch" is an alias for "repo branches".
del subcmds.all_commands['branch']
(MANDIR / 'repo-branch.1').write_text('.so man1/repo-branches.1')
del subcmds.all_commands["branch"]
(MANDIR / "repo-branch.1").write_text(".so man1/repo-branches.1")
version = RepoSourceVersion()
cmdlist = [['help2man', '-N', '-n', f'repo {cmd} - manual page for repo {cmd}',
'-S', f'repo {cmd}', '-m', 'Repo Manual', f'--version-string={version}',
'-o', MANDIR.joinpath(f'repo-{cmd}.1.tmp'), './repo',
'-h', f'help {cmd}'] for cmd in subcmds.all_commands]
cmdlist.append(['help2man', '-N', '-n', 'repository management tool built on top of git',
'-S', 'repo', '-m', 'Repo Manual', f'--version-string={version}',
'-o', MANDIR.joinpath('repo.1.tmp'), './repo',
'-h', '--help-all'])
cmdlist = [
[
"help2man",
"-N",
"-n",
f"repo {cmd} - manual page for repo {cmd}",
"-S",
f"repo {cmd}",
"-m",
"Repo Manual",
f"--version-string={version}",
"-o",
MANDIR.joinpath(f"repo-{cmd}.1.tmp"),
"./repo",
"-h",
f"help {cmd}",
]
for cmd in subcmds.all_commands
]
cmdlist.append(
[
"help2man",
"-N",
"-n",
"repository management tool built on top of git",
"-S",
"repo",
"-m",
"Repo Manual",
f"--version-string={version}",
"-o",
MANDIR.joinpath("repo.1.tmp"),
"./repo",
"-h",
"--help-all",
]
)
with tempfile.TemporaryDirectory() as tempdir:
tempdir = Path(tempdir)
repo_dir = tempdir / '.repo'
repo_dir = tempdir / ".repo"
repo_dir.mkdir()
(repo_dir / 'repo').symlink_to(TOPDIR)
(repo_dir / "repo").symlink_to(TOPDIR)
# Create a repo wrapper using the active Python executable. We can't pass
# this directly to help2man as it's too simple, so insert it via shebang.
data = (TOPDIR / 'repo').read_text(encoding='utf-8')
tempbin = tempdir / 'repo'
tempbin.write_text(f'#!{sys.executable}\n' + data, encoding='utf-8')
# Create a repo wrapper using the active Python executable. We can't
# pass this directly to help2man as it's too simple, so insert it via
# shebang.
data = (TOPDIR / "repo").read_text(encoding="utf-8")
tempbin = tempdir / "repo"
tempbin.write_text(f"#!{sys.executable}\n" + data, encoding="utf-8")
tempbin.chmod(0o755)
# Run all cmd in parallel, and wait for them to finish.
with multiprocessing.Pool() as pool:
pool.map(partial(worker, cwd=tempdir, check=True), cmdlist)
for tmp_path in MANDIR.glob('*.1.tmp'):
for tmp_path in MANDIR.glob("*.1.tmp"):
path = tmp_path.parent / tmp_path.stem
old_data = path.read_text() if path.exists() else ''
old_data = path.read_text() if path.exists() else ""
data = tmp_path.read_text()
tmp_path.unlink()
data = replace_regex(data)
# If the only thing that changed was the date, don't refresh. This avoids
# a lot of noise when only one file actually updates.
old_data = re.sub(r'^(\.TH REPO "1" ")([^"]+)', r'\1', old_data, flags=re.M)
new_data = re.sub(r'^(\.TH REPO "1" ")([^"]+)', r'\1', data, flags=re.M)
# If the only thing that changed was the date, don't refresh. This
# avoids a lot of noise when only one file actually updates.
old_data = re.sub(
r'^(\.TH REPO "1" ")([^"]+)', r"\1", old_data, flags=re.M
)
new_data = re.sub(r'^(\.TH REPO "1" ")([^"]+)', r"\1", data, flags=re.M)
if old_data != new_data:
path.write_text(data)
@ -109,10 +143,10 @@ def replace_regex(data):
Updated manpage text.
"""
regex = (
(r'(It was generated by help2man) [0-9.]+', r'\g<1>.'),
(r'^\033\[[0-9;]*m([^\033]*)\033\[m', r'\g<1>'),
(r'^\.IP\n(.*:)\n', r'.SS \g<1>\n'),
(r'^\.PP\nDescription', r'.SH DETAILS'),
(r"(It was generated by help2man) [0-9.]+", r"\g<1>."),
(r"^\033\[[0-9;]*m([^\033]*)\033\[m", r"\g<1>"),
(r"^\.IP\n(.*:)\n", r".SS \g<1>\n"),
(r"^\.PP\nDescription", r".SH DETAILS"),
)
for pattern, replacement in regex:
data = re.sub(pattern, replacement, data, flags=re.M)

View File

@ -20,54 +20,60 @@ import subprocess
import sys
assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
assert sys.version_info >= (3, 6), "This module requires Python 3.6+"
TOPDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
HOMEDIR = os.path.expanduser('~')
HOMEDIR = os.path.expanduser("~")
# These are the release keys we sign with.
KEYID_DSA = '8BB9AD793E8E6153AF0F9A4416530D5E920F5C65'
KEYID_RSA = 'A34A13BE8E76BFF46A0C022DA2E75A824AAB9624'
KEYID_ECC = 'E1F9040D7A3F6DAFAC897CD3D3B95DA243E48A39'
KEYID_DSA = "8BB9AD793E8E6153AF0F9A4416530D5E920F5C65"
KEYID_RSA = "A34A13BE8E76BFF46A0C022DA2E75A824AAB9624"
KEYID_ECC = "E1F9040D7A3F6DAFAC897CD3D3B95DA243E48A39"
def cmdstr(cmd):
"""Get a nicely quoted shell command."""
ret = []
for arg in cmd:
if not re.match(r'^[a-zA-Z0-9/_.=-]+$', arg):
if not re.match(r"^[a-zA-Z0-9/_.=-]+$", arg):
arg = f'"{arg}"'
ret.append(arg)
return ' '.join(ret)
return " ".join(ret)
def run(opts, cmd, check=True, **kwargs):
"""Helper around subprocess.run to include logging."""
print('+', cmdstr(cmd))
print("+", cmdstr(cmd))
if opts.dryrun:
cmd = ['true', '--'] + cmd
cmd = ["true", "--"] + cmd
try:
return subprocess.run(cmd, check=check, **kwargs)
except subprocess.CalledProcessError as e:
print(f'aborting: {e}', file=sys.stderr)
print(f"aborting: {e}", file=sys.stderr)
sys.exit(1)
def import_release_key(opts):
"""Import the public key of the official release repo signing key."""
# Extract the key from our repo launcher.
launcher = getattr(opts, 'launcher', os.path.join(TOPDIR, 'repo'))
launcher = getattr(opts, "launcher", os.path.join(TOPDIR, "repo"))
print(f'Importing keys from "{launcher}" launcher script')
with open(launcher, encoding='utf-8') as fp:
with open(launcher, encoding="utf-8") as fp:
data = fp.read()
keys = re.findall(
r'\n-----BEGIN PGP PUBLIC KEY BLOCK-----\n[^-]*'
r'\n-----END PGP PUBLIC KEY BLOCK-----\n', data, flags=re.M)
run(opts, ['gpg', '--import'], input='\n'.join(keys).encode('utf-8'))
r"\n-----BEGIN PGP PUBLIC KEY BLOCK-----\n[^-]*"
r"\n-----END PGP PUBLIC KEY BLOCK-----\n",
data,
flags=re.M,
)
run(opts, ["gpg", "--import"], input="\n".join(keys).encode("utf-8"))
print('Marking keys as fully trusted')
run(opts, ['gpg', '--import-ownertrust'],
input=f'{KEYID_DSA}:6:\n'.encode('utf-8'))
print("Marking keys as fully trusted")
run(
opts,
["gpg", "--import-ownertrust"],
input=f"{KEYID_DSA}:6:\n".encode("utf-8"),
)

View File

@ -29,15 +29,15 @@ from contextlib import ContextDecorator
import platform_utils
# Env var to implicitly turn on tracing.
REPO_TRACE = 'REPO_TRACE'
REPO_TRACE = "REPO_TRACE"
# Temporarily set tracing to always on unless user expicitly sets to 0.
_TRACE = os.environ.get(REPO_TRACE) != '0'
_TRACE = os.environ.get(REPO_TRACE) != "0"
_TRACE_TO_STDERR = False
_TRACE_FILE = None
_TRACE_FILE_NAME = 'TRACE_FILE'
_TRACE_FILE_NAME = "TRACE_FILE"
_MAX_SIZE = 70 # in MiB
_NEW_COMMAND_SEP = '+++++++++++++++NEW COMMAND+++++++++++++++++++'
_NEW_COMMAND_SEP = "+++++++++++++++NEW COMMAND+++++++++++++++++++"
def IsTraceToStderr():
@ -73,7 +73,7 @@ class Trace(ContextDecorator):
def _time(self):
"""Generate nanoseconds of time in a py3.6 safe way"""
return int(time.time() * 1e+9)
return int(time.time() * 1e9)
def __init__(self, fmt, *args, first_trace=False, quiet=True):
"""Initialize the object.
@ -93,15 +93,17 @@ class Trace(ContextDecorator):
if first_trace:
_ClearOldTraces()
self._trace_msg = f'{_NEW_COMMAND_SEP} {self._trace_msg}'
self._trace_msg = f"{_NEW_COMMAND_SEP} {self._trace_msg}"
def __enter__(self):
if not IsTrace():
return self
print_msg = f'PID: {os.getpid()} START: {self._time()} :{self._trace_msg}\n'
print_msg = (
f"PID: {os.getpid()} START: {self._time()} :{self._trace_msg}\n"
)
with open(_TRACE_FILE, 'a') as f:
with open(_TRACE_FILE, "a") as f:
print(print_msg, file=f)
if _TRACE_TO_STDERR:
@ -113,9 +115,11 @@ class Trace(ContextDecorator):
if not IsTrace():
return False
print_msg = f'PID: {os.getpid()} END: {self._time()} :{self._trace_msg}\n'
print_msg = (
f"PID: {os.getpid()} END: {self._time()} :{self._trace_msg}\n"
)
with open(_TRACE_FILE, 'a') as f:
with open(_TRACE_FILE, "a") as f:
print(print_msg, file=f)
if _TRACE_TO_STDERR:
@ -130,14 +134,14 @@ def _GetTraceFile(quiet):
repo_dir = os.path.dirname(os.path.dirname(__file__))
trace_file = os.path.join(repo_dir, _TRACE_FILE_NAME)
if not quiet:
print(f'Trace outputs in {trace_file}', file=sys.stderr)
print(f"Trace outputs in {trace_file}", file=sys.stderr)
return trace_file
def _ClearOldTraces():
"""Clear the oldest commands if trace file is too big."""
try:
with open(_TRACE_FILE, 'r', errors='ignore') as f:
with open(_TRACE_FILE, "r", errors="ignore") as f:
if os.path.getsize(f.name) / (1024 * 1024) <= _MAX_SIZE:
return
trace_lines = f.readlines()
@ -146,21 +150,21 @@ def _ClearOldTraces():
while sum(len(x) for x in trace_lines) / (1024 * 1024) > _MAX_SIZE:
for i, line in enumerate(trace_lines):
if 'END:' in line and _NEW_COMMAND_SEP in line:
if "END:" in line and _NEW_COMMAND_SEP in line:
trace_lines = trace_lines[i + 1 :]
break
else:
# The last chunk is bigger than _MAX_SIZE, so just throw everything away.
# The last chunk is bigger than _MAX_SIZE, so just throw everything
# away.
trace_lines = []
while trace_lines and trace_lines[-1] == '\n':
while trace_lines and trace_lines[-1] == "\n":
trace_lines = trace_lines[:-1]
# Write to a temporary file with a unique name in the same filesystem
# before replacing the original trace file.
temp_dir, temp_prefix = os.path.split(_TRACE_FILE)
with tempfile.NamedTemporaryFile('w',
dir=temp_dir,
prefix=temp_prefix,
delete=False) as f:
with tempfile.NamedTemporaryFile(
"w", dir=temp_dir, prefix=temp_prefix, delete=False
) as f:
f.writelines(trace_lines)
platform_utils.rename(f.name, _TRACE_FILE)

View File

@ -13,10 +13,28 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Wrapper to run pytest with the right settings."""
"""Wrapper to run black and pytest with the right settings."""
import os
import subprocess
import sys
import pytest
if __name__ == '__main__':
sys.exit(pytest.main(sys.argv[1:]))
def run_black():
"""Returns the exit code of running `black --check`."""
dirpath = os.path.dirname(os.path.realpath(__file__))
return subprocess.run(
[sys.executable, "-m", "black", "--check", dirpath], check=False
).returncode
def main(argv):
"""The main entry."""
black_ret = 0 if argv else run_black()
pytest_ret = pytest.main(argv)
return 0 if not black_ret and not pytest_ret else 1
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))

View File

@ -26,8 +26,8 @@ wheel: <
# Required by pytest==6.2.2
wheel: <
name: "infra/python/wheels/packaging-py2_py3"
version: "version:16.8"
name: "infra/python/wheels/packaging-py3"
version: "version:23.0"
>
# Required by pytest==6.2.2
@ -59,3 +59,44 @@ wheel: <
name: "infra/python/wheels/six-py2_py3"
version: "version:1.16.0"
>
wheel: <
name: "infra/python/wheels/black-py3"
version: "version:23.1.0"
>
# Required by black==23.1.0
wheel: <
name: "infra/python/wheels/mypy-extensions-py3"
version: "version:0.4.3"
>
# Required by black==23.1.0
wheel: <
name: "infra/python/wheels/tomli-py3"
version: "version:2.0.1"
>
# Required by black==23.1.0
wheel: <
name: "infra/python/wheels/platformdirs-py3"
version: "version:2.5.2"
>
# Required by black==23.1.0
wheel: <
name: "infra/python/wheels/pathspec-py3"
version: "version:0.9.0"
>
# Required by black==23.1.0
wheel: <
name: "infra/python/wheels/typing-extensions-py3"
version: "version:4.3.0"
>
# Required by black==23.1.0
wheel: <
name: "infra/python/wheels/click-py3"
version: "version:8.0.3"
>

View File

@ -23,39 +23,39 @@ TOPDIR = os.path.dirname(os.path.abspath(__file__))
# Rip out the first intro paragraph.
with open(os.path.join(TOPDIR, 'README.md')) as fp:
with open(os.path.join(TOPDIR, "README.md")) as fp:
lines = fp.read().splitlines()[2:]
end = lines.index('')
long_description = ' '.join(lines[0:end])
end = lines.index("")
long_description = " ".join(lines[0:end])
# https://packaging.python.org/tutorials/packaging-projects/
setuptools.setup(
name='repo',
version='2',
maintainer='Various',
maintainer_email='repo-discuss@googlegroups.com',
description='Repo helps manage many Git repositories',
name="repo",
version="2",
maintainer="Various",
maintainer_email="repo-discuss@googlegroups.com",
description="Repo helps manage many Git repositories",
long_description=long_description,
long_description_content_type='text/plain',
url='https://gerrit.googlesource.com/git-repo/',
long_description_content_type="text/plain",
url="https://gerrit.googlesource.com/git-repo/",
project_urls={
'Bug Tracker': 'https://bugs.chromium.org/p/gerrit/issues/list?q=component:Applications%3Erepo',
"Bug Tracker": "https://bugs.chromium.org/p/gerrit/issues/list?q=component:Applications%3Erepo", # noqa: E501
},
# https://pypi.org/classifiers/
classifiers=[
'Development Status :: 6 - Mature',
'Environment :: Console',
'Intended Audience :: Developers',
'License :: OSI Approved :: Apache Software License',
'Natural Language :: English',
'Operating System :: MacOS :: MacOS X',
'Operating System :: Microsoft :: Windows :: Windows 10',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3 :: Only',
'Topic :: Software Development :: Version Control :: Git',
"Development Status :: 6 - Mature",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Natural Language :: English",
"Operating System :: MacOS :: MacOS X",
"Operating System :: Microsoft :: Windows :: Windows 10",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Software Development :: Version Control :: Git",
],
python_requires='>=3.6',
packages=['subcmds'],
python_requires=">=3.6",
packages=["subcmds"],
)

122
ssh.py
View File

@ -28,21 +28,23 @@ import platform_utils
from repo_trace import Trace
PROXY_PATH = os.path.join(os.path.dirname(__file__), 'git_ssh')
PROXY_PATH = os.path.join(os.path.dirname(__file__), "git_ssh")
def _run_ssh_version():
"""run ssh -V to display the version number"""
return subprocess.check_output(['ssh', '-V'], stderr=subprocess.STDOUT).decode()
return subprocess.check_output(
["ssh", "-V"], stderr=subprocess.STDOUT
).decode()
def _parse_ssh_version(ver_str=None):
"""parse a ssh version string into a tuple"""
if ver_str is None:
ver_str = _run_ssh_version()
m = re.match(r'^OpenSSH_([0-9.]+)(p[0-9]+)?\s', ver_str)
m = re.match(r"^OpenSSH_([0-9.]+)(p[0-9]+)?\s", ver_str)
if m:
return tuple(int(x) for x in m.group(1).split('.'))
return tuple(int(x) for x in m.group(1).split("."))
else:
return ()
@ -53,15 +55,15 @@ def version():
try:
return _parse_ssh_version()
except FileNotFoundError:
print('fatal: ssh not installed', file=sys.stderr)
print("fatal: ssh not installed", file=sys.stderr)
sys.exit(1)
except subprocess.CalledProcessError:
print('fatal: unable to detect ssh version', file=sys.stderr)
print("fatal: unable to detect ssh version", file=sys.stderr)
sys.exit(1)
URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):')
URI_ALL = re.compile(r'^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/')
URI_SCP = re.compile(r"^([^@:]*@?[^:/]{1,}):")
URI_ALL = re.compile(r"^([a-z][a-z+-]*)://([^@/]*@?[^/]*)/")
class ProxyManager:
@ -70,8 +72,8 @@ class ProxyManager:
This will take care of sharing state between multiprocessing children, and
make sure that if we crash, we don't leak any of the ssh sessions.
The code should work with a single-process scenario too, and not add too much
overhead due to the manager.
The code should work with a single-process scenario too, and not add too
much overhead due to the manager.
"""
# Path to the ssh program to run which will pass our master settings along.
@ -81,16 +83,17 @@ class ProxyManager:
def __init__(self, manager):
# Protect access to the list of active masters.
self._lock = multiprocessing.Lock()
# List of active masters (pid). These will be spawned on demand, and we are
# responsible for shutting them all down at the end.
# List of active masters (pid). These will be spawned on demand, and we
# are responsible for shutting them all down at the end.
self._masters = manager.list()
# Set of active masters indexed by "host:port" information.
# The value isn't used, but multiprocessing doesn't provide a set class.
self._master_keys = manager.dict()
# Whether ssh masters are known to be broken, so we give up entirely.
self._master_broken = manager.Value('b', False)
self._master_broken = manager.Value("b", False)
# List of active ssh sesssions. Clients will be added & removed as
# connections finish, so this list is just for safety & cleanup if we crash.
# connections finish, so this list is just for safety & cleanup if we
# crash.
self._clients = manager.list()
# Path to directory for holding master sockets.
self._sock_path = None
@ -132,7 +135,7 @@ class ProxyManager:
while True:
try:
procs.pop(0)
except:
except: # noqa: E722
break
def close(self):
@ -155,64 +158,71 @@ class ProxyManager:
If one doesn't exist already, we'll create it.
We won't grab any locks, so the caller has to do that. This helps keep the
business logic of actually creating the master separate from grabbing locks.
We won't grab any locks, so the caller has to do that. This helps keep
the business logic of actually creating the master separate from
grabbing locks.
"""
# Check to see whether we already think that the master is running; if we
# think it's already running, return right away.
# Check to see whether we already think that the master is running; if
# we think it's already running, return right away.
if port is not None:
key = '%s:%s' % (host, port)
key = "%s:%s" % (host, port)
else:
key = host
if key in self._master_keys:
return True
if self._master_broken.value or 'GIT_SSH' in os.environ:
if self._master_broken.value or "GIT_SSH" in os.environ:
# Failed earlier, so don't retry.
return False
# We will make two calls to ssh; this is the common part of both calls.
command_base = ['ssh', '-o', 'ControlPath %s' % self.sock(), host]
command_base = ["ssh", "-o", "ControlPath %s" % self.sock(), host]
if port is not None:
command_base[1:1] = ['-p', str(port)]
command_base[1:1] = ["-p", str(port)]
# Since the key wasn't in _master_keys, we think that master isn't running.
# ...but before actually starting a master, we'll double-check. This can
# be important because we can't tell that that 'git@myhost.com' is the same
# as 'myhost.com' where "User git" is setup in the user's ~/.ssh/config file.
check_command = command_base + ['-O', 'check']
with Trace('Call to ssh (check call): %s', ' '.join(check_command)):
# Since the key wasn't in _master_keys, we think that master isn't
# running... but before actually starting a master, we'll double-check.
# This can be important because we can't tell that that 'git@myhost.com'
# is the same as 'myhost.com' where "User git" is setup in the user's
# ~/.ssh/config file.
check_command = command_base + ["-O", "check"]
with Trace("Call to ssh (check call): %s", " ".join(check_command)):
try:
check_process = subprocess.Popen(check_command,
check_process = subprocess.Popen(
check_command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stderr=subprocess.PIPE,
)
check_process.communicate() # read output, but ignore it...
isnt_running = check_process.wait()
if not isnt_running:
# Our double-check found that the master _was_ infact running. Add to
# the list of keys.
# Our double-check found that the master _was_ infact
# running. Add to the list of keys.
self._master_keys[key] = True
return True
except Exception:
# Ignore excpetions. We we will fall back to the normal command and
# print to the log there.
# Ignore excpetions. We we will fall back to the normal command
# and print to the log there.
pass
command = command_base[:1] + ['-M', '-N'] + command_base[1:]
command = command_base[:1] + ["-M", "-N"] + command_base[1:]
p = None
try:
with Trace('Call to ssh: %s', ' '.join(command)):
with Trace("Call to ssh: %s", " ".join(command)):
p = subprocess.Popen(command)
except Exception as e:
self._master_broken.value = True
print('\nwarn: cannot enable ssh control master for %s:%s\n%s'
% (host, port, str(e)), file=sys.stderr)
print(
"\nwarn: cannot enable ssh control master for %s:%s\n%s"
% (host, port, str(e)),
file=sys.stderr,
)
return False
time.sleep(1)
ssh_died = (p.poll() is not None)
ssh_died = p.poll() is not None
if ssh_died:
return False
@ -227,29 +237,29 @@ class ProxyManager:
This will obtain any necessary locks to avoid inter-process races.
"""
# Bail before grabbing the lock if we already know that we aren't going to
# try creating new masters below.
if sys.platform in ('win32', 'cygwin'):
# Bail before grabbing the lock if we already know that we aren't going
# to try creating new masters below.
if sys.platform in ("win32", "cygwin"):
return False
# Acquire the lock. This is needed to prevent opening multiple masters for
# the same host when we're running "repo sync -jN" (for N > 1) _and_ the
# manifest <remote fetch="ssh://xyz"> specifies a different host from the
# one that was passed to repo init.
# Acquire the lock. This is needed to prevent opening multiple masters
# for the same host when we're running "repo sync -jN" (for N > 1) _and_
# the manifest <remote fetch="ssh://xyz"> specifies a different host
# from the one that was passed to repo init.
with self._lock:
return self._open_unlocked(host, port)
def preconnect(self, url):
"""If |uri| will create a ssh connection, setup the ssh master for it."""
"""If |uri| will create a ssh connection, setup the ssh master for it.""" # noqa: E501
m = URI_ALL.match(url)
if m:
scheme = m.group(1)
host = m.group(2)
if ':' in host:
host, port = host.split(':')
if ":" in host:
host, port = host.split(":")
else:
port = None
if scheme in ('ssh', 'git+ssh', 'ssh+git'):
if scheme in ("ssh", "git+ssh", "ssh+git"):
return self._open(host, port)
return False
@ -268,14 +278,14 @@ class ProxyManager:
if self._sock_path is None:
if not create:
return None
tmp_dir = '/tmp'
tmp_dir = "/tmp"
if not os.path.exists(tmp_dir):
tmp_dir = tempfile.gettempdir()
if version() < (6, 7):
tokens = '%r@%h:%p'
tokens = "%r@%h:%p"
else:
tokens = '%C' # hash of %l%h%p%r
tokens = "%C" # hash of %l%h%p%r
self._sock_path = os.path.join(
tempfile.mkdtemp('', 'ssh-', tmp_dir),
'master-' + tokens)
tempfile.mkdtemp("", "ssh-", tmp_dir), "master-" + tokens
)
return self._sock_path

View File

@ -19,31 +19,29 @@ all_commands = {}
my_dir = os.path.dirname(__file__)
for py in os.listdir(my_dir):
if py == '__init__.py':
if py == "__init__.py":
continue
if py.endswith('.py'):
if py.endswith(".py"):
name = py[:-3]
clsn = name.capitalize()
while clsn.find('_') > 0:
h = clsn.index('_')
while clsn.find("_") > 0:
h = clsn.index("_")
clsn = clsn[0:h] + clsn[h + 1 :].capitalize()
mod = __import__(__name__,
globals(),
locals(),
['%s' % name])
mod = __import__(__name__, globals(), locals(), ["%s" % name])
mod = getattr(mod, name)
try:
cmd = getattr(mod, clsn)
except AttributeError:
raise SyntaxError('%s/%s does not define class %s' % (
__name__, py, clsn))
raise SyntaxError(
"%s/%s does not define class %s" % (__name__, py, clsn)
)
name = name.replace('_', '-')
name = name.replace("_", "-")
cmd.NAME = name
all_commands[name] = cmd
# Add 'branch' as an alias for 'branches'.
all_commands['branch'] = all_commands['branches']
all_commands["branch"] = all_commands["branches"]

View File

@ -36,9 +36,12 @@ It is equivalent to "git branch -D <branchname>".
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
def _Options(self, p):
p.add_option('--all',
dest='all', action='store_true',
help='delete all branches in all projects')
p.add_option(
"--all",
dest="all",
action="store_true",
help="delete all branches in all projects",
)
def ValidateOptions(self, opt, args):
if not opt.all and not args:
@ -46,7 +49,7 @@ It is equivalent to "git branch -D <branchname>".
if not opt.all:
nb = args[0]
if not git.check_ref_format('heads/%s' % nb):
if not git.check_ref_format("heads/%s" % nb):
self.OptionParser.error("'%s' is not a valid branch name" % nb)
else:
args.insert(0, "'All local branches'")
@ -69,11 +72,13 @@ It is equivalent to "git branch -D <branchname>".
nb = args[0]
err = defaultdict(list)
success = defaultdict(list)
all_projects = self.GetProjects(args[1:], all_manifests=not opt.this_manifest_only)
all_projects = self.GetProjects(
args[1:], all_manifests=not opt.this_manifest_only
)
_RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
def _ProcessResults(_pool, pm, states):
for (results, project) in states:
for results, project in states:
for branch, status in results.items():
if status:
success[branch].append(project)
@ -86,30 +91,46 @@ It is equivalent to "git branch -D <branchname>".
functools.partial(self._ExecuteOne, opt.all, nb),
all_projects,
callback=_ProcessResults,
output=Progress('Abandon %s' % (nb,), len(all_projects), quiet=opt.quiet))
output=Progress(
"Abandon %s" % (nb,), len(all_projects), quiet=opt.quiet
),
)
width = max(itertools.chain(
[25], (len(x) for x in itertools.chain(success, err))))
width = max(
itertools.chain(
[25], (len(x) for x in itertools.chain(success, err))
)
)
if err:
for br in err.keys():
err_msg = "error: cannot abandon %s" % br
print(err_msg, file=sys.stderr)
for proj in err[br]:
print(' ' * len(err_msg) + " | %s" % _RelPath(proj), file=sys.stderr)
print(
" " * len(err_msg) + " | %s" % _RelPath(proj),
file=sys.stderr,
)
sys.exit(1)
elif not success:
print('error: no project has local branch(es) : %s' % nb,
file=sys.stderr)
print(
"error: no project has local branch(es) : %s" % nb,
file=sys.stderr,
)
sys.exit(1)
else:
# Everything below here is displaying status.
if opt.quiet:
return
print('Abandoned branches:')
print("Abandoned branches:")
for br in success.keys():
if len(all_projects) > 1 and len(all_projects) == len(success[br]):
if len(all_projects) > 1 and len(all_projects) == len(
success[br]
):
result = "all project"
else:
result = "%s" % (
('\n' + ' ' * width + '| ').join(_RelPath(p) for p in success[br]))
print("%s%s| %s\n" % (br, ' ' * (width - len(br)), result))
("\n" + " " * width + "| ").join(
_RelPath(p) for p in success[br]
)
)
print("%s%s| %s\n" % (br, " " * (width - len(br)), result))

View File

@ -21,10 +21,10 @@ from command import Command, DEFAULT_LOCAL_JOBS
class BranchColoring(Coloring):
def __init__(self, config):
Coloring.__init__(self, config, 'branch')
self.current = self.printer('current', fg='green')
self.local = self.printer('local')
self.notinproject = self.printer('notinproject', fg='red')
Coloring.__init__(self, config, "branch")
self.current = self.printer("current", fg="green")
self.local = self.printer("local")
self.notinproject = self.printer("notinproject", fg="red")
class BranchInfo(object):
@ -98,7 +98,9 @@ is shown, then the branch appears in all projects.
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
def Execute(self, opt, args):
projects = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
projects = self.GetProjects(
args, all_manifests=not opt.this_manifest_only
)
out = BranchColoring(self.manifest.manifestProject.config)
all_branches = {}
project_cnt = len(projects)
@ -113,12 +115,13 @@ is shown, then the branch appears in all projects.
opt.jobs,
expand_project_to_branches,
projects,
callback=_ProcessResults)
callback=_ProcessResults,
)
names = sorted(all_branches)
if not names:
print(' (no branches)', file=sys.stderr)
print(" (no branches)", file=sys.stderr)
return
width = 25
@ -131,21 +134,21 @@ is shown, then the branch appears in all projects.
in_cnt = len(i.projects)
if i.IsCurrent:
current = '*'
current = "*"
hdr = out.current
else:
current = ' '
current = " "
hdr = out.local
if i.IsPublishedEqual:
published = 'P'
published = "P"
elif i.IsPublished:
published = 'p'
published = "p"
else:
published = ' '
published = " "
hdr('%c%c %-*s' % (current, published, width, name))
out.write(' |')
hdr("%c%c %-*s" % (current, published, width, name))
out.write(" |")
_RelPath = lambda p: p.RelPath(local=opt.this_manifest_only)
if in_cnt < project_cnt:
@ -153,7 +156,7 @@ is shown, then the branch appears in all projects.
paths = []
non_cur_paths = []
if i.IsSplitCurrent or (in_cnt <= project_cnt - in_cnt):
in_type = 'in'
in_type = "in"
for b in i.projects:
relpath = _RelPath(b.project)
if not i.IsSplitCurrent or b.current:
@ -162,7 +165,7 @@ is shown, then the branch appears in all projects.
non_cur_paths.append(relpath)
else:
fmt = out.notinproject
in_type = 'not in'
in_type = "not in"
have = set()
for b in i.projects:
have.add(_RelPath(b.project))
@ -170,22 +173,22 @@ is shown, then the branch appears in all projects.
if _RelPath(p) not in have:
paths.append(_RelPath(p))
s = ' %s %s' % (in_type, ', '.join(paths))
s = " %s %s" % (in_type, ", ".join(paths))
if not i.IsSplitCurrent and (width + 7 + len(s) < 80):
fmt = out.current if i.IsCurrent else fmt
fmt(s)
else:
fmt(' %s:' % in_type)
fmt(" %s:" % in_type)
fmt = out.current if i.IsCurrent else out.write
for p in paths:
out.nl()
fmt(width * ' ' + ' %s' % p)
fmt(width * " " + " %s" % p)
fmt = out.write
for p in non_cur_paths:
out.nl()
fmt(width * ' ' + ' %s' % p)
fmt(width * " " + " %s" % p)
else:
out.write(' in all projects')
out.write(" in all projects")
out.nl()

View File

@ -47,7 +47,9 @@ The command is equivalent to:
nb = args[0]
err = []
success = []
all_projects = self.GetProjects(args[1:], all_manifests=not opt.this_manifest_only)
all_projects = self.GetProjects(
args[1:], all_manifests=not opt.this_manifest_only
)
def _ProcessResults(_pool, pm, results):
for status, project in results:
@ -63,13 +65,18 @@ The command is equivalent to:
functools.partial(self._ExecuteOne, nb),
all_projects,
callback=_ProcessResults,
output=Progress('Checkout %s' % (nb,), len(all_projects), quiet=opt.quiet))
output=Progress(
"Checkout %s" % (nb,), len(all_projects), quiet=opt.quiet
),
)
if err:
for p in err:
print("error: %s/: cannot checkout %s" % (p.relpath, nb),
file=sys.stderr)
print(
"error: %s/: cannot checkout %s" % (p.relpath, nb),
file=sys.stderr,
)
sys.exit(1)
elif not success:
print('error: no project has branch %s' % nb, file=sys.stderr)
print("error: no project has branch %s" % nb, file=sys.stderr)
sys.exit(1)

View File

@ -17,7 +17,7 @@ import sys
from command import Command
from git_command import GitCommand
CHANGE_ID_RE = re.compile(r'^\s*Change-Id: I([0-9a-f]{40})\s*$')
CHANGE_ID_RE = re.compile(r"^\s*Change-Id: I([0-9a-f]{40})\s*$")
class CherryPick(Command):
@ -39,25 +39,31 @@ change id will be added.
def Execute(self, opt, args):
reference = args[0]
p = GitCommand(None,
['rev-parse', '--verify', reference],
p = GitCommand(
None,
["rev-parse", "--verify", reference],
capture_stdout=True,
capture_stderr=True)
capture_stderr=True,
)
if p.Wait() != 0:
print(p.stderr, file=sys.stderr)
sys.exit(1)
sha1 = p.stdout.strip()
p = GitCommand(None, ['cat-file', 'commit', sha1], capture_stdout=True)
p = GitCommand(None, ["cat-file", "commit", sha1], capture_stdout=True)
if p.Wait() != 0:
print("error: Failed to retrieve old commit message", file=sys.stderr)
print(
"error: Failed to retrieve old commit message", file=sys.stderr
)
sys.exit(1)
old_msg = self._StripHeader(p.stdout)
p = GitCommand(None,
['cherry-pick', sha1],
p = GitCommand(
None,
["cherry-pick", sha1],
capture_stdout=True,
capture_stderr=True)
capture_stderr=True,
)
status = p.Wait()
if p.stdout:
@ -70,17 +76,22 @@ change id will be added.
# commit message.
new_msg = self._Reformat(old_msg, sha1)
p = GitCommand(None, ['commit', '--amend', '-F', '-'],
p = GitCommand(
None,
["commit", "--amend", "-F", "-"],
input=new_msg,
capture_stdout=True,
capture_stderr=True)
capture_stderr=True,
)
if p.Wait() != 0:
print("error: Failed to update commit message", file=sys.stderr)
sys.exit(1)
else:
print('NOTE: When committing (please see above) and editing the commit '
'message, please remove the old Change-Id-line and add:')
print(
"NOTE: When committing (please see above) and editing the "
"commit message, please remove the old Change-Id-line and add:"
)
print(self._GetReference(sha1), file=sys.stderr)
print(file=sys.stderr)
@ -101,7 +112,7 @@ change id will be added.
if not self._IsChangeId(line):
new_msg.append(line)
# Add a blank line between the message and the change id/reference
# Add a blank line between the message and the change id/reference.
try:
if new_msg[-1].strip() != "":
new_msg.append("")

View File

@ -31,9 +31,13 @@ to the Unix 'patch' command.
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
def _Options(self, p):
p.add_option('-u', '--absolute',
dest='absolute', action='store_true',
help='paths are relative to the repository root')
p.add_option(
"-u",
"--absolute",
dest="absolute",
action="store_true",
help="paths are relative to the repository root",
)
def _ExecuteOne(self, absolute, local, project):
"""Obtains the diff for a specific project.
@ -41,8 +45,8 @@ to the Unix 'patch' command.
Args:
absolute: Paths are relative to the root.
local: a boolean, if True, the path is relative to the local
(sub)manifest. If false, the path is relative to the
outermost manifest.
(sub)manifest. If false, the path is relative to the outermost
manifest.
project: Project to get status of.
Returns:
@ -53,20 +57,25 @@ to the Unix 'patch' command.
return (ret, buf.getvalue())
def Execute(self, opt, args):
all_projects = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
all_projects = self.GetProjects(
args, all_manifests=not opt.this_manifest_only
)
def _ProcessResults(_pool, _output, results):
ret = 0
for (state, output) in results:
for state, output in results:
if output:
print(output, end='')
print(output, end="")
if not state:
ret = 1
return ret
return self.ExecuteInParallel(
opt.jobs,
functools.partial(self._ExecuteOne, opt.absolute, opt.this_manifest_only),
functools.partial(
self._ExecuteOne, opt.absolute, opt.this_manifest_only
),
all_projects,
callback=_ProcessResults,
ordered=True)
ordered=True,
)

View File

@ -66,145 +66,188 @@ synced and their revisions won't be found.
"""
def _Options(self, p):
p.add_option('--raw',
dest='raw', action='store_true',
help='display raw diff')
p.add_option('--no-color',
dest='color', action='store_false', default=True,
help='does not display the diff in color')
p.add_option('--pretty-format',
dest='pretty_format', action='store',
metavar='<FORMAT>',
help='print the log using a custom git pretty format string')
p.add_option(
"--raw", dest="raw", action="store_true", help="display raw diff"
)
p.add_option(
"--no-color",
dest="color",
action="store_false",
default=True,
help="does not display the diff in color",
)
p.add_option(
"--pretty-format",
dest="pretty_format",
action="store",
metavar="<FORMAT>",
help="print the log using a custom git pretty format string",
)
def _printRawDiff(self, diff, pretty_format=None, local=False):
_RelPath = lambda p: p.RelPath(local=local)
for project in diff['added']:
self.printText("A %s %s" % (_RelPath(project), project.revisionExpr))
for project in diff["added"]:
self.printText(
"A %s %s" % (_RelPath(project), project.revisionExpr)
)
self.out.nl()
for project in diff['removed']:
self.printText("R %s %s" % (_RelPath(project), project.revisionExpr))
for project in diff["removed"]:
self.printText(
"R %s %s" % (_RelPath(project), project.revisionExpr)
)
self.out.nl()
for project, otherProject in diff['changed']:
self.printText("C %s %s %s" % (_RelPath(project), project.revisionExpr,
otherProject.revisionExpr))
for project, otherProject in diff["changed"]:
self.printText(
"C %s %s %s"
% (
_RelPath(project),
project.revisionExpr,
otherProject.revisionExpr,
)
)
self.out.nl()
self._printLogs(project, otherProject, raw=True, color=False, pretty_format=pretty_format)
self._printLogs(
project,
otherProject,
raw=True,
color=False,
pretty_format=pretty_format,
)
for project, otherProject in diff['unreachable']:
self.printText("U %s %s %s" % (_RelPath(project), project.revisionExpr,
otherProject.revisionExpr))
for project, otherProject in diff["unreachable"]:
self.printText(
"U %s %s %s"
% (
_RelPath(project),
project.revisionExpr,
otherProject.revisionExpr,
)
)
self.out.nl()
def _printDiff(self, diff, color=True, pretty_format=None, local=False):
_RelPath = lambda p: p.RelPath(local=local)
if diff['added']:
if diff["added"]:
self.out.nl()
self.printText('added projects : \n')
self.printText("added projects : \n")
self.out.nl()
for project in diff['added']:
self.printProject('\t%s' % (_RelPath(project)))
self.printText(' at revision ')
for project in diff["added"]:
self.printProject("\t%s" % (_RelPath(project)))
self.printText(" at revision ")
self.printRevision(project.revisionExpr)
self.out.nl()
if diff['removed']:
if diff["removed"]:
self.out.nl()
self.printText('removed projects : \n')
self.printText("removed projects : \n")
self.out.nl()
for project in diff['removed']:
self.printProject('\t%s' % (_RelPath(project)))
self.printText(' at revision ')
for project in diff["removed"]:
self.printProject("\t%s" % (_RelPath(project)))
self.printText(" at revision ")
self.printRevision(project.revisionExpr)
self.out.nl()
if diff['missing']:
if diff["missing"]:
self.out.nl()
self.printText('missing projects : \n')
self.printText("missing projects : \n")
self.out.nl()
for project in diff['missing']:
self.printProject('\t%s' % (_RelPath(project)))
self.printText(' at revision ')
for project in diff["missing"]:
self.printProject("\t%s" % (_RelPath(project)))
self.printText(" at revision ")
self.printRevision(project.revisionExpr)
self.out.nl()
if diff['changed']:
if diff["changed"]:
self.out.nl()
self.printText('changed projects : \n')
self.printText("changed projects : \n")
self.out.nl()
for project, otherProject in diff['changed']:
self.printProject('\t%s' % (_RelPath(project)))
self.printText(' changed from ')
for project, otherProject in diff["changed"]:
self.printProject("\t%s" % (_RelPath(project)))
self.printText(" changed from ")
self.printRevision(project.revisionExpr)
self.printText(' to ')
self.printText(" to ")
self.printRevision(otherProject.revisionExpr)
self.out.nl()
self._printLogs(project, otherProject, raw=False, color=color,
pretty_format=pretty_format)
self._printLogs(
project,
otherProject,
raw=False,
color=color,
pretty_format=pretty_format,
)
self.out.nl()
if diff['unreachable']:
if diff["unreachable"]:
self.out.nl()
self.printText('projects with unreachable revisions : \n')
self.printText("projects with unreachable revisions : \n")
self.out.nl()
for project, otherProject in diff['unreachable']:
self.printProject('\t%s ' % (_RelPath(project)))
for project, otherProject in diff["unreachable"]:
self.printProject("\t%s " % (_RelPath(project)))
self.printRevision(project.revisionExpr)
self.printText(' or ')
self.printText(" or ")
self.printRevision(otherProject.revisionExpr)
self.printText(' not found')
self.printText(" not found")
self.out.nl()
def _printLogs(self, project, otherProject, raw=False, color=True,
pretty_format=None):
logs = project.getAddedAndRemovedLogs(otherProject,
def _printLogs(
self, project, otherProject, raw=False, color=True, pretty_format=None
):
logs = project.getAddedAndRemovedLogs(
otherProject,
oneline=(pretty_format is None),
color=color,
pretty_format=pretty_format)
if logs['removed']:
removedLogs = logs['removed'].split('\n')
pretty_format=pretty_format,
)
if logs["removed"]:
removedLogs = logs["removed"].split("\n")
for log in removedLogs:
if log.strip():
if raw:
self.printText(' R ' + log)
self.printText(" R " + log)
self.out.nl()
else:
self.printRemoved('\t\t[-] ')
self.printRemoved("\t\t[-] ")
self.printText(log)
self.out.nl()
if logs['added']:
addedLogs = logs['added'].split('\n')
if logs["added"]:
addedLogs = logs["added"].split("\n")
for log in addedLogs:
if log.strip():
if raw:
self.printText(' A ' + log)
self.printText(" A " + log)
self.out.nl()
else:
self.printAdded('\t\t[+] ')
self.printAdded("\t\t[+] ")
self.printText(log)
self.out.nl()
def ValidateOptions(self, opt, args):
if not args or len(args) > 2:
self.OptionParser.error('missing manifests to diff')
self.OptionParser.error("missing manifests to diff")
if opt.this_manifest_only is False:
raise self.OptionParser.error(
'`diffmanifest` only supports the current tree')
"`diffmanifest` only supports the current tree"
)
def Execute(self, opt, args):
self.out = _Coloring(self.client.globalConfig)
self.printText = self.out.nofmt_printer('text')
self.printText = self.out.nofmt_printer("text")
if opt.color:
self.printProject = self.out.nofmt_printer('project', attr='bold')
self.printAdded = self.out.nofmt_printer('green', fg='green', attr='bold')
self.printRemoved = self.out.nofmt_printer('red', fg='red', attr='bold')
self.printRevision = self.out.nofmt_printer('revision', fg='yellow')
self.printProject = self.out.nofmt_printer("project", attr="bold")
self.printAdded = self.out.nofmt_printer(
"green", fg="green", attr="bold"
)
self.printRemoved = self.out.nofmt_printer(
"red", fg="red", attr="bold"
)
self.printRevision = self.out.nofmt_printer("revision", fg="yellow")
else:
self.printProject = self.printAdded = self.printRemoved = self.printRevision = self.printText
self.printProject = (
self.printAdded
) = self.printRemoved = self.printRevision = self.printText
manifest1 = RepoClient(self.repodir)
manifest1.Override(args[0], load_local_manifests=False)
@ -216,8 +259,15 @@ synced and their revisions won't be found.
diff = manifest1.projectsDiff(manifest2)
if opt.raw:
self._printRawDiff(diff, pretty_format=opt.pretty_format,
local=opt.this_manifest_only)
self._printRawDiff(
diff,
pretty_format=opt.pretty_format,
local=opt.this_manifest_only,
)
else:
self._printDiff(diff, color=opt.color, pretty_format=opt.pretty_format,
local=opt.this_manifest_only)
self._printDiff(
diff,
color=opt.color,
pretty_format=opt.pretty_format,
local=opt.this_manifest_only,
)

View File

@ -18,7 +18,7 @@ import sys
from command import Command
from error import GitError, NoSuchProjectError
CHANGE_RE = re.compile(r'^([1-9][0-9]*)(?:[/\.-]([1-9][0-9]*))?$')
CHANGE_RE = re.compile(r"^([1-9][0-9]*)(?:[/\.-]([1-9][0-9]*))?$")
class Download(Command):
@ -34,19 +34,34 @@ If no project is specified try to use current directory as a project.
"""
def _Options(self, p):
p.add_option('-b', '--branch',
help='create a new branch first')
p.add_option('-c', '--cherry-pick',
dest='cherrypick', action='store_true',
help="cherry-pick instead of checkout")
p.add_option('-x', '--record-origin', action='store_true',
help='pass -x when cherry-picking')
p.add_option('-r', '--revert',
dest='revert', action='store_true',
help="revert instead of checkout")
p.add_option('-f', '--ff-only',
dest='ffonly', action='store_true',
help="force fast-forward merge")
p.add_option("-b", "--branch", help="create a new branch first")
p.add_option(
"-c",
"--cherry-pick",
dest="cherrypick",
action="store_true",
help="cherry-pick instead of checkout",
)
p.add_option(
"-x",
"--record-origin",
action="store_true",
help="pass -x when cherry-picking",
)
p.add_option(
"-r",
"--revert",
dest="revert",
action="store_true",
help="revert instead of checkout",
)
p.add_option(
"-f",
"--ff-only",
dest="ffonly",
action="store_true",
help="force fast-forward merge",
)
def _ParseChangeIds(self, opt, args):
if not args:
@ -60,16 +75,16 @@ If no project is specified try to use current directory as a project.
if m:
if not project:
project = self.GetProjects(".")[0]
print('Defaulting to cwd project', project.name)
print("Defaulting to cwd project", project.name)
chg_id = int(m.group(1))
if m.group(2):
ps_id = int(m.group(2))
else:
ps_id = 1
refs = 'refs/changes/%2.2d/%d/' % (chg_id % 100, chg_id)
output = project._LsRemote(refs + '*')
refs = "refs/changes/%2.2d/%d/" % (chg_id % 100, chg_id)
output = project._LsRemote(refs + "*")
if output:
regex = refs + r'(\d+)'
regex = refs + r"(\d+)"
rcomp = re.compile(regex, re.I)
for line in output.splitlines():
match = rcomp.search(line)
@ -77,73 +92,99 @@ If no project is specified try to use current directory as a project.
ps_id = max(int(match.group(1)), ps_id)
to_get.append((project, chg_id, ps_id))
else:
projects = self.GetProjects([a], all_manifests=not opt.this_manifest_only)
projects = self.GetProjects(
[a], all_manifests=not opt.this_manifest_only
)
if len(projects) > 1:
# If the cwd is one of the projects, assume they want that.
try:
project = self.GetProjects('.')[0]
project = self.GetProjects(".")[0]
except NoSuchProjectError:
project = None
if project not in projects:
print('error: %s matches too many projects; please re-run inside '
'the project checkout.' % (a,), file=sys.stderr)
print(
"error: %s matches too many projects; please "
"re-run inside the project checkout." % (a,),
file=sys.stderr,
)
for project in projects:
print(' %s/ @ %s' % (project.RelPath(local=opt.this_manifest_only),
project.revisionExpr), file=sys.stderr)
print(
" %s/ @ %s"
% (
project.RelPath(
local=opt.this_manifest_only
),
project.revisionExpr,
),
file=sys.stderr,
)
sys.exit(1)
else:
project = projects[0]
print('Defaulting to cwd project', project.name)
print("Defaulting to cwd project", project.name)
return to_get
def ValidateOptions(self, opt, args):
if opt.record_origin:
if not opt.cherrypick:
self.OptionParser.error('-x only makes sense with --cherry-pick')
self.OptionParser.error(
"-x only makes sense with --cherry-pick"
)
if opt.ffonly:
self.OptionParser.error('-x and --ff are mutually exclusive options')
self.OptionParser.error(
"-x and --ff are mutually exclusive options"
)
def Execute(self, opt, args):
for project, change_id, ps_id in self._ParseChangeIds(opt, args):
dl = project.DownloadPatchSet(change_id, ps_id)
if not dl:
print('[%s] change %d/%d not found'
print(
"[%s] change %d/%d not found"
% (project.name, change_id, ps_id),
file=sys.stderr)
file=sys.stderr,
)
sys.exit(1)
if not opt.revert and not dl.commits:
print('[%s] change %d/%d has already been merged'
print(
"[%s] change %d/%d has already been merged"
% (project.name, change_id, ps_id),
file=sys.stderr)
file=sys.stderr,
)
continue
if len(dl.commits) > 1:
print('[%s] %d/%d depends on %d unmerged changes:'
print(
"[%s] %d/%d depends on %d unmerged changes:"
% (project.name, change_id, ps_id, len(dl.commits)),
file=sys.stderr)
file=sys.stderr,
)
for c in dl.commits:
print(' %s' % (c), file=sys.stderr)
print(" %s" % (c), file=sys.stderr)
if opt.cherrypick:
mode = 'cherry-pick'
mode = "cherry-pick"
elif opt.revert:
mode = 'revert'
mode = "revert"
elif opt.ffonly:
mode = 'fast-forward merge'
mode = "fast-forward merge"
else:
mode = 'checkout'
mode = "checkout"
# We'll combine the branch+checkout operation, but all the rest need a
# dedicated branch start.
if opt.branch and mode != 'checkout':
# We'll combine the branch+checkout operation, but all the rest need
# a dedicated branch start.
if opt.branch and mode != "checkout":
project.StartBranch(opt.branch)
try:
if opt.cherrypick:
project._CherryPick(dl.commit, ffonly=opt.ffonly,
record_origin=opt.record_origin)
project._CherryPick(
dl.commit,
ffonly=opt.ffonly,
record_origin=opt.record_origin,
)
elif opt.revert:
project._Revert(dl.commit)
elif opt.ffonly:
@ -155,6 +196,9 @@ If no project is specified try to use current directory as a project.
project._Checkout(dl.commit)
except GitError:
print('[%s] Could not complete the %s of %s'
% (project.name, mode, dl.commit), file=sys.stderr)
print(
"[%s] Could not complete the %s of %s"
% (project.name, mode, dl.commit),
file=sys.stderr,
)
sys.exit(1)

View File

@ -23,21 +23,26 @@ import sys
import subprocess
from color import Coloring
from command import DEFAULT_LOCAL_JOBS, Command, MirrorSafeCommand, WORKER_BATCH_SIZE
from command import (
DEFAULT_LOCAL_JOBS,
Command,
MirrorSafeCommand,
WORKER_BATCH_SIZE,
)
from error import ManifestInvalidRevisionError
_CAN_COLOR = [
'branch',
'diff',
'grep',
'log',
"branch",
"diff",
"grep",
"log",
]
class ForallColoring(Coloring):
def __init__(self, config):
Coloring.__init__(self, config, 'forall')
self.project = self.printer('project', attr='bold')
Coloring.__init__(self, config, "forall")
self.project = self.printer("project", attr="bold")
class Forall(Command, MirrorSafeCommand):
@ -134,35 +139,61 @@ without iterating through the remaining projects.
del parser.rargs[0]
def _Options(self, p):
p.add_option('-r', '--regex',
dest='regex', action='store_true',
help='execute the command only on projects matching regex or wildcard expression')
p.add_option('-i', '--inverse-regex',
dest='inverse_regex', action='store_true',
help='execute the command only on projects not matching regex or '
'wildcard expression')
p.add_option('-g', '--groups',
dest='groups',
help='execute the command only on projects matching the specified groups')
p.add_option('-c', '--command',
help='command (and arguments) to execute',
dest='command',
action='callback',
callback=self._cmd_option)
p.add_option('-e', '--abort-on-errors',
dest='abort_on_errors', action='store_true',
help='abort if a command exits unsuccessfully')
p.add_option('--ignore-missing', action='store_true',
help='silently skip & do not exit non-zero due missing '
'checkouts')
p.add_option(
"-r",
"--regex",
dest="regex",
action="store_true",
help="execute the command only on projects matching regex or "
"wildcard expression",
)
p.add_option(
"-i",
"--inverse-regex",
dest="inverse_regex",
action="store_true",
help="execute the command only on projects not matching regex or "
"wildcard expression",
)
p.add_option(
"-g",
"--groups",
dest="groups",
help="execute the command only on projects matching the specified "
"groups",
)
p.add_option(
"-c",
"--command",
help="command (and arguments) to execute",
dest="command",
action="callback",
callback=self._cmd_option,
)
p.add_option(
"-e",
"--abort-on-errors",
dest="abort_on_errors",
action="store_true",
help="abort if a command exits unsuccessfully",
)
p.add_option(
"--ignore-missing",
action="store_true",
help="silently skip & do not exit non-zero due missing "
"checkouts",
)
g = p.get_option_group('--quiet')
g.add_option('-p',
dest='project_header', action='store_true',
help='show project headers before output')
p.add_option('--interactive',
action='store_true',
help='force interactive usage')
g = p.get_option_group("--quiet")
g.add_option(
"-p",
dest="project_header",
action="store_true",
help="show project headers before output",
)
p.add_option(
"--interactive", action="store_true", help="force interactive usage"
)
def WantPager(self, opt):
return opt.project_header and opt.jobs == 1
@ -176,44 +207,45 @@ without iterating through the remaining projects.
all_trees = not opt.this_manifest_only
shell = True
if re.compile(r'^[a-z0-9A-Z_/\.-]+$').match(cmd[0]):
if re.compile(r"^[a-z0-9A-Z_/\.-]+$").match(cmd[0]):
shell = False
if shell:
cmd.append(cmd[0])
cmd.extend(opt.command[1:])
# Historically, forall operated interactively, and in serial. If the user
# has selected 1 job, then default to interacive mode.
# Historically, forall operated interactively, and in serial. If the
# user has selected 1 job, then default to interacive mode.
if opt.jobs == 1:
opt.interactive = True
if opt.project_header \
and not shell \
and cmd[0] == 'git':
if opt.project_header and not shell and cmd[0] == "git":
# If this is a direct git command that can enable colorized
# output and the user prefers coloring, add --color into the
# command line because we are going to wrap the command into
# a pipe and git won't know coloring should activate.
#
for cn in cmd[1:]:
if not cn.startswith('-'):
if not cn.startswith("-"):
break
else:
cn = None
if cn and cn in _CAN_COLOR:
class ColorCmd(Coloring):
def __init__(self, config, cmd):
Coloring.__init__(self, config, cmd)
if ColorCmd(self.manifest.manifestProject.config, cn).is_on:
cmd.insert(cmd.index(cn) + 1, '--color')
cmd.insert(cmd.index(cn) + 1, "--color")
mirror = self.manifest.IsMirror
rc = 0
smart_sync_manifest_name = "smart_sync_override.xml"
smart_sync_manifest_path = os.path.join(
self.manifest.manifestProject.worktree, smart_sync_manifest_name)
self.manifest.manifestProject.worktree, smart_sync_manifest_name
)
if os.path.isfile(smart_sync_manifest_path):
self.manifest.Override(smart_sync_manifest_path)
@ -221,43 +253,53 @@ without iterating through the remaining projects.
if opt.regex:
projects = self.FindProjects(args, all_manifests=all_trees)
elif opt.inverse_regex:
projects = self.FindProjects(args, inverse=True, all_manifests=all_trees)
projects = self.FindProjects(
args, inverse=True, all_manifests=all_trees
)
else:
projects = self.GetProjects(args, groups=opt.groups, all_manifests=all_trees)
projects = self.GetProjects(
args, groups=opt.groups, all_manifests=all_trees
)
os.environ['REPO_COUNT'] = str(len(projects))
os.environ["REPO_COUNT"] = str(len(projects))
try:
config = self.manifest.manifestProject.config
with multiprocessing.Pool(opt.jobs, InitWorker) as pool:
results_it = pool.imap(
functools.partial(DoWorkWrapper, mirror, opt, cmd, shell, config),
functools.partial(
DoWorkWrapper, mirror, opt, cmd, shell, config
),
enumerate(projects),
chunksize=WORKER_BATCH_SIZE)
chunksize=WORKER_BATCH_SIZE,
)
first = True
for (r, output) in results_it:
for r, output in results_it:
if output:
if first:
first = False
elif opt.project_header:
print()
# To simplify the DoWorkWrapper, take care of automatic newlines.
end = '\n'
if output[-1] == '\n':
end = ''
# To simplify the DoWorkWrapper, take care of automatic
# newlines.
end = "\n"
if output[-1] == "\n":
end = ""
print(output, end=end)
rc = rc or r
if r != 0 and opt.abort_on_errors:
raise Exception('Aborting due to previous error')
raise Exception("Aborting due to previous error")
except (KeyboardInterrupt, WorkerKeyboardInterrupt):
# Catch KeyboardInterrupt raised inside and outside of workers
rc = rc or errno.EINTR
except Exception as e:
# Catch any other exceptions raised
print('forall: unhandled error, terminating the pool: %s: %s' %
(type(e).__name__, e),
file=sys.stderr)
rc = rc or getattr(e, 'errno', 1)
print(
"forall: unhandled error, terminating the pool: %s: %s"
% (type(e).__name__, e),
file=sys.stderr,
)
rc = rc or getattr(e, "errno", 1)
if rc != 0:
sys.exit(rc)
@ -273,16 +315,16 @@ def InitWorker():
def DoWorkWrapper(mirror, opt, cmd, shell, config, args):
"""A wrapper around the DoWork() method.
Catch the KeyboardInterrupt exceptions here and re-raise them as a different,
``Exception``-based exception to stop it flooding the console with stacktraces
and making the parent hang indefinitely.
Catch the KeyboardInterrupt exceptions here and re-raise them as a
different, ``Exception``-based exception to stop it flooding the console
with stacktraces and making the parent hang indefinitely.
"""
cnt, project = args
try:
return DoWork(project, mirror, opt, cmd, shell, cnt, config)
except KeyboardInterrupt:
print('%s: Worker interrupted' % project.name)
print("%s: Worker interrupted" % project.name)
raise WorkerKeyboardInterrupt()
@ -291,30 +333,31 @@ def DoWork(project, mirror, opt, cmd, shell, cnt, config):
def setenv(name, val):
if val is None:
val = ''
val = ""
env[name] = val
setenv('REPO_PROJECT', project.name)
setenv('REPO_OUTERPATH', project.manifest.path_prefix)
setenv('REPO_INNERPATH', project.relpath)
setenv('REPO_PATH', project.RelPath(local=opt.this_manifest_only))
setenv('REPO_REMOTE', project.remote.name)
setenv("REPO_PROJECT", project.name)
setenv("REPO_OUTERPATH", project.manifest.path_prefix)
setenv("REPO_INNERPATH", project.relpath)
setenv("REPO_PATH", project.RelPath(local=opt.this_manifest_only))
setenv("REPO_REMOTE", project.remote.name)
try:
# If we aren't in a fully synced state and we don't have the ref the manifest
# wants, then this will fail. Ignore it for the purposes of this code.
lrev = '' if mirror else project.GetRevisionId()
# If we aren't in a fully synced state and we don't have the ref the
# manifest wants, then this will fail. Ignore it for the purposes of
# this code.
lrev = "" if mirror else project.GetRevisionId()
except ManifestInvalidRevisionError:
lrev = ''
setenv('REPO_LREV', lrev)
setenv('REPO_RREV', project.revisionExpr)
setenv('REPO_UPSTREAM', project.upstream)
setenv('REPO_DEST_BRANCH', project.dest_branch)
setenv('REPO_I', str(cnt + 1))
lrev = ""
setenv("REPO_LREV", lrev)
setenv("REPO_RREV", project.revisionExpr)
setenv("REPO_UPSTREAM", project.upstream)
setenv("REPO_DEST_BRANCH", project.dest_branch)
setenv("REPO_I", str(cnt + 1))
for annotation in project.annotations:
setenv("REPO__%s" % (annotation.name), annotation.value)
if mirror:
setenv('GIT_DIR', project.gitdir)
setenv("GIT_DIR", project.gitdir)
cwd = project.gitdir
else:
cwd = project.worktree
@ -323,12 +366,13 @@ def DoWork(project, mirror, opt, cmd, shell, cnt, config):
# Allow the user to silently ignore missing checkouts so they can run on
# partial checkouts (good for infra recovery tools).
if opt.ignore_missing:
return (0, '')
return (0, "")
output = ''
if ((opt.project_header and opt.verbose)
or not opt.project_header):
output = 'skipping %s/' % project.RelPath(local=opt.this_manifest_only)
output = ""
if (opt.project_header and opt.verbose) or not opt.project_header:
output = "skipping %s/" % project.RelPath(
local=opt.this_manifest_only
)
return (1, output)
if opt.verbose:
@ -339,9 +383,17 @@ def DoWork(project, mirror, opt, cmd, shell, cnt, config):
stdin = None if opt.interactive else subprocess.DEVNULL
result = subprocess.run(
cmd, cwd=cwd, shell=shell, env=env, check=False,
encoding='utf-8', errors='replace',
stdin=stdin, stdout=subprocess.PIPE, stderr=stderr)
cmd,
cwd=cwd,
shell=shell,
env=env,
check=False,
encoding="utf-8",
errors="replace",
stdin=stdin,
stdout=subprocess.PIPE,
stderr=stderr,
)
output = result.stdout
if opt.project_header:
@ -352,8 +404,10 @@ def DoWork(project, mirror, opt, cmd, shell, cnt, config):
if mirror:
project_header_path = project.name
else:
project_header_path = project.RelPath(local=opt.this_manifest_only)
out.project('project %s/' % project_header_path)
project_header_path = project.RelPath(
local=opt.this_manifest_only
)
out.project("project %s/" % project_header_path)
out.nl()
buf.write(output)
output = buf.getvalue()

View File

@ -31,16 +31,22 @@ and all locally downloaded sources.
"""
def _Options(self, p):
p.add_option('-f', '--force',
dest='force', action='store_true',
help='force the deletion (no prompt)')
p.add_option(
"-f",
"--force",
dest="force",
action="store_true",
help="force the deletion (no prompt)",
)
def Execute(self, opt, args):
if not opt.force:
prompt = ('This will delete GITC client: %s\nAre you sure? (yes/no) ' %
self.gitc_manifest.gitc_client_name)
prompt = (
"This will delete GITC client: %s\nAre you sure? (yes/no) "
% self.gitc_manifest.gitc_client_name
)
response = input(prompt).lower()
if not response == 'yes':
if not response == "yes":
print('Response was not "yes"\n Exiting...')
sys.exit(1)
platform_utils.rmtree(self.gitc_manifest.gitc_client_dir)

View File

@ -52,25 +52,36 @@ use for this GITC client.
def Execute(self, opt, args):
gitc_client = gitc_utils.parse_clientdir(os.getcwd())
if not gitc_client or (opt.gitc_client and gitc_client != opt.gitc_client):
print('fatal: Please update your repo command. See go/gitc for instructions.',
file=sys.stderr)
if not gitc_client or (
opt.gitc_client and gitc_client != opt.gitc_client
):
print(
"fatal: Please update your repo command. See go/gitc for "
"instructions.",
file=sys.stderr,
)
sys.exit(1)
self.client_dir = os.path.join(gitc_utils.get_gitc_manifest_dir(),
gitc_client)
self.client_dir = os.path.join(
gitc_utils.get_gitc_manifest_dir(), gitc_client
)
super().Execute(opt, args)
manifest_file = self.manifest.manifestFile
if opt.manifest_file:
if not os.path.exists(opt.manifest_file):
print('fatal: Specified manifest file %s does not exist.' %
opt.manifest_file)
print(
"fatal: Specified manifest file %s does not exist."
% opt.manifest_file
)
sys.exit(1)
manifest_file = opt.manifest_file
manifest = GitcManifest(self.repodir, os.path.join(self.client_dir,
'.manifest'))
manifest = GitcManifest(
self.repodir, os.path.join(self.client_dir, ".manifest")
)
manifest.Override(manifest_file)
gitc_utils.generate_gitc_manifest(None, manifest)
print('Please run `cd %s` to view your GITC client.' %
os.path.join(wrapper.Wrapper().GITC_FS_ROOT_DIR, gitc_client))
print(
"Please run `cd %s` to view your GITC client."
% os.path.join(wrapper.Wrapper().GITC_FS_ROOT_DIR, gitc_client)
)

View File

@ -23,9 +23,9 @@ from git_command import GitCommand
class GrepColoring(Coloring):
def __init__(self, config):
Coloring.__init__(self, config, 'grep')
self.project = self.printer('project', attr='bold')
self.fail = self.printer('fail', fg='red')
Coloring.__init__(self, config, "grep")
self.project = self.printer("project", attr="bold")
self.fail = self.printer("fail", fg="red")
class Grep(PagedCommand):
@ -66,15 +66,15 @@ contain a line that matches both expressions:
@staticmethod
def _carry_option(_option, opt_str, value, parser):
pt = getattr(parser.values, 'cmd_argv', None)
pt = getattr(parser.values, "cmd_argv", None)
if pt is None:
pt = []
setattr(parser.values, 'cmd_argv', pt)
setattr(parser.values, "cmd_argv", pt)
if opt_str == '-(':
pt.append('(')
elif opt_str == '-)':
pt.append(')')
if opt_str == "-(":
pt.append("(")
elif opt_str == "-)":
pt.append(")")
else:
pt.append(opt_str)
@ -86,86 +86,167 @@ contain a line that matches both expressions:
super()._CommonOptions(p, opt_v=False)
def _Options(self, p):
g = p.add_option_group('Sources')
g.add_option('--cached',
action='callback', callback=self._carry_option,
help='Search the index, instead of the work tree')
g.add_option('-r', '--revision',
dest='revision', action='append', metavar='TREEish',
help='Search TREEish, instead of the work tree')
g = p.add_option_group("Sources")
g.add_option(
"--cached",
action="callback",
callback=self._carry_option,
help="Search the index, instead of the work tree",
)
g.add_option(
"-r",
"--revision",
dest="revision",
action="append",
metavar="TREEish",
help="Search TREEish, instead of the work tree",
)
g = p.add_option_group('Pattern')
g.add_option('-e',
action='callback', callback=self._carry_option,
metavar='PATTERN', type='str',
help='Pattern to search for')
g.add_option('-i', '--ignore-case',
action='callback', callback=self._carry_option,
help='Ignore case differences')
g.add_option('-a', '--text',
action='callback', callback=self._carry_option,
help="Process binary files as if they were text")
g.add_option('-I',
action='callback', callback=self._carry_option,
help="Don't match the pattern in binary files")
g.add_option('-w', '--word-regexp',
action='callback', callback=self._carry_option,
help='Match the pattern only at word boundaries')
g.add_option('-v', '--invert-match',
action='callback', callback=self._carry_option,
help='Select non-matching lines')
g.add_option('-G', '--basic-regexp',
action='callback', callback=self._carry_option,
help='Use POSIX basic regexp for patterns (default)')
g.add_option('-E', '--extended-regexp',
action='callback', callback=self._carry_option,
help='Use POSIX extended regexp for patterns')
g.add_option('-F', '--fixed-strings',
action='callback', callback=self._carry_option,
help='Use fixed strings (not regexp) for pattern')
g = p.add_option_group("Pattern")
g.add_option(
"-e",
action="callback",
callback=self._carry_option,
metavar="PATTERN",
type="str",
help="Pattern to search for",
)
g.add_option(
"-i",
"--ignore-case",
action="callback",
callback=self._carry_option,
help="Ignore case differences",
)
g.add_option(
"-a",
"--text",
action="callback",
callback=self._carry_option,
help="Process binary files as if they were text",
)
g.add_option(
"-I",
action="callback",
callback=self._carry_option,
help="Don't match the pattern in binary files",
)
g.add_option(
"-w",
"--word-regexp",
action="callback",
callback=self._carry_option,
help="Match the pattern only at word boundaries",
)
g.add_option(
"-v",
"--invert-match",
action="callback",
callback=self._carry_option,
help="Select non-matching lines",
)
g.add_option(
"-G",
"--basic-regexp",
action="callback",
callback=self._carry_option,
help="Use POSIX basic regexp for patterns (default)",
)
g.add_option(
"-E",
"--extended-regexp",
action="callback",
callback=self._carry_option,
help="Use POSIX extended regexp for patterns",
)
g.add_option(
"-F",
"--fixed-strings",
action="callback",
callback=self._carry_option,
help="Use fixed strings (not regexp) for pattern",
)
g = p.add_option_group('Pattern Grouping')
g.add_option('--all-match',
action='callback', callback=self._carry_option,
help='Limit match to lines that have all patterns')
g.add_option('--and', '--or', '--not',
action='callback', callback=self._carry_option,
help='Boolean operators to combine patterns')
g.add_option('-(', '-)',
action='callback', callback=self._carry_option,
help='Boolean operator grouping')
g = p.add_option_group("Pattern Grouping")
g.add_option(
"--all-match",
action="callback",
callback=self._carry_option,
help="Limit match to lines that have all patterns",
)
g.add_option(
"--and",
"--or",
"--not",
action="callback",
callback=self._carry_option,
help="Boolean operators to combine patterns",
)
g.add_option(
"-(",
"-)",
action="callback",
callback=self._carry_option,
help="Boolean operator grouping",
)
g = p.add_option_group('Output')
g.add_option('-n',
action='callback', callback=self._carry_option,
help='Prefix the line number to matching lines')
g.add_option('-C',
action='callback', callback=self._carry_option,
metavar='CONTEXT', type='str',
help='Show CONTEXT lines around match')
g.add_option('-B',
action='callback', callback=self._carry_option,
metavar='CONTEXT', type='str',
help='Show CONTEXT lines before match')
g.add_option('-A',
action='callback', callback=self._carry_option,
metavar='CONTEXT', type='str',
help='Show CONTEXT lines after match')
g.add_option('-l', '--name-only', '--files-with-matches',
action='callback', callback=self._carry_option,
help='Show only file names containing matching lines')
g.add_option('-L', '--files-without-match',
action='callback', callback=self._carry_option,
help='Show only file names not containing matching lines')
g = p.add_option_group("Output")
g.add_option(
"-n",
action="callback",
callback=self._carry_option,
help="Prefix the line number to matching lines",
)
g.add_option(
"-C",
action="callback",
callback=self._carry_option,
metavar="CONTEXT",
type="str",
help="Show CONTEXT lines around match",
)
g.add_option(
"-B",
action="callback",
callback=self._carry_option,
metavar="CONTEXT",
type="str",
help="Show CONTEXT lines before match",
)
g.add_option(
"-A",
action="callback",
callback=self._carry_option,
metavar="CONTEXT",
type="str",
help="Show CONTEXT lines after match",
)
g.add_option(
"-l",
"--name-only",
"--files-with-matches",
action="callback",
callback=self._carry_option,
help="Show only file names containing matching lines",
)
g.add_option(
"-L",
"--files-without-match",
action="callback",
callback=self._carry_option,
help="Show only file names not containing matching lines",
)
def _ExecuteOne(self, cmd_argv, project):
"""Process one project."""
try:
p = GitCommand(project,
p = GitCommand(
project,
cmd_argv,
bare=False,
capture_stdout=True,
capture_stderr=True)
capture_stderr=True,
)
except GitError as e:
return (project, -1, None, str(e))
@ -181,42 +262,42 @@ contain a line that matches both expressions:
for project, rc, stdout, stderr in results:
if rc < 0:
git_failed = True
out.project('--- project %s ---' % _RelPath(project))
out.project("--- project %s ---" % _RelPath(project))
out.nl()
out.fail('%s', stderr)
out.fail("%s", stderr)
out.nl()
continue
if rc:
# no results
if stderr:
if have_rev and 'fatal: ambiguous argument' in stderr:
if have_rev and "fatal: ambiguous argument" in stderr:
bad_rev = True
else:
out.project('--- project %s ---' % _RelPath(project))
out.project("--- project %s ---" % _RelPath(project))
out.nl()
out.fail('%s', stderr.strip())
out.fail("%s", stderr.strip())
out.nl()
continue
have_match = True
# We cut the last element, to avoid a blank line.
r = stdout.split('\n')
r = stdout.split("\n")
r = r[0:-1]
if have_rev and full_name:
for line in r:
rev, line = line.split(':', 1)
rev, line = line.split(":", 1)
out.write("%s", rev)
out.write(':')
out.write(":")
out.project(_RelPath(project))
out.write('/')
out.write("/")
out.write("%s", line)
out.nl()
elif full_name:
for line in r:
out.project(_RelPath(project))
out.write('/')
out.write("/")
out.write("%s", line)
out.nl()
else:
@ -228,41 +309,49 @@ contain a line that matches both expressions:
def Execute(self, opt, args):
out = GrepColoring(self.manifest.manifestProject.config)
cmd_argv = ['grep']
cmd_argv = ["grep"]
if out.is_on:
cmd_argv.append('--color')
cmd_argv.extend(getattr(opt, 'cmd_argv', []))
cmd_argv.append("--color")
cmd_argv.extend(getattr(opt, "cmd_argv", []))
if '-e' not in cmd_argv:
if "-e" not in cmd_argv:
if not args:
self.Usage()
cmd_argv.append('-e')
cmd_argv.append("-e")
cmd_argv.append(args[0])
args = args[1:]
projects = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
projects = self.GetProjects(
args, all_manifests=not opt.this_manifest_only
)
full_name = False
if len(projects) > 1:
cmd_argv.append('--full-name')
cmd_argv.append("--full-name")
full_name = True
have_rev = False
if opt.revision:
if '--cached' in cmd_argv:
print('fatal: cannot combine --cached and --revision', file=sys.stderr)
if "--cached" in cmd_argv:
print(
"fatal: cannot combine --cached and --revision",
file=sys.stderr,
)
sys.exit(1)
have_rev = True
cmd_argv.extend(opt.revision)
cmd_argv.append('--')
cmd_argv.append("--")
git_failed, bad_rev, have_match = self.ExecuteInParallel(
opt.jobs,
functools.partial(self._ExecuteOne, cmd_argv),
projects,
callback=functools.partial(self._ProcessResults, full_name, have_rev, opt),
callback=functools.partial(
self._ProcessResults, full_name, have_rev, opt
),
output=out,
ordered=True)
ordered=True,
)
if git_failed:
sys.exit(1)

View File

@ -18,7 +18,12 @@ import textwrap
from subcmds import all_commands
from color import Coloring
from command import PagedCommand, MirrorSafeCommand, GitcAvailableCommand, GitcClientCommand
from command import (
PagedCommand,
MirrorSafeCommand,
GitcAvailableCommand,
GitcClientCommand,
)
import gitc_utils
from wrapper import Wrapper
@ -38,37 +43,41 @@ Displays detailed usage information about a command.
maxlen = 0
for name in commandNames:
maxlen = max(maxlen, len(name))
fmt = ' %%-%ds %%s' % maxlen
fmt = " %%-%ds %%s" % maxlen
for name in commandNames:
command = all_commands[name]()
try:
summary = command.helpSummary.strip()
except AttributeError:
summary = ''
summary = ""
print(fmt % (name, summary))
def _PrintAllCommands(self):
print('usage: repo COMMAND [ARGS]')
print("usage: repo COMMAND [ARGS]")
self.PrintAllCommandsBody()
def PrintAllCommandsBody(self):
print('The complete list of recognized repo commands is:')
print("The complete list of recognized repo commands is:")
commandNames = list(sorted(all_commands))
self._PrintCommands(commandNames)
print("See 'repo help <command>' for more information on a "
'specific command.')
print('Bug reports:', Wrapper().BUG_URL)
print(
"See 'repo help <command>' for more information on a "
"specific command."
)
print("Bug reports:", Wrapper().BUG_URL)
def _PrintCommonCommands(self):
print('usage: repo COMMAND [ARGS]')
print("usage: repo COMMAND [ARGS]")
self.PrintCommonCommandsBody()
def PrintCommonCommandsBody(self):
print('The most commonly used repo commands are:')
print("The most commonly used repo commands are:")
def gitc_supported(cmd):
if not isinstance(cmd, GitcAvailableCommand) and not isinstance(cmd, GitcClientCommand):
if not isinstance(cmd, GitcAvailableCommand) and not isinstance(
cmd, GitcClientCommand
):
return True
if self.client.isGitcClient:
return True
@ -78,21 +87,29 @@ Displays detailed usage information about a command.
return True
return False
commandNames = list(sorted([name
commandNames = list(
sorted(
[
name
for name, command in all_commands.items()
if command.COMMON and gitc_supported(command)]))
if command.COMMON and gitc_supported(command)
]
)
)
self._PrintCommands(commandNames)
print(
"See 'repo help <command>' for more information on a specific command.\n"
"See 'repo help --all' for a complete list of recognized commands.")
print('Bug reports:', Wrapper().BUG_URL)
"See 'repo help <command>' for more information on a specific "
"command.\nSee 'repo help --all' for a complete list of recognized "
"commands."
)
print("Bug reports:", Wrapper().BUG_URL)
def _PrintCommandHelp(self, cmd, header_prefix=''):
def _PrintCommandHelp(self, cmd, header_prefix=""):
class _Out(Coloring):
def __init__(self, gc):
Coloring.__init__(self, gc, 'help')
self.heading = self.printer('heading', attr='bold')
Coloring.__init__(self, gc, "help")
self.heading = self.printer("heading", attr="bold")
self._first = True
def _PrintSection(self, heading, bodyAttr):
@ -100,61 +117,72 @@ Displays detailed usage information about a command.
body = getattr(cmd, bodyAttr)
except AttributeError:
return
if body == '' or body is None:
if body == "" or body is None:
return
if not self._first:
self.nl()
self._first = False
self.heading('%s%s', header_prefix, heading)
self.heading("%s%s", header_prefix, heading)
self.nl()
self.nl()
me = 'repo %s' % cmd.NAME
me = "repo %s" % cmd.NAME
body = body.strip()
body = body.replace('%prog', me)
body = body.replace("%prog", me)
# Extract the title, but skip any trailing {#anchors}.
asciidoc_hdr = re.compile(r'^\n?#+ ([^{]+)(\{#.+\})?$')
asciidoc_hdr = re.compile(r"^\n?#+ ([^{]+)(\{#.+\})?$")
for para in body.split("\n\n"):
if para.startswith(' '):
self.write('%s', para)
if para.startswith(" "):
self.write("%s", para)
self.nl()
self.nl()
continue
m = asciidoc_hdr.match(para)
if m:
self.heading('%s%s', header_prefix, m.group(1))
self.heading("%s%s", header_prefix, m.group(1))
self.nl()
self.nl()
continue
lines = textwrap.wrap(para.replace(' ', ' '), width=80,
break_long_words=False, break_on_hyphens=False)
lines = textwrap.wrap(
para.replace(" ", " "),
width=80,
break_long_words=False,
break_on_hyphens=False,
)
for line in lines:
self.write('%s', line)
self.write("%s", line)
self.nl()
self.nl()
out = _Out(self.client.globalConfig)
out._PrintSection('Summary', 'helpSummary')
out._PrintSection("Summary", "helpSummary")
cmd.OptionParser.print_help()
out._PrintSection('Description', 'helpDescription')
out._PrintSection("Description", "helpDescription")
def _PrintAllCommandHelp(self):
for name in sorted(all_commands):
cmd = all_commands[name](manifest=self.manifest)
self._PrintCommandHelp(cmd, header_prefix='[%s] ' % (name,))
self._PrintCommandHelp(cmd, header_prefix="[%s] " % (name,))
def _Options(self, p):
p.add_option('-a', '--all',
dest='show_all', action='store_true',
help='show the complete list of commands')
p.add_option('--help-all',
dest='show_all_help', action='store_true',
help='show the --help of all commands')
p.add_option(
"-a",
"--all",
dest="show_all",
action="store_true",
help="show the complete list of commands",
)
p.add_option(
"--help-all",
dest="show_all_help",
action="store_true",
help="show the --help of all commands",
)
def Execute(self, opt, args):
if len(args) == 0:
@ -171,7 +199,9 @@ Displays detailed usage information about a command.
try:
cmd = all_commands[name](manifest=self.manifest)
except KeyError:
print("repo: '%s' is not a repo command." % name, file=sys.stderr)
print(
"repo: '%s' is not a repo command." % name, file=sys.stderr
)
sys.exit(1)
self._PrintCommandHelp(cmd)

View File

@ -26,38 +26,62 @@ class _Coloring(Coloring):
class Info(PagedCommand):
COMMON = True
helpSummary = "Get info on the manifest branch, current branch or unmerged branches"
helpSummary = (
"Get info on the manifest branch, current branch or unmerged branches"
)
helpUsage = "%prog [-dl] [-o [-c]] [<project>...]"
def _Options(self, p):
p.add_option('-d', '--diff',
dest='all', action='store_true',
help="show full info and commit diff including remote branches")
p.add_option('-o', '--overview',
dest='overview', action='store_true',
help='show overview of all local commits')
p.add_option('-c', '--current-branch',
dest="current_branch", action="store_true",
help="consider only checked out branches")
p.add_option('--no-current-branch',
dest='current_branch', action='store_false',
help='consider all local branches')
p.add_option(
"-d",
"--diff",
dest="all",
action="store_true",
help="show full info and commit diff including remote branches",
)
p.add_option(
"-o",
"--overview",
dest="overview",
action="store_true",
help="show overview of all local commits",
)
p.add_option(
"-c",
"--current-branch",
dest="current_branch",
action="store_true",
help="consider only checked out branches",
)
p.add_option(
"--no-current-branch",
dest="current_branch",
action="store_false",
help="consider all local branches",
)
# Turn this into a warning & remove this someday.
p.add_option('-b',
dest='current_branch', action='store_true',
help=optparse.SUPPRESS_HELP)
p.add_option('-l', '--local-only',
dest="local", action="store_true",
help="disable all remote operations")
p.add_option(
"-b",
dest="current_branch",
action="store_true",
help=optparse.SUPPRESS_HELP,
)
p.add_option(
"-l",
"--local-only",
dest="local",
action="store_true",
help="disable all remote operations",
)
def Execute(self, opt, args):
self.out = _Coloring(self.client.globalConfig)
self.heading = self.out.printer('heading', attr='bold')
self.headtext = self.out.nofmt_printer('headtext', fg='yellow')
self.redtext = self.out.printer('redtext', fg='red')
self.sha = self.out.printer("sha", fg='yellow')
self.text = self.out.nofmt_printer('text')
self.dimtext = self.out.printer('dimtext', attr='dim')
self.heading = self.out.printer("heading", attr="bold")
self.headtext = self.out.nofmt_printer("headtext", fg="yellow")
self.redtext = self.out.printer("redtext", fg="red")
self.sha = self.out.printer("sha", fg="yellow")
self.text = self.out.nofmt_printer("text")
self.dimtext = self.out.printer("dimtext", attr="dim")
self.opt = opt
@ -108,7 +132,7 @@ class Info(PagedCommand):
currentBranch = p.CurrentBranch
if currentBranch:
self.heading('Current branch: ')
self.heading("Current branch: ")
self.headtext(currentBranch)
self.out.nl()
@ -135,7 +159,7 @@ class Info(PagedCommand):
if not self.opt.local:
project.Sync_NetworkHalf(quiet=True, current_branch_only=True)
branch = self.manifest.manifestProject.config.GetBranch('default').merge
branch = self.manifest.manifestProject.config.GetBranch("default").merge
if branch.startswith(R_HEADS):
branch = branch[len(R_HEADS) :]
logTarget = R_M + branch
@ -143,18 +167,20 @@ class Info(PagedCommand):
bareTmp = project.bare_git._bare
project.bare_git._bare = False
localCommits = project.bare_git.rev_list(
'--abbrev=8',
'--abbrev-commit',
'--pretty=oneline',
"--abbrev=8",
"--abbrev-commit",
"--pretty=oneline",
logTarget + "..",
'--')
"--",
)
originCommits = project.bare_git.rev_list(
'--abbrev=8',
'--abbrev-commit',
'--pretty=oneline',
"--abbrev=8",
"--abbrev-commit",
"--pretty=oneline",
".." + logTarget,
'--')
"--",
)
project.bare_git._bare = bareTmp
self.heading("Local Commits: ")
@ -182,9 +208,10 @@ class Info(PagedCommand):
def _printCommitOverview(self, opt, args):
all_branches = []
for project in self.GetProjects(args, all_manifests=not opt.this_manifest_only):
br = [project.GetUploadableBranch(x)
for x in project.GetBranches()]
for project in self.GetProjects(
args, all_manifests=not opt.this_manifest_only
):
br = [project.GetUploadableBranch(x) for x in project.GetBranches()]
br = [x for x in br if x]
if self.opt.current_branch:
br = [x for x in br if x.name == project.CurrentBranch]
@ -194,7 +221,7 @@ class Info(PagedCommand):
return
self.out.nl()
self.heading('Projects Overview')
self.heading("Projects Overview")
project = None
for branch in all_branches:
@ -206,17 +233,21 @@ class Info(PagedCommand):
commits = branch.commits
date = branch.date
self.text('%s %-33s (%2d commit%s, %s)' % (
branch.name == project.CurrentBranch and '*' or ' ',
self.text(
"%s %-33s (%2d commit%s, %s)"
% (
branch.name == project.CurrentBranch and "*" or " ",
branch.name,
len(commits),
len(commits) != 1 and 's' or '',
date))
len(commits) != 1 and "s" or "",
date,
)
)
self.out.nl()
for commit in commits:
split = commit.split()
self.text('{0:38}{1} '.format('', '-'))
self.text("{0:38}{1} ".format("", "-"))
self.sha(split[0] + " ")
self.text(" ".join(split[1:]))
self.out.nl()

View File

@ -82,20 +82,38 @@ to update the working directory files.
def _Options(self, p, gitc_init=False):
Wrapper().InitParser(p, gitc_init=gitc_init)
m = p.add_option_group('Multi-manifest')
m.add_option('--outer-manifest', action='store_true', default=True,
help='operate starting at the outermost manifest')
m.add_option('--no-outer-manifest', dest='outer_manifest',
action='store_false', help='do not operate on outer manifests')
m.add_option('--this-manifest-only', action='store_true', default=None,
help='only operate on this (sub)manifest')
m.add_option('--no-this-manifest-only', '--all-manifests',
dest='this_manifest_only', action='store_false',
help='operate on this manifest and its submanifests')
m = p.add_option_group("Multi-manifest")
m.add_option(
"--outer-manifest",
action="store_true",
default=True,
help="operate starting at the outermost manifest",
)
m.add_option(
"--no-outer-manifest",
dest="outer_manifest",
action="store_false",
help="do not operate on outer manifests",
)
m.add_option(
"--this-manifest-only",
action="store_true",
default=None,
help="only operate on this (sub)manifest",
)
m.add_option(
"--no-this-manifest-only",
"--all-manifests",
dest="this_manifest_only",
action="store_false",
help="operate on this manifest and its submanifests",
)
def _RegisteredEnvironmentOptions(self):
return {'REPO_MANIFEST_URL': 'manifest_url',
'REPO_MIRROR_LOCATION': 'reference'}
return {
"REPO_MANIFEST_URL": "manifest_url",
"REPO_MIRROR_LOCATION": "reference",
}
def _SyncManifest(self, opt):
"""Call manifestProject.Sync with arguments from opt.
@ -130,13 +148,14 @@ to update the working directory files.
tags=opt.tags,
depth=opt.depth,
git_event_log=self.git_event_log,
manifest_name=opt.manifest_name):
manifest_name=opt.manifest_name,
):
sys.exit(1)
def _Prompt(self, prompt, value):
print('%-10s [%s]: ' % (prompt, value), end='', flush=True)
print("%-10s [%s]: " % (prompt, value), end="", flush=True)
a = sys.stdin.readline().strip()
if a == '':
if a == "":
return value
return a
@ -145,18 +164,26 @@ to update the working directory files.
mp = self.manifest.manifestProject
# If we don't have local settings, get from global.
if not mp.config.Has('user.name') or not mp.config.Has('user.email'):
if not gc.Has('user.name') or not gc.Has('user.email'):
if not mp.config.Has("user.name") or not mp.config.Has("user.email"):
if not gc.Has("user.name") or not gc.Has("user.email"):
return True
mp.config.SetString('user.name', gc.GetString('user.name'))
mp.config.SetString('user.email', gc.GetString('user.email'))
mp.config.SetString("user.name", gc.GetString("user.name"))
mp.config.SetString("user.email", gc.GetString("user.email"))
if not opt.quiet and not existing_checkout or opt.verbose:
print()
print('Your identity is: %s <%s>' % (mp.config.GetString('user.name'),
mp.config.GetString('user.email')))
print("If you want to change this, please re-run 'repo init' with --config-name")
print(
"Your identity is: %s <%s>"
% (
mp.config.GetString("user.name"),
mp.config.GetString("user.email"),
)
)
print(
"If you want to change this, please re-run 'repo init' with "
"--config-name"
)
return False
def _ConfigureUser(self, opt):
@ -165,25 +192,25 @@ to update the working directory files.
while True:
if not opt.quiet:
print()
name = self._Prompt('Your Name', mp.UserName)
email = self._Prompt('Your Email', mp.UserEmail)
name = self._Prompt("Your Name", mp.UserName)
email = self._Prompt("Your Email", mp.UserEmail)
if not opt.quiet:
print()
print('Your identity is: %s <%s>' % (name, email))
print('is this correct [y/N]? ', end='', flush=True)
print("Your identity is: %s <%s>" % (name, email))
print("is this correct [y/N]? ", end="", flush=True)
a = sys.stdin.readline().strip().lower()
if a in ('yes', 'y', 't', 'true'):
if a in ("yes", "y", "t", "true"):
break
if name != mp.UserName:
mp.config.SetString('user.name', name)
mp.config.SetString("user.name", name)
if email != mp.UserEmail:
mp.config.SetString('user.email', email)
mp.config.SetString("user.email", email)
def _HasColorSet(self, gc):
for n in ['ui', 'diff', 'status']:
if gc.Has('color.%s' % n):
for n in ["ui", "diff", "status"]:
if gc.Has("color.%s" % n):
return True
return False
@ -194,92 +221,112 @@ to update the working directory files.
class _Test(Coloring):
def __init__(self):
Coloring.__init__(self, gc, 'test color display')
Coloring.__init__(self, gc, "test color display")
self._on = True
out = _Test()
print()
print("Testing colorized output (for 'repo diff', 'repo status'):")
for c in ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan']:
out.write(' ')
out.printer(fg=c)(' %-6s ', c)
out.write(' ')
out.printer(fg='white', bg='black')(' %s ' % 'white')
for c in ["black", "red", "green", "yellow", "blue", "magenta", "cyan"]:
out.write(" ")
out.printer(fg=c)(" %-6s ", c)
out.write(" ")
out.printer(fg="white", bg="black")(" %s " % "white")
out.nl()
for c in ['bold', 'dim', 'ul', 'reverse']:
out.write(' ')
out.printer(fg='black', attr=c)(' %-6s ', c)
for c in ["bold", "dim", "ul", "reverse"]:
out.write(" ")
out.printer(fg="black", attr=c)(" %-6s ", c)
out.nl()
print('Enable color display in this user account (y/N)? ', end='', flush=True)
print(
"Enable color display in this user account (y/N)? ",
end="",
flush=True,
)
a = sys.stdin.readline().strip().lower()
if a in ('y', 'yes', 't', 'true', 'on'):
gc.SetString('color.ui', 'auto')
if a in ("y", "yes", "t", "true", "on"):
gc.SetString("color.ui", "auto")
def _DisplayResult(self):
if self.manifest.IsMirror:
init_type = 'mirror '
init_type = "mirror "
else:
init_type = ''
init_type = ""
print()
print('repo %shas been initialized in %s' % (init_type, self.manifest.topdir))
print(
"repo %shas been initialized in %s"
% (init_type, self.manifest.topdir)
)
current_dir = os.getcwd()
if current_dir != self.manifest.topdir:
print('If this is not the directory in which you want to initialize '
'repo, please run:')
print(' rm -r %s' % os.path.join(self.manifest.topdir, '.repo'))
print('and try again.')
print(
"If this is not the directory in which you want to initialize "
"repo, please run:"
)
print(" rm -r %s" % os.path.join(self.manifest.topdir, ".repo"))
print("and try again.")
def ValidateOptions(self, opt, args):
if opt.reference:
opt.reference = os.path.expanduser(opt.reference)
# Check this here, else manifest will be tagged "not new" and init won't be
# possible anymore without removing the .repo/manifests directory.
# Check this here, else manifest will be tagged "not new" and init won't
# be possible anymore without removing the .repo/manifests directory.
if opt.mirror:
if opt.archive:
self.OptionParser.error('--mirror and --archive cannot be used '
'together.')
self.OptionParser.error(
"--mirror and --archive cannot be used " "together."
)
if opt.use_superproject is not None:
self.OptionParser.error('--mirror and --use-superproject cannot be '
'used together.')
self.OptionParser.error(
"--mirror and --use-superproject cannot be "
"used together."
)
if opt.archive and opt.use_superproject is not None:
self.OptionParser.error('--archive and --use-superproject cannot be used '
'together.')
self.OptionParser.error(
"--archive and --use-superproject cannot be used " "together."
)
if opt.standalone_manifest and (opt.manifest_branch or
opt.manifest_name != 'default.xml'):
self.OptionParser.error('--manifest-branch and --manifest-name cannot'
' be used with --standalone-manifest.')
if opt.standalone_manifest and (
opt.manifest_branch or opt.manifest_name != "default.xml"
):
self.OptionParser.error(
"--manifest-branch and --manifest-name cannot"
" be used with --standalone-manifest."
)
if args:
if opt.manifest_url:
self.OptionParser.error(
'--manifest-url option and URL argument both specified: only use '
'one to select the manifest URL.')
"--manifest-url option and URL argument both specified: "
"only use one to select the manifest URL."
)
opt.manifest_url = args.pop(0)
if args:
self.OptionParser.error('too many arguments to init')
self.OptionParser.error("too many arguments to init")
def Execute(self, opt, args):
git_require(MIN_GIT_VERSION_HARD, fail=True)
if not git_require(MIN_GIT_VERSION_SOFT):
print('repo: warning: git-%s+ will soon be required; please upgrade your '
'version of git to maintain support.'
% ('.'.join(str(x) for x in MIN_GIT_VERSION_SOFT),),
file=sys.stderr)
print(
"repo: warning: git-%s+ will soon be required; please upgrade "
"your version of git to maintain support."
% (".".join(str(x) for x in MIN_GIT_VERSION_SOFT),),
file=sys.stderr,
)
rp = self.manifest.repoProject
# Handle new --repo-url requests.
if opt.repo_url:
remote = rp.GetRemote('origin')
remote = rp.GetRemote("origin")
remote.url = opt.repo_url
remote.Save()
@ -288,30 +335,43 @@ to update the working directory files.
wrapper = Wrapper()
try:
remote_ref, rev = wrapper.check_repo_rev(
rp.gitdir, opt.repo_rev, repo_verify=opt.repo_verify, quiet=opt.quiet)
rp.gitdir,
opt.repo_rev,
repo_verify=opt.repo_verify,
quiet=opt.quiet,
)
except wrapper.CloneFailure:
print('fatal: double check your --repo-rev setting.', file=sys.stderr)
print(
"fatal: double check your --repo-rev setting.",
file=sys.stderr,
)
sys.exit(1)
branch = rp.GetBranch('default')
branch = rp.GetBranch("default")
branch.merge = remote_ref
rp.work_git.reset('--hard', rev)
rp.work_git.reset("--hard", rev)
branch.Save()
if opt.worktree:
# Older versions of git supported worktree, but had dangerous gc bugs.
git_require((2, 15, 0), fail=True, msg='git gc worktree corruption')
# Older versions of git supported worktree, but had dangerous gc
# bugs.
git_require((2, 15, 0), fail=True, msg="git gc worktree corruption")
# Provide a short notice that we're reinitializing an existing checkout.
# Sometimes developers might not realize that they're in one, or that
# repo doesn't do nested checkouts.
existing_checkout = self.manifest.manifestProject.Exists
if not opt.quiet and existing_checkout:
print('repo: reusing existing repo client checkout in', self.manifest.topdir)
print(
"repo: reusing existing repo client checkout in",
self.manifest.topdir,
)
self._SyncManifest(opt)
if os.isatty(0) and os.isatty(1) and not self.manifest.IsMirror:
if opt.config_name or self._ShouldConfigureUser(opt, existing_checkout):
if opt.config_name or self._ShouldConfigureUser(
opt, existing_checkout
):
self._ConfigureUser(opt)
self._ConfigureColor()

View File

@ -36,30 +36,58 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
"""
def _Options(self, p):
p.add_option('-r', '--regex',
dest='regex', action='store_true',
help='filter the project list based on regex or wildcard matching of strings')
p.add_option('-g', '--groups',
dest='groups',
help='filter the project list based on the groups the project is in')
p.add_option('-a', '--all',
action='store_true',
help='show projects regardless of checkout state')
p.add_option('-n', '--name-only',
dest='name_only', action='store_true',
help='display only the name of the repository')
p.add_option('-p', '--path-only',
dest='path_only', action='store_true',
help='display only the path of the repository')
p.add_option('-f', '--fullpath',
dest='fullpath', action='store_true',
help='display the full work tree path instead of the relative path')
p.add_option('--relative-to', metavar='PATH',
help='display paths relative to this one (default: top of repo client checkout)')
p.add_option(
"-r",
"--regex",
dest="regex",
action="store_true",
help="filter the project list based on regex or wildcard matching "
"of strings",
)
p.add_option(
"-g",
"--groups",
dest="groups",
help="filter the project list based on the groups the project is "
"in",
)
p.add_option(
"-a",
"--all",
action="store_true",
help="show projects regardless of checkout state",
)
p.add_option(
"-n",
"--name-only",
dest="name_only",
action="store_true",
help="display only the name of the repository",
)
p.add_option(
"-p",
"--path-only",
dest="path_only",
action="store_true",
help="display only the path of the repository",
)
p.add_option(
"-f",
"--fullpath",
dest="fullpath",
action="store_true",
help="display the full work tree path instead of the relative path",
)
p.add_option(
"--relative-to",
metavar="PATH",
help="display paths relative to this one (default: top of repo "
"client checkout)",
)
def ValidateOptions(self, opt, args):
if opt.fullpath and opt.name_only:
self.OptionParser.error('cannot combine -f and -n')
self.OptionParser.error("cannot combine -f and -n")
# Resolve any symlinks so the output is stable.
if opt.relative_to:
@ -77,10 +105,16 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
args: Positional args. Can be a list of projects to list, or empty.
"""
if not opt.regex:
projects = self.GetProjects(args, groups=opt.groups, missing_ok=opt.all,
all_manifests=not opt.this_manifest_only)
projects = self.GetProjects(
args,
groups=opt.groups,
missing_ok=opt.all,
all_manifests=not opt.this_manifest_only,
)
else:
projects = self.FindProjects(args, all_manifests=not opt.this_manifest_only)
projects = self.FindProjects(
args, all_manifests=not opt.this_manifest_only
)
def _getpath(x):
if opt.fullpath:
@ -100,4 +134,4 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
if lines:
lines.sort()
print('\n'.join(lines))
print("\n".join(lines))

View File

@ -42,86 +42,130 @@ to indicate the remote ref to push changes to via 'repo upload'.
@property
def helpDescription(self):
helptext = self._helpDescription + '\n'
helptext = self._helpDescription + "\n"
r = os.path.dirname(__file__)
r = os.path.dirname(r)
with open(os.path.join(r, 'docs', 'manifest-format.md')) as fd:
with open(os.path.join(r, "docs", "manifest-format.md")) as fd:
for line in fd:
helptext += line
return helptext
def _Options(self, p):
p.add_option('-r', '--revision-as-HEAD',
dest='peg_rev', action='store_true',
help='save revisions as current HEAD')
p.add_option('-m', '--manifest-name',
help='temporary manifest to use for this sync', metavar='NAME.xml')
p.add_option('--suppress-upstream-revision', dest='peg_rev_upstream',
default=True, action='store_false',
help='if in -r mode, do not write the upstream field '
'(only of use if the branch names for a sha1 manifest are '
'sensitive)')
p.add_option('--suppress-dest-branch', dest='peg_rev_dest_branch',
default=True, action='store_false',
help='if in -r mode, do not write the dest-branch field '
'(only of use if the branch names for a sha1 manifest are '
'sensitive)')
p.add_option('--json', default=False, action='store_true',
help='output manifest in JSON format (experimental)')
p.add_option('--pretty', default=False, action='store_true',
help='format output for humans to read')
p.add_option('--no-local-manifests', default=False, action='store_true',
dest='ignore_local_manifests', help='ignore local manifests')
p.add_option('-o', '--output-file',
dest='output_file',
default='-',
help='file to save the manifest to. (Filename prefix for multi-tree.)',
metavar='-|NAME.xml')
p.add_option(
"-r",
"--revision-as-HEAD",
dest="peg_rev",
action="store_true",
help="save revisions as current HEAD",
)
p.add_option(
"-m",
"--manifest-name",
help="temporary manifest to use for this sync",
metavar="NAME.xml",
)
p.add_option(
"--suppress-upstream-revision",
dest="peg_rev_upstream",
default=True,
action="store_false",
help="if in -r mode, do not write the upstream field "
"(only of use if the branch names for a sha1 manifest are "
"sensitive)",
)
p.add_option(
"--suppress-dest-branch",
dest="peg_rev_dest_branch",
default=True,
action="store_false",
help="if in -r mode, do not write the dest-branch field "
"(only of use if the branch names for a sha1 manifest are "
"sensitive)",
)
p.add_option(
"--json",
default=False,
action="store_true",
help="output manifest in JSON format (experimental)",
)
p.add_option(
"--pretty",
default=False,
action="store_true",
help="format output for humans to read",
)
p.add_option(
"--no-local-manifests",
default=False,
action="store_true",
dest="ignore_local_manifests",
help="ignore local manifests",
)
p.add_option(
"-o",
"--output-file",
dest="output_file",
default="-",
help="file to save the manifest to. (Filename prefix for "
"multi-tree.)",
metavar="-|NAME.xml",
)
def _Output(self, opt):
# If alternate manifest is specified, override the manifest file that we're using.
# If alternate manifest is specified, override the manifest file that
# we're using.
if opt.manifest_name:
self.manifest.Override(opt.manifest_name, False)
for manifest in self.ManifestList(opt):
output_file = opt.output_file
if output_file == '-':
if output_file == "-":
fd = sys.stdout
else:
if manifest.path_prefix:
output_file = f'{opt.output_file}:{manifest.path_prefix.replace("/", "%2f")}'
fd = open(output_file, 'w')
output_file = (
f"{opt.output_file}:"
f'{manifest.path_prefix.replace("/", "%2f")}'
)
fd = open(output_file, "w")
manifest.SetUseLocalManifests(not opt.ignore_local_manifests)
if opt.json:
print('warning: --json is experimental!', file=sys.stderr)
doc = manifest.ToDict(peg_rev=opt.peg_rev,
print("warning: --json is experimental!", file=sys.stderr)
doc = manifest.ToDict(
peg_rev=opt.peg_rev,
peg_rev_upstream=opt.peg_rev_upstream,
peg_rev_dest_branch=opt.peg_rev_dest_branch)
peg_rev_dest_branch=opt.peg_rev_dest_branch,
)
json_settings = {
# JSON style guide says Uunicode characters are fully allowed.
'ensure_ascii': False,
# JSON style guide says Unicode characters are fully
# allowed.
"ensure_ascii": False,
# We use 2 space indent to match JSON style guide.
'indent': 2 if opt.pretty else None,
'separators': (',', ': ') if opt.pretty else (',', ':'),
'sort_keys': True,
"indent": 2 if opt.pretty else None,
"separators": (",", ": ") if opt.pretty else (",", ":"),
"sort_keys": True,
}
fd.write(json.dumps(doc, **json_settings))
else:
manifest.Save(fd,
manifest.Save(
fd,
peg_rev=opt.peg_rev,
peg_rev_upstream=opt.peg_rev_upstream,
peg_rev_dest_branch=opt.peg_rev_dest_branch)
if output_file != '-':
peg_rev_dest_branch=opt.peg_rev_dest_branch,
)
if output_file != "-":
fd.close()
if manifest.path_prefix:
print(f'Saved {manifest.path_prefix} submanifest to {output_file}',
file=sys.stderr)
print(
f"Saved {manifest.path_prefix} submanifest to "
f"{output_file}",
file=sys.stderr,
)
else:
print(f'Saved manifest to {output_file}', file=sys.stderr)
print(f"Saved manifest to {output_file}", file=sys.stderr)
def ValidateOptions(self, opt, args):
if args:

View File

@ -34,22 +34,33 @@ are displayed.
"""
def _Options(self, p):
p.add_option('-c', '--current-branch',
dest="current_branch", action="store_true",
help="consider only checked out branches")
p.add_option('--no-current-branch',
dest='current_branch', action='store_false',
help='consider all local branches')
p.add_option(
"-c",
"--current-branch",
dest="current_branch",
action="store_true",
help="consider only checked out branches",
)
p.add_option(
"--no-current-branch",
dest="current_branch",
action="store_false",
help="consider all local branches",
)
# Turn this into a warning & remove this someday.
p.add_option('-b',
dest='current_branch', action='store_true',
help=optparse.SUPPRESS_HELP)
p.add_option(
"-b",
dest="current_branch",
action="store_true",
help=optparse.SUPPRESS_HELP,
)
def Execute(self, opt, args):
all_branches = []
for project in self.GetProjects(args, all_manifests=not opt.this_manifest_only):
br = [project.GetUploadableBranch(x)
for x in project.GetBranches()]
for project in self.GetProjects(
args, all_manifests=not opt.this_manifest_only
):
br = [project.GetUploadableBranch(x) for x in project.GetBranches()]
br = [x for x in br if x]
if opt.current_branch:
br = [x for x in br if x.name == project.CurrentBranch]
@ -60,14 +71,14 @@ are displayed.
class Report(Coloring):
def __init__(self, config):
Coloring.__init__(self, config, 'status')
self.project = self.printer('header', attr='bold')
self.text = self.printer('text')
Coloring.__init__(self, config, "status")
self.project = self.printer("header", attr="bold")
self.text = self.printer("text")
out = Report(all_branches[0].project.config)
out.text("Deprecated. See repo info -o.")
out.nl()
out.project('Projects Overview')
out.project("Projects Overview")
out.nl()
project = None
@ -76,16 +87,23 @@ are displayed.
if project != branch.project:
project = branch.project
out.nl()
out.project('project %s/' % project.RelPath(local=opt.this_manifest_only))
out.project(
"project %s/"
% project.RelPath(local=opt.this_manifest_only)
)
out.nl()
commits = branch.commits
date = branch.date
print('%s %-33s (%2d commit%s, %s)' % (
branch.name == project.CurrentBranch and '*' or ' ',
print(
"%s %-33s (%2d commit%s, %s)"
% (
branch.name == project.CurrentBranch and "*" or " ",
branch.name,
len(commits),
len(commits) != 1 and 's' or ' ',
date))
len(commits) != 1 and "s" or " ",
date,
)
)
for commit in commits:
print('%-35s - %s' % ('', commit))
print("%-35s - %s" % ("", commit))

View File

@ -31,10 +31,12 @@ class Prune(PagedCommand):
return project.PruneHeads()
def Execute(self, opt, args):
projects = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
projects = self.GetProjects(
args, all_manifests=not opt.this_manifest_only
)
# NB: Should be able to refactor this module to display summary as results
# come back from children.
# NB: Should be able to refactor this module to display summary as
# results come back from children.
def _ProcessResults(_pool, _output, results):
return list(itertools.chain.from_iterable(results))
@ -43,18 +45,19 @@ class Prune(PagedCommand):
self._ExecuteOne,
projects,
callback=_ProcessResults,
ordered=True)
ordered=True,
)
if not all_branches:
return
class Report(Coloring):
def __init__(self, config):
Coloring.__init__(self, config, 'status')
self.project = self.printer('header', attr='bold')
Coloring.__init__(self, config, "status")
self.project = self.printer("header", attr="bold")
out = Report(all_branches[0].project.config)
out.project('Pending Branches')
out.project("Pending Branches")
out.nl()
project = None
@ -63,19 +66,29 @@ class Prune(PagedCommand):
if project != branch.project:
project = branch.project
out.nl()
out.project('project %s/' % project.RelPath(local=opt.this_manifest_only))
out.project(
"project %s/"
% project.RelPath(local=opt.this_manifest_only)
)
out.nl()
print('%s %-33s ' % (
branch.name == project.CurrentBranch and '*' or ' ',
branch.name), end='')
print(
"%s %-33s "
% (
branch.name == project.CurrentBranch and "*" or " ",
branch.name,
),
end="",
)
if not branch.base_exists:
print('(ignoring: tracking branch is gone: %s)' % (branch.base,))
print(
"(ignoring: tracking branch is gone: %s)" % (branch.base,)
)
else:
commits = branch.commits
date = branch.date
print('(%2d commit%s, %s)' % (
len(commits),
len(commits) != 1 and 's' or ' ',
date))
print(
"(%2d commit%s, %s)"
% (len(commits), len(commits) != 1 and "s" or " ", date)
)

View File

@ -21,9 +21,9 @@ from git_command import GitCommand
class RebaseColoring(Coloring):
def __init__(self, config):
Coloring.__init__(self, config, 'rebase')
self.project = self.printer('project', attr='bold')
self.fail = self.printer('fail', fg='red')
Coloring.__init__(self, config, "rebase")
self.project = self.printer("project", attr="bold")
self.fail = self.printer("fail", fg="red")
class Rebase(Command):
@ -39,61 +39,98 @@ branch but need to incorporate new upstream changes "underneath" them.
"""
def _Options(self, p):
g = p.get_option_group('--quiet')
g.add_option('-i', '--interactive',
dest="interactive", action="store_true",
help="interactive rebase (single project only)")
g = p.get_option_group("--quiet")
g.add_option(
"-i",
"--interactive",
dest="interactive",
action="store_true",
help="interactive rebase (single project only)",
)
p.add_option('--fail-fast',
dest='fail_fast', action='store_true',
help='stop rebasing after first error is hit')
p.add_option('-f', '--force-rebase',
dest='force_rebase', action='store_true',
help='pass --force-rebase to git rebase')
p.add_option('--no-ff',
dest='ff', default=True, action='store_false',
help='pass --no-ff to git rebase')
p.add_option('--autosquash',
dest='autosquash', action='store_true',
help='pass --autosquash to git rebase')
p.add_option('--whitespace',
dest='whitespace', action='store', metavar='WS',
help='pass --whitespace to git rebase')
p.add_option('--auto-stash',
dest='auto_stash', action='store_true',
help='stash local modifications before starting')
p.add_option('-m', '--onto-manifest',
dest='onto_manifest', action='store_true',
help='rebase onto the manifest version instead of upstream '
'HEAD (this helps to make sure the local tree stays '
'consistent if you previously synced to a manifest)')
p.add_option(
"--fail-fast",
dest="fail_fast",
action="store_true",
help="stop rebasing after first error is hit",
)
p.add_option(
"-f",
"--force-rebase",
dest="force_rebase",
action="store_true",
help="pass --force-rebase to git rebase",
)
p.add_option(
"--no-ff",
dest="ff",
default=True,
action="store_false",
help="pass --no-ff to git rebase",
)
p.add_option(
"--autosquash",
dest="autosquash",
action="store_true",
help="pass --autosquash to git rebase",
)
p.add_option(
"--whitespace",
dest="whitespace",
action="store",
metavar="WS",
help="pass --whitespace to git rebase",
)
p.add_option(
"--auto-stash",
dest="auto_stash",
action="store_true",
help="stash local modifications before starting",
)
p.add_option(
"-m",
"--onto-manifest",
dest="onto_manifest",
action="store_true",
help="rebase onto the manifest version instead of upstream "
"HEAD (this helps to make sure the local tree stays "
"consistent if you previously synced to a manifest)",
)
def Execute(self, opt, args):
all_projects = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
all_projects = self.GetProjects(
args, all_manifests=not opt.this_manifest_only
)
one_project = len(all_projects) == 1
if opt.interactive and not one_project:
print('error: interactive rebase not supported with multiple projects',
file=sys.stderr)
print(
"error: interactive rebase not supported with multiple "
"projects",
file=sys.stderr,
)
if len(args) == 1:
print('note: project %s is mapped to more than one path' % (args[0],),
file=sys.stderr)
print(
"note: project %s is mapped to more than one path"
% (args[0],),
file=sys.stderr,
)
return 1
# Setup the common git rebase args that we use for all projects.
common_args = ['rebase']
common_args = ["rebase"]
if opt.whitespace:
common_args.append('--whitespace=%s' % opt.whitespace)
common_args.append("--whitespace=%s" % opt.whitespace)
if opt.quiet:
common_args.append('--quiet')
common_args.append("--quiet")
if opt.force_rebase:
common_args.append('--force-rebase')
common_args.append("--force-rebase")
if not opt.ff:
common_args.append('--no-ff')
common_args.append("--no-ff")
if opt.autosquash:
common_args.append('--autosquash')
common_args.append("--autosquash")
if opt.interactive:
common_args.append('-i')
common_args.append("-i")
config = self.manifest.manifestProject.config
out = RebaseColoring(config)
@ -108,30 +145,40 @@ branch but need to incorporate new upstream changes "underneath" them.
cb = project.CurrentBranch
if not cb:
if one_project:
print("error: project %s has a detached HEAD" % _RelPath(project),
file=sys.stderr)
print(
"error: project %s has a detached HEAD"
% _RelPath(project),
file=sys.stderr,
)
return 1
# ignore branches with detatched HEADs
# Ignore branches with detached HEADs.
continue
upbranch = project.GetBranch(cb)
if not upbranch.LocalMerge:
if one_project:
print("error: project %s does not track any remote branches"
% _RelPath(project), file=sys.stderr)
print(
"error: project %s does not track any remote branches"
% _RelPath(project),
file=sys.stderr,
)
return 1
# ignore branches without remotes
# Ignore branches without remotes.
continue
args = common_args[:]
if opt.onto_manifest:
args.append('--onto')
args.append("--onto")
args.append(project.revisionExpr)
args.append(upbranch.LocalMerge)
out.project('project %s: rebasing %s -> %s',
_RelPath(project), cb, upbranch.LocalMerge)
out.project(
"project %s: rebasing %s -> %s",
_RelPath(project),
cb,
upbranch.LocalMerge,
)
out.nl()
out.flush()
@ -153,13 +200,13 @@ branch but need to incorporate new upstream changes "underneath" them.
continue
if needs_stash:
stash_args.append('pop')
stash_args.append('--quiet')
stash_args.append("pop")
stash_args.append("--quiet")
if GitCommand(project, stash_args).Wait() != 0:
ret += 1
if ret:
out.fail('%i projects had errors', ret)
out.fail("%i projects had errors", ret)
out.nl()
return ret

View File

@ -35,13 +35,20 @@ need to be performed by an end-user.
"""
def _Options(self, p):
g = p.add_option_group('repo Version options')
g.add_option('--no-repo-verify',
dest='repo_verify', default=True, action='store_false',
help='do not verify repo source code')
g.add_option('--repo-upgraded',
dest='repo_upgraded', action='store_true',
help=SUPPRESS_HELP)
g = p.add_option_group("repo Version options")
g.add_option(
"--no-repo-verify",
dest="repo_verify",
default=True,
action="store_false",
help="do not verify repo source code",
)
g.add_option(
"--repo-upgraded",
dest="repo_upgraded",
action="store_true",
help=SUPPRESS_HELP,
)
def Execute(self, opt, args):
rp = self.manifest.repoProject
@ -55,7 +62,5 @@ need to be performed by an end-user.
print("error: can't update repo", file=sys.stderr)
sys.exit(1)
rp.bare_git.gc('--auto')
_PostRepoFetch(rp,
repo_verify=opt.repo_verify,
verbose=True)
rp.bare_git.gc("--auto")
_PostRepoFetch(rp, repo_verify=opt.repo_verify, verbose=True)

View File

@ -21,10 +21,10 @@ from git_command import GitCommand
class _ProjectList(Coloring):
def __init__(self, gc):
Coloring.__init__(self, gc, 'interactive')
self.prompt = self.printer('prompt', fg='blue', attr='bold')
self.header = self.printer('header', attr='bold')
self.help = self.printer('help', fg='red', attr='bold')
Coloring.__init__(self, gc, "interactive")
self.prompt = self.printer("prompt", fg="blue", attr="bold")
self.header = self.printer("header", attr="bold")
self.help = self.printer("help", fg="red", attr="bold")
class Stage(InteractiveCommand):
@ -38,10 +38,14 @@ The '%prog' command stages files to prepare the next commit.
"""
def _Options(self, p):
g = p.get_option_group('--quiet')
g.add_option('-i', '--interactive',
dest='interactive', action='store_true',
help='use interactive staging')
g = p.get_option_group("--quiet")
g.add_option(
"-i",
"--interactive",
dest="interactive",
action="store_true",
help="use interactive staging",
)
def Execute(self, opt, args):
if opt.interactive:
@ -51,42 +55,49 @@ The '%prog' command stages files to prepare the next commit.
def _Interactive(self, opt, args):
all_projects = [
p for p in self.GetProjects(args, all_manifests=not opt.this_manifest_only)
if p.IsDirty()]
p
for p in self.GetProjects(
args, all_manifests=not opt.this_manifest_only
)
if p.IsDirty()
]
if not all_projects:
print('no projects have uncommitted modifications', file=sys.stderr)
print("no projects have uncommitted modifications", file=sys.stderr)
return
out = _ProjectList(self.manifest.manifestProject.config)
while True:
out.header(' %s', 'project')
out.header(" %s", "project")
out.nl()
for i in range(len(all_projects)):
project = all_projects[i]
out.write('%3d: %s', i + 1,
project.RelPath(local=opt.this_manifest_only) + '/')
out.write(
"%3d: %s",
i + 1,
project.RelPath(local=opt.this_manifest_only) + "/",
)
out.nl()
out.nl()
out.write('%3d: (', 0)
out.prompt('q')
out.write('uit)')
out.write("%3d: (", 0)
out.prompt("q")
out.write("uit)")
out.nl()
out.prompt('project> ')
out.prompt("project> ")
out.flush()
try:
a = sys.stdin.readline()
except KeyboardInterrupt:
out.nl()
break
if a == '':
if a == "":
out.nl()
break
a = a.strip()
if a.lower() in ('q', 'quit', 'exit'):
if a.lower() in ("q", "quit", "exit"):
break
if not a:
continue
@ -104,14 +115,16 @@ The '%prog' command stages files to prepare the next commit.
continue
projects = [
p for p in all_projects
if a in [p.name, p.RelPath(local=opt.this_manifest_only)]]
p
for p in all_projects
if a in [p.name, p.RelPath(local=opt.this_manifest_only)]
]
if len(projects) == 1:
_AddI(projects[0])
continue
print('Bye.')
print("Bye.")
def _AddI(project):
p = GitCommand(project, ['add', '--interactive'], bare=False)
p = GitCommand(project, ["add", "--interactive"], bare=False)
p.Wait()

View File

@ -37,21 +37,34 @@ revision specified in the manifest.
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
def _Options(self, p):
p.add_option('--all',
dest='all', action='store_true',
help='begin branch in all projects')
p.add_option('-r', '--rev', '--revision', dest='revision',
help='point branch at this revision instead of upstream')
p.add_option('--head', '--HEAD',
dest='revision', action='store_const', const='HEAD',
help='abbreviation for --rev HEAD')
p.add_option(
"--all",
dest="all",
action="store_true",
help="begin branch in all projects",
)
p.add_option(
"-r",
"--rev",
"--revision",
dest="revision",
help="point branch at this revision instead of upstream",
)
p.add_option(
"--head",
"--HEAD",
dest="revision",
action="store_const",
const="HEAD",
help="abbreviation for --rev HEAD",
)
def ValidateOptions(self, opt, args):
if not args:
self.Usage()
nb = args[0]
if not git.check_ref_format('heads/%s' % nb):
if not git.check_ref_format("heads/%s" % nb):
self.OptionParser.error("'%s' is not a valid name" % nb)
def _ExecuteOne(self, revision, nb, project):
@ -59,7 +72,7 @@ revision specified in the manifest.
# If the current revision is immutable, such as a SHA1, a tag or
# a change, then we can't push back to it. Substitute with
# dest_branch, if defined; or with manifest default revision instead.
branch_merge = ''
branch_merge = ""
if IsImmutable(project.revisionExpr):
if project.dest_branch:
branch_merge = project.dest_branch
@ -68,9 +81,13 @@ revision specified in the manifest.
try:
ret = project.StartBranch(
nb, branch_merge=branch_merge, revision=revision)
nb, branch_merge=branch_merge, revision=revision
)
except Exception as e:
print('error: unable to checkout %s: %s' % (project.name, e), file=sys.stderr)
print(
"error: unable to checkout %s: %s" % (project.name, e),
file=sys.stderr,
)
ret = False
return (ret, project)
@ -81,17 +98,21 @@ revision specified in the manifest.
if not opt.all:
projects = args[1:]
if len(projects) < 1:
projects = ['.'] # start it in the local project by default
projects = ["."] # start it in the local project by default
all_projects = self.GetProjects(projects,
all_projects = self.GetProjects(
projects,
missing_ok=bool(self.gitc_manifest),
all_manifests=not opt.this_manifest_only)
all_manifests=not opt.this_manifest_only,
)
# This must happen after we find all_projects, since GetProjects may need
# the local directory, which will disappear once we save the GITC manifest.
# This must happen after we find all_projects, since GetProjects may
# need the local directory, which will disappear once we save the GITC
# manifest.
if self.gitc_manifest:
gitc_projects = self.GetProjects(projects, manifest=self.gitc_manifest,
missing_ok=True)
gitc_projects = self.GetProjects(
projects, manifest=self.gitc_manifest, missing_ok=True
)
for project in gitc_projects:
if project.old_revision:
project.already_synced = True
@ -102,17 +123,18 @@ revision specified in the manifest.
# Save the GITC manifest.
gitc_utils.save_manifest(self.gitc_manifest)
# Make sure we have a valid CWD
# Make sure we have a valid CWD.
if not os.path.exists(os.getcwd()):
os.chdir(self.manifest.topdir)
pm = Progress('Syncing %s' % nb, len(all_projects), quiet=opt.quiet)
pm = Progress("Syncing %s" % nb, len(all_projects), quiet=opt.quiet)
for project in all_projects:
gitc_project = self.gitc_manifest.paths[project.relpath]
# Sync projects that have not been opened.
if not gitc_project.already_synced:
proj_localdir = os.path.join(self.gitc_manifest.gitc_client_dir,
project.relpath)
proj_localdir = os.path.join(
self.gitc_manifest.gitc_client_dir, project.relpath
)
project.worktree = proj_localdir
if not os.path.exists(proj_localdir):
os.makedirs(proj_localdir)
@ -124,7 +146,7 @@ revision specified in the manifest.
pm.end()
def _ProcessResults(_pool, pm, results):
for (result, project) in results:
for result, project in results:
if not result:
err.append(project)
pm.update()
@ -134,10 +156,16 @@ revision specified in the manifest.
functools.partial(self._ExecuteOne, opt.revision, nb),
all_projects,
callback=_ProcessResults,
output=Progress('Starting %s' % (nb,), len(all_projects), quiet=opt.quiet))
output=Progress(
"Starting %s" % (nb,), len(all_projects), quiet=opt.quiet
),
)
if err:
for p in err:
print("error: %s/: cannot start %s" % (p.RelPath(local=opt.this_manifest_only), nb),
file=sys.stderr)
print(
"error: %s/: cannot start %s"
% (p.RelPath(local=opt.this_manifest_only), nb),
file=sys.stderr,
)
sys.exit(1)

View File

@ -79,9 +79,14 @@ the following meanings:
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
def _Options(self, p):
p.add_option('-o', '--orphans',
dest='orphans', action='store_true',
help="include objects in working directory outside of repo projects")
p.add_option(
"-o",
"--orphans",
dest="orphans",
action="store_true",
help="include objects in working directory outside of repo "
"projects",
)
def _StatusHelper(self, quiet, local, project):
"""Obtains the status for a specific project.
@ -92,92 +97,106 @@ the following meanings:
Args:
quiet: Where to output the status.
local: a boolean, if True, the path is relative to the local
(sub)manifest. If false, the path is relative to the
outermost manifest.
(sub)manifest. If false, the path is relative to the outermost
manifest.
project: Project to get status of.
Returns:
The status of the project.
"""
buf = io.StringIO()
ret = project.PrintWorkTreeStatus(quiet=quiet, output_redir=buf,
local=local)
ret = project.PrintWorkTreeStatus(
quiet=quiet, output_redir=buf, local=local
)
return (ret, buf.getvalue())
def _FindOrphans(self, dirs, proj_dirs, proj_dirs_parents, outstring):
"""find 'dirs' that are present in 'proj_dirs_parents' but not in 'proj_dirs'"""
status_header = ' --\t'
"""find 'dirs' that are present in 'proj_dirs_parents' but not in 'proj_dirs'""" # noqa: E501
status_header = " --\t"
for item in dirs:
if not platform_utils.isdir(item):
outstring.append(''.join([status_header, item]))
outstring.append("".join([status_header, item]))
continue
if item in proj_dirs:
continue
if item in proj_dirs_parents:
self._FindOrphans(glob.glob('%s/.*' % item) +
glob.glob('%s/*' % item),
proj_dirs, proj_dirs_parents, outstring)
self._FindOrphans(
glob.glob("%s/.*" % item) + glob.glob("%s/*" % item),
proj_dirs,
proj_dirs_parents,
outstring,
)
continue
outstring.append(''.join([status_header, item, '/']))
outstring.append("".join([status_header, item, "/"]))
def Execute(self, opt, args):
all_projects = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
all_projects = self.GetProjects(
args, all_manifests=not opt.this_manifest_only
)
def _ProcessResults(_pool, _output, results):
ret = 0
for (state, output) in results:
for state, output in results:
if output:
print(output, end='')
if state == 'CLEAN':
print(output, end="")
if state == "CLEAN":
ret += 1
return ret
counter = self.ExecuteInParallel(
opt.jobs,
functools.partial(self._StatusHelper, opt.quiet, opt.this_manifest_only),
functools.partial(
self._StatusHelper, opt.quiet, opt.this_manifest_only
),
all_projects,
callback=_ProcessResults,
ordered=True)
ordered=True,
)
if not opt.quiet and len(all_projects) == counter:
print('nothing to commit (working directory clean)')
print("nothing to commit (working directory clean)")
if opt.orphans:
proj_dirs = set()
proj_dirs_parents = set()
for project in self.GetProjects(None, missing_ok=True, all_manifests=not opt.this_manifest_only):
for project in self.GetProjects(
None, missing_ok=True, all_manifests=not opt.this_manifest_only
):
relpath = project.RelPath(local=opt.this_manifest_only)
proj_dirs.add(relpath)
(head, _tail) = os.path.split(relpath)
while head != "":
proj_dirs_parents.add(head)
(head, _tail) = os.path.split(head)
proj_dirs.add('.repo')
proj_dirs.add(".repo")
class StatusColoring(Coloring):
def __init__(self, config):
Coloring.__init__(self, config, 'status')
self.project = self.printer('header', attr='bold')
self.untracked = self.printer('untracked', fg='red')
Coloring.__init__(self, config, "status")
self.project = self.printer("header", attr="bold")
self.untracked = self.printer("untracked", fg="red")
orig_path = os.getcwd()
try:
os.chdir(self.manifest.topdir)
outstring = []
self._FindOrphans(glob.glob('.*') +
glob.glob('*'),
proj_dirs, proj_dirs_parents, outstring)
self._FindOrphans(
glob.glob(".*") + glob.glob("*"),
proj_dirs,
proj_dirs_parents,
outstring,
)
if outstring:
output = StatusColoring(self.client.globalConfig)
output.project('Objects not within a project (orphans)')
output.project("Objects not within a project (orphans)")
output.nl()
for entry in outstring:
output.untracked(entry)
output.nl()
else:
print('No orphan files or directories')
print("No orphan files or directories")
finally:
# Restore CWD.

File diff suppressed because it is too large Load Diff

View File

@ -49,7 +49,7 @@ def _VerifyPendingCommits(branches: List[ReviewableBranch]) -> bool:
#
# Each branch may be configured to have a different threshold.
remote = branch.project.GetBranch(branch.name).remote
key = f'review.{remote.review}.uploadwarningthreshold'
key = f"review.{remote.review}.uploadwarningthreshold"
threshold = branch.project.config.GetInt(key)
if threshold is None:
threshold = _DEFAULT_UNUSUAL_COMMIT_THRESHOLD
@ -62,29 +62,37 @@ def _VerifyPendingCommits(branches: List[ReviewableBranch]) -> bool:
# If any branch has many commits, prompt the user.
if many_commits:
if len(branches) > 1:
print('ATTENTION: One or more branches has an unusually high number '
'of commits.')
print(
"ATTENTION: One or more branches has an unusually high number "
"of commits."
)
else:
print('ATTENTION: You are uploading an unusually high number of commits.')
print('YOU PROBABLY DO NOT MEAN TO DO THIS. (Did you rebase across '
'branches?)')
print(
"ATTENTION: You are uploading an unusually high number of "
"commits."
)
print(
"YOU PROBABLY DO NOT MEAN TO DO THIS. (Did you rebase across "
"branches?)"
)
answer = input(
"If you are sure you intend to do this, type 'yes': ").strip()
return answer == 'yes'
"If you are sure you intend to do this, type 'yes': "
).strip()
return answer == "yes"
return True
def _die(fmt, *args):
msg = fmt % args
print('error: %s' % msg, file=sys.stderr)
print("error: %s" % msg, file=sys.stderr)
sys.exit(1)
def _SplitEmails(values):
result = []
for value in values:
result.extend([s.strip() for s in value.split(',')])
result.extend([s.strip() for s in value.split(",")])
return result
@ -198,80 +206,169 @@ Gerrit Code Review: https://www.gerritcodereview.com/
PARALLEL_JOBS = DEFAULT_LOCAL_JOBS
def _Options(self, p):
p.add_option('-t',
dest='auto_topic', action='store_true',
help='send local branch name to Gerrit Code Review')
p.add_option('--hashtag', '--ht',
dest='hashtags', action='append', default=[],
help='add hashtags (comma delimited) to the review')
p.add_option('--hashtag-branch', '--htb',
action='store_true',
help='add local branch name as a hashtag')
p.add_option('-l', '--label',
dest='labels', action='append', default=[],
help='add a label when uploading')
p.add_option('--re', '--reviewers',
type='string', action='append', dest='reviewers',
help='request reviews from these people')
p.add_option('--cc',
type='string', action='append', dest='cc',
help='also send email to these email addresses')
p.add_option('--br', '--branch',
type='string', action='store', dest='branch',
help='(local) branch to upload')
p.add_option('-c', '--current-branch',
dest='current_branch', action='store_true',
help='upload current git branch')
p.add_option('--no-current-branch',
dest='current_branch', action='store_false',
help='upload all git branches')
# Turn this into a warning & remove this someday.
p.add_option('--cbr',
dest='current_branch', action='store_true',
help=optparse.SUPPRESS_HELP)
p.add_option('--ne', '--no-emails',
action='store_false', dest='notify', default=True,
help='do not send e-mails on upload')
p.add_option('-p', '--private',
action='store_true', dest='private', default=False,
help='upload as a private change (deprecated; use --wip)')
p.add_option('-w', '--wip',
action='store_true', dest='wip', default=False,
help='upload as a work-in-progress change')
p.add_option('-r', '--ready',
action='store_true', default=False,
help='mark change as ready (clears work-in-progress setting)')
p.add_option('-o', '--push-option',
type='string', action='append', dest='push_options',
p.add_option(
"-t",
dest="auto_topic",
action="store_true",
help="send local branch name to Gerrit Code Review",
)
p.add_option(
"--hashtag",
"--ht",
dest="hashtags",
action="append",
default=[],
help='additional push options to transmit')
p.add_option('-D', '--destination', '--dest',
type='string', action='store', dest='dest_branch',
metavar='BRANCH',
help='submit for review on this target branch')
p.add_option('-n', '--dry-run',
dest='dryrun', default=False, action='store_true',
help='do everything except actually upload the CL')
p.add_option('-y', '--yes',
default=False, action='store_true',
help='answer yes to all safe prompts')
p.add_option('--ignore-untracked-files',
action='store_true', default=False,
help='ignore untracked files in the working copy')
p.add_option('--no-ignore-untracked-files',
dest='ignore_untracked_files', action='store_false',
help='always ask about untracked files in the working copy')
p.add_option('--no-cert-checks',
dest='validate_certs', action='store_false', default=True,
help='disable verifying ssl certs (unsafe)')
RepoHook.AddOptionGroup(p, 'pre-upload')
help="add hashtags (comma delimited) to the review",
)
p.add_option(
"--hashtag-branch",
"--htb",
action="store_true",
help="add local branch name as a hashtag",
)
p.add_option(
"-l",
"--label",
dest="labels",
action="append",
default=[],
help="add a label when uploading",
)
p.add_option(
"--re",
"--reviewers",
type="string",
action="append",
dest="reviewers",
help="request reviews from these people",
)
p.add_option(
"--cc",
type="string",
action="append",
dest="cc",
help="also send email to these email addresses",
)
p.add_option(
"--br",
"--branch",
type="string",
action="store",
dest="branch",
help="(local) branch to upload",
)
p.add_option(
"-c",
"--current-branch",
dest="current_branch",
action="store_true",
help="upload current git branch",
)
p.add_option(
"--no-current-branch",
dest="current_branch",
action="store_false",
help="upload all git branches",
)
# Turn this into a warning & remove this someday.
p.add_option(
"--cbr",
dest="current_branch",
action="store_true",
help=optparse.SUPPRESS_HELP,
)
p.add_option(
"--ne",
"--no-emails",
action="store_false",
dest="notify",
default=True,
help="do not send e-mails on upload",
)
p.add_option(
"-p",
"--private",
action="store_true",
dest="private",
default=False,
help="upload as a private change (deprecated; use --wip)",
)
p.add_option(
"-w",
"--wip",
action="store_true",
dest="wip",
default=False,
help="upload as a work-in-progress change",
)
p.add_option(
"-r",
"--ready",
action="store_true",
default=False,
help="mark change as ready (clears work-in-progress setting)",
)
p.add_option(
"-o",
"--push-option",
type="string",
action="append",
dest="push_options",
default=[],
help="additional push options to transmit",
)
p.add_option(
"-D",
"--destination",
"--dest",
type="string",
action="store",
dest="dest_branch",
metavar="BRANCH",
help="submit for review on this target branch",
)
p.add_option(
"-n",
"--dry-run",
dest="dryrun",
default=False,
action="store_true",
help="do everything except actually upload the CL",
)
p.add_option(
"-y",
"--yes",
default=False,
action="store_true",
help="answer yes to all safe prompts",
)
p.add_option(
"--ignore-untracked-files",
action="store_true",
default=False,
help="ignore untracked files in the working copy",
)
p.add_option(
"--no-ignore-untracked-files",
dest="ignore_untracked_files",
action="store_false",
help="always ask about untracked files in the working copy",
)
p.add_option(
"--no-cert-checks",
dest="validate_certs",
action="store_false",
default=True,
help="disable verifying ssl certs (unsafe)",
)
RepoHook.AddOptionGroup(p, "pre-upload")
def _SingleBranch(self, opt, branch, people):
project = branch.project
name = branch.name
remote = project.GetBranch(name).remote
key = 'review.%s.autoupload' % remote.review
key = "review.%s.autoupload" % remote.review
answer = project.config.GetBoolean(key)
if answer is False:
@ -281,25 +378,36 @@ Gerrit Code Review: https://www.gerritcodereview.com/
date = branch.date
commit_list = branch.commits
destination = opt.dest_branch or project.dest_branch or project.revisionExpr
print('Upload project %s/ to remote branch %s%s:' %
(project.RelPath(local=opt.this_manifest_only), destination,
' (private)' if opt.private else ''))
print(' branch %s (%2d commit%s, %s):' % (
destination = (
opt.dest_branch or project.dest_branch or project.revisionExpr
)
print(
"Upload project %s/ to remote branch %s%s:"
% (
project.RelPath(local=opt.this_manifest_only),
destination,
" (private)" if opt.private else "",
)
)
print(
" branch %s (%2d commit%s, %s):"
% (
name,
len(commit_list),
len(commit_list) != 1 and 's' or '',
date))
len(commit_list) != 1 and "s" or "",
date,
)
)
for commit in commit_list:
print(' %s' % commit)
print(" %s" % commit)
print('to %s (y/N)? ' % remote.review, end='', flush=True)
print("to %s (y/N)? " % remote.review, end="", flush=True)
if opt.yes:
print('<--yes>')
print("<--yes>")
answer = True
else:
answer = sys.stdin.readline().strip().lower()
answer = answer in ('y', 'yes', '1', 'true', 't')
answer = answer in ("y", "yes", "1", "true", "t")
if not answer:
_die("upload aborted by user")
@ -314,11 +422,11 @@ Gerrit Code Review: https://www.gerritcodereview.com/
branches = {}
script = []
script.append('# Uncomment the branches to upload:')
script.append("# Uncomment the branches to upload:")
for project, avail in pending:
project_path = project.RelPath(local=opt.this_manifest_only)
script.append('#')
script.append(f'# project {project_path}/:')
script.append("#")
script.append(f"# project {project_path}/:")
b = {}
for branch in avail:
@ -329,26 +437,34 @@ Gerrit Code Review: https://www.gerritcodereview.com/
commit_list = branch.commits
if b:
script.append('#')
destination = opt.dest_branch or project.dest_branch or project.revisionExpr
script.append('# branch %s (%2d commit%s, %s) to remote branch %s:' % (
script.append("#")
destination = (
opt.dest_branch
or project.dest_branch
or project.revisionExpr
)
script.append(
"# branch %s (%2d commit%s, %s) to remote branch %s:"
% (
name,
len(commit_list),
len(commit_list) != 1 and 's' or '',
len(commit_list) != 1 and "s" or "",
date,
destination))
destination,
)
)
for commit in commit_list:
script.append('# %s' % commit)
script.append("# %s" % commit)
b[name] = branch
projects[project_path] = project
branches[project_path] = b
script.append('')
script.append("")
script = Editor.EditString("\n".join(script)).split("\n")
project_re = re.compile(r'^#?\s*project\s*([^\s]+)/:$')
branch_re = re.compile(r'^\s*branch\s*([^\s(]+)\s*\(.*')
project_re = re.compile(r"^#?\s*project\s*([^\s]+)/:$")
branch_re = re.compile(r"^\s*branch\s*([^\s(]+)\s*\(.*")
project = None
todo = []
@ -359,18 +475,18 @@ Gerrit Code Review: https://www.gerritcodereview.com/
name = m.group(1)
project = projects.get(name)
if not project:
_die('project %s not available for upload', name)
_die("project %s not available for upload", name)
continue
m = branch_re.match(line)
if m:
name = m.group(1)
if not project:
_die('project for branch %s not in script', name)
_die("project for branch %s not in script", name)
project_path = project.RelPath(local=opt.this_manifest_only)
branch = branches[project_path].get(name)
if not branch:
_die('branch %s not in %s', name, project_path)
_die("branch %s not in %s", name, project_path)
todo.append(branch)
if not todo:
_die("nothing uncommented for upload")
@ -384,21 +500,21 @@ Gerrit Code Review: https://www.gerritcodereview.com/
def _AppendAutoList(self, branch, people):
"""
Appends the list of reviewers in the git project's config.
Appends the list of users in the CC list in the git project's config if a
non-empty reviewer list was found.
Appends the list of users in the CC list in the git project's config if
a non-empty reviewer list was found.
"""
name = branch.name
project = branch.project
key = 'review.%s.autoreviewer' % project.GetBranch(name).remote.review
key = "review.%s.autoreviewer" % project.GetBranch(name).remote.review
raw_list = project.config.GetString(key)
if raw_list is not None:
people[0].extend([entry.strip() for entry in raw_list.split(',')])
people[0].extend([entry.strip() for entry in raw_list.split(",")])
key = 'review.%s.autocopy' % project.GetBranch(name).remote.review
key = "review.%s.autocopy" % project.GetBranch(name).remote.review
raw_list = project.config.GetString(key)
if raw_list is not None and len(people[0]) > 0:
people[1].extend([entry.strip() for entry in raw_list.split(',')])
people[1].extend([entry.strip() for entry in raw_list.split(",")])
def _FindGerritChange(self, branch):
last_pub = branch.project.WasPublished(branch.name)
@ -408,7 +524,7 @@ Gerrit Code Review: https://www.gerritcodereview.com/
refs = branch.GetPublishedRefs()
try:
# refs/changes/XYZ/N --> XYZ
return refs.get(last_pub).split('/')[-2]
return refs.get(last_pub).split("/")[-2]
except (AttributeError, IndexError):
return ""
@ -419,93 +535,113 @@ Gerrit Code Review: https://www.gerritcodereview.com/
people = copy.deepcopy(original_people)
self._AppendAutoList(branch, people)
# Check if there are local changes that may have been forgotten
# Check if there are local changes that may have been forgotten.
changes = branch.project.UncommitedFiles()
if opt.ignore_untracked_files:
untracked = set(branch.project.UntrackedFiles())
changes = [x for x in changes if x not in untracked]
if changes:
key = 'review.%s.autoupload' % branch.project.remote.review
key = "review.%s.autoupload" % branch.project.remote.review
answer = branch.project.config.GetBoolean(key)
# if they want to auto upload, let's not ask because it could be automated
# If they want to auto upload, let's not ask because it
# could be automated.
if answer is None:
print()
print('Uncommitted changes in %s (did you forget to amend?):'
% branch.project.name)
print('\n'.join(changes))
print('Continue uploading? (y/N) ', end='', flush=True)
print(
"Uncommitted changes in %s (did you forget to "
"amend?):" % branch.project.name
)
print("\n".join(changes))
print("Continue uploading? (y/N) ", end="", flush=True)
if opt.yes:
print('<--yes>')
a = 'yes'
print("<--yes>")
a = "yes"
else:
a = sys.stdin.readline().strip().lower()
if a not in ('y', 'yes', 't', 'true', 'on'):
if a not in ("y", "yes", "t", "true", "on"):
print("skipping upload", file=sys.stderr)
branch.uploaded = False
branch.error = 'User aborted'
branch.error = "User aborted"
continue
# Check if topic branches should be sent to the server during upload
# Check if topic branches should be sent to the server during
# upload.
if opt.auto_topic is not True:
key = 'review.%s.uploadtopic' % branch.project.remote.review
key = "review.%s.uploadtopic" % branch.project.remote.review
opt.auto_topic = branch.project.config.GetBoolean(key)
def _ExpandCommaList(value):
"""Split |value| up into comma delimited entries."""
if not value:
return
for ret in value.split(','):
for ret in value.split(","):
ret = ret.strip()
if ret:
yield ret
# Check if hashtags should be included.
key = 'review.%s.uploadhashtags' % branch.project.remote.review
hashtags = set(_ExpandCommaList(branch.project.config.GetString(key)))
key = "review.%s.uploadhashtags" % branch.project.remote.review
hashtags = set(
_ExpandCommaList(branch.project.config.GetString(key))
)
for tag in opt.hashtags:
hashtags.update(_ExpandCommaList(tag))
if opt.hashtag_branch:
hashtags.add(branch.name)
# Check if labels should be included.
key = 'review.%s.uploadlabels' % branch.project.remote.review
labels = set(_ExpandCommaList(branch.project.config.GetString(key)))
key = "review.%s.uploadlabels" % branch.project.remote.review
labels = set(
_ExpandCommaList(branch.project.config.GetString(key))
)
for label in opt.labels:
labels.update(_ExpandCommaList(label))
# Handle e-mail notifications.
if opt.notify is False:
notify = 'NONE'
notify = "NONE"
else:
key = 'review.%s.uploadnotify' % branch.project.remote.review
key = (
"review.%s.uploadnotify" % branch.project.remote.review
)
notify = branch.project.config.GetString(key)
destination = opt.dest_branch or branch.project.dest_branch
if branch.project.dest_branch and not opt.dest_branch:
merge_branch = self._GetMergeBranch(
branch.project, local_branch=branch.name)
branch.project, local_branch=branch.name
)
full_dest = destination
if not full_dest.startswith(R_HEADS):
full_dest = R_HEADS + full_dest
# If the merge branch of the local branch is different from the
# project's revision AND destination, this might not be intentional.
if (merge_branch and merge_branch != branch.project.revisionExpr
and merge_branch != full_dest):
print(f'For local branch {branch.name}: merge branch '
f'{merge_branch} does not match destination branch '
f'{destination}')
print('skipping upload.')
print(f'Please use `--destination {destination}` if this is intentional')
# If the merge branch of the local branch is different from
# the project's revision AND destination, this might not be
# intentional.
if (
merge_branch
and merge_branch != branch.project.revisionExpr
and merge_branch != full_dest
):
print(
f"For local branch {branch.name}: merge branch "
f"{merge_branch} does not match destination branch "
f"{destination}"
)
print("skipping upload.")
print(
f"Please use `--destination {destination}` if this "
"is intentional"
)
branch.uploaded = False
continue
branch.UploadForReview(people,
branch.UploadForReview(
people,
dryrun=opt.dryrun,
auto_topic=opt.auto_topic,
hashtags=hashtags,
@ -516,7 +652,8 @@ Gerrit Code Review: https://www.gerritcodereview.com/
ready=opt.ready,
dest_branch=destination,
validate_certs=opt.validate_certs,
push_options=opt.push_options)
push_options=opt.push_options,
)
branch.uploaded = True
except UploadError as e:
@ -525,44 +662,58 @@ Gerrit Code Review: https://www.gerritcodereview.com/
have_errors = True
print(file=sys.stderr)
print('----------------------------------------------------------------------', file=sys.stderr)
print("-" * 70, file=sys.stderr)
if have_errors:
for branch in todo:
if not branch.uploaded:
if len(str(branch.error)) <= 30:
fmt = ' (%s)'
fmt = " (%s)"
else:
fmt = '\n (%s)'
print(('[FAILED] %-15s %-15s' + fmt) % (
branch.project.RelPath(local=opt.this_manifest_only) + '/',
fmt = "\n (%s)"
print(
("[FAILED] %-15s %-15s" + fmt)
% (
branch.project.RelPath(local=opt.this_manifest_only)
+ "/",
branch.name,
str(branch.error)),
file=sys.stderr)
str(branch.error),
),
file=sys.stderr,
)
print()
for branch in todo:
if branch.uploaded:
print('[OK ] %-15s %s' % (
branch.project.RelPath(local=opt.this_manifest_only) + '/',
branch.name),
file=sys.stderr)
print(
"[OK ] %-15s %s"
% (
branch.project.RelPath(local=opt.this_manifest_only)
+ "/",
branch.name,
),
file=sys.stderr,
)
if have_errors:
sys.exit(1)
def _GetMergeBranch(self, project, local_branch=None):
if local_branch is None:
p = GitCommand(project,
['rev-parse', '--abbrev-ref', 'HEAD'],
p = GitCommand(
project,
["rev-parse", "--abbrev-ref", "HEAD"],
capture_stdout=True,
capture_stderr=True)
capture_stderr=True,
)
p.Wait()
local_branch = p.stdout.strip()
p = GitCommand(project,
['config', '--get', 'branch.%s.merge' % local_branch],
p = GitCommand(
project,
["config", "--get", "branch.%s.merge" % local_branch],
capture_stdout=True,
capture_stderr=True)
capture_stderr=True,
)
p.Wait()
merge_branch = p.stdout.strip()
return merge_branch
@ -579,19 +730,26 @@ Gerrit Code Review: https://www.gerritcodereview.com/
return (project, avail)
def Execute(self, opt, args):
projects = self.GetProjects(args, all_manifests=not opt.this_manifest_only)
projects = self.GetProjects(
args, all_manifests=not opt.this_manifest_only
)
def _ProcessResults(_pool, _out, results):
pending = []
for result in results:
project, avail = result
if avail is None:
print('repo: error: %s: Unable to upload branch "%s". '
'You might be able to fix the branch by running:\n'
' git branch --set-upstream-to m/%s' %
(project.RelPath(local=opt.this_manifest_only), project.CurrentBranch,
project.manifest.branch),
file=sys.stderr)
print(
'repo: error: %s: Unable to upload branch "%s". '
"You might be able to fix the branch by running:\n"
" git branch --set-upstream-to m/%s"
% (
project.RelPath(local=opt.this_manifest_only),
project.CurrentBranch,
project.manifest.branch,
),
file=sys.stderr,
)
elif avail:
pending.append(result)
return pending
@ -600,29 +758,47 @@ Gerrit Code Review: https://www.gerritcodereview.com/
opt.jobs,
functools.partial(self._GatherOne, opt),
projects,
callback=_ProcessResults)
callback=_ProcessResults,
)
if not pending:
if opt.branch is None:
print('repo: error: no branches ready for upload', file=sys.stderr)
print(
"repo: error: no branches ready for upload", file=sys.stderr
)
else:
print('repo: error: no branches named "%s" ready for upload' %
(opt.branch,), file=sys.stderr)
print(
'repo: error: no branches named "%s" ready for upload'
% (opt.branch,),
file=sys.stderr,
)
return 1
manifests = {project.manifest.topdir: project.manifest
for (project, available) in pending}
manifests = {
project.manifest.topdir: project.manifest
for (project, available) in pending
}
ret = 0
for manifest in manifests.values():
pending_proj_names = [project.name for (project, available) in pending
if project.manifest.topdir == manifest.topdir]
pending_worktrees = [project.worktree for (project, available) in pending
if project.manifest.topdir == manifest.topdir]
pending_proj_names = [
project.name
for (project, available) in pending
if project.manifest.topdir == manifest.topdir
]
pending_worktrees = [
project.worktree
for (project, available) in pending
if project.manifest.topdir == manifest.topdir
]
hook = RepoHook.FromSubcmd(
hook_type='pre-upload', manifest=manifest,
opt=opt, abort_if_user_denies=True)
if not hook.Run(project_list=pending_proj_names,
worktree_list=pending_worktrees):
hook_type="pre-upload",
manifest=manifest,
opt=opt,
abort_if_user_denies=True,
)
if not hook.Run(
project_list=pending_proj_names, worktree_list=pending_worktrees
):
ret = 1
if ret:
return ret

View File

@ -34,33 +34,40 @@ class Version(Command, MirrorSafeCommand):
def Execute(self, opt, args):
rp = self.manifest.repoProject
rem = rp.GetRemote()
branch = rp.GetBranch('default')
branch = rp.GetBranch("default")
# These might not be the same. Report them both.
src_ver = RepoSourceVersion()
rp_ver = rp.bare_git.describe(HEAD)
print('repo version %s' % rp_ver)
print(' (from %s)' % rem.url)
print(' (tracking %s)' % branch.merge)
print(' (%s)' % rp.bare_git.log('-1', '--format=%cD', HEAD))
print("repo version %s" % rp_ver)
print(" (from %s)" % rem.url)
print(" (tracking %s)" % branch.merge)
print(" (%s)" % rp.bare_git.log("-1", "--format=%cD", HEAD))
if self.wrapper_path is not None:
print('repo launcher version %s' % self.wrapper_version)
print(' (from %s)' % self.wrapper_path)
print("repo launcher version %s" % self.wrapper_version)
print(" (from %s)" % self.wrapper_path)
if src_ver != rp_ver:
print(' (currently at %s)' % src_ver)
print(" (currently at %s)" % src_ver)
print('repo User-Agent %s' % user_agent.repo)
print('git %s' % git.version_tuple().full)
print('git User-Agent %s' % user_agent.git)
print('Python %s' % sys.version)
print("repo User-Agent %s" % user_agent.repo)
print("git %s" % git.version_tuple().full)
print("git User-Agent %s" % user_agent.git)
print("Python %s" % sys.version)
uname = platform.uname()
if sys.version_info.major < 3:
# Python 3 returns a named tuple, but Python 2 is simpler.
print(uname)
else:
print('OS %s %s (%s)' % (uname.system, uname.release, uname.version))
print('CPU %s (%s)' %
(uname.machine, uname.processor if uname.processor else 'unknown'))
print('Bug reports:', Wrapper().BUG_URL)
print(
"OS %s %s (%s)" % (uname.system, uname.release, uname.version)
)
print(
"CPU %s (%s)"
% (
uname.machine,
uname.processor if uname.processor else "unknown",
)
)
print("Bug reports:", Wrapper().BUG_URL)

View File

@ -21,5 +21,5 @@ import repo_trace
@pytest.fixture(autouse=True)
def disable_repo_trace(tmp_path):
"""Set an environment marker to relax certain strict checks for test code."""
repo_trace._TRACE_FILE = str(tmp_path / 'TRACE_FILE_from_test')
"""Set an environment marker to relax certain strict checks for test code.""" # noqa: E501
repo_trace._TRACE_FILE = str(tmp_path / "TRACE_FILE_from_test")

View File

@ -38,8 +38,8 @@ class GetEditor(EditorTestCase):
def test_basic(self):
"""Basic checking of _GetEditor."""
self.setEditor(':')
self.assertEqual(':', Editor._GetEditor())
self.setEditor(":")
self.assertEqual(":", Editor._GetEditor())
class EditString(EditorTestCase):
@ -47,10 +47,10 @@ class EditString(EditorTestCase):
def test_no_editor(self):
"""Check behavior when no editor is available."""
self.setEditor(':')
self.assertEqual('foo', Editor.EditString('foo'))
self.setEditor(":")
self.assertEqual("foo", Editor.EditString("foo"))
def test_cat_editor(self):
"""Check behavior when editor is `cat`."""
self.setEditor('cat')
self.assertEqual('foo', Editor.EditString('foo'))
self.setEditor("cat")
self.assertEqual("foo", Editor.EditString("foo"))

View File

@ -47,7 +47,9 @@ class PickleTests(unittest.TestCase):
try:
newobj = pickle.loads(p)
except Exception as e: # pylint: disable=broad-except
self.fail('Class %s is unable to be pickled: %s\n'
'Incomplete super().__init__(...) call?' % (cls, e))
self.fail(
"Class %s is unable to be pickled: %s\n"
"Incomplete super().__init__(...) call?" % (cls, e)
)
self.assertIsInstance(newobj, cls)
self.assertEqual(str(obj), str(newobj))

View File

@ -31,32 +31,38 @@ class GitCommandTest(unittest.TestCase):
"""Tests the GitCommand class (via git_command.git)."""
def setUp(self):
def realpath_mock(val):
return val
mock.patch.object(os.path, 'realpath', side_effect=realpath_mock).start()
mock.patch.object(
os.path, "realpath", side_effect=realpath_mock
).start()
def tearDown(self):
mock.patch.stopall()
def test_alternative_setting_when_matching(self):
r = git_command._build_env(
objdir = os.path.join('zap', 'objects'),
gitdir = 'zap'
objdir=os.path.join("zap", "objects"), gitdir="zap"
)
self.assertIsNone(r.get('GIT_ALTERNATE_OBJECT_DIRECTORIES'))
self.assertEqual(r.get('GIT_OBJECT_DIRECTORY'), os.path.join('zap', 'objects'))
self.assertIsNone(r.get("GIT_ALTERNATE_OBJECT_DIRECTORIES"))
self.assertEqual(
r.get("GIT_OBJECT_DIRECTORY"), os.path.join("zap", "objects")
)
def test_alternative_setting_when_different(self):
r = git_command._build_env(
objdir = os.path.join('wow', 'objects'),
gitdir = 'zap'
objdir=os.path.join("wow", "objects"), gitdir="zap"
)
self.assertEqual(r.get('GIT_ALTERNATE_OBJECT_DIRECTORIES'), os.path.join('zap', 'objects'))
self.assertEqual(r.get('GIT_OBJECT_DIRECTORY'), os.path.join('wow', 'objects'))
self.assertEqual(
r.get("GIT_ALTERNATE_OBJECT_DIRECTORIES"),
os.path.join("zap", "objects"),
)
self.assertEqual(
r.get("GIT_OBJECT_DIRECTORY"), os.path.join("wow", "objects")
)
class GitCallUnitTest(unittest.TestCase):
@ -68,8 +74,8 @@ class GitCallUnitTest(unittest.TestCase):
self.assertIsNotNone(ver)
# We don't dive too deep into the values here to avoid having to update
# whenever git versions change. We do check relative to this min version
# as this is what `repo` itself requires via MIN_GIT_VERSION.
# whenever git versions change. We do check relative to this min
# version as this is what `repo` itself requires via MIN_GIT_VERSION.
MIN_GIT_VERSION = (2, 10, 2)
self.assertTrue(isinstance(ver.major, int))
self.assertTrue(isinstance(ver.minor, int))
@ -82,7 +88,7 @@ class GitCallUnitTest(unittest.TestCase):
self.assertGreaterEqual(ver, MIN_GIT_VERSION)
self.assertLess(ver, (9999, 9999, 9999))
self.assertNotEqual('', ver.full)
self.assertNotEqual("", ver.full)
class UserAgentUnitTest(unittest.TestCase):
@ -91,25 +97,25 @@ class UserAgentUnitTest(unittest.TestCase):
def test_smoke_os(self):
"""Make sure UA OS setting returns something useful."""
os_name = git_command.user_agent.os
# We can't dive too deep because of OS/tool differences, but we can check
# the general form.
m = re.match(r'^[^ ]+$', os_name)
# We can't dive too deep because of OS/tool differences, but we can
# check the general form.
m = re.match(r"^[^ ]+$", os_name)
self.assertIsNotNone(m)
def test_smoke_repo(self):
"""Make sure repo UA returns something useful."""
ua = git_command.user_agent.repo
# We can't dive too deep because of OS/tool differences, but we can check
# the general form.
m = re.match(r'^git-repo/[^ ]+ ([^ ]+) git/[^ ]+ Python/[0-9.]+', ua)
# We can't dive too deep because of OS/tool differences, but we can
# check the general form.
m = re.match(r"^git-repo/[^ ]+ ([^ ]+) git/[^ ]+ Python/[0-9.]+", ua)
self.assertIsNotNone(m)
def test_smoke_git(self):
"""Make sure git UA returns something useful."""
ua = git_command.user_agent.git
# We can't dive too deep because of OS/tool differences, but we can check
# the general form.
m = re.match(r'^git/[^ ]+ ([^ ]+) git-repo/[^ ]+', ua)
# We can't dive too deep because of OS/tool differences, but we can
# check the general form.
m = re.match(r"^git/[^ ]+ ([^ ]+) git-repo/[^ ]+", ua)
self.assertIsNotNone(m)
@ -119,7 +125,9 @@ class GitRequireTests(unittest.TestCase):
def setUp(self):
self.wrapper = wrapper.Wrapper()
ver = self.wrapper.GitVersion(1, 2, 3, 4)
mock.patch.object(git_command.git, 'version_tuple', return_value=ver).start()
mock.patch.object(
git_command.git, "version_tuple", return_value=ver
).start()
def tearDown(self):
mock.patch.stopall()
@ -152,5 +160,5 @@ class GitRequireTests(unittest.TestCase):
def test_older_fatal_msg(self):
"""Test fatal require calls with old versions and message."""
with self.assertRaises(SystemExit) as e:
git_command.git_require((2,), fail=True, msg='so sad')
git_command.git_require((2,), fail=True, msg="so sad")
self.assertNotEqual(0, e.code)

View File

@ -22,18 +22,16 @@ import git_config
def fixture(*paths):
"""Return a path relative to test/fixtures.
"""
return os.path.join(os.path.dirname(__file__), 'fixtures', *paths)
"""Return a path relative to test/fixtures."""
return os.path.join(os.path.dirname(__file__), "fixtures", *paths)
class GitConfigReadOnlyTests(unittest.TestCase):
"""Read-only tests of the GitConfig class."""
def setUp(self):
"""Create a GitConfig object using the test.gitconfig fixture.
"""
config_fixture = fixture('test.gitconfig')
"""Create a GitConfig object using the test.gitconfig fixture."""
config_fixture = fixture("test.gitconfig")
self.config = git_config.GitConfig(config_fixture)
def test_GetString_with_empty_config_values(self):
@ -44,7 +42,7 @@ class GitConfigReadOnlyTests(unittest.TestCase):
empty
"""
val = self.config.GetString('section.empty')
val = self.config.GetString("section.empty")
self.assertEqual(val, None)
def test_GetString_with_true_value(self):
@ -55,54 +53,54 @@ class GitConfigReadOnlyTests(unittest.TestCase):
nonempty = true
"""
val = self.config.GetString('section.nonempty')
self.assertEqual(val, 'true')
val = self.config.GetString("section.nonempty")
self.assertEqual(val, "true")
def test_GetString_from_missing_file(self):
"""
Test missing config file
"""
config_fixture = fixture('not.present.gitconfig')
config_fixture = fixture("not.present.gitconfig")
config = git_config.GitConfig(config_fixture)
val = config.GetString('empty')
val = config.GetString("empty")
self.assertEqual(val, None)
def test_GetBoolean_undefined(self):
"""Test GetBoolean on key that doesn't exist."""
self.assertIsNone(self.config.GetBoolean('section.missing'))
self.assertIsNone(self.config.GetBoolean("section.missing"))
def test_GetBoolean_invalid(self):
"""Test GetBoolean on invalid boolean value."""
self.assertIsNone(self.config.GetBoolean('section.boolinvalid'))
self.assertIsNone(self.config.GetBoolean("section.boolinvalid"))
def test_GetBoolean_true(self):
"""Test GetBoolean on valid true boolean."""
self.assertTrue(self.config.GetBoolean('section.booltrue'))
self.assertTrue(self.config.GetBoolean("section.booltrue"))
def test_GetBoolean_false(self):
"""Test GetBoolean on valid false boolean."""
self.assertFalse(self.config.GetBoolean('section.boolfalse'))
self.assertFalse(self.config.GetBoolean("section.boolfalse"))
def test_GetInt_undefined(self):
"""Test GetInt on key that doesn't exist."""
self.assertIsNone(self.config.GetInt('section.missing'))
self.assertIsNone(self.config.GetInt("section.missing"))
def test_GetInt_invalid(self):
"""Test GetInt on invalid integer value."""
self.assertIsNone(self.config.GetBoolean('section.intinvalid'))
self.assertIsNone(self.config.GetBoolean("section.intinvalid"))
def test_GetInt_valid(self):
"""Test GetInt on valid integers."""
TESTS = (
('inthex', 16),
('inthexk', 16384),
('int', 10),
('intk', 10240),
('intm', 10485760),
('intg', 10737418240),
("inthex", 16),
("inthexk", 16384),
("int", 10),
("intk", 10240),
("intm", 10485760),
("intg", 10737418240),
)
for key, value in TESTS:
self.assertEqual(value, self.config.GetInt('section.%s' % (key,)))
self.assertEqual(value, self.config.GetInt("section.%s" % (key,)))
class GitConfigReadWriteTests(unittest.TestCase):
@ -119,70 +117,74 @@ class GitConfigReadWriteTests(unittest.TestCase):
def test_SetString(self):
"""Test SetString behavior."""
# Set a value.
self.assertIsNone(self.config.GetString('foo.bar'))
self.config.SetString('foo.bar', 'val')
self.assertEqual('val', self.config.GetString('foo.bar'))
self.assertIsNone(self.config.GetString("foo.bar"))
self.config.SetString("foo.bar", "val")
self.assertEqual("val", self.config.GetString("foo.bar"))
# Make sure the value was actually written out.
config = self.get_config()
self.assertEqual('val', config.GetString('foo.bar'))
self.assertEqual("val", config.GetString("foo.bar"))
# Update the value.
self.config.SetString('foo.bar', 'valll')
self.assertEqual('valll', self.config.GetString('foo.bar'))
self.config.SetString("foo.bar", "valll")
self.assertEqual("valll", self.config.GetString("foo.bar"))
config = self.get_config()
self.assertEqual('valll', config.GetString('foo.bar'))
self.assertEqual("valll", config.GetString("foo.bar"))
# Delete the value.
self.config.SetString('foo.bar', None)
self.assertIsNone(self.config.GetString('foo.bar'))
self.config.SetString("foo.bar", None)
self.assertIsNone(self.config.GetString("foo.bar"))
config = self.get_config()
self.assertIsNone(config.GetString('foo.bar'))
self.assertIsNone(config.GetString("foo.bar"))
def test_SetBoolean(self):
"""Test SetBoolean behavior."""
# Set a true value.
self.assertIsNone(self.config.GetBoolean('foo.bar'))
self.assertIsNone(self.config.GetBoolean("foo.bar"))
for val in (True, 1):
self.config.SetBoolean('foo.bar', val)
self.assertTrue(self.config.GetBoolean('foo.bar'))
self.config.SetBoolean("foo.bar", val)
self.assertTrue(self.config.GetBoolean("foo.bar"))
# Make sure the value was actually written out.
config = self.get_config()
self.assertTrue(config.GetBoolean('foo.bar'))
self.assertEqual('true', config.GetString('foo.bar'))
self.assertTrue(config.GetBoolean("foo.bar"))
self.assertEqual("true", config.GetString("foo.bar"))
# Set a false value.
for val in (False, 0):
self.config.SetBoolean('foo.bar', val)
self.assertFalse(self.config.GetBoolean('foo.bar'))
self.config.SetBoolean("foo.bar", val)
self.assertFalse(self.config.GetBoolean("foo.bar"))
# Make sure the value was actually written out.
config = self.get_config()
self.assertFalse(config.GetBoolean('foo.bar'))
self.assertEqual('false', config.GetString('foo.bar'))
self.assertFalse(config.GetBoolean("foo.bar"))
self.assertEqual("false", config.GetString("foo.bar"))
# Delete the value.
self.config.SetBoolean('foo.bar', None)
self.assertIsNone(self.config.GetBoolean('foo.bar'))
self.config.SetBoolean("foo.bar", None)
self.assertIsNone(self.config.GetBoolean("foo.bar"))
config = self.get_config()
self.assertIsNone(config.GetBoolean('foo.bar'))
self.assertIsNone(config.GetBoolean("foo.bar"))
def test_GetSyncAnalysisStateData(self):
"""Test config entries with a sync state analysis data."""
superproject_logging_data = {}
superproject_logging_data['test'] = False
options = type('options', (object,), {})()
options.verbose = 'true'
options.mp_update = 'false'
superproject_logging_data["test"] = False
options = type("options", (object,), {})()
options.verbose = "true"
options.mp_update = "false"
TESTS = (
('superproject.test', 'false'),
('options.verbose', 'true'),
('options.mpupdate', 'false'),
('main.version', '1'),
("superproject.test", "false"),
("options.verbose", "true"),
("options.mpupdate", "false"),
("main.version", "1"),
)
self.config.UpdateSyncAnalysisState(options, superproject_logging_data)
sync_data = self.config.GetSyncAnalysisStateData()
for key, value in TESTS:
self.assertEqual(sync_data[f'{git_config.SYNC_STATE_PREFIX}{key}'], value)
self.assertTrue(sync_data[f'{git_config.SYNC_STATE_PREFIX}main.synctime'])
self.assertEqual(
sync_data[f"{git_config.SYNC_STATE_PREFIX}{key}"], value
)
self.assertTrue(
sync_data[f"{git_config.SYNC_STATE_PREFIX}main.synctime"]
)

View File

@ -30,18 +30,19 @@ from test_manifest_xml import sort_attributes
class SuperprojectTestCase(unittest.TestCase):
"""TestCase for the Superproject module."""
PARENT_SID_KEY = 'GIT_TRACE2_PARENT_SID'
PARENT_SID_VALUE = 'parent_sid'
SELF_SID_REGEX = r'repo-\d+T\d+Z-.*'
FULL_SID_REGEX = r'^%s/%s' % (PARENT_SID_VALUE, SELF_SID_REGEX)
PARENT_SID_KEY = "GIT_TRACE2_PARENT_SID"
PARENT_SID_VALUE = "parent_sid"
SELF_SID_REGEX = r"repo-\d+T\d+Z-.*"
FULL_SID_REGEX = r"^%s/%s" % (PARENT_SID_VALUE, SELF_SID_REGEX)
def setUp(self):
"""Set up superproject every time."""
self.tempdirobj = tempfile.TemporaryDirectory(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.repodir, manifest_xml.MANIFEST_FILE_NAME)
self.repodir, manifest_xml.MANIFEST_FILE_NAME
)
os.mkdir(self.repodir)
self.platform = platform.system().lower()
@ -53,25 +54,35 @@ class SuperprojectTestCase(unittest.TestCase):
self.git_event_log = git_trace2_event_log.EventLog(env=env)
# The manifest parsing really wants a git repo currently.
gitdir = os.path.join(self.repodir, 'manifests.git')
gitdir = os.path.join(self.repodir, "manifests.git")
os.mkdir(gitdir)
with open(os.path.join(gitdir, 'config'), 'w') as fp:
fp.write("""[remote "origin"]
with open(os.path.join(gitdir, "config"), "w") as fp:
fp.write(
"""[remote "origin"]
url = https://localhost:0/manifest
""")
"""
)
manifest = self.getXmlManifest("""
manifest = self.getXmlManifest(
"""
<manifest>
<remote name="default-remote" fetch="http://localhost" />
<default remote="default-remote" revision="refs/heads/main" />
<superproject name="superproject"/>
<project path="art" name="platform/art" groups="notdefault,platform-""" + self.platform + """
<project path="art" name="platform/art" groups="notdefault,platform-"""
+ self.platform
+ """
" /></manifest>
""")
"""
)
self._superproject = git_superproject.Superproject(
manifest, name='superproject',
remote=manifest.remotes.get('default-remote').ToRemoteSpec('superproject'),
revision='refs/heads/main')
manifest,
name="superproject",
remote=manifest.remotes.get("default-remote").ToRemoteSpec(
"superproject"
),
revision="refs/heads/main",
)
def tearDown(self):
"""Tear down superproject every time."""
@ -79,29 +90,29 @@ class SuperprojectTestCase(unittest.TestCase):
def getXmlManifest(self, data):
"""Helper to initialize a manifest for testing."""
with open(self.manifest_file, 'w') as fp:
with open(self.manifest_file, "w") as fp:
fp.write(data)
return manifest_xml.XmlManifest(self.repodir, self.manifest_file)
def verifyCommonKeys(self, log_entry, expected_event_name, full_sid=True):
"""Helper function to verify common event log keys."""
self.assertIn('event', log_entry)
self.assertIn('sid', log_entry)
self.assertIn('thread', log_entry)
self.assertIn('time', log_entry)
self.assertIn("event", log_entry)
self.assertIn("sid", log_entry)
self.assertIn("thread", log_entry)
self.assertIn("time", log_entry)
# Do basic data format validation.
self.assertEqual(expected_event_name, log_entry['event'])
self.assertEqual(expected_event_name, log_entry["event"])
if full_sid:
self.assertRegex(log_entry['sid'], self.FULL_SID_REGEX)
self.assertRegex(log_entry["sid"], self.FULL_SID_REGEX)
else:
self.assertRegex(log_entry['sid'], self.SELF_SID_REGEX)
self.assertRegex(log_entry['time'], r'^\d+-\d+-\d+T\d+:\d+:\d+\.\d+Z$')
self.assertRegex(log_entry["sid"], self.SELF_SID_REGEX)
self.assertRegex(log_entry["time"], r"^\d+-\d+-\d+T\d+:\d+:\d+\.\d+Z$")
def readLog(self, log_path):
"""Helper function to read log data into a list."""
log_data = []
with open(log_path, mode='rb') as f:
with open(log_path, mode="rb") as f:
for line in f:
log_data.append(json.loads(line))
return log_data
@ -109,57 +120,71 @@ class SuperprojectTestCase(unittest.TestCase):
def verifyErrorEvent(self):
"""Helper to verify that error event is written."""
with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
log_path = self.git_event_log.Write(path=tempdir)
self.log_data = self.readLog(log_path)
self.assertEqual(len(self.log_data), 2)
error_event = self.log_data[1]
self.verifyCommonKeys(self.log_data[0], expected_event_name='version')
self.verifyCommonKeys(error_event, expected_event_name='error')
self.verifyCommonKeys(self.log_data[0], expected_event_name="version")
self.verifyCommonKeys(error_event, expected_event_name="error")
# Check for 'error' event specific fields.
self.assertIn('msg', error_event)
self.assertIn('fmt', error_event)
self.assertIn("msg", error_event)
self.assertIn("fmt", error_event)
def test_superproject_get_superproject_no_superproject(self):
"""Test with no url."""
manifest = self.getXmlManifest("""
manifest = self.getXmlManifest(
"""
<manifest>
</manifest>
""")
"""
)
self.assertIsNone(manifest.superproject)
def test_superproject_get_superproject_invalid_url(self):
"""Test with an invalid url."""
manifest = self.getXmlManifest("""
manifest = self.getXmlManifest(
"""
<manifest>
<remote name="test-remote" fetch="localhost" />
<default remote="test-remote" revision="refs/heads/main" />
<superproject name="superproject"/>
</manifest>
""")
"""
)
superproject = git_superproject.Superproject(
manifest, name='superproject',
remote=manifest.remotes.get('test-remote').ToRemoteSpec('superproject'),
revision='refs/heads/main')
manifest,
name="superproject",
remote=manifest.remotes.get("test-remote").ToRemoteSpec(
"superproject"
),
revision="refs/heads/main",
)
sync_result = superproject.Sync(self.git_event_log)
self.assertFalse(sync_result.success)
self.assertTrue(sync_result.fatal)
def test_superproject_get_superproject_invalid_branch(self):
"""Test with an invalid branch."""
manifest = self.getXmlManifest("""
manifest = self.getXmlManifest(
"""
<manifest>
<remote name="test-remote" fetch="localhost" />
<default remote="test-remote" revision="refs/heads/main" />
<superproject name="superproject"/>
</manifest>
""")
"""
)
self._superproject = git_superproject.Superproject(
manifest, name='superproject',
remote=manifest.remotes.get('test-remote').ToRemoteSpec('superproject'),
revision='refs/heads/main')
with mock.patch.object(self._superproject, '_branch', 'junk'):
manifest,
name="superproject",
remote=manifest.remotes.get("test-remote").ToRemoteSpec(
"superproject"
),
revision="refs/heads/main",
)
with mock.patch.object(self._superproject, "_branch", "junk"):
sync_result = self._superproject.Sync(self.git_event_log)
self.assertFalse(sync_result.success)
self.assertTrue(sync_result.fatal)
@ -167,48 +192,61 @@ class SuperprojectTestCase(unittest.TestCase):
def test_superproject_get_superproject_mock_init(self):
"""Test with _Init failing."""
with mock.patch.object(self._superproject, '_Init', return_value=False):
with mock.patch.object(self._superproject, "_Init", return_value=False):
sync_result = self._superproject.Sync(self.git_event_log)
self.assertFalse(sync_result.success)
self.assertTrue(sync_result.fatal)
def test_superproject_get_superproject_mock_fetch(self):
"""Test with _Fetch failing."""
with mock.patch.object(self._superproject, '_Init', return_value=True):
with mock.patch.object(self._superproject, "_Init", return_value=True):
os.mkdir(self._superproject._superproject_path)
with mock.patch.object(self._superproject, '_Fetch', return_value=False):
with mock.patch.object(
self._superproject, "_Fetch", return_value=False
):
sync_result = self._superproject.Sync(self.git_event_log)
self.assertFalse(sync_result.success)
self.assertTrue(sync_result.fatal)
def test_superproject_get_all_project_commit_ids_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, '_Init', return_value=True):
with mock.patch.object(self._superproject, '_Fetch', return_value=True):
with mock.patch.object(self._superproject, '_LsTree', return_value=data):
commit_ids_result = self._superproject._GetAllProjectsCommitIds()
self.assertEqual(commit_ids_result.commit_ids, {
'art': '2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea',
'bootable/recovery': 'e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06',
'build/bazel': 'ade9b7a0d874e25fff4bf2552488825c6f111928'
})
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, "_Init", return_value=True):
with mock.patch.object(
self._superproject, "_Fetch", return_value=True
):
with mock.patch.object(
self._superproject, "_LsTree", return_value=data
):
commit_ids_result = (
self._superproject._GetAllProjectsCommitIds()
)
self.assertEqual(
commit_ids_result.commit_ids,
{
"art": "2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea",
"bootable/recovery": "e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06",
"build/bazel": "ade9b7a0d874e25fff4bf2552488825c6f111928",
},
)
self.assertFalse(commit_ids_result.fatal)
def test_superproject_write_manifest_file(self):
"""Test with writing manifest to a file after setting revisionId."""
self.assertEqual(len(self._superproject._manifest.projects), 1)
project = self._superproject._manifest.projects[0]
project.SetRevisionId('ABCDEF')
project.SetRevisionId("ABCDEF")
# Create temporary directory so that it can write the file.
os.mkdir(self._superproject._superproject_path)
manifest_path = self._superproject._WriteManifestFile()
self.assertIsNotNone(manifest_path)
with open(manifest_path, 'r') as fp:
with open(manifest_path, "r") as fp:
manifest_xml_data = fp.read()
self.assertEqual(
sort_attributes(manifest_xml_data),
@ -218,46 +256,58 @@ class SuperprojectTestCase(unittest.TestCase):
'<project groups="notdefault,platform-' + self.platform + '" '
'name="platform/art" path="art" revision="ABCDEF" upstream="refs/heads/main"/>'
'<superproject name="superproject"/>'
'</manifest>')
"</manifest>",
)
def test_superproject_update_project_revision_id(self):
"""Test with LsTree being a mock."""
self.assertEqual(len(self._superproject._manifest.projects), 1)
projects = self._superproject._manifest.projects
data = ('160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00'
'160000 commit e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06\tbootable/recovery\x00')
with mock.patch.object(self._superproject, '_Init', return_value=True):
with mock.patch.object(self._superproject, '_Fetch', return_value=True):
with mock.patch.object(self._superproject,
'_LsTree',
return_value=data):
data = (
"160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00"
"160000 commit e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06\tbootable/recovery\x00"
)
with mock.patch.object(self._superproject, "_Init", return_value=True):
with mock.patch.object(
self._superproject, "_Fetch", return_value=True
):
with mock.patch.object(
self._superproject, "_LsTree", return_value=data
):
# Create temporary directory so that it can write the file.
os.mkdir(self._superproject._superproject_path)
update_result = self._superproject.UpdateProjectsRevisionId(projects, self.git_event_log)
update_result = self._superproject.UpdateProjectsRevisionId(
projects, self.git_event_log
)
self.assertIsNotNone(update_result.manifest_path)
self.assertFalse(update_result.fatal)
with open(update_result.manifest_path, 'r') as fp:
with open(update_result.manifest_path, "r") as fp:
manifest_xml_data = fp.read()
self.assertEqual(
sort_attributes(manifest_xml_data),
'<?xml version="1.0" ?><manifest>'
'<remote fetch="http://localhost" name="default-remote"/>'
'<default remote="default-remote" revision="refs/heads/main"/>'
'<project groups="notdefault,platform-' + self.platform + '" '
'<project groups="notdefault,platform-'
+ self.platform
+ '" '
'name="platform/art" path="art" '
'revision="2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea" upstream="refs/heads/main"/>'
'<superproject name="superproject"/>'
'</manifest>')
"</manifest>",
)
def test_superproject_update_project_revision_id_no_superproject_tag(self):
"""Test update of commit ids of a manifest without superproject tag."""
manifest = self.getXmlManifest("""
manifest = self.getXmlManifest(
"""
<manifest>
<remote name="default-remote" fetch="http://localhost" />
<default remote="default-remote" revision="refs/heads/main" />
<project name="test-name"/>
</manifest>
""")
"""
)
self.maxDiff = None
self.assertIsNone(manifest.superproject)
self.assertEqual(
@ -266,59 +316,81 @@ class SuperprojectTestCase(unittest.TestCase):
'<remote fetch="http://localhost" name="default-remote"/>'
'<default remote="default-remote" revision="refs/heads/main"/>'
'<project name="test-name"/>'
'</manifest>')
"</manifest>",
)
def test_superproject_update_project_revision_id_from_local_manifest_group(self):
def test_superproject_update_project_revision_id_from_local_manifest_group(
self,
):
"""Test update of commit ids of a manifest that have local manifest no superproject group."""
local_group = manifest_xml.LOCAL_MANIFEST_GROUP_PREFIX + ':local'
manifest = self.getXmlManifest("""
local_group = manifest_xml.LOCAL_MANIFEST_GROUP_PREFIX + ":local"
manifest = self.getXmlManifest(
"""
<manifest>
<remote name="default-remote" fetch="http://localhost" />
<remote name="goog" fetch="http://localhost2" />
<default remote="default-remote" revision="refs/heads/main" />
<superproject name="superproject"/>
<project path="vendor/x" name="platform/vendor/x" remote="goog"
groups=\"""" + local_group + """
groups=\""""
+ local_group
+ """
" revision="master-with-vendor" clone-depth="1" />
<project path="art" name="platform/art" groups="notdefault,platform-""" + self.platform + """
<project path="art" name="platform/art" groups="notdefault,platform-"""
+ self.platform
+ """
" /></manifest>
""")
"""
)
self.maxDiff = None
self._superproject = git_superproject.Superproject(
manifest, name='superproject',
remote=manifest.remotes.get('default-remote').ToRemoteSpec('superproject'),
revision='refs/heads/main')
manifest,
name="superproject",
remote=manifest.remotes.get("default-remote").ToRemoteSpec(
"superproject"
),
revision="refs/heads/main",
)
self.assertEqual(len(self._superproject._manifest.projects), 2)
projects = self._superproject._manifest.projects
data = ('160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00')
with mock.patch.object(self._superproject, '_Init', return_value=True):
with mock.patch.object(self._superproject, '_Fetch', return_value=True):
with mock.patch.object(self._superproject,
'_LsTree',
return_value=data):
data = "160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00"
with mock.patch.object(self._superproject, "_Init", return_value=True):
with mock.patch.object(
self._superproject, "_Fetch", return_value=True
):
with mock.patch.object(
self._superproject, "_LsTree", return_value=data
):
# Create temporary directory so that it can write the file.
os.mkdir(self._superproject._superproject_path)
update_result = self._superproject.UpdateProjectsRevisionId(projects, self.git_event_log)
update_result = self._superproject.UpdateProjectsRevisionId(
projects, self.git_event_log
)
self.assertIsNotNone(update_result.manifest_path)
self.assertFalse(update_result.fatal)
with open(update_result.manifest_path, 'r') as fp:
with open(update_result.manifest_path, "r") as fp:
manifest_xml_data = fp.read()
# Verify platform/vendor/x's project revision hasn't changed.
# Verify platform/vendor/x's project revision hasn't
# changed.
self.assertEqual(
sort_attributes(manifest_xml_data),
'<?xml version="1.0" ?><manifest>'
'<remote fetch="http://localhost" name="default-remote"/>'
'<remote fetch="http://localhost2" name="goog"/>'
'<default remote="default-remote" revision="refs/heads/main"/>'
'<project groups="notdefault,platform-' + self.platform + '" '
'<project groups="notdefault,platform-'
+ self.platform
+ '" '
'name="platform/art" path="art" '
'revision="2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea" upstream="refs/heads/main"/>'
'<superproject name="superproject"/>'
'</manifest>')
"</manifest>",
)
def test_superproject_update_project_revision_id_with_pinned_manifest(self):
"""Test update of commit ids of a pinned manifest."""
manifest = self.getXmlManifest("""
manifest = self.getXmlManifest(
"""
<manifest>
<remote name="default-remote" fetch="http://localhost" />
<default remote="default-remote" revision="refs/heads/main" />
@ -326,37 +398,53 @@ class SuperprojectTestCase(unittest.TestCase):
<project path="vendor/x" name="platform/vendor/x" revision="" />
<project path="vendor/y" name="platform/vendor/y"
revision="52d3c9f7c107839ece2319d077de0cd922aa9d8f" />
<project path="art" name="platform/art" groups="notdefault,platform-""" + self.platform + """
<project path="art" name="platform/art" groups="notdefault,platform-"""
+ self.platform
+ """
" /></manifest>
""")
"""
)
self.maxDiff = None
self._superproject = git_superproject.Superproject(
manifest, name='superproject',
remote=manifest.remotes.get('default-remote').ToRemoteSpec('superproject'),
revision='refs/heads/main')
manifest,
name="superproject",
remote=manifest.remotes.get("default-remote").ToRemoteSpec(
"superproject"
),
revision="refs/heads/main",
)
self.assertEqual(len(self._superproject._manifest.projects), 3)
projects = self._superproject._manifest.projects
data = ('160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00'
'160000 commit e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06\tvendor/x\x00')
with mock.patch.object(self._superproject, '_Init', return_value=True):
with mock.patch.object(self._superproject, '_Fetch', return_value=True):
with mock.patch.object(self._superproject,
'_LsTree',
return_value=data):
data = (
"160000 commit 2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea\tart\x00"
"160000 commit e9d25da64d8d365dbba7c8ee00fe8c4473fe9a06\tvendor/x\x00"
)
with mock.patch.object(self._superproject, "_Init", return_value=True):
with mock.patch.object(
self._superproject, "_Fetch", return_value=True
):
with mock.patch.object(
self._superproject, "_LsTree", return_value=data
):
# Create temporary directory so that it can write the file.
os.mkdir(self._superproject._superproject_path)
update_result = self._superproject.UpdateProjectsRevisionId(projects, self.git_event_log)
update_result = self._superproject.UpdateProjectsRevisionId(
projects, self.git_event_log
)
self.assertIsNotNone(update_result.manifest_path)
self.assertFalse(update_result.fatal)
with open(update_result.manifest_path, 'r') as fp:
with open(update_result.manifest_path, "r") as fp:
manifest_xml_data = fp.read()
# Verify platform/vendor/x's project revision hasn't changed.
# Verify platform/vendor/x's project revision hasn't
# changed.
self.assertEqual(
sort_attributes(manifest_xml_data),
'<?xml version="1.0" ?><manifest>'
'<remote fetch="http://localhost" name="default-remote"/>'
'<default remote="default-remote" revision="refs/heads/main"/>'
'<project groups="notdefault,platform-' + self.platform + '" '
'<project groups="notdefault,platform-'
+ self.platform
+ '" '
'name="platform/art" path="art" '
'revision="2c2724cb36cd5a9cec6c852c681efc3b7c6b86ea" upstream="refs/heads/main"/>'
'<project name="platform/vendor/x" path="vendor/x" '
@ -364,42 +452,78 @@ class SuperprojectTestCase(unittest.TestCase):
'<project name="platform/vendor/y" path="vendor/y" '
'revision="52d3c9f7c107839ece2319d077de0cd922aa9d8f"/>'
'<superproject name="superproject"/>'
'</manifest>')
"</manifest>",
)
def test_Fetch(self):
manifest = self.getXmlManifest("""
manifest = self.getXmlManifest(
"""
<manifest>
<remote name="default-remote" fetch="http://localhost" />
<default remote="default-remote" revision="refs/heads/main" />
<superproject name="superproject"/>
" /></manifest>
""")
"""
)
self.maxDiff = None
self._superproject = git_superproject.Superproject(
manifest, name='superproject',
remote=manifest.remotes.get('default-remote').ToRemoteSpec('superproject'),
revision='refs/heads/main')
manifest,
name="superproject",
remote=manifest.remotes.get("default-remote").ToRemoteSpec(
"superproject"
),
revision="refs/heads/main",
)
os.mkdir(self._superproject._superproject_path)
os.mkdir(self._superproject._work_git)
with mock.patch.object(self._superproject, '_Init', return_value=True):
with mock.patch('git_superproject.GitCommand', autospec=True) as mock_git_command:
with mock.patch('git_superproject.GitRefs.get', autospec=True) as mock_git_refs:
with mock.patch.object(self._superproject, "_Init", return_value=True):
with mock.patch(
"git_superproject.GitCommand", autospec=True
) as mock_git_command:
with mock.patch(
"git_superproject.GitRefs.get", autospec=True
) as mock_git_refs:
instance = mock_git_command.return_value
instance.Wait.return_value = 0
mock_git_refs.side_effect = ['', '1234']
mock_git_refs.side_effect = ["", "1234"]
self.assertTrue(self._superproject._Fetch())
self.assertEqual(mock_git_command.call_args.args,(None, [
'fetch', 'http://localhost/superproject', '--depth', '1',
'--force', '--no-tags', '--filter', 'blob:none',
'refs/heads/main:refs/heads/main'
]))
self.assertEqual(
mock_git_command.call_args.args,
(
None,
[
"fetch",
"http://localhost/superproject",
"--depth",
"1",
"--force",
"--no-tags",
"--filter",
"blob:none",
"refs/heads/main:refs/heads/main",
],
),
)
# If branch for revision exists, set as --negotiation-tip.
self.assertTrue(self._superproject._Fetch())
self.assertEqual(mock_git_command.call_args.args,(None, [
'fetch', 'http://localhost/superproject', '--depth', '1',
'--force', '--no-tags', '--filter', 'blob:none',
'--negotiation-tip', '1234',
'refs/heads/main:refs/heads/main'
]))
self.assertEqual(
mock_git_command.call_args.args,
(
None,
[
"fetch",
"http://localhost/superproject",
"--depth",
"1",
"--force",
"--no-tags",
"--filter",
"blob:none",
"--negotiation-tip",
"1234",
"refs/heads/main:refs/heads/main",
],
),
)

View File

@ -29,17 +29,18 @@ import platform_utils
def serverLoggingThread(socket_path, server_ready, received_traces):
"""Helper function to receive logs over a Unix domain socket.
Appends received messages on the provided socket and appends to received_traces.
Appends received messages on the provided socket and appends to
received_traces.
Args:
socket_path: path to a Unix domain socket on which to listen for traces
server_ready: a threading.Condition used to signal to the caller that this thread is ready to
accept connections
received_traces: a list to which received traces will be appended (after decoding to a utf-8
string).
server_ready: a threading.Condition used to signal to the caller that
this thread is ready to accept connections
received_traces: a list to which received traces will be appended (after
decoding to a utf-8 string).
"""
platform_utils.remove(socket_path, missing_ok=True)
data = b''
data = b""
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.bind(socket_path)
sock.listen(0)
@ -51,16 +52,16 @@ def serverLoggingThread(socket_path, server_ready, received_traces):
if not recved:
break
data += recved
received_traces.extend(data.decode('utf-8').splitlines())
received_traces.extend(data.decode("utf-8").splitlines())
class EventLogTestCase(unittest.TestCase):
"""TestCase for the EventLog module."""
PARENT_SID_KEY = 'GIT_TRACE2_PARENT_SID'
PARENT_SID_VALUE = 'parent_sid'
SELF_SID_REGEX = r'repo-\d+T\d+Z-.*'
FULL_SID_REGEX = r'^%s/%s' % (PARENT_SID_VALUE, SELF_SID_REGEX)
PARENT_SID_KEY = "GIT_TRACE2_PARENT_SID"
PARENT_SID_VALUE = "parent_sid"
SELF_SID_REGEX = r"repo-\d+T\d+Z-.*"
FULL_SID_REGEX = r"^%s/%s" % (PARENT_SID_VALUE, SELF_SID_REGEX)
def setUp(self):
"""Load the event_log module every time."""
@ -73,32 +74,35 @@ class EventLogTestCase(unittest.TestCase):
self._event_log_module = git_trace2_event_log.EventLog(env=env)
self._log_data = None
def verifyCommonKeys(self, log_entry, expected_event_name=None, full_sid=True):
def verifyCommonKeys(
self, log_entry, expected_event_name=None, full_sid=True
):
"""Helper function to verify common event log keys."""
self.assertIn('event', log_entry)
self.assertIn('sid', log_entry)
self.assertIn('thread', log_entry)
self.assertIn('time', log_entry)
self.assertIn("event", log_entry)
self.assertIn("sid", log_entry)
self.assertIn("thread", log_entry)
self.assertIn("time", log_entry)
# Do basic data format validation.
if expected_event_name:
self.assertEqual(expected_event_name, log_entry['event'])
self.assertEqual(expected_event_name, log_entry["event"])
if full_sid:
self.assertRegex(log_entry['sid'], self.FULL_SID_REGEX)
self.assertRegex(log_entry["sid"], self.FULL_SID_REGEX)
else:
self.assertRegex(log_entry['sid'], self.SELF_SID_REGEX)
self.assertRegex(log_entry['time'], r'^\d+-\d+-\d+T\d+:\d+:\d+\.\d+Z$')
self.assertRegex(log_entry["sid"], self.SELF_SID_REGEX)
self.assertRegex(log_entry["time"], r"^\d+-\d+-\d+T\d+:\d+:\d+\.\d+Z$")
def readLog(self, log_path):
"""Helper function to read log data into a list."""
log_data = []
with open(log_path, mode='rb') as f:
with open(log_path, mode="rb") as f:
for line in f:
log_data.append(json.loads(line))
return log_data
def remove_prefix(self, s, prefix):
"""Return a copy string after removing |prefix| from |s|, if present or the original string."""
"""Return a copy string after removing |prefix| from |s|, if present or
the original string."""
if s.startswith(prefix):
return s[len(prefix) :]
else:
@ -123,19 +127,19 @@ class EventLogTestCase(unittest.TestCase):
Expected event log:
<version event>
"""
with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
log_path = self._event_log_module.Write(path=tempdir)
self._log_data = self.readLog(log_path)
# A log with no added events should only have the version entry.
self.assertEqual(len(self._log_data), 1)
version_event = self._log_data[0]
self.verifyCommonKeys(version_event, expected_event_name='version')
self.verifyCommonKeys(version_event, expected_event_name="version")
# Check for 'version' event specific fields.
self.assertIn('evt', version_event)
self.assertIn('exe', version_event)
self.assertIn("evt", version_event)
self.assertIn("exe", version_event)
# Verify "evt" version field is a string.
self.assertIsInstance(version_event['evt'], str)
self.assertIsInstance(version_event["evt"], str)
def test_start_event(self):
"""Test and validate 'start' event data is valid.
@ -145,17 +149,17 @@ class EventLogTestCase(unittest.TestCase):
<start event>
"""
self._event_log_module.StartEvent()
with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
log_path = self._event_log_module.Write(path=tempdir)
self._log_data = self.readLog(log_path)
self.assertEqual(len(self._log_data), 2)
start_event = self._log_data[1]
self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
self.verifyCommonKeys(start_event, expected_event_name='start')
self.verifyCommonKeys(self._log_data[0], expected_event_name="version")
self.verifyCommonKeys(start_event, expected_event_name="start")
# Check for 'start' event specific fields.
self.assertIn('argv', start_event)
self.assertTrue(isinstance(start_event['argv'], list))
self.assertIn("argv", start_event)
self.assertTrue(isinstance(start_event["argv"], list))
def test_exit_event_result_none(self):
"""Test 'exit' event data is valid when result is None.
@ -167,18 +171,18 @@ class EventLogTestCase(unittest.TestCase):
<exit event>
"""
self._event_log_module.ExitEvent(None)
with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
log_path = self._event_log_module.Write(path=tempdir)
self._log_data = self.readLog(log_path)
self.assertEqual(len(self._log_data), 2)
exit_event = self._log_data[1]
self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
self.verifyCommonKeys(exit_event, expected_event_name='exit')
self.verifyCommonKeys(self._log_data[0], expected_event_name="version")
self.verifyCommonKeys(exit_event, expected_event_name="exit")
# Check for 'exit' event specific fields.
self.assertIn('code', exit_event)
self.assertIn("code", exit_event)
# 'None' result should convert to 0 (successful) return code.
self.assertEqual(exit_event['code'], 0)
self.assertEqual(exit_event["code"], 0)
def test_exit_event_result_integer(self):
"""Test 'exit' event data is valid when result is an integer.
@ -188,17 +192,17 @@ class EventLogTestCase(unittest.TestCase):
<exit event>
"""
self._event_log_module.ExitEvent(2)
with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
log_path = self._event_log_module.Write(path=tempdir)
self._log_data = self.readLog(log_path)
self.assertEqual(len(self._log_data), 2)
exit_event = self._log_data[1]
self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
self.verifyCommonKeys(exit_event, expected_event_name='exit')
self.verifyCommonKeys(self._log_data[0], expected_event_name="version")
self.verifyCommonKeys(exit_event, expected_event_name="exit")
# Check for 'exit' event specific fields.
self.assertIn('code', exit_event)
self.assertEqual(exit_event['code'], 2)
self.assertIn("code", exit_event)
self.assertEqual(exit_event["code"], 2)
def test_command_event(self):
"""Test and validate 'command' event data is valid.
@ -207,22 +211,24 @@ class EventLogTestCase(unittest.TestCase):
<version event>
<command event>
"""
name = 'repo'
subcommands = ['init' 'this']
self._event_log_module.CommandEvent(name='repo', subcommands=subcommands)
with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
name = "repo"
subcommands = ["init" "this"]
self._event_log_module.CommandEvent(
name="repo", subcommands=subcommands
)
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
log_path = self._event_log_module.Write(path=tempdir)
self._log_data = self.readLog(log_path)
self.assertEqual(len(self._log_data), 2)
command_event = self._log_data[1]
self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
self.verifyCommonKeys(command_event, expected_event_name='command')
self.verifyCommonKeys(self._log_data[0], expected_event_name="version")
self.verifyCommonKeys(command_event, expected_event_name="command")
# Check for 'command' event specific fields.
self.assertIn('name', command_event)
self.assertIn('subcommands', command_event)
self.assertEqual(command_event['name'], name)
self.assertEqual(command_event['subcommands'], subcommands)
self.assertIn("name", command_event)
self.assertIn("subcommands", command_event)
self.assertEqual(command_event["name"], name)
self.assertEqual(command_event["subcommands"], subcommands)
def test_def_params_event_repo_config(self):
"""Test 'def_params' event data outputs only repo config keys.
@ -233,26 +239,26 @@ class EventLogTestCase(unittest.TestCase):
<def_param event>
"""
config = {
'git.foo': 'bar',
'repo.partialclone': 'true',
'repo.partialclonefilter': 'blob:none',
"git.foo": "bar",
"repo.partialclone": "true",
"repo.partialclonefilter": "blob:none",
}
self._event_log_module.DefParamRepoEvents(config)
with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
log_path = self._event_log_module.Write(path=tempdir)
self._log_data = self.readLog(log_path)
self.assertEqual(len(self._log_data), 3)
def_param_events = self._log_data[1:]
self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
self.verifyCommonKeys(self._log_data[0], expected_event_name="version")
for event in def_param_events:
self.verifyCommonKeys(event, expected_event_name='def_param')
self.verifyCommonKeys(event, expected_event_name="def_param")
# Check for 'def_param' event specific fields.
self.assertIn('param', event)
self.assertIn('value', event)
self.assertTrue(event['param'].startswith('repo.'))
self.assertIn("param", event)
self.assertIn("value", event)
self.assertTrue(event["param"].startswith("repo."))
def test_def_params_event_no_repo_config(self):
"""Test 'def_params' event data won't output non-repo config keys.
@ -261,17 +267,17 @@ class EventLogTestCase(unittest.TestCase):
<version event>
"""
config = {
'git.foo': 'bar',
'git.core.foo2': 'baz',
"git.foo": "bar",
"git.core.foo2": "baz",
}
self._event_log_module.DefParamRepoEvents(config)
with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
log_path = self._event_log_module.Write(path=tempdir)
self._log_data = self.readLog(log_path)
self.assertEqual(len(self._log_data), 1)
self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
self.verifyCommonKeys(self._log_data[0], expected_event_name="version")
def test_data_event_config(self):
"""Test 'data' event data outputs all config keys.
@ -282,31 +288,33 @@ class EventLogTestCase(unittest.TestCase):
<data event>
"""
config = {
'git.foo': 'bar',
'repo.partialclone': 'false',
'repo.syncstate.superproject.hassuperprojecttag': 'true',
'repo.syncstate.superproject.sys.argv': ['--', 'sync', 'protobuf'],
"git.foo": "bar",
"repo.partialclone": "false",
"repo.syncstate.superproject.hassuperprojecttag": "true",
"repo.syncstate.superproject.sys.argv": ["--", "sync", "protobuf"],
}
prefix_value = 'prefix'
prefix_value = "prefix"
self._event_log_module.LogDataConfigEvents(config, prefix_value)
with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
log_path = self._event_log_module.Write(path=tempdir)
self._log_data = self.readLog(log_path)
self.assertEqual(len(self._log_data), 5)
data_events = self._log_data[1:]
self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
self.verifyCommonKeys(self._log_data[0], expected_event_name="version")
for event in data_events:
self.verifyCommonKeys(event)
# Check for 'data' event specific fields.
self.assertIn('key', event)
self.assertIn('value', event)
key = event['key']
key = self.remove_prefix(key, f'{prefix_value}/')
value = event['value']
self.assertEqual(self._event_log_module.GetDataEventName(value), event['event'])
self.assertIn("key", event)
self.assertIn("value", event)
key = event["key"]
key = self.remove_prefix(key, f"{prefix_value}/")
value = event["value"]
self.assertEqual(
self._event_log_module.GetDataEventName(value), event["event"]
)
self.assertTrue(key in config and value == config[key])
def test_error_event(self):
@ -316,38 +324,45 @@ class EventLogTestCase(unittest.TestCase):
<version event>
<error event>
"""
msg = 'invalid option: --cahced'
fmt = 'invalid option: %s'
msg = "invalid option: --cahced"
fmt = "invalid option: %s"
self._event_log_module.ErrorEvent(msg, fmt)
with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
log_path = self._event_log_module.Write(path=tempdir)
self._log_data = self.readLog(log_path)
self.assertEqual(len(self._log_data), 2)
error_event = self._log_data[1]
self.verifyCommonKeys(self._log_data[0], expected_event_name='version')
self.verifyCommonKeys(error_event, expected_event_name='error')
self.verifyCommonKeys(self._log_data[0], expected_event_name="version")
self.verifyCommonKeys(error_event, expected_event_name="error")
# Check for 'error' event specific fields.
self.assertIn('msg', error_event)
self.assertIn('fmt', error_event)
self.assertEqual(error_event['msg'], msg)
self.assertEqual(error_event['fmt'], fmt)
self.assertIn("msg", error_event)
self.assertIn("fmt", error_event)
self.assertEqual(error_event["msg"], msg)
self.assertEqual(error_event["fmt"], fmt)
def test_write_with_filename(self):
"""Test Write() with a path to a file exits with None."""
self.assertIsNone(self._event_log_module.Write(path='path/to/file'))
self.assertIsNone(self._event_log_module.Write(path="path/to/file"))
def test_write_with_git_config(self):
"""Test Write() uses the git config path when 'git config' call succeeds."""
with tempfile.TemporaryDirectory(prefix='event_log_tests') as tempdir:
with mock.patch.object(self._event_log_module,
'_GetEventTargetPath', return_value=tempdir):
self.assertEqual(os.path.dirname(self._event_log_module.Write()), tempdir)
"""Test Write() uses the git config path when 'git config' call
succeeds."""
with tempfile.TemporaryDirectory(prefix="event_log_tests") as tempdir:
with mock.patch.object(
self._event_log_module,
"_GetEventTargetPath",
return_value=tempdir,
):
self.assertEqual(
os.path.dirname(self._event_log_module.Write()), tempdir
)
def test_write_no_git_config(self):
"""Test Write() with no git config variable present exits with None."""
with mock.patch.object(self._event_log_module,
'_GetEventTargetPath', return_value=None):
with mock.patch.object(
self._event_log_module, "_GetEventTargetPath", return_value=None
):
self.assertIsNone(self._event_log_module.Write())
def test_write_non_string(self):
@ -356,32 +371,38 @@ class EventLogTestCase(unittest.TestCase):
self._event_log_module.Write(path=1234)
def test_write_socket(self):
"""Test Write() with Unix domain socket for |path| and validate received traces."""
"""Test Write() with Unix domain socket for |path| and validate received
traces."""
received_traces = []
with tempfile.TemporaryDirectory(prefix='test_server_sockets') as tempdir:
with tempfile.TemporaryDirectory(
prefix="test_server_sockets"
) as tempdir:
socket_path = os.path.join(tempdir, "server.sock")
server_ready = threading.Condition()
# Start "server" listening on Unix domain socket at socket_path.
try:
server_thread = threading.Thread(
target=serverLoggingThread,
args=(socket_path, server_ready, received_traces))
args=(socket_path, server_ready, received_traces),
)
server_thread.start()
with server_ready:
server_ready.wait(timeout=120)
self._event_log_module.StartEvent()
path = self._event_log_module.Write(path=f'af_unix:{socket_path}')
path = self._event_log_module.Write(
path=f"af_unix:{socket_path}"
)
finally:
server_thread.join(timeout=5)
self.assertEqual(path, f'af_unix:stream:{socket_path}')
self.assertEqual(path, f"af_unix:stream:{socket_path}")
self.assertEqual(len(received_traces), 2)
version_event = json.loads(received_traces[0])
start_event = json.loads(received_traces[1])
self.verifyCommonKeys(version_event, expected_event_name='version')
self.verifyCommonKeys(start_event, expected_event_name='start')
self.verifyCommonKeys(version_event, expected_event_name="version")
self.verifyCommonKeys(start_event, expected_event_name="start")
# Check for 'start' event specific fields.
self.assertIn('argv', start_event)
self.assertIsInstance(start_event['argv'], list)
self.assertIn("argv", start_event)
self.assertIsInstance(start_event["argv"], list)

View File

@ -17,39 +17,38 @@
import hooks
import unittest
class RepoHookShebang(unittest.TestCase):
"""Check shebang parsing in RepoHook."""
def test_no_shebang(self):
"""Lines w/out shebangs should be rejected."""
DATA = (
'',
'#\n# foo\n',
'# Bad shebang in script\n#!/foo\n'
)
DATA = ("", "#\n# foo\n", "# Bad shebang in script\n#!/foo\n")
for data in DATA:
self.assertIsNone(hooks.RepoHook._ExtractInterpFromShebang(data))
def test_direct_interp(self):
"""Lines whose shebang points directly to the interpreter."""
DATA = (
('#!/foo', '/foo'),
('#! /foo', '/foo'),
('#!/bin/foo ', '/bin/foo'),
('#! /usr/foo ', '/usr/foo'),
('#! /usr/foo -args', '/usr/foo'),
("#!/foo", "/foo"),
("#! /foo", "/foo"),
("#!/bin/foo ", "/bin/foo"),
("#! /usr/foo ", "/usr/foo"),
("#! /usr/foo -args", "/usr/foo"),
)
for shebang, interp in DATA:
self.assertEqual(hooks.RepoHook._ExtractInterpFromShebang(shebang),
interp)
self.assertEqual(
hooks.RepoHook._ExtractInterpFromShebang(shebang), interp
)
def test_env_interp(self):
"""Lines whose shebang launches through `env`."""
DATA = (
('#!/usr/bin/env foo', 'foo'),
('#!/bin/env foo', 'foo'),
('#! /bin/env /bin/foo ', '/bin/foo'),
("#!/usr/bin/env foo", "foo"),
("#!/bin/env foo", "foo"),
("#! /bin/env /bin/foo ", "/bin/foo"),
)
for shebang, interp in DATA:
self.assertEqual(hooks.RepoHook._ExtractInterpFromShebang(shebang),
interp)
self.assertEqual(
hooks.RepoHook._ExtractInterpFromShebang(shebang), interp
)

File diff suppressed because it is too large Load Diff

View File

@ -27,24 +27,26 @@ class RemoveTests(unittest.TestCase):
def testMissingOk(self):
"""Check missing_ok handling."""
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, 'test')
path = os.path.join(tmpdir, "test")
# Should not fail.
platform_utils.remove(path, missing_ok=True)
# Should fail.
self.assertRaises(OSError, platform_utils.remove, path)
self.assertRaises(OSError, platform_utils.remove, path, missing_ok=False)
self.assertRaises(
OSError, platform_utils.remove, path, missing_ok=False
)
# Should not fail if it exists.
open(path, 'w').close()
open(path, "w").close()
platform_utils.remove(path, missing_ok=True)
self.assertFalse(os.path.exists(path))
open(path, 'w').close()
open(path, "w").close()
platform_utils.remove(path)
self.assertFalse(os.path.exists(path))
open(path, 'w').close()
open(path, "w").close()
platform_utils.remove(path, missing_ok=False)
self.assertFalse(os.path.exists(path))

View File

@ -32,18 +32,18 @@ import project
@contextlib.contextmanager
def TempGitTree():
"""Create a new empty git checkout for testing."""
with tempfile.TemporaryDirectory(prefix='repo-tests') as tempdir:
with tempfile.TemporaryDirectory(prefix="repo-tests") as tempdir:
# Tests need to assume, that main is default branch at init,
# which is not supported in config until 2.28.
cmd = ['git', 'init']
cmd = ["git", "init"]
if git_command.git_require((2, 28, 0)):
cmd += ['--initial-branch=main']
cmd += ["--initial-branch=main"]
else:
# Use template dir for init.
templatedir = tempfile.mkdtemp(prefix='.test-template')
with open(os.path.join(templatedir, 'HEAD'), 'w') as fp:
fp.write('ref: refs/heads/main\n')
cmd += ['--template', templatedir]
templatedir = tempfile.mkdtemp(prefix=".test-template")
with open(os.path.join(templatedir, "HEAD"), "w") as fp:
fp.write("ref: refs/heads/main\n")
cmd += ["--template", templatedir]
subprocess.check_call(cmd, cwd=tempdir)
yield tempdir
@ -53,12 +53,14 @@ class FakeProject(object):
def __init__(self, worktree):
self.worktree = worktree
self.gitdir = os.path.join(worktree, '.git')
self.name = 'fakeproject'
self.gitdir = os.path.join(worktree, ".git")
self.name = "fakeproject"
self.work_git = project.Project._GitGetByExec(
self, bare=False, gitdir=self.gitdir)
self, bare=False, gitdir=self.gitdir
)
self.bare_git = project.Project._GitGetByExec(
self, bare=True, gitdir=self.gitdir)
self, bare=True, gitdir=self.gitdir
)
self.config = git_config.GitConfig.ForRepository(gitdir=self.gitdir)
@ -71,20 +73,21 @@ class ReviewableBranchTests(unittest.TestCase):
fakeproj = FakeProject(tempdir)
# Generate some commits.
with open(os.path.join(tempdir, 'readme'), 'w') as fp:
fp.write('txt')
fakeproj.work_git.add('readme')
fakeproj.work_git.commit('-mAdd file')
fakeproj.work_git.checkout('-b', 'work')
fakeproj.work_git.rm('-f', 'readme')
fakeproj.work_git.commit('-mDel file')
with open(os.path.join(tempdir, "readme"), "w") as fp:
fp.write("txt")
fakeproj.work_git.add("readme")
fakeproj.work_git.commit("-mAdd file")
fakeproj.work_git.checkout("-b", "work")
fakeproj.work_git.rm("-f", "readme")
fakeproj.work_git.commit("-mDel file")
# Start off with the normal details.
rb = project.ReviewableBranch(
fakeproj, fakeproj.config.GetBranch('work'), 'main')
self.assertEqual('work', rb.name)
fakeproj, fakeproj.config.GetBranch("work"), "main"
)
self.assertEqual("work", rb.name)
self.assertEqual(1, len(rb.commits))
self.assertIn('Del file', rb.commits[0])
self.assertIn("Del file", rb.commits[0])
d = rb.unabbrev_commits
self.assertEqual(1, len(d))
short, long = next(iter(d.items()))
@ -94,9 +97,10 @@ class ReviewableBranchTests(unittest.TestCase):
self.assertTrue(rb.date)
# Now delete the tracking branch!
fakeproj.work_git.branch('-D', 'main')
fakeproj.work_git.branch("-D", "main")
rb = project.ReviewableBranch(
fakeproj, fakeproj.config.GetBranch('work'), 'main')
fakeproj, fakeproj.config.GetBranch("work"), "main"
)
self.assertEqual(0, len(rb.commits))
self.assertFalse(rb.base_exists)
# Hard to assert anything useful about this.
@ -118,10 +122,10 @@ class CopyLinkTestCase(unittest.TestCase):
"""
def setUp(self):
self.tempdirobj = tempfile.TemporaryDirectory(prefix='repo_tests')
self.tempdirobj = tempfile.TemporaryDirectory(prefix="repo_tests")
self.tempdir = self.tempdirobj.name
self.topdir = os.path.join(self.tempdir, 'checkout')
self.worktree = os.path.join(self.topdir, 'git-project')
self.topdir = os.path.join(self.tempdir, "checkout")
self.worktree = os.path.join(self.topdir, "git-project")
os.makedirs(self.topdir)
os.makedirs(self.worktree)
@ -130,7 +134,7 @@ class CopyLinkTestCase(unittest.TestCase):
@staticmethod
def touch(path):
with open(path, 'w'):
with open(path, "w"):
pass
def assertExists(self, path, msg=None):
@ -139,18 +143,19 @@ class CopyLinkTestCase(unittest.TestCase):
return
if msg is None:
msg = ['path is missing: %s' % path]
while path != '/':
msg = ["path is missing: %s" % path]
while path != "/":
path = os.path.dirname(path)
if not path:
# If we're given something like "foo", abort once we get to "".
# If we're given something like "foo", abort once we get to
# "".
break
result = os.path.exists(path)
msg.append('\tos.path.exists(%s): %s' % (path, result))
msg.append("\tos.path.exists(%s): %s" % (path, result))
if result:
msg.append('\tcontents: %r' % os.listdir(path))
msg.append("\tcontents: %r" % os.listdir(path))
break
msg = '\n'.join(msg)
msg = "\n".join(msg)
raise self.failureException(msg)
@ -163,98 +168,99 @@ class CopyFile(CopyLinkTestCase):
def test_basic(self):
"""Basic test of copying a file from a project to the toplevel."""
src = os.path.join(self.worktree, 'foo.txt')
src = os.path.join(self.worktree, "foo.txt")
self.touch(src)
cf = self.CopyFile('foo.txt', 'foo')
cf = self.CopyFile("foo.txt", "foo")
cf._Copy()
self.assertExists(os.path.join(self.topdir, 'foo'))
self.assertExists(os.path.join(self.topdir, "foo"))
def test_src_subdir(self):
"""Copy a file from a subdir of a project."""
src = os.path.join(self.worktree, 'bar', 'foo.txt')
src = os.path.join(self.worktree, "bar", "foo.txt")
os.makedirs(os.path.dirname(src))
self.touch(src)
cf = self.CopyFile('bar/foo.txt', 'new.txt')
cf = self.CopyFile("bar/foo.txt", "new.txt")
cf._Copy()
self.assertExists(os.path.join(self.topdir, 'new.txt'))
self.assertExists(os.path.join(self.topdir, "new.txt"))
def test_dest_subdir(self):
"""Copy a file to a subdir of a checkout."""
src = os.path.join(self.worktree, 'foo.txt')
src = os.path.join(self.worktree, "foo.txt")
self.touch(src)
cf = self.CopyFile('foo.txt', 'sub/dir/new.txt')
self.assertFalse(os.path.exists(os.path.join(self.topdir, 'sub')))
cf = self.CopyFile("foo.txt", "sub/dir/new.txt")
self.assertFalse(os.path.exists(os.path.join(self.topdir, "sub")))
cf._Copy()
self.assertExists(os.path.join(self.topdir, 'sub', 'dir', 'new.txt'))
self.assertExists(os.path.join(self.topdir, "sub", "dir", "new.txt"))
def test_update(self):
"""Make sure changed files get copied again."""
src = os.path.join(self.worktree, 'foo.txt')
dest = os.path.join(self.topdir, 'bar')
with open(src, 'w') as f:
f.write('1st')
cf = self.CopyFile('foo.txt', 'bar')
src = os.path.join(self.worktree, "foo.txt")
dest = os.path.join(self.topdir, "bar")
with open(src, "w") as f:
f.write("1st")
cf = self.CopyFile("foo.txt", "bar")
cf._Copy()
self.assertExists(dest)
with open(dest) as f:
self.assertEqual(f.read(), '1st')
self.assertEqual(f.read(), "1st")
with open(src, 'w') as f:
f.write('2nd!')
with open(src, "w") as f:
f.write("2nd!")
cf._Copy()
with open(dest) as f:
self.assertEqual(f.read(), '2nd!')
self.assertEqual(f.read(), "2nd!")
def test_src_block_symlink(self):
"""Do not allow reading from a symlinked path."""
src = os.path.join(self.worktree, 'foo.txt')
sym = os.path.join(self.worktree, 'sym')
src = os.path.join(self.worktree, "foo.txt")
sym = os.path.join(self.worktree, "sym")
self.touch(src)
platform_utils.symlink('foo.txt', sym)
platform_utils.symlink("foo.txt", sym)
self.assertExists(sym)
cf = self.CopyFile('sym', 'foo')
cf = self.CopyFile("sym", "foo")
self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
def test_src_block_symlink_traversal(self):
"""Do not allow reading through a symlink dir."""
realfile = os.path.join(self.tempdir, 'file.txt')
realfile = os.path.join(self.tempdir, "file.txt")
self.touch(realfile)
src = os.path.join(self.worktree, 'bar', 'file.txt')
platform_utils.symlink(self.tempdir, os.path.join(self.worktree, 'bar'))
src = os.path.join(self.worktree, "bar", "file.txt")
platform_utils.symlink(self.tempdir, os.path.join(self.worktree, "bar"))
self.assertExists(src)
cf = self.CopyFile('bar/file.txt', 'foo')
cf = self.CopyFile("bar/file.txt", "foo")
self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
def test_src_block_copy_from_dir(self):
"""Do not allow copying from a directory."""
src = os.path.join(self.worktree, 'dir')
src = os.path.join(self.worktree, "dir")
os.makedirs(src)
cf = self.CopyFile('dir', 'foo')
cf = self.CopyFile("dir", "foo")
self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
def test_dest_block_symlink(self):
"""Do not allow writing to a symlink."""
src = os.path.join(self.worktree, 'foo.txt')
src = os.path.join(self.worktree, "foo.txt")
self.touch(src)
platform_utils.symlink('dest', os.path.join(self.topdir, 'sym'))
cf = self.CopyFile('foo.txt', 'sym')
platform_utils.symlink("dest", os.path.join(self.topdir, "sym"))
cf = self.CopyFile("foo.txt", "sym")
self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
def test_dest_block_symlink_traversal(self):
"""Do not allow writing through a symlink dir."""
src = os.path.join(self.worktree, 'foo.txt')
src = os.path.join(self.worktree, "foo.txt")
self.touch(src)
platform_utils.symlink(tempfile.gettempdir(),
os.path.join(self.topdir, 'sym'))
cf = self.CopyFile('foo.txt', 'sym/foo.txt')
platform_utils.symlink(
tempfile.gettempdir(), os.path.join(self.topdir, "sym")
)
cf = self.CopyFile("foo.txt", "sym/foo.txt")
self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
def test_src_block_copy_to_dir(self):
"""Do not allow copying to a directory."""
src = os.path.join(self.worktree, 'foo.txt')
src = os.path.join(self.worktree, "foo.txt")
self.touch(src)
os.makedirs(os.path.join(self.topdir, 'dir'))
cf = self.CopyFile('foo.txt', 'dir')
os.makedirs(os.path.join(self.topdir, "dir"))
cf = self.CopyFile("foo.txt", "dir")
self.assertRaises(error.ManifestInvalidPathError, cf._Copy)
@ -266,86 +272,106 @@ class LinkFile(CopyLinkTestCase):
def test_basic(self):
"""Basic test of linking a file from a project into the toplevel."""
src = os.path.join(self.worktree, 'foo.txt')
src = os.path.join(self.worktree, "foo.txt")
self.touch(src)
lf = self.LinkFile('foo.txt', 'foo')
lf = self.LinkFile("foo.txt", "foo")
lf._Link()
dest = os.path.join(self.topdir, 'foo')
dest = os.path.join(self.topdir, "foo")
self.assertExists(dest)
self.assertTrue(os.path.islink(dest))
self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest))
self.assertEqual(
os.path.join("git-project", "foo.txt"), os.readlink(dest)
)
def test_src_subdir(self):
"""Link to a file in a subdir of a project."""
src = os.path.join(self.worktree, 'bar', 'foo.txt')
src = os.path.join(self.worktree, "bar", "foo.txt")
os.makedirs(os.path.dirname(src))
self.touch(src)
lf = self.LinkFile('bar/foo.txt', 'foo')
lf = self.LinkFile("bar/foo.txt", "foo")
lf._Link()
self.assertExists(os.path.join(self.topdir, 'foo'))
self.assertExists(os.path.join(self.topdir, "foo"))
def test_src_self(self):
"""Link to the project itself."""
dest = os.path.join(self.topdir, 'foo', 'bar')
lf = self.LinkFile('.', 'foo/bar')
dest = os.path.join(self.topdir, "foo", "bar")
lf = self.LinkFile(".", "foo/bar")
lf._Link()
self.assertExists(dest)
self.assertEqual(os.path.join('..', 'git-project'), os.readlink(dest))
self.assertEqual(os.path.join("..", "git-project"), os.readlink(dest))
def test_dest_subdir(self):
"""Link a file to a subdir of a checkout."""
src = os.path.join(self.worktree, 'foo.txt')
src = os.path.join(self.worktree, "foo.txt")
self.touch(src)
lf = self.LinkFile('foo.txt', 'sub/dir/foo/bar')
self.assertFalse(os.path.exists(os.path.join(self.topdir, 'sub')))
lf = self.LinkFile("foo.txt", "sub/dir/foo/bar")
self.assertFalse(os.path.exists(os.path.join(self.topdir, "sub")))
lf._Link()
self.assertExists(os.path.join(self.topdir, 'sub', 'dir', 'foo', 'bar'))
self.assertExists(os.path.join(self.topdir, "sub", "dir", "foo", "bar"))
def test_src_block_relative(self):
"""Do not allow relative symlinks."""
BAD_SOURCES = (
'./',
'..',
'../',
'foo/.',
'foo/./bar',
'foo/..',
'foo/../foo',
"./",
"..",
"../",
"foo/.",
"foo/./bar",
"foo/..",
"foo/../foo",
)
for src in BAD_SOURCES:
lf = self.LinkFile(src, 'foo')
lf = self.LinkFile(src, "foo")
self.assertRaises(error.ManifestInvalidPathError, lf._Link)
def test_update(self):
"""Make sure changed targets get updated."""
dest = os.path.join(self.topdir, 'sym')
dest = os.path.join(self.topdir, "sym")
src = os.path.join(self.worktree, 'foo.txt')
src = os.path.join(self.worktree, "foo.txt")
self.touch(src)
lf = self.LinkFile('foo.txt', 'sym')
lf = self.LinkFile("foo.txt", "sym")
lf._Link()
self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest))
self.assertEqual(
os.path.join("git-project", "foo.txt"), os.readlink(dest)
)
# Point the symlink somewhere else.
os.unlink(dest)
platform_utils.symlink(self.tempdir, dest)
lf._Link()
self.assertEqual(os.path.join('git-project', 'foo.txt'), os.readlink(dest))
self.assertEqual(
os.path.join("git-project", "foo.txt"), os.readlink(dest)
)
class MigrateWorkTreeTests(unittest.TestCase):
"""Check _MigrateOldWorkTreeGitDir handling."""
_SYMLINKS = {
'config', 'description', 'hooks', 'info', 'logs', 'objects',
'packed-refs', 'refs', 'rr-cache', 'shallow', 'svn',
"config",
"description",
"hooks",
"info",
"logs",
"objects",
"packed-refs",
"refs",
"rr-cache",
"shallow",
"svn",
}
_FILES = {
'COMMIT_EDITMSG', 'FETCH_HEAD', 'HEAD', 'index', 'ORIG_HEAD',
'unknown-file-should-be-migrated',
"COMMIT_EDITMSG",
"FETCH_HEAD",
"HEAD",
"index",
"ORIG_HEAD",
"unknown-file-should-be-migrated",
}
_CLEAN_FILES = {
'a-vim-temp-file~', '#an-emacs-temp-file#',
"a-vim-temp-file~",
"#an-emacs-temp-file#",
}
@classmethod
@ -355,15 +381,17 @@ class MigrateWorkTreeTests(unittest.TestCase):
with tempfile.TemporaryDirectory() as tempdir:
tempdir = Path(tempdir)
gitdir = tempdir / '.repo/projects/src/test.git'
gitdir = tempdir / ".repo/projects/src/test.git"
gitdir.mkdir(parents=True)
cmd = ['git', 'init', '--bare', str(gitdir)]
cmd = ["git", "init", "--bare", str(gitdir)]
subprocess.check_call(cmd)
dotgit = tempdir / 'src/test/.git'
dotgit = tempdir / "src/test/.git"
dotgit.mkdir(parents=True)
for name in cls._SYMLINKS:
(dotgit / name).symlink_to(f'../../../.repo/projects/src/test.git/{name}')
(dotgit / name).symlink_to(
f"../../../.repo/projects/src/test.git/{name}"
)
for name in cls._FILES | cls._CLEAN_FILES:
(dotgit / name).write_text(name)
@ -372,15 +400,18 @@ class MigrateWorkTreeTests(unittest.TestCase):
def test_standard(self):
"""Migrate a standard checkout that we expect."""
with self._simple_layout() as tempdir:
dotgit = tempdir / 'src/test/.git'
dotgit = tempdir / "src/test/.git"
project.Project._MigrateOldWorkTreeGitDir(str(dotgit))
# Make sure the dir was transformed into a symlink.
self.assertTrue(dotgit.is_symlink())
self.assertEqual(os.readlink(dotgit), os.path.normpath('../../.repo/projects/src/test.git'))
self.assertEqual(
os.readlink(dotgit),
os.path.normpath("../../.repo/projects/src/test.git"),
)
# Make sure files were moved over.
gitdir = tempdir / '.repo/projects/src/test.git'
gitdir = tempdir / ".repo/projects/src/test.git"
for name in self._FILES:
self.assertEqual(name, (gitdir / name).read_text())
# Make sure files were removed.
@ -390,9 +421,11 @@ class MigrateWorkTreeTests(unittest.TestCase):
def test_unknown(self):
"""A checkout with unknown files should abort."""
with self._simple_layout() as tempdir:
dotgit = tempdir / 'src/test/.git'
(tempdir / '.repo/projects/src/test.git/random-file').write_text('one')
(dotgit / 'random-file').write_text('two')
dotgit = tempdir / "src/test/.git"
(tempdir / ".repo/projects/src/test.git/random-file").write_text(
"one"
)
(dotgit / "random-file").write_text("two")
with self.assertRaises(error.GitError):
project.Project._MigrateOldWorkTreeGitDir(str(dotgit))
@ -410,18 +443,16 @@ class ManifestPropertiesFetchedCorrectly(unittest.TestCase):
"""Ensure properties are fetched properly."""
def setUpManifest(self, tempdir):
repodir = os.path.join(tempdir, '.repo')
manifest_dir = os.path.join(repodir, 'manifests')
manifest_file = os.path.join(
repodir, manifest_xml.MANIFEST_FILE_NAME)
local_manifest_dir = os.path.join(
repodir, manifest_xml.LOCAL_MANIFESTS_DIR_NAME)
repodir = os.path.join(tempdir, ".repo")
manifest_dir = os.path.join(repodir, "manifests")
manifest_file = os.path.join(repodir, manifest_xml.MANIFEST_FILE_NAME)
os.mkdir(repodir)
os.mkdir(manifest_dir)
manifest = manifest_xml.XmlManifest(repodir, manifest_file)
return project.ManifestProject(
manifest, 'test/manifest', os.path.join(tempdir, '.git'), tempdir)
manifest, "test/manifest", os.path.join(tempdir, ".git"), tempdir
)
def test_manifest_config_properties(self):
"""Test we are fetching the manifest config properties correctly."""
@ -432,51 +463,61 @@ class ManifestPropertiesFetchedCorrectly(unittest.TestCase):
# Set property using the expected Set method, then ensure
# the porperty functions are using the correct Get methods.
fakeproj.config.SetString(
'manifest.standalone', 'https://chicken/manifest.git')
"manifest.standalone", "https://chicken/manifest.git"
)
self.assertEqual(
fakeproj.standalone_manifest_url, 'https://chicken/manifest.git')
fakeproj.standalone_manifest_url, "https://chicken/manifest.git"
)
fakeproj.config.SetString('manifest.groups', 'test-group, admin-group')
self.assertEqual(fakeproj.manifest_groups, 'test-group, admin-group')
fakeproj.config.SetString(
"manifest.groups", "test-group, admin-group"
)
self.assertEqual(
fakeproj.manifest_groups, "test-group, admin-group"
)
fakeproj.config.SetString('repo.reference', 'mirror/ref')
self.assertEqual(fakeproj.reference, 'mirror/ref')
fakeproj.config.SetString("repo.reference", "mirror/ref")
self.assertEqual(fakeproj.reference, "mirror/ref")
fakeproj.config.SetBoolean('repo.dissociate', False)
fakeproj.config.SetBoolean("repo.dissociate", False)
self.assertFalse(fakeproj.dissociate)
fakeproj.config.SetBoolean('repo.archive', False)
fakeproj.config.SetBoolean("repo.archive", False)
self.assertFalse(fakeproj.archive)
fakeproj.config.SetBoolean('repo.mirror', False)
fakeproj.config.SetBoolean("repo.mirror", False)
self.assertFalse(fakeproj.mirror)
fakeproj.config.SetBoolean('repo.worktree', False)
fakeproj.config.SetBoolean("repo.worktree", False)
self.assertFalse(fakeproj.use_worktree)
fakeproj.config.SetBoolean('repo.clonebundle', False)
fakeproj.config.SetBoolean("repo.clonebundle", False)
self.assertFalse(fakeproj.clone_bundle)
fakeproj.config.SetBoolean('repo.submodules', False)
fakeproj.config.SetBoolean("repo.submodules", False)
self.assertFalse(fakeproj.submodules)
fakeproj.config.SetBoolean('repo.git-lfs', False)
fakeproj.config.SetBoolean("repo.git-lfs", False)
self.assertFalse(fakeproj.git_lfs)
fakeproj.config.SetBoolean('repo.superproject', False)
fakeproj.config.SetBoolean("repo.superproject", False)
self.assertFalse(fakeproj.use_superproject)
fakeproj.config.SetBoolean('repo.partialclone', False)
fakeproj.config.SetBoolean("repo.partialclone", False)
self.assertFalse(fakeproj.partial_clone)
fakeproj.config.SetString('repo.depth', '48')
self.assertEqual(fakeproj.depth, '48')
fakeproj.config.SetString("repo.depth", "48")
self.assertEqual(fakeproj.depth, "48")
fakeproj.config.SetString('repo.clonefilter', 'blob:limit=10M')
self.assertEqual(fakeproj.clone_filter, 'blob:limit=10M')
fakeproj.config.SetString("repo.clonefilter", "blob:limit=10M")
self.assertEqual(fakeproj.clone_filter, "blob:limit=10M")
fakeproj.config.SetString('repo.partialcloneexclude', 'third_party/big_repo')
self.assertEqual(fakeproj.partial_clone_exclude, 'third_party/big_repo')
fakeproj.config.SetString(
"repo.partialcloneexclude", "third_party/big_repo"
)
self.assertEqual(
fakeproj.partial_clone_exclude, "third_party/big_repo"
)
fakeproj.config.SetString('manifest.platform', 'auto')
self.assertEqual(fakeproj.manifest_platform, 'auto')
fakeproj.config.SetString("manifest.platform", "auto")
self.assertEqual(fakeproj.manifest_platform, "auto")

View File

@ -25,7 +25,7 @@ class TraceTests(unittest.TestCase):
"""Check Trace behavior."""
def testTrace_MaxSizeEnforced(self):
content = 'git chicken'
content = "git chicken"
with repo_trace.Trace(content, first_trace=True):
pass
@ -34,23 +34,27 @@ class TraceTests(unittest.TestCase):
with repo_trace.Trace(content):
pass
self.assertGreater(
os.path.getsize(repo_trace._TRACE_FILE), first_trace_size)
os.path.getsize(repo_trace._TRACE_FILE), first_trace_size
)
# Check we clear everything is the last chunk is larger than _MAX_SIZE.
with mock.patch('repo_trace._MAX_SIZE', 0):
with mock.patch("repo_trace._MAX_SIZE", 0):
with repo_trace.Trace(content, first_trace=True):
pass
self.assertEqual(first_trace_size,
os.path.getsize(repo_trace._TRACE_FILE))
self.assertEqual(
first_trace_size, os.path.getsize(repo_trace._TRACE_FILE)
)
# Check we only clear the chunks we need to.
repo_trace._MAX_SIZE = (first_trace_size + 1) / (1024 * 1024)
with repo_trace.Trace(content, first_trace=True):
pass
self.assertEqual(first_trace_size * 2,
os.path.getsize(repo_trace._TRACE_FILE))
self.assertEqual(
first_trace_size * 2, os.path.getsize(repo_trace._TRACE_FILE)
)
with repo_trace.Trace(content, first_trace=True):
pass
self.assertEqual(first_trace_size * 2,
os.path.getsize(repo_trace._TRACE_FILE))
self.assertEqual(
first_trace_size * 2, os.path.getsize(repo_trace._TRACE_FILE)
)

View File

@ -27,18 +27,22 @@ class SshTests(unittest.TestCase):
def test_parse_ssh_version(self):
"""Check _parse_ssh_version() handling."""
ver = ssh._parse_ssh_version('Unknown\n')
ver = ssh._parse_ssh_version("Unknown\n")
self.assertEqual(ver, ())
ver = ssh._parse_ssh_version('OpenSSH_1.0\n')
ver = ssh._parse_ssh_version("OpenSSH_1.0\n")
self.assertEqual(ver, (1, 0))
ver = ssh._parse_ssh_version('OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.13, OpenSSL 1.0.1f 6 Jan 2014\n')
ver = ssh._parse_ssh_version(
"OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.13, OpenSSL 1.0.1f 6 Jan 2014\n"
)
self.assertEqual(ver, (6, 6, 1))
ver = ssh._parse_ssh_version('OpenSSH_7.6p1 Ubuntu-4ubuntu0.3, OpenSSL 1.0.2n 7 Dec 2017\n')
ver = ssh._parse_ssh_version(
"OpenSSH_7.6p1 Ubuntu-4ubuntu0.3, OpenSSL 1.0.2n 7 Dec 2017\n"
)
self.assertEqual(ver, (7, 6))
def test_version(self):
"""Check version() handling."""
with mock.patch('ssh._run_ssh_version', return_value='OpenSSH_1.2\n'):
with mock.patch("ssh._run_ssh_version", return_value="OpenSSH_1.2\n"):
self.assertEqual(ssh.version(), (1, 2))
def test_context_manager_empty(self):
@ -51,9 +55,9 @@ class SshTests(unittest.TestCase):
"""Verify orphaned clients & masters get cleaned up."""
with multiprocessing.Manager() as manager:
with ssh.ProxyManager(manager) as ssh_proxy:
client = subprocess.Popen(['sleep', '964853320'])
client = subprocess.Popen(["sleep", "964853320"])
ssh_proxy.add_client(client)
master = subprocess.Popen(['sleep', '964853321'])
master = subprocess.Popen(["sleep", "964853321"])
ssh_proxy.add_master(master)
# If the process still exists, these will throw timeout errors.
client.wait(0)
@ -63,12 +67,12 @@ class SshTests(unittest.TestCase):
"""Check sock() function."""
manager = multiprocessing.Manager()
proxy = ssh.ProxyManager(manager)
with mock.patch('tempfile.mkdtemp', return_value='/tmp/foo'):
# old ssh version uses port
with mock.patch('ssh.version', return_value=(6, 6)):
self.assertTrue(proxy.sock().endswith('%p'))
with mock.patch("tempfile.mkdtemp", return_value="/tmp/foo"):
# Old ssh version uses port.
with mock.patch("ssh.version", return_value=(6, 6)):
self.assertTrue(proxy.sock().endswith("%p"))
proxy._sock_path = None
# new ssh version uses hash
with mock.patch('ssh.version', return_value=(6, 7)):
self.assertTrue(proxy.sock().endswith('%C'))
# New ssh version uses hash.
with mock.patch("ssh.version", return_value=(6, 7)):
self.assertTrue(proxy.sock().endswith("%C"))

View File

@ -25,30 +25,30 @@ class AllCommands(unittest.TestCase):
def test_required_basic(self):
"""Basic checking of registered commands."""
# NB: We don't test all subcommands as we want to avoid "change detection"
# tests, so we just look for the most common/important ones here that are
# unlikely to ever change.
for cmd in {'cherry-pick', 'help', 'init', 'start', 'sync', 'upload'}:
# NB: We don't test all subcommands as we want to avoid "change
# detection" tests, so we just look for the most common/important ones
# here that are unlikely to ever change.
for cmd in {"cherry-pick", "help", "init", "start", "sync", "upload"}:
self.assertIn(cmd, subcmds.all_commands)
def test_naming(self):
"""Verify we don't add things that we shouldn't."""
for cmd in subcmds.all_commands:
# Reject filename suffixes like "help.py".
self.assertNotIn('.', cmd)
self.assertNotIn(".", cmd)
# Make sure all '_' were converted to '-'.
self.assertNotIn('_', cmd)
self.assertNotIn("_", cmd)
# Reject internal python paths like "__init__".
self.assertFalse(cmd.startswith('__'))
self.assertFalse(cmd.startswith("__"))
def test_help_desc_style(self):
"""Force some consistency in option descriptions.
Python's optparse & argparse has a few default options like --help. Their
option description text uses lowercase sentence fragments, so enforce our
options follow the same style so UI is consistent.
Python's optparse & argparse has a few default options like --help.
Their option description text uses lowercase sentence fragments, so
enforce our options follow the same style so UI is consistent.
We enforce:
* Text starts with lowercase.
@ -63,11 +63,15 @@ class AllCommands(unittest.TestCase):
c = option.help[0]
self.assertEqual(
c.lower(), c,
msg=f'subcmds/{name}.py: {option.get_opt_string()}: help text '
f'should start with lowercase: "{option.help}"')
c.lower(),
c,
msg=f"subcmds/{name}.py: {option.get_opt_string()}: "
f'help text should start with lowercase: "{option.help}"',
)
self.assertNotEqual(
option.help[-1], '.',
msg=f'subcmds/{name}.py: {option.get_opt_string()}: help text '
f'should not end in a period: "{option.help}"')
option.help[-1],
".",
msg=f"subcmds/{name}.py: {option.get_opt_string()}: "
f'help text should not end in a period: "{option.help}"',
)

View File

@ -27,9 +27,7 @@ class InitCommand(unittest.TestCase):
def test_cli_parser_good(self):
"""Check valid command line options."""
ARGV = (
[],
)
ARGV = ([],)
for argv in ARGV:
opts, args = self.cmd.OptionParser.parse_args(argv)
self.cmd.ValidateOptions(opts, args)
@ -38,10 +36,9 @@ class InitCommand(unittest.TestCase):
"""Check invalid command line options."""
ARGV = (
# Too many arguments.
['url', 'asdf'],
["url", "asdf"],
# Conflicting options.
['--mirror', '--archive'],
["--mirror", "--archive"],
)
for argv in ARGV:
opts, args = self.cmd.OptionParser.parse_args(argv)

View File

@ -23,56 +23,70 @@ import command
from subcmds import sync
@pytest.mark.parametrize('use_superproject, cli_args, result', [
(True, ['--current-branch'], True),
(True, ['--no-current-branch'], True),
@pytest.mark.parametrize(
"use_superproject, cli_args, result",
[
(True, ["--current-branch"], True),
(True, ["--no-current-branch"], True),
(True, [], True),
(False, ['--current-branch'], True),
(False, ['--no-current-branch'], False),
(False, ["--current-branch"], True),
(False, ["--no-current-branch"], False),
(False, [], None),
])
],
)
def test_get_current_branch_only(use_superproject, cli_args, result):
"""Test Sync._GetCurrentBranchOnly logic.
Sync._GetCurrentBranchOnly should return True if a superproject is requested,
and otherwise the value of the current_branch_only option.
Sync._GetCurrentBranchOnly should return True if a superproject is
requested, and otherwise the value of the current_branch_only option.
"""
cmd = sync.Sync()
opts, _ = cmd.OptionParser.parse_args(cli_args)
with mock.patch('git_superproject.UseSuperproject',
return_value=use_superproject):
with mock.patch(
"git_superproject.UseSuperproject", return_value=use_superproject
):
assert cmd._GetCurrentBranchOnly(opts, cmd.manifest) == result
# Used to patch os.cpu_count() for reliable results.
OS_CPU_COUNT = 24
@pytest.mark.parametrize('argv, jobs_manifest, jobs, jobs_net, jobs_check', [
@pytest.mark.parametrize(
"argv, jobs_manifest, jobs, jobs_net, jobs_check",
[
# No user or manifest settings.
([], None, OS_CPU_COUNT, 1, command.DEFAULT_LOCAL_JOBS),
# No user settings, so manifest settings control.
([], 3, 3, 3, 3),
# User settings, but no manifest.
(['--jobs=4'], None, 4, 4, 4),
(['--jobs=4', '--jobs-network=5'], None, 4, 5, 4),
(['--jobs=4', '--jobs-checkout=6'], None, 4, 4, 6),
(['--jobs=4', '--jobs-network=5', '--jobs-checkout=6'], None, 4, 5, 6),
(['--jobs-network=5'], None, OS_CPU_COUNT, 5, command.DEFAULT_LOCAL_JOBS),
(['--jobs-checkout=6'], None, OS_CPU_COUNT, 1, 6),
(['--jobs-network=5', '--jobs-checkout=6'], None, OS_CPU_COUNT, 5, 6),
(["--jobs=4"], None, 4, 4, 4),
(["--jobs=4", "--jobs-network=5"], None, 4, 5, 4),
(["--jobs=4", "--jobs-checkout=6"], None, 4, 4, 6),
(["--jobs=4", "--jobs-network=5", "--jobs-checkout=6"], None, 4, 5, 6),
(
["--jobs-network=5"],
None,
OS_CPU_COUNT,
5,
command.DEFAULT_LOCAL_JOBS,
),
(["--jobs-checkout=6"], None, OS_CPU_COUNT, 1, 6),
(["--jobs-network=5", "--jobs-checkout=6"], None, OS_CPU_COUNT, 5, 6),
# User settings with manifest settings.
(['--jobs=4'], 3, 4, 4, 4),
(['--jobs=4', '--jobs-network=5'], 3, 4, 5, 4),
(['--jobs=4', '--jobs-checkout=6'], 3, 4, 4, 6),
(['--jobs=4', '--jobs-network=5', '--jobs-checkout=6'], 3, 4, 5, 6),
(['--jobs-network=5'], 3, 3, 5, 3),
(['--jobs-checkout=6'], 3, 3, 3, 6),
(['--jobs-network=5', '--jobs-checkout=6'], 3, 3, 5, 6),
(["--jobs=4"], 3, 4, 4, 4),
(["--jobs=4", "--jobs-network=5"], 3, 4, 5, 4),
(["--jobs=4", "--jobs-checkout=6"], 3, 4, 4, 6),
(["--jobs=4", "--jobs-network=5", "--jobs-checkout=6"], 3, 4, 5, 6),
(["--jobs-network=5"], 3, 3, 5, 3),
(["--jobs-checkout=6"], 3, 3, 3, 6),
(["--jobs-network=5", "--jobs-checkout=6"], 3, 3, 5, 6),
# Settings that exceed rlimits get capped.
(['--jobs=1000000'], None, 83, 83, 83),
(["--jobs=1000000"], None, 83, 83, 83),
([], 1000000, 83, 83, 83),
])
],
)
def test_cli_jobs(argv, jobs_manifest, jobs, jobs_net, jobs_check):
"""Tests --jobs option behavior."""
mp = mock.MagicMock()
@ -82,8 +96,8 @@ def test_cli_jobs(argv, jobs_manifest, jobs, jobs_net, jobs_check):
opts, args = cmd.OptionParser.parse_args(argv)
cmd.ValidateOptions(opts, args)
with mock.patch.object(sync, '_rlimit_nofile', return_value=(256, 256)):
with mock.patch.object(os, 'cpu_count', return_value=OS_CPU_COUNT):
with mock.patch.object(sync, "_rlimit_nofile", return_value=(256, 256)):
with mock.patch.object(os, "cpu_count", return_value=OS_CPU_COUNT):
cmd._ValidateOptionsWithManifest(opts, mp)
assert opts.jobs == jobs
assert opts.jobs_network == jobs_net
@ -96,38 +110,51 @@ class GetPreciousObjectsState(unittest.TestCase):
def setUp(self):
"""Common setup."""
self.cmd = sync.Sync()
self.project = p = mock.MagicMock(use_git_worktrees=False,
UseAlternates=False)
self.project = p = mock.MagicMock(
use_git_worktrees=False, UseAlternates=False
)
p.manifest.GetProjectsWithName.return_value = [p]
self.opt = mock.Mock(spec_set=['this_manifest_only'])
self.opt = mock.Mock(spec_set=["this_manifest_only"])
self.opt.this_manifest_only = False
def test_worktrees(self):
"""False for worktrees."""
self.project.use_git_worktrees = True
self.assertFalse(self.cmd._GetPreciousObjectsState(self.project, self.opt))
self.assertFalse(
self.cmd._GetPreciousObjectsState(self.project, self.opt)
)
def test_not_shared(self):
"""Singleton project."""
self.assertFalse(self.cmd._GetPreciousObjectsState(self.project, self.opt))
self.assertFalse(
self.cmd._GetPreciousObjectsState(self.project, self.opt)
)
def test_shared(self):
"""Shared project."""
self.project.manifest.GetProjectsWithName.return_value = [
self.project, self.project
self.project,
self.project,
]
self.assertTrue(self.cmd._GetPreciousObjectsState(self.project, self.opt))
self.assertTrue(
self.cmd._GetPreciousObjectsState(self.project, self.opt)
)
def test_shared_with_alternates(self):
"""Shared project, with alternates."""
self.project.manifest.GetProjectsWithName.return_value = [
self.project, self.project
self.project,
self.project,
]
self.project.UseAlternates = True
self.assertFalse(self.cmd._GetPreciousObjectsState(self.project, self.opt))
self.assertFalse(
self.cmd._GetPreciousObjectsState(self.project, self.opt)
)
def test_not_found(self):
"""Project not found in manifest."""
self.project.manifest.GetProjectsWithName.return_value = []
self.assertFalse(self.cmd._GetPreciousObjectsState(self.project, self.opt))
self.assertFalse(
self.cmd._GetPreciousObjectsState(self.project, self.opt)
)

View File

@ -24,5 +24,5 @@ class UpdateManpagesTest(unittest.TestCase):
def test_replace_regex(self):
"""Check that replace_regex works."""
data = '\n\033[1mSummary\033[m\n'
self.assertEqual(update_manpages.replace_regex(data),'\nSummary\n')
data = "\n\033[1mSummary\033[m\n"
self.assertEqual(update_manpages.replace_regex(data), "\nSummary\n")

View File

@ -28,9 +28,8 @@ import wrapper
def fixture(*paths):
"""Return a path relative to tests/fixtures.
"""
return os.path.join(os.path.dirname(__file__), 'fixtures', *paths)
"""Return a path relative to tests/fixtures."""
return os.path.join(os.path.dirname(__file__), "fixtures", *paths)
class RepoWrapperTestCase(unittest.TestCase):
@ -43,28 +42,31 @@ class RepoWrapperTestCase(unittest.TestCase):
class RepoWrapperUnitTest(RepoWrapperTestCase):
"""Tests helper functions in the repo wrapper
"""
"""Tests helper functions in the repo wrapper"""
def test_version(self):
"""Make sure _Version works."""
with self.assertRaises(SystemExit) as e:
with mock.patch('sys.stdout', new_callable=StringIO) as stdout:
with mock.patch('sys.stderr', new_callable=StringIO) as stderr:
with mock.patch("sys.stdout", new_callable=StringIO) as stdout:
with mock.patch("sys.stderr", new_callable=StringIO) as stderr:
self.wrapper._Version()
self.assertEqual(0, e.exception.code)
self.assertEqual('', stderr.getvalue())
self.assertIn('repo launcher version', stdout.getvalue())
self.assertEqual("", stderr.getvalue())
self.assertIn("repo launcher version", stdout.getvalue())
def test_python_constraints(self):
"""The launcher should never require newer than main.py."""
self.assertGreaterEqual(main.MIN_PYTHON_VERSION_HARD,
self.wrapper.MIN_PYTHON_VERSION_HARD)
self.assertGreaterEqual(main.MIN_PYTHON_VERSION_SOFT,
self.wrapper.MIN_PYTHON_VERSION_SOFT)
self.assertGreaterEqual(
main.MIN_PYTHON_VERSION_HARD, self.wrapper.MIN_PYTHON_VERSION_HARD
)
self.assertGreaterEqual(
main.MIN_PYTHON_VERSION_SOFT, self.wrapper.MIN_PYTHON_VERSION_SOFT
)
# Make sure the versions are themselves in sync.
self.assertGreaterEqual(self.wrapper.MIN_PYTHON_VERSION_SOFT,
self.wrapper.MIN_PYTHON_VERSION_HARD)
self.assertGreaterEqual(
self.wrapper.MIN_PYTHON_VERSION_SOFT,
self.wrapper.MIN_PYTHON_VERSION_HARD,
)
def test_init_parser(self):
"""Make sure 'init' GetParser works."""
@ -84,48 +86,76 @@ class RepoWrapperUnitTest(RepoWrapperTestCase):
"""
Test reading a missing gitc config file
"""
self.wrapper.GITC_CONFIG_FILE = fixture('missing_gitc_config')
self.wrapper.GITC_CONFIG_FILE = fixture("missing_gitc_config")
val = self.wrapper.get_gitc_manifest_dir()
self.assertEqual(val, '')
self.assertEqual(val, "")
def test_get_gitc_manifest_dir(self):
"""
Test reading the gitc config file and parsing the directory
"""
self.wrapper.GITC_CONFIG_FILE = fixture('gitc_config')
self.wrapper.GITC_CONFIG_FILE = fixture("gitc_config")
val = self.wrapper.get_gitc_manifest_dir()
self.assertEqual(val, '/test/usr/local/google/gitc')
self.assertEqual(val, "/test/usr/local/google/gitc")
def test_gitc_parse_clientdir_no_gitc(self):
"""
Test parsing the gitc clientdir without gitc running
"""
self.wrapper.GITC_CONFIG_FILE = fixture('missing_gitc_config')
self.assertEqual(self.wrapper.gitc_parse_clientdir('/something'), None)
self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/test'), 'test')
self.wrapper.GITC_CONFIG_FILE = fixture("missing_gitc_config")
self.assertEqual(self.wrapper.gitc_parse_clientdir("/something"), None)
self.assertEqual(
self.wrapper.gitc_parse_clientdir("/gitc/manifest-rw/test"), "test"
)
def test_gitc_parse_clientdir(self):
"""
Test parsing the gitc clientdir
"""
self.wrapper.GITC_CONFIG_FILE = fixture('gitc_config')
self.assertEqual(self.wrapper.gitc_parse_clientdir('/something'), None)
self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/test'), 'test')
self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/test/'), 'test')
self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/test/extra'), 'test')
self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/test'), 'test')
self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/test/'), 'test')
self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/test/extra'),
'test')
self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/'), None)
self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/'), None)
self.wrapper.GITC_CONFIG_FILE = fixture("gitc_config")
self.assertEqual(self.wrapper.gitc_parse_clientdir("/something"), None)
self.assertEqual(
self.wrapper.gitc_parse_clientdir("/gitc/manifest-rw/test"), "test"
)
self.assertEqual(
self.wrapper.gitc_parse_clientdir("/gitc/manifest-rw/test/"), "test"
)
self.assertEqual(
self.wrapper.gitc_parse_clientdir("/gitc/manifest-rw/test/extra"),
"test",
)
self.assertEqual(
self.wrapper.gitc_parse_clientdir(
"/test/usr/local/google/gitc/test"
),
"test",
)
self.assertEqual(
self.wrapper.gitc_parse_clientdir(
"/test/usr/local/google/gitc/test/"
),
"test",
)
self.assertEqual(
self.wrapper.gitc_parse_clientdir(
"/test/usr/local/google/gitc/test/extra"
),
"test",
)
self.assertEqual(
self.wrapper.gitc_parse_clientdir("/gitc/manifest-rw/"), None
)
self.assertEqual(
self.wrapper.gitc_parse_clientdir("/test/usr/local/google/gitc/"),
None,
)
class SetGitTrace2ParentSid(RepoWrapperTestCase):
"""Check SetGitTrace2ParentSid behavior."""
KEY = 'GIT_TRACE2_PARENT_SID'
VALID_FORMAT = re.compile(r'^repo-[0-9]{8}T[0-9]{6}Z-P[0-9a-f]{8}$')
KEY = "GIT_TRACE2_PARENT_SID"
VALID_FORMAT = re.compile(r"^repo-[0-9]{8}T[0-9]{6}Z-P[0-9a-f]{8}$")
def test_first_set(self):
"""Test env var not yet set."""
@ -137,11 +167,11 @@ class SetGitTrace2ParentSid(RepoWrapperTestCase):
def test_append(self):
"""Test env var is appended."""
env = {self.KEY: 'pfx'}
env = {self.KEY: "pfx"}
self.wrapper.SetGitTrace2ParentSid(env)
self.assertIn(self.KEY, env)
value = env[self.KEY]
self.assertTrue(value.startswith('pfx/'))
self.assertTrue(value.startswith("pfx/"))
self.assertRegex(value[4:], self.VALID_FORMAT)
def test_global_context(self):
@ -158,18 +188,18 @@ class RunCommand(RepoWrapperTestCase):
def test_capture(self):
"""Check capture_output handling."""
ret = self.wrapper.run_command(['echo', 'hi'], capture_output=True)
ret = self.wrapper.run_command(["echo", "hi"], capture_output=True)
# echo command appends OS specific linesep, but on Windows + Git Bash
# we get UNIX ending, so we allow both.
self.assertIn(ret.stdout, ['hi' + os.linesep, 'hi\n'])
self.assertIn(ret.stdout, ["hi" + os.linesep, "hi\n"])
def test_check(self):
"""Check check handling."""
self.wrapper.run_command(['true'], check=False)
self.wrapper.run_command(['true'], check=True)
self.wrapper.run_command(['false'], check=False)
self.wrapper.run_command(["true"], check=False)
self.wrapper.run_command(["true"], check=True)
self.wrapper.run_command(["false"], check=False)
with self.assertRaises(self.wrapper.RunError):
self.wrapper.run_command(['false'], check=True)
self.wrapper.run_command(["false"], check=True)
class RunGit(RepoWrapperTestCase):
@ -177,14 +207,14 @@ class RunGit(RepoWrapperTestCase):
def test_capture(self):
"""Check capture_output handling."""
ret = self.wrapper.run_git('--version')
self.assertIn('git', ret.stdout)
ret = self.wrapper.run_git("--version")
self.assertIn("git", ret.stdout)
def test_check(self):
"""Check check handling."""
with self.assertRaises(self.wrapper.CloneFailure):
self.wrapper.run_git('--version-asdfasdf')
self.wrapper.run_git('--version-asdfasdf', check=False)
self.wrapper.run_git("--version-asdfasdf")
self.wrapper.run_git("--version-asdfasdf", check=False)
class ParseGitVersion(RepoWrapperTestCase):
@ -197,25 +227,26 @@ class ParseGitVersion(RepoWrapperTestCase):
def test_bad_ver(self):
"""Check handling of bad git versions."""
ret = self.wrapper.ParseGitVersion(ver_str='asdf')
ret = self.wrapper.ParseGitVersion(ver_str="asdf")
self.assertIsNone(ret)
def test_normal_ver(self):
"""Check handling of normal git versions."""
ret = self.wrapper.ParseGitVersion(ver_str='git version 2.25.1')
ret = self.wrapper.ParseGitVersion(ver_str="git version 2.25.1")
self.assertEqual(2, ret.major)
self.assertEqual(25, ret.minor)
self.assertEqual(1, ret.micro)
self.assertEqual('2.25.1', ret.full)
self.assertEqual("2.25.1", ret.full)
def test_extended_ver(self):
"""Check handling of extended distro git versions."""
ret = self.wrapper.ParseGitVersion(
ver_str='git version 1.30.50.696.g5e7596f4ac-goog')
ver_str="git version 1.30.50.696.g5e7596f4ac-goog"
)
self.assertEqual(1, ret.major)
self.assertEqual(30, ret.minor)
self.assertEqual(50, ret.micro)
self.assertEqual('1.30.50.696.g5e7596f4ac-goog', ret.full)
self.assertEqual("1.30.50.696.g5e7596f4ac-goog", ret.full)
class CheckGitVersion(RepoWrapperTestCase):
@ -223,23 +254,29 @@ class CheckGitVersion(RepoWrapperTestCase):
def test_unknown(self):
"""Unknown versions should abort."""
with mock.patch.object(self.wrapper, 'ParseGitVersion', return_value=None):
with mock.patch.object(
self.wrapper, "ParseGitVersion", return_value=None
):
with self.assertRaises(self.wrapper.CloneFailure):
self.wrapper._CheckGitVersion()
def test_old(self):
"""Old versions should abort."""
with mock.patch.object(
self.wrapper, 'ParseGitVersion',
return_value=self.wrapper.GitVersion(1, 0, 0, '1.0.0')):
self.wrapper,
"ParseGitVersion",
return_value=self.wrapper.GitVersion(1, 0, 0, "1.0.0"),
):
with self.assertRaises(self.wrapper.CloneFailure):
self.wrapper._CheckGitVersion()
def test_new(self):
"""Newer versions should run fine."""
with mock.patch.object(
self.wrapper, 'ParseGitVersion',
return_value=self.wrapper.GitVersion(100, 0, 0, '100.0.0')):
self.wrapper,
"ParseGitVersion",
return_value=self.wrapper.GitVersion(100, 0, 0, "100.0.0"),
):
self.wrapper._CheckGitVersion()
@ -250,26 +287,34 @@ class Requirements(RepoWrapperTestCase):
"""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')))
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'))
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'{}'))
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')))
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]))
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."""
@ -278,44 +323,48 @@ class Requirements(RepoWrapperTestCase):
def test_assert_all_new_repo(self):
"""Check assert_all accepts new enough repo."""
reqs = self.wrapper.Requirements({'repo': {'hard': [1, 0]}})
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]}})
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 = self.wrapper.Requirements({"python": {"hard": sys.version_info}})
reqs.assert_all()
def test_assert_all_old_python(self):
"""Check assert_all rejects old python."""
reqs = self.wrapper.Requirements({'python': {'hard': [99999, 0]}})
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))
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))
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]}})
reqs = self.wrapper.Requirements(
{"git": {"hard": [1, 0], "soft": [2, 0]}}
)
with self.assertRaises(SystemExit):
reqs.assert_ver('git', (0, 5))
reqs.assert_ver("git", (0, 5))
class NeedSetupGnuPG(RepoWrapperTestCase):
@ -323,38 +372,38 @@ class NeedSetupGnuPG(RepoWrapperTestCase):
def test_missing_dir(self):
"""The ~/.repoconfig tree doesn't exist yet."""
with tempfile.TemporaryDirectory(prefix='repo-tests') as tempdir:
self.wrapper.home_dot_repo = os.path.join(tempdir, 'foo')
with tempfile.TemporaryDirectory(prefix="repo-tests") as tempdir:
self.wrapper.home_dot_repo = os.path.join(tempdir, "foo")
self.assertTrue(self.wrapper.NeedSetupGnuPG())
def test_missing_keyring(self):
"""The keyring-version file doesn't exist yet."""
with tempfile.TemporaryDirectory(prefix='repo-tests') as tempdir:
with tempfile.TemporaryDirectory(prefix="repo-tests") as tempdir:
self.wrapper.home_dot_repo = tempdir
self.assertTrue(self.wrapper.NeedSetupGnuPG())
def test_empty_keyring(self):
"""The keyring-version file exists, but is empty."""
with tempfile.TemporaryDirectory(prefix='repo-tests') as tempdir:
with tempfile.TemporaryDirectory(prefix="repo-tests") as 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
self.assertTrue(self.wrapper.NeedSetupGnuPG())
def test_old_keyring(self):
"""The keyring-version file exists, but it's old."""
with tempfile.TemporaryDirectory(prefix='repo-tests') as tempdir:
with tempfile.TemporaryDirectory(prefix="repo-tests") as tempdir:
self.wrapper.home_dot_repo = tempdir
with open(os.path.join(tempdir, 'keyring-version'), 'w') as fp:
fp.write('1.0\n')
with open(os.path.join(tempdir, "keyring-version"), "w") as fp:
fp.write("1.0\n")
self.assertTrue(self.wrapper.NeedSetupGnuPG())
def test_new_keyring(self):
"""The keyring-version file exists, and is up-to-date."""
with tempfile.TemporaryDirectory(prefix='repo-tests') as tempdir:
with tempfile.TemporaryDirectory(prefix="repo-tests") as tempdir:
self.wrapper.home_dot_repo = tempdir
with open(os.path.join(tempdir, 'keyring-version'), 'w') as fp:
fp.write('1000.0\n')
with open(os.path.join(tempdir, "keyring-version"), "w") as fp:
fp.write("1000.0\n")
self.assertFalse(self.wrapper.NeedSetupGnuPG())
@ -363,14 +412,18 @@ class SetupGnuPG(RepoWrapperTestCase):
def test_full(self):
"""Make sure it works completely."""
with tempfile.TemporaryDirectory(prefix='repo-tests') as tempdir:
with tempfile.TemporaryDirectory(prefix="repo-tests") as 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))
with open(os.path.join(tempdir, 'keyring-version'), 'r') as fp:
with open(os.path.join(tempdir, "keyring-version"), "r") as fp:
data = fp.read()
self.assertEqual('.'.join(str(x) for x in self.wrapper.KEYRING_VERSION),
data.strip())
self.assertEqual(
".".join(str(x) for x in self.wrapper.KEYRING_VERSION),
data.strip(),
)
class VerifyRev(RepoWrapperTestCase):
@ -378,30 +431,37 @@ class VerifyRev(RepoWrapperTestCase):
def test_verify_passes(self):
"""Check when we have a valid signed tag."""
desc_result = self.wrapper.RunResult(0, 'v1.0\n', '')
gpg_result = self.wrapper.RunResult(0, '', '')
with mock.patch.object(self.wrapper, 'run_git',
side_effect=(desc_result, gpg_result)):
ret = self.wrapper.verify_rev('/', 'refs/heads/stable', '1234', True)
self.assertEqual('v1.0^0', ret)
desc_result = self.wrapper.RunResult(0, "v1.0\n", "")
gpg_result = self.wrapper.RunResult(0, "", "")
with mock.patch.object(
self.wrapper, "run_git", side_effect=(desc_result, gpg_result)
):
ret = self.wrapper.verify_rev(
"/", "refs/heads/stable", "1234", True
)
self.assertEqual("v1.0^0", ret)
def test_unsigned_commit(self):
"""Check we fall back to signed tag when we have an unsigned commit."""
desc_result = self.wrapper.RunResult(0, 'v1.0-10-g1234\n', '')
gpg_result = self.wrapper.RunResult(0, '', '')
with mock.patch.object(self.wrapper, 'run_git',
side_effect=(desc_result, gpg_result)):
ret = self.wrapper.verify_rev('/', 'refs/heads/stable', '1234', True)
self.assertEqual('v1.0^0', ret)
desc_result = self.wrapper.RunResult(0, "v1.0-10-g1234\n", "")
gpg_result = self.wrapper.RunResult(0, "", "")
with mock.patch.object(
self.wrapper, "run_git", side_effect=(desc_result, gpg_result)
):
ret = self.wrapper.verify_rev(
"/", "refs/heads/stable", "1234", True
)
self.assertEqual("v1.0^0", ret)
def test_verify_fails(self):
"""Check we fall back to signed tag when we have an unsigned commit."""
desc_result = self.wrapper.RunResult(0, 'v1.0-10-g1234\n', '')
desc_result = self.wrapper.RunResult(0, "v1.0-10-g1234\n", "")
gpg_result = Exception
with mock.patch.object(self.wrapper, 'run_git',
side_effect=(desc_result, gpg_result)):
with mock.patch.object(
self.wrapper, "run_git", side_effect=(desc_result, gpg_result)
):
with self.assertRaises(Exception):
self.wrapper.verify_rev('/', 'refs/heads/stable', '1234', True)
self.wrapper.verify_rev("/", "refs/heads/stable", "1234", True)
class GitCheckoutTestCase(RepoWrapperTestCase):
@ -413,33 +473,40 @@ class GitCheckoutTestCase(RepoWrapperTestCase):
@classmethod
def setUpClass(cls):
# Create a repo to operate on, but do it once per-class.
cls.tempdirobj = tempfile.TemporaryDirectory(prefix='repo-rev-tests')
cls.tempdirobj = tempfile.TemporaryDirectory(prefix="repo-rev-tests")
cls.GIT_DIR = cls.tempdirobj.name
run_git = wrapper.Wrapper().run_git
remote = os.path.join(cls.GIT_DIR, 'remote')
remote = os.path.join(cls.GIT_DIR, "remote")
os.mkdir(remote)
# Tests need to assume, that main is default branch at init,
# which is not supported in config until 2.28.
if git_command.git_require((2, 28, 0)):
initstr = '--initial-branch=main'
initstr = "--initial-branch=main"
else:
# Use template dir for init.
templatedir = tempfile.mkdtemp(prefix='.test-template')
with open(os.path.join(templatedir, 'HEAD'), 'w') as fp:
fp.write('ref: refs/heads/main\n')
initstr = '--template=' + templatedir
templatedir = tempfile.mkdtemp(prefix=".test-template")
with open(os.path.join(templatedir, "HEAD"), "w") as fp:
fp.write("ref: refs/heads/main\n")
initstr = "--template=" + templatedir
run_git('init', initstr, cwd=remote)
run_git('commit', '--allow-empty', '-minit', cwd=remote)
run_git('branch', 'stable', cwd=remote)
run_git('tag', 'v1.0', cwd=remote)
run_git('commit', '--allow-empty', '-m2nd commit', cwd=remote)
cls.REV_LIST = run_git('rev-list', 'HEAD', cwd=remote).stdout.splitlines()
run_git("init", initstr, cwd=remote)
run_git("commit", "--allow-empty", "-minit", cwd=remote)
run_git("branch", "stable", cwd=remote)
run_git("tag", "v1.0", cwd=remote)
run_git("commit", "--allow-empty", "-m2nd commit", cwd=remote)
cls.REV_LIST = run_git(
"rev-list", "HEAD", cwd=remote
).stdout.splitlines()
run_git('init', cwd=cls.GIT_DIR)
run_git('fetch', remote, '+refs/heads/*:refs/remotes/origin/*', cwd=cls.GIT_DIR)
run_git("init", cwd=cls.GIT_DIR)
run_git(
"fetch",
remote,
"+refs/heads/*:refs/remotes/origin/*",
cwd=cls.GIT_DIR,
)
@classmethod
def tearDownClass(cls):
@ -454,36 +521,40 @@ class ResolveRepoRev(GitCheckoutTestCase):
def test_explicit_branch(self):
"""Check refs/heads/branch argument."""
rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, 'refs/heads/stable')
self.assertEqual('refs/heads/stable', rrev)
rrev, lrev = self.wrapper.resolve_repo_rev(
self.GIT_DIR, "refs/heads/stable"
)
self.assertEqual("refs/heads/stable", rrev)
self.assertEqual(self.REV_LIST[1], lrev)
with self.assertRaises(self.wrapper.CloneFailure):
self.wrapper.resolve_repo_rev(self.GIT_DIR, 'refs/heads/unknown')
self.wrapper.resolve_repo_rev(self.GIT_DIR, "refs/heads/unknown")
def test_explicit_tag(self):
"""Check refs/tags/tag argument."""
rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, 'refs/tags/v1.0')
self.assertEqual('refs/tags/v1.0', rrev)
rrev, lrev = self.wrapper.resolve_repo_rev(
self.GIT_DIR, "refs/tags/v1.0"
)
self.assertEqual("refs/tags/v1.0", rrev)
self.assertEqual(self.REV_LIST[1], lrev)
with self.assertRaises(self.wrapper.CloneFailure):
self.wrapper.resolve_repo_rev(self.GIT_DIR, 'refs/tags/unknown')
self.wrapper.resolve_repo_rev(self.GIT_DIR, "refs/tags/unknown")
def test_branch_name(self):
"""Check branch argument."""
rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, 'stable')
self.assertEqual('refs/heads/stable', rrev)
rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, "stable")
self.assertEqual("refs/heads/stable", rrev)
self.assertEqual(self.REV_LIST[1], lrev)
rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, 'main')
self.assertEqual('refs/heads/main', rrev)
rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, "main")
self.assertEqual("refs/heads/main", rrev)
self.assertEqual(self.REV_LIST[0], lrev)
def test_tag_name(self):
"""Check tag argument."""
rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, 'v1.0')
self.assertEqual('refs/tags/v1.0', rrev)
rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, "v1.0")
self.assertEqual("refs/tags/v1.0", rrev)
self.assertEqual(self.REV_LIST[1], lrev)
def test_full_commit(self):
@ -503,7 +574,7 @@ class ResolveRepoRev(GitCheckoutTestCase):
def test_unknown(self):
"""Check unknown ref/commit argument."""
with self.assertRaises(self.wrapper.CloneFailure):
self.wrapper.resolve_repo_rev(self.GIT_DIR, 'boooooooya')
self.wrapper.resolve_repo_rev(self.GIT_DIR, "boooooooya")
class CheckRepoVerify(RepoWrapperTestCase):
@ -515,13 +586,17 @@ class CheckRepoVerify(RepoWrapperTestCase):
def test_gpg_initialized(self):
"""Should pass if gpg is setup already."""
with mock.patch.object(self.wrapper, 'NeedSetupGnuPG', return_value=False):
with mock.patch.object(
self.wrapper, "NeedSetupGnuPG", return_value=False
):
self.assertTrue(self.wrapper.check_repo_verify(True))
def test_need_gpg_setup(self):
"""Should pass/fail based on gpg setup."""
with mock.patch.object(self.wrapper, 'NeedSetupGnuPG', return_value=True):
with mock.patch.object(self.wrapper, 'SetupGnuPG') as m:
with mock.patch.object(
self.wrapper, "NeedSetupGnuPG", return_value=True
):
with mock.patch.object(self.wrapper, "SetupGnuPG") as m:
m.return_value = True
self.assertTrue(self.wrapper.check_repo_verify(True))
@ -534,22 +609,34 @@ class CheckRepoRev(GitCheckoutTestCase):
def test_verify_works(self):
"""Should pass when verification passes."""
with mock.patch.object(self.wrapper, 'check_repo_verify', return_value=True):
with mock.patch.object(self.wrapper, 'verify_rev', return_value='12345'):
rrev, lrev = self.wrapper.check_repo_rev(self.GIT_DIR, 'stable')
self.assertEqual('refs/heads/stable', rrev)
self.assertEqual('12345', lrev)
with mock.patch.object(
self.wrapper, "check_repo_verify", return_value=True
):
with mock.patch.object(
self.wrapper, "verify_rev", return_value="12345"
):
rrev, lrev = self.wrapper.check_repo_rev(self.GIT_DIR, "stable")
self.assertEqual("refs/heads/stable", rrev)
self.assertEqual("12345", lrev)
def test_verify_fails(self):
"""Should fail when verification fails."""
with mock.patch.object(self.wrapper, 'check_repo_verify', return_value=True):
with mock.patch.object(self.wrapper, 'verify_rev', side_effect=Exception):
with mock.patch.object(
self.wrapper, "check_repo_verify", return_value=True
):
with mock.patch.object(
self.wrapper, "verify_rev", side_effect=Exception
):
with self.assertRaises(Exception):
self.wrapper.check_repo_rev(self.GIT_DIR, 'stable')
self.wrapper.check_repo_rev(self.GIT_DIR, "stable")
def test_verify_ignore(self):
"""Should pass when verification is disabled."""
with mock.patch.object(self.wrapper, 'verify_rev', side_effect=Exception):
rrev, lrev = self.wrapper.check_repo_rev(self.GIT_DIR, 'stable', repo_verify=False)
self.assertEqual('refs/heads/stable', rrev)
with mock.patch.object(
self.wrapper, "verify_rev", side_effect=Exception
):
rrev, lrev = self.wrapper.check_repo_rev(
self.GIT_DIR, "stable", repo_verify=False
)
self.assertEqual("refs/heads/stable", rrev)
self.assertEqual(self.REV_LIST[1], lrev)

View File

@ -27,6 +27,7 @@ python =
[testenv]
deps =
black
pytest
pytest-timeout
commands = {envpython} run_tests {posargs}

View File

@ -19,12 +19,12 @@ import os
def WrapperPath():
return os.path.join(os.path.dirname(__file__), 'repo')
return os.path.join(os.path.dirname(__file__), "repo")
@functools.lru_cache(maxsize=None)
def Wrapper():
modname = 'wrapper'
modname = "wrapper"
loader = importlib.machinery.SourceFileLoader(modname, WrapperPath())
spec = importlib.util.spec_from_loader(modname, loader)
module = importlib.util.module_from_spec(spec)