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 |