blob: 1f242191999604c48c6563637db38eff38cfc3d3 [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
maruel@chromium.orgf61fc932010-08-19 13:05:24 +0000454 x.startswith('svn: Unknown hostname') or
455 x.startswith('svn: Server sent unexpected return value')):
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000456 return True
457 return False
458
maruel@chromium.org953586a2010-06-15 14:22:24 +0000459 # Subversion client is really misbehaving with Google Code.
460 if args[0] == 'checkout':
461 # Ensure at least one file was checked out, otherwise *delete* the
462 # directory.
463 if len(file_list) == previous_list_len:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000464 if not IsKnownFailure():
maruel@chromium.org953586a2010-06-15 14:22:24 +0000465 # No known svn error was found, bail out.
466 raise
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000467 # No file were checked out, so make sure the directory is
468 # deleted in case it's messed up and try again.
469 # Warning: It's bad, it assumes args[2] is the directory
470 # argument.
471 if os.path.isdir(args[2]):
472 gclient_utils.RemoveDirectory(args[2])
maruel@chromium.org953586a2010-06-15 14:22:24 +0000473 else:
474 # Progress was made, convert to update since an aborted checkout
475 # is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000476 args = ['update'] + args[1:]
maruel@chromium.org953586a2010-06-15 14:22:24 +0000477 else:
478 # It was an update or export.
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000479 # We enforce that some progress has been made or a known failure.
480 if len(file_list) == previous_list_len and not IsKnownFailure():
481 # No known svn error was found and no progress, bail out.
482 raise
maruel@chromium.org953586a2010-06-15 14:22:24 +0000483 print "Sleeping 15 seconds and retrying...."
484 time.sleep(15)
485 continue
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000486 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000487
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000488 @staticmethod
489 def RunAndFilterOutput(args,
490 in_directory,
491 print_messages,
492 print_stdout,
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000493 filter_fn):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000494 """Runs a command, optionally outputting to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000495
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000496 stdout is passed line-by-line to the given filter_fn function. If
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000497 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000498
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000499 Args:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000500 args: A sequence of command line parameters to be passed.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000501 in_directory: The directory where svn is to be run.
502 print_messages: Whether to print status messages to stdout about
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000503 which commands are being run.
504 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000505 filter_fn: A function taking one argument (a string) which will be
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000506 passed each line (with the ending newline character removed) of
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000507 program's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000508
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000509 Raises:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000510 gclient_utils.Error: An error occurred while running the command.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000511 """
512 command = [SVN.COMMAND]
513 command.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000514 gclient_utils.SubprocessCallAndFilter(command,
515 in_directory,
516 print_messages,
517 print_stdout,
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000518 filter_fn=filter_fn)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000519
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000520 @staticmethod
521 def CaptureInfo(relpath, in_directory=None, print_error=True):
522 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000523
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000524 Args:
525 relpath: The directory where the working copy resides relative to
526 the directory given by in_directory.
527 in_directory: The directory where svn is to be run.
528 """
529 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
530 dom = gclient_utils.ParseXML(output)
531 result = {}
532 if dom:
533 GetNamedNodeText = gclient_utils.GetNamedNodeText
534 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
535 def C(item, f):
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000536 if item is not None:
537 return f(item)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000538 # /info/entry/
539 # url
540 # reposityory/(root|uuid)
541 # wc-info/(schedule|depth)
542 # commit/(author|date)
543 # str() the results because they may be returned as Unicode, which
544 # interferes with the higher layers matching up things in the deps
545 # dictionary.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000546 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
547 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
548 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
549 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
550 'revision'),
551 int)
552 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
553 str)
554 # Differs across versions.
555 if result['Node Kind'] == 'dir':
556 result['Node Kind'] = 'directory'
557 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
558 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
559 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
560 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
561 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000562
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000563 @staticmethod
564 def CaptureHeadRevision(url):
565 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000566
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000567 Returns:
568 Int head revision
569 """
570 info = SVN.Capture(["info", "--xml", url], os.getcwd())
571 dom = xml.dom.minidom.parseString(info)
572 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000573
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000574 @staticmethod
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000575 def CaptureBaseRevision(cwd):
576 """Get the base revision of a SVN repository.
577
578 Returns:
579 Int base revision
580 """
581 info = SVN.Capture(["info", "--xml"], cwd)
582 dom = xml.dom.minidom.parseString(info)
583 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
584
585 @staticmethod
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000586 def CaptureStatus(files):
587 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000588
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000589 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000590
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000591 Returns an array of (status, file) tuples."""
592 command = ["status", "--xml"]
593 if not files:
594 pass
595 elif isinstance(files, basestring):
596 command.append(files)
597 else:
598 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000599
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000600 status_letter = {
601 None: ' ',
602 '': ' ',
603 'added': 'A',
604 'conflicted': 'C',
605 'deleted': 'D',
606 'external': 'X',
607 'ignored': 'I',
608 'incomplete': '!',
609 'merged': 'G',
610 'missing': '!',
611 'modified': 'M',
612 'none': ' ',
613 'normal': ' ',
614 'obstructed': '~',
615 'replaced': 'R',
616 'unversioned': '?',
617 }
618 dom = gclient_utils.ParseXML(SVN.Capture(command))
619 results = []
620 if dom:
621 # /status/target/entry/(wc-status|commit|author|date)
622 for target in dom.getElementsByTagName('target'):
623 #base_path = target.getAttribute('path')
624 for entry in target.getElementsByTagName('entry'):
625 file_path = entry.getAttribute('path')
626 wc_status = entry.getElementsByTagName('wc-status')
627 assert len(wc_status) == 1
628 # Emulate svn 1.5 status ouput...
629 statuses = [' '] * 7
630 # Col 0
631 xml_item_status = wc_status[0].getAttribute('item')
632 if xml_item_status in status_letter:
633 statuses[0] = status_letter[xml_item_status]
634 else:
635 raise Exception('Unknown item status "%s"; please implement me!' %
636 xml_item_status)
637 # Col 1
638 xml_props_status = wc_status[0].getAttribute('props')
639 if xml_props_status == 'modified':
640 statuses[1] = 'M'
641 elif xml_props_status == 'conflicted':
642 statuses[1] = 'C'
643 elif (not xml_props_status or xml_props_status == 'none' or
644 xml_props_status == 'normal'):
645 pass
646 else:
647 raise Exception('Unknown props status "%s"; please implement me!' %
648 xml_props_status)
649 # Col 2
650 if wc_status[0].getAttribute('wc-locked') == 'true':
651 statuses[2] = 'L'
652 # Col 3
653 if wc_status[0].getAttribute('copied') == 'true':
654 statuses[3] = '+'
655 # Col 4
656 if wc_status[0].getAttribute('switched') == 'true':
657 statuses[4] = 'S'
658 # TODO(maruel): Col 5 and 6
659 item = (''.join(statuses), file_path)
660 results.append(item)
661 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000662
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000663 @staticmethod
664 def IsMoved(filename):
665 """Determine if a file has been added through svn mv"""
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000666 return SVN.IsMovedInfo(SVN.CaptureInfo(filename))
667
668 @staticmethod
669 def IsMovedInfo(info):
670 """Determine if a file has been added through svn mv"""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000671 return (info.get('Copied From URL') and
672 info.get('Copied From Rev') and
673 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000674
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000675 @staticmethod
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000676 def GetFileProperty(filename, property_name):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000677 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000678
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000679 Args:
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000680 filename: The file to check
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000681 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000682
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000683 Returns:
684 The value of the property, which will be the empty string if the property
685 is not set on the file. If the file is not under version control, the
686 empty string is also returned.
687 """
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000688 output = SVN.Capture(["propget", property_name, filename])
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000689 if (output.startswith("svn: ") and
690 output.endswith("is not under version control")):
691 return ""
692 else:
693 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000694
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000695 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000696 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000697 """Diffs a single file.
698
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000699 Should be simple, eh? No it isn't.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000700 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000701 expected relative path.
702 full_move means that move or copy operations should completely recreate the
703 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000704 # If the user specified a custom diff command in their svn config file,
705 # then it'll be used when we do svn diff, which we don't want to happen
706 # since we want the unified diff. Using --diff-cmd=diff doesn't always
707 # work, since they can have another diff executable in their path that
708 # gives different line endings. So we use a bogus temp directory as the
709 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000710 bogus_dir = tempfile.mkdtemp()
711 try:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000712 # Use "svn info" output instead of os.path.isdir because the latter fails
713 # when the file is deleted.
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000714 return SVN._DiffItemInternal(filename, SVN.CaptureInfo(filename),
715 bogus_dir,
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000716 full_move=full_move, revision=revision)
717 finally:
718 shutil.rmtree(bogus_dir)
719
720 @staticmethod
721 def _DiffItemInternal(filename, info, bogus_dir, full_move=False,
722 revision=None):
723 """Grabs the diff data."""
724 command = ["diff", "--config-dir", bogus_dir, filename]
725 if revision:
726 command.extend(['--revision', revision])
727 data = None
728 if SVN.IsMovedInfo(info):
729 if full_move:
730 if info.get("Node Kind") == "directory":
731 # Things become tricky here. It's a directory copy/move. We need to
732 # diff all the files inside it.
733 # This will put a lot of pressure on the heap. This is why StringIO
734 # is used and converted back into a string at the end. The reason to
735 # return a string instead of a StringIO is that StringIO.write()
736 # doesn't accept a StringIO object. *sigh*.
737 for (dirpath, dirnames, filenames) in os.walk(filename):
738 # Cleanup all files starting with a '.'.
739 for d in dirnames:
740 if d.startswith('.'):
741 dirnames.remove(d)
742 for f in filenames:
743 if f.startswith('.'):
744 filenames.remove(f)
745 for f in filenames:
746 if data is None:
747 data = cStringIO.StringIO()
748 data.write(GenFakeDiff(os.path.join(dirpath, f)))
749 if data:
750 tmp = data.getvalue()
751 data.close()
752 data = tmp
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000753 else:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000754 data = GenFakeDiff(filename)
755 else:
756 if info.get("Node Kind") != "directory":
maruel@chromium.org0836c562010-01-22 01:10:06 +0000757 # svn diff on a mv/cp'd file outputs nothing if there was no change.
758 data = SVN.Capture(command, None)
759 if not data:
760 # We put in an empty Index entry so upload.py knows about them.
maruel@chromium.orgc6d170e2010-06-03 00:06:00 +0000761 data = "Index: %s\n" % filename.replace(os.sep, '/')
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000762 # Otherwise silently ignore directories.
763 else:
764 if info.get("Node Kind") != "directory":
765 # Normal simple case.
maruel@chromium.org0836c562010-01-22 01:10:06 +0000766 data = SVN.Capture(command, None)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000767 # Otherwise silently ignore directories.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000768 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000769
770 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000771 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000772 """Returns a string containing the diff for the given file list.
773
774 The files in the list should either be absolute paths or relative to the
775 given root. If no root directory is provided, the repository root will be
776 used.
777 The diff will always use relative paths.
778 """
779 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000780 root = root or SVN.GetCheckoutRoot(previous_cwd)
781 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000782 def RelativePath(path, root):
783 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000784 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000785 return path[len(root):]
786 return path
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000787 # If the user specified a custom diff command in their svn config file,
788 # then it'll be used when we do svn diff, which we don't want to happen
789 # since we want the unified diff. Using --diff-cmd=diff doesn't always
790 # work, since they can have another diff executable in their path that
791 # gives different line endings. So we use a bogus temp directory as the
792 # config directory, which gets around these problems.
793 bogus_dir = tempfile.mkdtemp()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000794 try:
795 os.chdir(root)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000796 # Cleanup filenames
797 filenames = [RelativePath(f, root) for f in filenames]
798 # Get information about the modified items (files and directories)
799 data = dict([(f, SVN.CaptureInfo(f)) for f in filenames])
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000800 diffs = []
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000801 if full_move:
802 # Eliminate modified files inside moved/copied directory.
803 for (filename, info) in data.iteritems():
804 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
805 # Remove files inside the directory.
806 filenames = [f for f in filenames
807 if not f.startswith(filename + os.path.sep)]
808 for filename in data.keys():
809 if not filename in filenames:
810 # Remove filtered out items.
811 del data[filename]
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000812 else:
813 metaheaders = []
814 for (filename, info) in data.iteritems():
815 if SVN.IsMovedInfo(info):
816 # for now, the most common case is a head copy,
817 # so let's just encode that as a straight up cp.
818 srcurl = info.get('Copied From URL')
819 root = info.get('Repository Root')
820 rev = int(info.get('Copied From Rev'))
821 assert srcurl.startswith(root)
822 src = srcurl[len(root)+1:]
823 srcinfo = SVN.CaptureInfo(srcurl)
824 if (srcinfo.get('Revision') != rev and
825 SVN.Capture(['diff', '-r', '%d:head' % rev, srcurl])):
826 metaheaders.append("#$ svn cp -r %d %s %s "
827 "### WARNING: note non-trunk copy\n" %
828 (rev, src, filename))
829 else:
830 metaheaders.append("#$ cp %s %s\n" % (src,
831 filename))
832
833 if metaheaders:
834 diffs.append("### BEGIN SVN COPY METADATA\n")
835 diffs.extend(metaheaders)
836 diffs.append("### END SVN COPY METADATA\n")
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000837 # Now ready to do the actual diff.
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000838 for filename in sorted(data.iterkeys()):
839 diffs.append(SVN._DiffItemInternal(filename, data[filename], bogus_dir,
840 full_move=full_move,
841 revision=revision))
842 # Use StringIO since it can be messy when diffing a directory move with
843 # full_move=True.
844 buf = cStringIO.StringIO()
845 for d in filter(None, diffs):
846 buf.write(d)
847 result = buf.getvalue()
848 buf.close()
849 return result
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000850 finally:
851 os.chdir(previous_cwd)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000852 shutil.rmtree(bogus_dir)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000853
854 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000855 def GetEmail(repo_root):
856 """Retrieves the svn account which we assume is an email address."""
857 infos = SVN.CaptureInfo(repo_root)
858 uuid = infos.get('UUID')
859 root = infos.get('Repository Root')
860 if not root:
861 return None
862
863 # Should check for uuid but it is incorrectly saved for https creds.
864 realm = root.rsplit('/', 1)[0]
865 if root.startswith('https') or not uuid:
866 regexp = re.compile(r'<%s:\d+>.*' % realm)
867 else:
868 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
869 if regexp is None:
870 return None
871 if sys.platform.startswith('win'):
872 if not 'APPDATA' in os.environ:
873 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000874 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
875 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000876 else:
877 if not 'HOME' in os.environ:
878 return None
879 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
880 'svn.simple')
881 for credfile in os.listdir(auth_dir):
882 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
883 if regexp.match(cred_info.get('svn:realmstring')):
884 return cred_info.get('username')
885
886 @staticmethod
887 def ReadSimpleAuth(filename):
888 f = open(filename, 'r')
889 values = {}
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000890 def ReadOneItem(item_type):
891 m = re.match(r'%s (\d+)' % item_type, f.readline())
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000892 if not m:
893 return None
894 data = f.read(int(m.group(1)))
895 if f.read(1) != '\n':
896 return None
897 return data
898
899 while True:
900 key = ReadOneItem('K')
901 if not key:
902 break
903 value = ReadOneItem('V')
904 if not value:
905 break
906 values[key] = value
907 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000908
909 @staticmethod
910 def GetCheckoutRoot(directory):
911 """Returns the top level directory of the current repository.
912
913 The directory is returned as an absolute path.
914 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000915 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000916 infos = SVN.CaptureInfo(directory, print_error=False)
917 cur_dir_repo_root = infos.get("Repository Root")
918 if not cur_dir_repo_root:
919 return None
920
921 while True:
922 parent = os.path.dirname(directory)
923 if (SVN.CaptureInfo(parent, print_error=False).get(
924 "Repository Root") != cur_dir_repo_root):
925 break
926 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000927 return GetCasedPath(directory)
tony@chromium.org57564662010-04-14 02:35:12 +0000928
929 @staticmethod
930 def AssertVersion(min_version):
931 """Asserts svn's version is at least min_version."""
932 def only_int(val):
933 if val.isdigit():
934 return int(val)
935 else:
936 return 0
937 if not SVN.current_version:
938 SVN.current_version = SVN.Capture(['--version']).split()[2]
939 current_version_list = map(only_int, SVN.current_version.split('.'))
940 for min_ver in map(int, min_version.split('.')):
941 ver = current_version_list.pop(0)
942 if ver < min_ver:
943 return (False, SVN.current_version)
944 elif ver > min_ver:
945 return (True, SVN.current_version)
946 return (True, SVN.current_version)