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 8import os 9import re 10import shutil 11import subprocess 12import tempfile 13 14import utils 15 16 17class GitLocalConfig(object): 18 """Class to manage local git configs.""" 19 def __init__(self, config_dict): 20 self._config_dict = config_dict 21 self._previous_values = {} 22 23 def __enter__(self): 24 for k, v in self._config_dict.items(): 25 try: 26 prev = subprocess.check_output(['git', 'config', '--local', k]).rstrip() 27 if prev: 28 self._previous_values[k] = prev 29 except subprocess.CalledProcessError: 30 # We are probably here because the key did not exist in the config. 31 pass 32 subprocess.check_call(['git', 'config', '--local', k, v]) 33 34 def __exit__(self, exc_type, _value, _traceback): 35 for k in self._config_dict: 36 if self._previous_values.get(k): 37 subprocess.check_call( 38 ['git', 'config', '--local', k, self._previous_values[k]]) 39 else: 40 subprocess.check_call(['git', 'config', '--local', '--unset', k]) 41 42 43class GitBranch(object): 44 """Class to manage git branches. 45 46 This class allows one to create a new branch in a repository to make changes, 47 then it commits the changes, switches to main branch, and deletes the 48 created temporary branch upon exit. 49 """ 50 def __init__(self, branch_name, commit_msg, upload=True, commit_queue=False, 51 delete_when_finished=True, cc_list=None): 52 self._branch_name = branch_name 53 self._commit_msg = commit_msg 54 self._upload = upload 55 self._commit_queue = commit_queue 56 self._patch_set = 0 57 self._delete_when_finished = delete_when_finished 58 self._cc_list = cc_list 59 60 def __enter__(self): 61 subprocess.check_call(['git', 'reset', '--hard', 'HEAD']) 62 subprocess.check_call(['git', 'checkout', 'main']) 63 if self._branch_name in subprocess.check_output(['git', 'branch']).split(): 64 subprocess.check_call(['git', 'branch', '-D', self._branch_name]) 65 subprocess.check_call(['git', 'checkout', '-b', self._branch_name, 66 '-t', 'origin/main']) 67 return self 68 69 def commit_and_upload(self, use_commit_queue=False): 70 """Commit all changes and upload a CL, returning the issue URL.""" 71 subprocess.check_call(['git', 'commit', '-a', '-m', self._commit_msg]) 72 upload_cmd = ['git', 'cl', 'upload', '-f', '--bypass-hooks', 73 '--bypass-watchlists'] 74 self._patch_set += 1 75 if self._patch_set > 1: 76 upload_cmd.extend(['-t', 'Patch set %d' % self._patch_set]) 77 if use_commit_queue: 78 upload_cmd.append('--use-commit-queue') 79 # Need the --send-mail flag to publish the CL and remove WIP bit. 80 upload_cmd.append('--send-mail') 81 if self._cc_list: 82 upload_cmd.extend(['--cc=%s' % ','.join(self._cc_list)]) 83 subprocess.check_call(upload_cmd) 84 output = subprocess.check_output(['git', 'cl', 'issue']).rstrip() 85 return re.match('^Issue number: (?P<issue>\d+) \((?P<issue_url>.+)\)$', 86 output).group('issue_url') 87 88 def __exit__(self, exc_type, _value, _traceback): 89 if self._upload: 90 # Only upload if no error occurred. 91 try: 92 if exc_type is None: 93 self.commit_and_upload(use_commit_queue=self._commit_queue) 94 finally: 95 subprocess.check_call(['git', 'checkout', 'main']) 96 if self._delete_when_finished: 97 subprocess.check_call(['git', 'branch', '-D', self._branch_name]) 98 99 100class NewGitCheckout(utils.tmp_dir): 101 """Creates a new local checkout of a Git repository.""" 102 103 def __init__(self, repository, local=None): 104 """Set parameters for this local copy of a Git repository. 105 106 Because this is a new checkout, rather than a reference to an existing 107 checkout on disk, it is safe to assume that the calling thread is the 108 only thread manipulating the checkout. 109 110 You must use the 'with' statement to create this object: 111 112 with NewGitCheckout(*args) as checkout: 113 # use checkout instance 114 # the checkout is automatically cleaned up here 115 116 Args: 117 repository: URL of the remote repository (e.g., 118 'https://skia.googlesource.com/common') or path to a local repository 119 (e.g., '/path/to/repo/.git') to check out a copy of 120 local: optional path to an existing copy of the remote repo on local disk. 121 If provided, the initial clone is performed with the local copy as the 122 upstream, then the upstream is switched to the remote repo and the 123 new copy is updated from there. 124 """ 125 super(NewGitCheckout, self).__init__() 126 self._checkout_root = '' 127 self._repository = repository 128 self._local = local 129 130 @property 131 def name(self): 132 return self._checkout_root 133 134 @property 135 def root(self): 136 """Returns the root directory containing the checked-out files.""" 137 return self.name 138 139 def __enter__(self): 140 """Check out a new local copy of the repository. 141 142 Uses the parameters that were passed into the constructor. 143 """ 144 super(NewGitCheckout, self).__enter__() 145 remote = self._repository 146 if self._local: 147 remote = self._local 148 subprocess.check_output(args=['git', 'clone', remote]) 149 repo_name = remote.split('/')[-1] 150 if repo_name.endswith('.git'): 151 repo_name = repo_name[:-len('.git')] 152 self._checkout_root = os.path.join(os.getcwd(), repo_name) 153 os.chdir(repo_name) 154 if self._local: 155 subprocess.check_call([ 156 'git', 'remote', 'set-url', 'origin', self._repository]) 157 subprocess.check_call(['git', 'remote', 'update']) 158 subprocess.check_call(['git', 'checkout', 'main']) 159 subprocess.check_call(['git', 'reset', '--hard', 'origin/main']) 160 return self 161