| #!/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) |