blob: 413d8634949170bf5b339b047dfda8abcd79e0e4 [file] [log] [blame]
#!/usr/bin/python3
import argparse, binascii, configparser, email.message, git, io, logging, math, os
import os.path, re, subprocess, sys, yaml
def read_dotconfig():
xdg_config_home = os.path.join(os.path.expanduser('~'), '.config')
filename = os.path.join(xdg_config_home, 'tms', 'config')
config = configparser.ConfigParser()
try:
with io.open(filename, 'r') as fobj:
config.read_file(fobj)
return config
except:
return None
class fragile(object):
class Break(Exception):
'''Break out of the with statement'''
def __init__(self, value):
self.value = value
def __enter__(self):
return self.value.__enter__()
def __exit__(self, etype, value, traceback):
error = self.value.__exit__(etype, value, traceback)
if etype == self.Break:
return True
return error
class Log:
ESC = '\033'
CSI = ESC + '['
RESET = CSI + 'm'
COLOR_NONE = '30'
COLOR_RED = '31'
COLOR_GREEN = '32'
COLOR_YELLOW = '33'
COLOR_BLUE = '34'
COLOR_MAGENTA = '35'
STYLE_NONE = '0'
STYLE_BOLD = '1'
STYLE_DIM = '2'
STYLE_ITALIC = '3'
STYLE_UNDERLINE = '4'
def __init__(self, colorize = True):
self.color = Log.COLOR_NONE
self.style = Log.STYLE_NONE
self.colorize = colorize
self.stack = []
def push(self, obj, color = COLOR_NONE, style = STYLE_NONE):
if not self.colorize:
color = None
style = None
if color is None and style is None:
return str(obj)
self.stack.append((self.color, self.style))
self.color = color
self.style = style
return Log.CSI + self.style + ';' + self.color + 'm' + str(obj)
def pop(self, obj = None):
if not self.colorize:
return str(obj)
if self.stack:
self.color, self.style = self.stack.pop()
else:
print('ERROR: unbalanced pop()')
self.color = Log.COLOR_NONE
self.style = Log.STYLE_NONE
if obj is not None:
return Log.CSI + self.style + ';' + self.color + 'm' + str(obj)
def wrap(self, obj, color = COLOR_NONE, style = STYLE_NONE):
if not self.colorize:
color = None
style = None
if color is None and style is None:
return str(obj)
return Log.CSI + style + ';' + color + 'm' + str(obj) + Log.RESET
def red(self, obj, push = False, style = STYLE_NONE):
if push:
return self.push(obj, Log.COLOR_RED, style)
else:
return self.wrap(obj, Log.COLOR_RED, style)
def green(self, obj, push = False, style = STYLE_NONE):
if push:
return self.push(obj, Log.COLOR_GREEN, style)
else:
return self.wrap(obj, Log.COLOR_GREEN, style)
def yellow(self, obj, push = False, style = STYLE_NONE):
if push:
return self.push(obj, Log.COLOR_YELLOW, style)
else:
return self.wrap(obj, Log.COLOR_YELLOW, style)
def yellow(self, obj, push = False, style = STYLE_NONE):
if push:
return self.push(obj, Log.COLOR_YELLOW, style)
else:
return self.wrap(obj, Log.COLOR_YELLOW, style)
def blue(self, obj, push = False, style = STYLE_NONE):
if push:
return self.push(obj, Log.COLOR_BLUE, style)
else:
return self.wrap(obj, Log.COLOR_BLUE, style)
def magenta(self, obj, push = False, style = STYLE_NONE):
if push:
return self.push(obj, Log.COLOR_MAGENTA, style)
else:
return self.wrap(obj, Log.COLOR_MAGENTA, style)
class CrossCompiler:
def __init__(self, arch):
self.prefix = None
self.path = None
filename = os.path.expanduser('~/.cross-compile')
with open(filename, 'r') as fobj:
for line in fobj:
if line.startswith('#'):
continue
key, value = line.split(':', 1)
if key == 'path':
value = os.path.expandvars(value.strip())
if self.path:
self.path = ':'.join([self.path, value])
else:
self.path = value
continue
if key == arch:
self.prefix = value.strip()
self.arch = key
break
else:
raise Exception('foobar')
class Remote:
def __init__(self, name, data):
self.name = name
self.push = None
self.pull = None
if 'push' in data:
self.push = data['push']
if 'pull' in data:
self.pull = data['pull']
def __str__(self):
return self.name
def dump(self, indent = 0, output = sys.stdout):
prefix = ' ' * indent
print('%s%s: %s' % (prefix, self.name, self.url), file = output)
class Target:
def __init__(self, name, data):
self.name = name
self.prefix = None
self.data = data
if 'tag-prefix' in data:
self.prefix = data['tag-prefix']
else:
self.prefix = ''
if 'from' in data:
self.author = data['from']
else:
self.author = None
if 'to' in data:
self.to = data['to']
else:
self.to = []
if 'cc' in data:
self.cc = data['cc']
else:
self.cc = []
if 'addressee' in data:
self.addressee = data['addressee']
else:
self.addressee = None
def __str__(self):
return self.name
def dump(self, indent = 0, output = sys.stdout):
prefix = ' ' * indent
print('%s%s:' % (prefix, self.name), file = output)
class Branch:
def __init__(self, tree, name, data):
self.tree = tree
self.name = name
self.data = data
self.remote = None
self.target = None
self.merged = False
self.branches = []
if 'merged' in data:
self.merged = data['merged']
if 'remote' in data:
self.remote = tree.find_remote(data['remote'])
if 'target' in data:
self.target = tree.find_target(data['target'])
if 'dependencies' in data:
for name, branch in data['dependencies'].items():
branch = Branch(self.tree, name, branch)
self.branches.append(branch)
def __iter__(self):
return iter(self.branches)
def __str__(self):
return self.name
def build(self, repo, build, source, output, jobs, targets, log,
verbose = False, checks = 0, warnings = 0):
arch, config = build.name.split('/')
build_log = os.path.join(output, 'build.log')
extra_args = []
if not verbose:
print('building %s for %s...' % (log.magenta(self),
log.blue(build)),
end = '')
sys.stdout.flush()
else:
print('building %s for %s...' % (log.magenta(self),
log.blue(build)))
print(' jobs: %u' % jobs)
print(' output: %s' % output)
print(' architecture: %s' % arch)
print(' configuration: %s' % config)
os.makedirs(output, exist_ok = True)
cross_compile = CrossCompiler(arch)
env = os.environ.copy()
path = ':'.join([env['PATH'], cross_compile.path])
env.update({
'ARCH': cross_compile.arch,
'CROSS_COMPILE': cross_compile.prefix,
'KBUILD_OUTPUT': output,
'PATH': path,
})
# if we're acting on a worktree, make sure to use the worktree as the
# source directory for the builds
if source:
extra_args += [ '-C', source ]
if checks:
extra_args += [ 'C=%u' % checks ]
if warnings:
extra_args += [ 'W=%u' % warnings ]
# check out the base branch
base = self.branches[0]
if source:
repo.git.reset('--hard', base.name)
else:
repo.git.checkout(base.name)
# ... and build it
with fragile(open(build_log, 'w')) as fobj:
cmd = ['make', *extra_args, '-j%u' % args.jobs, config ]
if verbose:
print(' environment:', env)
print(' command:', cmd)
complete = subprocess.run(cmd, env = env, stdout = fobj,
stderr = subprocess.STDOUT)
if complete.returncode != 0:
raise fragile.Break
cmd = ['make', *extra_args, '-j%u' % args.jobs, *args.targets ]
if verbose:
print(' command:', cmd)
complete = subprocess.run(cmd, env = env, stdout = fobj,
stderr = subprocess.STDOUT)
if complete.returncode != 0:
raise fragile.Break
# check out the branch and build it to get a more sensible
# warnings/checks diff
if source:
repo.git.reset('--hard', self.name)
else:
repo.git.checkout(self.name)
with fragile(open(build_log, 'w')) as fobj:
cmd = ['make', *extra_args, '-j%u' % args.jobs, config ]
if verbose:
print(' environment:', env)
print(' command:', cmd)
complete = subprocess.run(cmd, env = env, stdout = fobj,
stderr = subprocess.STDOUT)
if complete.returncode != 0:
raise fragile.Break
cmd = ['make', *extra_args, '-j%u' % args.jobs, *args.targets ]
if verbose:
print(' command:', cmd)
complete = subprocess.run(cmd, env = env, stdout = fobj,
stderr = subprocess.STDOUT)
if complete.returncode != 0:
raise fragile.Break
if complete.returncode == 0:
print(log.green('done'))
else:
print(log.red('failed'))
def check_trailers(self, repo, commit, log):
def identity(actor):
return '%s <%s>' % (actor.name, actor.email)
def sign_off(actor):
return 'Signed-off-by: %s <%s>' % (actor.name, actor.email)
with repo.config_reader() as config:
abbrev = config.get_value('core', 'abbrev', 12)
signoffs = {
'committer': {
'identity': identity(commit.committer),
'present': False,
},
'author': {
'identity': identity(commit.author),
'present': False,
},
}
# skip merge commits
if len(commit.parents) > 1:
return
committer = identity(commit.committer)
author = identity(commit.author)
proc = repo.git.execute(['git', 'interpret-trailers', '--parse'], as_process = True,
istream = subprocess.PIPE)
stdout, _ = proc.communicate(str(commit.message).encode())
for trailer in stdout.decode().splitlines():
key, value = map(lambda x: x.strip(), trailer.split(':', 1))
if key == 'Signed-off-by':
if value == committer:
signoffs['committer']['present'] = True
if value == author:
signoffs['author']['present'] = True
hexsha = binascii.hexlify(commit.binsha).decode()[0:abbrev]
for key, value in signoffs.items():
if not value['present']:
print('%s: commit %s ("%s") in branch %s' % (log.red('ERROR', Log.STYLE_BOLD),
hexsha, commit.summary, self))
print('%s is missing a Signed-off-by: from its %s %s' % (' ' * 5, key,
value['identity']))
def check_references(self, repo, commit, log):
with repo.config_reader() as config:
abbrev = config.get_value('core', 'abbrev', 12)
proc = repo.git.execute(['git', 'interpret-trailers', '--parse'], as_process = True,
istream = subprocess.PIPE)
stdout, _ = proc.communicate(str(commit.message).encode())
trailers = []
for trailer in stdout.decode().splitlines():
key, value = map(lambda x: x.strip(), trailer.split(':', 1))
if key == 'Fixes':
match = re.match(r'([0-9a-f]+) \("(.*)"\)', value)
ref, subject = match.group(1, 2)
branches = repo.git.branch('--contains', ref)
for branch in branches.splitlines():
match = re.match(r'\*?\W+(.*)', branch)
if self.name == match.group(1):
break
else:
hexsha = binascii.hexlify(commit.binsha).decode()[0:abbrev]
print('%s: commit %s ("%s") referenced by' % (log.red('ERROR',
style = Log.STYLE_BOLD),
log.yellow(ref), subject))
print('%s commit %s ("%s")' % (' ' * 5, log.yellow(hexsha), commit.summary))
print('%s was not found in branch %s' % (' ' * 5, log.green(self)))
def check(self, repo, log):
rev_list = '%s..%s' % (self.branches[0], self)
print('checking branch %s...' % self)
for commit in repo.iter_commits(rev_list):
self.check_trailers(repo, commit, log)
self.check_references(repo, commit, log)
def checkout(self, repo, dry_run = False):
print('checking out %s...' % self.name)
if not dry_run:
repo.git.checkout(self.name)
#branch = repo.refs[self.name]
#repo.head.reference = branch
#repo.head.reset(index = True, working_tree = True)
def filter(self, remotes = []):
branches = []
if remotes:
for branch in self.branches:
if branch.remote in remotes:
branches.append(branch)
return branches
def reset(self, repo, branch, dry_run = False):
if branch.name in repo.tags:
print('resetting branch %s to tag %s...' % (self.name, branch.name))
base = '%s' % branch.name
else:
print('resetting branch %s to %s...' % (self.name, base))
if branch.remote:
base = '%s/%s' % (branch.remote.name, branch.name)
else:
base = '%s' % branch.name
if not dry_run:
repo.git.reset(base, hard = True)
def merge(self, repo, dry_run = False):
base = None
self.checkout(repo, dry_run = dry_run)
for branch in self.branches:
if not base:
self.reset(repo, branch, dry_run = dry_run)
base = branch
print('Merging branch %s into %s' % (branch.name, self.name))
if not dry_run:
message = 'Merge branch %s into %s' % (branch.name, self.name)
repo.git.merge(branch.name, m = message, no_ff = True, signoff = True)
def push(self, repo, push_tags = False, targets = None, iteration = 1, dry_run = False):
if not self.remote:
raise Exception('cannot push branch %s without remote' % self)
remote = self.remote.name
branches = []
tags = []
# first force-push (with no verification) the base of each branch to
# avoid running git hooks on the entire base
for branch in self.branches:
if branch.target and not branch.merged:
if targets and branch.target.name not in targets:
continue
else:
continue
if len(branch.branches) > 0:
base = branch.branches[0]
else:
base = None
if base:
branches.append('%s^{commit}:refs/heads/%s' % (base, branch.name))
base = branch.branches[0]
branches.append('%s^{commit}:refs/heads/%s' % (base, self.name))
print('pushing branches:', branches)
if not dry_run and branches:
(status, stdout, stderr) = repo.git.push(remote, branches,
with_extended_output = True,
with_exceptions = False,
no_verify = True,
dry_run = dry_run,
force = True)
print(stdout);
print(stderr)
if status != 0:
sys.exit(status)
# now push the new contents to the remote
branches = []
for branch in self.branches:
if branch.target and not branch.merged:
if targets and branch.target.name not in targets:
continue
else:
continue
branches.append('%s' % branch.name)
if push_tags:
tag_prefix = branch.target.prefix
if iteration > 1:
suffix = '-v%u' % iteration
else:
suffix = ''
tag = '%s%s%s' % (tag_prefix, branch.name.replace('/', '-'), suffix)
tags.append('%s' % tag)
branches.append('%s' % self.name)
branches.extend(tags)
print('pushing branches:', branches)
for branch in branches:
print(' pushing %s...' % branch)
if not dry_run:
(status, stdout, stderr) = repo.git.push(remote, branches,
with_extended_output = True,
with_exceptions = False,
dry_run = dry_run,
force = True)
print(stdout);
print(stderr)
if status != 0:
sys.exit(status)
def request_pull(self, repo, targets = None, branches = None, iteration = 1, dry_run = False):
git_config = repo.config_reader()
config = read_dotconfig()
buckets = {}
for branch in self.branches:
if branch.target and not branch.merged:
if targets and branch.target.name not in targets:
continue
if branches and branch.name not in branches:
continue
if branch.target not in buckets:
buckets[branch.target] = [ ]
buckets[branch.target].append(branch)
for target, branches in buckets.items():
print('target: %s' % target)
if not config:
author = (git_config.get('user', 'name'),
git_config.get('user', 'email'))
else:
author = (config.get('user', 'name'),
config.get('user', 'email'))
author = email.utils.formataddr(author)
releases = {}
for branch in branches:
release = branch.name.split('/')[0]
if release not in releases:
releases[release] = []
releases[release].append(branch)
for release, branches in releases.items():
count = len(branches)
path = os.path.join('pull-request/%s/%s' % (release, target))
os.makedirs(path, exist_ok = True)
for index, branch in enumerate(branches, 1):
base = branch.branches[-1].name
prefix = branch.target.prefix
suffix = ''
if iteration > 1:
suffix = '-v%u' % iteration
tag_name = '%s%s' % (prefix, branch.name.replace('/', '-'))
tag = '%s%s' % (tag_name, suffix)
print(' requesting pull for %s based on %s' % (tag, base))
# abort early for dry-run
if dry_run:
continue
subject = repo.tags[tag].tag.message.splitlines()[0]
firstname = author.split()[0]
to = ', '.join(target.to)
cc = ', '.join(target.cc)
if count > 1:
# make [GIT PULL index/count] look pretty
width = math.ceil(math.log10(count + 1))
if iteration > 1:
subject = '[GIT PULL v%u %0*u/%0*u] %s' % (iteration, width, index,
width, count, subject)
else:
subject = '[GIT PULL %0*u/%0*u] %s' % (width, index, width,
count, subject)
else:
if iteration > 1:
subject = '[GIT PULL v%u] %s' % (iteration, subject)
else:
subject = '[GIT PULL] %s' % subject
message = email.message.EmailMessage()
message['From'] = author
message['To'] = to
message['Cc'] = cc
message['Subject'] = subject
if target.addressee:
content = 'Hi %s,\n' % target.addressee
content += '\n'
else:
content = 'Hi,\n'
content += '\n'
pull_request = repo.git.request_pull(base, branch.remote.pull,
tag)
separator = False
prefix = ''
for line in pull_request.splitlines():
if line == '-' * 64 and not separator:
content += '%sThanks,' % prefix
content += '%s%s\n' % (prefix, firstname)
separator = True
content += '%s%s' % (prefix, line)
prefix = '\n'
message.set_content(content)
if iteration > 1:
filename = os.path.join(path, 'v%u-%04u-%s' % (iteration, index, tag_name))
else:
filename = os.path.join(path, '%04u-%s' % (index, tag_name))
print('writing message to %s' % filename)
with io.open(filename, 'w') as output:
print(message, file = output, end = '')
def tag(self, repo, prefix, targets = None, branches = None, iteration = 1, dry_run = False):
for branch in self.branches:
if branch.target and not branch.merged:
if targets and branch.target.name not in targets:
continue
if branches and branch.name not in branches:
continue
if prefix is None:
tag_prefix = branch.target.prefix
else:
tag_prefix = prefix
if iteration > 1:
suffix = '-v%u' % iteration
else:
suffix = ''
tag = '%s%s%s' % (tag_prefix, branch.name.replace('/', '-'), suffix)
print(' tagging %s as %s' % (branch, tag))
if tag in repo.tags:
print('WARNING: tag %s already exists, skipping' % tag)
continue
# gitpython will always redirect the stdout to a PIPE and the
# $EDITOR won't be able to display anything on screen in that
# case so call git manually for signed tags.
if not dry_run:
res = subprocess.call([repo.git.GIT_PYTHON_GIT_EXECUTABLE,
'tag', '--sign', tag, branch.name])
if res != 0:
break
def dump(self, indent = 0, output = sys.stdout):
prefix = ' ' * indent
print('%s%s:' % (prefix, self.name), file = output)
for dependency in self.branches:
dependency.dump(indent + 2, output = output)
class Build:
def __init__(self, tree, name, data):
self.tree = tree
self.name = name
self.data = data
self.branches = []
self.builds = []
if not 'branches' in data:
for name, build in data.items():
build = Build(tree, '%s/%s' % (self.name, name), build)
self.builds.append(build)
else:
remotes = []
for remote in data['branches']['remotes']:
remote = tree.find_remote(remote)
remotes.append(remote)
for branch in tree.filter(remotes = remotes):
self.branches.append(branch)
def __str__(self):
return self.name
class Tree:
def __init__(self, data):
self.data = data
self.remotes = []
self.targets = []
self.branches = []
self.builds = []
for name, remote in data['remotes'].items():
remote = Remote(name, remote)
self.remotes.append(remote)
for name, target in data['targets'].items():
target = Target(name, target)
self.targets.append(target)
for name, branch in data['branches'].items():
branch = Branch(self, name, branch)
self.branches.append(branch)
for name, build in data['builds'].items():
build = Build(self, name, build)
self.builds.extend(build.builds)
self.builds.append(build)
def __iter__(self):
return iter(self.branches)
def find_remote(self, name):
for remote in self.remotes:
if remote.name == name:
return remote
return None
def find_target(self, name):
for target in self.targets:
if target.name == name:
return target
return None
def dump(self, indent = 0, output = sys.stdout):
prefix = ' ' * indent
print('%sremotes:' % prefix, file = output)
for remote in self.remotes:
remote.dump(indent = indent + 2, output = output)
print('%stargets:' % prefix, file = output)
for target in self.targets:
target.dump(indent = indent + 2, output = output)
print('%sbranches:' % prefix, file = output)
for branch in self.branches:
branch.dump(indent = indent + 2, output = output)
def filter(self, remotes = []):
result = []
if remotes:
for branch in self:
branches = branch.filter(remotes)
result.extend(branches)
result.append(branch)
return result
def load_tree():
topdir = os.path.dirname(sys.argv[0])
filename = os.path.join(topdir, 'tegra-branches.yaml')
with io.open(filename, 'r') as fobj:
data = yaml.load(fobj, Loader = yaml.SafeLoader)
return Tree(data)
class Command:
@classmethod
def setup(cls, parser):
if hasattr(cls, 'subcommands'):
sub_parsers = parser.add_subparsers(title = 'subcommands')
for subcommand in cls.subcommands:
sub_parser = sub_parsers.add_parser(subcommand.name,
help = subcommand.help)
sub_parser.set_defaults(run = subcommand.run)
subcommand.setup(sub_parser)
@classmethod
def run(cls, args):
pass
class CommandBuild(Command):
name = 'build'
help = 'build branches'
@classmethod
def setup(cls, parser):
super().setup(parser)
parser.add_argument('-b', '--branches', nargs = '*', default = [],
help = 'names of branches to build')
parser.add_argument('-c', '--color', action = 'store_true',
default = True)
parser.add_argument('--no-color', action = 'store_false',
dest = 'color', help = 'disable log coloring')
parser.add_argument('-C', '--checks', action = 'count', help = 'enable checks')
parser.add_argument('-j', '--jobs', type = int, default = 1,
help = 'number of parallel jobs to run')
parser.add_argument('-k', '--keep', action = 'store_true',
help = 'do not clean up worktree')
parser.add_argument('-o', '--output', help = 'build output directory')
parser.add_argument('-v', '--verbose', action = 'store_true',
help = 'increase verbosity')
parser.add_argument('-w', '--worktree', help = 'worktree directory')
parser.add_argument('-W', '--warnings', action = 'count', help = 'enable extra warnings')
parser.add_argument('targets', metavar = 'TARGET', nargs = '*',
help = 'names of targets to build')
@classmethod
def run(cls, args):
log = Log(args.color)
repo = git.Repo('.')
tree = load_tree()
if args.worktree:
worktree = os.path.abspath(args.worktree)
else:
worktree = None
if not args.output:
if not worktree:
output = os.path.join(os.getcwd(), 'build')
else:
output = os.path.join(worktree, 'build')
else:
output = os.path.abspath(args.output)
if worktree:
print('creating worktree %s' % worktree)
try:
repo.git.worktree('add', '--detach', worktree)
except git.exc.GitCommandError as e:
# XXX find a better way to deal with this
if not args.keep and e.stderr.endswith('already exists'):
raise e
repo = git.Repo(worktree)
print('output directory: %s' % output)
for build in tree.builds:
parts = build.name.split('/')
build_directory = os.path.join(output, *parts)
for branch in build.branches:
branch_name = '-'.join(branch.name.split('/'))
branch_directory = os.path.join(build_directory, branch_name)
if args.branches and branch.name not in args.branches:
continue
branch.build(repo, build, worktree, branch_directory,
args.jobs, args.targets, log, args.verbose,
checks = args.checks, warnings = args.warnings)
if worktree and not args.keep:
repo = git.Repo('.')
repo.git.worktree('remove', worktree)
class CommandCheck(Command):
name = 'check'
help = 'check branches'
@classmethod
def setup(cls, parser):
super().setup(parser)
parser.add_argument('branches', metavar = 'BRANCH', nargs = '*',
help = 'names of branches to check')
parser.add_argument('-c', '--color', action = 'store_true',
default = True)
parser.add_argument('--no-color', action = 'store_false',
dest = 'color', help = 'disable log coloring')
parser.add_argument('-v', '--verbose', action = 'store_true',
help = 'increase verbosity')
@classmethod
def run(cls, args):
log = Log(args.color)
repo = git.Repo('.')
tree = load_tree()
branches = []
for build in tree.builds:
for branch in build.branches:
if args.branches and branch.name not in args.branches:
continue
if branch not in branches:
branches.append(branch)
for branch in branches:
branch.check(repo, log)
class CommandMerge(Command):
name = 'merge'
help = 'merge branch'
@classmethod
def setup(cls, parser):
super().setup(parser)
parser.add_argument('-n', '--dry-run', action = 'store_true',
help = 'display the actions that would be performed')
parser.add_argument('branches', metavar = 'BRANCH', nargs = '*',
help = 'names of branches to merge')
@classmethod
def run(cls, args):
repo = git.Repo('.')
tree = load_tree()
for branch in tree:
if args.branches and branch.name not in args.branches:
continue
print('creating branch %s...' % branch)
branch.merge(repo, dry_run = args.dry_run)
class CommandPush(Command):
name = 'push'
help = 'push branches'
@classmethod
def setup(cls, parser):
super().setup(parser)
parser.add_argument('-n', '--dry-run', action = 'store_true',
help = 'do everything except actually send the updates')
parser.add_argument('--tags', action = 'store_true',
help = 'push tags along with branches')
parser.add_argument('-t', '--target', dest = 'targets',
action = 'append', type = str,
help = 'list of targets for which to push')
parser.add_argument('-v', '--reroll-count', dest = 'iteration',
action = 'store', type = int, default = 1,
help = 'mark the pull-requests as the <n>-th iteration')
parser.add_argument('branches', metavar = 'BRANCH', nargs = '*',
help = 'names of branches to push')
@classmethod
def run(cls, args):
repo = git.Repo('.')
tree = load_tree()
for branch in tree:
if args.branches and branch.name not in args.branches:
continue
print('pushing branch %s...' % branch)
branch.push(repo, push_tags = args.tags, targets = args.targets,
iteration = args.iteration, dry_run = args.dry_run)
class CommandRequestPull(Command):
name = 'request-pull'
help = 'request-pull for a branch'
@classmethod
def setup(cls, parser):
super().setup(parser)
parser.add_argument('-n', '--dry-run', action = 'store_true',
help = 'display the actions that would be performed')
parser.add_argument('-t', '--target', dest = 'targets',
action = 'append', type = str,
help = 'list of targets for which to request-pull')
parser.add_argument('-v', '--reroll-count', dest = 'iteration',
action = 'store', type = int, default = 1,
help = 'mark the pull-requests as the <n>-th iteration')
parser.add_argument('branches', metavar = 'BRANCH', nargs = '*',
help = 'names of branches to tag')
@classmethod
def run(cls, args):
repo = git.Repo('.')
tree = load_tree()
for branch in tree:
print('request-pull for %s...' % branch)
branch.request_pull(repo, targets = args.targets, branches = args.branches,
iteration = args.iteration, dry_run = args.dry_run)
class CommandTag(Command):
name = 'tag'
help = 'tag branches'
@classmethod
def setup(cls, parser):
super().setup(parser)
parser.add_argument('-n', '--dry-run', action = 'store_true',
help = 'display the actions that would be performed')
parser.add_argument('-p', '--prefix', type = str,
help = 'prefix to prepend to tag names')
parser.add_argument('-t', '--target', dest = 'targets',
action = 'append', type = str,
help = 'list of targets for which to tag')
parser.add_argument('-v', '--reroll-count', dest = 'iteration',
action = 'store', type = int, default = 1,
help = 'mark the pull-requests as the <n>-th iteration')
parser.add_argument('branches', metavar = 'BRANCH', nargs = '*',
help = 'names of branches to tag')
@classmethod
def run(cls, args):
repo = git.Repo('.')
tree = load_tree()
for branch in tree:
print('tagging branch %s...' % branch)
branch.tag(repo, args.prefix, targets = args.targets, branches = args.branches,
iteration = args.iteration, dry_run = args.dry_run)
commands = [
CommandBuild,
CommandCheck,
CommandMerge,
CommandPush,
CommandRequestPull,
CommandTag,
]
if __name__ == '__main__':
logging.basicConfig(level = logging.INFO)
parser = argparse.ArgumentParser()
cmd_parsers = parser.add_subparsers(title = 'commands')
for cmd in commands:
cmd_parser = cmd_parsers.add_parser(cmd.name, help = cmd.help)
cmd_parser.set_defaults(run = cmd.run)
cmd.setup(cmd_parser)
args = parser.parse_args()
if not hasattr(args, 'run'):
parser.print_help()
sys.exit(1)
args.run(args)