blob: 5ddd113ce5ae2a7cff4311f9b95059d9f6ac9de2 [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
Guenter Roeck4a390422021-01-20 12:33:04 -0800193 def __reset_hard_ref(self, reference):
194 """Force reset to provided reference"""
Guenter Roeck19a4be82021-01-20 14:01:53 -0800195 reset_cmd = ['reset', '-q', '--hard', reference]
196 self.__git_run(reset_cmd)
Guenter Roeck4a390422021-01-20 12:33:04 -0800197
198 def __reset_hard_head(self):
199 """Force hard reset to git head in checked out branch"""
200 self.__reset_hard_ref('HEAD')
201
202 def __reset_hard_origin(self):
203 """Force hard reset to head of remote branch"""
204 self.__reset_hard_ref('origin/%s' % self.branchname)
205
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800206 def __checkout_and_clean(self):
207 """Clean up uncommitted files in branch and checkout to be up to date with origin."""
Guenter Roeck19a4be82021-01-20 14:01:53 -0800208 clean_untracked = ['clean', '-d', '-x', '-f', '-q']
209 checkout = ['checkout', '-q', self.branchname]
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800210
Guenter Roeck4a390422021-01-20 12:33:04 -0800211 self.__reset_hard_head()
Guenter Roeck19a4be82021-01-20 14:01:53 -0800212 self.__git_run(clean_untracked)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800213
214 if self.current_branch != self.branchname:
Guenter Roeck19a4be82021-01-20 14:01:53 -0800215 self.__git_run(checkout)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800216 self.current_branch = self.branchname
217
Guenter Roeck4a390422021-01-20 12:33:04 -0800218 self.__reset_hard_origin()
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800219
Guenter Roeck012a69f2021-01-22 10:25:30 -0800220 def __setup(self, branch=None):
221 """Local setup function, to be called for each access.
222
223 Also sets the active branch if provided.
224 """
225 if branch and branch != self.branch:
226 self.branch = branch
227 self.merge_base = self.metadata.tag_template % branch
228 self.branchname = self.metadata.get_kernel_branch(branch)
229 self.status = 'unknown'
230
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800231 if self.status == 'unknown':
232 self.__checkout_and_clean()
233 elif self.status == 'changed':
Guenter Roeck4a390422021-01-20 12:33:04 -0800234 self.__reset_hard_origin()
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800235
236 self.status = 'clean'
237
238 def __search_subject(self, sha):
239 """Check if subject associated with 'sha' exists in the current branch"""
240
241 try:
242 # Retrieve subject line of provided SHA
Guenter Roeck19a4be82021-01-20 14:01:53 -0800243 cmd = ['log', '--pretty=format:%s', '-n', '1', sha]
244 subject = self.__git_check_output(cmd)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800245 except subprocess.CalledProcessError:
246 logging.error('Failed to get subject for sha %s', sha)
247 return False
248
249 if self.branch not in self.commit_list:
Guenter Roeck19a4be82021-01-20 14:01:53 -0800250 cmd = ['log', '--no-merges', '--format=%s',
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800251 '%s..%s' % (self.merge_base, self.branchname)]
Guenter Roeck19a4be82021-01-20 14:01:53 -0800252 subjects = self.__git_check_output(cmd)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800253 self.commit_list[self.branch] = subjects.splitlines()
254
255 # The following is a raw search which will match, for example, a revert of a commit.
256 # A better method to check if commits have been applied would be desirable.
257 subjects = self.commit_list[self.branch]
258 return any(subject in s for s in subjects)
259
260 def __get_git_push_cmd(self, reviewers):
261 """Generates git push command with added reviewers and autogenerated tag.
262
263 Read more about gerrit tags here:
264 https://gerrit-review.googlesource.com/Documentation/cmd-receive-pack.html
265 """
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)
Guenter Roeck61e294f2021-01-20 14:44:46 -0800269 head = 'HEAD:refs/for/%s%%%s' % (self.branchname, tags)
270 return ['push', 'origin', head]
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800271
Guenter Roeck76b864c2021-01-20 15:24:16 -0800272 def fetch(self, remote=None):
273 """Fetch changes from provided remote or from origin"""
274 if not remote:
275 remote = 'origin'
276 self.__setup()
277 fetch_cmd = ['fetch', '-q', remote]
278 self.__git_run(fetch_cmd)
279 self.status = 'changed'
280
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800281 def pull(self, branch=None):
282 """Pull changes from remote repository into provided or default branch"""
Guenter Roeck012a69f2021-01-22 10:25:30 -0800283 self.__setup(branch)
Guenter Roeck19a4be82021-01-20 14:01:53 -0800284 pull_cmd = ['pull', '-q']
285 self.__git_run(pull_cmd)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800286
287 def cherry_pick_and_push(self, fixer_upstream_sha, fixer_changeid, fix_commit_message,
288 reviewers):
289 """Cherry picks upstream commit into chrome repo.
290
291 Adds reviewers and autogenerated tag with the pushed commit.
292 """
293
294 self.__setup()
295 try:
296 self.status = 'changed'
Guenter Roeck19a4be82021-01-20 14:01:53 -0800297 self.__git_run(['cherry-pick', '-n', fixer_upstream_sha])
298 self.__git_run(['commit', '-s', '-m', fix_commit_message])
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800299
300 # commit has been cherry-picked and committed locally, precommit hook
301 # in git repository adds changeid to the commit message. Pick it unless
302 # we already have one passed as parameter.
303 if not fixer_changeid:
304 fixer_changeid = get_commit_changeid_linux_chrome('HEAD')
305
306 # Sometimes the commit hook doesn't attach the Change-Id to the last
307 # paragraph in the commit message. This seems to happen if the commit
308 # message includes '---' which would normally identify the start of
309 # comments. If the Change-Id is not in the last paragraph, uploading
310 # the patch is rejected by Gerrit. Force-move the Change-Id to the end
311 # of the commit message to solve the problem. This conveniently also
312 # replaces the auto-generated Change-Id with the optional Change-Id
313 # passed as parameter.
314 commit_message = get_chrome_commit_message('HEAD')
315 commit_message = re.sub(r'Change-Id:.*\n?', '', commit_message)
316 commit_message = commit_message.rstrip()
317 commit_message += '\nChange-Id: %s' % fixer_changeid
Guenter Roeck19a4be82021-01-20 14:01:53 -0800318 self.__git_run(['commit', '--amend', '-m', commit_message])
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800319
320 git_push_cmd = self.__get_git_push_cmd(reviewers)
Guenter Roeck61e294f2021-01-20 14:44:46 -0800321 self.__git_run(git_push_cmd)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800322
323 return fixer_changeid
324 except subprocess.CalledProcessError as e:
325 raise ValueError('Failed to cherrypick and push upstream fix %s on branch %s'
326 % (fixer_upstream_sha, self.branchname)) from e
327 finally:
Guenter Roeck4a390422021-01-20 12:33:04 -0800328 self.__reset_hard_head()
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800329 self.status = 'changed'
330
331 def cherrypick_status(self, sha, branch=None, apply=True):
332 """cherry-pick provided sha into repository and branch identified by this class instance
333
334 Return Status Enum:
335 MERGED if the patch has already been applied,
336 OPEN if the patch is missing and applies cleanly,
337 CONFLICT if the patch is missing and fails to apply.
338 """
339
Guenter Roeck012a69f2021-01-22 10:25:30 -0800340 self.__setup(branch)
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800341
342 ret = None
343 try:
344 applied = self.__search_subject(sha)
345 if applied:
346 ret = common.Status.MERGED
347 raise ValueError
348
349 if not apply:
350 raise ValueError
351
Guenter Roeck19a4be82021-01-20 14:01:53 -0800352 result = subprocess.call(self.__git_command(['cherry-pick', '-n', sha]),
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800353 stdout=subprocess.DEVNULL,
354 stderr=subprocess.DEVNULL)
355 if result:
356 ret = common.Status.CONFLICT
357 raise ValueError
358
Guenter Roeck19a4be82021-01-20 14:01:53 -0800359 diff = self.__git_check_output(['diff', 'HEAD'])
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800360 if diff:
361 ret = common.Status.OPEN
362 raise ValueError
363
364 ret = common.Status.MERGED
365
366 except ValueError:
367 pass
368
369 except subprocess.CalledProcessError:
370 ret = common.Status.CONFLICT
371
372 finally:
Guenter Roeck4a390422021-01-20 12:33:04 -0800373 self.__reset_hard_head()
Guenter Roeck32b23dc2021-01-17 13:29:06 -0800374 self.status = 'clean'
375
376 return ret