blob: 3f1c1f09a5b83542cd5cf2dbed0787175a170606 [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
Guenter Roecke3f32ae2021-01-26 19:03:45 -0800168 commit_list = { }
169
Guenter Roeck20686c22021-01-25 09:39:10 -0800170 def __init__(self, kernel, branch=None, full_reset=True):
Guenter Roecke3f32ae2021-01-26 19:03:45 -0800171 self.kernel = kernel
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800172 self.metadata = common.get_kernel_metadata(kernel)
173 if not branch:
174 branch = self.metadata.branches[0]
175 self.branch = branch
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800176 self.branchname = self.metadata.get_kernel_branch(branch)
177 self.path = common.get_kernel_absolute_path(self.metadata.path)
178 self.status = 'unknown'
Guenter Roeck20686c22021-01-25 09:39:10 -0800179 self.full_reset = full_reset
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800180
Guenter Roecke3f32ae2021-01-26 19:03:45 -0800181 if kernel not in self.commit_list:
182 self.commit_list[kernel] = { }
183
Guenter Roeck19a4be82021-01-20 14:01:53 -0800184 current_branch_cmd = ['symbolic-ref', '-q', '--short', 'HEAD']
185 self.current_branch = self.__git_check_output(current_branch_cmd).rstrip()
186
Guenter Roeck95e88932021-01-25 11:28:54 -0800187 def __base_tag(self):
188 """Return base tag for selected branch
189
190 The base tag is derived from the tag template in metadata or,
191 if the tag template is empty, from the most recent tag in the
192 selected branch.
193 """
194 if self.metadata.tag_template:
195 return self.metadata.tag_template % self.branch
196 # Pick base tag from most recent tag in branch (self.branchname)
197 cmd = ['describe', '--abbrev=0', self.branchname]
198 return self.__git_check_output(cmd).rstrip()
199
Guenter Roeck19a4be82021-01-20 14:01:53 -0800200 def __git_command(self, command):
201 return ['git', '-C', self.path] + command
202
203 def __git_check_output(self, command):
204 cmd = self.__git_command(command)
205 return subprocess.check_output(cmd, encoding='utf-8', errors='ignore')
206
207 def __git_run(self, command):
208 cmd = self.__git_command(command)
209 subprocess.run(cmd, check=True)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800210
Guenter Roeck4a390422021-01-20 12:33:04 -0800211 def __reset_hard_ref(self, reference):
212 """Force reset to provided reference"""
Guenter Roeck19a4be82021-01-20 14:01:53 -0800213 reset_cmd = ['reset', '-q', '--hard', reference]
214 self.__git_run(reset_cmd)
Guenter Roeck4a390422021-01-20 12:33:04 -0800215
216 def __reset_hard_head(self):
217 """Force hard reset to git head in checked out branch"""
218 self.__reset_hard_ref('HEAD')
219
220 def __reset_hard_origin(self):
221 """Force hard reset to head of remote branch"""
222 self.__reset_hard_ref('origin/%s' % self.branchname)
223
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800224 def __checkout_and_clean(self):
225 """Clean up uncommitted files in branch and checkout to be up to date with origin."""
Guenter Roeck19a4be82021-01-20 14:01:53 -0800226 clean_untracked = ['clean', '-d', '-x', '-f', '-q']
227 checkout = ['checkout', '-q', self.branchname]
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800228
Guenter Roeck4a390422021-01-20 12:33:04 -0800229 self.__reset_hard_head()
Guenter Roeck19a4be82021-01-20 14:01:53 -0800230 self.__git_run(clean_untracked)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800231
232 if self.current_branch != self.branchname:
Guenter Roeck19a4be82021-01-20 14:01:53 -0800233 self.__git_run(checkout)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800234 self.current_branch = self.branchname
235
Guenter Roeck20686c22021-01-25 09:39:10 -0800236 if self.full_reset:
237 self.__reset_hard_origin()
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800238
Guenter Roeck012a69f2021-01-22 10:25:30 -0800239 def __setup(self, branch=None):
240 """Local setup function, to be called for each access.
241
242 Also sets the active branch if provided.
243 """
244 if branch and branch != self.branch:
245 self.branch = branch
Guenter Roeck012a69f2021-01-22 10:25:30 -0800246 self.branchname = self.metadata.get_kernel_branch(branch)
247 self.status = 'unknown'
248
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800249 if self.status == 'unknown':
250 self.__checkout_and_clean()
251 elif self.status == 'changed':
Guenter Roeck4a390422021-01-20 12:33:04 -0800252 self.__reset_hard_origin()
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800253
254 self.status = 'clean'
255
256 def __search_subject(self, sha):
257 """Check if subject associated with 'sha' exists in the current branch"""
258
259 try:
260 # Retrieve subject line of provided SHA
Guenter Roeck19a4be82021-01-20 14:01:53 -0800261 cmd = ['log', '--pretty=format:%s', '-n', '1', sha]
262 subject = self.__git_check_output(cmd)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800263 except subprocess.CalledProcessError:
264 logging.error('Failed to get subject for sha %s', sha)
265 return False
266
Guenter Roecke3f32ae2021-01-26 19:03:45 -0800267 commit_list = self.commit_list[self.kernel]
268 if self.branch not in commit_list:
Guenter Roeck19a4be82021-01-20 14:01:53 -0800269 cmd = ['log', '--no-merges', '--format=%s',
Guenter Roeck95e88932021-01-25 11:28:54 -0800270 '%s..%s' % (self.__base_tag(), self.branchname)]
Guenter Roeck19a4be82021-01-20 14:01:53 -0800271 subjects = self.__git_check_output(cmd)
Guenter Roecke3f32ae2021-01-26 19:03:45 -0800272 commit_list[self.branch] = subjects.splitlines()
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800273
274 # The following is a raw search which will match, for example, a revert of a commit.
275 # A better method to check if commits have been applied would be desirable.
Guenter Roecke3f32ae2021-01-26 19:03:45 -0800276 subjects = commit_list[self.branch]
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800277 return any(subject in s for s in subjects)
278
279 def __get_git_push_cmd(self, reviewers):
280 """Generates git push command with added reviewers and autogenerated tag.
281
282 Read more about gerrit tags here:
283 https://gerrit-review.googlesource.com/Documentation/cmd-receive-pack.html
284 """
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800285 reviewers_tag = ['r=%s'% r for r in reviewers]
286 autogenerated_tag = ['t=autogenerated']
287 tags = ','.join(reviewers_tag + autogenerated_tag)
Guenter Roeck61e294f2021-01-20 14:44:46 -0800288 head = 'HEAD:refs/for/%s%%%s' % (self.branchname, tags)
289 return ['push', 'origin', head]
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800290
Guenter Roeck76b864c2021-01-20 15:24:16 -0800291 def fetch(self, remote=None):
292 """Fetch changes from provided remote or from origin"""
293 if not remote:
294 remote = 'origin'
295 self.__setup()
296 fetch_cmd = ['fetch', '-q', remote]
297 self.__git_run(fetch_cmd)
298 self.status = 'changed'
299
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800300 def pull(self, branch=None):
301 """Pull changes from remote repository into provided or default branch"""
Guenter Roeck012a69f2021-01-22 10:25:30 -0800302 self.__setup(branch)
Guenter Roeck19a4be82021-01-20 14:01:53 -0800303 pull_cmd = ['pull', '-q']
304 self.__git_run(pull_cmd)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800305
306 def cherry_pick_and_push(self, fixer_upstream_sha, fixer_changeid, fix_commit_message,
307 reviewers):
308 """Cherry picks upstream commit into chrome repo.
309
310 Adds reviewers and autogenerated tag with the pushed commit.
311 """
312
313 self.__setup()
314 try:
315 self.status = 'changed'
Guenter Roeck19a4be82021-01-20 14:01:53 -0800316 self.__git_run(['cherry-pick', '-n', fixer_upstream_sha])
317 self.__git_run(['commit', '-s', '-m', fix_commit_message])
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800318
319 # commit has been cherry-picked and committed locally, precommit hook
320 # in git repository adds changeid to the commit message. Pick it unless
321 # we already have one passed as parameter.
322 if not fixer_changeid:
323 fixer_changeid = get_commit_changeid_linux_chrome('HEAD')
324
325 # Sometimes the commit hook doesn't attach the Change-Id to the last
326 # paragraph in the commit message. This seems to happen if the commit
327 # message includes '---' which would normally identify the start of
328 # comments. If the Change-Id is not in the last paragraph, uploading
329 # the patch is rejected by Gerrit. Force-move the Change-Id to the end
330 # of the commit message to solve the problem. This conveniently also
331 # replaces the auto-generated Change-Id with the optional Change-Id
332 # passed as parameter.
333 commit_message = get_chrome_commit_message('HEAD')
334 commit_message = re.sub(r'Change-Id:.*\n?', '', commit_message)
335 commit_message = commit_message.rstrip()
336 commit_message += '\nChange-Id: %s' % fixer_changeid
Guenter Roeck19a4be82021-01-20 14:01:53 -0800337 self.__git_run(['commit', '--amend', '-m', commit_message])
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800338
339 git_push_cmd = self.__get_git_push_cmd(reviewers)
Guenter Roeck61e294f2021-01-20 14:44:46 -0800340 self.__git_run(git_push_cmd)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800341
342 return fixer_changeid
343 except subprocess.CalledProcessError as e:
344 raise ValueError('Failed to cherrypick and push upstream fix %s on branch %s'
345 % (fixer_upstream_sha, self.branchname)) from e
346 finally:
Guenter Roeck4a390422021-01-20 12:33:04 -0800347 self.__reset_hard_head()
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800348 self.status = 'changed'
349
350 def cherrypick_status(self, sha, branch=None, apply=True):
351 """cherry-pick provided sha into repository and branch identified by this class instance
352
353 Return Status Enum:
354 MERGED if the patch has already been applied,
355 OPEN if the patch is missing and applies cleanly,
356 CONFLICT if the patch is missing and fails to apply.
357 """
358
Guenter Roeck012a69f2021-01-22 10:25:30 -0800359 self.__setup(branch)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800360
361 ret = None
362 try:
363 applied = self.__search_subject(sha)
364 if applied:
365 ret = common.Status.MERGED
366 raise ValueError
367
368 if not apply:
369 raise ValueError
370
Guenter Roeck19a4be82021-01-20 14:01:53 -0800371 result = subprocess.call(self.__git_command(['cherry-pick', '-n', sha]),
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800372 stdout=subprocess.DEVNULL,
373 stderr=subprocess.DEVNULL)
374 if result:
375 ret = common.Status.CONFLICT
376 raise ValueError
377
Guenter Roeck19a4be82021-01-20 14:01:53 -0800378 diff = self.__git_check_output(['diff', 'HEAD'])
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800379 if diff:
380 ret = common.Status.OPEN
381 raise ValueError
382
383 ret = common.Status.MERGED
384
385 except ValueError:
386 pass
387
388 except subprocess.CalledProcessError:
389 ret = common.Status.CONFLICT
390
391 finally:
Guenter Roeck4a390422021-01-20 12:33:04 -0800392 self.__reset_hard_head()
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800393 self.status = 'clean'
394
395 return ret