blob: ae765f0cd05b106ffc6532236574328ac625cb4f [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.org2b9aa8e2010-08-25 20:01:42 +0000120 def RunAndFilterOutput(args, **kwargs):
121 """Wrapper to gclient_utils.SubprocessCallAndFilter()."""
122 return gclient_utils.SubprocessCallAndFilter([GIT.COMMAND] + args, **kwargs)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000123
124 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000125 def GetEmail(repo_root):
126 """Retrieves the user email address if known."""
127 # We could want to look at the svn cred when it has a svn remote but it
128 # should be fine for now, users should simply configure their git settings.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000129 return GIT.Capture(['config', 'user.email'],
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000130 repo_root, error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000131
132 @staticmethod
133 def ShortBranchName(branch):
134 """Converts a name like 'refs/heads/foo' to just 'foo'."""
135 return branch.replace('refs/heads/', '')
136
137 @staticmethod
138 def GetBranchRef(cwd):
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000139 """Returns the full branch reference, e.g. 'refs/heads/master'."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000140 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000141
142 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000143 def GetBranch(cwd):
144 """Returns the short branch name, e.g. 'master'."""
maruel@chromium.orgc308a742009-12-22 18:29:33 +0000145 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000146
147 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000148 def IsGitSvn(cwd):
149 """Returns true if this repo looks like it's using git-svn."""
150 # If you have any "svn-remote.*" config keys, we think you're using svn.
151 try:
152 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd)
153 return True
154 except gclient_utils.CheckCallError:
155 return False
156
157 @staticmethod
158 def GetSVNBranch(cwd):
159 """Returns the svn branch name if found."""
160 # Try to figure out which remote branch we're based on.
161 # Strategy:
162 # 1) find all git-svn branches and note their svn URLs.
163 # 2) iterate through our branch history and match up the URLs.
164
165 # regexp matching the git-svn line that contains the URL.
166 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
167
168 # Get the refname and svn url for all refs/remotes/*.
169 remotes = GIT.Capture(
170 ['for-each-ref', '--format=%(refname)', 'refs/remotes'],
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000171 cwd)[0].splitlines()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000172 svn_refs = {}
173 for ref in remotes:
174 match = git_svn_re.search(
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000175 GIT.Capture(['cat-file', '-p', ref], cwd)[0])
sky@chromium.org42d8da52010-04-23 18:25:07 +0000176 # Prefer origin/HEAD over all others.
177 if match and (match.group(1) not in svn_refs or
178 ref == "refs/remotes/origin/HEAD"):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000179 svn_refs[match.group(1)] = ref
180
181 svn_branch = ''
182 if len(svn_refs) == 1:
183 # Only one svn branch exists -- seems like a good candidate.
184 svn_branch = svn_refs.values()[0]
185 elif len(svn_refs) > 1:
186 # We have more than one remote branch available. We don't
187 # want to go through all of history, so read a line from the
188 # pipe at a time.
189 # The -100 is an arbitrary limit so we don't search forever.
190 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org3a292682010-08-23 18:54:55 +0000191 proc = gclient_utils.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000192 for line in proc.stdout:
193 match = git_svn_re.match(line)
194 if match:
195 url = match.group(1)
196 if url in svn_refs:
197 svn_branch = svn_refs[url]
198 proc.stdout.close() # Cut pipe.
199 break
200 return svn_branch
201
202 @staticmethod
203 def FetchUpstreamTuple(cwd):
204 """Returns a tuple containg remote and remote ref,
205 e.g. 'origin', 'refs/heads/master'
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000206 Tries to be intelligent and understand git-svn.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000207 """
208 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000209 branch = GIT.GetBranch(cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000210 upstream_branch = None
211 upstream_branch = GIT.Capture(
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000212 ['config', 'branch.%s.merge' % branch], in_directory=cwd,
213 error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000214 if upstream_branch:
215 remote = GIT.Capture(
216 ['config', 'branch.%s.remote' % branch],
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000217 in_directory=cwd, error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000218 else:
219 # Fall back on trying a git-svn upstream branch.
220 if GIT.IsGitSvn(cwd):
221 upstream_branch = GIT.GetSVNBranch(cwd)
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000222 else:
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000223 # Else, try to guess the origin remote.
224 remote_branches = GIT.Capture(
225 ['branch', '-r'], in_directory=cwd)[0].split()
226 if 'origin/master' in remote_branches:
227 # Fall back on origin/master if it exits.
228 remote = 'origin'
229 upstream_branch = 'refs/heads/master'
230 elif 'origin/trunk' in remote_branches:
231 # Fall back on origin/trunk if it exists. Generally a shared
232 # git-svn clone
233 remote = 'origin'
234 upstream_branch = 'refs/heads/trunk'
235 else:
236 # Give up.
237 remote = None
238 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000239 return remote, upstream_branch
240
241 @staticmethod
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000242 def GetUpstreamBranch(cwd):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000243 """Gets the current branch's upstream branch."""
244 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000245 if remote != '.' and upstream_branch:
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000246 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
247 return upstream_branch
248
249 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000250 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
251 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000252 """Diffs against the upstream branch or optionally another branch.
253
254 full_move means that move or copy operations should completely recreate the
255 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000256 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000257 branch = GIT.GetUpstreamBranch(cwd)
evan@chromium.org400f3e72010-05-19 14:23:36 +0000258 command = ['diff', '-p', '--no-prefix', '--no-ext-diff',
259 branch + "..." + branch_head]
maruel@chromium.orga9371762009-12-22 18:27:38 +0000260 if not full_move:
261 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000262 # TODO(maruel): --binary support.
263 if files:
264 command.append('--')
265 command.extend(files)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000266 diff = GIT.Capture(command, cwd)[0].splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000267 for i in range(len(diff)):
268 # In the case of added files, replace /dev/null with the path to the
269 # file being added.
270 if diff[i].startswith('--- /dev/null'):
271 diff[i] = '--- %s' % diff[i+1][4:]
272 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000273
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000274 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000275 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
276 """Returns the list of modified files between two branches."""
277 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000278 branch = GIT.GetUpstreamBranch(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000279 command = ['diff', '--name-only', branch + "..." + branch_head]
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000280 return GIT.Capture(command, cwd)[0].splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000281
282 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000283 def GetPatchName(cwd):
284 """Constructs a name for this patch."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000285 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd)[0].strip()
maruel@chromium.org862ff8e2010-08-06 15:29:16 +0000286 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000287
288 @staticmethod
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000289 def GetCheckoutRoot(path):
290 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000291 """
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000292 root = GIT.Capture(['rev-parse', '--show-cdup'], path)[0].strip()
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000293 return os.path.abspath(os.path.join(path, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000294
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000295 @staticmethod
296 def AssertVersion(min_version):
297 """Asserts git's version is at least min_version."""
298 def only_int(val):
299 if val.isdigit():
300 return int(val)
301 else:
302 return 0
303 current_version = GIT.Capture(['--version'])[0].split()[-1]
304 current_version_list = map(only_int, current_version.split('.'))
305 for min_ver in map(int, min_version.split('.')):
306 ver = current_version_list.pop(0)
307 if ver < min_ver:
308 return (False, current_version)
309 elif ver > min_ver:
310 return (True, current_version)
311 return (True, current_version)
312
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000313
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000314class SVN(object):
315 COMMAND = "svn"
tony@chromium.org57564662010-04-14 02:35:12 +0000316 current_version = None
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000317
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000318 @staticmethod
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000319 def Run(args, **kwargs):
320 """Wrappers to gclient_utils.SubprocessCall()."""
321 return gclient_utils.SubprocessCall([SVN.COMMAND] + args, **kwargs)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000322
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000323 @staticmethod
324 def Capture(args, in_directory=None, print_error=True):
325 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000326
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000327 Args:
328 args: A sequence of command line parameters to be passed to svn.
329 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000330
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000331 Returns:
332 The output sent to stdout as a string.
333 """
334 c = [SVN.COMMAND]
335 c.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000336 stderr = None
337 if not print_error:
338 stderr = subprocess.PIPE
maruel@chromium.org3a292682010-08-23 18:54:55 +0000339 return gclient_utils.Popen(c, cwd=in_directory, stdout=subprocess.PIPE,
340 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000341
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000342 @staticmethod
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000343 def RunAndGetFileList(verbose, args, cwd, file_list, stdout=None):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000344 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000345
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000346 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000347
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000348 svn's stdout is parsed to collect a list of files checked out or updated.
349 These files are appended to file_list. svn's stdout is also printed to
350 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000351
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000352 Args:
maruel@chromium.org03807072010-08-16 17:18:44 +0000353 verbose: If True, uses verbose output
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000354 args: A sequence of command line parameters to be passed to svn.
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000355 cwd: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000356
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000357 Raises:
358 Error: An error occurred while running the svn command.
359 """
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000360 stdout = stdout or sys.stdout
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000361
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000362 # svn update and svn checkout use the same pattern: the first three columns
363 # are for file status, property status, and lock status. This is followed
364 # by two spaces, and then the path to the file.
365 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000366
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000367 # The first three columns of svn status are the same as for svn update and
368 # svn checkout. The next three columns indicate addition-with-history,
369 # switch, and remote lock status. This is followed by one space, and then
370 # the path to the file.
371 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000372
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000373 # args[0] must be a supported command. This will blow up if it's something
374 # else, which is good. Note that the patterns are only effective when
375 # these commands are used in their ordinary forms, the patterns are invalid
376 # for "svn status --show-updates", for example.
377 pattern = {
378 'checkout': update_pattern,
379 'status': status_pattern,
380 'update': update_pattern,
381 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000382 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000383 # Place an upper limit.
maruel@chromium.orgfd876172010-04-30 14:01:05 +0000384 for _ in range(10):
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000385 previous_list_len = len(file_list)
386 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000387
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000388 def CaptureMatchingLines(line):
389 match = compiled_pattern.search(line)
390 if match:
391 file_list.append(match.group(1))
392 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000393 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000394
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000395 try:
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000396 SVN.RunAndFilterOutput(args, cwd=cwd, print_messages=verbose,
397 print_stdout=True,
398 filter_fn=CaptureMatchingLines,
maruel@chromium.org559c3f82010-08-23 19:26:08 +0000399 stdout=stdout)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000400 except gclient_utils.Error:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000401 def IsKnownFailure():
402 for x in failure:
403 if (x.startswith('svn: OPTIONS of') or
404 x.startswith('svn: PROPFIND of') or
405 x.startswith('svn: REPORT of') or
maruel@chromium.orgf61fc932010-08-19 13:05:24 +0000406 x.startswith('svn: Unknown hostname') or
407 x.startswith('svn: Server sent unexpected return value')):
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000408 return True
409 return False
410
maruel@chromium.org953586a2010-06-15 14:22:24 +0000411 # Subversion client is really misbehaving with Google Code.
412 if args[0] == 'checkout':
413 # Ensure at least one file was checked out, otherwise *delete* the
414 # directory.
415 if len(file_list) == previous_list_len:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000416 if not IsKnownFailure():
maruel@chromium.org953586a2010-06-15 14:22:24 +0000417 # No known svn error was found, bail out.
418 raise
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000419 # No file were checked out, so make sure the directory is
420 # deleted in case it's messed up and try again.
421 # Warning: It's bad, it assumes args[2] is the directory
422 # argument.
423 if os.path.isdir(args[2]):
424 gclient_utils.RemoveDirectory(args[2])
maruel@chromium.org953586a2010-06-15 14:22:24 +0000425 else:
426 # Progress was made, convert to update since an aborted checkout
427 # is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000428 args = ['update'] + args[1:]
maruel@chromium.org953586a2010-06-15 14:22:24 +0000429 else:
430 # It was an update or export.
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000431 # We enforce that some progress has been made or a known failure.
432 if len(file_list) == previous_list_len and not IsKnownFailure():
433 # No known svn error was found and no progress, bail out.
434 raise
maruel@chromium.org953586a2010-06-15 14:22:24 +0000435 print "Sleeping 15 seconds and retrying...."
436 time.sleep(15)
437 continue
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000438 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000439
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000440 @staticmethod
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000441 def RunAndFilterOutput(args, **kwargs):
442 """Wrapper for gclient_utils.SubprocessCallAndFilter()."""
443 return gclient_utils.SubprocessCallAndFilter([SVN.COMMAND] + args, **kwargs)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000444
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000445 @staticmethod
446 def CaptureInfo(relpath, in_directory=None, print_error=True):
447 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000448
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000449 Args:
450 relpath: The directory where the working copy resides relative to
451 the directory given by in_directory.
452 in_directory: The directory where svn is to be run.
453 """
454 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
455 dom = gclient_utils.ParseXML(output)
456 result = {}
457 if dom:
458 GetNamedNodeText = gclient_utils.GetNamedNodeText
459 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
460 def C(item, f):
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000461 if item is not None:
462 return f(item)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000463 # /info/entry/
464 # url
465 # reposityory/(root|uuid)
466 # wc-info/(schedule|depth)
467 # commit/(author|date)
468 # str() the results because they may be returned as Unicode, which
469 # interferes with the higher layers matching up things in the deps
470 # dictionary.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000471 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
472 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
473 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
474 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
475 'revision'),
476 int)
477 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
478 str)
479 # Differs across versions.
480 if result['Node Kind'] == 'dir':
481 result['Node Kind'] = 'directory'
482 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
483 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
484 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
485 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
486 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000487
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000488 @staticmethod
489 def CaptureHeadRevision(url):
490 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000491
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000492 Returns:
493 Int head revision
494 """
495 info = SVN.Capture(["info", "--xml", url], os.getcwd())
496 dom = xml.dom.minidom.parseString(info)
497 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000498
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000499 @staticmethod
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000500 def CaptureBaseRevision(cwd):
501 """Get the base revision of a SVN repository.
502
503 Returns:
504 Int base revision
505 """
506 info = SVN.Capture(["info", "--xml"], cwd)
507 dom = xml.dom.minidom.parseString(info)
508 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
509
510 @staticmethod
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000511 def CaptureStatus(files):
512 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000513
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000514 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000515
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000516 Returns an array of (status, file) tuples."""
517 command = ["status", "--xml"]
518 if not files:
519 pass
520 elif isinstance(files, basestring):
521 command.append(files)
522 else:
523 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000524
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000525 status_letter = {
526 None: ' ',
527 '': ' ',
528 'added': 'A',
529 'conflicted': 'C',
530 'deleted': 'D',
531 'external': 'X',
532 'ignored': 'I',
533 'incomplete': '!',
534 'merged': 'G',
535 'missing': '!',
536 'modified': 'M',
537 'none': ' ',
538 'normal': ' ',
539 'obstructed': '~',
540 'replaced': 'R',
541 'unversioned': '?',
542 }
543 dom = gclient_utils.ParseXML(SVN.Capture(command))
544 results = []
545 if dom:
546 # /status/target/entry/(wc-status|commit|author|date)
547 for target in dom.getElementsByTagName('target'):
548 #base_path = target.getAttribute('path')
549 for entry in target.getElementsByTagName('entry'):
550 file_path = entry.getAttribute('path')
551 wc_status = entry.getElementsByTagName('wc-status')
552 assert len(wc_status) == 1
553 # Emulate svn 1.5 status ouput...
554 statuses = [' '] * 7
555 # Col 0
556 xml_item_status = wc_status[0].getAttribute('item')
557 if xml_item_status in status_letter:
558 statuses[0] = status_letter[xml_item_status]
559 else:
560 raise Exception('Unknown item status "%s"; please implement me!' %
561 xml_item_status)
562 # Col 1
563 xml_props_status = wc_status[0].getAttribute('props')
564 if xml_props_status == 'modified':
565 statuses[1] = 'M'
566 elif xml_props_status == 'conflicted':
567 statuses[1] = 'C'
568 elif (not xml_props_status or xml_props_status == 'none' or
569 xml_props_status == 'normal'):
570 pass
571 else:
572 raise Exception('Unknown props status "%s"; please implement me!' %
573 xml_props_status)
574 # Col 2
575 if wc_status[0].getAttribute('wc-locked') == 'true':
576 statuses[2] = 'L'
577 # Col 3
578 if wc_status[0].getAttribute('copied') == 'true':
579 statuses[3] = '+'
580 # Col 4
581 if wc_status[0].getAttribute('switched') == 'true':
582 statuses[4] = 'S'
583 # TODO(maruel): Col 5 and 6
584 item = (''.join(statuses), file_path)
585 results.append(item)
586 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000587
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000588 @staticmethod
589 def IsMoved(filename):
590 """Determine if a file has been added through svn mv"""
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000591 return SVN.IsMovedInfo(SVN.CaptureInfo(filename))
592
593 @staticmethod
594 def IsMovedInfo(info):
595 """Determine if a file has been added through svn mv"""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000596 return (info.get('Copied From URL') and
597 info.get('Copied From Rev') and
598 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000599
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000600 @staticmethod
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000601 def GetFileProperty(filename, property_name):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000602 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000603
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000604 Args:
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000605 filename: The file to check
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000606 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000607
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000608 Returns:
609 The value of the property, which will be the empty string if the property
610 is not set on the file. If the file is not under version control, the
611 empty string is also returned.
612 """
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000613 output = SVN.Capture(["propget", property_name, filename])
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000614 if (output.startswith("svn: ") and
615 output.endswith("is not under version control")):
616 return ""
617 else:
618 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000619
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000620 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000621 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000622 """Diffs a single file.
623
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000624 Should be simple, eh? No it isn't.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000625 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000626 expected relative path.
627 full_move means that move or copy operations should completely recreate the
628 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000629 # If the user specified a custom diff command in their svn config file,
630 # then it'll be used when we do svn diff, which we don't want to happen
631 # since we want the unified diff. Using --diff-cmd=diff doesn't always
632 # work, since they can have another diff executable in their path that
633 # gives different line endings. So we use a bogus temp directory as the
634 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000635 bogus_dir = tempfile.mkdtemp()
636 try:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000637 # Use "svn info" output instead of os.path.isdir because the latter fails
638 # when the file is deleted.
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000639 return SVN._DiffItemInternal(filename, SVN.CaptureInfo(filename),
640 bogus_dir,
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000641 full_move=full_move, revision=revision)
642 finally:
643 shutil.rmtree(bogus_dir)
644
645 @staticmethod
646 def _DiffItemInternal(filename, info, bogus_dir, full_move=False,
647 revision=None):
648 """Grabs the diff data."""
649 command = ["diff", "--config-dir", bogus_dir, filename]
650 if revision:
651 command.extend(['--revision', revision])
652 data = None
653 if SVN.IsMovedInfo(info):
654 if full_move:
655 if info.get("Node Kind") == "directory":
656 # Things become tricky here. It's a directory copy/move. We need to
657 # diff all the files inside it.
658 # This will put a lot of pressure on the heap. This is why StringIO
659 # is used and converted back into a string at the end. The reason to
660 # return a string instead of a StringIO is that StringIO.write()
661 # doesn't accept a StringIO object. *sigh*.
662 for (dirpath, dirnames, filenames) in os.walk(filename):
663 # Cleanup all files starting with a '.'.
664 for d in dirnames:
665 if d.startswith('.'):
666 dirnames.remove(d)
667 for f in filenames:
668 if f.startswith('.'):
669 filenames.remove(f)
670 for f in filenames:
671 if data is None:
672 data = cStringIO.StringIO()
673 data.write(GenFakeDiff(os.path.join(dirpath, f)))
674 if data:
675 tmp = data.getvalue()
676 data.close()
677 data = tmp
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000678 else:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000679 data = GenFakeDiff(filename)
680 else:
681 if info.get("Node Kind") != "directory":
maruel@chromium.org0836c562010-01-22 01:10:06 +0000682 # svn diff on a mv/cp'd file outputs nothing if there was no change.
683 data = SVN.Capture(command, None)
684 if not data:
685 # We put in an empty Index entry so upload.py knows about them.
maruel@chromium.orgc6d170e2010-06-03 00:06:00 +0000686 data = "Index: %s\n" % filename.replace(os.sep, '/')
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000687 # Otherwise silently ignore directories.
688 else:
689 if info.get("Node Kind") != "directory":
690 # Normal simple case.
maruel@chromium.org0836c562010-01-22 01:10:06 +0000691 data = SVN.Capture(command, None)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000692 # Otherwise silently ignore directories.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000693 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000694
695 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000696 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000697 """Returns a string containing the diff for the given file list.
698
699 The files in the list should either be absolute paths or relative to the
700 given root. If no root directory is provided, the repository root will be
701 used.
702 The diff will always use relative paths.
703 """
704 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000705 root = root or SVN.GetCheckoutRoot(previous_cwd)
706 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000707 def RelativePath(path, root):
708 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000709 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000710 return path[len(root):]
711 return path
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000712 # If the user specified a custom diff command in their svn config file,
713 # then it'll be used when we do svn diff, which we don't want to happen
714 # since we want the unified diff. Using --diff-cmd=diff doesn't always
715 # work, since they can have another diff executable in their path that
716 # gives different line endings. So we use a bogus temp directory as the
717 # config directory, which gets around these problems.
718 bogus_dir = tempfile.mkdtemp()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000719 try:
720 os.chdir(root)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000721 # Cleanup filenames
722 filenames = [RelativePath(f, root) for f in filenames]
723 # Get information about the modified items (files and directories)
724 data = dict([(f, SVN.CaptureInfo(f)) for f in filenames])
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000725 diffs = []
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000726 if full_move:
727 # Eliminate modified files inside moved/copied directory.
728 for (filename, info) in data.iteritems():
729 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
730 # Remove files inside the directory.
731 filenames = [f for f in filenames
732 if not f.startswith(filename + os.path.sep)]
733 for filename in data.keys():
734 if not filename in filenames:
735 # Remove filtered out items.
736 del data[filename]
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000737 else:
738 metaheaders = []
739 for (filename, info) in data.iteritems():
740 if SVN.IsMovedInfo(info):
741 # for now, the most common case is a head copy,
742 # so let's just encode that as a straight up cp.
743 srcurl = info.get('Copied From URL')
744 root = info.get('Repository Root')
745 rev = int(info.get('Copied From Rev'))
746 assert srcurl.startswith(root)
747 src = srcurl[len(root)+1:]
748 srcinfo = SVN.CaptureInfo(srcurl)
749 if (srcinfo.get('Revision') != rev and
750 SVN.Capture(['diff', '-r', '%d:head' % rev, srcurl])):
751 metaheaders.append("#$ svn cp -r %d %s %s "
752 "### WARNING: note non-trunk copy\n" %
753 (rev, src, filename))
754 else:
755 metaheaders.append("#$ cp %s %s\n" % (src,
756 filename))
757
758 if metaheaders:
759 diffs.append("### BEGIN SVN COPY METADATA\n")
760 diffs.extend(metaheaders)
761 diffs.append("### END SVN COPY METADATA\n")
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000762 # Now ready to do the actual diff.
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000763 for filename in sorted(data.iterkeys()):
764 diffs.append(SVN._DiffItemInternal(filename, data[filename], bogus_dir,
765 full_move=full_move,
766 revision=revision))
767 # Use StringIO since it can be messy when diffing a directory move with
768 # full_move=True.
769 buf = cStringIO.StringIO()
770 for d in filter(None, diffs):
771 buf.write(d)
772 result = buf.getvalue()
773 buf.close()
774 return result
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000775 finally:
776 os.chdir(previous_cwd)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000777 shutil.rmtree(bogus_dir)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000778
779 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000780 def GetEmail(repo_root):
781 """Retrieves the svn account which we assume is an email address."""
782 infos = SVN.CaptureInfo(repo_root)
783 uuid = infos.get('UUID')
784 root = infos.get('Repository Root')
785 if not root:
786 return None
787
788 # Should check for uuid but it is incorrectly saved for https creds.
789 realm = root.rsplit('/', 1)[0]
790 if root.startswith('https') or not uuid:
791 regexp = re.compile(r'<%s:\d+>.*' % realm)
792 else:
793 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
794 if regexp is None:
795 return None
796 if sys.platform.startswith('win'):
797 if not 'APPDATA' in os.environ:
798 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000799 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
800 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000801 else:
802 if not 'HOME' in os.environ:
803 return None
804 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
805 'svn.simple')
806 for credfile in os.listdir(auth_dir):
807 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
808 if regexp.match(cred_info.get('svn:realmstring')):
809 return cred_info.get('username')
810
811 @staticmethod
812 def ReadSimpleAuth(filename):
813 f = open(filename, 'r')
814 values = {}
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000815 def ReadOneItem(item_type):
816 m = re.match(r'%s (\d+)' % item_type, f.readline())
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000817 if not m:
818 return None
819 data = f.read(int(m.group(1)))
820 if f.read(1) != '\n':
821 return None
822 return data
823
824 while True:
825 key = ReadOneItem('K')
826 if not key:
827 break
828 value = ReadOneItem('V')
829 if not value:
830 break
831 values[key] = value
832 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000833
834 @staticmethod
835 def GetCheckoutRoot(directory):
836 """Returns the top level directory of the current repository.
837
838 The directory is returned as an absolute path.
839 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000840 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000841 infos = SVN.CaptureInfo(directory, print_error=False)
842 cur_dir_repo_root = infos.get("Repository Root")
843 if not cur_dir_repo_root:
844 return None
845
846 while True:
847 parent = os.path.dirname(directory)
848 if (SVN.CaptureInfo(parent, print_error=False).get(
849 "Repository Root") != cur_dir_repo_root):
850 break
851 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000852 return GetCasedPath(directory)
tony@chromium.org57564662010-04-14 02:35:12 +0000853
854 @staticmethod
855 def AssertVersion(min_version):
856 """Asserts svn's version is at least min_version."""
857 def only_int(val):
858 if val.isdigit():
859 return int(val)
860 else:
861 return 0
862 if not SVN.current_version:
863 SVN.current_version = SVN.Capture(['--version']).split()[2]
864 current_version_list = map(only_int, SVN.current_version.split('.'))
865 for min_ver in map(int, min_version.split('.')):
866 ver = current_version_list.pop(0)
867 if ver < min_ver:
868 return (False, SVN.current_version)
869 elif ver > min_ver:
870 return (True, SVN.current_version)
871 return (True, SVN.current_version)