blob: 8949eb5700564b23484146f1ec4f4824ca7c68b1 [file] [log] [blame]
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -08001#!/usr/bin/env python2
2#
3# 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.
6"""This is a tool for picking patches from upstream and applying them."""
7
8from __future__ import print_function
9
Brian Norris9f8a2be2018-06-01 11:14:08 -070010import ConfigParser
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080011import argparse
12import os
13import re
14import signal
15import subprocess
16import sys
17
18LINUX_URLS = (
19 'git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git',
20 'https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git',
21 'https://kernel.googlesource.com/pub/scm/linux/kernel/git/torvalds/linux.git',
22)
23
Brian Norris9f8a2be2018-06-01 11:14:08 -070024_PWCLIENTRC = os.path.expanduser('~/.pwclientrc')
25
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070026def _get_conflicts():
27 """Report conflicting files."""
28 resolutions = ('DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU')
29 conflicts = []
Douglas Anderson46287f92018-04-30 09:58:24 -070030 lines = subprocess.check_output(['git', 'status', '--porcelain',
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070031 '--untracked-files=no']).split('\n')
Douglas Anderson46287f92018-04-30 09:58:24 -070032 for line in lines:
33 if not line:
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070034 continue
Douglas Anderson46287f92018-04-30 09:58:24 -070035 resolution, name = line.split(None, 1)
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070036 if resolution in resolutions:
37 conflicts.append(' ' + name)
38 if not conflicts:
39 return ""
40 return '\nConflicts:\n%s\n' % '\n'.join(conflicts)
41
Guenter Roeckd66daa72018-04-19 10:31:25 -070042def _find_linux_remote():
43 """Find a remote pointing to a Linux upstream repository."""
44 git_remote = subprocess.Popen(['git', 'remote'], stdout=subprocess.PIPE)
45 remotes = git_remote.communicate()[0].strip()
46 for remote in remotes.splitlines():
47 rurl = subprocess.Popen(['git', 'remote', 'get-url', remote],
48 stdout=subprocess.PIPE)
49 url = rurl.communicate()[0].strip()
50 if not rurl.returncode and url in LINUX_URLS:
51 return remote
52 return None
53
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070054def _pause_for_merge(conflicts):
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080055 """Pause and go in the background till user resolves the conflicts."""
56
57 git_root = subprocess.check_output(['git', 'rev-parse',
58 '--show-toplevel']).strip('\n')
59
60 paths = (
61 os.path.join(git_root, '.git', 'rebase-apply'),
62 os.path.join(git_root, '.git', 'CHERRY_PICK_HEAD'),
63 )
64 for path in paths:
65 if os.path.exists(path):
66 sys.stderr.write('Found "%s".\n' % path)
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070067 sys.stderr.write(conflicts)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080068 sys.stderr.write('Please resolve the conflicts and restart the ' +
69 'shell job when done. Kill this job if you ' +
70 'aborted the conflict.\n')
71 os.kill(os.getpid(), signal.SIGTSTP)
72 # TODO: figure out what the state is after the merging, and go based on
73 # that (should we abort? skip? continue?)
74 # Perhaps check last commit message to see if it's the one we were using.
75
Brian Norris9f8a2be2018-06-01 11:14:08 -070076def _get_pw_url(project):
77 """Retrieve the patchwork server URL from .pwclientrc.
78
79 @param project: patchwork project name; if None, we retrieve the default
80 from pwclientrc
81 """
82 config = ConfigParser.ConfigParser()
83 config.read([_PWCLIENTRC])
84
85 if project is None:
86 try:
87 project = config.get('options', 'default')
88 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
89 sys.stderr.write(
90 'Error: no default patchwork project found in %s.\n'
91 % _PWCLIENTRC)
92 sys.exit(1)
93
94 if not config.has_option(project, 'url'):
95 sys.stderr.write('Error: patchwork URL not found for project \'%s\'\n'
96 % project)
97 sys.exit(1)
98
99 url = config.get(project, 'url')
100 return re.sub('(/xmlrpc/)$', '', url)
101
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800102def main(args):
103 """This is the main entrypoint for fromupstream.
104
105 Args:
106 args: sys.argv[1:]
107
108 Returns:
109 An int return code.
110 """
111 parser = argparse.ArgumentParser()
112
113 parser.add_argument('--bug', '-b',
Douglas Andersonb8881842018-05-04 17:02:52 -0700114 type=str, required=True, help='BUG= line')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800115 parser.add_argument('--test', '-t',
Douglas Andersonb8881842018-05-04 17:02:52 -0700116 type=str, required=True, help='TEST= line')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800117 parser.add_argument('--changeid', '-c',
118 help='Overrides the gerrit generated Change-Id line')
119
120 parser.add_argument('--replace',
121 action='store_true',
122 help='Replaces the HEAD commit with this one, taking ' +
123 'its properties(BUG, TEST, Change-Id). Useful for ' +
124 'updating commits.')
125 parser.add_argument('--nosignoff',
126 dest='signoff', action='store_false')
127
128 parser.add_argument('--tag',
129 help='Overrides the tag from the title')
130 parser.add_argument('--source', '-s',
131 dest='source_line', type=str,
132 help='Overrides the source line, last line, ex: ' +
133 '(am from http://....)')
134 parser.add_argument('locations',
Douglas Andersonc77a8b82018-05-04 17:02:03 -0700135 nargs='+',
Brian Norris9f8a2be2018-06-01 11:14:08 -0700136 help='Patchwork ID (pw://### or pw://PROJECT/###, ' +
137 'where PROJECT is defined in ~/.pwclientrc; if no ' +
138 'PROJECT is specified, the default is retrieved from ' +
139 '~/.pwclientrc), ' +
140 'linux commit like linux://HASH, or ' +
Brian Norrisc9aeb2e2018-06-01 10:37:29 -0700141 'git reference like fromgit://remote/branch/HASH')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800142
143 args = vars(parser.parse_args(args))
144
145 if args['replace']:
146 old_commit_message = subprocess.check_output(
147 ['git', 'show', '-s', '--format=%B', 'HEAD']
148 ).strip('\n')
149 args['changeid'] = re.findall('Change-Id: (.*)$',
150 old_commit_message, re.MULTILINE)[0]
151 if args['bug'] == parser.get_default('bug'):
152 args['bug'] = '\nBUG='.join(re.findall('BUG=(.*)$',
153 old_commit_message,
154 re.MULTILINE))
155 if args['test'] == parser.get_default('test'):
156 args['test'] = '\nTEST='.join(re.findall('TEST=(.*)$',
157 old_commit_message,
158 re.MULTILINE))
159 # TODO: deal with multiline BUG/TEST better
160 subprocess.call(['git', 'reset', '--hard', 'HEAD~1'])
161
162 while len(args['locations']) > 0:
163 location = args['locations'].pop(0)
164
165 patchwork_match = re.match(
Brian Norris9f8a2be2018-06-01 11:14:08 -0700166 r'pw://(([-A-z]+)/)?(\d+)', location
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800167 )
168 linux_match = re.match(
169 r'linux://([0-9a-f]+)', location
170 )
171 fromgit_match = re.match(
172 r'fromgit://([^/]+)/(.+)/([0-9a-f]+)$', location
173 )
174
175 if patchwork_match is not None:
Brian Norris9f8a2be2018-06-01 11:14:08 -0700176 pw_project = patchwork_match.group(2)
177 patch_id = int(patchwork_match.group(3))
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800178
179 if args['source_line'] is None:
Brian Norris9f8a2be2018-06-01 11:14:08 -0700180 url = _get_pw_url(pw_project)
181 args['source_line'] = '(am from %s/patch/%d/)' % (url, patch_id)
182
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800183 if args['tag'] is None:
184 args['tag'] = 'FROMLIST: '
185
Brian Norris9f8a2be2018-06-01 11:14:08 -0700186 pw_args = []
187 if pw_project is not None:
188 pw_args += ['-p', pw_project]
189
190 pw_pipe = subprocess.Popen(['pwclient', 'view'] + pw_args +
191 [str(patch_id)], stdout=subprocess.PIPE)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800192 s = pw_pipe.communicate()[0]
193
194 if not s:
195 sys.stderr.write('Error: No patch content found\n')
196 sys.exit(1)
197 git_am = subprocess.Popen(['git', 'am', '-3'], stdin=subprocess.PIPE)
Douglas Anderson8a6e1122018-07-02 16:03:53 -0700198 git_am.communicate(s)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800199 ret = git_am.returncode
200 elif linux_match:
201 commit = linux_match.group(1)
202
203 # Confirm a 'linux' remote is setup.
Guenter Roeckd66daa72018-04-19 10:31:25 -0700204 linux_remote = _find_linux_remote()
205 if not linux_remote:
206 sys.stderr.write('Error: need a valid upstream remote\n')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800207 sys.exit(1)
208
Guenter Roeckd66daa72018-04-19 10:31:25 -0700209 linux_master = '%s/master' % linux_remote
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800210 ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
Guenter Roeckd66daa72018-04-19 10:31:25 -0700211 commit, linux_master])
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800212 if ret:
Guenter Roeckd66daa72018-04-19 10:31:25 -0700213 sys.stderr.write('Error: Commit not in %s\n' % linux_master)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800214 sys.exit(1)
215
216 if args['source_line'] is None:
217 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
218 stdout=subprocess.PIPE)
219 commit = git_pipe.communicate()[0].strip()
220
221 args['source_line'] = ('(cherry picked from commit %s)' %
222 (commit))
223 if args['tag'] is None:
224 args['tag'] = 'UPSTREAM: '
225
226 ret = subprocess.call(['git', 'cherry-pick', commit])
227 elif fromgit_match is not None:
228 remote = fromgit_match.group(1)
229 branch = fromgit_match.group(2)
230 commit = fromgit_match.group(3)
231
232 ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
233 commit, '%s/%s' % (remote, branch)])
234 if ret:
235 sys.stderr.write('Error: Commit not in %s/%s\n' %
236 (remote, branch))
237 sys.exit(1)
238
239 git_pipe = subprocess.Popen(['git', 'remote', 'get-url', remote],
240 stdout=subprocess.PIPE)
241 url = git_pipe.communicate()[0].strip()
242
243 if args['source_line'] is None:
244 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
245 stdout=subprocess.PIPE)
246 commit = git_pipe.communicate()[0].strip()
247
248 args['source_line'] = \
249 '(cherry picked from commit %s\n %s %s)' % \
250 (commit, url, branch)
251 if args['tag'] is None:
252 args['tag'] = 'FROMGIT: '
253
254 ret = subprocess.call(['git', 'cherry-pick', commit])
255 else:
256 sys.stderr.write('Don\'t know what "%s" means.\n' % location)
257 sys.exit(1)
258
259 if ret != 0:
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700260 conflicts = _get_conflicts()
Douglas Anderson2108e532018-04-30 09:50:42 -0700261 if args['tag'] == 'UPSTREAM: ':
262 args['tag'] = 'BACKPORT: '
263 else:
264 args['tag'] = 'BACKPORT: ' + args['tag']
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700265 _pause_for_merge(conflicts)
266 else:
267 conflicts = ""
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800268
269 # extract commit message
270 commit_message = subprocess.check_output(
271 ['git', 'show', '-s', '--format=%B', 'HEAD']
272 ).strip('\n')
273
Guenter Roeck2e4f2512018-04-24 09:20:51 -0700274 # Remove stray Change-Id, most likely from merge resolution
275 commit_message = re.sub(r'Change-Id:.*\n?', '', commit_message)
276
Brian Norris7a41b982018-06-01 10:28:29 -0700277 # Note the source location before tagging anything else
278 commit_message += '\n' + args['source_line']
279
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800280 # add automatic Change ID, BUG, and TEST (and maybe signoff too) so
281 # next commands know where to work on
282 commit_message += '\n'
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700283 commit_message += conflicts
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800284 commit_message += '\n' + 'BUG=' + args['bug']
285 commit_message += '\n' + 'TEST=' + args['test']
286 if args['signoff']:
287 extra = ['-s']
288 else:
289 extra = []
290 commit = subprocess.Popen(
291 ['git', 'commit'] + extra + ['--amend', '-F', '-'],
292 stdin=subprocess.PIPE
293 ).communicate(commit_message)
294
295 # re-extract commit message
296 commit_message = subprocess.check_output(
297 ['git', 'show', '-s', '--format=%B', 'HEAD']
298 ).strip('\n')
299
300 # replace changeid if needed
301 if args['changeid'] is not None:
302 commit_message = re.sub(r'(Change-Id: )(\w+)', r'\1%s' %
303 args['changeid'], commit_message)
304 args['changeid'] = None
305
306 # decorate it that it's from outside
307 commit_message = args['tag'] + commit_message
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800308
309 # commit everything
310 commit = subprocess.Popen(
311 ['git', 'commit', '--amend', '-F', '-'], stdin=subprocess.PIPE
312 ).communicate(commit_message)
313
314 return 0
315
316if __name__ == '__main__':
317 sys.exit(main(sys.argv[1:]))