blob: b7674caa483e725dde6151d659c2d8baead728bf [file] [log] [blame]
Alexandru M Stan725c71f2019-12-11 16:53:33 -08001#!/usr/bin/env python3
Brian Norris6baeb2e2020-03-18 12:13:30 -07002# -*- coding: utf-8 -*-
3#
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -08004# Copyright 2017 The Chromium OS Authors. All rights reserved.
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
Mike Frysingerf80ca212018-07-13 15:02:52 -04007
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -08008"""This is a tool for picking patches from upstream and applying them."""
9
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080010import argparse
Alexandru M Stan725c71f2019-12-11 16:53:33 -080011import configparser
Tzung-Bi Shih436fdba2019-09-04 19:05:00 +080012import functools
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
Douglas Anderson297a3062020-09-09 12:47:09 -070018import ssl
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080019import subprocess
20import sys
Harry Cuttsae372f32019-02-12 18:01:14 -080021import textwrap
Alexandru M Stan725c71f2019-12-11 16:53:33 -080022import urllib.request
23import xmlrpc.client
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080024
Tzung-Bi Shih436fdba2019-09-04 19:05:00 +080025errprint = functools.partial(print, file=sys.stderr)
26
Brian Norris00148182020-08-20 10:46:51 -070027# pylint: disable=line-too-long
Abhishek Pandit-Subediaea8c502020-07-09 21:56:12 -070028UPSTREAM_URLS = (
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080029 'git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git',
30 'https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git',
31 'https://kernel.googlesource.com/pub/scm/linux/kernel/git/torvalds/linux.git',
Brian Norris8043cfd2020-03-19 11:46:16 -070032 'git://w1.fi/srv/git/hostap.git',
Abhishek Pandit-Subediaea8c502020-07-09 21:56:12 -070033 'git://git.kernel.org/pub/scm/bluetooth/bluez.git',
Douglas Anderson482a6d62020-09-21 09:31:54 -070034 'https://github.com/andersson/qrtr.git',
Brian Norris8043cfd2020-03-19 11:46:16 -070035)
36
Stephen Boydb68c17a2019-09-26 15:08:02 -070037PATCHWORK_URLS = (
38 'https://lore.kernel.org/patchwork',
39 'https://patchwork.kernel.org',
40 'https://patchwork.ozlabs.org',
41 'https://patchwork.freedesktop.org',
42 'https://patchwork.linux-mips.org',
43)
44
Harry Cuttsae372f32019-02-12 18:01:14 -080045COMMIT_MESSAGE_WIDTH = 75
46
Brian Norris9f8a2be2018-06-01 11:14:08 -070047_PWCLIENTRC = os.path.expanduser('~/.pwclientrc')
48
Alexandru M Stan725c71f2019-12-11 16:53:33 -080049def _git(args, stdin=None, encoding='utf-8'):
50 """Calls a git subcommand.
51
52 Similar to subprocess.check_output.
53
54 Args:
Brian Norris6baeb2e2020-03-18 12:13:30 -070055 args: subcommand + args passed to 'git'.
Alexandru M Stan725c71f2019-12-11 16:53:33 -080056 stdin: a string or bytes (depending on encoding) that will be passed
57 to the git subcommand.
58 encoding: either 'utf-8' (default) or None. Override it to None if
59 you want both stdin and stdout to be raw bytes.
60
61 Returns:
62 the stdout of the git subcommand, same type as stdin. The output is
63 also run through strip to make sure there's no extra whitespace.
64
65 Raises:
66 subprocess.CalledProcessError: when return code is not zero.
67 The exception has a .returncode attribute.
68 """
69 return subprocess.run(
70 ['git'] + args,
71 encoding=encoding,
72 input=stdin,
73 stdout=subprocess.PIPE,
74 check=True,
75 ).stdout.strip()
76
77def _git_returncode(*args, **kwargs):
78 """Same as _git, but return returncode instead of stdout.
79
80 Similar to subprocess.call.
81
82 Never raises subprocess.CalledProcessError.
83 """
84 try:
85 _git(*args, **kwargs)
86 return 0
87 except subprocess.CalledProcessError as e:
88 return e.returncode
89
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070090def _get_conflicts():
91 """Report conflicting files."""
92 resolutions = ('DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU')
93 conflicts = []
Alexandru M Stan725c71f2019-12-11 16:53:33 -080094 output = _git(['status', '--porcelain', '--untracked-files=no'])
95 for line in output.splitlines():
Douglas Anderson46287f92018-04-30 09:58:24 -070096 if not line:
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070097 continue
Douglas Anderson46287f92018-04-30 09:58:24 -070098 resolution, name = line.split(None, 1)
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070099 if resolution in resolutions:
100 conflicts.append(' ' + name)
101 if not conflicts:
Douglas Andersonb6a10fe2019-08-12 13:53:30 -0700102 return ''
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700103 return '\nConflicts:\n%s\n' % '\n'.join(conflicts)
104
Brian Norris8043cfd2020-03-19 11:46:16 -0700105def _find_upstream_remote(urls):
106 """Find a remote pointing to an upstream repository."""
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800107 for remote in _git(['remote']).splitlines():
108 try:
Brian Norris8043cfd2020-03-19 11:46:16 -0700109 if _git(['remote', 'get-url', remote]) in urls:
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800110 return remote
111 except subprocess.CalledProcessError:
112 # Kinda weird, get-url failing on an item that git just gave us.
113 continue
Guenter Roeckd66daa72018-04-19 10:31:25 -0700114 return None
115
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700116def _pause_for_merge(conflicts):
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800117 """Pause and go in the background till user resolves the conflicts."""
118
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800119 git_root = _git(['rev-parse', '--show-toplevel'])
Harry Cutts2bcd9af2020-02-20 16:27:50 -0800120 previous_head_hash = _git(['rev-parse', 'HEAD'])
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800121
122 paths = (
123 os.path.join(git_root, '.git', 'rebase-apply'),
124 os.path.join(git_root, '.git', 'CHERRY_PICK_HEAD'),
125 )
126 for path in paths:
127 if os.path.exists(path):
Tzung-Bi Shih436fdba2019-09-04 19:05:00 +0800128 errprint('Found "%s".' % path)
129 errprint(conflicts)
130 errprint('Please resolve the conflicts and restart the '
131 'shell job when done. Kill this job if you '
132 'aborted the conflict.')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800133 os.kill(os.getpid(), signal.SIGTSTP)
Harry Cutts2bcd9af2020-02-20 16:27:50 -0800134
135 # Check the conflicts actually got resolved. Otherwise we'll end up
136 # modifying the wrong commit message and probably confusing people.
137 while previous_head_hash == _git(['rev-parse', 'HEAD']):
138 errprint('Error: no new commit has been made. Did you forget to run '
139 '`git am --continue` or `git cherry-pick --continue`?')
140 errprint('Please create a new commit and restart the shell job (or kill'
141 ' it if you aborted the conflict).')
142 os.kill(os.getpid(), signal.SIGTSTP)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800143
Brian Norris9f8a2be2018-06-01 11:14:08 -0700144def _get_pw_url(project):
145 """Retrieve the patchwork server URL from .pwclientrc.
146
Mike Frysingerf80ca212018-07-13 15:02:52 -0400147 Args:
148 project: patchwork project name; if None, we retrieve the default
149 from pwclientrc
Brian Norris9f8a2be2018-06-01 11:14:08 -0700150 """
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800151 config = configparser.ConfigParser()
Brian Norris9f8a2be2018-06-01 11:14:08 -0700152 config.read([_PWCLIENTRC])
153
154 if project is None:
155 try:
156 project = config.get('options', 'default')
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800157 except (configparser.NoSectionError, configparser.NoOptionError) as e:
158 errprint('Error: no default patchwork project found in %s. (%r)'
159 % (_PWCLIENTRC, e))
Brian Norris9f8a2be2018-06-01 11:14:08 -0700160 sys.exit(1)
161
162 if not config.has_option(project, 'url'):
Tzung-Bi Shih436fdba2019-09-04 19:05:00 +0800163 errprint("Error: patchwork URL not found for project '%s'" % project)
Brian Norris9f8a2be2018-06-01 11:14:08 -0700164 sys.exit(1)
165
166 url = config.get(project, 'url')
Brian Norris2d4e9762018-08-15 13:11:47 -0700167 # Strip trailing 'xmlrpc' and/or trailing slash.
168 return re.sub('/(xmlrpc/)?$', '', url)
Brian Norris9f8a2be2018-06-01 11:14:08 -0700169
Harry Cuttsae372f32019-02-12 18:01:14 -0800170def _wrap_commit_line(prefix, content):
Tzung-Bi Shih2d061112020-08-17 10:27:24 +0800171 line = prefix + content
172 indent = ' ' * len(prefix)
Tzung-Bi Shihc4cfabb2020-08-10 12:57:23 +0800173
174 ret = textwrap.fill(line, COMMIT_MESSAGE_WIDTH, subsequent_indent=indent)
Tzung-Bi Shih2d061112020-08-17 10:27:24 +0800175 return ret[len(prefix):]
Harry Cuttsae372f32019-02-12 18:01:14 -0800176
Stephen Boydb68c17a2019-09-26 15:08:02 -0700177def _pick_patchwork(url, patch_id, args):
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800178 if args['tag'] is None:
179 args['tag'] = 'FROMLIST: '
180
Brian Norris8553f032020-03-18 11:59:02 -0700181 try:
182 opener = urllib.request.urlopen('%s/patch/%d/mbox' % (url, patch_id))
183 except urllib.error.HTTPError as e:
184 errprint('Error: could not download patch: %s' % e)
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800185 sys.exit(1)
186 patch_contents = opener.read()
187
188 if not patch_contents:
Tzung-Bi Shih436fdba2019-09-04 19:05:00 +0800189 errprint('Error: No patch content found')
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800190 sys.exit(1)
191
Douglas Andersonbecd4e62019-09-25 13:40:55 -0700192 message_id = mailbox.Message(patch_contents)['Message-Id']
193 message_id = re.sub('^<|>$', '', message_id.strip())
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800194 if args['source_line'] is None:
195 args['source_line'] = '(am from %s/patch/%d/)' % (url, patch_id)
Brian Norris8553f032020-03-18 11:59:02 -0700196 for url_template in [
Brian Norris655b5ce2020-05-08 11:37:38 -0700197 'https://lore.kernel.org/r/%s',
Brian Norris8553f032020-03-18 11:59:02 -0700198 # hostap project (and others) are here, but not kernel.org.
199 'https://marc.info/?i=%s',
200 # public-inbox comes last as a "default"; it has a nice error page
201 # pointing to other redirectors, even if it doesn't have what
202 # you're looking for directly.
203 'https://public-inbox.org/git/%s',
204 ]:
205 alt_url = url_template % message_id
206 if args['debug']:
207 print('Probing archive for message at: %s' % alt_url)
208 try:
209 urllib.request.urlopen(alt_url)
210 except urllib.error.HTTPError as e:
211 # Skip all HTTP errors. We can expect 404 for archives that
212 # don't have this MessageId, or 300 for public-inbox ("not
213 # found, but try these other redirects"). It's less clear what
214 # to do with transitory (or is it permanent?) server failures.
215 if args['debug']:
216 print('Skipping URL %s, error: %s' % (alt_url, e))
217 continue
218 # Success!
219 if args['debug']:
220 print('Found at %s' % alt_url)
221 break
222 else:
223 errprint(
224 "WARNING: couldn't find working MessageId URL; "
225 'defaulting to "%s"' % alt_url)
226 args['source_line'] += '\n(also found at %s)' % alt_url
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800227
Douglas Andersonbecd4e62019-09-25 13:40:55 -0700228 # Auto-snarf the Change-Id if it was encoded into the Message-Id.
229 mo = re.match(r'.*(I[a-f0-9]{40})@changeid$', message_id)
230 if mo and args['changeid'] is None:
231 args['changeid'] = mo.group(1)
232
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800233 if args['replace']:
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800234 _git(['reset', '--hard', 'HEAD~1'])
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800235
Douglas Andersona2e91c42020-08-21 08:46:09 -0700236 return _git_returncode(['am', '-3'], stdin=patch_contents, encoding=None)
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800237
Stephen Boydb68c17a2019-09-26 15:08:02 -0700238def _match_patchwork(match, args):
239 """Match location: pw://### or pw://PROJECT/###."""
240 pw_project = match.group(2)
241 patch_id = int(match.group(3))
242
243 if args['debug']:
244 print('_match_patchwork: pw_project=%s, patch_id=%d' %
245 (pw_project, patch_id))
246
247 url = _get_pw_url(pw_project)
248 return _pick_patchwork(url, patch_id, args)
249
250def _match_msgid(match, args):
251 """Match location: msgid://MSGID."""
252 msgid = match.group(1)
253
254 if args['debug']:
255 print('_match_msgid: message_id=%s' % (msgid))
256
257 # Patchwork requires the brackets so force it
258 msgid = '<' + msgid + '>'
259 url = None
260 for url in PATCHWORK_URLS:
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800261 rpc = xmlrpc.client.ServerProxy(url + '/xmlrpc/')
Douglas Anderson297a3062020-09-09 12:47:09 -0700262 try:
263 res = rpc.patch_list({'msgid': msgid})
264 except ssl.SSLCertVerificationError:
265 errprint('Error: server "%s" gave an SSL error, skipping' % url)
266 continue
Stephen Boydb68c17a2019-09-26 15:08:02 -0700267 if res:
268 patch_id = res[0]['id']
269 break
270 else:
271 errprint('Error: could not find patch based on message id')
272 sys.exit(1)
273
274 return _pick_patchwork(url, patch_id, args)
275
Abhishek Pandit-Subediaea8c502020-07-09 21:56:12 -0700276def _upstream(commit, urls, args):
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800277 if args['debug']:
Abhishek Pandit-Subediaea8c502020-07-09 21:56:12 -0700278 print('_upstream: commit=%s' % commit)
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800279
Brian Norris8043cfd2020-03-19 11:46:16 -0700280 # Confirm an upstream remote is setup.
281 remote = _find_upstream_remote(urls)
282 if not remote:
Tzung-Bi Shih436fdba2019-09-04 19:05:00 +0800283 errprint('Error: need a valid upstream remote')
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800284 sys.exit(1)
285
Brian Norris8043cfd2020-03-19 11:46:16 -0700286 remote_ref = '%s/master' % remote
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800287 try:
Brian Norris8043cfd2020-03-19 11:46:16 -0700288 _git(['merge-base', '--is-ancestor', commit, remote_ref])
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800289 except subprocess.CalledProcessError:
Brian Norris8043cfd2020-03-19 11:46:16 -0700290 errprint('Error: Commit not in %s' % remote_ref)
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800291 sys.exit(1)
292
293 if args['source_line'] is None:
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800294 commit = _git(['rev-parse', commit])
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800295 args['source_line'] = ('(cherry picked from commit %s)' %
296 (commit))
297 if args['tag'] is None:
298 args['tag'] = 'UPSTREAM: '
299
300 if args['replace']:
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800301 _git(['reset', '--hard', 'HEAD~1'])
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800302
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800303 return _git_returncode(['cherry-pick', commit])
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800304
Abhishek Pandit-Subediaea8c502020-07-09 21:56:12 -0700305def _match_upstream(match, args):
306 """Match location: linux://HASH and upstream://HASH."""
Brian Norris8043cfd2020-03-19 11:46:16 -0700307 commit = match.group(1)
Abhishek Pandit-Subediaea8c502020-07-09 21:56:12 -0700308 return _upstream(commit, urls=UPSTREAM_URLS, args=args)
Brian Norris8043cfd2020-03-19 11:46:16 -0700309
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800310def _match_fromgit(match, args):
311 """Match location: git://remote/branch/HASH."""
312 remote = match.group(2)
313 branch = match.group(3)
314 commit = match.group(4)
315
316 if args['debug']:
317 print('_match_fromgit: remote=%s branch=%s commit=%s' %
318 (remote, branch, commit))
319
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800320 try:
321 _git(['merge-base', '--is-ancestor', commit,
322 '%s/%s' % (remote, branch)])
323 except subprocess.CalledProcessError:
Tzung-Bi Shih436fdba2019-09-04 19:05:00 +0800324 errprint('Error: Commit not in %s/%s' % (remote, branch))
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800325 sys.exit(1)
326
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800327 url = _git(['remote', 'get-url', remote])
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800328
329 if args['source_line'] is None:
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800330 commit = _git(['rev-parse', commit])
Tzung-Bi Shiha8f310d2019-09-04 19:10:10 +0800331 args['source_line'] = (
332 '(cherry picked from commit %s\n %s %s)' % (commit, url, branch))
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800333 if args['tag'] is None:
334 args['tag'] = 'FROMGIT: '
335
336 if args['replace']:
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800337 _git(['reset', '--hard', 'HEAD~1'])
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800338
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800339 return _git_returncode(['cherry-pick', commit])
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800340
341def _match_gitfetch(match, args):
342 """Match location: (git|https)://repoURL#branch/HASH."""
343 remote = match.group(1)
344 branch = match.group(3)
345 commit = match.group(4)
346
347 if args['debug']:
348 print('_match_gitfetch: remote=%s branch=%s commit=%s' %
349 (remote, branch, commit))
350
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800351 try:
352 _git(['fetch', remote, branch])
353 except subprocess.CalledProcessError:
Tzung-Bi Shih436fdba2019-09-04 19:05:00 +0800354 errprint('Error: Branch not in %s' % remote)
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800355 sys.exit(1)
356
357 url = remote
358
359 if args['source_line'] is None:
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800360 commit = _git(['rev-parse', commit])
Tzung-Bi Shiha8f310d2019-09-04 19:10:10 +0800361 args['source_line'] = (
362 '(cherry picked from commit %s\n %s %s)' % (commit, url, branch))
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800363 if args['tag'] is None:
364 args['tag'] = 'FROMGIT: '
365
Stephen Boyd4b3869a2020-01-24 15:35:37 -0800366 if args['replace']:
367 _git(['reset', '--hard', 'HEAD~1'])
368
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800369 return _git_returncode(['cherry-pick', commit])
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800370
Stephen Boyd96396032020-02-25 10:12:59 -0800371def _match_gitweb(match, args):
372 """Match location: https://repoURL/commit/?h=branch&id=HASH."""
373 remote = match.group(1)
374 branch = match.group(2)
375 commit = match.group(3)
376
377 if args['debug']:
378 print('_match_gitweb: remote=%s branch=%s commit=%s' %
379 (remote, branch, commit))
380
381 try:
382 _git(['fetch', remote, branch])
383 except subprocess.CalledProcessError:
384 errprint('Error: Branch not in %s' % remote)
385 sys.exit(1)
386
387 url = remote
388
389 if args['source_line'] is None:
390 commit = _git(['rev-parse', commit])
391 args['source_line'] = (
392 '(cherry picked from commit %s\n %s %s)' % (commit, url, branch))
393 if args['tag'] is None:
394 args['tag'] = 'FROMGIT: '
395
396 if args['replace']:
397 _git(['reset', '--hard', 'HEAD~1'])
398
399 return _git_returncode(['cherry-pick', commit])
400
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800401def main(args):
402 """This is the main entrypoint for fromupstream.
403
404 Args:
405 args: sys.argv[1:]
406
407 Returns:
408 An int return code.
409 """
410 parser = argparse.ArgumentParser()
411
412 parser.add_argument('--bug', '-b',
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700413 type=str, help='BUG= line')
Brian Norris6bcfa392020-08-20 10:38:05 -0700414 parser.add_argument('--test', '-t', action='append', default=[],
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700415 type=str, help='TEST= line')
Stephen Boyd24b309b2018-11-06 22:11:00 -0800416 parser.add_argument('--crbug', action='append',
417 type=int, help='BUG=chromium: line')
418 parser.add_argument('--buganizer', action='append',
419 type=int, help='BUG=b: line')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800420 parser.add_argument('--changeid', '-c',
421 help='Overrides the gerrit generated Change-Id line')
Tzung-Bi Shih1a262c02020-08-13 10:49:55 +0800422 parser.add_argument('--cqdepend',
423 type=str, help='Cq-Depend: line')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800424
Tzung-Bi Shihf5d25a82019-09-02 11:40:09 +0800425 parser.add_argument('--replace', '-r',
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800426 action='store_true',
Tzung-Bi Shih196d31e2019-09-01 18:16:28 +0800427 help='Replaces the HEAD commit with this one, taking '
428 'its properties(BUG, TEST, Change-Id). Useful for '
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800429 'updating commits.')
430 parser.add_argument('--nosignoff',
431 dest='signoff', action='store_false')
Tzung-Bi Shih5100c742019-09-02 10:28:32 +0800432 parser.add_argument('--debug', '-d', action='store_true',
433 help='Prints more verbose logs.')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800434
435 parser.add_argument('--tag',
436 help='Overrides the tag from the title')
437 parser.add_argument('--source', '-s',
438 dest='source_line', type=str,
Tzung-Bi Shih196d31e2019-09-01 18:16:28 +0800439 help='Overrides the source line, last line, ex: '
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800440 '(am from http://....)')
441 parser.add_argument('locations',
Douglas Andersonc77a8b82018-05-04 17:02:03 -0700442 nargs='+',
Tzung-Bi Shih196d31e2019-09-01 18:16:28 +0800443 help='Patchwork ID (pw://### or pw://PROJECT/###, '
444 'where PROJECT is defined in ~/.pwclientrc; if no '
445 'PROJECT is specified, the default is retrieved from '
446 '~/.pwclientrc), '
Stephen Boydb68c17a2019-09-26 15:08:02 -0700447 'Message-ID (msgid://MSGID), '
Brian Norris8043cfd2020-03-19 11:46:16 -0700448 'linux commit like linux://HASH, '
Abhishek Pandit-Subediaea8c502020-07-09 21:56:12 -0700449 'upstream commit like upstream://HASH, or '
Tzung-Bi Shih196d31e2019-09-01 18:16:28 +0800450 'git reference like git://remote/branch/HASH or '
451 'git://repoURL#branch/HASH or '
Stephen Boyd96396032020-02-25 10:12:59 -0800452 'https://repoURL#branch/HASH or '
453 'https://repoURL/commit/?h=branch&id=HASH')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800454
455 args = vars(parser.parse_args(args))
456
Tzung-Bi Shih621fed02020-08-14 22:58:55 +0800457 cq_depends = [args['cqdepend']] if args['cqdepend'] else []
458
Tzung-Bi Shih2d061112020-08-17 10:27:24 +0800459 bug_lines = []
460 if args['bug']:
461 # un-wrap intentionally
462 bug_lines += [args['bug']]
Stephen Boyd24b309b2018-11-06 22:11:00 -0800463 if args['buganizer']:
Tzung-Bi Shih2d061112020-08-17 10:27:24 +0800464 buganizers = ', '.join('b:%d' % x for x in args['buganizer'])
Brian Norris00148182020-08-20 10:46:51 -0700465 bug_lines += [x.strip(' ,') for x in
466 _wrap_commit_line('BUG=', buganizers).split('\n')]
Stephen Boyd24b309b2018-11-06 22:11:00 -0800467 if args['crbug']:
Tzung-Bi Shih2d061112020-08-17 10:27:24 +0800468 crbugs = ', '.join('chromium:%d' % x for x in args['crbug'])
Brian Norris00148182020-08-20 10:46:51 -0700469 bug_lines += [x.strip(' ,') for x in
470 _wrap_commit_line('BUG=', crbugs).split('\n')]
Stephen Boyd24b309b2018-11-06 22:11:00 -0800471
Brian Norris6bcfa392020-08-20 10:38:05 -0700472 test_lines = [_wrap_commit_line('TEST=', x) for x in args['test']]
Tzung-Bi Shihc4cfabb2020-08-10 12:57:23 +0800473
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800474 if args['replace']:
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800475 old_commit_message = _git(['show', '-s', '--format=%B', 'HEAD'])
Tzung-Bi Shih231fada2019-09-02 00:54:59 +0800476
477 # It is possible that multiple Change-Ids are in the commit message
478 # (due to cherry picking). We only want to pull out the first one.
479 changeid_match = re.search('^Change-Id: (.*)$',
480 old_commit_message, re.MULTILINE)
Tzung-Bi Shih5fd0fd52020-08-13 11:06:33 +0800481 if args['changeid'] is None and changeid_match:
Tzung-Bi Shih231fada2019-09-02 00:54:59 +0800482 args['changeid'] = changeid_match.group(1)
483
Tzung-Bi Shih621fed02020-08-14 22:58:55 +0800484 if not cq_depends:
485 cq_depends = re.findall(r'^Cq-Depend:\s+(.*)$',
486 old_commit_message, re.MULTILINE)
Tzung-Bi Shihdfe82002020-08-13 11:00:56 +0800487
Tzung-Bi Shih621fed02020-08-14 22:58:55 +0800488 if not bug_lines:
489 bug_lines = re.findall(r'^BUG=(.*)$',
490 old_commit_message, re.MULTILINE)
Tzung-Bi Shih04345302019-09-02 12:04:01 +0800491
Tzung-Bi Shih621fed02020-08-14 22:58:55 +0800492 if not test_lines:
493 # Note: use (?=...) to avoid to consume the source string
494 test_lines = re.findall(r"""
495 ^TEST=(.*?) # Match start from TEST= until
496 \n # (to remove the tailing newlines)
497 (?=^$| # a blank line
498 ^Cq-Depend:| # or Cq-Depend:
499 ^Change-Id:| # or Change-Id:
500 ^BUG=| # or following BUG=
501 ^TEST=) # or another TEST=
502 """,
503 old_commit_message, re.MULTILINE | re.DOTALL | re.VERBOSE)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800504
Tzung-Bi Shih621fed02020-08-14 22:58:55 +0800505 if not bug_lines or not test_lines:
Tzung-Bi Shih196d31e2019-09-01 18:16:28 +0800506 parser.error('BUG=/TEST= lines are required; --replace can help '
Stephen Boyde6fdf912018-11-09 10:30:57 -0800507 'automate, or set via --bug/--test')
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700508
Tzung-Bi Shih5100c742019-09-02 10:28:32 +0800509 if args['debug']:
510 pprint.pprint(args)
511
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800512 re_matches = (
Tzung-Bi Shihe1e0e7d2019-09-02 15:04:38 +0800513 (re.compile(r'^pw://(([^/]+)/)?(\d+)'), _match_patchwork),
Stephen Boydb68c17a2019-09-26 15:08:02 -0700514 (re.compile(r'^msgid://<?([^>]*)>?'), _match_msgid),
Abhishek Pandit-Subediaea8c502020-07-09 21:56:12 -0700515 (re.compile(r'^linux://([0-9a-f]+)'), _match_upstream),
516 (re.compile(r'^upstream://([0-9a-f]+)'), _match_upstream),
Tzung-Bi Shihe1e0e7d2019-09-02 15:04:38 +0800517 (re.compile(r'^(from)?git://([^/\#]+)/([^#]+)/([0-9a-f]+)$'),
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800518 _match_fromgit),
Tzung-Bi Shihe1e0e7d2019-09-02 15:04:38 +0800519 (re.compile(r'^((git|https)://.+)#(.+)/([0-9a-f]+)$'), _match_gitfetch),
Stephen Boyd96396032020-02-25 10:12:59 -0800520 (re.compile(r'^(https://.+)/commit/\?h=(.+)\&id=([0-9a-f]+)$'), _match_gitweb),
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800521 )
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800522
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800523 for location in args['locations']:
Tzung-Bi Shih5100c742019-09-02 10:28:32 +0800524 if args['debug']:
525 print('location=%s' % location)
526
Tzung-Bi Shih886c9092019-09-02 12:46:16 +0800527 for reg, handler in re_matches:
528 match = reg.match(location)
529 if match:
530 ret = handler(match, args)
531 break
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800532 else:
Tzung-Bi Shih436fdba2019-09-04 19:05:00 +0800533 errprint('Don\'t know what "%s" means.' % location)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800534 sys.exit(1)
535
536 if ret != 0:
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700537 conflicts = _get_conflicts()
Douglas Anderson2108e532018-04-30 09:50:42 -0700538 if args['tag'] == 'UPSTREAM: ':
539 args['tag'] = 'BACKPORT: '
540 else:
541 args['tag'] = 'BACKPORT: ' + args['tag']
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700542 _pause_for_merge(conflicts)
543 else:
Douglas Andersonb6a10fe2019-08-12 13:53:30 -0700544 conflicts = ''
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800545
546 # extract commit message
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800547 commit_message = _git(['show', '-s', '--format=%B', 'HEAD'])
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800548
Guenter Roeck2e4f2512018-04-24 09:20:51 -0700549 # Remove stray Change-Id, most likely from merge resolution
550 commit_message = re.sub(r'Change-Id:.*\n?', '', commit_message)
551
Brian Norris7a41b982018-06-01 10:28:29 -0700552 # Note the source location before tagging anything else
553 commit_message += '\n' + args['source_line']
554
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800555 # add automatic Change ID, BUG, and TEST (and maybe signoff too) so
556 # next commands know where to work on
557 commit_message += '\n'
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700558 commit_message += conflicts
Tzung-Bi Shih621fed02020-08-14 22:58:55 +0800559 commit_message += '\n'
560 commit_message += '\n'.join('BUG=%s' % bug for bug in bug_lines)
561 commit_message += '\n'
562 commit_message += '\n'.join('TEST=%s' % t for t in test_lines)
Brian Norris674209e2020-04-22 15:33:53 -0700563
564 extra = []
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800565 if args['signoff']:
Brian Norris674209e2020-04-22 15:33:53 -0700566 signoff = 'Signed-off-by: %s <%s>' % (
567 _git(['config', 'user.name']),
568 _git(['config', 'user.email']))
569 if not signoff in commit_message.splitlines():
570 extra += ['-s']
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800571 _git(['commit'] + extra + ['--amend', '-F', '-'], stdin=commit_message)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800572
573 # re-extract commit message
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800574 commit_message = _git(['show', '-s', '--format=%B', 'HEAD'])
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800575
Douglas Andersonbecd4e62019-09-25 13:40:55 -0700576 # If we see a "Link: " that seems to point to a Message-Id with an
577 # automatic Change-Id we'll snarf it out.
578 mo = re.search(r'^Link:.*(I[a-f0-9]{40})@changeid', commit_message,
579 re.MULTILINE)
580 if mo and args['changeid'] is None:
581 args['changeid'] = mo.group(1)
582
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800583 # replace changeid if needed
584 if args['changeid'] is not None:
585 commit_message = re.sub(r'(Change-Id: )(\w+)', r'\1%s' %
586 args['changeid'], commit_message)
587 args['changeid'] = None
588
Tzung-Bi Shih621fed02020-08-14 22:58:55 +0800589 if cq_depends:
Tzung-Bi Shih1a262c02020-08-13 10:49:55 +0800590 commit_message = re.sub(
Tzung-Bi Shih621fed02020-08-14 22:58:55 +0800591 r'(Change-Id: \w+)',
592 r'%s\n\1' % '\n'.join('Cq-Depend: %s' % c for c in cq_depends),
Tzung-Bi Shih1a262c02020-08-13 10:49:55 +0800593 commit_message)
594
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800595 # decorate it that it's from outside
596 commit_message = args['tag'] + commit_message
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800597
598 # commit everything
Alexandru M Stan725c71f2019-12-11 16:53:33 -0800599 _git(['commit', '--amend', '-F', '-'], stdin=commit_message)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800600
Chirantan Ekbote4b08e712019-06-12 15:35:41 +0900601 return 0
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800602
603if __name__ == '__main__':
604 sys.exit(main(sys.argv[1:]))