blob: f89bde60bd99aa04f4aaa6e934114a8e8d54f94e [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
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020035
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020036class Repo(object):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020037
38 def __init__(self, name, is_current, upstream):
39 self.name = name
40 self.is_current = is_current
41 self.upstream = upstream
42
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020043
44def ParseOptions(argv):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020045 result = optparse.OptionParser()
46 result.add_option('--bypass-hooks',
47 help='Bypass presubmit hooks',
48 action='store_true')
49 result.add_option('--delete',
50 '-d',
51 help='Delete closed branches',
52 choices=['y', 'n', 'ask'],
53 default='ask')
54 result.add_option('--from_branch',
55 '-f',
56 help='Uppermost upstream to sync from',
57 default='main')
58 result.add_option(
59 '--leave_upstream',
60 '--leave-upstream',
61 help='To not update the upstream of the first open branch',
62 action='store_true')
63 result.add_option('--message',
64 '-m',
65 help='Message for patchset',
66 default='Sync')
67 result.add_option('--rebase',
68 help='To use `git pull --rebase` instead of `git pull`',
69 action='store_true')
70 result.add_option('--no_upload',
71 '--no-upload',
72 help='Disable uploading to Gerrit',
73 action='store_true')
74 result.add_option('--skip_main',
75 '--skip-main',
76 help='Disable syncing for main',
77 action='store_true')
78 (options, args) = result.parse_args(argv)
79 options.upload = not options.no_upload
80 assert options.delete != 'y' or not options.leave_upstream, (
81 'Inconsistent options: cannot leave the upstream of the first open ' +
82 'branch (--leave_upstream) and delete the closed branches at the same '
83 + 'time (--delete).')
84 assert options.message, 'A message for the patchset is required.'
85 assert len(args) == 0
86 return options
87
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020088
89def main(argv):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020090 options = ParseOptions(argv)
91 with utils.ChangedWorkingDirectory(REPO_ROOT, quiet=True):
92 branches = [
93 parse(line)
94 for line in utils.RunCmd(['git', 'branch', '-vv'], quiet=True)
95 ]
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020096
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020097 current_branch = None
98 for branch in branches:
99 if branch.is_current:
100 current_branch = branch
101 break
102 assert current_branch is not None
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200103
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200104 if is_root_branch(current_branch, options):
105 print('Nothing to sync')
106 return
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200107
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200108 stack = []
109 while current_branch:
110 stack.append(current_branch)
111 if is_root_branch(current_branch, options):
112 break
113 current_branch = get_branch_with_name(current_branch.upstream,
114 branches)
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200115
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200116 closed_branches = []
117 has_seen_local_branch = False # A branch that is not uploaded.
118 has_seen_open_branch = False # A branch that is not closed.
119 while len(stack) > 0:
120 branch = stack.pop()
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100121
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200122 utils.RunCmd(['git', 'checkout', branch.name], quiet=True)
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100123
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200124 status = get_status_for_current_branch(branch)
125 print('Syncing %s (status: %s)' % (branch.name, status))
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100126
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200127 pull_for_current_branch(branch, options)
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100128
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200129 if branch.name == 'main':
130 continue
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100131
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200132 if status == 'closed':
133 assert not has_seen_local_branch, (
134 'Unexpected closed branch %s after new branch' %
135 branch.name)
136 assert not has_seen_open_branch, (
137 'Unexpected closed branch %s after open branch' %
138 branch.name)
139 closed_branches.append(branch.name)
140 continue
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100141
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200142 if not options.leave_upstream:
143 if not has_seen_open_branch and len(closed_branches) > 0:
144 print('Setting upstream for first open branch %s to main' %
145 branch.name)
146 set_upstream_for_current_branch_to_main()
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100147
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200148 has_seen_open_branch = True
149 has_seen_local_branch = has_seen_local_branch or (status == 'None')
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100150
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200151 if options.upload and status != 'closed':
152 if has_seen_local_branch:
153 print(
154 'Cannot upload branch %s since it comes after a local branch'
155 % branch.name)
156 else:
157 upload_cmd = ['git', 'cl', 'upload', '-m', options.message]
158 if options.bypass_hooks:
159 upload_cmd.append('--bypass-hooks')
160 utils.RunCmd(upload_cmd, quiet=True)
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100161
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200162 if get_delete_branches_option(closed_branches, options):
163 delete_branches(closed_branches)
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200164
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200165 utils.RunCmd(['git', 'cl', 'issue'])
166
Morten Krogh-Jespersenc2be3f82019-08-14 15:30:55 +0200167
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100168def delete_branches(branches):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200169 assert len(branches) > 0
170 cmd = ['git', 'branch', '-D']
171 cmd.extend(branches)
172 utils.RunCmd(cmd, quiet=True)
173
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100174
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200175def get_branch_with_name(name, branches):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200176 for branch in branches:
177 if branch.name == name:
178 return branch
179 return None
180
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200181
Christoffer Quist Adamsend3230442020-11-06 11:12:14 +0100182def get_delete_branches_option(closed_branches, options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200183 if len(closed_branches) == 0:
184 return False
185 if options.leave_upstream:
186 return False
187 if options.delete == 'y':
188 return True
189 if options.delete == 'n':
190 return False
191 assert options.delete == 'ask'
192 print('Delete closed branches: %s (Y/N)?' % ", ".join(closed_branches))
193 answer = sys.stdin.read(1)
194 return answer.lower() == 'y'
195
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100196
Christoffer Quist Adamsen7607ebe2022-06-28 11:52:46 +0200197def get_status_for_current_branch(current_branch):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200198 if current_branch.name == 'main':
199 return 'main'
200 return utils.RunCmd(['git', 'cl', 'status', '--field', 'status'],
201 quiet=True)[0].strip()
202
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100203
Christoffer Quist Adamsene2d3f852022-01-12 13:24:07 +0100204def is_root_branch(branch, options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200205 return branch.name == options.from_branch or branch.upstream is None
206
Christoffer Quist Adamsene2d3f852022-01-12 13:24:07 +0100207
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100208def pull_for_current_branch(branch, options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200209 if branch.name == 'main' and options.skip_main:
210 return
211 rebase_args = ['--rebase'] if options.rebase else []
212 utils.RunCmd(['git', 'pull'] + rebase_args, quiet=True)
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100213
214
Rico Wind1b52acf2021-03-21 12:36:55 +0100215def set_upstream_for_current_branch_to_main():
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200216 utils.RunCmd(['git', 'cl', 'upstream', 'main'], quiet=True)
217
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100218
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200219# Parses a line from the output of `git branch -vv`.
220#
221# Example output ('*' denotes the current branch):
222#
223# $ git branch -vv
224# * feature_final xxxxxxxxx [feature_prereq_c: ...] ...
225# feature_prereq_c xxxxxxxxx [feature_prereq_b: ...] ...
226# feature_prereq_b xxxxxxxxx [feature_prereq_a: ...] ...
Rico Wind1b52acf2021-03-21 12:36:55 +0100227# feature_prereq_a xxxxxxxxx [main: ...] ...
228# main xxxxxxxxx [origin/main] ...
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200229def parse(line):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200230 is_current = False
231 if line.startswith('*'):
232 is_current = True
233 line = line[1:].lstrip()
234 else:
235 line = line.lstrip()
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200236
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200237 name_end_index = line.index(' ')
238 name = line[:name_end_index]
239 line = line[name_end_index:].lstrip()
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200240
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200241 if '[' in line:
242 line = line[line.index('[') + 1:]
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200243
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200244 if ':' in line:
245 upstream = line[:line.index(':')]
246 return Repo(name, is_current, upstream)
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200247
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200248 if ']' in line:
249 upstream = line[:line.index(']')]
250 return Repo(name, is_current, upstream)
Christoffer Quist Adamsen7a5deed2020-11-16 11:31:15 +0100251
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200252 return Repo(name, is_current, None)
253
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200254
255if __name__ == '__main__':
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200256 sys.exit(main(sys.argv[1:]))