1# Copyright 2014 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""This module contains the SourceControl class and related functions.""" 6 7import os 8 9import bisect_utils 10 11CROS_VERSION_PATTERN = 'new version number from %s' 12 13 14def DetermineAndCreateSourceControl(opts): 15 """Attempts to determine the underlying source control workflow and returns 16 a SourceControl object. 17 18 Returns: 19 An instance of a SourceControl object, or None if the current workflow 20 is unsupported. 21 """ 22 (output, _) = bisect_utils.RunGit(['rev-parse', '--is-inside-work-tree']) 23 24 if output.strip() == 'true': 25 return GitSourceControl(opts) 26 27 return None 28 29 30# TODO(qyearsley): Almost all of the methods below could be top-level functions 31# (or class methods). Refactoring may make this simpler. 32# pylint: disable=R0201 33class SourceControl(object): 34 """SourceControl is an abstraction over the source control system.""" 35 36 def __init__(self): 37 super(SourceControl, self).__init__() 38 39 def SyncToRevisionWithGClient(self, revision): 40 """Uses gclient to sync to the specified revision. 41 42 This is like running gclient sync --revision <revision>. 43 44 Args: 45 revision: A git SHA1 hash or SVN revision number (depending on workflow). 46 47 Returns: 48 The return code of the call. 49 """ 50 return bisect_utils.RunGClient(['sync', '--verbose', '--reset', '--force', 51 '--delete_unversioned_trees', '--nohooks', '--revision', revision]) 52 53 def SyncToRevisionWithRepo(self, timestamp): 54 """Uses the repo command to sync all the underlying git depots to the 55 specified time. 56 57 Args: 58 timestamp: The Unix timestamp to sync to. 59 60 Returns: 61 The return code of the call. 62 """ 63 return bisect_utils.RunRepoSyncAtTimestamp(timestamp) 64 65 66class GitSourceControl(SourceControl): 67 """GitSourceControl is used to query the underlying source control.""" 68 69 def __init__(self, opts): 70 super(GitSourceControl, self).__init__() 71 self.opts = opts 72 73 def IsGit(self): 74 return True 75 76 def GetRevisionList(self, revision_range_end, revision_range_start, cwd=None): 77 """Retrieves a list of revisions between |revision_range_start| and 78 |revision_range_end|. 79 80 Args: 81 revision_range_end: The SHA1 for the end of the range. 82 revision_range_start: The SHA1 for the beginning of the range. 83 84 Returns: 85 A list of the revisions between |revision_range_start| and 86 |revision_range_end| (inclusive). 87 """ 88 revision_range = '%s..%s' % (revision_range_start, revision_range_end) 89 cmd = ['log', '--format=%H', '-10000', '--first-parent', revision_range] 90 log_output = bisect_utils.CheckRunGit(cmd, cwd=cwd) 91 92 revision_hash_list = log_output.split() 93 revision_hash_list.append(revision_range_start) 94 95 return revision_hash_list 96 97 def SyncToRevision(self, revision, sync_client=None): 98 """Syncs to the specified revision. 99 100 Args: 101 revision: The revision to sync to. 102 use_gclient: Specifies whether or not we should sync using gclient or 103 just use source control directly. 104 105 Returns: 106 True if successful. 107 """ 108 109 if not sync_client: 110 results = bisect_utils.RunGit(['checkout', revision])[1] 111 elif sync_client == 'gclient': 112 results = self.SyncToRevisionWithGClient(revision) 113 elif sync_client == 'repo': 114 results = self.SyncToRevisionWithRepo(revision) 115 116 return not results 117 118 def ResolveToRevision(self, revision_to_check, depot, depot_deps_dict, 119 search, cwd=None): 120 """Tries to resolve an SVN revision or commit position to a git SHA1. 121 122 Args: 123 revision_to_check: The user supplied revision string that may need to be 124 resolved to a git SHA1. 125 depot: The depot the revision_to_check is from. 126 depot_deps_dict: A dictionary with information about different depots. 127 search: The number of changelists to try if the first fails to resolve 128 to a git hash. If the value is negative, the function will search 129 backwards chronologically, otherwise it will search forward. 130 131 Returns: 132 A string containing a git SHA1 hash, otherwise None. 133 """ 134 # Android-chrome is git only, so no need to resolve this to anything else. 135 if depot == 'android-chrome': 136 return revision_to_check 137 138 if depot != 'cros': 139 if not bisect_utils.IsStringInt(revision_to_check): 140 return revision_to_check 141 142 depot_svn = 'svn://svn.chromium.org/chrome/trunk/src' 143 144 if depot != 'chromium': 145 depot_svn = depot_deps_dict[depot]['svn'] 146 147 svn_revision = int(revision_to_check) 148 git_revision = None 149 150 if search > 0: 151 search_range = xrange(svn_revision, svn_revision + search, 1) 152 else: 153 search_range = xrange(svn_revision, svn_revision + search, -1) 154 155 for i in search_range: 156 svn_pattern = 'git-svn-id: %s@%d' % (depot_svn, i) 157 commit_position_pattern = '^Cr-Commit-Position: .*@{#%d}' % i 158 cmd = ['log', '--format=%H', '-1', '--grep', svn_pattern, 159 '--grep', commit_position_pattern, 'origin/master'] 160 161 (log_output, return_code) = bisect_utils.RunGit(cmd, cwd=cwd) 162 163 assert not return_code, 'An error occurred while running'\ 164 ' "git %s"' % ' '.join(cmd) 165 166 if not return_code: 167 log_output = log_output.strip() 168 169 if log_output: 170 git_revision = log_output 171 172 break 173 174 return git_revision 175 else: 176 if bisect_utils.IsStringInt(revision_to_check): 177 return int(revision_to_check) 178 else: 179 cwd = os.getcwd() 180 os.chdir(os.path.join(os.getcwd(), 'src', 'third_party', 181 'chromiumos-overlay')) 182 pattern = CROS_VERSION_PATTERN % revision_to_check 183 cmd = ['log', '--format=%ct', '-1', '--grep', pattern] 184 185 git_revision = None 186 187 log_output = bisect_utils.CheckRunGit(cmd, cwd=cwd) 188 if log_output: 189 git_revision = log_output 190 git_revision = int(log_output.strip()) 191 os.chdir(cwd) 192 193 return git_revision 194 195 def IsInProperBranch(self): 196 """Confirms they're in the master branch for performing the bisection. 197 This is needed or gclient will fail to sync properly. 198 199 Returns: 200 True if the current branch on src is 'master' 201 """ 202 cmd = ['rev-parse', '--abbrev-ref', 'HEAD'] 203 log_output = bisect_utils.CheckRunGit(cmd) 204 log_output = log_output.strip() 205 206 return log_output == "master" 207 208 def GetCommitPosition(self, git_revision, cwd=None): 209 """Finds git commit postion for the given git hash. 210 211 This function executes "git footer --position-num <git hash>" command to get 212 commit position the given revision. 213 214 Args: 215 git_revision: The git SHA1 to use. 216 cwd: Working directory to run the command from. 217 218 Returns: 219 Git commit position as integer or None. 220 """ 221 cmd = ['footers', '--position-num', git_revision] 222 output = bisect_utils.CheckRunGit(cmd, cwd) 223 commit_position = output.strip() 224 225 if bisect_utils.IsStringInt(commit_position): 226 return int(commit_position) 227 228 return None 229 230 def QueryRevisionInfo(self, revision, cwd=None): 231 """Gathers information on a particular revision, such as author's name, 232 email, subject, and date. 233 234 Args: 235 revision: Revision you want to gather information on. 236 237 Returns: 238 A dict in the following format: 239 { 240 'author': %s, 241 'email': %s, 242 'date': %s, 243 'subject': %s, 244 'body': %s, 245 } 246 """ 247 commit_info = {} 248 249 formats = ['%aN', '%aE', '%s', '%cD', '%b'] 250 targets = ['author', 'email', 'subject', 'date', 'body'] 251 252 for i in xrange(len(formats)): 253 cmd = ['log', '--format=%s' % formats[i], '-1', revision] 254 output = bisect_utils.CheckRunGit(cmd, cwd=cwd) 255 commit_info[targets[i]] = output.rstrip() 256 257 return commit_info 258 259 def CheckoutFileAtRevision(self, file_name, revision, cwd=None): 260 """Performs a checkout on a file at the given revision. 261 262 Returns: 263 True if successful. 264 """ 265 return not bisect_utils.RunGit( 266 ['checkout', revision, file_name], cwd=cwd)[1] 267 268 def RevertFileToHead(self, file_name): 269 """Un-stages a file and resets the file's state to HEAD. 270 271 Returns: 272 True if successful. 273 """ 274 # Reset doesn't seem to return 0 on success. 275 bisect_utils.RunGit(['reset', 'HEAD', file_name]) 276 277 return not bisect_utils.RunGit(['checkout', bisect_utils.FILE_DEPS_GIT])[1] 278 279 def QueryFileRevisionHistory(self, filename, revision_start, revision_end): 280 """Returns a list of commits that modified this file. 281 282 Args: 283 filename: Name of file. 284 revision_start: Start of revision range. 285 revision_end: End of revision range. 286 287 Returns: 288 Returns a list of commits that touched this file. 289 """ 290 cmd = ['log', '--format=%H', '%s~1..%s' % (revision_start, revision_end), 291 '--', filename] 292 output = bisect_utils.CheckRunGit(cmd) 293 294 return [o for o in output.split('\n') if o] 295