blob: ea67f63f5ffc532bd8fd82dc6fd28b874f15d4aa [file] [log] [blame]
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +02001#!/usr/bin/env python
2# 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 Adamsen6fac89e2020-11-06 11:03:09 +010043 result.add_option('--delete', '-d',
44 help='Delete closed branches',
45 choices=['y', 'n', 'ask'],
46 default='ask')
47 result.add_option('--leave_upstream', '--leave-upstream',
48 help='To not update the upstream of the first open branch',
49 action='store_true')
50 result.add_option('--message', '-m',
51 help='Message for patchset', default='Sync')
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020052 result.add_option('--rebase',
53 help='To use `git pull --rebase` instead of `git pull`',
54 action='store_true')
Christoffer Quist Adamsena8212812019-08-14 15:38:50 +020055 result.add_option('--no_upload', '--no-upload',
56 help='Disable uploading to Gerrit', action='store_true')
Rico Wind1b52acf2021-03-21 12:36:55 +010057 result.add_option('--skip_main', '--skip-main',
58 help='Disable syncing for main',
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +010059 action='store_true')
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020060 (options, args) = result.parse_args(argv)
Christoffer Quist Adamsena8212812019-08-14 15:38:50 +020061 options.upload = not options.no_upload
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +010062 assert options.delete != 'y' or not options.leave_upstream, (
63 'Inconsistent options: cannot leave the upstream of the first open ' +
64 'branch (--leave_upstream) and delete the closed branches at the same ' +
65 'time (--delete).')
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020066 assert options.message, 'A message for the patchset is required.'
67 assert len(args) == 0
68 return options
69
70def main(argv):
71 options = ParseOptions(argv)
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020072 with utils.ChangedWorkingDirectory(REPO_ROOT, quiet=True):
73 branches = [
74 parse(line)
75 for line in utils.RunCmd(['git', 'branch', '-vv'], quiet=True)]
76
77 current_branch = None
78 for branch in branches:
79 if branch.is_current:
80 current_branch = branch
81 break
82 assert current_branch is not None
83
84 if current_branch.upstream == None:
85 print('Nothing to sync')
86 return
87
88 stack = []
Morten Krogh-Jespersen5b225f42019-08-14 15:26:06 +020089 while current_branch:
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020090 stack.append(current_branch)
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +010091 if current_branch.upstream is None:
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020092 break
93 current_branch = get_branch_with_name(current_branch.upstream, branches)
94
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +010095 closed_branches = []
96 has_seen_local_branch = False # A branch that is not uploaded.
97 has_seen_open_branch = False # A branch that is not closed.
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020098 while len(stack) > 0:
99 branch = stack.pop()
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100100
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200101 utils.RunCmd(['git', 'checkout', branch.name], quiet=True)
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100102
103 status = get_status_for_current_branch()
104 print('Syncing %s (status: %s)' % (branch.name, status))
105
106 pull_for_current_branch(branch, options)
107
Rico Wind1b52acf2021-03-21 12:36:55 +0100108 if branch.name == 'main':
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100109 continue
110
111 if status == 'closed':
112 assert not has_seen_local_branch, (
113 'Unexpected closed branch %s after new branch' % branch.name)
114 assert not has_seen_open_branch, (
115 'Unexpected closed branch %s after open branch' % branch.name)
116 closed_branches.append(branch.name)
117 continue
118
119 if not options.leave_upstream:
120 if not has_seen_open_branch and len(closed_branches) > 0:
121 print(
Rico Wind1b52acf2021-03-21 12:36:55 +0100122 'Setting upstream for first open branch %s to main'
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100123 % branch.name)
Rico Wind1b52acf2021-03-21 12:36:55 +0100124 set_upstream_for_current_branch_to_main()
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100125
126 has_seen_open_branch = True
127 has_seen_local_branch = has_seen_local_branch or (status == 'None')
128
Morten Krogh-Jespersen25b512f2020-11-10 11:52:26 +0100129 if options.upload and status != 'closed':
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100130 if has_seen_local_branch:
131 print(
132 'Cannot upload branch %s since it comes after a local branch'
133 % branch.name)
134 else:
135 utils.RunCmd(
136 ['git', 'cl', 'upload', '-m', options.message], quiet=True)
137
Christoffer Quist Adamsend3230442020-11-06 11:12:14 +0100138 if get_delete_branches_option(closed_branches, options):
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100139 delete_branches(closed_branches)
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200140
Morten Krogh-Jespersenc2be3f82019-08-14 15:30:55 +0200141 utils.RunCmd(['git', 'cl', 'issue'])
142
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100143def delete_branches(branches):
144 assert len(branches) > 0
Christoffer Quist Adamsen601aa3c2020-11-27 14:50:31 +0100145 cmd = ['git', 'branch', '-D']
146 cmd.extend(branches)
147 utils.RunCmd(cmd, quiet=True)
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100148
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200149def get_branch_with_name(name, branches):
150 for branch in branches:
151 if branch.name == name:
152 return branch
153 return None
154
Christoffer Quist Adamsend3230442020-11-06 11:12:14 +0100155def get_delete_branches_option(closed_branches, options):
156 if len(closed_branches) == 0:
157 return False
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100158 if options.leave_upstream:
159 return False
160 if options.delete == 'y':
161 return True
162 if options.delete == 'n':
163 return False
164 assert options.delete == 'ask'
165 print('Delete closed branches: %s (Y/N)?' % ", ".join(closed_branches))
166 answer = sys.stdin.read(1)
167 return answer.lower() == 'y'
168
169def get_status_for_current_branch():
170 return utils.RunCmd(['git', 'cl', 'status', '--field', 'status'], quiet=True)[0].strip()
171
172def pull_for_current_branch(branch, options):
Rico Wind1b52acf2021-03-21 12:36:55 +0100173 if branch.name == 'main' and options.skip_main:
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100174 return
175 rebase_args = ['--rebase'] if options.rebase else []
176 utils.RunCmd(['git', 'pull'] + rebase_args, quiet=True)
177
178
Rico Wind1b52acf2021-03-21 12:36:55 +0100179def set_upstream_for_current_branch_to_main():
180 utils.RunCmd(['git', 'cl', 'upstream', 'main'], quiet=True)
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100181
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200182# Parses a line from the output of `git branch -vv`.
183#
184# Example output ('*' denotes the current branch):
185#
186# $ git branch -vv
187# * feature_final xxxxxxxxx [feature_prereq_c: ...] ...
188# feature_prereq_c xxxxxxxxx [feature_prereq_b: ...] ...
189# feature_prereq_b xxxxxxxxx [feature_prereq_a: ...] ...
Rico Wind1b52acf2021-03-21 12:36:55 +0100190# feature_prereq_a xxxxxxxxx [main: ...] ...
191# main xxxxxxxxx [origin/main] ...
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200192def parse(line):
193 is_current = False
194 if line.startswith('*'):
195 is_current = True
196 line = line[1:].lstrip()
197 else:
198 line = line.lstrip()
199
200 name_end_index = line.index(' ')
201 name = line[:name_end_index]
202 line = line[name_end_index:].lstrip()
203
Christoffer Quist Adamsen7a5deed2020-11-16 11:31:15 +0100204 if '[' in line:
205 line = line[line.index('[')+1:]
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200206
Christoffer Quist Adamsen7a5deed2020-11-16 11:31:15 +0100207 if ':' in line:
208 upstream = line[:line.index(':')]
209 return Repo(name, is_current, upstream)
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200210
Christoffer Quist Adamsen7a5deed2020-11-16 11:31:15 +0100211 if ']' in line:
212 upstream = line[:line.index(']')]
213 return Repo(name, is_current, upstream)
214
215 return Repo(name, is_current, None)
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200216
217if __name__ == '__main__':
218 sys.exit(main(sys.argv[1:]))