blob: f755c65c1adc4f3c1568be4503ded7bc63d3f871 [file] [log] [blame]
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -08001#!/usr/bin/env python2
Mike Frysingerf80ca212018-07-13 15:02:52 -04002# -*- coding: utf-8 -*-
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -08003# Copyright 2017 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
Mike Frysingerf80ca212018-07-13 15:02:52 -04006
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -08007"""This is a tool for picking patches from upstream and applying them."""
8
9from __future__ import print_function
10
Brian Norris9f8a2be2018-06-01 11:14:08 -070011import ConfigParser
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080012import argparse
Brian Norrisc3421042018-08-15 14:17:26 -070013import mailbox
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080014import os
Tzung-Bi Shih5100c742019-09-02 10:28:32 +080015import pprint
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080016import re
17import signal
18import subprocess
19import sys
Harry Cuttsae372f32019-02-12 18:01:14 -080020import textwrap
Brian Norris2d4e9762018-08-15 13:11:47 -070021import urllib
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080022
23LINUX_URLS = (
24 'git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git',
25 'https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git',
26 'https://kernel.googlesource.com/pub/scm/linux/kernel/git/torvalds/linux.git',
27)
28
Harry Cuttsae372f32019-02-12 18:01:14 -080029COMMIT_MESSAGE_WIDTH = 75
30
Brian Norris9f8a2be2018-06-01 11:14:08 -070031_PWCLIENTRC = os.path.expanduser('~/.pwclientrc')
32
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070033def _get_conflicts():
34 """Report conflicting files."""
35 resolutions = ('DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU')
36 conflicts = []
Douglas Anderson46287f92018-04-30 09:58:24 -070037 lines = subprocess.check_output(['git', 'status', '--porcelain',
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070038 '--untracked-files=no']).split('\n')
Douglas Anderson46287f92018-04-30 09:58:24 -070039 for line in lines:
40 if not line:
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070041 continue
Douglas Anderson46287f92018-04-30 09:58:24 -070042 resolution, name = line.split(None, 1)
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070043 if resolution in resolutions:
44 conflicts.append(' ' + name)
45 if not conflicts:
Douglas Andersonb6a10fe2019-08-12 13:53:30 -070046 return ''
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070047 return '\nConflicts:\n%s\n' % '\n'.join(conflicts)
48
Guenter Roeckd66daa72018-04-19 10:31:25 -070049def _find_linux_remote():
50 """Find a remote pointing to a Linux upstream repository."""
51 git_remote = subprocess.Popen(['git', 'remote'], stdout=subprocess.PIPE)
52 remotes = git_remote.communicate()[0].strip()
53 for remote in remotes.splitlines():
54 rurl = subprocess.Popen(['git', 'remote', 'get-url', remote],
55 stdout=subprocess.PIPE)
56 url = rurl.communicate()[0].strip()
57 if not rurl.returncode and url in LINUX_URLS:
58 return remote
59 return None
60
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070061def _pause_for_merge(conflicts):
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080062 """Pause and go in the background till user resolves the conflicts."""
63
64 git_root = subprocess.check_output(['git', 'rev-parse',
65 '--show-toplevel']).strip('\n')
66
67 paths = (
68 os.path.join(git_root, '.git', 'rebase-apply'),
69 os.path.join(git_root, '.git', 'CHERRY_PICK_HEAD'),
70 )
71 for path in paths:
72 if os.path.exists(path):
73 sys.stderr.write('Found "%s".\n' % path)
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070074 sys.stderr.write(conflicts)
Tzung-Bi Shih196d31e2019-09-01 18:16:28 +080075 sys.stderr.write('Please resolve the conflicts and restart the '
76 'shell job when done. Kill this job if you '
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080077 'aborted the conflict.\n')
78 os.kill(os.getpid(), signal.SIGTSTP)
79 # TODO: figure out what the state is after the merging, and go based on
80 # that (should we abort? skip? continue?)
81 # Perhaps check last commit message to see if it's the one we were using.
82
Brian Norris9f8a2be2018-06-01 11:14:08 -070083def _get_pw_url(project):
84 """Retrieve the patchwork server URL from .pwclientrc.
85
Mike Frysingerf80ca212018-07-13 15:02:52 -040086 Args:
87 project: patchwork project name; if None, we retrieve the default
88 from pwclientrc
Brian Norris9f8a2be2018-06-01 11:14:08 -070089 """
90 config = ConfigParser.ConfigParser()
91 config.read([_PWCLIENTRC])
92
93 if project is None:
94 try:
95 project = config.get('options', 'default')
96 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
97 sys.stderr.write(
98 'Error: no default patchwork project found in %s.\n'
99 % _PWCLIENTRC)
100 sys.exit(1)
101
102 if not config.has_option(project, 'url'):
Douglas Andersonb6a10fe2019-08-12 13:53:30 -0700103 sys.stderr.write("Error: patchwork URL not found for project '%s'\n"
Brian Norris9f8a2be2018-06-01 11:14:08 -0700104 % project)
105 sys.exit(1)
106
107 url = config.get(project, 'url')
Brian Norris2d4e9762018-08-15 13:11:47 -0700108 # Strip trailing 'xmlrpc' and/or trailing slash.
109 return re.sub('/(xmlrpc/)?$', '', url)
Brian Norris9f8a2be2018-06-01 11:14:08 -0700110
Harry Cuttsae372f32019-02-12 18:01:14 -0800111def _wrap_commit_line(prefix, content):
112 line = prefix + '=' + content
113 indent = ' ' * (len(prefix) + 1)
114 return textwrap.fill(line, COMMIT_MESSAGE_WIDTH, subsequent_indent=indent)
115
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800116def _match_patchwork(match, args):
117 """Match location: pw://### or pw://PROJECT/###."""
118 pw_project = match.group(2)
119 patch_id = int(match.group(3))
120
121 if args['debug']:
122 print('_match_patchwork: pw_project=%s, patch_id=%d' %
123 (pw_project, patch_id))
124
125 if args['tag'] is None:
126 args['tag'] = 'FROMLIST: '
127
128 url = _get_pw_url(pw_project)
129 opener = urllib.urlopen('%s/patch/%d/mbox' % (url, patch_id))
130 if opener.getcode() != 200:
131 sys.stderr.write('Error: could not download patch - error code %d\n' \
132 % opener.getcode())
133 sys.exit(1)
134 patch_contents = opener.read()
135
136 if not patch_contents:
137 sys.stderr.write('Error: No patch content found\n')
138 sys.exit(1)
139
140 if args['source_line'] is None:
141 args['source_line'] = '(am from %s/patch/%d/)' % (url, patch_id)
142 message_id = mailbox.Message(patch_contents)['Message-Id']
143 message_id = re.sub('^<|>$', '', message_id.strip())
144 args['source_line'] += \
145 '\n(also found at https://lkml.kernel.org/r/%s)' % \
146 message_id
147
148 if args['replace']:
149 subprocess.call(['git', 'reset', '--hard', 'HEAD~1'])
150
151 git_am = subprocess.Popen(['git', 'am', '-3'], stdin=subprocess.PIPE)
152 git_am.communicate(patch_contents)
153 return git_am.returncode
154
155def _match_linux(match, args):
156 """Match location: linux://HASH."""
157 commit = match.group(1)
158
159 if args['debug']:
160 print('_match_linux: commit=%s' % commit)
161
162 # Confirm a 'linux' remote is setup.
163 linux_remote = _find_linux_remote()
164 if not linux_remote:
165 sys.stderr.write('Error: need a valid upstream remote\n')
166 sys.exit(1)
167
168 linux_master = '%s/master' % linux_remote
169 ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
170 commit, linux_master])
171 if ret:
172 sys.stderr.write('Error: Commit not in %s\n' % linux_master)
173 sys.exit(1)
174
175 if args['source_line'] is None:
176 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
177 stdout=subprocess.PIPE)
178 commit = git_pipe.communicate()[0].strip()
179
180 args['source_line'] = ('(cherry picked from commit %s)' %
181 (commit))
182 if args['tag'] is None:
183 args['tag'] = 'UPSTREAM: '
184
185 if args['replace']:
186 subprocess.call(['git', 'reset', '--hard', 'HEAD~1'])
187
188 return subprocess.call(['git', 'cherry-pick', commit])
189
190def _match_fromgit(match, args):
191 """Match location: git://remote/branch/HASH."""
192 remote = match.group(2)
193 branch = match.group(3)
194 commit = match.group(4)
195
196 if args['debug']:
197 print('_match_fromgit: remote=%s branch=%s commit=%s' %
198 (remote, branch, commit))
199
200 ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
201 commit, '%s/%s' % (remote, branch)])
202 if ret:
203 sys.stderr.write('Error: Commit not in %s/%s\n' %
204 (remote, branch))
205 sys.exit(1)
206
207 git_pipe = subprocess.Popen(['git', 'remote', 'get-url', remote],
208 stdout=subprocess.PIPE)
209 url = git_pipe.communicate()[0].strip()
210
211 if args['source_line'] is None:
212 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
213 stdout=subprocess.PIPE)
214 commit = git_pipe.communicate()[0].strip()
215
216 args['source_line'] = \
217 '(cherry picked from commit %s\n %s %s)' % \
218 (commit, url, branch)
219 if args['tag'] is None:
220 args['tag'] = 'FROMGIT: '
221
222 if args['replace']:
223 subprocess.call(['git', 'reset', '--hard', 'HEAD~1'])
224
225 return subprocess.call(['git', 'cherry-pick', commit])
226
227def _match_gitfetch(match, args):
228 """Match location: (git|https)://repoURL#branch/HASH."""
229 remote = match.group(1)
230 branch = match.group(3)
231 commit = match.group(4)
232
233 if args['debug']:
234 print('_match_gitfetch: remote=%s branch=%s commit=%s' %
235 (remote, branch, commit))
236
237 ret = subprocess.call(['git', 'fetch', remote, branch])
238 if ret:
239 sys.stderr.write('Error: Branch not in %s\n' % remote)
240 sys.exit(1)
241
242 url = remote
243
244 if args['source_line'] is None:
245 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
246 stdout=subprocess.PIPE)
247 commit = git_pipe.communicate()[0].strip()
248
249 args['source_line'] = \
250 '(cherry picked from commit %s\n %s %s)' % \
251 (commit, url, branch)
252 if args['tag'] is None:
253 args['tag'] = 'FROMGIT: '
254
255 return subprocess.call(['git', 'cherry-pick', commit])
256
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800257def main(args):
258 """This is the main entrypoint for fromupstream.
259
260 Args:
261 args: sys.argv[1:]
262
263 Returns:
264 An int return code.
265 """
266 parser = argparse.ArgumentParser()
267
268 parser.add_argument('--bug', '-b',
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700269 type=str, help='BUG= line')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800270 parser.add_argument('--test', '-t',
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700271 type=str, help='TEST= line')
Stephen Boyd24b309b2018-11-06 22:11:00 -0800272 parser.add_argument('--crbug', action='append',
273 type=int, help='BUG=chromium: line')
274 parser.add_argument('--buganizer', action='append',
275 type=int, help='BUG=b: line')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800276 parser.add_argument('--changeid', '-c',
277 help='Overrides the gerrit generated Change-Id line')
278
Tzung-Bi Shihf5d25a82019-09-02 11:40:09 +0800279 parser.add_argument('--replace', '-r',
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800280 action='store_true',
Tzung-Bi Shih196d31e2019-09-01 18:16:28 +0800281 help='Replaces the HEAD commit with this one, taking '
282 'its properties(BUG, TEST, Change-Id). Useful for '
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800283 'updating commits.')
284 parser.add_argument('--nosignoff',
285 dest='signoff', action='store_false')
Tzung-Bi Shih5100c742019-09-02 10:28:32 +0800286 parser.add_argument('--debug', '-d', action='store_true',
287 help='Prints more verbose logs.')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800288
289 parser.add_argument('--tag',
290 help='Overrides the tag from the title')
291 parser.add_argument('--source', '-s',
292 dest='source_line', type=str,
Tzung-Bi Shih196d31e2019-09-01 18:16:28 +0800293 help='Overrides the source line, last line, ex: '
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800294 '(am from http://....)')
295 parser.add_argument('locations',
Douglas Andersonc77a8b82018-05-04 17:02:03 -0700296 nargs='+',
Tzung-Bi Shih196d31e2019-09-01 18:16:28 +0800297 help='Patchwork ID (pw://### or pw://PROJECT/###, '
298 'where PROJECT is defined in ~/.pwclientrc; if no '
299 'PROJECT is specified, the default is retrieved from '
300 '~/.pwclientrc), '
301 'linux commit like linux://HASH, or '
302 'git reference like git://remote/branch/HASH or '
303 'git://repoURL#branch/HASH or '
Stephen Boyd66ea1912018-10-30 11:26:54 -0700304 'https://repoURL#branch/HASH')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800305
306 args = vars(parser.parse_args(args))
307
Stephen Boyd24b309b2018-11-06 22:11:00 -0800308 buglist = [args['bug']] if args['bug'] else []
309 if args['buganizer']:
310 buglist += ['b:{0}'.format(x) for x in args['buganizer']]
311 if args['crbug']:
312 buglist += ['chromium:{0}'.format(x) for x in args['crbug']]
Brian Norris667a0cb2018-12-07 09:28:46 -0800313 if buglist:
314 args['bug'] = ', '.join(buglist)
Stephen Boyd24b309b2018-11-06 22:11:00 -0800315
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800316 if args['replace']:
317 old_commit_message = subprocess.check_output(
318 ['git', 'show', '-s', '--format=%B', 'HEAD']
319 ).strip('\n')
Tzung-Bi Shih231fada2019-09-02 00:54:59 +0800320
321 # It is possible that multiple Change-Ids are in the commit message
322 # (due to cherry picking). We only want to pull out the first one.
323 changeid_match = re.search('^Change-Id: (.*)$',
324 old_commit_message, re.MULTILINE)
325 if changeid_match:
326 args['changeid'] = changeid_match.group(1)
327
Tzung-Bi Shih04345302019-09-02 12:04:01 +0800328 bugs = re.findall('BUG=(.*)$', old_commit_message, re.MULTILINE)
329 if args['bug'] is None and bugs:
330 args['bug'] = '\nBUG='.join(bugs)
331
332 tests = re.findall('TEST=(.*)$', old_commit_message, re.MULTILINE)
333 if args['test'] is None and tests:
334 args['test'] = '\nTEST='.join(tests)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800335 # TODO: deal with multiline BUG/TEST better
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800336
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700337 if args['bug'] is None or args['test'] is None:
Tzung-Bi Shih196d31e2019-09-01 18:16:28 +0800338 parser.error('BUG=/TEST= lines are required; --replace can help '
Stephen Boyde6fdf912018-11-09 10:30:57 -0800339 'automate, or set via --bug/--test')
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700340
Tzung-Bi Shih5100c742019-09-02 10:28:32 +0800341 if args['debug']:
342 pprint.pprint(args)
343
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800344 re_matches = (
345 (re.compile(r'pw://(([^/]+)/)?(\d+)'), _match_patchwork),
346 (re.compile(r'linux://([0-9a-f]+)'), _match_linux),
347 (re.compile(r'(from)?git://([^/\#]+)/([^#]+)/([0-9a-f]+)$'),
348 _match_fromgit),
349 (re.compile(r'((git|https)://.+)#(.+)/([0-9a-f]+)$'), _match_gitfetch),
350 )
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800351
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800352 for location in args['locations']:
Tzung-Bi Shih5100c742019-09-02 10:28:32 +0800353 if args['debug']:
354 print('location=%s' % location)
355
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800356 for reg, handler in re_matches:
357 match = reg.match(location)
358 if match:
359 ret = handler(match, args)
360 break
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800361 else:
362 sys.stderr.write('Don\'t know what "%s" means.\n' % location)
363 sys.exit(1)
364
365 if ret != 0:
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700366 conflicts = _get_conflicts()
Douglas Anderson2108e532018-04-30 09:50:42 -0700367 if args['tag'] == 'UPSTREAM: ':
368 args['tag'] = 'BACKPORT: '
369 else:
370 args['tag'] = 'BACKPORT: ' + args['tag']
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700371 _pause_for_merge(conflicts)
372 else:
Douglas Andersonb6a10fe2019-08-12 13:53:30 -0700373 conflicts = ''
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800374
375 # extract commit message
376 commit_message = subprocess.check_output(
377 ['git', 'show', '-s', '--format=%B', 'HEAD']
378 ).strip('\n')
379
Guenter Roeck2e4f2512018-04-24 09:20:51 -0700380 # Remove stray Change-Id, most likely from merge resolution
381 commit_message = re.sub(r'Change-Id:.*\n?', '', commit_message)
382
Brian Norris7a41b982018-06-01 10:28:29 -0700383 # Note the source location before tagging anything else
384 commit_message += '\n' + args['source_line']
385
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800386 # add automatic Change ID, BUG, and TEST (and maybe signoff too) so
387 # next commands know where to work on
388 commit_message += '\n'
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700389 commit_message += conflicts
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800390 commit_message += '\n' + 'BUG=' + args['bug']
Harry Cuttsae372f32019-02-12 18:01:14 -0800391 commit_message += '\n' + _wrap_commit_line('TEST', args['test'])
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800392 if args['signoff']:
393 extra = ['-s']
394 else:
395 extra = []
Stephen Boydaa4e7e02018-11-09 08:48:46 -0800396 subprocess.Popen(
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800397 ['git', 'commit'] + extra + ['--amend', '-F', '-'],
398 stdin=subprocess.PIPE
399 ).communicate(commit_message)
400
401 # re-extract commit message
402 commit_message = subprocess.check_output(
403 ['git', 'show', '-s', '--format=%B', 'HEAD']
404 ).strip('\n')
405
406 # replace changeid if needed
407 if args['changeid'] is not None:
408 commit_message = re.sub(r'(Change-Id: )(\w+)', r'\1%s' %
409 args['changeid'], commit_message)
410 args['changeid'] = None
411
412 # decorate it that it's from outside
413 commit_message = args['tag'] + commit_message
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800414
415 # commit everything
Stephen Boydaa4e7e02018-11-09 08:48:46 -0800416 subprocess.Popen(
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800417 ['git', 'commit', '--amend', '-F', '-'], stdin=subprocess.PIPE
418 ).communicate(commit_message)
419
Chirantan Ekbote4b08e712019-06-12 15:35:41 +0900420 return 0
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800421
422if __name__ == '__main__':
423 sys.exit(main(sys.argv[1:]))