blob: 0fd65c677cd5f4531cbadc4ae4933fdb7e20ee3c [file] [log] [blame]
Ian Zernydcb172e2022-02-22 15:36:45 +01001#!/usr/bin/env python3
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +02002# Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
3# for details. All rights reserved. Use of this source code is governed by a
4# BSD-style license that can be found in the LICENSE file.
5
6# Script that automatically pulls and uploads all upstream direct and indirect
7# branches into the current branch.
8#
9# Example:
10#
11# $ git branch -vv
12# * feature_final xxxxxxxxx [feature_prereq_c: ...] ...
13# feature_prereq_c xxxxxxxxx [feature_prereq_b: ...] ...
14# feature_prereq_b xxxxxxxxx [feature_prereq_a: ...] ...
Rico Wind1b52acf2021-03-21 12:36:55 +010015# feature_prereq_a xxxxxxxxx [main: ...] ...
16# main xxxxxxxxx [origin/main] ...
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020017#
18# Executing `git_sync_cl_chain.py -m <message>` causes the following chain of
19# commands to be executed:
20#
21# $ git checkout feature_prereq_a; git pull; git cl upload -m <message>
22# $ git checkout feature_prereq_b; git pull; git cl upload -m <message>
23# $ git checkout feature_prereq_c; git pull; git cl upload -m <message>
24# $ git checkout feature_final; git pull; git cl upload -m <message>
25
26import optparse
27import os
28import sys
29
30import defines
31import utils
32
33REPO_ROOT = defines.REPO_ROOT
34
35class Repo(object):
36 def __init__(self, name, is_current, upstream):
37 self.name = name
38 self.is_current = is_current
39 self.upstream = upstream
40
41def ParseOptions(argv):
42 result = optparse.OptionParser()
Christoffer Quist Adamsen7607ebe2022-06-28 11:52:46 +020043 result.add_option('--bypass-hooks',
44 help='Bypass presubmit hooks',
45 action='store_true')
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +010046 result.add_option('--delete', '-d',
47 help='Delete closed branches',
48 choices=['y', 'n', 'ask'],
49 default='ask')
Christoffer Quist Adamsene2d3f852022-01-12 13:24:07 +010050 result.add_option('--from_branch', '-f',
51 help='Uppermost upstream to sync from',
52 default='main')
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +010053 result.add_option('--leave_upstream', '--leave-upstream',
54 help='To not update the upstream of the first open branch',
55 action='store_true')
56 result.add_option('--message', '-m',
57 help='Message for patchset', default='Sync')
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020058 result.add_option('--rebase',
59 help='To use `git pull --rebase` instead of `git pull`',
60 action='store_true')
Christoffer Quist Adamsena8212812019-08-14 15:38:50 +020061 result.add_option('--no_upload', '--no-upload',
62 help='Disable uploading to Gerrit', action='store_true')
Rico Wind1b52acf2021-03-21 12:36:55 +010063 result.add_option('--skip_main', '--skip-main',
64 help='Disable syncing for main',
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +010065 action='store_true')
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020066 (options, args) = result.parse_args(argv)
Christoffer Quist Adamsena8212812019-08-14 15:38:50 +020067 options.upload = not options.no_upload
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +010068 assert options.delete != 'y' or not options.leave_upstream, (
69 'Inconsistent options: cannot leave the upstream of the first open ' +
70 'branch (--leave_upstream) and delete the closed branches at the same ' +
71 'time (--delete).')
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020072 assert options.message, 'A message for the patchset is required.'
73 assert len(args) == 0
74 return options
75
76def main(argv):
77 options = ParseOptions(argv)
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020078 with utils.ChangedWorkingDirectory(REPO_ROOT, quiet=True):
79 branches = [
80 parse(line)
81 for line in utils.RunCmd(['git', 'branch', '-vv'], quiet=True)]
82
83 current_branch = None
84 for branch in branches:
85 if branch.is_current:
86 current_branch = branch
87 break
88 assert current_branch is not None
89
Christoffer Quist Adamsene2d3f852022-01-12 13:24:07 +010090 if is_root_branch(current_branch, options):
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020091 print('Nothing to sync')
92 return
93
94 stack = []
Morten Krogh-Jespersen5b225f42019-08-14 15:26:06 +020095 while current_branch:
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020096 stack.append(current_branch)
Christoffer Quist Adamsene2d3f852022-01-12 13:24:07 +010097 if is_root_branch(current_branch, options):
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020098 break
99 current_branch = get_branch_with_name(current_branch.upstream, branches)
100
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100101 closed_branches = []
102 has_seen_local_branch = False # A branch that is not uploaded.
103 has_seen_open_branch = False # A branch that is not closed.
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200104 while len(stack) > 0:
105 branch = stack.pop()
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100106
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200107 utils.RunCmd(['git', 'checkout', branch.name], quiet=True)
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100108
Christoffer Quist Adamsen7607ebe2022-06-28 11:52:46 +0200109 status = get_status_for_current_branch(branch)
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100110 print('Syncing %s (status: %s)' % (branch.name, status))
111
112 pull_for_current_branch(branch, options)
113
Rico Wind1b52acf2021-03-21 12:36:55 +0100114 if branch.name == 'main':
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100115 continue
116
117 if status == 'closed':
118 assert not has_seen_local_branch, (
119 'Unexpected closed branch %s after new branch' % branch.name)
120 assert not has_seen_open_branch, (
121 'Unexpected closed branch %s after open branch' % branch.name)
122 closed_branches.append(branch.name)
123 continue
124
125 if not options.leave_upstream:
126 if not has_seen_open_branch and len(closed_branches) > 0:
127 print(
Rico Wind1b52acf2021-03-21 12:36:55 +0100128 'Setting upstream for first open branch %s to main'
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100129 % branch.name)
Rico Wind1b52acf2021-03-21 12:36:55 +0100130 set_upstream_for_current_branch_to_main()
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100131
132 has_seen_open_branch = True
133 has_seen_local_branch = has_seen_local_branch or (status == 'None')
134
Morten Krogh-Jespersen25b512f2020-11-10 11:52:26 +0100135 if options.upload and status != 'closed':
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100136 if has_seen_local_branch:
137 print(
138 'Cannot upload branch %s since it comes after a local branch'
139 % branch.name)
140 else:
Christoffer Quist Adamsen7607ebe2022-06-28 11:52:46 +0200141 upload_cmd = ['git', 'cl', 'upload', '-m', options.message]
142 if options.bypass_hooks:
143 upload_cmd.append('--bypass-hooks')
144 utils.RunCmd(upload_cmd, quiet=True)
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100145
Christoffer Quist Adamsend3230442020-11-06 11:12:14 +0100146 if get_delete_branches_option(closed_branches, options):
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100147 delete_branches(closed_branches)
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200148
Morten Krogh-Jespersenc2be3f82019-08-14 15:30:55 +0200149 utils.RunCmd(['git', 'cl', 'issue'])
150
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100151def delete_branches(branches):
152 assert len(branches) > 0
Christoffer Quist Adamsen601aa3c2020-11-27 14:50:31 +0100153 cmd = ['git', 'branch', '-D']
154 cmd.extend(branches)
155 utils.RunCmd(cmd, quiet=True)
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100156
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200157def get_branch_with_name(name, branches):
158 for branch in branches:
159 if branch.name == name:
160 return branch
161 return None
162
Christoffer Quist Adamsend3230442020-11-06 11:12:14 +0100163def get_delete_branches_option(closed_branches, options):
164 if len(closed_branches) == 0:
165 return False
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100166 if options.leave_upstream:
167 return False
168 if options.delete == 'y':
169 return True
170 if options.delete == 'n':
171 return False
172 assert options.delete == 'ask'
173 print('Delete closed branches: %s (Y/N)?' % ", ".join(closed_branches))
174 answer = sys.stdin.read(1)
175 return answer.lower() == 'y'
176
Christoffer Quist Adamsen7607ebe2022-06-28 11:52:46 +0200177def get_status_for_current_branch(current_branch):
178 if current_branch.name == 'main':
179 return 'main'
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100180 return utils.RunCmd(['git', 'cl', 'status', '--field', 'status'], quiet=True)[0].strip()
181
Christoffer Quist Adamsene2d3f852022-01-12 13:24:07 +0100182def is_root_branch(branch, options):
Christoffer Quist Adamsen7607ebe2022-06-28 11:52:46 +0200183 return branch.name == options.from_branch or branch.upstream is None
Christoffer Quist Adamsene2d3f852022-01-12 13:24:07 +0100184
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100185def pull_for_current_branch(branch, options):
Rico Wind1b52acf2021-03-21 12:36:55 +0100186 if branch.name == 'main' and options.skip_main:
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100187 return
188 rebase_args = ['--rebase'] if options.rebase else []
189 utils.RunCmd(['git', 'pull'] + rebase_args, quiet=True)
190
191
Rico Wind1b52acf2021-03-21 12:36:55 +0100192def set_upstream_for_current_branch_to_main():
193 utils.RunCmd(['git', 'cl', 'upstream', 'main'], quiet=True)
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100194
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200195# Parses a line from the output of `git branch -vv`.
196#
197# Example output ('*' denotes the current branch):
198#
199# $ git branch -vv
200# * feature_final xxxxxxxxx [feature_prereq_c: ...] ...
201# feature_prereq_c xxxxxxxxx [feature_prereq_b: ...] ...
202# feature_prereq_b xxxxxxxxx [feature_prereq_a: ...] ...
Rico Wind1b52acf2021-03-21 12:36:55 +0100203# feature_prereq_a xxxxxxxxx [main: ...] ...
204# main xxxxxxxxx [origin/main] ...
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200205def parse(line):
206 is_current = False
207 if line.startswith('*'):
208 is_current = True
209 line = line[1:].lstrip()
210 else:
211 line = line.lstrip()
212
213 name_end_index = line.index(' ')
214 name = line[:name_end_index]
215 line = line[name_end_index:].lstrip()
216
Christoffer Quist Adamsen7a5deed2020-11-16 11:31:15 +0100217 if '[' in line:
218 line = line[line.index('[')+1:]
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200219
Christoffer Quist Adamsen7a5deed2020-11-16 11:31:15 +0100220 if ':' in line:
221 upstream = line[:line.index(':')]
222 return Repo(name, is_current, upstream)
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200223
Christoffer Quist Adamsen7a5deed2020-11-16 11:31:15 +0100224 if ']' in line:
225 upstream = line[:line.index(']')]
226 return Repo(name, is_current, upstream)
227
228 return Repo(name, is_current, None)
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200229
230if __name__ == '__main__':
231 sys.exit(main(sys.argv[1:]))