| #!/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.manual = False | |
| self.merged = False | |
| self.branches = [] | |
| if 'manual' in data: | |
| self.manual = data['manual'] | |
| 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: | |
| if branch.remote: | |
| base = '%s/%s' % (branch.remote.name, branch.name) | |
| else: | |
| base = '%s' % branch.name | |
| print('resetting branch %s to %s...' % (self.name, base)) | |
| 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, prefix = None, 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: | |
| if base.remote and base.name not in repo.tags: | |
| base = f'{base.remote.name}/{base.name}' | |
| else: | |
| base = f'{base.name}^{{commit}}' | |
| branches.append('%s:refs/heads/%s' % (base, branch.name)) | |
| base = self.branches[0] | |
| if base.remote and base.name not in repo.tags: | |
| base = f'{base.remote.name}/{base.name}' | |
| else: | |
| base = f'{base.name}^{{commit}}' | |
| branches.append('%s: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 = branch.make_tag(prefix, iteration) | |
| if tag: | |
| tags.append('%s' % tag) | |
| if not targets or (self.target and self.target.name in targets): | |
| tag = self.make_tag(prefix, iteration) | |
| if tag: | |
| 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, prefix = None, 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) | |
| if self.manual: | |
| if self.target and self.target not in buckets: | |
| buckets[self.target] = [] | |
| buckets[self.target].append(self) | |
| 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[0].name | |
| tag = branch.make_tag(prefix, iteration) | |
| 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 | |
| line_prefix = '' | |
| for line in pull_request.splitlines(): | |
| if line == '-' * 64 and not separator: | |
| content += '%sThanks,' % line_prefix | |
| content += '%s%s\n' % (line_prefix, firstname) | |
| separator = True | |
| content += '%s%s' % (line_prefix, line) | |
| line_prefix = '\n' | |
| message.set_content(content) | |
| if iteration > 1: | |
| filename = os.path.join(path, 'v%u-%04u-%s' % (iteration, index, tag)) | |
| else: | |
| filename = os.path.join(path, '%04u-%s' % (index, tag)) | |
| print('writing message to %s' % filename) | |
| with io.open(filename, 'w') as output: | |
| print(message, file = output, end = '') | |
| def make_tag(self, prefix = None, iteration = 1): | |
| if self.target: | |
| if prefix is None: | |
| tag_prefix = self.target.prefix | |
| else: | |
| tag_prefix = prefix | |
| else: | |
| return None | |
| if iteration > 1: | |
| suffix = '-v%u' % iteration | |
| else: | |
| suffix = '' | |
| return '%s%s%s' % (tag_prefix, self.name.replace('/', '-'), suffix) | |
| def tag(self, repo, prefix, targets = None, branches = None, iteration = 1, dry_run = False): | |
| # for manually maintained branches, only tag the top-level branch and | |
| # assume all others have already been tagged previously | |
| if not self.manual: | |
| all_branches = self.branches | |
| else: | |
| all_branches = [ self ] | |
| for branch in all_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 | |
| tag = branch.make_tag(prefix, iteration) | |
| 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 | |
| # do not recreate manually maintained branches (i.e. those that | |
| # don't have any children or ones that are explicitly marked as | |
| # manual) | |
| if not branch.branches or branch.manual: | |
| 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('-p', '--prefix', type = str, | |
| help = 'prefix to prepend to tag names') | |
| 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, prefix = args.prefix, | |
| 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('-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 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: | |
| if args.branches and branch.name not in args.branches: | |
| continue | |
| print('request-pull for %s...' % branch) | |
| branch.request_pull(repo, prefix = args.prefix, targets = args.targets, | |
| branches = None, 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: | |
| if args.branches and branch.name not in args.branches: | |
| continue | |
| print('tagging branch %s...' % branch) | |
| branch.tag(repo, prefix = args.prefix, targets = args.targets, | |
| branches = None, 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) |