blob: 8fe3c6ff9407e6045d606473bf6bb6c02af5e2ea [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
Brian Norris2d4e9762018-08-15 13:11:47 -070019import urllib
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080020
21LINUX_URLS = (
22 'git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git',
23 'https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git',
24 'https://kernel.googlesource.com/pub/scm/linux/kernel/git/torvalds/linux.git',
25)
26
Brian Norris9f8a2be2018-06-01 11:14:08 -070027_PWCLIENTRC = os.path.expanduser('~/.pwclientrc')
28
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070029def _get_conflicts():
30 """Report conflicting files."""
31 resolutions = ('DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU')
32 conflicts = []
Douglas Anderson46287f92018-04-30 09:58:24 -070033 lines = subprocess.check_output(['git', 'status', '--porcelain',
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070034 '--untracked-files=no']).split('\n')
Douglas Anderson46287f92018-04-30 09:58:24 -070035 for line in lines:
36 if not line:
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070037 continue
Douglas Anderson46287f92018-04-30 09:58:24 -070038 resolution, name = line.split(None, 1)
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070039 if resolution in resolutions:
40 conflicts.append(' ' + name)
41 if not conflicts:
42 return ""
43 return '\nConflicts:\n%s\n' % '\n'.join(conflicts)
44
Guenter Roeckd66daa72018-04-19 10:31:25 -070045def _find_linux_remote():
46 """Find a remote pointing to a Linux upstream repository."""
47 git_remote = subprocess.Popen(['git', 'remote'], stdout=subprocess.PIPE)
48 remotes = git_remote.communicate()[0].strip()
49 for remote in remotes.splitlines():
50 rurl = subprocess.Popen(['git', 'remote', 'get-url', remote],
51 stdout=subprocess.PIPE)
52 url = rurl.communicate()[0].strip()
53 if not rurl.returncode and url in LINUX_URLS:
54 return remote
55 return None
56
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070057def _pause_for_merge(conflicts):
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080058 """Pause and go in the background till user resolves the conflicts."""
59
60 git_root = subprocess.check_output(['git', 'rev-parse',
61 '--show-toplevel']).strip('\n')
62
63 paths = (
64 os.path.join(git_root, '.git', 'rebase-apply'),
65 os.path.join(git_root, '.git', 'CHERRY_PICK_HEAD'),
66 )
67 for path in paths:
68 if os.path.exists(path):
69 sys.stderr.write('Found "%s".\n' % path)
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070070 sys.stderr.write(conflicts)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080071 sys.stderr.write('Please resolve the conflicts and restart the ' +
72 'shell job when done. Kill this job if you ' +
73 'aborted the conflict.\n')
74 os.kill(os.getpid(), signal.SIGTSTP)
75 # TODO: figure out what the state is after the merging, and go based on
76 # that (should we abort? skip? continue?)
77 # Perhaps check last commit message to see if it's the one we were using.
78
Brian Norris9f8a2be2018-06-01 11:14:08 -070079def _get_pw_url(project):
80 """Retrieve the patchwork server URL from .pwclientrc.
81
Mike Frysingerf80ca212018-07-13 15:02:52 -040082 Args:
83 project: patchwork project name; if None, we retrieve the default
84 from pwclientrc
Brian Norris9f8a2be2018-06-01 11:14:08 -070085 """
86 config = ConfigParser.ConfigParser()
87 config.read([_PWCLIENTRC])
88
89 if project is None:
90 try:
91 project = config.get('options', 'default')
92 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
93 sys.stderr.write(
94 'Error: no default patchwork project found in %s.\n'
95 % _PWCLIENTRC)
96 sys.exit(1)
97
98 if not config.has_option(project, 'url'):
99 sys.stderr.write('Error: patchwork URL not found for project \'%s\'\n'
100 % project)
101 sys.exit(1)
102
103 url = config.get(project, 'url')
Brian Norris2d4e9762018-08-15 13:11:47 -0700104 # Strip trailing 'xmlrpc' and/or trailing slash.
105 return re.sub('/(xmlrpc/)?$', '', url)
Brian Norris9f8a2be2018-06-01 11:14:08 -0700106
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800107def main(args):
108 """This is the main entrypoint for fromupstream.
109
110 Args:
111 args: sys.argv[1:]
112
113 Returns:
114 An int return code.
115 """
116 parser = argparse.ArgumentParser()
117
118 parser.add_argument('--bug', '-b',
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700119 type=str, help='BUG= line')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800120 parser.add_argument('--test', '-t',
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700121 type=str, help='TEST= line')
Stephen Boyd24b309b2018-11-06 22:11:00 -0800122 parser.add_argument('--crbug', action='append',
123 type=int, help='BUG=chromium: line')
124 parser.add_argument('--buganizer', action='append',
125 type=int, help='BUG=b: line')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800126 parser.add_argument('--changeid', '-c',
127 help='Overrides the gerrit generated Change-Id line')
128
129 parser.add_argument('--replace',
130 action='store_true',
131 help='Replaces the HEAD commit with this one, taking ' +
132 'its properties(BUG, TEST, Change-Id). Useful for ' +
133 'updating commits.')
134 parser.add_argument('--nosignoff',
135 dest='signoff', action='store_false')
136
137 parser.add_argument('--tag',
138 help='Overrides the tag from the title')
139 parser.add_argument('--source', '-s',
140 dest='source_line', type=str,
141 help='Overrides the source line, last line, ex: ' +
142 '(am from http://....)')
143 parser.add_argument('locations',
Douglas Andersonc77a8b82018-05-04 17:02:03 -0700144 nargs='+',
Brian Norris9f8a2be2018-06-01 11:14:08 -0700145 help='Patchwork ID (pw://### or pw://PROJECT/###, ' +
146 'where PROJECT is defined in ~/.pwclientrc; if no ' +
147 'PROJECT is specified, the default is retrieved from ' +
148 '~/.pwclientrc), ' +
149 'linux commit like linux://HASH, or ' +
Stephen Boyd66ea1912018-10-30 11:26:54 -0700150 'git reference like git://remote/branch/HASH or ' +
151 'git://repoURL#branch/HASH or ' +
152 'https://repoURL#branch/HASH')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800153
154 args = vars(parser.parse_args(args))
155
Stephen Boyd24b309b2018-11-06 22:11:00 -0800156 buglist = [args['bug']] if args['bug'] else []
157 if args['buganizer']:
158 buglist += ['b:{0}'.format(x) for x in args['buganizer']]
159 if args['crbug']:
160 buglist += ['chromium:{0}'.format(x) for x in args['crbug']]
Brian Norris667a0cb2018-12-07 09:28:46 -0800161 if buglist:
162 args['bug'] = ', '.join(buglist)
Stephen Boyd24b309b2018-11-06 22:11:00 -0800163
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800164 if args['replace']:
165 old_commit_message = subprocess.check_output(
166 ['git', 'show', '-s', '--format=%B', 'HEAD']
167 ).strip('\n')
Guenter Roeckf7cebec2018-07-26 16:37:21 -0700168 changeid = re.findall('Change-Id: (.*)$', old_commit_message, re.MULTILINE)
169 if changeid:
170 args['changeid'] = changeid[0]
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700171 if args['bug'] == parser.get_default('bug') and \
172 re.findall('BUG=(.*)$', old_commit_message, re.MULTILINE):
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800173 args['bug'] = '\nBUG='.join(re.findall('BUG=(.*)$',
174 old_commit_message,
175 re.MULTILINE))
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700176 if args['test'] == parser.get_default('test') and \
177 re.findall('TEST=(.*)$', old_commit_message, re.MULTILINE):
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800178 args['test'] = '\nTEST='.join(re.findall('TEST=(.*)$',
179 old_commit_message,
180 re.MULTILINE))
181 # TODO: deal with multiline BUG/TEST better
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800182
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700183 if args['bug'] is None or args['test'] is None:
Stephen Boyde6fdf912018-11-09 10:30:57 -0800184 parser.error('BUG=/TEST= lines are required; --replace can help ' +
185 'automate, or set via --bug/--test')
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700186
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800187 while len(args['locations']) > 0:
188 location = args['locations'].pop(0)
189
190 patchwork_match = re.match(
Brian Norris9f8a2be2018-06-01 11:14:08 -0700191 r'pw://(([-A-z]+)/)?(\d+)', location
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800192 )
193 linux_match = re.match(
194 r'linux://([0-9a-f]+)', location
195 )
196 fromgit_match = re.match(
Stephen Boyd66ea1912018-10-30 11:26:54 -0700197 r'(from)?git://([^/\#]+)/([^#]+)/([0-9a-f]+)$', location
198 )
199 gitfetch_match = re.match(
200 r'((git|https)://.+)#(.+)/([0-9a-f]+)$', location
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800201 )
202
203 if patchwork_match is not None:
Brian Norris9f8a2be2018-06-01 11:14:08 -0700204 pw_project = patchwork_match.group(2)
205 patch_id = int(patchwork_match.group(3))
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800206
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800207 if args['tag'] is None:
208 args['tag'] = 'FROMLIST: '
209
Brian Norris2d4e9762018-08-15 13:11:47 -0700210 url = _get_pw_url(pw_project)
211 opener = urllib.urlopen("%s/patch/%d/mbox" % (url, patch_id))
212 if opener.getcode() != 200:
213 sys.stderr.write('Error: could not download patch - error code %d\n' \
214 % opener.getcode())
215 sys.exit(1)
216 patch_contents = opener.read()
Brian Norris9f8a2be2018-06-01 11:14:08 -0700217
Brian Norris2d4e9762018-08-15 13:11:47 -0700218 if not patch_contents:
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800219 sys.stderr.write('Error: No patch content found\n')
220 sys.exit(1)
Guenter Roeckabe49d82018-07-25 14:11:48 -0700221
Brian Norris2d4e9762018-08-15 13:11:47 -0700222 if args['source_line'] is None:
223 args['source_line'] = '(am from %s/patch/%d/)' % (url, patch_id)
Brian Norrisc3421042018-08-15 14:17:26 -0700224 message_id = mailbox.Message(patch_contents)['Message-Id']
225 message_id = re.sub('^<|>$', '', message_id.strip())
226 args['source_line'] += \
227 '\n(also found at https://lkml.kernel.org/r/%s)' % \
228 message_id
Brian Norris2d4e9762018-08-15 13:11:47 -0700229
Guenter Roeckabe49d82018-07-25 14:11:48 -0700230 if args['replace']:
231 subprocess.call(['git', 'reset', '--hard', 'HEAD~1'])
232
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800233 git_am = subprocess.Popen(['git', 'am', '-3'], stdin=subprocess.PIPE)
Brian Norris2d4e9762018-08-15 13:11:47 -0700234 git_am.communicate(patch_contents)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800235 ret = git_am.returncode
236 elif linux_match:
237 commit = linux_match.group(1)
238
239 # Confirm a 'linux' remote is setup.
Guenter Roeckd66daa72018-04-19 10:31:25 -0700240 linux_remote = _find_linux_remote()
241 if not linux_remote:
242 sys.stderr.write('Error: need a valid upstream remote\n')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800243 sys.exit(1)
244
Guenter Roeckd66daa72018-04-19 10:31:25 -0700245 linux_master = '%s/master' % linux_remote
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800246 ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
Guenter Roeckd66daa72018-04-19 10:31:25 -0700247 commit, linux_master])
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800248 if ret:
Guenter Roeckd66daa72018-04-19 10:31:25 -0700249 sys.stderr.write('Error: Commit not in %s\n' % linux_master)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800250 sys.exit(1)
251
252 if args['source_line'] is None:
253 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
254 stdout=subprocess.PIPE)
255 commit = git_pipe.communicate()[0].strip()
256
257 args['source_line'] = ('(cherry picked from commit %s)' %
258 (commit))
259 if args['tag'] is None:
260 args['tag'] = 'UPSTREAM: '
261
Guenter Roeckabe49d82018-07-25 14:11:48 -0700262 if args['replace']:
263 subprocess.call(['git', 'reset', '--hard', 'HEAD~1'])
264
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800265 ret = subprocess.call(['git', 'cherry-pick', commit])
266 elif fromgit_match is not None:
Stephen Boyd66ea1912018-10-30 11:26:54 -0700267 remote = fromgit_match.group(2)
268 branch = fromgit_match.group(3)
269 commit = fromgit_match.group(4)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800270
271 ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
272 commit, '%s/%s' % (remote, branch)])
273 if ret:
274 sys.stderr.write('Error: Commit not in %s/%s\n' %
275 (remote, branch))
276 sys.exit(1)
277
278 git_pipe = subprocess.Popen(['git', 'remote', 'get-url', remote],
279 stdout=subprocess.PIPE)
280 url = git_pipe.communicate()[0].strip()
281
282 if args['source_line'] is None:
283 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
284 stdout=subprocess.PIPE)
285 commit = git_pipe.communicate()[0].strip()
286
287 args['source_line'] = \
288 '(cherry picked from commit %s\n %s %s)' % \
289 (commit, url, branch)
290 if args['tag'] is None:
291 args['tag'] = 'FROMGIT: '
292
Guenter Roeckabe49d82018-07-25 14:11:48 -0700293 if args['replace']:
294 subprocess.call(['git', 'reset', '--hard', 'HEAD~1'])
295
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800296 ret = subprocess.call(['git', 'cherry-pick', commit])
Stephen Boyd66ea1912018-10-30 11:26:54 -0700297 elif gitfetch_match is not None:
298 remote = gitfetch_match.group(1)
299 branch = gitfetch_match.group(3)
300 commit = gitfetch_match.group(4)
301
302 ret = subprocess.call(['git', 'fetch', remote, branch])
303 if ret:
304 sys.stderr.write('Error: Branch not in %s\n' % remote)
305 sys.exit(1)
306
307 url = remote
308
309 if args['source_line'] is None:
310 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
311 stdout=subprocess.PIPE)
312 commit = git_pipe.communicate()[0].strip()
313
314 args['source_line'] = \
315 '(cherry picked from commit %s\n %s %s)' % \
316 (commit, url, branch)
317 if args['tag'] is None:
318 args['tag'] = 'FROMGIT: '
319
320 ret = subprocess.call(['git', 'cherry-pick', commit])
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800321 else:
322 sys.stderr.write('Don\'t know what "%s" means.\n' % location)
323 sys.exit(1)
324
325 if ret != 0:
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700326 conflicts = _get_conflicts()
Douglas Anderson2108e532018-04-30 09:50:42 -0700327 if args['tag'] == 'UPSTREAM: ':
328 args['tag'] = 'BACKPORT: '
329 else:
330 args['tag'] = 'BACKPORT: ' + args['tag']
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700331 _pause_for_merge(conflicts)
332 else:
333 conflicts = ""
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800334
335 # extract commit message
336 commit_message = subprocess.check_output(
337 ['git', 'show', '-s', '--format=%B', 'HEAD']
338 ).strip('\n')
339
Guenter Roeck2e4f2512018-04-24 09:20:51 -0700340 # Remove stray Change-Id, most likely from merge resolution
341 commit_message = re.sub(r'Change-Id:.*\n?', '', commit_message)
342
Brian Norris7a41b982018-06-01 10:28:29 -0700343 # Note the source location before tagging anything else
344 commit_message += '\n' + args['source_line']
345
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800346 # add automatic Change ID, BUG, and TEST (and maybe signoff too) so
347 # next commands know where to work on
348 commit_message += '\n'
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700349 commit_message += conflicts
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800350 commit_message += '\n' + 'BUG=' + args['bug']
351 commit_message += '\n' + 'TEST=' + args['test']
352 if args['signoff']:
353 extra = ['-s']
354 else:
355 extra = []
Stephen Boydaa4e7e02018-11-09 08:48:46 -0800356 subprocess.Popen(
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800357 ['git', 'commit'] + extra + ['--amend', '-F', '-'],
358 stdin=subprocess.PIPE
359 ).communicate(commit_message)
360
361 # re-extract commit message
362 commit_message = subprocess.check_output(
363 ['git', 'show', '-s', '--format=%B', 'HEAD']
364 ).strip('\n')
365
366 # replace changeid if needed
367 if args['changeid'] is not None:
368 commit_message = re.sub(r'(Change-Id: )(\w+)', r'\1%s' %
369 args['changeid'], commit_message)
370 args['changeid'] = None
371
372 # decorate it that it's from outside
373 commit_message = args['tag'] + commit_message
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800374
375 # commit everything
Stephen Boydaa4e7e02018-11-09 08:48:46 -0800376 subprocess.Popen(
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800377 ['git', 'commit', '--amend', '-F', '-'], stdin=subprocess.PIPE
378 ).communicate(commit_message)
379
380 return 0
381
382if __name__ == '__main__':
383 sys.exit(main(sys.argv[1:]))