| OLD | NEW |
| 1 # Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2006-2009 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 os | 7 import os |
| 8 import re | 8 import re |
| 9 import shutil |
| 9 import subprocess | 10 import subprocess |
| 10 import sys | 11 import sys |
| 11 import tempfile | 12 import tempfile |
| 12 import xml.dom.minidom | 13 import xml.dom.minidom |
| 13 | 14 |
| 14 import gclient_utils | 15 import gclient_utils |
| 15 | 16 |
| 16 | 17 |
| 17 class GIT(object): | 18 class GIT(object): |
| 18 COMMAND = "git" | 19 COMMAND = "git" |
| 19 | 20 |
| 20 @staticmethod | 21 @staticmethod |
| 21 def Capture(args, in_directory=None, print_error=True): | 22 def Capture(args, in_directory=None, print_error=True, error_ok=False): |
| 22 """Runs git, capturing output sent to stdout as a string. | 23 """Runs git, capturing output sent to stdout as a string. |
| 23 | 24 |
| 24 Args: | 25 Args: |
| 25 args: A sequence of command line parameters to be passed to git. | 26 args: A sequence of command line parameters to be passed to git. |
| 26 in_directory: The directory where git is to be run. | 27 in_directory: The directory where git is to be run. |
| 27 | 28 |
| 28 Returns: | 29 Returns: |
| 29 The output sent to stdout as a string. | 30 The output sent to stdout as a string. |
| 30 """ | 31 """ |
| 31 c = [GIT.COMMAND] | 32 c = [GIT.COMMAND] |
| 32 c.extend(args) | 33 c.extend(args) |
| 33 | 34 try: |
| 34 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for | 35 return gclient_utils.CheckCall(c, in_directory, print_error) |
| 35 # the git.exe executable, but shell=True makes subprocess on Linux fail | 36 except gclient_utils.CheckCallError: |
| 36 # when it's called with a list because it only tries to execute the | 37 if error_ok: |
| 37 # first string ("git"). | 38 return '' |
| 38 stderr = None | 39 raise |
| 39 if not print_error: | |
| 40 stderr = subprocess.PIPE | |
| 41 return subprocess.Popen(c, | |
| 42 cwd=in_directory, | |
| 43 shell=sys.platform.startswith('win'), | |
| 44 stdout=subprocess.PIPE, | |
| 45 stderr=stderr).communicate()[0] | |
| 46 | |
| 47 | 40 |
| 48 @staticmethod | 41 @staticmethod |
| 49 def CaptureStatus(files, upstream_branch='origin'): | 42 def CaptureStatus(files, upstream_branch='origin'): |
| 50 """Returns git status. | 43 """Returns git status. |
| 51 | 44 |
| 52 @files can be a string (one file) or a list of files. | 45 @files can be a string (one file) or a list of files. |
| 53 | 46 |
| 54 Returns an array of (status, file) tuples.""" | 47 Returns an array of (status, file) tuples.""" |
| 55 command = ["diff", "--name-status", "-r", "%s.." % upstream_branch] | 48 command = ["diff", "--name-status", "-r", "%s.." % upstream_branch] |
| 56 if not files: | 49 if not files: |
| (...skipping 11 matching lines...) Expand all Loading... |
| 68 if not m: | 61 if not m: |
| 69 raise Exception("status currently unsupported: %s" % statusline) | 62 raise Exception("status currently unsupported: %s" % statusline) |
| 70 results.append(('%s ' % m.group(1), m.group(2))) | 63 results.append(('%s ' % m.group(1), m.group(2))) |
| 71 return results | 64 return results |
| 72 | 65 |
| 73 @staticmethod | 66 @staticmethod |
| 74 def GetEmail(repo_root): | 67 def GetEmail(repo_root): |
| 75 """Retrieves the user email address if known.""" | 68 """Retrieves the user email address if known.""" |
| 76 # We could want to look at the svn cred when it has a svn remote but it | 69 # We could want to look at the svn cred when it has a svn remote but it |
| 77 # should be fine for now, users should simply configure their git settings. | 70 # should be fine for now, users should simply configure their git settings. |
| 78 return GIT.Capture(['config', 'user.email'], repo_root).strip() | 71 return GIT.Capture(['config', 'user.email'], |
| 72 repo_root, error_ok=True).strip() |
| 73 |
| 74 @staticmethod |
| 75 def ShortBranchName(branch): |
| 76 """Converts a name like 'refs/heads/foo' to just 'foo'.""" |
| 77 return branch.replace('refs/heads/', '') |
| 78 |
| 79 @staticmethod |
| 80 def GetBranchRef(cwd): |
| 81 """Returns the short branch name, e.g. 'master'.""" |
| 82 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd).strip() |
| 83 |
| 84 @staticmethod |
| 85 def IsGitSvn(cwd): |
| 86 """Returns true if this repo looks like it's using git-svn.""" |
| 87 # If you have any "svn-remote.*" config keys, we think you're using svn. |
| 88 try: |
| 89 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd) |
| 90 return True |
| 91 except gclient_utils.CheckCallError: |
| 92 return False |
| 93 |
| 94 @staticmethod |
| 95 def GetSVNBranch(cwd): |
| 96 """Returns the svn branch name if found.""" |
| 97 # Try to figure out which remote branch we're based on. |
| 98 # Strategy: |
| 99 # 1) find all git-svn branches and note their svn URLs. |
| 100 # 2) iterate through our branch history and match up the URLs. |
| 101 |
| 102 # regexp matching the git-svn line that contains the URL. |
| 103 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE) |
| 104 |
| 105 # Get the refname and svn url for all refs/remotes/*. |
| 106 remotes = GIT.Capture( |
| 107 ['for-each-ref', '--format=%(refname)', 'refs/remotes'], |
| 108 cwd).splitlines() |
| 109 svn_refs = {} |
| 110 for ref in remotes: |
| 111 match = git_svn_re.search( |
| 112 GIT.Capture(['cat-file', '-p', ref], cwd)) |
| 113 if match: |
| 114 svn_refs[match.group(1)] = ref |
| 115 |
| 116 svn_branch = '' |
| 117 if len(svn_refs) == 1: |
| 118 # Only one svn branch exists -- seems like a good candidate. |
| 119 svn_branch = svn_refs.values()[0] |
| 120 elif len(svn_refs) > 1: |
| 121 # We have more than one remote branch available. We don't |
| 122 # want to go through all of history, so read a line from the |
| 123 # pipe at a time. |
| 124 # The -100 is an arbitrary limit so we don't search forever. |
| 125 cmd = ['git', 'log', '-100', '--pretty=medium'] |
| 126 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd) |
| 127 for line in proc.stdout: |
| 128 match = git_svn_re.match(line) |
| 129 if match: |
| 130 url = match.group(1) |
| 131 if url in svn_refs: |
| 132 svn_branch = svn_refs[url] |
| 133 proc.stdout.close() # Cut pipe. |
| 134 break |
| 135 return svn_branch |
| 136 |
| 137 @staticmethod |
| 138 def FetchUpstreamTuple(cwd): |
| 139 """Returns a tuple containg remote and remote ref, |
| 140 e.g. 'origin', 'refs/heads/master' |
| 141 """ |
| 142 remote = '.' |
| 143 branch = GIT.ShortBranchName(GIT.GetBranchRef(cwd)) |
| 144 upstream_branch = None |
| 145 upstream_branch = GIT.Capture( |
| 146 ['config', 'branch.%s.merge' % branch], error_ok=True).strip() |
| 147 if upstream_branch: |
| 148 remote = GIT.Capture( |
| 149 ['config', 'branch.%s.remote' % branch], |
| 150 error_ok=True).strip() |
| 151 else: |
| 152 # Fall back on trying a git-svn upstream branch. |
| 153 if GIT.IsGitSvn(cwd): |
| 154 upstream_branch = GIT.GetSVNBranch(cwd) |
| 155 # Fall back on origin/master if it exits. |
| 156 if not upstream_branch: |
| 157 GIT.Capture(['branch', '-r']).split().count('origin/master') |
| 158 remote = 'origin' |
| 159 upstream_branch = 'refs/heads/master' |
| 160 return remote, upstream_branch |
| 161 |
| 162 @staticmethod |
| 163 def GetUpstream(cwd): |
| 164 """Gets the current branch's upstream branch.""" |
| 165 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd) |
| 166 if remote is not '.': |
| 167 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote) |
| 168 return upstream_branch |
| 169 |
| 170 @staticmethod |
| 171 def GenerateDiff(cwd, branch=None): |
| 172 """Diffs against the upstream branch or optionally another branch.""" |
| 173 if not branch: |
| 174 branch = GIT.GetUpstream(cwd) |
| 175 diff = GIT.Capture(['diff-tree', '-p', '--no-prefix', branch, 'HEAD'], |
| 176 cwd).splitlines(True) |
| 177 for i in range(len(diff)): |
| 178 # In the case of added files, replace /dev/null with the path to the |
| 179 # file being added. |
| 180 if diff[i].startswith('--- /dev/null'): |
| 181 diff[i] = '--- %s' % diff[i+1][4:] |
| 182 return ''.join(diff) |
| 79 | 183 |
| 80 | 184 |
| 81 class SVN(object): | 185 class SVN(object): |
| 82 COMMAND = "svn" | 186 COMMAND = "svn" |
| 83 | 187 |
| 84 @staticmethod | 188 @staticmethod |
| 85 def Run(args, in_directory): | 189 def Run(args, in_directory): |
| 86 """Runs svn, sending output to stdout. | 190 """Runs svn, sending output to stdout. |
| 87 | 191 |
| 88 Args: | 192 Args: |
| (...skipping 296 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 385 empty string is also returned. | 489 empty string is also returned. |
| 386 """ | 490 """ |
| 387 output = SVN.Capture(["propget", property_name, file]) | 491 output = SVN.Capture(["propget", property_name, file]) |
| 388 if (output.startswith("svn: ") and | 492 if (output.startswith("svn: ") and |
| 389 output.endswith("is not under version control")): | 493 output.endswith("is not under version control")): |
| 390 return "" | 494 return "" |
| 391 else: | 495 else: |
| 392 return output | 496 return output |
| 393 | 497 |
| 394 @staticmethod | 498 @staticmethod |
| 395 def DiffItem(filename): | 499 def DiffItem(filename, full_move=False): |
| 396 """Diff a single file""" | 500 """Diffs a single file. |
| 501 |
| 502 Be sure to be in the appropriate directory before calling to have the |
| 503 expected relative path.""" |
| 397 # Use svn info output instead of os.path.isdir because the latter fails | 504 # Use svn info output instead of os.path.isdir because the latter fails |
| 398 # when the file is deleted. | 505 # when the file is deleted. |
| 399 if SVN.CaptureInfo(filename).get("Node Kind") == "directory": | 506 if SVN.CaptureInfo(filename).get("Node Kind") == "directory": |
| 400 return None | 507 return None |
| 401 # If the user specified a custom diff command in their svn config file, | 508 # If the user specified a custom diff command in their svn config file, |
| 402 # then it'll be used when we do svn diff, which we don't want to happen | 509 # then it'll be used when we do svn diff, which we don't want to happen |
| 403 # since we want the unified diff. Using --diff-cmd=diff doesn't always | 510 # since we want the unified diff. Using --diff-cmd=diff doesn't always |
| 404 # work, since they can have another diff executable in their path that | 511 # work, since they can have another diff executable in their path that |
| 405 # gives different line endings. So we use a bogus temp directory as the | 512 # gives different line endings. So we use a bogus temp directory as the |
| 406 # config directory, which gets around these problems. | 513 # config directory, which gets around these problems. |
| 407 if sys.platform.startswith("win"): | 514 bogus_dir = tempfile.mkdtemp() |
| 408 parent_dir = tempfile.gettempdir() | 515 try: |
| 409 else: | 516 # Grabs the diff data. |
| 410 parent_dir = sys.path[0] # tempdir is not secure. | 517 data = SVN.Capture(["diff", "--config-dir", bogus_dir, filename], None) |
| 411 bogus_dir = os.path.join(parent_dir, "temp_svn_config") | 518 if data: |
| 412 if not os.path.exists(bogus_dir): | 519 pass |
| 413 os.mkdir(bogus_dir) | 520 elif SVN.IsMoved(filename): |
| 414 # Grabs the diff data. | 521 if full_move: |
| 415 data = SVN.Capture(["diff", "--config-dir", bogus_dir, filename], None) | 522 file_content = gclient_utils.FileRead(filename, 'rb') |
| 523 # Prepend '+' to every lines. |
| 524 file_content = ['+' + i for i in file_content.splitlines(True)] |
| 525 nb_lines = len(file_content) |
| 526 # We need to use / since patch on unix will fail otherwise. |
| 527 filename = filename.replace('\\', '/') |
| 528 data = "Index: %s\n" % filename |
| 529 data += '=' * 67 + '\n' |
| 530 # Note: Should we use /dev/null instead? |
| 531 data += "--- %s\n" % filename |
| 532 data += "+++ %s\n" % filename |
| 533 data += "@@ -0,0 +1,%d @@\n" % nb_lines |
| 534 data += ''.join(file_content) |
| 535 else: |
| 536 # svn diff on a mv/cp'd file outputs nothing. |
| 537 # We put in an empty Index entry so upload.py knows about them. |
| 538 data = "Index: %s\n" % filename |
| 539 else: |
| 540 # The file is not modified anymore. It should be removed from the set. |
| 541 pass |
| 542 finally: |
| 543 shutil.rmtree(bogus_dir) |
| 544 return data |
| 416 | 545 |
| 417 # We know the diff will be incorrectly formatted. Fix it. | 546 @staticmethod |
| 418 if SVN.IsMoved(filename): | 547 def GenerateDiff(filenames, root=None, full_move=False): |
| 419 file_content = gclient_utils.FileRead(filename, 'rb') | 548 """Returns a string containing the diff for the given file list. |
| 420 # Prepend '+' to every lines. | 549 |
| 421 file_content = ['+' + i for i in file_content.splitlines(True)] | 550 The files in the list should either be absolute paths or relative to the |
| 422 nb_lines = len(file_content) | 551 given root. If no root directory is provided, the repository root will be |
| 423 # We need to use / since patch on unix will fail otherwise. | 552 used. |
| 424 filename = filename.replace('\\', '/') | 553 The diff will always use relative paths. |
| 425 data = "Index: %s\n" % filename | 554 """ |
| 426 data += ("=============================================================" | 555 previous_cwd = os.getcwd() |
| 427 "======\n") | 556 root = os.path.join(root or SVN.GetCheckoutRoot(previous_cwd), '') |
| 428 # Note: Should we use /dev/null instead? | 557 def RelativePath(path, root): |
| 429 data += "--- %s\n" % filename | 558 """We must use relative paths.""" |
| 430 data += "+++ %s\n" % filename | 559 if path.startswith(root): |
| 431 data += "@@ -0,0 +1,%d @@\n" % nb_lines | 560 return path[len(root):] |
| 432 data += ''.join(file_content) | 561 return path |
| 433 return data | 562 try: |
| 563 os.chdir(root) |
| 564 diff = "".join(filter(None, |
| 565 [SVN.DiffItem(RelativePath(f, root), |
| 566 full_move=full_move) |
| 567 for f in filenames])) |
| 568 finally: |
| 569 os.chdir(previous_cwd) |
| 570 return diff |
| 571 |
| 434 | 572 |
| 435 @staticmethod | 573 @staticmethod |
| 436 def GetEmail(repo_root): | 574 def GetEmail(repo_root): |
| 437 """Retrieves the svn account which we assume is an email address.""" | 575 """Retrieves the svn account which we assume is an email address.""" |
| 438 infos = SVN.CaptureInfo(repo_root) | 576 infos = SVN.CaptureInfo(repo_root) |
| 439 uuid = infos.get('UUID') | 577 uuid = infos.get('UUID') |
| 440 root = infos.get('Repository Root') | 578 root = infos.get('Repository Root') |
| 441 if not root: | 579 if not root: |
| 442 return None | 580 return None |
| 443 | 581 |
| (...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 498 if not cur_dir_repo_root: | 636 if not cur_dir_repo_root: |
| 499 return None | 637 return None |
| 500 | 638 |
| 501 while True: | 639 while True: |
| 502 parent = os.path.dirname(directory) | 640 parent = os.path.dirname(directory) |
| 503 if (SVN.CaptureInfo(parent, print_error=False).get( | 641 if (SVN.CaptureInfo(parent, print_error=False).get( |
| 504 "Repository Root") != cur_dir_repo_root): | 642 "Repository Root") != cur_dir_repo_root): |
| 505 break | 643 break |
| 506 directory = parent | 644 directory = parent |
| 507 return directory | 645 return directory |
| OLD | NEW |