blob: d3eecb3f2be73657481d168e089e27538e0adc9e [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
18
19LINUX_URLS = (
20 'git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git',
21 'https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git',
22 'https://kernel.googlesource.com/pub/scm/linux/kernel/git/torvalds/linux.git',
23)
24
Brian Norris9f8a2be2018-06-01 11:14:08 -070025_PWCLIENTRC = os.path.expanduser('~/.pwclientrc')
26
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070027def _get_conflicts():
28 """Report conflicting files."""
29 resolutions = ('DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU')
30 conflicts = []
Douglas Anderson46287f92018-04-30 09:58:24 -070031 lines = subprocess.check_output(['git', 'status', '--porcelain',
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070032 '--untracked-files=no']).split('\n')
Douglas Anderson46287f92018-04-30 09:58:24 -070033 for line in lines:
34 if not line:
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070035 continue
Douglas Anderson46287f92018-04-30 09:58:24 -070036 resolution, name = line.split(None, 1)
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070037 if resolution in resolutions:
38 conflicts.append(' ' + name)
39 if not conflicts:
40 return ""
41 return '\nConflicts:\n%s\n' % '\n'.join(conflicts)
42
Guenter Roeckd66daa72018-04-19 10:31:25 -070043def _find_linux_remote():
44 """Find a remote pointing to a Linux upstream repository."""
45 git_remote = subprocess.Popen(['git', 'remote'], stdout=subprocess.PIPE)
46 remotes = git_remote.communicate()[0].strip()
47 for remote in remotes.splitlines():
48 rurl = subprocess.Popen(['git', 'remote', 'get-url', remote],
49 stdout=subprocess.PIPE)
50 url = rurl.communicate()[0].strip()
51 if not rurl.returncode and url in LINUX_URLS:
52 return remote
53 return None
54
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070055def _pause_for_merge(conflicts):
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080056 """Pause and go in the background till user resolves the conflicts."""
57
58 git_root = subprocess.check_output(['git', 'rev-parse',
59 '--show-toplevel']).strip('\n')
60
61 paths = (
62 os.path.join(git_root, '.git', 'rebase-apply'),
63 os.path.join(git_root, '.git', 'CHERRY_PICK_HEAD'),
64 )
65 for path in paths:
66 if os.path.exists(path):
67 sys.stderr.write('Found "%s".\n' % path)
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070068 sys.stderr.write(conflicts)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080069 sys.stderr.write('Please resolve the conflicts and restart the ' +
70 'shell job when done. Kill this job if you ' +
71 'aborted the conflict.\n')
72 os.kill(os.getpid(), signal.SIGTSTP)
73 # TODO: figure out what the state is after the merging, and go based on
74 # that (should we abort? skip? continue?)
75 # Perhaps check last commit message to see if it's the one we were using.
76
Brian Norris9f8a2be2018-06-01 11:14:08 -070077def _get_pw_url(project):
78 """Retrieve the patchwork server URL from .pwclientrc.
79
Mike Frysingerf80ca212018-07-13 15:02:52 -040080 Args:
81 project: patchwork project name; if None, we retrieve the default
82 from pwclientrc
Brian Norris9f8a2be2018-06-01 11:14:08 -070083 """
84 config = ConfigParser.ConfigParser()
85 config.read([_PWCLIENTRC])
86
87 if project is None:
88 try:
89 project = config.get('options', 'default')
90 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
91 sys.stderr.write(
92 'Error: no default patchwork project found in %s.\n'
93 % _PWCLIENTRC)
94 sys.exit(1)
95
96 if not config.has_option(project, 'url'):
97 sys.stderr.write('Error: patchwork URL not found for project \'%s\'\n'
98 % project)
99 sys.exit(1)
100
101 url = config.get(project, 'url')
102 return re.sub('(/xmlrpc/)$', '', url)
103
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800104def main(args):
105 """This is the main entrypoint for fromupstream.
106
107 Args:
108 args: sys.argv[1:]
109
110 Returns:
111 An int return code.
112 """
113 parser = argparse.ArgumentParser()
114
115 parser.add_argument('--bug', '-b',
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700116 type=str, help='BUG= line')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800117 parser.add_argument('--test', '-t',
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700118 type=str, help='TEST= line')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800119 parser.add_argument('--changeid', '-c',
120 help='Overrides the gerrit generated Change-Id line')
121
122 parser.add_argument('--replace',
123 action='store_true',
124 help='Replaces the HEAD commit with this one, taking ' +
125 'its properties(BUG, TEST, Change-Id). Useful for ' +
126 'updating commits.')
127 parser.add_argument('--nosignoff',
128 dest='signoff', action='store_false')
129
130 parser.add_argument('--tag',
131 help='Overrides the tag from the title')
132 parser.add_argument('--source', '-s',
133 dest='source_line', type=str,
134 help='Overrides the source line, last line, ex: ' +
135 '(am from http://....)')
136 parser.add_argument('locations',
Douglas Andersonc77a8b82018-05-04 17:02:03 -0700137 nargs='+',
Brian Norris9f8a2be2018-06-01 11:14:08 -0700138 help='Patchwork ID (pw://### or pw://PROJECT/###, ' +
139 'where PROJECT is defined in ~/.pwclientrc; if no ' +
140 'PROJECT is specified, the default is retrieved from ' +
141 '~/.pwclientrc), ' +
142 'linux commit like linux://HASH, or ' +
Brian Norrisc9aeb2e2018-06-01 10:37:29 -0700143 'git reference like fromgit://remote/branch/HASH')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800144
145 args = vars(parser.parse_args(args))
146
147 if args['replace']:
148 old_commit_message = subprocess.check_output(
149 ['git', 'show', '-s', '--format=%B', 'HEAD']
150 ).strip('\n')
Guenter Roeckf7cebec2018-07-26 16:37:21 -0700151 changeid = re.findall('Change-Id: (.*)$', old_commit_message, re.MULTILINE)
152 if changeid:
153 args['changeid'] = changeid[0]
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700154 if args['bug'] == parser.get_default('bug') and \
155 re.findall('BUG=(.*)$', old_commit_message, re.MULTILINE):
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800156 args['bug'] = '\nBUG='.join(re.findall('BUG=(.*)$',
157 old_commit_message,
158 re.MULTILINE))
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700159 if args['test'] == parser.get_default('test') and \
160 re.findall('TEST=(.*)$', old_commit_message, re.MULTILINE):
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800161 args['test'] = '\nTEST='.join(re.findall('TEST=(.*)$',
162 old_commit_message,
163 re.MULTILINE))
164 # TODO: deal with multiline BUG/TEST better
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800165
Guenter Roeckf47a50c2018-07-25 12:41:36 -0700166 if args['bug'] is None or args['test'] is None:
167 parser.error('BUG=/TEST= lines are required; --replace can help automate, or set via --bug/--test')
168
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800169 while len(args['locations']) > 0:
170 location = args['locations'].pop(0)
171
172 patchwork_match = re.match(
Brian Norris9f8a2be2018-06-01 11:14:08 -0700173 r'pw://(([-A-z]+)/)?(\d+)', location
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800174 )
175 linux_match = re.match(
176 r'linux://([0-9a-f]+)', location
177 )
178 fromgit_match = re.match(
179 r'fromgit://([^/]+)/(.+)/([0-9a-f]+)$', location
180 )
181
182 if patchwork_match is not None:
Brian Norris9f8a2be2018-06-01 11:14:08 -0700183 pw_project = patchwork_match.group(2)
184 patch_id = int(patchwork_match.group(3))
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800185
186 if args['source_line'] is None:
Brian Norris9f8a2be2018-06-01 11:14:08 -0700187 url = _get_pw_url(pw_project)
188 args['source_line'] = '(am from %s/patch/%d/)' % (url, patch_id)
189
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800190 if args['tag'] is None:
191 args['tag'] = 'FROMLIST: '
192
Brian Norris9f8a2be2018-06-01 11:14:08 -0700193 pw_args = []
194 if pw_project is not None:
195 pw_args += ['-p', pw_project]
196
197 pw_pipe = subprocess.Popen(['pwclient', 'view'] + pw_args +
198 [str(patch_id)], stdout=subprocess.PIPE)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800199 s = pw_pipe.communicate()[0]
200
201 if not s:
202 sys.stderr.write('Error: No patch content found\n')
203 sys.exit(1)
Guenter Roeckabe49d82018-07-25 14:11:48 -0700204
205 if args['replace']:
206 subprocess.call(['git', 'reset', '--hard', 'HEAD~1'])
207
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800208 git_am = subprocess.Popen(['git', 'am', '-3'], stdin=subprocess.PIPE)
Douglas Anderson8a6e1122018-07-02 16:03:53 -0700209 git_am.communicate(s)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800210 ret = git_am.returncode
211 elif linux_match:
212 commit = linux_match.group(1)
213
214 # Confirm a 'linux' remote is setup.
Guenter Roeckd66daa72018-04-19 10:31:25 -0700215 linux_remote = _find_linux_remote()
216 if not linux_remote:
217 sys.stderr.write('Error: need a valid upstream remote\n')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800218 sys.exit(1)
219
Guenter Roeckd66daa72018-04-19 10:31:25 -0700220 linux_master = '%s/master' % linux_remote
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800221 ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
Guenter Roeckd66daa72018-04-19 10:31:25 -0700222 commit, linux_master])
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800223 if ret:
Guenter Roeckd66daa72018-04-19 10:31:25 -0700224 sys.stderr.write('Error: Commit not in %s\n' % linux_master)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800225 sys.exit(1)
226
227 if args['source_line'] is None:
228 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
229 stdout=subprocess.PIPE)
230 commit = git_pipe.communicate()[0].strip()
231
232 args['source_line'] = ('(cherry picked from commit %s)' %
233 (commit))
234 if args['tag'] is None:
235 args['tag'] = 'UPSTREAM: '
236
Guenter Roeckabe49d82018-07-25 14:11:48 -0700237 if args['replace']:
238 subprocess.call(['git', 'reset', '--hard', 'HEAD~1'])
239
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800240 ret = subprocess.call(['git', 'cherry-pick', commit])
241 elif fromgit_match is not None:
242 remote = fromgit_match.group(1)
243 branch = fromgit_match.group(2)
244 commit = fromgit_match.group(3)
245
246 ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
247 commit, '%s/%s' % (remote, branch)])
248 if ret:
249 sys.stderr.write('Error: Commit not in %s/%s\n' %
250 (remote, branch))
251 sys.exit(1)
252
253 git_pipe = subprocess.Popen(['git', 'remote', 'get-url', remote],
254 stdout=subprocess.PIPE)
255 url = git_pipe.communicate()[0].strip()
256
257 if args['source_line'] is None:
258 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
259 stdout=subprocess.PIPE)
260 commit = git_pipe.communicate()[0].strip()
261
262 args['source_line'] = \
263 '(cherry picked from commit %s\n %s %s)' % \
264 (commit, url, branch)
265 if args['tag'] is None:
266 args['tag'] = 'FROMGIT: '
267
Guenter Roeckabe49d82018-07-25 14:11:48 -0700268 if args['replace']:
269 subprocess.call(['git', 'reset', '--hard', 'HEAD~1'])
270
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800271 ret = subprocess.call(['git', 'cherry-pick', commit])
272 else:
273 sys.stderr.write('Don\'t know what "%s" means.\n' % location)
274 sys.exit(1)
275
276 if ret != 0:
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700277 conflicts = _get_conflicts()
Douglas Anderson2108e532018-04-30 09:50:42 -0700278 if args['tag'] == 'UPSTREAM: ':
279 args['tag'] = 'BACKPORT: '
280 else:
281 args['tag'] = 'BACKPORT: ' + args['tag']
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700282 _pause_for_merge(conflicts)
283 else:
284 conflicts = ""
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800285
286 # extract commit message
287 commit_message = subprocess.check_output(
288 ['git', 'show', '-s', '--format=%B', 'HEAD']
289 ).strip('\n')
290
Guenter Roeck2e4f2512018-04-24 09:20:51 -0700291 # Remove stray Change-Id, most likely from merge resolution
292 commit_message = re.sub(r'Change-Id:.*\n?', '', commit_message)
293
Brian Norris7a41b982018-06-01 10:28:29 -0700294 # Note the source location before tagging anything else
295 commit_message += '\n' + args['source_line']
296
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800297 # add automatic Change ID, BUG, and TEST (and maybe signoff too) so
298 # next commands know where to work on
299 commit_message += '\n'
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700300 commit_message += conflicts
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800301 commit_message += '\n' + 'BUG=' + args['bug']
302 commit_message += '\n' + 'TEST=' + args['test']
303 if args['signoff']:
304 extra = ['-s']
305 else:
306 extra = []
307 commit = subprocess.Popen(
308 ['git', 'commit'] + extra + ['--amend', '-F', '-'],
309 stdin=subprocess.PIPE
310 ).communicate(commit_message)
311
312 # re-extract commit message
313 commit_message = subprocess.check_output(
314 ['git', 'show', '-s', '--format=%B', 'HEAD']
315 ).strip('\n')
316
317 # replace changeid if needed
318 if args['changeid'] is not None:
319 commit_message = re.sub(r'(Change-Id: )(\w+)', r'\1%s' %
320 args['changeid'], commit_message)
321 args['changeid'] = None
322
323 # decorate it that it's from outside
324 commit_message = args['tag'] + commit_message
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800325
326 # commit everything
327 commit = subprocess.Popen(
328 ['git', 'commit', '--amend', '-F', '-'], stdin=subprocess.PIPE
329 ).communicate(commit_message)
330
331 return 0
332
333if __name__ == '__main__':
334 sys.exit(main(sys.argv[1:]))