blob: e7b6ed730f1c80d2fa3615afa0867dd75d4f766c [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
Guenter Roeckabea3d22021-01-21 07:14:46 -080020def _git_check_output(path, command):
21 git_cmd = ['git', '-C', path] + command
22 return subprocess.check_output(git_cmd, encoding='utf-8', errors='ignore',
23 stderr=subprocess.DEVNULL)
24
25
Hirthanan Subenderanb1866552020-03-20 14:01:14 -070026def get_upstream_fullsha(abbrev_sha):
27 """Returns the full upstream sha for an abbreviated 12 digit sha using git cli"""
28 upstream_absolute_path = common.get_kernel_absolute_path(common.UPSTREAM_PATH)
29 try:
Guenter Roeckabea3d22021-01-21 07:14:46 -080030 cmd = ['rev-parse', abbrev_sha]
31 full_sha = _git_check_output(upstream_absolute_path, cmd)
Hirthanan Subenderanb1866552020-03-20 14:01:14 -070032 return full_sha.rstrip()
33 except subprocess.CalledProcessError as e:
34 raise type(e)('Could not find full upstream sha for %s' % abbrev_sha, e.cmd) from e
35
36
Guenter Roeck32b23dc2021-01-17 13:29:06 -080037def _get_commit_message(kernel_path, sha):
Hirthanan Subenderand6922c32020-03-23 14:17:40 -070038 """Returns the commit message for a sha in a given local path to kernel."""
Hirthanan Subenderanb1866552020-03-20 14:01:14 -070039 try:
Guenter Roeckabea3d22021-01-21 07:14:46 -080040 cmd = ['log', '--format=%B', '-n', '1', sha]
41 commit_message = _git_check_output(kernel_path, cmd)
Hirthanan Subenderan3450f512020-04-09 22:36:50 -070042
43 # Single newline following commit message
44 return commit_message.rstrip() + '\n'
Hirthanan Subenderanb1866552020-03-20 14:01:14 -070045 except subprocess.CalledProcessError as e:
Hirthanan Subenderand6922c32020-03-23 14:17:40 -070046 raise type(e)('Couldnt retrieve commit in kernel path %s for sha %s'
47 % (kernel_path, sha), e.cmd) from e
48
49
50def get_upstream_commit_message(upstream_sha):
51 """Returns the commit message for a given upstream sha using git cli."""
52 upstream_absolute_path = common.get_kernel_absolute_path(common.UPSTREAM_PATH)
Guenter Roeck32b23dc2021-01-17 13:29:06 -080053 return _get_commit_message(upstream_absolute_path, upstream_sha)
Hirthanan Subenderand6922c32020-03-23 14:17:40 -070054
55
56def get_chrome_commit_message(chrome_sha):
57 """Returns the commit message for a given chrome sha using git cli."""
58 chrome_absolute_path = common.get_kernel_absolute_path(common.CHROMEOS_PATH)
Guenter Roeck32b23dc2021-01-17 13:29:06 -080059 return _get_commit_message(chrome_absolute_path, chrome_sha)
Hirthanan Subenderanb1866552020-03-20 14:01:14 -070060
61
Guenter Roeckbfda1242020-12-29 15:24:18 -080062def get_merge_sha(branch, sha):
63 """Returns SHA of merge commit for provided SHA if available"""
64
65 chrome_absolute_path = common.get_kernel_absolute_path(common.CHROMEOS_PATH)
66
67 try:
68 # Get list of merges in <branch> since <sha>
Guenter Roeckabea3d22021-01-21 07:14:46 -080069 cmd = ['log', '--format=%h', '--abbrev=12', '--ancestry-path', '--merges',
70 '%s..%s' % (sha, branch)]
71 sha_list = _git_check_output(chrome_absolute_path, cmd)
Guenter Roeckbfda1242020-12-29 15:24:18 -080072 if not sha_list:
73 logging.info('No merge commit for sha %s in branch %s', sha, branch)
74 return None
75 # merge_sha is our presumed merge commit
76 merge_sha = sha_list.splitlines()[-1]
77 # Verify if <sha> is indeed part of the merge
Guenter Roeckabea3d22021-01-21 07:14:46 -080078 cmd = ['log', '--format=%h', '--abbrev=12', '%s~1..%s' % (merge_sha, merge_sha)]
79 sha_list = _git_check_output(chrome_absolute_path, cmd)
Guenter Roeckbfda1242020-12-29 15:24:18 -080080 if sha_list and sha in sha_list.splitlines():
81 return merge_sha
82 logging.info('Merge commit for sha %s found as %s, but sha is missing in merge',
83 sha, merge_sha)
84
85 except subprocess.CalledProcessError as e:
86 logging.info('Error "%s" while trying to find merge commit for sha %s in branch %s',
87 e, sha, branch)
88
89 return None
90
91
Hirthanan Subenderanb1866552020-03-20 14:01:14 -070092def get_commit_changeid_linux_chrome(kernel_sha):
Hirthanan Subenderand6922c32020-03-23 14:17:40 -070093 """Returns the changeid of the kernel_sha commit by parsing linux_chrome git log.
94
95 kernel_sha will be one of linux_stable or linux_chrome commits.
96 """
Hirthanan Subenderanb1866552020-03-20 14:01:14 -070097 chrome_absolute_path = common.get_kernel_absolute_path(common.CHROMEOS_PATH)
98 try:
Guenter Roeckabea3d22021-01-21 07:14:46 -080099 cmd = ['log', '--format=%B', '-n', '1', kernel_sha]
100 commit_message = _git_check_output(chrome_absolute_path, cmd)
Hirthanan Subenderanb1866552020-03-20 14:01:14 -0700101
102 m = re.findall('^Change-Id: (I[a-z0-9]{40})$', commit_message, re.M)
103
104 # Get last change-id in case chrome sha cherry-picked/reverted into new commit
105 return m[-1]
106 except subprocess.CalledProcessError as e:
Hirthanan Subenderand6922c32020-03-23 14:17:40 -0700107 raise type(e)('Couldnt retrieve changeid for commit %s' % kernel_sha, e.cmd) from e
Hirthanan Subenderanb1866552020-03-20 14:01:14 -0700108 except IndexError as e:
Hirthanan Subenderand6922c32020-03-23 14:17:40 -0700109 # linux_stable kernel_sha's do not have an associated ChangeID
110 return None
Hirthanan Subenderanb1866552020-03-20 14:01:14 -0700111
112
Guenter Roeck949d05b2020-05-12 12:35:36 -0700113def get_tag_emails_linux_chrome(sha):
114 """Returns unique list of chromium.org or google.com e-mails.
115
116 The returned lust of e-mails is associated with tags found after
117 the last 'cherry picked from commit' message in the commit identified
118 by sha. Tags and e-mails are found by parsing the commit log.
119
120 sha is expected to be be a commit in linux_stable or in linux_chrome.
121 """
122 absolute_path = common.get_kernel_absolute_path(common.CHROMEOS_PATH)
123 try:
Guenter Roeckabea3d22021-01-21 07:14:46 -0800124 cmd = ['log', '--format=%B', '-n', '1', sha]
125 commit_message = _git_check_output(absolute_path, cmd)
Guenter Roeck949d05b2020-05-12 12:35:36 -0700126 # If the commit has been cherry-picked, use subsequent tags to create
127 # list of reviewers. Otherwise, use all tags. Either case, only return
128 # e-mail addresses from Google domains.
129 s = commit_message.split('cherry picked from commit')
130 tags = 'Signed-off-by|Reviewed-by|Tested-by|Commit-Queue'
131 domains = 'chromium.org|google.com'
132 m = '^(?:%s): .* <(.*@(?:%s))>$' % (tags, domains)
133 emails = re.findall(m, s[-1], re.M)
134 if not emails:
135 # Final fallback: In some situations, "cherry picked from"
136 # is at the very end of the commit description, with no
137 # subsequent tags. If that happens, look for tags in the
138 # entire description.
139 emails = re.findall(m, commit_message, re.M)
140 return list(set(emails))
141 except subprocess.CalledProcessError as e:
142 raise type(e)('Could not retrieve tag e-mails for commit %s' % sha, e.cmd) from e
143 except IndexError:
144 # sha does do not have a recognized tag
145 return None
146
147
Guenter Roeckd9b6c6c2020-04-16 10:06:46 -0700148# match "vX.Y[.Z][.rcN]"
149version = re.compile(r'(v[0-9]+(?:\.[0-9]+)+(?:-rc[0-9]+)?)\s*')
150
151def get_integrated_tag(sha):
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800152 """For a given SHA, find the first upstream tag that includes it."""
Guenter Roeckd9b6c6c2020-04-16 10:06:46 -0700153
154 try:
155 path = common.get_kernel_absolute_path(common.UPSTREAM_PATH)
Guenter Roeckabea3d22021-01-21 07:14:46 -0800156 cmd = ['describe', '--match', 'v*', '--contains', sha]
157 tag = _git_check_output(path, cmd)
Guenter Roeckd9b6c6c2020-04-16 10:06:46 -0700158 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 Roeck32b23dc2021-01-17 13:29:06 -0800265 reviewers_tag = ['r=%s'% r for r in reviewers]
266 autogenerated_tag = ['t=autogenerated']
267 tags = ','.join(reviewers_tag + autogenerated_tag)
Guenter Roeck61e294f2021-01-20 14:44:46 -0800268 head = 'HEAD:refs/for/%s%%%s' % (self.branchname, tags)
269 return ['push', 'origin', head]
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800270
Guenter Roeck76b864c2021-01-20 15:24:16 -0800271 def fetch(self, remote=None):
272 """Fetch changes from provided remote or from origin"""
273 if not remote:
274 remote = 'origin'
275 self.__setup()
276 fetch_cmd = ['fetch', '-q', remote]
277 self.__git_run(fetch_cmd)
278 self.status = 'changed'
279
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800280 def pull(self, branch=None):
281 """Pull changes from remote repository into provided or default branch"""
282 if branch:
283 self.__set_branch(branch)
284 self.__setup()
Guenter Roeck19a4be82021-01-20 14:01:53 -0800285 pull_cmd = ['pull', '-q']
286 self.__git_run(pull_cmd)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800287
288 def cherry_pick_and_push(self, fixer_upstream_sha, fixer_changeid, fix_commit_message,
289 reviewers):
290 """Cherry picks upstream commit into chrome repo.
291
292 Adds reviewers and autogenerated tag with the pushed commit.
293 """
294
295 self.__setup()
296 try:
297 self.status = 'changed'
Guenter Roeck19a4be82021-01-20 14:01:53 -0800298 self.__git_run(['cherry-pick', '-n', fixer_upstream_sha])
299 self.__git_run(['commit', '-s', '-m', fix_commit_message])
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800300
301 # commit has been cherry-picked and committed locally, precommit hook
302 # in git repository adds changeid to the commit message. Pick it unless
303 # we already have one passed as parameter.
304 if not fixer_changeid:
305 fixer_changeid = get_commit_changeid_linux_chrome('HEAD')
306
307 # Sometimes the commit hook doesn't attach the Change-Id to the last
308 # paragraph in the commit message. This seems to happen if the commit
309 # message includes '---' which would normally identify the start of
310 # comments. If the Change-Id is not in the last paragraph, uploading
311 # the patch is rejected by Gerrit. Force-move the Change-Id to the end
312 # of the commit message to solve the problem. This conveniently also
313 # replaces the auto-generated Change-Id with the optional Change-Id
314 # passed as parameter.
315 commit_message = get_chrome_commit_message('HEAD')
316 commit_message = re.sub(r'Change-Id:.*\n?', '', commit_message)
317 commit_message = commit_message.rstrip()
318 commit_message += '\nChange-Id: %s' % fixer_changeid
Guenter Roeck19a4be82021-01-20 14:01:53 -0800319 self.__git_run(['commit', '--amend', '-m', commit_message])
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800320
321 git_push_cmd = self.__get_git_push_cmd(reviewers)
Guenter Roeck61e294f2021-01-20 14:44:46 -0800322 self.__git_run(git_push_cmd)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800323
324 return fixer_changeid
325 except subprocess.CalledProcessError as e:
326 raise ValueError('Failed to cherrypick and push upstream fix %s on branch %s'
327 % (fixer_upstream_sha, self.branchname)) from e
328 finally:
Guenter Roeck4a390422021-01-20 12:33:04 -0800329 self.__reset_hard_head()
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800330 self.status = 'changed'
331
332 def cherrypick_status(self, sha, branch=None, apply=True):
333 """cherry-pick provided sha into repository and branch identified by this class instance
334
335 Return Status Enum:
336 MERGED if the patch has already been applied,
337 OPEN if the patch is missing and applies cleanly,
338 CONFLICT if the patch is missing and fails to apply.
339 """
340
341 if branch:
342 self.__set_branch(branch)
343
344 self.__setup()
345
346 ret = None
347 try:
348 applied = self.__search_subject(sha)
349 if applied:
350 ret = common.Status.MERGED
351 raise ValueError
352
353 if not apply:
354 raise ValueError
355
Guenter Roeck19a4be82021-01-20 14:01:53 -0800356 result = subprocess.call(self.__git_command(['cherry-pick', '-n', sha]),
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800357 stdout=subprocess.DEVNULL,
358 stderr=subprocess.DEVNULL)
359 if result:
360 ret = common.Status.CONFLICT
361 raise ValueError
362
Guenter Roeck19a4be82021-01-20 14:01:53 -0800363 diff = self.__git_check_output(['diff', 'HEAD'])
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800364 if diff:
365 ret = common.Status.OPEN
366 raise ValueError
367
368 ret = common.Status.MERGED
369
370 except ValueError:
371 pass
372
373 except subprocess.CalledProcessError:
374 ret = common.Status.CONFLICT
375
376 finally:
Guenter Roeck4a390422021-01-20 12:33:04 -0800377 self.__reset_hard_head()
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800378 self.status = 'clean'
379
380 return ret