Hirthanan Subenderan | b186655 | 2020-03-20 14:01:14 -0700 | [diff] [blame] | 1 | #!/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 | |
| 10 | i.e Parsing git logs for change-id, full commit sha's, etc. |
| 11 | """ |
| 12 | |
| 13 | from __future__ import print_function |
Guenter Roeck | 85c10bb | 2020-04-16 15:36:47 -0700 | [diff] [blame] | 14 | import logging |
Hirthanan Subenderan | b186655 | 2020-03-20 14:01:14 -0700 | [diff] [blame] | 15 | import os |
| 16 | import re |
| 17 | import subprocess |
| 18 | import common |
| 19 | |
| 20 | |
Hirthanan Subenderan | a90d8f8 | 2020-04-17 15:47:05 -0700 | [diff] [blame] | 21 | def checkout_and_clean(kernel_path, branch): |
| 22 | """Cleanup uncommitted files in branch and checkout to be up to date with origin.""" |
| 23 | reset_head = ['git', '-C', kernel_path, 'reset', '-q', '--hard', 'HEAD'] |
| 24 | clean_untracked = ['git', '-C', kernel_path, 'clean', '-d', '-x', '-f', '-q'] |
| 25 | checkout = ['git', '-C', kernel_path, 'checkout', '-q', branch] |
| 26 | reset_origin = ['git', '-C', kernel_path, 'reset', '-q', '--hard', 'origin/%s' % branch] |
| 27 | subprocess.run(reset_head, check=True) |
| 28 | subprocess.run(clean_untracked, check=True) |
| 29 | subprocess.run(checkout, check=True) |
| 30 | subprocess.run(reset_origin, check=True) |
Hirthanan Subenderan | 7a81bed | 2020-04-16 17:44:31 -0700 | [diff] [blame] | 31 | |
| 32 | |
Hirthanan Subenderan | b186655 | 2020-03-20 14:01:14 -0700 | [diff] [blame] | 33 | def get_upstream_fullsha(abbrev_sha): |
| 34 | """Returns the full upstream sha for an abbreviated 12 digit sha using git cli""" |
| 35 | upstream_absolute_path = common.get_kernel_absolute_path(common.UPSTREAM_PATH) |
| 36 | try: |
| 37 | cmd = ['git', '-C', upstream_absolute_path, 'rev-parse', abbrev_sha] |
Hirthanan Subenderan | affcc4a | 2020-03-25 14:42:33 -0700 | [diff] [blame] | 38 | full_sha = subprocess.check_output(cmd, encoding='utf-8') |
Hirthanan Subenderan | b186655 | 2020-03-20 14:01:14 -0700 | [diff] [blame] | 39 | return full_sha.rstrip() |
| 40 | except subprocess.CalledProcessError as e: |
| 41 | raise type(e)('Could not find full upstream sha for %s' % abbrev_sha, e.cmd) from e |
| 42 | |
| 43 | |
Hirthanan Subenderan | d6922c3 | 2020-03-23 14:17:40 -0700 | [diff] [blame] | 44 | def get_commit_message(kernel_path, sha): |
| 45 | """Returns the commit message for a sha in a given local path to kernel.""" |
Hirthanan Subenderan | b186655 | 2020-03-20 14:01:14 -0700 | [diff] [blame] | 46 | try: |
Hirthanan Subenderan | d6922c3 | 2020-03-23 14:17:40 -0700 | [diff] [blame] | 47 | cmd = ['git', '-C', kernel_path, 'log', |
| 48 | '--format=%B', '-n', '1', sha] |
Hirthanan Subenderan | affcc4a | 2020-03-25 14:42:33 -0700 | [diff] [blame] | 49 | commit_message = subprocess.check_output(cmd, encoding='utf-8', errors='ignore') |
Hirthanan Subenderan | 3450f51 | 2020-04-09 22:36:50 -0700 | [diff] [blame] | 50 | |
| 51 | # Single newline following commit message |
| 52 | return commit_message.rstrip() + '\n' |
Hirthanan Subenderan | b186655 | 2020-03-20 14:01:14 -0700 | [diff] [blame] | 53 | except subprocess.CalledProcessError as e: |
Hirthanan Subenderan | d6922c3 | 2020-03-23 14:17:40 -0700 | [diff] [blame] | 54 | raise type(e)('Couldnt retrieve commit in kernel path %s for sha %s' |
| 55 | % (kernel_path, sha), e.cmd) from e |
| 56 | |
| 57 | |
| 58 | def get_upstream_commit_message(upstream_sha): |
| 59 | """Returns the commit message for a given upstream sha using git cli.""" |
| 60 | upstream_absolute_path = common.get_kernel_absolute_path(common.UPSTREAM_PATH) |
| 61 | return get_commit_message(upstream_absolute_path, upstream_sha) |
| 62 | |
| 63 | |
| 64 | def get_chrome_commit_message(chrome_sha): |
| 65 | """Returns the commit message for a given chrome sha using git cli.""" |
| 66 | chrome_absolute_path = common.get_kernel_absolute_path(common.CHROMEOS_PATH) |
| 67 | return get_commit_message(chrome_absolute_path, chrome_sha) |
Hirthanan Subenderan | b186655 | 2020-03-20 14:01:14 -0700 | [diff] [blame] | 68 | |
| 69 | |
Guenter Roeck | bfda124 | 2020-12-29 15:24:18 -0800 | [diff] [blame^] | 70 | def get_merge_sha(branch, sha): |
| 71 | """Returns SHA of merge commit for provided SHA if available""" |
| 72 | |
| 73 | chrome_absolute_path = common.get_kernel_absolute_path(common.CHROMEOS_PATH) |
| 74 | |
| 75 | try: |
| 76 | # Get list of merges in <branch> since <sha> |
| 77 | cmd = ['git', '-C', chrome_absolute_path, 'log', '--format=%h', '--abbrev=12', |
| 78 | '--ancestry-path', '--merges', '%s..%s' % (sha, branch)] |
| 79 | sha_list = subprocess.check_output(cmd, encoding='utf-8', errors='ignore', |
| 80 | stderr=subprocess.DEVNULL) |
| 81 | if not sha_list: |
| 82 | logging.info('No merge commit for sha %s in branch %s', sha, branch) |
| 83 | return None |
| 84 | # merge_sha is our presumed merge commit |
| 85 | merge_sha = sha_list.splitlines()[-1] |
| 86 | # Verify if <sha> is indeed part of the merge |
| 87 | cmd = ['git', '-C', chrome_absolute_path, 'log', '--format=%h', '--abbrev=12', |
| 88 | '%s~1..%s' % (merge_sha, merge_sha)] |
| 89 | sha_list = subprocess.check_output(cmd, encoding='utf-8', errors='ignore', |
| 90 | stderr=subprocess.DEVNULL) |
| 91 | if sha_list and sha in sha_list.splitlines(): |
| 92 | return merge_sha |
| 93 | logging.info('Merge commit for sha %s found as %s, but sha is missing in merge', |
| 94 | sha, merge_sha) |
| 95 | |
| 96 | except subprocess.CalledProcessError as e: |
| 97 | logging.info('Error "%s" while trying to find merge commit for sha %s in branch %s', |
| 98 | e, sha, branch) |
| 99 | |
| 100 | return None |
| 101 | |
| 102 | |
Hirthanan Subenderan | b186655 | 2020-03-20 14:01:14 -0700 | [diff] [blame] | 103 | def get_commit_changeid_linux_chrome(kernel_sha): |
Hirthanan Subenderan | d6922c3 | 2020-03-23 14:17:40 -0700 | [diff] [blame] | 104 | """Returns the changeid of the kernel_sha commit by parsing linux_chrome git log. |
| 105 | |
| 106 | kernel_sha will be one of linux_stable or linux_chrome commits. |
| 107 | """ |
Hirthanan Subenderan | b186655 | 2020-03-20 14:01:14 -0700 | [diff] [blame] | 108 | chrome_absolute_path = common.get_kernel_absolute_path(common.CHROMEOS_PATH) |
| 109 | try: |
| 110 | cmd = ['git', '-C', chrome_absolute_path, 'log', '--format=%B', '-n', '1', kernel_sha] |
Hirthanan Subenderan | affcc4a | 2020-03-25 14:42:33 -0700 | [diff] [blame] | 111 | commit_message = subprocess.check_output(cmd, encoding='utf-8', errors='ignore') |
Hirthanan Subenderan | b186655 | 2020-03-20 14:01:14 -0700 | [diff] [blame] | 112 | |
| 113 | m = re.findall('^Change-Id: (I[a-z0-9]{40})$', commit_message, re.M) |
| 114 | |
| 115 | # Get last change-id in case chrome sha cherry-picked/reverted into new commit |
| 116 | return m[-1] |
| 117 | except subprocess.CalledProcessError as e: |
Hirthanan Subenderan | d6922c3 | 2020-03-23 14:17:40 -0700 | [diff] [blame] | 118 | raise type(e)('Couldnt retrieve changeid for commit %s' % kernel_sha, e.cmd) from e |
Hirthanan Subenderan | b186655 | 2020-03-20 14:01:14 -0700 | [diff] [blame] | 119 | except IndexError as e: |
Hirthanan Subenderan | d6922c3 | 2020-03-23 14:17:40 -0700 | [diff] [blame] | 120 | # linux_stable kernel_sha's do not have an associated ChangeID |
| 121 | return None |
Hirthanan Subenderan | b186655 | 2020-03-20 14:01:14 -0700 | [diff] [blame] | 122 | |
| 123 | |
Guenter Roeck | 949d05b | 2020-05-12 12:35:36 -0700 | [diff] [blame] | 124 | def get_tag_emails_linux_chrome(sha): |
| 125 | """Returns unique list of chromium.org or google.com e-mails. |
| 126 | |
| 127 | The returned lust of e-mails is associated with tags found after |
| 128 | the last 'cherry picked from commit' message in the commit identified |
| 129 | by sha. Tags and e-mails are found by parsing the commit log. |
| 130 | |
| 131 | sha is expected to be be a commit in linux_stable or in linux_chrome. |
| 132 | """ |
| 133 | absolute_path = common.get_kernel_absolute_path(common.CHROMEOS_PATH) |
| 134 | try: |
| 135 | cmd = ['git', '-C', absolute_path, 'log', '--format=%B', '-n', '1', sha] |
| 136 | commit_message = subprocess.check_output(cmd, encoding='utf-8', errors='ignore') |
| 137 | # If the commit has been cherry-picked, use subsequent tags to create |
| 138 | # list of reviewers. Otherwise, use all tags. Either case, only return |
| 139 | # e-mail addresses from Google domains. |
| 140 | s = commit_message.split('cherry picked from commit') |
| 141 | tags = 'Signed-off-by|Reviewed-by|Tested-by|Commit-Queue' |
| 142 | domains = 'chromium.org|google.com' |
| 143 | m = '^(?:%s): .* <(.*@(?:%s))>$' % (tags, domains) |
| 144 | emails = re.findall(m, s[-1], re.M) |
| 145 | if not emails: |
| 146 | # Final fallback: In some situations, "cherry picked from" |
| 147 | # is at the very end of the commit description, with no |
| 148 | # subsequent tags. If that happens, look for tags in the |
| 149 | # entire description. |
| 150 | emails = re.findall(m, commit_message, re.M) |
| 151 | return list(set(emails)) |
| 152 | except subprocess.CalledProcessError as e: |
| 153 | raise type(e)('Could not retrieve tag e-mails for commit %s' % sha, e.cmd) from e |
| 154 | except IndexError: |
| 155 | # sha does do not have a recognized tag |
| 156 | return None |
| 157 | |
| 158 | |
Hirthanan Subenderan | bbafac4 | 2020-04-10 16:18:16 -0700 | [diff] [blame] | 159 | def get_git_push_cmd(chromeos_branch, reviewers): |
| 160 | """Generates git push command with added reviewers and autogenerated tag. |
| 161 | |
| 162 | Read more about gerrit tags here: |
| 163 | https://gerrit-review.googlesource.com/Documentation/cmd-receive-pack.html |
| 164 | """ |
| 165 | git_push_head = 'git push origin HEAD:refs/for/%s' % chromeos_branch |
| 166 | reviewers_tag = ['r=%s'% r for r in reviewers] |
| 167 | autogenerated_tag = ['t=autogenerated'] |
| 168 | tags = ','.join(reviewers_tag + autogenerated_tag) |
| 169 | return git_push_head + '%' + tags |
| 170 | |
| 171 | |
Guenter Roeck | a3182e2 | 2020-07-02 11:31:26 -0700 | [diff] [blame] | 172 | def cherry_pick_and_push_fix(fixer_upstream_sha, fixer_changeid, chromeos_branch, |
Hirthanan Subenderan | bbafac4 | 2020-04-10 16:18:16 -0700 | [diff] [blame] | 173 | fix_commit_message, reviewers): |
| 174 | """Cherry picks upstream commit into chrome repo. |
| 175 | |
| 176 | Adds reviewers and autogenerated tag with the pushed commit. |
| 177 | """ |
Hirthanan Subenderan | b186655 | 2020-03-20 14:01:14 -0700 | [diff] [blame] | 178 | cwd = os.getcwd() |
| 179 | chrome_absolute_path = common.get_kernel_absolute_path(common.CHROMEOS_PATH) |
| 180 | |
| 181 | # reset linux_chrome repo to remove local changes |
| 182 | try: |
| 183 | os.chdir(chrome_absolute_path) |
Hirthanan Subenderan | a90d8f8 | 2020-04-17 15:47:05 -0700 | [diff] [blame] | 184 | checkout_and_clean(chrome_absolute_path, chromeos_branch) |
Hirthanan Subenderan | bbafac4 | 2020-04-10 16:18:16 -0700 | [diff] [blame] | 185 | subprocess.run(['git', 'cherry-pick', '-n', fixer_upstream_sha], check=True) |
| 186 | subprocess.run(['git', 'commit', '-s', '-m', fix_commit_message], check=True) |
Hirthanan Subenderan | b186655 | 2020-03-20 14:01:14 -0700 | [diff] [blame] | 187 | |
| 188 | # commit has been cherry-picked and committed locally, precommit hook |
Guenter Roeck | a3182e2 | 2020-07-02 11:31:26 -0700 | [diff] [blame] | 189 | # in git repository adds changeid to the commit message. Pick it unless |
| 190 | # we already have one passed as parameter. |
| 191 | if not fixer_changeid: |
| 192 | fixer_changeid = get_commit_changeid_linux_chrome('HEAD') |
Guenter Roeck | f82d018 | 2020-06-19 07:50:01 -0700 | [diff] [blame] | 193 | |
| 194 | # Sometimes the commit hook doesn't attach the Change-Id to the last |
| 195 | # paragraph in the commit message. This seems to happen if the commit |
| 196 | # message includes '---' which would normally identify the start of |
| 197 | # comments. If the Change-Id is not in the last paragraph, uploading |
| 198 | # the patch is rejected by Gerrit. Force-move the Change-Id to the end |
Guenter Roeck | a3182e2 | 2020-07-02 11:31:26 -0700 | [diff] [blame] | 199 | # of the commit message to solve the problem. This conveniently also |
| 200 | # replaces the auto-generated Change-Id with the optional Change-Id |
| 201 | # passed as parameter. |
Guenter Roeck | 9a5adc3 | 2020-07-24 08:13:36 -0700 | [diff] [blame] | 202 | commit_message = get_chrome_commit_message('HEAD') |
Guenter Roeck | f82d018 | 2020-06-19 07:50:01 -0700 | [diff] [blame] | 203 | commit_message = re.sub(r'Change-Id:.*\n?', '', commit_message) |
Guenter Roeck | 9a5adc3 | 2020-07-24 08:13:36 -0700 | [diff] [blame] | 204 | commit_message = commit_message.rstrip() |
| 205 | commit_message += '\nChange-Id: %s' % fixer_changeid |
Guenter Roeck | f82d018 | 2020-06-19 07:50:01 -0700 | [diff] [blame] | 206 | subprocess.run(['git', 'commit', '--amend', '-m', commit_message], check=True) |
Hirthanan Subenderan | b186655 | 2020-03-20 14:01:14 -0700 | [diff] [blame] | 207 | |
Hirthanan Subenderan | bbafac4 | 2020-04-10 16:18:16 -0700 | [diff] [blame] | 208 | git_push_cmd = get_git_push_cmd(chromeos_branch, reviewers) |
| 209 | subprocess.run(git_push_cmd.split(' '), check=True) |
Hirthanan Subenderan | b186655 | 2020-03-20 14:01:14 -0700 | [diff] [blame] | 210 | |
| 211 | return fixer_changeid |
| 212 | except subprocess.CalledProcessError as e: |
| 213 | raise ValueError('Failed to cherrypick and push upstream fix %s on branch %s' |
| 214 | % (fixer_upstream_sha, chromeos_branch)) from e |
| 215 | finally: |
Hirthanan Subenderan | a90d8f8 | 2020-04-17 15:47:05 -0700 | [diff] [blame] | 216 | checkout_and_clean(chrome_absolute_path, chromeos_branch) |
Hirthanan Subenderan | b186655 | 2020-03-20 14:01:14 -0700 | [diff] [blame] | 217 | os.chdir(cwd) |
Guenter Roeck | 85c10bb | 2020-04-16 15:36:47 -0700 | [diff] [blame] | 218 | |
| 219 | |
| 220 | def search_subject_in_branch(merge_base, sha): |
| 221 | """Check if sha subject line is in the current branch. |
| 222 | |
| 223 | Assumes function is run from correct directory/branch. |
| 224 | """ |
| 225 | |
| 226 | try: |
| 227 | # Retrieve subject line of provided SHA |
| 228 | cmd = ['git', 'log', '--pretty=format:%s', '-n', '1', sha] |
| 229 | subject = subprocess.check_output(cmd, encoding='utf-8', errors='ignore') |
| 230 | except subprocess.CalledProcessError: |
| 231 | logging.error('Error locating subject for sha %s', sha) |
| 232 | raise |
| 233 | |
| 234 | try: |
Guenter Roeck | 32b33aa | 2020-05-28 09:56:26 -0700 | [diff] [blame] | 235 | cmd = ['git', 'log', '--no-merges', '-F', '--grep', subject, |
Guenter Roeck | 85c10bb | 2020-04-16 15:36:47 -0700 | [diff] [blame] | 236 | '%s..' % merge_base] |
| 237 | result = subprocess.check_output(cmd, stderr=subprocess.DEVNULL) |
| 238 | return bool(result) |
| 239 | except subprocess.CalledProcessError: |
| 240 | logging.error('Error while searching for subject "%s"', subject) |
| 241 | raise |
| 242 | |
| 243 | |
| 244 | def get_cherrypick_status(repository, merge_base, branch, sha): |
| 245 | """cherry-pick provided sha into provided repository and branch. |
| 246 | |
| 247 | Return Status Enum: |
| 248 | MERGED if the patch has already been applied, |
| 249 | OPEN if the patch is missing and applies cleanly, |
| 250 | CONFLICT if the patch is missing and fails to apply. |
| 251 | """ |
| 252 | # Save current working directory |
| 253 | cwd = os.getcwd() |
| 254 | |
| 255 | # Switch to repository directory to apply cherry-pick |
| 256 | absolute_path = common.get_kernel_absolute_path(repository) |
| 257 | |
| 258 | os.chdir(absolute_path) |
| 259 | checkout_and_clean(absolute_path, branch) |
| 260 | |
| 261 | ret = None |
| 262 | try: |
| 263 | applied = search_subject_in_branch(merge_base, sha) |
| 264 | if applied: |
| 265 | ret = common.Status.MERGED |
| 266 | raise ValueError |
| 267 | |
| 268 | result = subprocess.call(['git', 'cherry-pick', '-n', sha], |
| 269 | stdout=subprocess.DEVNULL, |
| 270 | stderr=subprocess.DEVNULL) |
| 271 | if result: |
| 272 | ret = common.Status.CONFLICT |
| 273 | raise ValueError |
| 274 | |
| 275 | diff = subprocess.check_output(['git', 'diff', 'HEAD']) |
| 276 | if diff: |
| 277 | ret = common.Status.OPEN |
| 278 | raise ValueError |
| 279 | |
| 280 | ret = common.Status.MERGED |
| 281 | |
| 282 | except ValueError: |
| 283 | pass |
| 284 | |
| 285 | except subprocess.CalledProcessError: |
| 286 | ret = common.Status.CONFLICT |
| 287 | |
| 288 | finally: |
| 289 | checkout_and_clean(absolute_path, branch) |
| 290 | os.chdir(cwd) |
| 291 | |
| 292 | return ret |
Guenter Roeck | d9b6c6c | 2020-04-16 10:06:46 -0700 | [diff] [blame] | 293 | |
| 294 | |
| 295 | # match "vX.Y[.Z][.rcN]" |
| 296 | version = re.compile(r'(v[0-9]+(?:\.[0-9]+)+(?:-rc[0-9]+)?)\s*') |
| 297 | |
| 298 | def get_integrated_tag(sha): |
| 299 | """For a given SHA, find the first tag that includes it.""" |
| 300 | |
| 301 | try: |
| 302 | path = common.get_kernel_absolute_path(common.UPSTREAM_PATH) |
| 303 | cmd = ['git', '-C', path, 'describe', '--match', 'v*', |
| 304 | '--contains', sha] |
| 305 | tag = subprocess.check_output(cmd, encoding='utf-8', |
| 306 | stderr=subprocess.DEVNULL) |
| 307 | return version.match(tag).group() |
| 308 | except AttributeError: |
| 309 | return None |
| 310 | except subprocess.CalledProcessError: |
| 311 | return None |