blob: 9fb03833ffa79d0c58456cf919ecc76dcf964bee [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.org953586a2010-06-15 14:22:24 +0000449 # Subversion client is really misbehaving with Google Code.
450 if args[0] == 'checkout':
451 # Ensure at least one file was checked out, otherwise *delete* the
452 # directory.
453 if len(file_list) == previous_list_len:
454 for x in failure:
455 if ('502 Bad Gateway' in x or
456 'svn: REPORT of \'/svn/!svn/vcc/default\': 200 OK' in x):
457 # No file were checked out, so make sure the directory is
458 # deleted in case it's messed up and try again.
459 # Warning: It's bad, it assumes args[2] is the directory
460 # argument.
461 if os.path.isdir(args[2]):
maruel@chromium.org99793392010-06-18 12:20:21 +0000462 gclient_utils.RemoveDirectory(args[2])
maruel@chromium.org953586a2010-06-15 14:22:24 +0000463 break
464 else:
465 # No known svn error was found, bail out.
466 raise
467 else:
468 # Progress was made, convert to update since an aborted checkout
469 # is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000470 args = ['update'] + args[1:]
maruel@chromium.org953586a2010-06-15 14:22:24 +0000471 else:
472 # It was an update or export.
473 # We enforce that some progress has been made or HTTP 502.
474 if len(file_list) == previous_list_len:
475 for x in failure:
476 if ('502 Bad Gateway' in x or
477 'svn: REPORT of \'/svn/!svn/vcc/default\': 200 OK' in x):
478 # Ok, know failure code.
479 break
480 else:
481 # No known svn error was found, bail out.
482 raise
483 else:
484 # Progress was made, it's fine.
485 pass
486 print "Sleeping 15 seconds and retrying...."
487 time.sleep(15)
488 continue
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000489 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000490
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000491 @staticmethod
492 def RunAndFilterOutput(args,
493 in_directory,
494 print_messages,
495 print_stdout,
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000496 filter_fn):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000497 """Runs a command, optionally outputting to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000498
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000499 stdout is passed line-by-line to the given filter_fn function. If
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000500 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000501
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000502 Args:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000503 args: A sequence of command line parameters to be passed.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000504 in_directory: The directory where svn is to be run.
505 print_messages: Whether to print status messages to stdout about
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000506 which commands are being run.
507 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000508 filter_fn: A function taking one argument (a string) which will be
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000509 passed each line (with the ending newline character removed) of
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000510 program's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000511
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000512 Raises:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000513 gclient_utils.Error: An error occurred while running the command.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000514 """
515 command = [SVN.COMMAND]
516 command.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000517 gclient_utils.SubprocessCallAndFilter(command,
518 in_directory,
519 print_messages,
520 print_stdout,
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000521 filter_fn=filter_fn)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000522
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000523 @staticmethod
524 def CaptureInfo(relpath, in_directory=None, print_error=True):
525 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000526
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000527 Args:
528 relpath: The directory where the working copy resides relative to
529 the directory given by in_directory.
530 in_directory: The directory where svn is to be run.
531 """
532 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
533 dom = gclient_utils.ParseXML(output)
534 result = {}
535 if dom:
536 GetNamedNodeText = gclient_utils.GetNamedNodeText
537 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
538 def C(item, f):
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000539 if item is not None:
540 return f(item)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000541 # /info/entry/
542 # url
543 # reposityory/(root|uuid)
544 # wc-info/(schedule|depth)
545 # commit/(author|date)
546 # str() the results because they may be returned as Unicode, which
547 # interferes with the higher layers matching up things in the deps
548 # dictionary.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000549 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
550 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
551 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
552 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
553 'revision'),
554 int)
555 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
556 str)
557 # Differs across versions.
558 if result['Node Kind'] == 'dir':
559 result['Node Kind'] = 'directory'
560 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
561 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
562 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
563 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
564 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000565
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000566 @staticmethod
567 def CaptureHeadRevision(url):
568 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000569
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000570 Returns:
571 Int head revision
572 """
573 info = SVN.Capture(["info", "--xml", url], os.getcwd())
574 dom = xml.dom.minidom.parseString(info)
575 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000576
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000577 @staticmethod
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000578 def CaptureBaseRevision(cwd):
579 """Get the base revision of a SVN repository.
580
581 Returns:
582 Int base revision
583 """
584 info = SVN.Capture(["info", "--xml"], cwd)
585 dom = xml.dom.minidom.parseString(info)
586 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
587
588 @staticmethod
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000589 def CaptureStatus(files):
590 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000591
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000592 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000593
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000594 Returns an array of (status, file) tuples."""
595 command = ["status", "--xml"]
596 if not files:
597 pass
598 elif isinstance(files, basestring):
599 command.append(files)
600 else:
601 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000602
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000603 status_letter = {
604 None: ' ',
605 '': ' ',
606 'added': 'A',
607 'conflicted': 'C',
608 'deleted': 'D',
609 'external': 'X',
610 'ignored': 'I',
611 'incomplete': '!',
612 'merged': 'G',
613 'missing': '!',
614 'modified': 'M',
615 'none': ' ',
616 'normal': ' ',
617 'obstructed': '~',
618 'replaced': 'R',
619 'unversioned': '?',
620 }
621 dom = gclient_utils.ParseXML(SVN.Capture(command))
622 results = []
623 if dom:
624 # /status/target/entry/(wc-status|commit|author|date)
625 for target in dom.getElementsByTagName('target'):
626 #base_path = target.getAttribute('path')
627 for entry in target.getElementsByTagName('entry'):
628 file_path = entry.getAttribute('path')
629 wc_status = entry.getElementsByTagName('wc-status')
630 assert len(wc_status) == 1
631 # Emulate svn 1.5 status ouput...
632 statuses = [' '] * 7
633 # Col 0
634 xml_item_status = wc_status[0].getAttribute('item')
635 if xml_item_status in status_letter:
636 statuses[0] = status_letter[xml_item_status]
637 else:
638 raise Exception('Unknown item status "%s"; please implement me!' %
639 xml_item_status)
640 # Col 1
641 xml_props_status = wc_status[0].getAttribute('props')
642 if xml_props_status == 'modified':
643 statuses[1] = 'M'
644 elif xml_props_status == 'conflicted':
645 statuses[1] = 'C'
646 elif (not xml_props_status or xml_props_status == 'none' or
647 xml_props_status == 'normal'):
648 pass
649 else:
650 raise Exception('Unknown props status "%s"; please implement me!' %
651 xml_props_status)
652 # Col 2
653 if wc_status[0].getAttribute('wc-locked') == 'true':
654 statuses[2] = 'L'
655 # Col 3
656 if wc_status[0].getAttribute('copied') == 'true':
657 statuses[3] = '+'
658 # Col 4
659 if wc_status[0].getAttribute('switched') == 'true':
660 statuses[4] = 'S'
661 # TODO(maruel): Col 5 and 6
662 item = (''.join(statuses), file_path)
663 results.append(item)
664 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000665
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000666 @staticmethod
667 def IsMoved(filename):
668 """Determine if a file has been added through svn mv"""
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000669 return SVN.IsMovedInfo(SVN.CaptureInfo(filename))
670
671 @staticmethod
672 def IsMovedInfo(info):
673 """Determine if a file has been added through svn mv"""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000674 return (info.get('Copied From URL') and
675 info.get('Copied From Rev') and
676 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000677
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000678 @staticmethod
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000679 def GetFileProperty(filename, property_name):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000680 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000681
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000682 Args:
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000683 filename: The file to check
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000684 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000685
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000686 Returns:
687 The value of the property, which will be the empty string if the property
688 is not set on the file. If the file is not under version control, the
689 empty string is also returned.
690 """
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000691 output = SVN.Capture(["propget", property_name, filename])
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000692 if (output.startswith("svn: ") and
693 output.endswith("is not under version control")):
694 return ""
695 else:
696 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000697
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000698 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000699 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000700 """Diffs a single file.
701
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000702 Should be simple, eh? No it isn't.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000703 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000704 expected relative path.
705 full_move means that move or copy operations should completely recreate the
706 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000707 # If the user specified a custom diff command in their svn config file,
708 # then it'll be used when we do svn diff, which we don't want to happen
709 # since we want the unified diff. Using --diff-cmd=diff doesn't always
710 # work, since they can have another diff executable in their path that
711 # gives different line endings. So we use a bogus temp directory as the
712 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000713 bogus_dir = tempfile.mkdtemp()
714 try:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000715 # Use "svn info" output instead of os.path.isdir because the latter fails
716 # when the file is deleted.
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000717 return SVN._DiffItemInternal(filename, SVN.CaptureInfo(filename),
718 bogus_dir,
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000719 full_move=full_move, revision=revision)
720 finally:
721 shutil.rmtree(bogus_dir)
722
723 @staticmethod
724 def _DiffItemInternal(filename, info, bogus_dir, full_move=False,
725 revision=None):
726 """Grabs the diff data."""
727 command = ["diff", "--config-dir", bogus_dir, filename]
728 if revision:
729 command.extend(['--revision', revision])
730 data = None
731 if SVN.IsMovedInfo(info):
732 if full_move:
733 if info.get("Node Kind") == "directory":
734 # Things become tricky here. It's a directory copy/move. We need to
735 # diff all the files inside it.
736 # This will put a lot of pressure on the heap. This is why StringIO
737 # is used and converted back into a string at the end. The reason to
738 # return a string instead of a StringIO is that StringIO.write()
739 # doesn't accept a StringIO object. *sigh*.
740 for (dirpath, dirnames, filenames) in os.walk(filename):
741 # Cleanup all files starting with a '.'.
742 for d in dirnames:
743 if d.startswith('.'):
744 dirnames.remove(d)
745 for f in filenames:
746 if f.startswith('.'):
747 filenames.remove(f)
748 for f in filenames:
749 if data is None:
750 data = cStringIO.StringIO()
751 data.write(GenFakeDiff(os.path.join(dirpath, f)))
752 if data:
753 tmp = data.getvalue()
754 data.close()
755 data = tmp
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000756 else:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000757 data = GenFakeDiff(filename)
758 else:
759 if info.get("Node Kind") != "directory":
maruel@chromium.org0836c562010-01-22 01:10:06 +0000760 # svn diff on a mv/cp'd file outputs nothing if there was no change.
761 data = SVN.Capture(command, None)
762 if not data:
763 # We put in an empty Index entry so upload.py knows about them.
maruel@chromium.orgc6d170e2010-06-03 00:06:00 +0000764 data = "Index: %s\n" % filename.replace(os.sep, '/')
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000765 # Otherwise silently ignore directories.
766 else:
767 if info.get("Node Kind") != "directory":
768 # Normal simple case.
maruel@chromium.org0836c562010-01-22 01:10:06 +0000769 data = SVN.Capture(command, None)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000770 # Otherwise silently ignore directories.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000771 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000772
773 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000774 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000775 """Returns a string containing the diff for the given file list.
776
777 The files in the list should either be absolute paths or relative to the
778 given root. If no root directory is provided, the repository root will be
779 used.
780 The diff will always use relative paths.
781 """
782 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000783 root = root or SVN.GetCheckoutRoot(previous_cwd)
784 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000785 def RelativePath(path, root):
786 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000787 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000788 return path[len(root):]
789 return path
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000790 # If the user specified a custom diff command in their svn config file,
791 # then it'll be used when we do svn diff, which we don't want to happen
792 # since we want the unified diff. Using --diff-cmd=diff doesn't always
793 # work, since they can have another diff executable in their path that
794 # gives different line endings. So we use a bogus temp directory as the
795 # config directory, which gets around these problems.
796 bogus_dir = tempfile.mkdtemp()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000797 try:
798 os.chdir(root)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000799 # Cleanup filenames
800 filenames = [RelativePath(f, root) for f in filenames]
801 # Get information about the modified items (files and directories)
802 data = dict([(f, SVN.CaptureInfo(f)) for f in filenames])
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000803 diffs = []
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000804 if full_move:
805 # Eliminate modified files inside moved/copied directory.
806 for (filename, info) in data.iteritems():
807 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
808 # Remove files inside the directory.
809 filenames = [f for f in filenames
810 if not f.startswith(filename + os.path.sep)]
811 for filename in data.keys():
812 if not filename in filenames:
813 # Remove filtered out items.
814 del data[filename]
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000815 else:
816 metaheaders = []
817 for (filename, info) in data.iteritems():
818 if SVN.IsMovedInfo(info):
819 # for now, the most common case is a head copy,
820 # so let's just encode that as a straight up cp.
821 srcurl = info.get('Copied From URL')
822 root = info.get('Repository Root')
823 rev = int(info.get('Copied From Rev'))
824 assert srcurl.startswith(root)
825 src = srcurl[len(root)+1:]
826 srcinfo = SVN.CaptureInfo(srcurl)
827 if (srcinfo.get('Revision') != rev and
828 SVN.Capture(['diff', '-r', '%d:head' % rev, srcurl])):
829 metaheaders.append("#$ svn cp -r %d %s %s "
830 "### WARNING: note non-trunk copy\n" %
831 (rev, src, filename))
832 else:
833 metaheaders.append("#$ cp %s %s\n" % (src,
834 filename))
835
836 if metaheaders:
837 diffs.append("### BEGIN SVN COPY METADATA\n")
838 diffs.extend(metaheaders)
839 diffs.append("### END SVN COPY METADATA\n")
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000840 # Now ready to do the actual diff.
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000841 for filename in sorted(data.iterkeys()):
842 diffs.append(SVN._DiffItemInternal(filename, data[filename], bogus_dir,
843 full_move=full_move,
844 revision=revision))
845 # Use StringIO since it can be messy when diffing a directory move with
846 # full_move=True.
847 buf = cStringIO.StringIO()
848 for d in filter(None, diffs):
849 buf.write(d)
850 result = buf.getvalue()
851 buf.close()
852 return result
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000853 finally:
854 os.chdir(previous_cwd)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000855 shutil.rmtree(bogus_dir)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000856
857 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000858 def GetEmail(repo_root):
859 """Retrieves the svn account which we assume is an email address."""
860 infos = SVN.CaptureInfo(repo_root)
861 uuid = infos.get('UUID')
862 root = infos.get('Repository Root')
863 if not root:
864 return None
865
866 # Should check for uuid but it is incorrectly saved for https creds.
867 realm = root.rsplit('/', 1)[0]
868 if root.startswith('https') or not uuid:
869 regexp = re.compile(r'<%s:\d+>.*' % realm)
870 else:
871 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
872 if regexp is None:
873 return None
874 if sys.platform.startswith('win'):
875 if not 'APPDATA' in os.environ:
876 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000877 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
878 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000879 else:
880 if not 'HOME' in os.environ:
881 return None
882 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
883 'svn.simple')
884 for credfile in os.listdir(auth_dir):
885 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
886 if regexp.match(cred_info.get('svn:realmstring')):
887 return cred_info.get('username')
888
889 @staticmethod
890 def ReadSimpleAuth(filename):
891 f = open(filename, 'r')
892 values = {}
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000893 def ReadOneItem(item_type):
894 m = re.match(r'%s (\d+)' % item_type, f.readline())
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000895 if not m:
896 return None
897 data = f.read(int(m.group(1)))
898 if f.read(1) != '\n':
899 return None
900 return data
901
902 while True:
903 key = ReadOneItem('K')
904 if not key:
905 break
906 value = ReadOneItem('V')
907 if not value:
908 break
909 values[key] = value
910 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000911
912 @staticmethod
913 def GetCheckoutRoot(directory):
914 """Returns the top level directory of the current repository.
915
916 The directory is returned as an absolute path.
917 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000918 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000919 infos = SVN.CaptureInfo(directory, print_error=False)
920 cur_dir_repo_root = infos.get("Repository Root")
921 if not cur_dir_repo_root:
922 return None
923
924 while True:
925 parent = os.path.dirname(directory)
926 if (SVN.CaptureInfo(parent, print_error=False).get(
927 "Repository Root") != cur_dir_repo_root):
928 break
929 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000930 return GetCasedPath(directory)
tony@chromium.org57564662010-04-14 02:35:12 +0000931
932 @staticmethod
933 def AssertVersion(min_version):
934 """Asserts svn's version is at least min_version."""
935 def only_int(val):
936 if val.isdigit():
937 return int(val)
938 else:
939 return 0
940 if not SVN.current_version:
941 SVN.current_version = SVN.Capture(['--version']).split()[2]
942 current_version_list = map(only_int, SVN.current_version.split('.'))
943 for min_ver in map(int, min_version.split('.')):
944 ver = current_version_list.pop(0)
945 if ver < min_ver:
946 return (False, SVN.current_version)
947 elif ver > min_ver:
948 return (True, SVN.current_version)
949 return (True, SVN.current_version)