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