blob: 06716db672988dbc16c7e7173ec811ce42980454 [file] [log] [blame]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00001# 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
3# found in the LICENSE file.
4
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00005"""SCM-specific utility classes."""
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00006
maruel@chromium.org3c55d982010-05-06 14:25:44 +00007import cStringIO
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +00008import glob
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00009import os
10import re
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000011import shutil
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000012import subprocess
13import sys
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000014import tempfile
maruel@chromium.orgfd876172010-04-30 14:01:05 +000015import time
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000016import xml.dom.minidom
17
18import gclient_utils
19
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000020def ValidateEmail(email):
maruel@chromium.org6e29d572010-06-04 17:32:20 +000021 return (re.match(r"^[a-zA-Z0-9._%-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", email)
22 is not None)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000023
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000024
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +000025def GetCasedPath(path):
26 """Elcheapos way to get the real path case on Windows."""
27 if sys.platform.startswith('win') and os.path.exists(path):
28 # Reconstruct the path.
29 path = os.path.abspath(path)
30 paths = path.split('\\')
31 for i in range(len(paths)):
32 if i == 0:
33 # Skip drive letter.
34 continue
35 subpath = '\\'.join(paths[:i+1])
36 prev = len('\\'.join(paths[:i]))
37 # glob.glob will return the cased path for the last item only. This is why
38 # we are calling it in a loop. Extract the data we want and put it back
39 # into the list.
40 paths[i] = glob.glob(subpath + '*')[0][prev+1:len(subpath)]
41 path = '\\'.join(paths)
42 return path
43
44
maruel@chromium.org3c55d982010-05-06 14:25:44 +000045def GenFakeDiff(filename):
46 """Generates a fake diff from a file."""
47 file_content = gclient_utils.FileRead(filename, 'rb').splitlines(True)
maruel@chromium.orgc6d170e2010-06-03 00:06:00 +000048 filename = filename.replace(os.sep, '/')
maruel@chromium.org3c55d982010-05-06 14:25:44 +000049 nb_lines = len(file_content)
50 # We need to use / since patch on unix will fail otherwise.
51 data = cStringIO.StringIO()
52 data.write("Index: %s\n" % filename)
53 data.write('=' * 67 + '\n')
54 # Note: Should we use /dev/null instead?
55 data.write("--- %s\n" % filename)
56 data.write("+++ %s\n" % filename)
57 data.write("@@ -0,0 +1,%d @@\n" % nb_lines)
58 # Prepend '+' to every lines.
59 for line in file_content:
60 data.write('+')
61 data.write(line)
62 result = data.getvalue()
63 data.close()
64 return result
65
66
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000067class GIT(object):
68 COMMAND = "git"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000069
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000070 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000071 def Capture(args, in_directory=None, print_error=True, error_ok=False):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000072 """Runs git, capturing output sent to stdout as a string.
73
74 Args:
75 args: A sequence of command line parameters to be passed to git.
76 in_directory: The directory where git is to be run.
77
78 Returns:
79 The output sent to stdout as a string.
80 """
81 c = [GIT.COMMAND]
82 c.extend(args)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000083 try:
84 return gclient_utils.CheckCall(c, in_directory, print_error)
85 except gclient_utils.CheckCallError:
86 if error_ok:
nasser@codeaurora.orgcd968c12010-02-01 06:05:00 +000087 return ('', '')
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000088 raise
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000089
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000090 @staticmethod
msb@chromium.org786fb682010-06-02 15:16:23 +000091 def CaptureStatus(files, upstream_branch=None):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000092 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000093
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000094 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000095
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000096 Returns an array of (status, file) tuples."""
msb@chromium.org786fb682010-06-02 15:16:23 +000097 if upstream_branch is None:
98 upstream_branch = GIT.GetUpstreamBranch(os.getcwd())
99 if upstream_branch is None:
100 raise Exception("Cannot determine upstream branch")
bauerb@chromium.org14ec5042010-03-30 18:19:09 +0000101 command = ["diff", "--name-status", "-r", "%s..." % upstream_branch]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000102 if not files:
103 pass
104 elif isinstance(files, basestring):
105 command.append(files)
106 else:
107 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000108
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000109 status = GIT.Capture(command)[0].rstrip()
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000110 results = []
111 if status:
112 for statusline in status.split('\n'):
113 m = re.match('^(\w)\t(.+)$', statusline)
114 if not m:
115 raise Exception("status currently unsupported: %s" % statusline)
116 results.append(('%s ' % m.group(1), m.group(2)))
117 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000118
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000119 @staticmethod
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000120 def RunAndFilterOutput(args,
121 in_directory,
122 print_messages,
123 print_stdout,
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000124 filter_fn):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000125 """Runs a command, optionally outputting to stdout.
126
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000127 stdout is passed line-by-line to the given filter_fn function. If
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000128 print_stdout is true, it is also printed to sys.stdout as in Run.
129
130 Args:
131 args: A sequence of command line parameters to be passed.
msb@chromium.orgd6504212010-01-13 17:34:31 +0000132 in_directory: The directory where git is to be run.
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000133 print_messages: Whether to print status messages to stdout about
134 which commands are being run.
135 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000136 filter_fn: A function taking one argument (a string) which will be
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000137 passed each line (with the ending newline character removed) of
138 program's output for filtering.
139
140 Raises:
141 gclient_utils.Error: An error occurred while running the command.
142 """
143 command = [GIT.COMMAND]
144 command.extend(args)
145 gclient_utils.SubprocessCallAndFilter(command,
146 in_directory,
147 print_messages,
148 print_stdout,
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000149 filter_fn=filter_fn)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000150
151 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000152 def GetEmail(repo_root):
153 """Retrieves the user email address if known."""
154 # We could want to look at the svn cred when it has a svn remote but it
155 # should be fine for now, users should simply configure their git settings.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000156 return GIT.Capture(['config', 'user.email'],
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000157 repo_root, error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000158
159 @staticmethod
160 def ShortBranchName(branch):
161 """Converts a name like 'refs/heads/foo' to just 'foo'."""
162 return branch.replace('refs/heads/', '')
163
164 @staticmethod
165 def GetBranchRef(cwd):
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000166 """Returns the full branch reference, e.g. 'refs/heads/master'."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000167 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000168
169 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000170 def GetBranch(cwd):
171 """Returns the short branch name, e.g. 'master'."""
maruel@chromium.orgc308a742009-12-22 18:29:33 +0000172 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000173
174 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000175 def IsGitSvn(cwd):
176 """Returns true if this repo looks like it's using git-svn."""
177 # If you have any "svn-remote.*" config keys, we think you're using svn.
178 try:
179 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd)
180 return True
181 except gclient_utils.CheckCallError:
182 return False
183
184 @staticmethod
185 def GetSVNBranch(cwd):
186 """Returns the svn branch name if found."""
187 # Try to figure out which remote branch we're based on.
188 # Strategy:
189 # 1) find all git-svn branches and note their svn URLs.
190 # 2) iterate through our branch history and match up the URLs.
191
192 # regexp matching the git-svn line that contains the URL.
193 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
194
195 # Get the refname and svn url for all refs/remotes/*.
196 remotes = GIT.Capture(
197 ['for-each-ref', '--format=%(refname)', 'refs/remotes'],
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000198 cwd)[0].splitlines()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000199 svn_refs = {}
200 for ref in remotes:
201 match = git_svn_re.search(
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000202 GIT.Capture(['cat-file', '-p', ref], cwd)[0])
sky@chromium.org42d8da52010-04-23 18:25:07 +0000203 # Prefer origin/HEAD over all others.
204 if match and (match.group(1) not in svn_refs or
205 ref == "refs/remotes/origin/HEAD"):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000206 svn_refs[match.group(1)] = ref
207
208 svn_branch = ''
209 if len(svn_refs) == 1:
210 # Only one svn branch exists -- seems like a good candidate.
211 svn_branch = svn_refs.values()[0]
212 elif len(svn_refs) > 1:
213 # We have more than one remote branch available. We don't
214 # want to go through all of history, so read a line from the
215 # pipe at a time.
216 # The -100 is an arbitrary limit so we don't search forever.
217 cmd = ['git', 'log', '-100', '--pretty=medium']
218 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd)
219 for line in proc.stdout:
220 match = git_svn_re.match(line)
221 if match:
222 url = match.group(1)
223 if url in svn_refs:
224 svn_branch = svn_refs[url]
225 proc.stdout.close() # Cut pipe.
226 break
227 return svn_branch
228
229 @staticmethod
230 def FetchUpstreamTuple(cwd):
231 """Returns a tuple containg remote and remote ref,
232 e.g. 'origin', 'refs/heads/master'
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000233 Tries to be intelligent and understand git-svn.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000234 """
235 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000236 branch = GIT.GetBranch(cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000237 upstream_branch = None
238 upstream_branch = GIT.Capture(
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000239 ['config', 'branch.%s.merge' % branch], in_directory=cwd,
240 error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000241 if upstream_branch:
242 remote = GIT.Capture(
243 ['config', 'branch.%s.remote' % branch],
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000244 in_directory=cwd, error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000245 else:
246 # Fall back on trying a git-svn upstream branch.
247 if GIT.IsGitSvn(cwd):
248 upstream_branch = GIT.GetSVNBranch(cwd)
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000249 else:
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000250 # Else, try to guess the origin remote.
251 remote_branches = GIT.Capture(
252 ['branch', '-r'], in_directory=cwd)[0].split()
253 if 'origin/master' in remote_branches:
254 # Fall back on origin/master if it exits.
255 remote = 'origin'
256 upstream_branch = 'refs/heads/master'
257 elif 'origin/trunk' in remote_branches:
258 # Fall back on origin/trunk if it exists. Generally a shared
259 # git-svn clone
260 remote = 'origin'
261 upstream_branch = 'refs/heads/trunk'
262 else:
263 # Give up.
264 remote = None
265 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000266 return remote, upstream_branch
267
268 @staticmethod
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000269 def GetUpstreamBranch(cwd):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000270 """Gets the current branch's upstream branch."""
271 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000272 if remote != '.' and upstream_branch:
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000273 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
274 return upstream_branch
275
276 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000277 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
278 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000279 """Diffs against the upstream branch or optionally another branch.
280
281 full_move means that move or copy operations should completely recreate the
282 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000283 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000284 branch = GIT.GetUpstreamBranch(cwd)
evan@chromium.org400f3e72010-05-19 14:23:36 +0000285 command = ['diff', '-p', '--no-prefix', '--no-ext-diff',
286 branch + "..." + branch_head]
maruel@chromium.orga9371762009-12-22 18:27:38 +0000287 if not full_move:
288 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000289 # TODO(maruel): --binary support.
290 if files:
291 command.append('--')
292 command.extend(files)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000293 diff = GIT.Capture(command, cwd)[0].splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000294 for i in range(len(diff)):
295 # In the case of added files, replace /dev/null with the path to the
296 # file being added.
297 if diff[i].startswith('--- /dev/null'):
298 diff[i] = '--- %s' % diff[i+1][4:]
299 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000300
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000301 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000302 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
303 """Returns the list of modified files between two branches."""
304 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000305 branch = GIT.GetUpstreamBranch(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000306 command = ['diff', '--name-only', branch + "..." + branch_head]
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000307 return GIT.Capture(command, cwd)[0].splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000308
309 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000310 def GetPatchName(cwd):
311 """Constructs a name for this patch."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000312 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd)[0].strip()
maruel@chromium.org862ff8e2010-08-06 15:29:16 +0000313 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000314
315 @staticmethod
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000316 def GetCheckoutRoot(path):
317 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000318 """
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000319 root = GIT.Capture(['rev-parse', '--show-cdup'], path)[0].strip()
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000320 return os.path.abspath(os.path.join(path, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000321
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000322 @staticmethod
323 def AssertVersion(min_version):
324 """Asserts git's version is at least min_version."""
325 def only_int(val):
326 if val.isdigit():
327 return int(val)
328 else:
329 return 0
330 current_version = GIT.Capture(['--version'])[0].split()[-1]
331 current_version_list = map(only_int, current_version.split('.'))
332 for min_ver in map(int, min_version.split('.')):
333 ver = current_version_list.pop(0)
334 if ver < min_ver:
335 return (False, current_version)
336 elif ver > min_ver:
337 return (True, current_version)
338 return (True, current_version)
339
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000340
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000341class SVN(object):
342 COMMAND = "svn"
tony@chromium.org57564662010-04-14 02:35:12 +0000343 current_version = None
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000344
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000345 @staticmethod
346 def Run(args, in_directory):
347 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000348
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000349 Args:
350 args: A sequence of command line parameters to be passed to svn.
351 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000352
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000353 Raises:
354 Error: An error occurred while running the svn command.
355 """
356 c = [SVN.COMMAND]
357 c.extend(args)
maruel@chromium.org2185f002009-12-18 21:03:47 +0000358 # TODO(maruel): This is very gclient-specific.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000359 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000360
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000361 @staticmethod
362 def Capture(args, in_directory=None, print_error=True):
363 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000364
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000365 Args:
366 args: A sequence of command line parameters to be passed to svn.
367 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000368
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000369 Returns:
370 The output sent to stdout as a string.
371 """
372 c = [SVN.COMMAND]
373 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000374
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000375 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
376 # the svn.exe executable, but shell=True makes subprocess on Linux fail
377 # when it's called with a list because it only tries to execute the
378 # first string ("svn").
379 stderr = None
380 if not print_error:
381 stderr = subprocess.PIPE
382 return subprocess.Popen(c,
383 cwd=in_directory,
384 shell=(sys.platform == 'win32'),
385 stdout=subprocess.PIPE,
386 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000387
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000388 @staticmethod
maruel@chromium.org03807072010-08-16 17:18:44 +0000389 def RunAndGetFileList(verbose, args, in_directory, file_list):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000390 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000391
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000392 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000393
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000394 svn's stdout is parsed to collect a list of files checked out or updated.
395 These files are appended to file_list. svn's stdout is also printed to
396 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000397
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000398 Args:
maruel@chromium.org03807072010-08-16 17:18:44 +0000399 verbose: If True, uses verbose output
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000400 args: A sequence of command line parameters to be passed to svn.
401 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000402
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000403 Raises:
404 Error: An error occurred while running the svn command.
405 """
406 command = [SVN.COMMAND]
407 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000408
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000409 # svn update and svn checkout use the same pattern: the first three columns
410 # are for file status, property status, and lock status. This is followed
411 # by two spaces, and then the path to the file.
412 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000413
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000414 # The first three columns of svn status are the same as for svn update and
415 # svn checkout. The next three columns indicate addition-with-history,
416 # switch, and remote lock status. This is followed by one space, and then
417 # the path to the file.
418 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000419
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000420 # args[0] must be a supported command. This will blow up if it's something
421 # else, which is good. Note that the patterns are only effective when
422 # these commands are used in their ordinary forms, the patterns are invalid
423 # for "svn status --show-updates", for example.
424 pattern = {
425 'checkout': update_pattern,
426 'status': status_pattern,
427 'update': update_pattern,
428 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000429 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000430 # Place an upper limit.
maruel@chromium.orgfd876172010-04-30 14:01:05 +0000431 for _ in range(10):
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000432 previous_list_len = len(file_list)
433 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000434
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000435 def CaptureMatchingLines(line):
436 match = compiled_pattern.search(line)
437 if match:
438 file_list.append(match.group(1))
439 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000440 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000441
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000442 try:
443 SVN.RunAndFilterOutput(args,
444 in_directory,
maruel@chromium.org03807072010-08-16 17:18:44 +0000445 verbose,
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000446 True,
447 CaptureMatchingLines)
448 except gclient_utils.Error:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000449 def IsKnownFailure():
450 for x in failure:
451 if (x.startswith('svn: OPTIONS of') or
452 x.startswith('svn: PROPFIND of') or
453 x.startswith('svn: REPORT of') or
454 x.startswith('svn: Unknown hostname')):
455 return True
456 return False
457
maruel@chromium.org953586a2010-06-15 14:22:24 +0000458 # Subversion client is really misbehaving with Google Code.
459 if args[0] == 'checkout':
460 # Ensure at least one file was checked out, otherwise *delete* the
461 # directory.
462 if len(file_list) == previous_list_len:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000463 if not IsKnownFailure():
maruel@chromium.org953586a2010-06-15 14:22:24 +0000464 # No known svn error was found, bail out.
465 raise
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000466 # No file were checked out, so make sure the directory is
467 # deleted in case it's messed up and try again.
468 # Warning: It's bad, it assumes args[2] is the directory
469 # argument.
470 if os.path.isdir(args[2]):
471 gclient_utils.RemoveDirectory(args[2])
maruel@chromium.org953586a2010-06-15 14:22:24 +0000472 else:
473 # Progress was made, convert to update since an aborted checkout
474 # is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000475 args = ['update'] + args[1:]
maruel@chromium.org953586a2010-06-15 14:22:24 +0000476 else:
477 # It was an update or export.
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000478 # We enforce that some progress has been made or a known failure.
479 if len(file_list) == previous_list_len and not IsKnownFailure():
480 # No known svn error was found and no progress, bail out.
481 raise
maruel@chromium.org953586a2010-06-15 14:22:24 +0000482 print "Sleeping 15 seconds and retrying...."
483 time.sleep(15)
484 continue
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000485 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000486
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000487 @staticmethod
488 def RunAndFilterOutput(args,
489 in_directory,
490 print_messages,
491 print_stdout,
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000492 filter_fn):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000493 """Runs a command, optionally outputting to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000494
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000495 stdout is passed line-by-line to the given filter_fn function. If
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000496 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000497
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000498 Args:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000499 args: A sequence of command line parameters to be passed.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000500 in_directory: The directory where svn is to be run.
501 print_messages: Whether to print status messages to stdout about
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000502 which commands are being run.
503 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000504 filter_fn: A function taking one argument (a string) which will be
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000505 passed each line (with the ending newline character removed) of
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000506 program's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000507
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000508 Raises:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000509 gclient_utils.Error: An error occurred while running the command.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000510 """
511 command = [SVN.COMMAND]
512 command.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000513 gclient_utils.SubprocessCallAndFilter(command,
514 in_directory,
515 print_messages,
516 print_stdout,
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000517 filter_fn=filter_fn)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000518
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000519 @staticmethod
520 def CaptureInfo(relpath, in_directory=None, print_error=True):
521 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000522
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000523 Args:
524 relpath: The directory where the working copy resides relative to
525 the directory given by in_directory.
526 in_directory: The directory where svn is to be run.
527 """
528 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
529 dom = gclient_utils.ParseXML(output)
530 result = {}
531 if dom:
532 GetNamedNodeText = gclient_utils.GetNamedNodeText
533 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
534 def C(item, f):
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000535 if item is not None:
536 return f(item)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000537 # /info/entry/
538 # url
539 # reposityory/(root|uuid)
540 # wc-info/(schedule|depth)
541 # commit/(author|date)
542 # str() the results because they may be returned as Unicode, which
543 # interferes with the higher layers matching up things in the deps
544 # dictionary.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000545 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
546 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
547 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
548 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
549 'revision'),
550 int)
551 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
552 str)
553 # Differs across versions.
554 if result['Node Kind'] == 'dir':
555 result['Node Kind'] = 'directory'
556 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
557 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
558 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
559 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
560 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000561
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000562 @staticmethod
563 def CaptureHeadRevision(url):
564 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000565
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000566 Returns:
567 Int head revision
568 """
569 info = SVN.Capture(["info", "--xml", url], os.getcwd())
570 dom = xml.dom.minidom.parseString(info)
571 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000572
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000573 @staticmethod
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000574 def CaptureBaseRevision(cwd):
575 """Get the base revision of a SVN repository.
576
577 Returns:
578 Int base revision
579 """
580 info = SVN.Capture(["info", "--xml"], cwd)
581 dom = xml.dom.minidom.parseString(info)
582 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
583
584 @staticmethod
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000585 def CaptureStatus(files):
586 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000587
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000588 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000589
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000590 Returns an array of (status, file) tuples."""
591 command = ["status", "--xml"]
592 if not files:
593 pass
594 elif isinstance(files, basestring):
595 command.append(files)
596 else:
597 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000598
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000599 status_letter = {
600 None: ' ',
601 '': ' ',
602 'added': 'A',
603 'conflicted': 'C',
604 'deleted': 'D',
605 'external': 'X',
606 'ignored': 'I',
607 'incomplete': '!',
608 'merged': 'G',
609 'missing': '!',
610 'modified': 'M',
611 'none': ' ',
612 'normal': ' ',
613 'obstructed': '~',
614 'replaced': 'R',
615 'unversioned': '?',
616 }
617 dom = gclient_utils.ParseXML(SVN.Capture(command))
618 results = []
619 if dom:
620 # /status/target/entry/(wc-status|commit|author|date)
621 for target in dom.getElementsByTagName('target'):
622 #base_path = target.getAttribute('path')
623 for entry in target.getElementsByTagName('entry'):
624 file_path = entry.getAttribute('path')
625 wc_status = entry.getElementsByTagName('wc-status')
626 assert len(wc_status) == 1
627 # Emulate svn 1.5 status ouput...
628 statuses = [' '] * 7
629 # Col 0
630 xml_item_status = wc_status[0].getAttribute('item')
631 if xml_item_status in status_letter:
632 statuses[0] = status_letter[xml_item_status]
633 else:
634 raise Exception('Unknown item status "%s"; please implement me!' %
635 xml_item_status)
636 # Col 1
637 xml_props_status = wc_status[0].getAttribute('props')
638 if xml_props_status == 'modified':
639 statuses[1] = 'M'
640 elif xml_props_status == 'conflicted':
641 statuses[1] = 'C'
642 elif (not xml_props_status or xml_props_status == 'none' or
643 xml_props_status == 'normal'):
644 pass
645 else:
646 raise Exception('Unknown props status "%s"; please implement me!' %
647 xml_props_status)
648 # Col 2
649 if wc_status[0].getAttribute('wc-locked') == 'true':
650 statuses[2] = 'L'
651 # Col 3
652 if wc_status[0].getAttribute('copied') == 'true':
653 statuses[3] = '+'
654 # Col 4
655 if wc_status[0].getAttribute('switched') == 'true':
656 statuses[4] = 'S'
657 # TODO(maruel): Col 5 and 6
658 item = (''.join(statuses), file_path)
659 results.append(item)
660 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000661
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000662 @staticmethod
663 def IsMoved(filename):
664 """Determine if a file has been added through svn mv"""
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000665 return SVN.IsMovedInfo(SVN.CaptureInfo(filename))
666
667 @staticmethod
668 def IsMovedInfo(info):
669 """Determine if a file has been added through svn mv"""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000670 return (info.get('Copied From URL') and
671 info.get('Copied From Rev') and
672 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000673
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000674 @staticmethod
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000675 def GetFileProperty(filename, property_name):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000676 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000677
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000678 Args:
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000679 filename: The file to check
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000680 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000681
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000682 Returns:
683 The value of the property, which will be the empty string if the property
684 is not set on the file. If the file is not under version control, the
685 empty string is also returned.
686 """
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000687 output = SVN.Capture(["propget", property_name, filename])
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000688 if (output.startswith("svn: ") and
689 output.endswith("is not under version control")):
690 return ""
691 else:
692 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000693
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000694 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000695 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000696 """Diffs a single file.
697
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000698 Should be simple, eh? No it isn't.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000699 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000700 expected relative path.
701 full_move means that move or copy operations should completely recreate the
702 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000703 # If the user specified a custom diff command in their svn config file,
704 # then it'll be used when we do svn diff, which we don't want to happen
705 # since we want the unified diff. Using --diff-cmd=diff doesn't always
706 # work, since they can have another diff executable in their path that
707 # gives different line endings. So we use a bogus temp directory as the
708 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000709 bogus_dir = tempfile.mkdtemp()
710 try:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000711 # Use "svn info" output instead of os.path.isdir because the latter fails
712 # when the file is deleted.
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000713 return SVN._DiffItemInternal(filename, SVN.CaptureInfo(filename),
714 bogus_dir,
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000715 full_move=full_move, revision=revision)
716 finally:
717 shutil.rmtree(bogus_dir)
718
719 @staticmethod
720 def _DiffItemInternal(filename, info, bogus_dir, full_move=False,
721 revision=None):
722 """Grabs the diff data."""
723 command = ["diff", "--config-dir", bogus_dir, filename]
724 if revision:
725 command.extend(['--revision', revision])
726 data = None
727 if SVN.IsMovedInfo(info):
728 if full_move:
729 if info.get("Node Kind") == "directory":
730 # Things become tricky here. It's a directory copy/move. We need to
731 # diff all the files inside it.
732 # This will put a lot of pressure on the heap. This is why StringIO
733 # is used and converted back into a string at the end. The reason to
734 # return a string instead of a StringIO is that StringIO.write()
735 # doesn't accept a StringIO object. *sigh*.
736 for (dirpath, dirnames, filenames) in os.walk(filename):
737 # Cleanup all files starting with a '.'.
738 for d in dirnames:
739 if d.startswith('.'):
740 dirnames.remove(d)
741 for f in filenames:
742 if f.startswith('.'):
743 filenames.remove(f)
744 for f in filenames:
745 if data is None:
746 data = cStringIO.StringIO()
747 data.write(GenFakeDiff(os.path.join(dirpath, f)))
748 if data:
749 tmp = data.getvalue()
750 data.close()
751 data = tmp
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000752 else:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000753 data = GenFakeDiff(filename)
754 else:
755 if info.get("Node Kind") != "directory":
maruel@chromium.org0836c562010-01-22 01:10:06 +0000756 # svn diff on a mv/cp'd file outputs nothing if there was no change.
757 data = SVN.Capture(command, None)
758 if not data:
759 # We put in an empty Index entry so upload.py knows about them.
maruel@chromium.orgc6d170e2010-06-03 00:06:00 +0000760 data = "Index: %s\n" % filename.replace(os.sep, '/')
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000761 # Otherwise silently ignore directories.
762 else:
763 if info.get("Node Kind") != "directory":
764 # Normal simple case.
maruel@chromium.org0836c562010-01-22 01:10:06 +0000765 data = SVN.Capture(command, None)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000766 # Otherwise silently ignore directories.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000767 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000768
769 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000770 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000771 """Returns a string containing the diff for the given file list.
772
773 The files in the list should either be absolute paths or relative to the
774 given root. If no root directory is provided, the repository root will be
775 used.
776 The diff will always use relative paths.
777 """
778 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000779 root = root or SVN.GetCheckoutRoot(previous_cwd)
780 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000781 def RelativePath(path, root):
782 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000783 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000784 return path[len(root):]
785 return path
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000786 # If the user specified a custom diff command in their svn config file,
787 # then it'll be used when we do svn diff, which we don't want to happen
788 # since we want the unified diff. Using --diff-cmd=diff doesn't always
789 # work, since they can have another diff executable in their path that
790 # gives different line endings. So we use a bogus temp directory as the
791 # config directory, which gets around these problems.
792 bogus_dir = tempfile.mkdtemp()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000793 try:
794 os.chdir(root)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000795 # Cleanup filenames
796 filenames = [RelativePath(f, root) for f in filenames]
797 # Get information about the modified items (files and directories)
798 data = dict([(f, SVN.CaptureInfo(f)) for f in filenames])
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000799 diffs = []
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000800 if full_move:
801 # Eliminate modified files inside moved/copied directory.
802 for (filename, info) in data.iteritems():
803 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
804 # Remove files inside the directory.
805 filenames = [f for f in filenames
806 if not f.startswith(filename + os.path.sep)]
807 for filename in data.keys():
808 if not filename in filenames:
809 # Remove filtered out items.
810 del data[filename]
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000811 else:
812 metaheaders = []
813 for (filename, info) in data.iteritems():
814 if SVN.IsMovedInfo(info):
815 # for now, the most common case is a head copy,
816 # so let's just encode that as a straight up cp.
817 srcurl = info.get('Copied From URL')
818 root = info.get('Repository Root')
819 rev = int(info.get('Copied From Rev'))
820 assert srcurl.startswith(root)
821 src = srcurl[len(root)+1:]
822 srcinfo = SVN.CaptureInfo(srcurl)
823 if (srcinfo.get('Revision') != rev and
824 SVN.Capture(['diff', '-r', '%d:head' % rev, srcurl])):
825 metaheaders.append("#$ svn cp -r %d %s %s "
826 "### WARNING: note non-trunk copy\n" %
827 (rev, src, filename))
828 else:
829 metaheaders.append("#$ cp %s %s\n" % (src,
830 filename))
831
832 if metaheaders:
833 diffs.append("### BEGIN SVN COPY METADATA\n")
834 diffs.extend(metaheaders)
835 diffs.append("### END SVN COPY METADATA\n")
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000836 # Now ready to do the actual diff.
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000837 for filename in sorted(data.iterkeys()):
838 diffs.append(SVN._DiffItemInternal(filename, data[filename], bogus_dir,
839 full_move=full_move,
840 revision=revision))
841 # Use StringIO since it can be messy when diffing a directory move with
842 # full_move=True.
843 buf = cStringIO.StringIO()
844 for d in filter(None, diffs):
845 buf.write(d)
846 result = buf.getvalue()
847 buf.close()
848 return result
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000849 finally:
850 os.chdir(previous_cwd)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000851 shutil.rmtree(bogus_dir)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000852
853 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000854 def GetEmail(repo_root):
855 """Retrieves the svn account which we assume is an email address."""
856 infos = SVN.CaptureInfo(repo_root)
857 uuid = infos.get('UUID')
858 root = infos.get('Repository Root')
859 if not root:
860 return None
861
862 # Should check for uuid but it is incorrectly saved for https creds.
863 realm = root.rsplit('/', 1)[0]
864 if root.startswith('https') or not uuid:
865 regexp = re.compile(r'<%s:\d+>.*' % realm)
866 else:
867 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
868 if regexp is None:
869 return None
870 if sys.platform.startswith('win'):
871 if not 'APPDATA' in os.environ:
872 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000873 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
874 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000875 else:
876 if not 'HOME' in os.environ:
877 return None
878 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
879 'svn.simple')
880 for credfile in os.listdir(auth_dir):
881 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
882 if regexp.match(cred_info.get('svn:realmstring')):
883 return cred_info.get('username')
884
885 @staticmethod
886 def ReadSimpleAuth(filename):
887 f = open(filename, 'r')
888 values = {}
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000889 def ReadOneItem(item_type):
890 m = re.match(r'%s (\d+)' % item_type, f.readline())
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000891 if not m:
892 return None
893 data = f.read(int(m.group(1)))
894 if f.read(1) != '\n':
895 return None
896 return data
897
898 while True:
899 key = ReadOneItem('K')
900 if not key:
901 break
902 value = ReadOneItem('V')
903 if not value:
904 break
905 values[key] = value
906 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000907
908 @staticmethod
909 def GetCheckoutRoot(directory):
910 """Returns the top level directory of the current repository.
911
912 The directory is returned as an absolute path.
913 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000914 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000915 infos = SVN.CaptureInfo(directory, print_error=False)
916 cur_dir_repo_root = infos.get("Repository Root")
917 if not cur_dir_repo_root:
918 return None
919
920 while True:
921 parent = os.path.dirname(directory)
922 if (SVN.CaptureInfo(parent, print_error=False).get(
923 "Repository Root") != cur_dir_repo_root):
924 break
925 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000926 return GetCasedPath(directory)
tony@chromium.org57564662010-04-14 02:35:12 +0000927
928 @staticmethod
929 def AssertVersion(min_version):
930 """Asserts svn's version is at least min_version."""
931 def only_int(val):
932 if val.isdigit():
933 return int(val)
934 else:
935 return 0
936 if not SVN.current_version:
937 SVN.current_version = SVN.Capture(['--version']).split()[2]
938 current_version_list = map(only_int, SVN.current_version.split('.'))
939 for min_ver in map(int, min_version.split('.')):
940 ver = current_version_list.pop(0)
941 if ver < min_ver:
942 return (False, SVN.current_version)
943 elif ver > min_ver:
944 return (True, SVN.current_version)
945 return (True, SVN.current_version)