blob: 0fd65c677cd5f4531cbadc4ae4933fdb7e20ee3c [file] [log] [blame]
#!/usr/bin/env python3
# Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.
# Script that automatically pulls and uploads all upstream direct and indirect
# branches into the current branch.
#
# Example:
#
# $ git branch -vv
# * feature_final xxxxxxxxx [feature_prereq_c: ...] ...
# feature_prereq_c xxxxxxxxx [feature_prereq_b: ...] ...
# feature_prereq_b xxxxxxxxx [feature_prereq_a: ...] ...
# feature_prereq_a xxxxxxxxx [main: ...] ...
# main xxxxxxxxx [origin/main] ...
#
# Executing `git_sync_cl_chain.py -m <message>` causes the following chain of
# commands to be executed:
#
# $ git checkout feature_prereq_a; git pull; git cl upload -m <message>
# $ git checkout feature_prereq_b; git pull; git cl upload -m <message>
# $ git checkout feature_prereq_c; git pull; git cl upload -m <message>
# $ git checkout feature_final; git pull; git cl upload -m <message>
import optparse
import os
import sys
import defines
import utils
REPO_ROOT = defines.REPO_ROOT
class Repo(object):
def __init__(self, name, is_current, upstream):
self.name = name
self.is_current = is_current
self.upstream = upstream
def ParseOptions(argv):
result = optparse.OptionParser()
result.add_option('--bypass-hooks',
help='Bypass presubmit hooks',
action='store_true')
result.add_option('--delete', '-d',
help='Delete closed branches',
choices=['y', 'n', 'ask'],
default='ask')
result.add_option('--from_branch', '-f',
help='Uppermost upstream to sync from',
default='main')
result.add_option('--leave_upstream', '--leave-upstream',
help='To not update the upstream of the first open branch',
action='store_true')
result.add_option('--message', '-m',
help='Message for patchset', default='Sync')
result.add_option('--rebase',
help='To use `git pull --rebase` instead of `git pull`',
action='store_true')
result.add_option('--no_upload', '--no-upload',
help='Disable uploading to Gerrit', action='store_true')
result.add_option('--skip_main', '--skip-main',
help='Disable syncing for main',
action='store_true')
(options, args) = result.parse_args(argv)
options.upload = not options.no_upload
assert options.delete != 'y' or not options.leave_upstream, (
'Inconsistent options: cannot leave the upstream of the first open ' +
'branch (--leave_upstream) and delete the closed branches at the same ' +
'time (--delete).')
assert options.message, 'A message for the patchset is required.'
assert len(args) == 0
return options
def main(argv):
options = ParseOptions(argv)
with utils.ChangedWorkingDirectory(REPO_ROOT, quiet=True):
branches = [
parse(line)
for line in utils.RunCmd(['git', 'branch', '-vv'], quiet=True)]
current_branch = None
for branch in branches:
if branch.is_current:
current_branch = branch
break
assert current_branch is not None
if is_root_branch(current_branch, options):
print('Nothing to sync')
return
stack = []
while current_branch:
stack.append(current_branch)
if is_root_branch(current_branch, options):
break
current_branch = get_branch_with_name(current_branch.upstream, branches)
closed_branches = []
has_seen_local_branch = False # A branch that is not uploaded.
has_seen_open_branch = False # A branch that is not closed.
while len(stack) > 0:
branch = stack.pop()
utils.RunCmd(['git', 'checkout', branch.name], quiet=True)
status = get_status_for_current_branch(branch)
print('Syncing %s (status: %s)' % (branch.name, status))
pull_for_current_branch(branch, options)
if branch.name == 'main':
continue
if status == 'closed':
assert not has_seen_local_branch, (
'Unexpected closed branch %s after new branch' % branch.name)
assert not has_seen_open_branch, (
'Unexpected closed branch %s after open branch' % branch.name)
closed_branches.append(branch.name)
continue
if not options.leave_upstream:
if not has_seen_open_branch and len(closed_branches) > 0:
print(
'Setting upstream for first open branch %s to main'
% branch.name)
set_upstream_for_current_branch_to_main()
has_seen_open_branch = True
has_seen_local_branch = has_seen_local_branch or (status == 'None')
if options.upload and status != 'closed':
if has_seen_local_branch:
print(
'Cannot upload branch %s since it comes after a local branch'
% branch.name)
else:
upload_cmd = ['git', 'cl', 'upload', '-m', options.message]
if options.bypass_hooks:
upload_cmd.append('--bypass-hooks')
utils.RunCmd(upload_cmd, quiet=True)
if get_delete_branches_option(closed_branches, options):
delete_branches(closed_branches)
utils.RunCmd(['git', 'cl', 'issue'])
def delete_branches(branches):
assert len(branches) > 0
cmd = ['git', 'branch', '-D']
cmd.extend(branches)
utils.RunCmd(cmd, quiet=True)
def get_branch_with_name(name, branches):
for branch in branches:
if branch.name == name:
return branch
return None
def get_delete_branches_option(closed_branches, options):
if len(closed_branches) == 0:
return False
if options.leave_upstream:
return False
if options.delete == 'y':
return True
if options.delete == 'n':
return False
assert options.delete == 'ask'
print('Delete closed branches: %s (Y/N)?' % ", ".join(closed_branches))
answer = sys.stdin.read(1)
return answer.lower() == 'y'
def get_status_for_current_branch(current_branch):
if current_branch.name == 'main':
return 'main'
return utils.RunCmd(['git', 'cl', 'status', '--field', 'status'], quiet=True)[0].strip()
def is_root_branch(branch, options):
return branch.name == options.from_branch or branch.upstream is None
def pull_for_current_branch(branch, options):
if branch.name == 'main' and options.skip_main:
return
rebase_args = ['--rebase'] if options.rebase else []
utils.RunCmd(['git', 'pull'] + rebase_args, quiet=True)
def set_upstream_for_current_branch_to_main():
utils.RunCmd(['git', 'cl', 'upstream', 'main'], quiet=True)
# Parses a line from the output of `git branch -vv`.
#
# Example output ('*' denotes the current branch):
#
# $ git branch -vv
# * feature_final xxxxxxxxx [feature_prereq_c: ...] ...
# feature_prereq_c xxxxxxxxx [feature_prereq_b: ...] ...
# feature_prereq_b xxxxxxxxx [feature_prereq_a: ...] ...
# feature_prereq_a xxxxxxxxx [main: ...] ...
# main xxxxxxxxx [origin/main] ...
def parse(line):
is_current = False
if line.startswith('*'):
is_current = True
line = line[1:].lstrip()
else:
line = line.lstrip()
name_end_index = line.index(' ')
name = line[:name_end_index]
line = line[name_end_index:].lstrip()
if '[' in line:
line = line[line.index('[')+1:]
if ':' in line:
upstream = line[:line.index(':')]
return Repo(name, is_current, upstream)
if ']' in line:
upstream = line[:line.index(']')]
return Repo(name, is_current, upstream)
return Repo(name, is_current, None)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))