Ravi Mistry | bd2d111 | 2016-11-17 08:13:51 -0500 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # Copyright (c) 2014 The Chromium Authors. All rights reserved. |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
| 6 | """This module contains functions for using git.""" |
| 7 | |
| 8 | import re |
Ravi Mistry | badc137 | 2016-11-23 08:38:18 -0500 | [diff] [blame] | 9 | import shutil |
Ravi Mistry | bd2d111 | 2016-11-17 08:13:51 -0500 | [diff] [blame] | 10 | import subprocess |
Ravi Mistry | badc137 | 2016-11-23 08:38:18 -0500 | [diff] [blame] | 11 | import tempfile |
| 12 | |
| 13 | import utils |
Ravi Mistry | bd2d111 | 2016-11-17 08:13:51 -0500 | [diff] [blame] | 14 | |
| 15 | |
| 16 | class GitLocalConfig(object): |
| 17 | """Class to manage local git configs.""" |
| 18 | def __init__(self, config_dict): |
| 19 | self._config_dict = config_dict |
| 20 | self._previous_values = {} |
| 21 | |
| 22 | def __enter__(self): |
| 23 | for k, v in self._config_dict.iteritems(): |
| 24 | try: |
| 25 | prev = subprocess.check_output(['git', 'config', '--local', k]).rstrip() |
| 26 | if prev: |
| 27 | self._previous_values[k] = prev |
| 28 | except subprocess.CalledProcessError: |
| 29 | # We are probably here because the key did not exist in the config. |
| 30 | pass |
| 31 | subprocess.check_call(['git', 'config', '--local', k, v]) |
| 32 | |
| 33 | def __exit__(self, exc_type, _value, _traceback): |
| 34 | for k in self._config_dict: |
| 35 | if self._previous_values.get(k): |
| 36 | subprocess.check_call( |
| 37 | ['git', 'config', '--local', k, self._previous_values[k]]) |
| 38 | else: |
| 39 | subprocess.check_call(['git', 'config', '--local', '--unset', k]) |
| 40 | |
| 41 | |
| 42 | class GitBranch(object): |
| 43 | """Class to manage git branches. |
| 44 | |
| 45 | This class allows one to create a new branch in a repository to make changes, |
| 46 | then it commits the changes, switches to master branch, and deletes the |
| 47 | created temporary branch upon exit. |
| 48 | """ |
| 49 | def __init__(self, branch_name, commit_msg, upload=True, commit_queue=False, |
Ravi Mistry | a4ceaa1 | 2017-12-07 14:46:40 -0500 | [diff] [blame] | 50 | delete_when_finished=True, cc_list=None): |
Ravi Mistry | bd2d111 | 2016-11-17 08:13:51 -0500 | [diff] [blame] | 51 | self._branch_name = branch_name |
| 52 | self._commit_msg = commit_msg |
| 53 | self._upload = upload |
| 54 | self._commit_queue = commit_queue |
| 55 | self._patch_set = 0 |
| 56 | self._delete_when_finished = delete_when_finished |
Ravi Mistry | a4ceaa1 | 2017-12-07 14:46:40 -0500 | [diff] [blame] | 57 | self._cc_list = cc_list |
Ravi Mistry | bd2d111 | 2016-11-17 08:13:51 -0500 | [diff] [blame] | 58 | |
| 59 | def __enter__(self): |
| 60 | subprocess.check_call(['git', 'reset', '--hard', 'HEAD']) |
| 61 | subprocess.check_call(['git', 'checkout', 'master']) |
| 62 | if self._branch_name in subprocess.check_output(['git', 'branch']).split(): |
| 63 | subprocess.check_call(['git', 'branch', '-D', self._branch_name]) |
| 64 | subprocess.check_call(['git', 'checkout', '-b', self._branch_name, |
| 65 | '-t', 'origin/master']) |
| 66 | return self |
| 67 | |
| 68 | def commit_and_upload(self, use_commit_queue=False): |
| 69 | """Commit all changes and upload a CL, returning the issue URL.""" |
| 70 | subprocess.check_call(['git', 'commit', '-a', '-m', self._commit_msg]) |
| 71 | upload_cmd = ['git', 'cl', 'upload', '-f', '--bypass-hooks', |
| 72 | '--bypass-watchlists'] |
| 73 | self._patch_set += 1 |
| 74 | if self._patch_set > 1: |
| 75 | upload_cmd.extend(['-t', 'Patch set %d' % self._patch_set]) |
| 76 | if use_commit_queue: |
| 77 | upload_cmd.append('--use-commit-queue') |
Ravi Mistry | cf79ee5 | 2017-07-14 08:15:58 -0400 | [diff] [blame] | 78 | # Need the --send-mail flag to publish the CL and remove WIP bit. |
| 79 | upload_cmd.append('--send-mail') |
Ravi Mistry | a4ceaa1 | 2017-12-07 14:46:40 -0500 | [diff] [blame] | 80 | if self._cc_list: |
| 81 | upload_cmd.extend(['--cc=%s' % ','.join(self._cc_list)]) |
Ravi Mistry | bd2d111 | 2016-11-17 08:13:51 -0500 | [diff] [blame] | 82 | subprocess.check_call(upload_cmd) |
| 83 | output = subprocess.check_output(['git', 'cl', 'issue']).rstrip() |
| 84 | return re.match('^Issue number: (?P<issue>\d+) \((?P<issue_url>.+)\)$', |
| 85 | output).group('issue_url') |
| 86 | |
| 87 | def __exit__(self, exc_type, _value, _traceback): |
| 88 | if self._upload: |
| 89 | # Only upload if no error occurred. |
| 90 | try: |
| 91 | if exc_type is None: |
| 92 | self.commit_and_upload(use_commit_queue=self._commit_queue) |
| 93 | finally: |
| 94 | subprocess.check_call(['git', 'checkout', 'master']) |
| 95 | if self._delete_when_finished: |
| 96 | subprocess.check_call(['git', 'branch', '-D', self._branch_name]) |
Ravi Mistry | badc137 | 2016-11-23 08:38:18 -0500 | [diff] [blame] | 97 | |
| 98 | |
| 99 | class NewGitCheckout(utils.tmp_dir): |
| 100 | """Creates a new local checkout of a Git repository.""" |
| 101 | |
| 102 | def __init__(self, repository, commit='HEAD'): |
| 103 | """Set parameters for this local copy of a Git repository. |
| 104 | |
| 105 | Because this is a new checkout, rather than a reference to an existing |
| 106 | checkout on disk, it is safe to assume that the calling thread is the |
| 107 | only thread manipulating the checkout. |
| 108 | |
| 109 | You must use the 'with' statement to create this object: |
| 110 | |
| 111 | with NewGitCheckout(*args) as checkout: |
| 112 | # use checkout instance |
| 113 | # the checkout is automatically cleaned up here |
| 114 | |
| 115 | Args: |
| 116 | repository: URL of the remote repository (e.g., |
| 117 | 'https://skia.googlesource.com/common') or path to a local repository |
| 118 | (e.g., '/path/to/repo/.git') to check out a copy of |
| 119 | commit: commit hash, branch, or tag within refspec, indicating what point |
| 120 | to update the local checkout to |
| 121 | """ |
| 122 | super(NewGitCheckout, self).__init__() |
| 123 | self._repository = repository |
| 124 | self._commit = commit |
| 125 | |
| 126 | @property |
| 127 | def root(self): |
| 128 | """Returns the root directory containing the checked-out files.""" |
| 129 | return self.name |
| 130 | |
| 131 | def __enter__(self): |
| 132 | """Check out a new local copy of the repository. |
| 133 | |
| 134 | Uses the parameters that were passed into the constructor. |
| 135 | """ |
| 136 | super(NewGitCheckout, self).__enter__() |
| 137 | subprocess.check_output(args=['git', 'clone', self._repository, self.root]) |
| 138 | return self |