Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2012 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 """SCM-specific utility classes.""" | 5 """SCM-specific utility classes.""" |
| 6 | 6 |
| 7 import cStringIO | 7 import cStringIO |
| 8 import glob | 8 import glob |
| 9 import logging | 9 import logging |
| 10 import os | 10 import os |
| (...skipping 82 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 93 return 0 | 93 return 0 |
| 94 | 94 |
| 95 | 95 |
| 96 class GIT(object): | 96 class GIT(object): |
| 97 current_version = None | 97 current_version = None |
| 98 | 98 |
| 99 @staticmethod | 99 @staticmethod |
| 100 def Capture(args, cwd, **kwargs): | 100 def Capture(args, cwd, **kwargs): |
| 101 return subprocess2.check_output( | 101 return subprocess2.check_output( |
| 102 ['git', '--no-pager'] + args, | 102 ['git', '--no-pager'] + args, |
| 103 cwd=cwd, stderr=subprocess2.PIPE, **kwargs) | 103 cwd=cwd, stderr=subprocess2.PIPE, **kwargs).strip() |
| 104 | 104 |
| 105 @staticmethod | 105 @staticmethod |
| 106 def CaptureStatus(files, cwd, upstream_branch): | 106 def CaptureStatus(files, cwd, upstream_branch): |
| 107 """Returns git status. | 107 """Returns git status. |
| 108 | 108 |
| 109 @files can be a string (one file) or a list of files. | 109 @files can be a string (one file) or a list of files. |
| 110 | 110 |
| 111 Returns an array of (status, file) tuples.""" | 111 Returns an array of (status, file) tuples.""" |
| 112 if upstream_branch is None: | 112 if upstream_branch is None: |
| 113 upstream_branch = GIT.GetUpstreamBranch(cwd) | 113 upstream_branch = GIT.GetUpstreamBranch(cwd) |
| 114 if upstream_branch is None: | 114 if upstream_branch is None: |
| 115 raise gclient_utils.Error('Cannot determine upstream branch') | 115 raise gclient_utils.Error('Cannot determine upstream branch') |
| 116 command = ['diff', '--name-status', '--no-renames', | 116 command = ['diff', '--name-status', '--no-renames', |
| 117 '-r', '%s...' % upstream_branch] | 117 '-r', '%s...' % upstream_branch] |
| 118 if not files: | 118 if not files: |
| 119 pass | 119 pass |
| 120 elif isinstance(files, basestring): | 120 elif isinstance(files, basestring): |
| 121 command.append(files) | 121 command.append(files) |
| 122 else: | 122 else: |
| 123 command.extend(files) | 123 command.extend(files) |
| 124 status = GIT.Capture(command, cwd).rstrip() | 124 status = GIT.Capture(command, cwd) |
| 125 results = [] | 125 results = [] |
| 126 if status: | 126 if status: |
| 127 for statusline in status.splitlines(): | 127 for statusline in status.splitlines(): |
| 128 # 3-way merges can cause the status can be 'MMM' instead of 'M'. This | 128 # 3-way merges can cause the status can be 'MMM' instead of 'M'. This |
| 129 # can happen when the user has 2 local branches and he diffs between | 129 # can happen when the user has 2 local branches and he diffs between |
| 130 # these 2 branches instead diffing to upstream. | 130 # these 2 branches instead diffing to upstream. |
| 131 m = re.match('^(\w)+\t(.+)$', statusline) | 131 m = re.match('^(\w)+\t(.+)$', statusline) |
| 132 if not m: | 132 if not m: |
| 133 raise gclient_utils.Error( | 133 raise gclient_utils.Error( |
| 134 'status currently unsupported: %s' % statusline) | 134 'status currently unsupported: %s' % statusline) |
| 135 # Only grab the first letter. | 135 # Only grab the first letter. |
| 136 results.append(('%s ' % m.group(1)[0], m.group(2))) | 136 results.append(('%s ' % m.group(1)[0], m.group(2))) |
| 137 return results | 137 return results |
| 138 | 138 |
| 139 @staticmethod | 139 @staticmethod |
| 140 def GetEmail(cwd): | 140 def GetEmail(cwd): |
| 141 """Retrieves the user email address if known.""" | 141 """Retrieves the user email address if known.""" |
| 142 # We could want to look at the svn cred when it has a svn remote but it | 142 # We could want to look at the svn cred when it has a svn remote but it |
| 143 # should be fine for now, users should simply configure their git settings. | 143 # should be fine for now, users should simply configure their git settings. |
| 144 try: | 144 try: |
| 145 return GIT.Capture(['config', 'user.email'], cwd=cwd).strip() | 145 return GIT.Capture(['config', 'user.email'], cwd=cwd) |
| 146 except subprocess2.CalledProcessError: | 146 except subprocess2.CalledProcessError: |
| 147 return '' | 147 return '' |
| 148 | 148 |
| 149 @staticmethod | 149 @staticmethod |
| 150 def ShortBranchName(branch): | 150 def ShortBranchName(branch): |
| 151 """Converts a name like 'refs/heads/foo' to just 'foo'.""" | 151 """Converts a name like 'refs/heads/foo' to just 'foo'.""" |
| 152 return branch.replace('refs/heads/', '') | 152 return branch.replace('refs/heads/', '') |
| 153 | 153 |
| 154 @staticmethod | 154 @staticmethod |
| 155 def GetBranchRef(cwd): | 155 def GetBranchRef(cwd): |
| 156 """Returns the full branch reference, e.g. 'refs/heads/master'.""" | 156 """Returns the full branch reference, e.g. 'refs/heads/master'.""" |
| 157 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd=cwd).strip() | 157 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd=cwd) |
| 158 | 158 |
| 159 @staticmethod | 159 @staticmethod |
| 160 def GetBranch(cwd): | 160 def GetBranch(cwd): |
| 161 """Returns the short branch name, e.g. 'master'.""" | 161 """Returns the short branch name, e.g. 'master'.""" |
| 162 return GIT.ShortBranchName(GIT.GetBranchRef(cwd)) | 162 return GIT.ShortBranchName(GIT.GetBranchRef(cwd)) |
| 163 | 163 |
| 164 @staticmethod | 164 @staticmethod |
| 165 def IsGitSvn(cwd): | 165 def IsGitSvn(cwd): |
| 166 """Returns true if this repo looks like it's using git-svn.""" | 166 """Returns true if this repo looks like it's using git-svn.""" |
| 167 # If you have any "svn-remote.*" config keys, we think you're using svn. | 167 # If you have any "svn-remote.*" config keys, we think you're using svn. |
| 168 try: | 168 try: |
| 169 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd=cwd) | 169 GIT.Capture(['config', '--local', '--get-regexp', r'^svn-remote\.'], |
| 170 cwd=cwd) | |
| 170 return True | 171 return True |
| 171 except subprocess2.CalledProcessError: | 172 except subprocess2.CalledProcessError: |
| 172 return False | 173 return False |
| 173 | 174 |
| 174 @staticmethod | 175 @staticmethod |
| 175 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards): | 176 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards): |
| 176 """Return the corresponding git ref if |base_url| together with |glob_spec| | 177 """Return the corresponding git ref if |base_url| together with |glob_spec| |
| 177 matches the full |url|. | 178 matches the full |url|. |
| 178 | 179 |
| 179 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below). | 180 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below). |
| (...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 233 for line in proc.stdout: | 234 for line in proc.stdout: |
| 234 match = git_svn_re.match(line) | 235 match = git_svn_re.match(line) |
| 235 if match: | 236 if match: |
| 236 url = match.group(1) | 237 url = match.group(1) |
| 237 proc.stdout.close() # Cut pipe. | 238 proc.stdout.close() # Cut pipe. |
| 238 break | 239 break |
| 239 | 240 |
| 240 if url: | 241 if url: |
| 241 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$') | 242 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$') |
| 242 remotes = GIT.Capture( | 243 remotes = GIT.Capture( |
| 243 ['config', '--get-regexp', r'^svn-remote\..*\.url'], | 244 ['config', '--local', '--get-regexp', r'^svn-remote\..*\.url'], |
| 244 cwd=cwd).splitlines() | 245 cwd=cwd).splitlines() |
| 245 for remote in remotes: | 246 for remote in remotes: |
| 246 match = svn_remote_re.match(remote) | 247 match = svn_remote_re.match(remote) |
| 247 if match: | 248 if match: |
| 248 remote = match.group(1) | 249 remote = match.group(1) |
| 249 base_url = match.group(2) | 250 base_url = match.group(2) |
| 250 try: | 251 try: |
| 251 fetch_spec = GIT.Capture( | 252 fetch_spec = GIT.Capture( |
| 252 ['config', 'svn-remote.%s.fetch' % remote], | 253 ['config', '--local', 'svn-remote.%s.fetch' % remote], |
| 253 cwd=cwd).strip() | 254 cwd=cwd) |
| 254 branch = GIT.MatchSvnGlob(url, base_url, fetch_spec, False) | 255 branch = GIT.MatchSvnGlob(url, base_url, fetch_spec, False) |
| 255 except subprocess2.CalledProcessError: | 256 except subprocess2.CalledProcessError: |
| 256 branch = None | 257 branch = None |
| 257 if branch: | 258 if branch: |
| 258 return branch | 259 return branch |
| 259 try: | 260 try: |
| 260 branch_spec = GIT.Capture( | 261 branch_spec = GIT.Capture( |
| 261 ['config', 'svn-remote.%s.branches' % remote], | 262 ['config', '--local', 'svn-remote.%s.branches' % remote], |
| 262 cwd=cwd).strip() | 263 cwd=cwd) |
| 263 branch = GIT.MatchSvnGlob(url, base_url, branch_spec, True) | 264 branch = GIT.MatchSvnGlob(url, base_url, branch_spec, True) |
| 264 except subprocess2.CalledProcessError: | 265 except subprocess2.CalledProcessError: |
| 265 branch = None | 266 branch = None |
| 266 if branch: | 267 if branch: |
| 267 return branch | 268 return branch |
| 268 try: | 269 try: |
| 269 tag_spec = GIT.Capture( | 270 tag_spec = GIT.Capture( |
| 270 ['config', 'svn-remote.%s.tags' % remote], | 271 ['config', '--local', 'svn-remote.%s.tags' % remote], |
| 271 cwd=cwd).strip() | 272 cwd=cwd) |
| 272 branch = GIT.MatchSvnGlob(url, base_url, tag_spec, True) | 273 branch = GIT.MatchSvnGlob(url, base_url, tag_spec, True) |
| 273 except subprocess2.CalledProcessError: | 274 except subprocess2.CalledProcessError: |
| 274 branch = None | 275 branch = None |
| 275 if branch: | 276 if branch: |
| 276 return branch | 277 return branch |
| 277 | 278 |
| 278 @staticmethod | 279 @staticmethod |
| 279 def FetchUpstreamTuple(cwd): | 280 def FetchUpstreamTuple(cwd): |
| 280 """Returns a tuple containg remote and remote ref, | 281 """Returns a tuple containg remote and remote ref, |
| 281 e.g. 'origin', 'refs/heads/master' | 282 e.g. 'origin', 'refs/heads/master' |
| 282 Tries to be intelligent and understand git-svn. | 283 Tries to be intelligent and understand git-svn. |
| 283 """ | 284 """ |
| 284 remote = '.' | 285 remote = '.' |
| 285 branch = GIT.GetBranch(cwd) | 286 branch = GIT.GetBranch(cwd) |
| 286 try: | 287 try: |
| 287 upstream_branch = GIT.Capture( | 288 upstream_branch = GIT.Capture( |
| 288 ['config', 'branch.%s.merge' % branch], cwd=cwd).strip() | 289 ['config', '--local', 'branch.%s.merge' % branch], cwd=cwd) |
| 289 except subprocess2.CalledProcessError: | 290 except subprocess2.CalledProcessError: |
| 290 upstream_branch = None | 291 upstream_branch = None |
| 291 if upstream_branch: | 292 if upstream_branch: |
| 292 try: | 293 try: |
| 293 remote = GIT.Capture( | 294 remote = GIT.Capture( |
| 294 ['config', 'branch.%s.remote' % branch], cwd=cwd).strip() | 295 ['config', '--local', 'branch.%s.remote' % branch], cwd=cwd) |
| 295 except subprocess2.CalledProcessError: | 296 except subprocess2.CalledProcessError: |
| 296 pass | 297 pass |
| 297 else: | 298 else: |
| 298 try: | 299 try: |
| 299 upstream_branch = GIT.Capture( | 300 upstream_branch = GIT.Capture( |
| 300 ['config', 'rietveld.upstream-branch'], cwd=cwd).strip() | 301 ['config', '--local', 'rietveld.upstream-branch'], cwd=cwd) |
| 301 except subprocess2.CalledProcessError: | 302 except subprocess2.CalledProcessError: |
| 302 upstream_branch = None | 303 upstream_branch = None |
| 303 if upstream_branch: | 304 if upstream_branch: |
| 304 try: | 305 try: |
| 305 remote = GIT.Capture( | 306 remote = GIT.Capture( |
| 306 ['config', 'rietveld.upstream-remote'], cwd=cwd).strip() | 307 ['config', '--local', 'rietveld.upstream-remote'], cwd=cwd) |
| 307 except subprocess2.CalledProcessError: | 308 except subprocess2.CalledProcessError: |
| 308 pass | 309 pass |
| 309 else: | 310 else: |
| 310 # Fall back on trying a git-svn upstream branch. | 311 # Fall back on trying a git-svn upstream branch. |
| 311 if GIT.IsGitSvn(cwd): | 312 if GIT.IsGitSvn(cwd): |
| 312 upstream_branch = GIT.GetSVNBranch(cwd) | 313 upstream_branch = GIT.GetSVNBranch(cwd) |
| 313 else: | 314 else: |
| 314 # Else, try to guess the origin remote. | 315 # Else, try to guess the origin remote. |
| 315 remote_branches = GIT.Capture(['branch', '-r'], cwd=cwd).split() | 316 remote_branches = GIT.Capture(['branch', '-r'], cwd=cwd).split() |
| 316 if 'origin/master' in remote_branches: | 317 if 'origin/master' in remote_branches: |
| (...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 367 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'): | 368 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'): |
| 368 """Returns the list of modified files between two branches.""" | 369 """Returns the list of modified files between two branches.""" |
| 369 if not branch: | 370 if not branch: |
| 370 branch = GIT.GetUpstreamBranch(cwd) | 371 branch = GIT.GetUpstreamBranch(cwd) |
| 371 command = ['diff', '--name-only', branch + "..." + branch_head] | 372 command = ['diff', '--name-only', branch + "..." + branch_head] |
| 372 return GIT.Capture(command, cwd=cwd).splitlines(False) | 373 return GIT.Capture(command, cwd=cwd).splitlines(False) |
| 373 | 374 |
| 374 @staticmethod | 375 @staticmethod |
| 375 def GetPatchName(cwd): | 376 def GetPatchName(cwd): |
| 376 """Constructs a name for this patch.""" | 377 """Constructs a name for this patch.""" |
| 377 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd=cwd).strip() | 378 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd=cwd) |
| 378 return "%s#%s" % (GIT.GetBranch(cwd), short_sha) | 379 return "%s#%s" % (GIT.GetBranch(cwd), short_sha) |
| 379 | 380 |
| 380 @staticmethod | 381 @staticmethod |
| 381 def GetCheckoutRoot(cwd): | 382 def GetCheckoutRoot(cwd): |
| 382 """Returns the top level directory of a git checkout as an absolute path. | 383 """Returns the top level directory of a git checkout as an absolute path. |
| 383 """ | 384 """ |
| 384 root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd).strip() | 385 root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd) |
| 385 return os.path.abspath(os.path.join(cwd, root)) | 386 return os.path.abspath(os.path.join(cwd, root)) |
| 386 | 387 |
| 387 @staticmethod | 388 @staticmethod |
| 388 def GetGitSvnHeadRev(cwd): | 389 def GetGitSvnHeadRev(cwd): |
| 389 """Gets the most recently pulled git-svn revision.""" | 390 """Gets the most recently pulled git-svn revision.""" |
| 390 try: | 391 try: |
| 391 output = GIT.Capture(['svn', 'info'], cwd=cwd) | 392 output = GIT.Capture(['svn', 'info'], cwd=cwd) |
| 392 match = re.search(r'^Revision: ([0-9]+)$', output, re.MULTILINE) | 393 match = re.search(r'^Revision: ([0-9]+)$', output, re.MULTILINE) |
| 393 return int(match.group(1)) if match else None | 394 return int(match.group(1)) if match else None |
| 394 except (subprocess2.CalledProcessError, ValueError): | 395 except (subprocess2.CalledProcessError, ValueError): |
| (...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 433 if not sha1: | 434 if not sha1: |
| 434 return None | 435 return None |
| 435 output = GIT.Capture(['rev-list', '-n', '1', '%s^1' % sha1], cwd=cwd) | 436 output = GIT.Capture(['rev-list', '-n', '1', '%s^1' % sha1], cwd=cwd) |
| 436 if git_svn_rev != output.rstrip(): | 437 if git_svn_rev != output.rstrip(): |
| 437 raise gclient_utils.Error(sha1) | 438 raise gclient_utils.Error(sha1) |
| 438 return sha1 | 439 return sha1 |
| 439 except subprocess2.CalledProcessError: | 440 except subprocess2.CalledProcessError: |
| 440 return None | 441 return None |
| 441 | 442 |
| 442 @staticmethod | 443 @staticmethod |
| 443 def IsValidRevision(cwd, rev): | 444 def IsValidRevision(cwd, rev, sha_only=False): |
| 444 """Verifies the revision is a proper git revision.""" | 445 """Verifies the revision is a proper git revision. |
| 446 | |
| 447 sha_only: Fail unless rev is a sha hash. | |
| 448 """ | |
| 445 # 'git rev-parse foo' where foo is *any* 40 character hex string will return | 449 # 'git rev-parse foo' where foo is *any* 40 character hex string will return |
| 446 # the string and return code 0. So strip one character to force 'git | 450 # the string and return code 0. So strip one character to force 'git |
| 447 # rev-parse' to do a hash table look-up and returns 128 if the hash is not | 451 # rev-parse' to do a hash table look-up and returns 128 if the hash is not |
| 448 # present. | 452 # present. |
| 453 lookup_rev = rev | |
| 449 if re.match(r'^[0-9a-fA-F]{40}$', rev): | 454 if re.match(r'^[0-9a-fA-F]{40}$', rev): |
| 450 rev = rev[:-1] | 455 lookup_rev = rev[:-1] |
| 451 try: | 456 try: |
| 452 GIT.Capture(['rev-parse', rev], cwd=cwd) | 457 sha = GIT.Capture(['rev-parse', lookup_rev], cwd=cwd) |
| 458 if lookup_rev != rev: | |
| 459 # Make sure we get the original 40 chars back. | |
| 460 return rev == sha | |
|
M-A Ruel
2013/07/08 18:44:41
what about lower/upper case?
return rev.lower() ==
Isaac (away)
2013/07/08 20:03:34
Good point. It looks like git always returns lowe
| |
| 461 if sha_only: | |
| 462 return sha.startswith(rev) | |
|
M-A Ruel
2013/07/08 18:44:41
sha.lower().startswith(rev.lower())
| |
| 453 return True | 463 return True |
| 454 except subprocess2.CalledProcessError: | 464 except subprocess2.CalledProcessError: |
| 455 return False | 465 return False |
| 456 | 466 |
| 457 @classmethod | 467 @classmethod |
| 458 def AssertVersion(cls, min_version): | 468 def AssertVersion(cls, min_version): |
| 459 """Asserts git's version is at least min_version.""" | 469 """Asserts git's version is at least min_version.""" |
| 460 if cls.current_version is None: | 470 if cls.current_version is None: |
| 461 current_version = cls.Capture(['--version'], '.') | 471 current_version = cls.Capture(['--version'], '.') |
| 462 matched = re.search(r'version ([0-9\.]+)', current_version) | 472 matched = re.search(r'version ([0-9\.]+)', current_version) |
| (...skipping 627 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1090 # revert, like for properties. | 1100 # revert, like for properties. |
| 1091 if not os.path.isdir(cwd): | 1101 if not os.path.isdir(cwd): |
| 1092 # '.' was deleted. It's not worth continuing. | 1102 # '.' was deleted. It's not worth continuing. |
| 1093 return | 1103 return |
| 1094 try: | 1104 try: |
| 1095 SVN.Capture(['revert', file_status[1]], cwd=cwd) | 1105 SVN.Capture(['revert', file_status[1]], cwd=cwd) |
| 1096 except subprocess2.CalledProcessError: | 1106 except subprocess2.CalledProcessError: |
| 1097 if not os.path.exists(file_path): | 1107 if not os.path.exists(file_path): |
| 1098 continue | 1108 continue |
| 1099 raise | 1109 raise |
| OLD | NEW |