blob: 470b41defba266efda2220e05885dbe418da6d75 [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
15import re
16import signal
17import subprocess
18import sys
Harry Cuttsae372f32019-02-12 18:01:14 -080019import textwrap
Brian Norris2d4e9762018-08-15 13:11:47 -070020import urllib
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080021
22LINUX_URLS = (
23 'git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git',
24 'https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git',
25 'https://kernel.googlesource.com/pub/scm/linux/kernel/git/torvalds/linux.git',
26)
27
Harry Cuttsae372f32019-02-12 18:01:14 -080028COMMIT_MESSAGE_WIDTH = 75
29
Brian Norris9f8a2be2018-06-01 11:14:08 -070030_PWCLIENTRC = os.path.expanduser('~/.pwclientrc')
31
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070032def _get_conflicts():
33 """Report conflicting files."""
34 resolutions = ('DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU')
35 conflicts = []
Douglas Anderson46287f92018-04-30 09:58:24 -070036 lines = subprocess.check_output(['git', 'status', '--porcelain',
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070037 '--untracked-files=no']).split('\n')
Douglas Anderson46287f92018-04-30 09:58:24 -070038 for line in lines:
39 if not line:
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070040 continue
Douglas Anderson46287f92018-04-30 09:58:24 -070041 resolution, name = line.split(None, 1)
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070042 if resolution in resolutions:
43 conflicts.append(' ' + name)
44 if not conflicts:
Douglas Andersonb6a10fe2019-08-12 13:53:30 -070045 return ''
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070046 return '\nConflicts:\n%s\n' % '\n'.join(conflicts)
47
Guenter Roeckd66daa72018-04-19 10:31:25 -070048def _find_linux_remote():
49 """Find a remote pointing to a Linux upstream repository."""
50 git_remote = subprocess.Popen(['git', 'remote'], stdout=subprocess.PIPE)
51 remotes = git_remote.communicate()[0].strip()
52 for remote in remotes.splitlines():
53 rurl = subprocess.Popen(['git', 'remote', 'get-url', remote],
54 stdout=subprocess.PIPE)
55 url = rurl.communicate()[0].strip()
56 if not rurl.returncode and url in LINUX_URLS:
57 return remote
58 return None
59
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070060def _pause_for_merge(conflicts):
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080061 """Pause and go in the background till user resolves the conflicts."""
62
63 git_root = subprocess.check_output(['git', 'rev-parse',
64 '--show-toplevel']).strip('\n')
65
66 paths = (
67 os.path.join(git_root, '.git', 'rebase-apply'),
68 os.path.join(git_root, '.git', 'CHERRY_PICK_HEAD'),
69 )
70 for path in paths:
71 if os.path.exists(path):
72 sys.stderr.write('Found "%s".\n' % path)
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070073 sys.stderr.write(conflicts)
Tzung-Bi Shih196d31e2019-09-01 18:16:28 +080074 sys.stderr.write('Please resolve the conflicts and restart the '
75 'shell job when done. Kill this job if you '
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080076 'aborted the conflict.\n')
77 os.kill(os.getpid(), signal.SIGTSTP)
78 # TODO: figure out what the state is after the merging, and go based on
79 # that (should we abort? skip? continue?)
80 # Perhaps check last commit message to see if it's the one we were using.
81
Brian Norris9f8a2be2018-06-01 11:14:08 -070082def _get_pw_url(project):
83 """Retrieve the patchwork server URL from .pwclientrc.
84
Mike Frysingerf80ca212018-07-13 15:02:52 -040085 Args:
86 project: patchwork project name; if None, we retrieve the default
87 from pwclientrc
Brian Norris9f8a2be2018-06-01 11:14:08 -070088 """
89 config = ConfigParser.ConfigParser()
90 config.read([_PWCLIENTRC])
91
92 if project is None:
93 try:
94 project = config.get('options', 'default')
95 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
96 sys.stderr.write(
97 'Error: no default patchwork project found in %s.\n'
98 % _PWCLIENTRC)
99 sys.exit(1)
100
101 if not config.has_option(project, 'url'):
Douglas Andersonb6a10fe2019-08-12 13:53:30 -0700102 sys.stderr.write("Error: patchwork URL not found for project '%s'\n"
Brian Norris9f8a2be2018-06-01 11:14:08 -0700103 % project)
104 sys.exit(1)
105
106 url = config.get(project, 'url')
Brian Norris2d4e9762018-08-15 13:11:47 -0700107 # Strip trailing 'xmlrpc' and/or trailing slash.
108 return re.sub('/(xmlrpc/)?$', '', url)
Brian Norris9f8a2be2018-06-01 11:14:08 -0700109
Harry Cuttsae372f32019-02-12 18:01:14 -0800110def _wrap_commit_line(prefix, content):
111 line = prefix + '=' + content
112 indent = ' ' * (len(prefix) + 1)
113 return textwrap.fill(line, COMMIT_MESSAGE_WIDTH, subsequent_indent=indent)
114
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800115def main(args):
116 """This is the main entrypoint for fromupstream.
117
118 Args:
119 args: sys.argv[1:]
120
121 Returns:
122 An int return code.
123 """
124 parser = argparse.ArgumentParser()
125
126 parser.add_argument('--bug', '-b',
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700127 type=str, help='BUG= line')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800128 parser.add_argument('--test', '-t',
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700129 type=str, help='TEST= line')
Stephen Boyd24b309b2018-11-06 22:11:00 -0800130 parser.add_argument('--crbug', action='append',
131 type=int, help='BUG=chromium: line')
132 parser.add_argument('--buganizer', action='append',
133 type=int, help='BUG=b: line')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800134 parser.add_argument('--changeid', '-c',
135 help='Overrides the gerrit generated Change-Id line')
136
137 parser.add_argument('--replace',
138 action='store_true',
Tzung-Bi Shih196d31e2019-09-01 18:16:28 +0800139 help='Replaces the HEAD commit with this one, taking '
140 'its properties(BUG, TEST, Change-Id). Useful for '
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800141 'updating commits.')
142 parser.add_argument('--nosignoff',
143 dest='signoff', action='store_false')
144
145 parser.add_argument('--tag',
146 help='Overrides the tag from the title')
147 parser.add_argument('--source', '-s',
148 dest='source_line', type=str,
Tzung-Bi Shih196d31e2019-09-01 18:16:28 +0800149 help='Overrides the source line, last line, ex: '
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800150 '(am from http://....)')
151 parser.add_argument('locations',
Douglas Andersonc77a8b82018-05-04 17:02:03 -0700152 nargs='+',
Tzung-Bi Shih196d31e2019-09-01 18:16:28 +0800153 help='Patchwork ID (pw://### or pw://PROJECT/###, '
154 'where PROJECT is defined in ~/.pwclientrc; if no '
155 'PROJECT is specified, the default is retrieved from '
156 '~/.pwclientrc), '
157 'linux commit like linux://HASH, or '
158 'git reference like git://remote/branch/HASH or '
159 'git://repoURL#branch/HASH or '
Stephen Boyd66ea1912018-10-30 11:26:54 -0700160 'https://repoURL#branch/HASH')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800161
162 args = vars(parser.parse_args(args))
163
Stephen Boyd24b309b2018-11-06 22:11:00 -0800164 buglist = [args['bug']] if args['bug'] else []
165 if args['buganizer']:
166 buglist += ['b:{0}'.format(x) for x in args['buganizer']]
167 if args['crbug']:
168 buglist += ['chromium:{0}'.format(x) for x in args['crbug']]
Brian Norris667a0cb2018-12-07 09:28:46 -0800169 if buglist:
170 args['bug'] = ', '.join(buglist)
Stephen Boyd24b309b2018-11-06 22:11:00 -0800171
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800172 if args['replace']:
173 old_commit_message = subprocess.check_output(
174 ['git', 'show', '-s', '--format=%B', 'HEAD']
175 ).strip('\n')
Guenter Roeckf7cebec2018-07-26 16:37:21 -0700176 changeid = re.findall('Change-Id: (.*)$', old_commit_message, re.MULTILINE)
177 if changeid:
178 args['changeid'] = changeid[0]
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700179 if args['bug'] == parser.get_default('bug') and \
180 re.findall('BUG=(.*)$', old_commit_message, re.MULTILINE):
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800181 args['bug'] = '\nBUG='.join(re.findall('BUG=(.*)$',
182 old_commit_message,
183 re.MULTILINE))
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700184 if args['test'] == parser.get_default('test') and \
185 re.findall('TEST=(.*)$', old_commit_message, re.MULTILINE):
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800186 args['test'] = '\nTEST='.join(re.findall('TEST=(.*)$',
187 old_commit_message,
188 re.MULTILINE))
189 # TODO: deal with multiline BUG/TEST better
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800190
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700191 if args['bug'] is None or args['test'] is None:
Tzung-Bi Shih196d31e2019-09-01 18:16:28 +0800192 parser.error('BUG=/TEST= lines are required; --replace can help '
Stephen Boyde6fdf912018-11-09 10:30:57 -0800193 'automate, or set via --bug/--test')
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700194
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800195 while len(args['locations']) > 0:
196 location = args['locations'].pop(0)
197
198 patchwork_match = re.match(
Douglas Anderson4a974f02019-06-03 08:57:15 -0700199 r'pw://(([^/]+)/)?(\d+)', location
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800200 )
201 linux_match = re.match(
202 r'linux://([0-9a-f]+)', location
203 )
204 fromgit_match = re.match(
Stephen Boyd66ea1912018-10-30 11:26:54 -0700205 r'(from)?git://([^/\#]+)/([^#]+)/([0-9a-f]+)$', location
206 )
207 gitfetch_match = re.match(
208 r'((git|https)://.+)#(.+)/([0-9a-f]+)$', location
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800209 )
210
211 if patchwork_match is not None:
Brian Norris9f8a2be2018-06-01 11:14:08 -0700212 pw_project = patchwork_match.group(2)
213 patch_id = int(patchwork_match.group(3))
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800214
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800215 if args['tag'] is None:
216 args['tag'] = 'FROMLIST: '
217
Brian Norris2d4e9762018-08-15 13:11:47 -0700218 url = _get_pw_url(pw_project)
Douglas Andersonb6a10fe2019-08-12 13:53:30 -0700219 opener = urllib.urlopen('%s/patch/%d/mbox' % (url, patch_id))
Brian Norris2d4e9762018-08-15 13:11:47 -0700220 if opener.getcode() != 200:
221 sys.stderr.write('Error: could not download patch - error code %d\n' \
222 % opener.getcode())
223 sys.exit(1)
224 patch_contents = opener.read()
Brian Norris9f8a2be2018-06-01 11:14:08 -0700225
Brian Norris2d4e9762018-08-15 13:11:47 -0700226 if not patch_contents:
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800227 sys.stderr.write('Error: No patch content found\n')
228 sys.exit(1)
Guenter Roeckabe49d82018-07-25 14:11:48 -0700229
Brian Norris2d4e9762018-08-15 13:11:47 -0700230 if args['source_line'] is None:
231 args['source_line'] = '(am from %s/patch/%d/)' % (url, patch_id)
Brian Norrisc3421042018-08-15 14:17:26 -0700232 message_id = mailbox.Message(patch_contents)['Message-Id']
233 message_id = re.sub('^<|>$', '', message_id.strip())
234 args['source_line'] += \
235 '\n(also found at https://lkml.kernel.org/r/%s)' % \
236 message_id
Brian Norris2d4e9762018-08-15 13:11:47 -0700237
Guenter Roeckabe49d82018-07-25 14:11:48 -0700238 if args['replace']:
239 subprocess.call(['git', 'reset', '--hard', 'HEAD~1'])
240
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800241 git_am = subprocess.Popen(['git', 'am', '-3'], stdin=subprocess.PIPE)
Brian Norris2d4e9762018-08-15 13:11:47 -0700242 git_am.communicate(patch_contents)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800243 ret = git_am.returncode
244 elif linux_match:
245 commit = linux_match.group(1)
246
247 # Confirm a 'linux' remote is setup.
Guenter Roeckd66daa72018-04-19 10:31:25 -0700248 linux_remote = _find_linux_remote()
249 if not linux_remote:
250 sys.stderr.write('Error: need a valid upstream remote\n')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800251 sys.exit(1)
252
Guenter Roeckd66daa72018-04-19 10:31:25 -0700253 linux_master = '%s/master' % linux_remote
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800254 ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
Guenter Roeckd66daa72018-04-19 10:31:25 -0700255 commit, linux_master])
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800256 if ret:
Guenter Roeckd66daa72018-04-19 10:31:25 -0700257 sys.stderr.write('Error: Commit not in %s\n' % linux_master)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800258 sys.exit(1)
259
260 if args['source_line'] is None:
261 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
262 stdout=subprocess.PIPE)
263 commit = git_pipe.communicate()[0].strip()
264
265 args['source_line'] = ('(cherry picked from commit %s)' %
266 (commit))
267 if args['tag'] is None:
268 args['tag'] = 'UPSTREAM: '
269
Guenter Roeckabe49d82018-07-25 14:11:48 -0700270 if args['replace']:
271 subprocess.call(['git', 'reset', '--hard', 'HEAD~1'])
272
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800273 ret = subprocess.call(['git', 'cherry-pick', commit])
274 elif fromgit_match is not None:
Stephen Boyd66ea1912018-10-30 11:26:54 -0700275 remote = fromgit_match.group(2)
276 branch = fromgit_match.group(3)
277 commit = fromgit_match.group(4)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800278
279 ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
280 commit, '%s/%s' % (remote, branch)])
281 if ret:
282 sys.stderr.write('Error: Commit not in %s/%s\n' %
283 (remote, branch))
284 sys.exit(1)
285
286 git_pipe = subprocess.Popen(['git', 'remote', 'get-url', remote],
287 stdout=subprocess.PIPE)
288 url = git_pipe.communicate()[0].strip()
289
290 if args['source_line'] is None:
291 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
292 stdout=subprocess.PIPE)
293 commit = git_pipe.communicate()[0].strip()
294
295 args['source_line'] = \
296 '(cherry picked from commit %s\n %s %s)' % \
297 (commit, url, branch)
298 if args['tag'] is None:
299 args['tag'] = 'FROMGIT: '
300
Guenter Roeckabe49d82018-07-25 14:11:48 -0700301 if args['replace']:
302 subprocess.call(['git', 'reset', '--hard', 'HEAD~1'])
303
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800304 ret = subprocess.call(['git', 'cherry-pick', commit])
Stephen Boyd66ea1912018-10-30 11:26:54 -0700305 elif gitfetch_match is not None:
306 remote = gitfetch_match.group(1)
307 branch = gitfetch_match.group(3)
308 commit = gitfetch_match.group(4)
309
310 ret = subprocess.call(['git', 'fetch', remote, branch])
311 if ret:
312 sys.stderr.write('Error: Branch not in %s\n' % remote)
313 sys.exit(1)
314
315 url = remote
316
317 if args['source_line'] is None:
318 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
319 stdout=subprocess.PIPE)
320 commit = git_pipe.communicate()[0].strip()
321
322 args['source_line'] = \
323 '(cherry picked from commit %s\n %s %s)' % \
324 (commit, url, branch)
325 if args['tag'] is None:
326 args['tag'] = 'FROMGIT: '
327
328 ret = subprocess.call(['git', 'cherry-pick', commit])
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800329 else:
330 sys.stderr.write('Don\'t know what "%s" means.\n' % location)
331 sys.exit(1)
332
333 if ret != 0:
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700334 conflicts = _get_conflicts()
Douglas Anderson2108e532018-04-30 09:50:42 -0700335 if args['tag'] == 'UPSTREAM: ':
336 args['tag'] = 'BACKPORT: '
337 else:
338 args['tag'] = 'BACKPORT: ' + args['tag']
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700339 _pause_for_merge(conflicts)
340 else:
Douglas Andersonb6a10fe2019-08-12 13:53:30 -0700341 conflicts = ''
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800342
343 # extract commit message
344 commit_message = subprocess.check_output(
345 ['git', 'show', '-s', '--format=%B', 'HEAD']
346 ).strip('\n')
347
Guenter Roeck2e4f2512018-04-24 09:20:51 -0700348 # Remove stray Change-Id, most likely from merge resolution
349 commit_message = re.sub(r'Change-Id:.*\n?', '', commit_message)
350
Brian Norris7a41b982018-06-01 10:28:29 -0700351 # Note the source location before tagging anything else
352 commit_message += '\n' + args['source_line']
353
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800354 # add automatic Change ID, BUG, and TEST (and maybe signoff too) so
355 # next commands know where to work on
356 commit_message += '\n'
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700357 commit_message += conflicts
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800358 commit_message += '\n' + 'BUG=' + args['bug']
Harry Cuttsae372f32019-02-12 18:01:14 -0800359 commit_message += '\n' + _wrap_commit_line('TEST', args['test'])
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800360 if args['signoff']:
361 extra = ['-s']
362 else:
363 extra = []
Stephen Boydaa4e7e02018-11-09 08:48:46 -0800364 subprocess.Popen(
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800365 ['git', 'commit'] + extra + ['--amend', '-F', '-'],
366 stdin=subprocess.PIPE
367 ).communicate(commit_message)
368
369 # re-extract commit message
370 commit_message = subprocess.check_output(
371 ['git', 'show', '-s', '--format=%B', 'HEAD']
372 ).strip('\n')
373
374 # replace changeid if needed
375 if args['changeid'] is not None:
376 commit_message = re.sub(r'(Change-Id: )(\w+)', r'\1%s' %
377 args['changeid'], commit_message)
378 args['changeid'] = None
379
380 # decorate it that it's from outside
381 commit_message = args['tag'] + commit_message
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800382
383 # commit everything
Stephen Boydaa4e7e02018-11-09 08:48:46 -0800384 subprocess.Popen(
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800385 ['git', 'commit', '--amend', '-F', '-'], stdin=subprocess.PIPE
386 ).communicate(commit_message)
387
Chirantan Ekbote4b08e712019-06-12 15:35:41 +0900388 return 0
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800389
390if __name__ == '__main__':
391 sys.exit(main(sys.argv[1:]))