blob: 684ab02d2d567f5729e2e14d972982433a8820b6 [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
10import argparse
11import os
12import re
13import signal
14import subprocess
15import sys
16
17LINUX_URLS = (
18 'git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git',
19 'https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git',
20 'https://kernel.googlesource.com/pub/scm/linux/kernel/git/torvalds/linux.git',
21)
22
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070023def _get_conflicts():
24 """Report conflicting files."""
25 resolutions = ('DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU')
26 conflicts = []
Douglas Anderson46287f92018-04-30 09:58:24 -070027 lines = subprocess.check_output(['git', 'status', '--porcelain',
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070028 '--untracked-files=no']).split('\n')
Douglas Anderson46287f92018-04-30 09:58:24 -070029 for line in lines:
30 if not line:
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070031 continue
Douglas Anderson46287f92018-04-30 09:58:24 -070032 resolution, name = line.split(None, 1)
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070033 if resolution in resolutions:
34 conflicts.append(' ' + name)
35 if not conflicts:
36 return ""
37 return '\nConflicts:\n%s\n' % '\n'.join(conflicts)
38
Guenter Roeckd66daa72018-04-19 10:31:25 -070039def _find_linux_remote():
40 """Find a remote pointing to a Linux upstream repository."""
41 git_remote = subprocess.Popen(['git', 'remote'], stdout=subprocess.PIPE)
42 remotes = git_remote.communicate()[0].strip()
43 for remote in remotes.splitlines():
44 rurl = subprocess.Popen(['git', 'remote', 'get-url', remote],
45 stdout=subprocess.PIPE)
46 url = rurl.communicate()[0].strip()
47 if not rurl.returncode and url in LINUX_URLS:
48 return remote
49 return None
50
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070051def _pause_for_merge(conflicts):
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080052 """Pause and go in the background till user resolves the conflicts."""
53
54 git_root = subprocess.check_output(['git', 'rev-parse',
55 '--show-toplevel']).strip('\n')
56
57 paths = (
58 os.path.join(git_root, '.git', 'rebase-apply'),
59 os.path.join(git_root, '.git', 'CHERRY_PICK_HEAD'),
60 )
61 for path in paths:
62 if os.path.exists(path):
63 sys.stderr.write('Found "%s".\n' % path)
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070064 sys.stderr.write(conflicts)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080065 sys.stderr.write('Please resolve the conflicts and restart the ' +
66 'shell job when done. Kill this job if you ' +
67 'aborted the conflict.\n')
68 os.kill(os.getpid(), signal.SIGTSTP)
69 # TODO: figure out what the state is after the merging, and go based on
70 # that (should we abort? skip? continue?)
71 # Perhaps check last commit message to see if it's the one we were using.
72
73def main(args):
74 """This is the main entrypoint for fromupstream.
75
76 Args:
77 args: sys.argv[1:]
78
79 Returns:
80 An int return code.
81 """
82 parser = argparse.ArgumentParser()
83
84 parser.add_argument('--bug', '-b',
Douglas Andersonb8881842018-05-04 17:02:52 -070085 type=str, required=True, help='BUG= line')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080086 parser.add_argument('--test', '-t',
Douglas Andersonb8881842018-05-04 17:02:52 -070087 type=str, required=True, help='TEST= line')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080088 parser.add_argument('--changeid', '-c',
89 help='Overrides the gerrit generated Change-Id line')
90
91 parser.add_argument('--replace',
92 action='store_true',
93 help='Replaces the HEAD commit with this one, taking ' +
94 'its properties(BUG, TEST, Change-Id). Useful for ' +
95 'updating commits.')
96 parser.add_argument('--nosignoff',
97 dest='signoff', action='store_false')
98
99 parser.add_argument('--tag',
100 help='Overrides the tag from the title')
101 parser.add_argument('--source', '-s',
102 dest='source_line', type=str,
103 help='Overrides the source line, last line, ex: ' +
104 '(am from http://....)')
105 parser.add_argument('locations',
Douglas Andersonc77a8b82018-05-04 17:02:03 -0700106 nargs='+',
Brian Norrisc9aeb2e2018-06-01 10:37:29 -0700107 help='Patchwork ID (pw://###), ' +
108 'linux commit like linux://HASH, ' +
109 'git reference like fromgit://remote/branch/HASH')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800110
111 args = vars(parser.parse_args(args))
112
113 if args['replace']:
114 old_commit_message = subprocess.check_output(
115 ['git', 'show', '-s', '--format=%B', 'HEAD']
116 ).strip('\n')
117 args['changeid'] = re.findall('Change-Id: (.*)$',
118 old_commit_message, re.MULTILINE)[0]
119 if args['bug'] == parser.get_default('bug'):
120 args['bug'] = '\nBUG='.join(re.findall('BUG=(.*)$',
121 old_commit_message,
122 re.MULTILINE))
123 if args['test'] == parser.get_default('test'):
124 args['test'] = '\nTEST='.join(re.findall('TEST=(.*)$',
125 old_commit_message,
126 re.MULTILINE))
127 # TODO: deal with multiline BUG/TEST better
128 subprocess.call(['git', 'reset', '--hard', 'HEAD~1'])
129
130 while len(args['locations']) > 0:
131 location = args['locations'].pop(0)
132
133 patchwork_match = re.match(
Brian Norrisc9aeb2e2018-06-01 10:37:29 -0700134 r'pw://(\d+)', location
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800135 )
136 linux_match = re.match(
137 r'linux://([0-9a-f]+)', location
138 )
139 fromgit_match = re.match(
140 r'fromgit://([^/]+)/(.+)/([0-9a-f]+)$', location
141 )
142
143 if patchwork_match is not None:
Brian Norrisc9aeb2e2018-06-01 10:37:29 -0700144 patch_id = int(patchwork_match.group(1))
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800145
146 if args['source_line'] is None:
147 args['source_line'] = \
148 '(am from https://patchwork.kernel.org/patch/%d/)' % \
149 patch_id
150 if args['tag'] is None:
151 args['tag'] = 'FROMLIST: '
152
153 pw_pipe = subprocess.Popen(['pwclient', 'view', str(patch_id)],
154 stdout=subprocess.PIPE)
155 s = pw_pipe.communicate()[0]
156
157 if not s:
158 sys.stderr.write('Error: No patch content found\n')
159 sys.exit(1)
160 git_am = subprocess.Popen(['git', 'am', '-3'], stdin=subprocess.PIPE)
161 git_am.communicate(unicode(s).encode('utf-8'))
162 ret = git_am.returncode
163 elif linux_match:
164 commit = linux_match.group(1)
165
166 # Confirm a 'linux' remote is setup.
Guenter Roeckd66daa72018-04-19 10:31:25 -0700167 linux_remote = _find_linux_remote()
168 if not linux_remote:
169 sys.stderr.write('Error: need a valid upstream remote\n')
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800170 sys.exit(1)
171
Guenter Roeckd66daa72018-04-19 10:31:25 -0700172 linux_master = '%s/master' % linux_remote
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800173 ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
Guenter Roeckd66daa72018-04-19 10:31:25 -0700174 commit, linux_master])
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800175 if ret:
Guenter Roeckd66daa72018-04-19 10:31:25 -0700176 sys.stderr.write('Error: Commit not in %s\n' % linux_master)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800177 sys.exit(1)
178
179 if args['source_line'] is None:
180 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
181 stdout=subprocess.PIPE)
182 commit = git_pipe.communicate()[0].strip()
183
184 args['source_line'] = ('(cherry picked from commit %s)' %
185 (commit))
186 if args['tag'] is None:
187 args['tag'] = 'UPSTREAM: '
188
189 ret = subprocess.call(['git', 'cherry-pick', commit])
190 elif fromgit_match is not None:
191 remote = fromgit_match.group(1)
192 branch = fromgit_match.group(2)
193 commit = fromgit_match.group(3)
194
195 ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
196 commit, '%s/%s' % (remote, branch)])
197 if ret:
198 sys.stderr.write('Error: Commit not in %s/%s\n' %
199 (remote, branch))
200 sys.exit(1)
201
202 git_pipe = subprocess.Popen(['git', 'remote', 'get-url', remote],
203 stdout=subprocess.PIPE)
204 url = git_pipe.communicate()[0].strip()
205
206 if args['source_line'] is None:
207 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
208 stdout=subprocess.PIPE)
209 commit = git_pipe.communicate()[0].strip()
210
211 args['source_line'] = \
212 '(cherry picked from commit %s\n %s %s)' % \
213 (commit, url, branch)
214 if args['tag'] is None:
215 args['tag'] = 'FROMGIT: '
216
217 ret = subprocess.call(['git', 'cherry-pick', commit])
218 else:
219 sys.stderr.write('Don\'t know what "%s" means.\n' % location)
220 sys.exit(1)
221
222 if ret != 0:
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700223 conflicts = _get_conflicts()
Douglas Anderson2108e532018-04-30 09:50:42 -0700224 if args['tag'] == 'UPSTREAM: ':
225 args['tag'] = 'BACKPORT: '
226 else:
227 args['tag'] = 'BACKPORT: ' + args['tag']
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700228 _pause_for_merge(conflicts)
229 else:
230 conflicts = ""
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800231
232 # extract commit message
233 commit_message = subprocess.check_output(
234 ['git', 'show', '-s', '--format=%B', 'HEAD']
235 ).strip('\n')
236
Guenter Roeck2e4f2512018-04-24 09:20:51 -0700237 # Remove stray Change-Id, most likely from merge resolution
238 commit_message = re.sub(r'Change-Id:.*\n?', '', commit_message)
239
Brian Norris7a41b982018-06-01 10:28:29 -0700240 # Note the source location before tagging anything else
241 commit_message += '\n' + args['source_line']
242
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800243 # add automatic Change ID, BUG, and TEST (and maybe signoff too) so
244 # next commands know where to work on
245 commit_message += '\n'
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700246 commit_message += conflicts
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800247 commit_message += '\n' + 'BUG=' + args['bug']
248 commit_message += '\n' + 'TEST=' + args['test']
249 if args['signoff']:
250 extra = ['-s']
251 else:
252 extra = []
253 commit = subprocess.Popen(
254 ['git', 'commit'] + extra + ['--amend', '-F', '-'],
255 stdin=subprocess.PIPE
256 ).communicate(commit_message)
257
258 # re-extract commit message
259 commit_message = subprocess.check_output(
260 ['git', 'show', '-s', '--format=%B', 'HEAD']
261 ).strip('\n')
262
263 # replace changeid if needed
264 if args['changeid'] is not None:
265 commit_message = re.sub(r'(Change-Id: )(\w+)', r'\1%s' %
266 args['changeid'], commit_message)
267 args['changeid'] = None
268
269 # decorate it that it's from outside
270 commit_message = args['tag'] + commit_message
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800271
272 # commit everything
273 commit = subprocess.Popen(
274 ['git', 'commit', '--amend', '-F', '-'], stdin=subprocess.PIPE
275 ).communicate(commit_message)
276
277 return 0
278
279if __name__ == '__main__':
280 sys.exit(main(sys.argv[1:]))