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