Index: scm.py |
diff --git a/scm.py b/scm.py |
index 96ae2647c603a739ccf184ef801f503d93f260a3..d23ab62d60d924f31e0e7eb6716649da0fa67bcd 100644 |
--- a/scm.py |
+++ b/scm.py |
@@ -6,6 +6,7 @@ |
import os |
import re |
+import shutil |
import subprocess |
import sys |
import tempfile |
@@ -18,7 +19,7 @@ class GIT(object): |
COMMAND = "git" |
@staticmethod |
- def Capture(args, in_directory=None, print_error=True): |
+ def Capture(args, in_directory=None, print_error=True, error_ok=False): |
"""Runs git, capturing output sent to stdout as a string. |
Args: |
@@ -30,20 +31,12 @@ class GIT(object): |
""" |
c = [GIT.COMMAND] |
c.extend(args) |
- |
- # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for |
- # the git.exe executable, but shell=True makes subprocess on Linux fail |
- # when it's called with a list because it only tries to execute the |
- # first string ("git"). |
- stderr = None |
- if not print_error: |
- stderr = subprocess.PIPE |
- return subprocess.Popen(c, |
- cwd=in_directory, |
- shell=sys.platform.startswith('win'), |
- stdout=subprocess.PIPE, |
- stderr=stderr).communicate()[0] |
- |
+ try: |
+ return gclient_utils.CheckCall(c, in_directory, print_error) |
+ except gclient_utils.CheckCallError: |
+ if error_ok: |
+ return '' |
+ raise |
@staticmethod |
def CaptureStatus(files, upstream_branch='origin'): |
@@ -75,7 +68,118 @@ class GIT(object): |
"""Retrieves the user email address if known.""" |
# We could want to look at the svn cred when it has a svn remote but it |
# should be fine for now, users should simply configure their git settings. |
- return GIT.Capture(['config', 'user.email'], repo_root).strip() |
+ return GIT.Capture(['config', 'user.email'], |
+ repo_root, error_ok=True).strip() |
+ |
+ @staticmethod |
+ def ShortBranchName(branch): |
+ """Converts a name like 'refs/heads/foo' to just 'foo'.""" |
+ return branch.replace('refs/heads/', '') |
+ |
+ @staticmethod |
+ def GetBranchRef(cwd): |
+ """Returns the short branch name, e.g. 'master'.""" |
+ return GIT.Capture(['symbolic-ref', 'HEAD'], cwd).strip() |
+ |
+ @staticmethod |
+ def IsGitSvn(cwd): |
+ """Returns true if this repo looks like it's using git-svn.""" |
+ # If you have any "svn-remote.*" config keys, we think you're using svn. |
+ try: |
+ GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd) |
+ return True |
+ except gclient_utils.CheckCallError: |
+ return False |
+ |
+ @staticmethod |
+ def GetSVNBranch(cwd): |
+ """Returns the svn branch name if found.""" |
+ # Try to figure out which remote branch we're based on. |
+ # Strategy: |
+ # 1) find all git-svn branches and note their svn URLs. |
+ # 2) iterate through our branch history and match up the URLs. |
+ |
+ # regexp matching the git-svn line that contains the URL. |
+ git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE) |
+ |
+ # Get the refname and svn url for all refs/remotes/*. |
+ remotes = GIT.Capture( |
+ ['for-each-ref', '--format=%(refname)', 'refs/remotes'], |
+ cwd).splitlines() |
+ svn_refs = {} |
+ for ref in remotes: |
+ match = git_svn_re.search( |
+ GIT.Capture(['cat-file', '-p', ref], cwd)) |
+ if match: |
+ svn_refs[match.group(1)] = ref |
+ |
+ svn_branch = '' |
+ if len(svn_refs) == 1: |
+ # Only one svn branch exists -- seems like a good candidate. |
+ svn_branch = svn_refs.values()[0] |
+ elif len(svn_refs) > 1: |
+ # We have more than one remote branch available. We don't |
+ # want to go through all of history, so read a line from the |
+ # pipe at a time. |
+ # The -100 is an arbitrary limit so we don't search forever. |
+ cmd = ['git', 'log', '-100', '--pretty=medium'] |
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd) |
+ for line in proc.stdout: |
+ match = git_svn_re.match(line) |
+ if match: |
+ url = match.group(1) |
+ if url in svn_refs: |
+ svn_branch = svn_refs[url] |
+ proc.stdout.close() # Cut pipe. |
+ break |
+ return svn_branch |
+ |
+ @staticmethod |
+ def FetchUpstreamTuple(cwd): |
+ """Returns a tuple containg remote and remote ref, |
+ e.g. 'origin', 'refs/heads/master' |
+ """ |
+ remote = '.' |
+ branch = GIT.ShortBranchName(GIT.GetBranchRef(cwd)) |
+ upstream_branch = None |
+ upstream_branch = GIT.Capture( |
+ ['config', 'branch.%s.merge' % branch], error_ok=True).strip() |
+ if upstream_branch: |
+ remote = GIT.Capture( |
+ ['config', 'branch.%s.remote' % branch], |
+ error_ok=True).strip() |
+ else: |
+ # Fall back on trying a git-svn upstream branch. |
+ if GIT.IsGitSvn(cwd): |
+ upstream_branch = GIT.GetSVNBranch(cwd) |
+ # Fall back on origin/master if it exits. |
+ if not upstream_branch: |
+ GIT.Capture(['branch', '-r']).split().count('origin/master') |
+ remote = 'origin' |
+ upstream_branch = 'refs/heads/master' |
+ return remote, upstream_branch |
+ |
+ @staticmethod |
+ def GetUpstream(cwd): |
+ """Gets the current branch's upstream branch.""" |
+ remote, upstream_branch = GIT.FetchUpstreamTuple(cwd) |
+ if remote is not '.': |
+ upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote) |
+ return upstream_branch |
+ |
+ @staticmethod |
+ def GenerateDiff(cwd, branch=None): |
+ """Diffs against the upstream branch or optionally another branch.""" |
+ if not branch: |
+ branch = GIT.GetUpstream(cwd) |
+ diff = GIT.Capture(['diff-tree', '-p', '--no-prefix', branch, 'HEAD'], |
+ cwd).splitlines(True) |
+ for i in range(len(diff)): |
+ # In the case of added files, replace /dev/null with the path to the |
+ # file being added. |
+ if diff[i].startswith('--- /dev/null'): |
+ diff[i] = '--- %s' % diff[i+1][4:] |
+ return ''.join(diff) |
class SVN(object): |
@@ -392,8 +496,11 @@ class SVN(object): |
return output |
@staticmethod |
- def DiffItem(filename): |
- """Diff a single file""" |
+ def DiffItem(filename, full_move=False): |
+ """Diffs a single file. |
+ |
+ Be sure to be in the appropriate directory before calling to have the |
+ expected relative path.""" |
# Use svn info output instead of os.path.isdir because the latter fails |
# when the file is deleted. |
if SVN.CaptureInfo(filename).get("Node Kind") == "directory": |
@@ -404,35 +511,66 @@ class SVN(object): |
# work, since they can have another diff executable in their path that |
# gives different line endings. So we use a bogus temp directory as the |
# config directory, which gets around these problems. |
- if sys.platform.startswith("win"): |
- parent_dir = tempfile.gettempdir() |
- else: |
- parent_dir = sys.path[0] # tempdir is not secure. |
- bogus_dir = os.path.join(parent_dir, "temp_svn_config") |
- if not os.path.exists(bogus_dir): |
- os.mkdir(bogus_dir) |
- # Grabs the diff data. |
- data = SVN.Capture(["diff", "--config-dir", bogus_dir, filename], None) |
- |
- # We know the diff will be incorrectly formatted. Fix it. |
- if SVN.IsMoved(filename): |
- file_content = gclient_utils.FileRead(filename, 'rb') |
- # Prepend '+' to every lines. |
- file_content = ['+' + i for i in file_content.splitlines(True)] |
- nb_lines = len(file_content) |
- # We need to use / since patch on unix will fail otherwise. |
- filename = filename.replace('\\', '/') |
- data = "Index: %s\n" % filename |
- data += ("=============================================================" |
- "======\n") |
- # Note: Should we use /dev/null instead? |
- data += "--- %s\n" % filename |
- data += "+++ %s\n" % filename |
- data += "@@ -0,0 +1,%d @@\n" % nb_lines |
- data += ''.join(file_content) |
+ bogus_dir = tempfile.mkdtemp() |
+ try: |
+ # Grabs the diff data. |
+ data = SVN.Capture(["diff", "--config-dir", bogus_dir, filename], None) |
+ if data: |
+ pass |
+ elif SVN.IsMoved(filename): |
+ if full_move: |
+ file_content = gclient_utils.FileRead(filename, 'rb') |
+ # Prepend '+' to every lines. |
+ file_content = ['+' + i for i in file_content.splitlines(True)] |
+ nb_lines = len(file_content) |
+ # We need to use / since patch on unix will fail otherwise. |
+ filename = filename.replace('\\', '/') |
+ data = "Index: %s\n" % filename |
+ data += '=' * 67 + '\n' |
+ # Note: Should we use /dev/null instead? |
+ data += "--- %s\n" % filename |
+ data += "+++ %s\n" % filename |
+ data += "@@ -0,0 +1,%d @@\n" % nb_lines |
+ data += ''.join(file_content) |
+ else: |
+ # svn diff on a mv/cp'd file outputs nothing. |
+ # We put in an empty Index entry so upload.py knows about them. |
+ data = "Index: %s\n" % filename |
+ else: |
+ # The file is not modified anymore. It should be removed from the set. |
+ pass |
+ finally: |
+ shutil.rmtree(bogus_dir) |
return data |
@staticmethod |
+ def GenerateDiff(filenames, root=None, full_move=False): |
+ """Returns a string containing the diff for the given file list. |
+ |
+ The files in the list should either be absolute paths or relative to the |
+ given root. If no root directory is provided, the repository root will be |
+ used. |
+ The diff will always use relative paths. |
+ """ |
+ previous_cwd = os.getcwd() |
+ root = os.path.join(root or SVN.GetCheckoutRoot(previous_cwd), '') |
+ def RelativePath(path, root): |
+ """We must use relative paths.""" |
+ if path.startswith(root): |
+ return path[len(root):] |
+ return path |
+ try: |
+ os.chdir(root) |
+ diff = "".join(filter(None, |
+ [SVN.DiffItem(RelativePath(f, root), |
+ full_move=full_move) |
+ for f in filenames])) |
+ finally: |
+ os.chdir(previous_cwd) |
+ return diff |
+ |
+ |
+ @staticmethod |
def GetEmail(repo_root): |
"""Retrieves the svn account which we assume is an email address.""" |
infos = SVN.CaptureInfo(repo_root) |