blob: 06b28b9f61586a12801854badbe66c08f12205cb [file] [log] [blame]
Hirthanan Subenderanb1866552020-03-20 14:01:14 -07001#!/usr/bin/env python3
2# -*- coding: utf-8 -*-"
3#
4# Copyright 2020 The Chromium OS Authors. All rights reserved.
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
8"""Module containing methods interfacing with git
9
10i.e Parsing git logs for change-id, full commit sha's, etc.
11"""
12
13from __future__ import print_function
Guenter Roeck85c10bb2020-04-16 15:36:47 -070014import logging
Hirthanan Subenderanb1866552020-03-20 14:01:14 -070015import re
16import subprocess
17import common
18
19
20def get_upstream_fullsha(abbrev_sha):
21 """Returns the full upstream sha for an abbreviated 12 digit sha using git cli"""
22 upstream_absolute_path = common.get_kernel_absolute_path(common.UPSTREAM_PATH)
23 try:
24 cmd = ['git', '-C', upstream_absolute_path, 'rev-parse', abbrev_sha]
Hirthanan Subenderanaffcc4a2020-03-25 14:42:33 -070025 full_sha = subprocess.check_output(cmd, encoding='utf-8')
Hirthanan Subenderanb1866552020-03-20 14:01:14 -070026 return full_sha.rstrip()
27 except subprocess.CalledProcessError as e:
28 raise type(e)('Could not find full upstream sha for %s' % abbrev_sha, e.cmd) from e
29
30
Guenter Roeck32b23dc2021-01-17 13:29:06 -080031def _get_commit_message(kernel_path, sha):
Hirthanan Subenderand6922c32020-03-23 14:17:40 -070032 """Returns the commit message for a sha in a given local path to kernel."""
Hirthanan Subenderanb1866552020-03-20 14:01:14 -070033 try:
Hirthanan Subenderand6922c32020-03-23 14:17:40 -070034 cmd = ['git', '-C', kernel_path, 'log',
35 '--format=%B', '-n', '1', sha]
Hirthanan Subenderanaffcc4a2020-03-25 14:42:33 -070036 commit_message = subprocess.check_output(cmd, encoding='utf-8', errors='ignore')
Hirthanan Subenderan3450f512020-04-09 22:36:50 -070037
38 # Single newline following commit message
39 return commit_message.rstrip() + '\n'
Hirthanan Subenderanb1866552020-03-20 14:01:14 -070040 except subprocess.CalledProcessError as e:
Hirthanan Subenderand6922c32020-03-23 14:17:40 -070041 raise type(e)('Couldnt retrieve commit in kernel path %s for sha %s'
42 % (kernel_path, sha), e.cmd) from e
43
44
45def get_upstream_commit_message(upstream_sha):
46 """Returns the commit message for a given upstream sha using git cli."""
47 upstream_absolute_path = common.get_kernel_absolute_path(common.UPSTREAM_PATH)
Guenter Roeck32b23dc2021-01-17 13:29:06 -080048 return _get_commit_message(upstream_absolute_path, upstream_sha)
Hirthanan Subenderand6922c32020-03-23 14:17:40 -070049
50
51def get_chrome_commit_message(chrome_sha):
52 """Returns the commit message for a given chrome sha using git cli."""
53 chrome_absolute_path = common.get_kernel_absolute_path(common.CHROMEOS_PATH)
Guenter Roeck32b23dc2021-01-17 13:29:06 -080054 return _get_commit_message(chrome_absolute_path, chrome_sha)
Hirthanan Subenderanb1866552020-03-20 14:01:14 -070055
56
Guenter Roeckbfda1242020-12-29 15:24:18 -080057def get_merge_sha(branch, sha):
58 """Returns SHA of merge commit for provided SHA if available"""
59
60 chrome_absolute_path = common.get_kernel_absolute_path(common.CHROMEOS_PATH)
61
62 try:
63 # Get list of merges in <branch> since <sha>
64 cmd = ['git', '-C', chrome_absolute_path, 'log', '--format=%h', '--abbrev=12',
65 '--ancestry-path', '--merges', '%s..%s' % (sha, branch)]
66 sha_list = subprocess.check_output(cmd, encoding='utf-8', errors='ignore',
67 stderr=subprocess.DEVNULL)
68 if not sha_list:
69 logging.info('No merge commit for sha %s in branch %s', sha, branch)
70 return None
71 # merge_sha is our presumed merge commit
72 merge_sha = sha_list.splitlines()[-1]
73 # Verify if <sha> is indeed part of the merge
74 cmd = ['git', '-C', chrome_absolute_path, 'log', '--format=%h', '--abbrev=12',
75 '%s~1..%s' % (merge_sha, merge_sha)]
76 sha_list = subprocess.check_output(cmd, encoding='utf-8', errors='ignore',
77 stderr=subprocess.DEVNULL)
78 if sha_list and sha in sha_list.splitlines():
79 return merge_sha
80 logging.info('Merge commit for sha %s found as %s, but sha is missing in merge',
81 sha, merge_sha)
82
83 except subprocess.CalledProcessError as e:
84 logging.info('Error "%s" while trying to find merge commit for sha %s in branch %s',
85 e, sha, branch)
86
87 return None
88
89
Hirthanan Subenderanb1866552020-03-20 14:01:14 -070090def get_commit_changeid_linux_chrome(kernel_sha):
Hirthanan Subenderand6922c32020-03-23 14:17:40 -070091 """Returns the changeid of the kernel_sha commit by parsing linux_chrome git log.
92
93 kernel_sha will be one of linux_stable or linux_chrome commits.
94 """
Hirthanan Subenderanb1866552020-03-20 14:01:14 -070095 chrome_absolute_path = common.get_kernel_absolute_path(common.CHROMEOS_PATH)
96 try:
97 cmd = ['git', '-C', chrome_absolute_path, 'log', '--format=%B', '-n', '1', kernel_sha]
Hirthanan Subenderanaffcc4a2020-03-25 14:42:33 -070098 commit_message = subprocess.check_output(cmd, encoding='utf-8', errors='ignore')
Hirthanan Subenderanb1866552020-03-20 14:01:14 -070099
100 m = re.findall('^Change-Id: (I[a-z0-9]{40})$', commit_message, re.M)
101
102 # Get last change-id in case chrome sha cherry-picked/reverted into new commit
103 return m[-1]
104 except subprocess.CalledProcessError as e:
Hirthanan Subenderand6922c32020-03-23 14:17:40 -0700105 raise type(e)('Couldnt retrieve changeid for commit %s' % kernel_sha, e.cmd) from e
Hirthanan Subenderanb1866552020-03-20 14:01:14 -0700106 except IndexError as e:
Hirthanan Subenderand6922c32020-03-23 14:17:40 -0700107 # linux_stable kernel_sha's do not have an associated ChangeID
108 return None
Hirthanan Subenderanb1866552020-03-20 14:01:14 -0700109
110
Guenter Roeck949d05b2020-05-12 12:35:36 -0700111def get_tag_emails_linux_chrome(sha):
112 """Returns unique list of chromium.org or google.com e-mails.
113
114 The returned lust of e-mails is associated with tags found after
115 the last 'cherry picked from commit' message in the commit identified
116 by sha. Tags and e-mails are found by parsing the commit log.
117
118 sha is expected to be be a commit in linux_stable or in linux_chrome.
119 """
120 absolute_path = common.get_kernel_absolute_path(common.CHROMEOS_PATH)
121 try:
122 cmd = ['git', '-C', absolute_path, 'log', '--format=%B', '-n', '1', sha]
123 commit_message = subprocess.check_output(cmd, encoding='utf-8', errors='ignore')
124 # If the commit has been cherry-picked, use subsequent tags to create
125 # list of reviewers. Otherwise, use all tags. Either case, only return
126 # e-mail addresses from Google domains.
127 s = commit_message.split('cherry picked from commit')
128 tags = 'Signed-off-by|Reviewed-by|Tested-by|Commit-Queue'
129 domains = 'chromium.org|google.com'
130 m = '^(?:%s): .* <(.*@(?:%s))>$' % (tags, domains)
131 emails = re.findall(m, s[-1], re.M)
132 if not emails:
133 # Final fallback: In some situations, "cherry picked from"
134 # is at the very end of the commit description, with no
135 # subsequent tags. If that happens, look for tags in the
136 # entire description.
137 emails = re.findall(m, commit_message, re.M)
138 return list(set(emails))
139 except subprocess.CalledProcessError as e:
140 raise type(e)('Could not retrieve tag e-mails for commit %s' % sha, e.cmd) from e
141 except IndexError:
142 # sha does do not have a recognized tag
143 return None
144
145
Guenter Roeckd9b6c6c2020-04-16 10:06:46 -0700146# match "vX.Y[.Z][.rcN]"
147version = re.compile(r'(v[0-9]+(?:\.[0-9]+)+(?:-rc[0-9]+)?)\s*')
148
149def get_integrated_tag(sha):
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800150 """For a given SHA, find the first upstream tag that includes it."""
Guenter Roeckd9b6c6c2020-04-16 10:06:46 -0700151
152 try:
153 path = common.get_kernel_absolute_path(common.UPSTREAM_PATH)
154 cmd = ['git', '-C', path, 'describe', '--match', 'v*',
155 '--contains', sha]
156 tag = subprocess.check_output(cmd, encoding='utf-8',
157 stderr=subprocess.DEVNULL)
158 return version.match(tag).group()
159 except AttributeError:
160 return None
161 except subprocess.CalledProcessError:
162 return None
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800163
164
165class commitHandler:
166 """Class to control active accesses on a git repository"""
167
168 def __init__(self, kernel, branch=None):
169 self.metadata = common.get_kernel_metadata(kernel)
170 if not branch:
171 branch = self.metadata.branches[0]
172 self.branch = branch
173 self.merge_base = self.metadata.tag_template % branch
174 self.branchname = self.metadata.get_kernel_branch(branch)
175 self.path = common.get_kernel_absolute_path(self.metadata.path)
176 self.status = 'unknown'
177 self.commit_list = { } # indexed by merge_base
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800178
Guenter Roeck19a4be82021-01-20 14:01:53 -0800179 current_branch_cmd = ['symbolic-ref', '-q', '--short', 'HEAD']
180 self.current_branch = self.__git_check_output(current_branch_cmd).rstrip()
181
182 def __git_command(self, command):
183 return ['git', '-C', self.path] + command
184
185 def __git_check_output(self, command):
186 cmd = self.__git_command(command)
187 return subprocess.check_output(cmd, encoding='utf-8', errors='ignore')
188
189 def __git_run(self, command):
190 cmd = self.__git_command(command)
191 subprocess.run(cmd, check=True)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800192
193 def __set_branch(self, branch):
194 """Set the active branch"""
195 if branch != self.branch:
196 self.branch = branch
197 self.merge_base = self.metadata.tag_template % branch
198 self.branchname = self.metadata.get_kernel_branch(branch)
199 self.status = 'unknown'
200
Guenter Roeck4a390422021-01-20 12:33:04 -0800201 def __reset_hard_ref(self, reference):
202 """Force reset to provided reference"""
Guenter Roeck19a4be82021-01-20 14:01:53 -0800203 reset_cmd = ['reset', '-q', '--hard', reference]
204 self.__git_run(reset_cmd)
Guenter Roeck4a390422021-01-20 12:33:04 -0800205
206 def __reset_hard_head(self):
207 """Force hard reset to git head in checked out branch"""
208 self.__reset_hard_ref('HEAD')
209
210 def __reset_hard_origin(self):
211 """Force hard reset to head of remote branch"""
212 self.__reset_hard_ref('origin/%s' % self.branchname)
213
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800214 def __checkout_and_clean(self):
215 """Clean up uncommitted files in branch and checkout to be up to date with origin."""
Guenter Roeck19a4be82021-01-20 14:01:53 -0800216 clean_untracked = ['clean', '-d', '-x', '-f', '-q']
217 checkout = ['checkout', '-q', self.branchname]
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800218
Guenter Roeck4a390422021-01-20 12:33:04 -0800219 self.__reset_hard_head()
Guenter Roeck19a4be82021-01-20 14:01:53 -0800220 self.__git_run(clean_untracked)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800221
222 if self.current_branch != self.branchname:
Guenter Roeck19a4be82021-01-20 14:01:53 -0800223 self.__git_run(checkout)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800224 self.current_branch = self.branchname
225
Guenter Roeck4a390422021-01-20 12:33:04 -0800226 self.__reset_hard_origin()
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800227
228 def __setup(self):
229 """Local setup function, to be called for each access"""
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800230 if self.status == 'unknown':
231 self.__checkout_and_clean()
232 elif self.status == 'changed':
Guenter Roeck4a390422021-01-20 12:33:04 -0800233 self.__reset_hard_origin()
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800234
235 self.status = 'clean'
236
237 def __search_subject(self, sha):
238 """Check if subject associated with 'sha' exists in the current branch"""
239
240 try:
241 # Retrieve subject line of provided SHA
Guenter Roeck19a4be82021-01-20 14:01:53 -0800242 cmd = ['log', '--pretty=format:%s', '-n', '1', sha]
243 subject = self.__git_check_output(cmd)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800244 except subprocess.CalledProcessError:
245 logging.error('Failed to get subject for sha %s', sha)
246 return False
247
248 if self.branch not in self.commit_list:
Guenter Roeck19a4be82021-01-20 14:01:53 -0800249 cmd = ['log', '--no-merges', '--format=%s',
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800250 '%s..%s' % (self.merge_base, self.branchname)]
Guenter Roeck19a4be82021-01-20 14:01:53 -0800251 subjects = self.__git_check_output(cmd)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800252 self.commit_list[self.branch] = subjects.splitlines()
253
254 # The following is a raw search which will match, for example, a revert of a commit.
255 # A better method to check if commits have been applied would be desirable.
256 subjects = self.commit_list[self.branch]
257 return any(subject in s for s in subjects)
258
259 def __get_git_push_cmd(self, reviewers):
260 """Generates git push command with added reviewers and autogenerated tag.
261
262 Read more about gerrit tags here:
263 https://gerrit-review.googlesource.com/Documentation/cmd-receive-pack.html
264 """
Guenter Roeck19a4be82021-01-20 14:01:53 -0800265 git_push_head = 'push origin HEAD:refs/for/%s' % self.branchname
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800266 reviewers_tag = ['r=%s'% r for r in reviewers]
267 autogenerated_tag = ['t=autogenerated']
268 tags = ','.join(reviewers_tag + autogenerated_tag)
269 return git_push_head + '%' + tags
270
271 def pull(self, branch=None):
272 """Pull changes from remote repository into provided or default branch"""
273 if branch:
274 self.__set_branch(branch)
275 self.__setup()
Guenter Roeck19a4be82021-01-20 14:01:53 -0800276 pull_cmd = ['pull', '-q']
277 self.__git_run(pull_cmd)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800278
279 def cherry_pick_and_push(self, fixer_upstream_sha, fixer_changeid, fix_commit_message,
280 reviewers):
281 """Cherry picks upstream commit into chrome repo.
282
283 Adds reviewers and autogenerated tag with the pushed commit.
284 """
285
286 self.__setup()
287 try:
288 self.status = 'changed'
Guenter Roeck19a4be82021-01-20 14:01:53 -0800289 self.__git_run(['cherry-pick', '-n', fixer_upstream_sha])
290 self.__git_run(['commit', '-s', '-m', fix_commit_message])
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800291
292 # commit has been cherry-picked and committed locally, precommit hook
293 # in git repository adds changeid to the commit message. Pick it unless
294 # we already have one passed as parameter.
295 if not fixer_changeid:
296 fixer_changeid = get_commit_changeid_linux_chrome('HEAD')
297
298 # Sometimes the commit hook doesn't attach the Change-Id to the last
299 # paragraph in the commit message. This seems to happen if the commit
300 # message includes '---' which would normally identify the start of
301 # comments. If the Change-Id is not in the last paragraph, uploading
302 # the patch is rejected by Gerrit. Force-move the Change-Id to the end
303 # of the commit message to solve the problem. This conveniently also
304 # replaces the auto-generated Change-Id with the optional Change-Id
305 # passed as parameter.
306 commit_message = get_chrome_commit_message('HEAD')
307 commit_message = re.sub(r'Change-Id:.*\n?', '', commit_message)
308 commit_message = commit_message.rstrip()
309 commit_message += '\nChange-Id: %s' % fixer_changeid
Guenter Roeck19a4be82021-01-20 14:01:53 -0800310 self.__git_run(['commit', '--amend', '-m', commit_message])
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800311
312 git_push_cmd = self.__get_git_push_cmd(reviewers)
Guenter Roeck19a4be82021-01-20 14:01:53 -0800313 self.__git_run(git_push_cmd.split(' '))
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800314
315 return fixer_changeid
316 except subprocess.CalledProcessError as e:
317 raise ValueError('Failed to cherrypick and push upstream fix %s on branch %s'
318 % (fixer_upstream_sha, self.branchname)) from e
319 finally:
Guenter Roeck4a390422021-01-20 12:33:04 -0800320 self.__reset_hard_head()
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800321 self.status = 'changed'
322
323 def cherrypick_status(self, sha, branch=None, apply=True):
324 """cherry-pick provided sha into repository and branch identified by this class instance
325
326 Return Status Enum:
327 MERGED if the patch has already been applied,
328 OPEN if the patch is missing and applies cleanly,
329 CONFLICT if the patch is missing and fails to apply.
330 """
331
332 if branch:
333 self.__set_branch(branch)
334
335 self.__setup()
336
337 ret = None
338 try:
339 applied = self.__search_subject(sha)
340 if applied:
341 ret = common.Status.MERGED
342 raise ValueError
343
344 if not apply:
345 raise ValueError
346
Guenter Roeck19a4be82021-01-20 14:01:53 -0800347 result = subprocess.call(self.__git_command(['cherry-pick', '-n', sha]),
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800348 stdout=subprocess.DEVNULL,
349 stderr=subprocess.DEVNULL)
350 if result:
351 ret = common.Status.CONFLICT
352 raise ValueError
353
Guenter Roeck19a4be82021-01-20 14:01:53 -0800354 diff = self.__git_check_output(['diff', 'HEAD'])
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800355 if diff:
356 ret = common.Status.OPEN
357 raise ValueError
358
359 ret = common.Status.MERGED
360
361 except ValueError:
362 pass
363
364 except subprocess.CalledProcessError:
365 ret = common.Status.CONFLICT
366
367 finally:
Guenter Roeck4a390422021-01-20 12:33:04 -0800368 self.__reset_hard_head()
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800369 self.status = 'clean'
370
371 return ret