blob: 0d739d3e5b09a68e83de26ce8ab0e0a67ae04a39 [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 = []
27 files = subprocess.check_output(['git', 'status', '--porcelain',
28 '--untracked-files=no']).split('\n')
29 for file in files:
30 if not file:
31 continue
32 resolution, name = file.split(None, 1)
33 if resolution in resolutions:
34 conflicts.append(' ' + name)
35 if not conflicts:
36 return ""
37 return '\nConflicts:\n%s\n' % '\n'.join(conflicts)
38
39def _pause_for_merge(conflicts):
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080040 """Pause and go in the background till user resolves the conflicts."""
41
42 git_root = subprocess.check_output(['git', 'rev-parse',
43 '--show-toplevel']).strip('\n')
44
45 paths = (
46 os.path.join(git_root, '.git', 'rebase-apply'),
47 os.path.join(git_root, '.git', 'CHERRY_PICK_HEAD'),
48 )
49 for path in paths:
50 if os.path.exists(path):
51 sys.stderr.write('Found "%s".\n' % path)
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -070052 sys.stderr.write(conflicts)
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -080053 sys.stderr.write('Please resolve the conflicts and restart the ' +
54 'shell job when done. Kill this job if you ' +
55 'aborted the conflict.\n')
56 os.kill(os.getpid(), signal.SIGTSTP)
57 # TODO: figure out what the state is after the merging, and go based on
58 # that (should we abort? skip? continue?)
59 # Perhaps check last commit message to see if it's the one we were using.
60
61def main(args):
62 """This is the main entrypoint for fromupstream.
63
64 Args:
65 args: sys.argv[1:]
66
67 Returns:
68 An int return code.
69 """
70 parser = argparse.ArgumentParser()
71
72 parser.add_argument('--bug', '-b',
73 type=str, default='None', help='BUG= line')
74 parser.add_argument('--test', '-t',
75 type=str, default='Build and boot', help='TEST= line')
76 parser.add_argument('--changeid', '-c',
77 help='Overrides the gerrit generated Change-Id line')
78
79 parser.add_argument('--replace',
80 action='store_true',
81 help='Replaces the HEAD commit with this one, taking ' +
82 'its properties(BUG, TEST, Change-Id). Useful for ' +
83 'updating commits.')
84 parser.add_argument('--nosignoff',
85 dest='signoff', action='store_false')
86
87 parser.add_argument('--tag',
88 help='Overrides the tag from the title')
89 parser.add_argument('--source', '-s',
90 dest='source_line', type=str,
91 help='Overrides the source line, last line, ex: ' +
92 '(am from http://....)')
93 parser.add_argument('locations',
94 nargs='*',
95 help='Patchwork url (either ' +
96 'https://patchwork.kernel.org/patch/###/ or ' +
97 'pw://###), linux commit like linux://HASH, git ' +
98 'refrerence like fromgit://remote/branch/HASH')
99
100 args = vars(parser.parse_args(args))
101
102 if args['replace']:
103 old_commit_message = subprocess.check_output(
104 ['git', 'show', '-s', '--format=%B', 'HEAD']
105 ).strip('\n')
106 args['changeid'] = re.findall('Change-Id: (.*)$',
107 old_commit_message, re.MULTILINE)[0]
108 if args['bug'] == parser.get_default('bug'):
109 args['bug'] = '\nBUG='.join(re.findall('BUG=(.*)$',
110 old_commit_message,
111 re.MULTILINE))
112 if args['test'] == parser.get_default('test'):
113 args['test'] = '\nTEST='.join(re.findall('TEST=(.*)$',
114 old_commit_message,
115 re.MULTILINE))
116 # TODO: deal with multiline BUG/TEST better
117 subprocess.call(['git', 'reset', '--hard', 'HEAD~1'])
118
119 while len(args['locations']) > 0:
120 location = args['locations'].pop(0)
121
122 patchwork_match = re.match(
123 r'((pw://)|(https?://patchwork.kernel.org/patch/))(\d+)/?', location
124 )
125 linux_match = re.match(
126 r'linux://([0-9a-f]+)', location
127 )
128 fromgit_match = re.match(
129 r'fromgit://([^/]+)/(.+)/([0-9a-f]+)$', location
130 )
131
132 if patchwork_match is not None:
133 patch_id = int(patchwork_match.group(4))
134
135 if args['source_line'] is None:
136 args['source_line'] = \
137 '(am from https://patchwork.kernel.org/patch/%d/)' % \
138 patch_id
139 if args['tag'] is None:
140 args['tag'] = 'FROMLIST: '
141
142 pw_pipe = subprocess.Popen(['pwclient', 'view', str(patch_id)],
143 stdout=subprocess.PIPE)
144 s = pw_pipe.communicate()[0]
145
146 if not s:
147 sys.stderr.write('Error: No patch content found\n')
148 sys.exit(1)
149 git_am = subprocess.Popen(['git', 'am', '-3'], stdin=subprocess.PIPE)
150 git_am.communicate(unicode(s).encode('utf-8'))
151 ret = git_am.returncode
152 elif linux_match:
153 commit = linux_match.group(1)
154
155 # Confirm a 'linux' remote is setup.
156 git_pipe = subprocess.Popen(['git', 'remote', 'get-url', 'linux'],
157 stdout=subprocess.PIPE)
158 url = git_pipe.communicate()[0].strip()
159 if url not in LINUX_URLS:
160 sys.stderr.write('Error: need a "linux" remote w/ valid URL\n')
161 sys.exit(1)
162
163 ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
164 commit, 'linux/master'])
165 if ret:
166 sys.stderr.write('Error: Commit not in linux/master\n')
167 sys.exit(1)
168
169 if args['source_line'] is None:
170 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
171 stdout=subprocess.PIPE)
172 commit = git_pipe.communicate()[0].strip()
173
174 args['source_line'] = ('(cherry picked from commit %s)' %
175 (commit))
176 if args['tag'] is None:
177 args['tag'] = 'UPSTREAM: '
178
179 ret = subprocess.call(['git', 'cherry-pick', commit])
180 elif fromgit_match is not None:
181 remote = fromgit_match.group(1)
182 branch = fromgit_match.group(2)
183 commit = fromgit_match.group(3)
184
185 ret = subprocess.call(['git', 'merge-base', '--is-ancestor',
186 commit, '%s/%s' % (remote, branch)])
187 if ret:
188 sys.stderr.write('Error: Commit not in %s/%s\n' %
189 (remote, branch))
190 sys.exit(1)
191
192 git_pipe = subprocess.Popen(['git', 'remote', 'get-url', remote],
193 stdout=subprocess.PIPE)
194 url = git_pipe.communicate()[0].strip()
195
196 if args['source_line'] is None:
197 git_pipe = subprocess.Popen(['git', 'rev-parse', commit],
198 stdout=subprocess.PIPE)
199 commit = git_pipe.communicate()[0].strip()
200
201 args['source_line'] = \
202 '(cherry picked from commit %s\n %s %s)' % \
203 (commit, url, branch)
204 if args['tag'] is None:
205 args['tag'] = 'FROMGIT: '
206
207 ret = subprocess.call(['git', 'cherry-pick', commit])
208 else:
209 sys.stderr.write('Don\'t know what "%s" means.\n' % location)
210 sys.exit(1)
211
212 if ret != 0:
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700213 conflicts = _get_conflicts()
214 args['tag'] = 'BACKPORT: '
215 _pause_for_merge(conflicts)
216 else:
217 conflicts = ""
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800218
219 # extract commit message
220 commit_message = subprocess.check_output(
221 ['git', 'show', '-s', '--format=%B', 'HEAD']
222 ).strip('\n')
223
224 # add automatic Change ID, BUG, and TEST (and maybe signoff too) so
225 # next commands know where to work on
226 commit_message += '\n'
Guenter Roeckbdbb9cc2018-04-19 10:05:08 -0700227 commit_message += conflicts
Alexandru M Stanfb5b5ee2014-12-04 13:32:55 -0800228 commit_message += '\n' + 'BUG=' + args['bug']
229 commit_message += '\n' + 'TEST=' + args['test']
230 if args['signoff']:
231 extra = ['-s']
232 else:
233 extra = []
234 commit = subprocess.Popen(
235 ['git', 'commit'] + extra + ['--amend', '-F', '-'],
236 stdin=subprocess.PIPE
237 ).communicate(commit_message)
238
239 # re-extract commit message
240 commit_message = subprocess.check_output(
241 ['git', 'show', '-s', '--format=%B', 'HEAD']
242 ).strip('\n')
243
244 # replace changeid if needed
245 if args['changeid'] is not None:
246 commit_message = re.sub(r'(Change-Id: )(\w+)', r'\1%s' %
247 args['changeid'], commit_message)
248 args['changeid'] = None
249
250 # decorate it that it's from outside
251 commit_message = args['tag'] + commit_message
252 commit_message += '\n' + args['source_line']
253
254 # commit everything
255 commit = subprocess.Popen(
256 ['git', 'commit', '--amend', '-F', '-'], stdin=subprocess.PIPE
257 ).communicate(commit_message)
258
259 return 0
260
261if __name__ == '__main__':
262 sys.exit(main(sys.argv[1:]))