blob: 98656c028023a3857b4c219a8b31e58a83c207f6 [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
13import os
14import re
15import signal
16import subprocess
17import sys
Brian Norris2d4e9762018-08-15 13:11:47 -070018import urllib
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080019
20LINUX_URLS = (
21 'git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git',
22 'https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git',
23 'https://kernel.googlesource.com/pub/scm/linux/kernel/git/torvalds/linux.git',
24)
25
Brian Norris9f8a2be2018-06-01 11:14:08 -070026_PWCLIENTRC = os.path.expanduser('~/.pwclientrc')
27
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070028def _get_conflicts():
29 """Report conflicting files."""
30 resolutions = ('DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU')
31 conflicts = []
Douglas Anderson46287f92018-04-30 09:58:24 -070032 lines = subprocess.check_output(['git', 'status', '--porcelain',
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070033 '--untracked-files=no']).split('\n')
Douglas Anderson46287f92018-04-30 09:58:24 -070034 for line in lines:
35 if not line:
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070036 continue
Douglas Anderson46287f92018-04-30 09:58:24 -070037 resolution, name = line.split(None, 1)
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070038 if resolution in resolutions:
39 conflicts.append(' ' + name)
40 if not conflicts:
41 return ""
42 return '\nConflicts:\n%s\n' % '\n'.join(conflicts)
43
Guenter Roeckd66daa72018-04-19 10:31:25 -070044def _find_linux_remote():
45 """Find a remote pointing to a Linux upstream repository."""
46 git_remote = subprocess.Popen(['git', 'remote'], stdout=subprocess.PIPE)
47 remotes = git_remote.communicate()[0].strip()
48 for remote in remotes.splitlines():
49 rurl = subprocess.Popen(['git', 'remote', 'get-url', remote],
50 stdout=subprocess.PIPE)
51 url = rurl.communicate()[0].strip()
52 if not rurl.returncode and url in LINUX_URLS:
53 return remote
54 return None
55
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070056def _pause_for_merge(conflicts):
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080057 """Pause and go in the background till user resolves the conflicts."""
58
59 git_root = subprocess.check_output(['git', 'rev-parse',
60 '--show-toplevel']).strip('\n')
61
62 paths = (
63 os.path.join(git_root, '.git', 'rebase-apply'),
64 os.path.join(git_root, '.git', 'CHERRY_PICK_HEAD'),
65 )
66 for path in paths:
67 if os.path.exists(path):
68 sys.stderr.write('Found "%s".\n' % path)
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070069 sys.stderr.write(conflicts)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080070 sys.stderr.write('Please resolve the conflicts and restart the ' +
71 'shell job when done. Kill this job if you ' +
72 'aborted the conflict.\n')
73 os.kill(os.getpid(), signal.SIGTSTP)
74 # TODO: figure out what the state is after the merging, and go based on
75 # that (should we abort? skip? continue?)
76 # Perhaps check last commit message to see if it's the one we were using.
77
Brian Norris9f8a2be2018-06-01 11:14:08 -070078def _get_pw_url(project):
79 """Retrieve the patchwork server URL from .pwclientrc.
80
Mike Frysingerf80ca212018-07-13 15:02:52 -040081 Args:
82 project: patchwork project name; if None, we retrieve the default
83 from pwclientrc
Brian Norris9f8a2be2018-06-01 11:14:08 -070084 """
85 config = ConfigParser.ConfigParser()
86 config.read([_PWCLIENTRC])
87
88 if project is None:
89 try:
90 project = config.get('options', 'default')
91 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
92 sys.stderr.write(
93 'Error: no default patchwork project found in %s.\n'
94 % _PWCLIENTRC)
95 sys.exit(1)
96
97 if not config.has_option(project, 'url'):
98 sys.stderr.write('Error: patchwork URL not found for project \'%s\'\n'
99 % project)
100 sys.exit(1)
101
102 url = config.get(project, 'url')
Brian Norris2d4e9762018-08-15 13:11:47 -0700103 # Strip trailing 'xmlrpc' and/or trailing slash.
104 return re.sub('/(xmlrpc/)?$', '', url)
Brian Norris9f8a2be2018-06-01 11:14:08 -0700105
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800106def main(args):
107 """This is the main entrypoint for fromupstream.
108
109 Args:
110 args: sys.argv[1:]
111
112 Returns:
113 An int return code.
114 """
115 parser = argparse.ArgumentParser()
116
117 parser.add_argument('--bug', '-b',
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700118 type=str, help='BUG= line')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800119 parser.add_argument('--test', '-t',
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700120 type=str, help='TEST= line')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800121 parser.add_argument('--changeid', '-c',
122 help='Overrides the gerrit generated Change-Id line')
123
124 parser.add_argument('--replace',
125 action='store_true',
126 help='Replaces the HEAD commit with this one, taking ' +
127 'its properties(BUG, TEST, Change-Id). Useful for ' +
128 'updating commits.')
129 parser.add_argument('--nosignoff',
130 dest='signoff', action='store_false')
131
132 parser.add_argument('--tag',
133 help='Overrides the tag from the title')
134 parser.add_argument('--source', '-s',
135 dest='source_line', type=str,
136 help='Overrides the source line, last line, ex: ' +
137 '(am from http://....)')
138 parser.add_argument('locations',
Douglas Andersonc77a8b82018-05-04 17:02:03 -0700139 nargs='+',
Brian Norris9f8a2be2018-06-01 11:14:08 -0700140 help='Patchwork ID (pw://### or pw://PROJECT/###, ' +
141 'where PROJECT is defined in ~/.pwclientrc; if no ' +
142 'PROJECT is specified, the default is retrieved from ' +
143 '~/.pwclientrc), ' +
144 'linux commit like linux://HASH, or ' +
Brian Norrisc9aeb2e2018-06-01 10:37:29 -0700145 'git reference like fromgit://remote/branch/HASH')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800146
147 args = vars(parser.parse_args(args))
148
149 if args['replace']:
150 old_commit_message = subprocess.check_output(
151 ['git', 'show', '-s', '--format=%B', 'HEAD']
152 ).strip('\n')
Guenter Roeckf7cebec2018-07-26 16:37:21 -0700153 changeid = re.findall('Change-Id: (.*)$', old_commit_message, re.MULTILINE)
154 if changeid:
155 args['changeid'] = changeid[0]
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700156 if args['bug'] == parser.get_default('bug') and \
157 re.findall('BUG=(.*)$', old_commit_message, re.MULTILINE):
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800158 args['bug'] = '\nBUG='.join(re.findall('BUG=(.*)$',
159 old_commit_message,
160 re.MULTILINE))
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700161 if args['test'] == parser.get_default('test') and \
162 re.findall('TEST=(.*)$', old_commit_message, re.MULTILINE):
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800163 args['test'] = '\nTEST='.join(re.findall('TEST=(.*)$',
164 old_commit_message,
165 re.MULTILINE))
166 # TODO: deal with multiline BUG/TEST better
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800167
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700168 if args['bug'] is None or args['test'] is None:
169 parser.error('BUG=/TEST= lines are required; --replace can help automate, or set via --bug/--test')
170
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800171 while len(args['locations']) > 0:
172 location = args['locations'].pop(0)
173
174 patchwork_match = re.match(
Brian Norris9f8a2be2018-06-01 11:14:08 -0700175 r'pw://(([-A-z]+)/)?(\d+)', location
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800176 )
177 linux_match = re.match(
178 r'linux://([0-9a-f]+)', location
179 )
180 fromgit_match = re.match(
181 r'fromgit://([^/]+)/(.+)/([0-9a-f]+)$', location
182 )
183
184 if patchwork_match is not None:
Brian Norris9f8a2be2018-06-01 11:14:08 -0700185 pw_project = patchwork_match.group(2)
186 patch_id = int(patchwork_match.group(3))
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800187
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800188 if args['tag'] is None:
189 args['tag'] = 'FROMLIST: '
190
Brian Norris2d4e9762018-08-15 13:11:47 -0700191 url = _get_pw_url(pw_project)
192 opener = urllib.urlopen("%s/patch/%d/mbox" % (url, patch_id))
193 if opener.getcode() != 200:
194 sys.stderr.write('Error: could not download patch - error code %d\n' \
195 % opener.getcode())
196 sys.exit(1)
197 patch_contents = opener.read()
Brian Norris9f8a2be2018-06-01 11:14:08 -0700198
Brian Norris2d4e9762018-08-15 13:11:47 -0700199 if not patch_contents:
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800200 sys.stderr.write('Error: No patch content found\n')
201 sys.exit(1)
Guenter Roeckabe49d82018-07-25 14:11:48 -0700202
Brian Norris2d4e9762018-08-15 13:11:47 -0700203 if args['source_line'] is None:
204 args['source_line'] = '(am from %s/patch/%d/)' % (url, patch_id)
205
Guenter Roeckabe49d82018-07-25 14:11:48 -0700206 if args['replace']:
207 subprocess.call(['git', 'reset', '--hard', 'HEAD~1'])
208
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800209 git_am = subprocess.Popen(['git', 'am', '-3'], stdin=subprocess.PIPE)
Brian Norris2d4e9762018-08-15 13:11:47 -0700210 git_am.communicate(patch_contents)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800211 ret = git_am.returncode
212 elif linux_match:
213 commit = linux_match.group(1)
214
215 # Confirm a 'linux' remote is setup.
Guenter Roeckd66daa72018-04-19 10:31:25 -0700216 linux_remote = _find_linux_remote()
217 if not linux_remote:
218 sys.stderr.write('Error: need a valid upstream remote\n')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800219 sys.exit(1)
220
Guenter Roeckd66daa72018-04-19 10:31:25 -0700221 linux_master = '%s/master' % linux_remote
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800222 ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
Guenter Roeckd66daa72018-04-19 10:31:25 -0700223 commit, linux_master])
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800224 if ret:
Guenter Roeckd66daa72018-04-19 10:31:25 -0700225 sys.stderr.write('Error: Commit not in %s\n' % linux_master)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800226 sys.exit(1)
227
228 if args['source_line'] is None:
229 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
230 stdout=subprocess.PIPE)
231 commit = git_pipe.communicate()[0].strip()
232
233 args['source_line'] = ('(cherry picked from commit %s)' %
234 (commit))
235 if args['tag'] is None:
236 args['tag'] = 'UPSTREAM: '
237
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 ret = subprocess.call(['git', 'cherry-pick', commit])
242 elif fromgit_match is not None:
243 remote = fromgit_match.group(1)
244 branch = fromgit_match.group(2)
245 commit = fromgit_match.group(3)
246
247 ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
248 commit, '%s/%s' % (remote, branch)])
249 if ret:
250 sys.stderr.write('Error: Commit not in %s/%s\n' %
251 (remote, branch))
252 sys.exit(1)
253
254 git_pipe = subprocess.Popen(['git', 'remote', 'get-url', remote],
255 stdout=subprocess.PIPE)
256 url = git_pipe.communicate()[0].strip()
257
258 if args['source_line'] is None:
259 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
260 stdout=subprocess.PIPE)
261 commit = git_pipe.communicate()[0].strip()
262
263 args['source_line'] = \
264 '(cherry picked from commit %s\n %s %s)' % \
265 (commit, url, branch)
266 if args['tag'] is None:
267 args['tag'] = 'FROMGIT: '
268
Guenter Roeckabe49d82018-07-25 14:11:48 -0700269 if args['replace']:
270 subprocess.call(['git', 'reset', '--hard', 'HEAD~1'])
271
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800272 ret = subprocess.call(['git', 'cherry-pick', commit])
273 else:
274 sys.stderr.write('Don\'t know what "%s" means.\n' % location)
275 sys.exit(1)
276
277 if ret != 0:
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700278 conflicts = _get_conflicts()
Douglas Anderson2108e532018-04-30 09:50:42 -0700279 if args['tag'] == 'UPSTREAM: ':
280 args['tag'] = 'BACKPORT: '
281 else:
282 args['tag'] = 'BACKPORT: ' + args['tag']
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700283 _pause_for_merge(conflicts)
284 else:
285 conflicts = ""
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800286
287 # extract commit message
288 commit_message = subprocess.check_output(
289 ['git', 'show', '-s', '--format=%B', 'HEAD']
290 ).strip('\n')
291
Guenter Roeck2e4f2512018-04-24 09:20:51 -0700292 # Remove stray Change-Id, most likely from merge resolution
293 commit_message = re.sub(r'Change-Id:.*\n?', '', commit_message)
294
Brian Norris7a41b982018-06-01 10:28:29 -0700295 # Note the source location before tagging anything else
296 commit_message += '\n' + args['source_line']
297
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800298 # add automatic Change ID, BUG, and TEST (and maybe signoff too) so
299 # next commands know where to work on
300 commit_message += '\n'
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700301 commit_message += conflicts
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800302 commit_message += '\n' + 'BUG=' + args['bug']
303 commit_message += '\n' + 'TEST=' + args['test']
304 if args['signoff']:
305 extra = ['-s']
306 else:
307 extra = []
308 commit = subprocess.Popen(
309 ['git', 'commit'] + extra + ['--amend', '-F', '-'],
310 stdin=subprocess.PIPE
311 ).communicate(commit_message)
312
313 # re-extract commit message
314 commit_message = subprocess.check_output(
315 ['git', 'show', '-s', '--format=%B', 'HEAD']
316 ).strip('\n')
317
318 # replace changeid if needed
319 if args['changeid'] is not None:
320 commit_message = re.sub(r'(Change-Id: )(\w+)', r'\1%s' %
321 args['changeid'], commit_message)
322 args['changeid'] = None
323
324 # decorate it that it's from outside
325 commit_message = args['tag'] + commit_message
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800326
327 # commit everything
328 commit = subprocess.Popen(
329 ['git', 'commit', '--amend', '-F', '-'], stdin=subprocess.PIPE
330 ).communicate(commit_message)
331
332 return 0
333
334if __name__ == '__main__':
335 sys.exit(main(sys.argv[1:]))