blob: ea91c7b699cf45c933acdde1c590e67acf8db0b0 [file] [log] [blame]
maruel@chromium.orgbec588d2010-10-26 13:50:25 +00001# Copyright (c) 2010 The Chromium Authors. All rights reserved.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00002# 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):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000068 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +000069 def Capture(args, **kwargs):
70 return gclient_utils.CheckCall(['git'] + args, print_error=False,
71 **kwargs)[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000072
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000073 @staticmethod
msb@chromium.org786fb682010-06-02 15:16:23 +000074 def CaptureStatus(files, upstream_branch=None):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000075 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000076
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000077 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000078
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000079 Returns an array of (status, file) tuples."""
msb@chromium.org786fb682010-06-02 15:16:23 +000080 if upstream_branch is None:
81 upstream_branch = GIT.GetUpstreamBranch(os.getcwd())
82 if upstream_branch is None:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +000083 raise gclient_utils.Error('Cannot determine upstream branch')
84 command = ['diff', '--name-status', '-r', '%s...' % upstream_branch]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000085 if not files:
86 pass
87 elif isinstance(files, basestring):
88 command.append(files)
89 else:
90 command.extend(files)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +000091 status = GIT.Capture(command).rstrip()
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000092 results = []
93 if status:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +000094 for statusline in status.splitlines():
maruel@chromium.orgcc1614b2010-09-20 17:13:17 +000095 # 3-way merges can cause the status can be 'MMM' instead of 'M'. This
96 # can happen when the user has 2 local branches and he diffs between
97 # these 2 branches instead diffing to upstream.
98 m = re.match('^(\w)+\t(.+)$', statusline)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000099 if not m:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000100 raise gclient_utils.Error(
101 'status currently unsupported: %s' % statusline)
maruel@chromium.orgcc1614b2010-09-20 17:13:17 +0000102 # Only grab the first letter.
103 results.append(('%s ' % m.group(1)[0], m.group(2)))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000104 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000105
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000106 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000107 def GetEmail(cwd):
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000108 """Retrieves the user email address if known."""
109 # We could want to look at the svn cred when it has a svn remote but it
110 # should be fine for now, users should simply configure their git settings.
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000111 try:
112 return GIT.Capture(['config', 'user.email'], cwd=cwd).strip()
113 except gclient_utils.CheckCallError:
114 return ''
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000115
116 @staticmethod
117 def ShortBranchName(branch):
118 """Converts a name like 'refs/heads/foo' to just 'foo'."""
119 return branch.replace('refs/heads/', '')
120
121 @staticmethod
122 def GetBranchRef(cwd):
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000123 """Returns the full branch reference, e.g. 'refs/heads/master'."""
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000124 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd=cwd).strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000125
126 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000127 def GetBranch(cwd):
128 """Returns the short branch name, e.g. 'master'."""
maruel@chromium.orgc308a742009-12-22 18:29:33 +0000129 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000130
131 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000132 def IsGitSvn(cwd):
133 """Returns true if this repo looks like it's using git-svn."""
134 # If you have any "svn-remote.*" config keys, we think you're using svn.
135 try:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000136 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd=cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000137 return True
138 except gclient_utils.CheckCallError:
139 return False
140
141 @staticmethod
142 def GetSVNBranch(cwd):
143 """Returns the svn branch name if found."""
144 # Try to figure out which remote branch we're based on.
145 # Strategy:
146 # 1) find all git-svn branches and note their svn URLs.
147 # 2) iterate through our branch history and match up the URLs.
148
149 # regexp matching the git-svn line that contains the URL.
150 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
151
152 # Get the refname and svn url for all refs/remotes/*.
153 remotes = GIT.Capture(
154 ['for-each-ref', '--format=%(refname)', 'refs/remotes'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000155 cwd=cwd).splitlines()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000156 svn_refs = {}
157 for ref in remotes:
158 match = git_svn_re.search(
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000159 GIT.Capture(['cat-file', '-p', ref], cwd=cwd))
sky@chromium.org42d8da52010-04-23 18:25:07 +0000160 # Prefer origin/HEAD over all others.
161 if match and (match.group(1) not in svn_refs or
162 ref == "refs/remotes/origin/HEAD"):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000163 svn_refs[match.group(1)] = ref
164
165 svn_branch = ''
166 if len(svn_refs) == 1:
167 # Only one svn branch exists -- seems like a good candidate.
168 svn_branch = svn_refs.values()[0]
169 elif len(svn_refs) > 1:
170 # We have more than one remote branch available. We don't
171 # want to go through all of history, so read a line from the
172 # pipe at a time.
173 # The -100 is an arbitrary limit so we don't search forever.
174 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org3a292682010-08-23 18:54:55 +0000175 proc = gclient_utils.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000176 for line in proc.stdout:
177 match = git_svn_re.match(line)
178 if match:
179 url = match.group(1)
180 if url in svn_refs:
181 svn_branch = svn_refs[url]
182 proc.stdout.close() # Cut pipe.
183 break
184 return svn_branch
185
186 @staticmethod
187 def FetchUpstreamTuple(cwd):
188 """Returns a tuple containg remote and remote ref,
189 e.g. 'origin', 'refs/heads/master'
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000190 Tries to be intelligent and understand git-svn.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000191 """
192 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000193 branch = GIT.GetBranch(cwd)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000194 try:
195 upstream_branch = GIT.Capture(
196 ['config', 'branch.%s.merge' % branch], cwd=cwd).strip()
197 except gclient_utils.Error:
198 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000199 if upstream_branch:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000200 try:
201 remote = GIT.Capture(
202 ['config', 'branch.%s.remote' % branch], cwd=cwd).strip()
203 except gclient_utils.Error:
204 pass
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000205 else:
206 # Fall back on trying a git-svn upstream branch.
207 if GIT.IsGitSvn(cwd):
208 upstream_branch = GIT.GetSVNBranch(cwd)
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000209 else:
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000210 # Else, try to guess the origin remote.
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000211 remote_branches = GIT.Capture(['branch', '-r'], cwd=cwd).split()
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000212 if 'origin/master' in remote_branches:
213 # Fall back on origin/master if it exits.
214 remote = 'origin'
215 upstream_branch = 'refs/heads/master'
216 elif 'origin/trunk' in remote_branches:
217 # Fall back on origin/trunk if it exists. Generally a shared
218 # git-svn clone
219 remote = 'origin'
220 upstream_branch = 'refs/heads/trunk'
221 else:
222 # Give up.
223 remote = None
224 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000225 return remote, upstream_branch
226
227 @staticmethod
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000228 def GetUpstreamBranch(cwd):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000229 """Gets the current branch's upstream branch."""
230 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000231 if remote != '.' and upstream_branch:
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000232 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
233 return upstream_branch
234
235 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000236 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
237 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000238 """Diffs against the upstream branch or optionally another branch.
239
240 full_move means that move or copy operations should completely recreate the
241 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000242 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000243 branch = GIT.GetUpstreamBranch(cwd)
evan@chromium.org400f3e72010-05-19 14:23:36 +0000244 command = ['diff', '-p', '--no-prefix', '--no-ext-diff',
245 branch + "..." + branch_head]
maruel@chromium.orga9371762009-12-22 18:27:38 +0000246 if not full_move:
247 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000248 # TODO(maruel): --binary support.
249 if files:
250 command.append('--')
251 command.extend(files)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000252 diff = GIT.Capture(command, cwd=cwd).splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000253 for i in range(len(diff)):
254 # In the case of added files, replace /dev/null with the path to the
255 # file being added.
256 if diff[i].startswith('--- /dev/null'):
257 diff[i] = '--- %s' % diff[i+1][4:]
258 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000259
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000260 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000261 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
262 """Returns the list of modified files between two branches."""
263 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000264 branch = GIT.GetUpstreamBranch(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000265 command = ['diff', '--name-only', branch + "..." + branch_head]
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000266 return GIT.Capture(command, cwd=cwd).splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000267
268 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000269 def GetPatchName(cwd):
270 """Constructs a name for this patch."""
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000271 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd=cwd).strip()
maruel@chromium.org862ff8e2010-08-06 15:29:16 +0000272 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000273
274 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000275 def GetCheckoutRoot(cwd):
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000276 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000277 """
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000278 root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd).strip()
279 return os.path.abspath(os.path.join(cwd, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000280
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000281 @staticmethod
282 def AssertVersion(min_version):
283 """Asserts git's version is at least min_version."""
284 def only_int(val):
285 if val.isdigit():
286 return int(val)
287 else:
288 return 0
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000289 current_version = GIT.Capture(['--version']).split()[-1]
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000290 current_version_list = map(only_int, current_version.split('.'))
291 for min_ver in map(int, min_version.split('.')):
292 ver = current_version_list.pop(0)
293 if ver < min_ver:
294 return (False, current_version)
295 elif ver > min_ver:
296 return (True, current_version)
297 return (True, current_version)
298
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000299
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000300class SVN(object):
tony@chromium.org57564662010-04-14 02:35:12 +0000301 current_version = None
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000302
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000303 @staticmethod
maruel@chromium.org54019f32010-09-09 13:50:11 +0000304 def Capture(args, **kwargs):
305 """Always redirect stderr.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000306
maruel@chromium.org54019f32010-09-09 13:50:11 +0000307 Throws an exception if non-0 is returned."""
308 return gclient_utils.CheckCall(['svn'] + args, print_error=False,
309 **kwargs)[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000310
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000311 @staticmethod
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000312 def RunAndGetFileList(verbose, args, cwd, file_list, stdout=None):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000313 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000314
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000315 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000316
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000317 svn's stdout is parsed to collect a list of files checked out or updated.
318 These files are appended to file_list. svn's stdout is also printed to
319 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000320
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000321 Args:
maruel@chromium.org03807072010-08-16 17:18:44 +0000322 verbose: If True, uses verbose output
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000323 args: A sequence of command line parameters to be passed to svn.
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000324 cwd: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000325
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000326 Raises:
327 Error: An error occurred while running the svn command.
328 """
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000329 stdout = stdout or sys.stdout
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000330
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000331 # svn update and svn checkout use the same pattern: the first three columns
332 # are for file status, property status, and lock status. This is followed
333 # by two spaces, and then the path to the file.
334 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000335
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000336 # The first three columns of svn status are the same as for svn update and
337 # svn checkout. The next three columns indicate addition-with-history,
338 # switch, and remote lock status. This is followed by one space, and then
339 # the path to the file.
340 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000341
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000342 # args[0] must be a supported command. This will blow up if it's something
343 # else, which is good. Note that the patterns are only effective when
344 # these commands are used in their ordinary forms, the patterns are invalid
345 # for "svn status --show-updates", for example.
346 pattern = {
347 'checkout': update_pattern,
348 'status': status_pattern,
349 'update': update_pattern,
350 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000351 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000352 # Place an upper limit.
cbentzel@chromium.org2aee2292010-09-03 14:15:25 +0000353 backoff_time = 5
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000354 retries = 0
maruel@chromium.org03507062010-10-26 00:58:27 +0000355 while True:
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000356 retries += 1
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000357 previous_list_len = len(file_list)
358 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000359
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000360 def CaptureMatchingLines(line):
361 match = compiled_pattern.search(line)
362 if match:
363 file_list.append(match.group(1))
364 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000365 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000366
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000367 try:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000368 gclient_utils.CheckCallAndFilterAndHeader(
369 ['svn'] + args,
370 cwd=cwd,
371 always=verbose,
372 filter_fn=CaptureMatchingLines,
373 stdout=stdout)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000374 except gclient_utils.Error:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000375 def IsKnownFailure():
376 for x in failure:
377 if (x.startswith('svn: OPTIONS of') or
378 x.startswith('svn: PROPFIND of') or
379 x.startswith('svn: REPORT of') or
maruel@chromium.orgf61fc932010-08-19 13:05:24 +0000380 x.startswith('svn: Unknown hostname') or
381 x.startswith('svn: Server sent unexpected return value')):
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000382 return True
383 return False
384
maruel@chromium.org953586a2010-06-15 14:22:24 +0000385 # Subversion client is really misbehaving with Google Code.
386 if args[0] == 'checkout':
387 # Ensure at least one file was checked out, otherwise *delete* the
388 # directory.
389 if len(file_list) == previous_list_len:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000390 if not IsKnownFailure():
maruel@chromium.org953586a2010-06-15 14:22:24 +0000391 # No known svn error was found, bail out.
392 raise
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000393 # No file were checked out, so make sure the directory is
394 # deleted in case it's messed up and try again.
395 # Warning: It's bad, it assumes args[2] is the directory
396 # argument.
397 if os.path.isdir(args[2]):
398 gclient_utils.RemoveDirectory(args[2])
maruel@chromium.org953586a2010-06-15 14:22:24 +0000399 else:
400 # Progress was made, convert to update since an aborted checkout
401 # is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000402 args = ['update'] + args[1:]
maruel@chromium.org953586a2010-06-15 14:22:24 +0000403 else:
404 # It was an update or export.
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000405 # We enforce that some progress has been made or a known failure.
406 if len(file_list) == previous_list_len and not IsKnownFailure():
407 # No known svn error was found and no progress, bail out.
408 raise
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000409 if retries == 10:
maruel@chromium.org03507062010-10-26 00:58:27 +0000410 raise
cbentzel@chromium.org2aee2292010-09-03 14:15:25 +0000411 print "Sleeping %.1f seconds and retrying...." % backoff_time
412 time.sleep(backoff_time)
413 backoff_time *= 1.3
maruel@chromium.org953586a2010-06-15 14:22:24 +0000414 continue
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000415 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000416
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000417 @staticmethod
maruel@chromium.org54019f32010-09-09 13:50:11 +0000418 def CaptureInfo(cwd):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000419 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000420
maruel@chromium.org54019f32010-09-09 13:50:11 +0000421 Throws an exception if svn info fails."""
422 output = SVN.Capture(['info', '--xml', cwd])
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000423 dom = gclient_utils.ParseXML(output)
424 result = {}
425 if dom:
426 GetNamedNodeText = gclient_utils.GetNamedNodeText
427 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
428 def C(item, f):
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000429 if item is not None:
430 return f(item)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000431 # /info/entry/
432 # url
433 # reposityory/(root|uuid)
434 # wc-info/(schedule|depth)
435 # commit/(author|date)
436 # str() the results because they may be returned as Unicode, which
437 # interferes with the higher layers matching up things in the deps
438 # dictionary.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000439 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
440 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
441 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
442 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
443 'revision'),
444 int)
445 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
446 str)
447 # Differs across versions.
448 if result['Node Kind'] == 'dir':
449 result['Node Kind'] = 'directory'
450 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
451 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
452 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
453 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
454 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000455
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000456 @staticmethod
maruel@chromium.org54019f32010-09-09 13:50:11 +0000457 def CaptureRevision(cwd):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000458 """Get the base revision of a SVN repository.
459
460 Returns:
461 Int base revision
462 """
maruel@chromium.org54019f32010-09-09 13:50:11 +0000463 info = SVN.Capture(['info', '--xml'], cwd=cwd)
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000464 dom = xml.dom.minidom.parseString(info)
465 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
466
467 @staticmethod
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000468 def CaptureStatus(files):
469 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000470
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000471 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000472
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000473 Returns an array of (status, file) tuples."""
474 command = ["status", "--xml"]
475 if not files:
476 pass
477 elif isinstance(files, basestring):
478 command.append(files)
479 else:
480 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000481
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000482 status_letter = {
483 None: ' ',
484 '': ' ',
485 'added': 'A',
486 'conflicted': 'C',
487 'deleted': 'D',
488 'external': 'X',
489 'ignored': 'I',
490 'incomplete': '!',
491 'merged': 'G',
492 'missing': '!',
493 'modified': 'M',
494 'none': ' ',
495 'normal': ' ',
496 'obstructed': '~',
497 'replaced': 'R',
498 'unversioned': '?',
499 }
500 dom = gclient_utils.ParseXML(SVN.Capture(command))
501 results = []
502 if dom:
503 # /status/target/entry/(wc-status|commit|author|date)
504 for target in dom.getElementsByTagName('target'):
505 #base_path = target.getAttribute('path')
506 for entry in target.getElementsByTagName('entry'):
507 file_path = entry.getAttribute('path')
508 wc_status = entry.getElementsByTagName('wc-status')
509 assert len(wc_status) == 1
510 # Emulate svn 1.5 status ouput...
511 statuses = [' '] * 7
512 # Col 0
513 xml_item_status = wc_status[0].getAttribute('item')
514 if xml_item_status in status_letter:
515 statuses[0] = status_letter[xml_item_status]
516 else:
maruel@chromium.org54019f32010-09-09 13:50:11 +0000517 raise gclient_utils.Error(
518 'Unknown item status "%s"; please implement me!' %
519 xml_item_status)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000520 # Col 1
521 xml_props_status = wc_status[0].getAttribute('props')
522 if xml_props_status == 'modified':
523 statuses[1] = 'M'
524 elif xml_props_status == 'conflicted':
525 statuses[1] = 'C'
526 elif (not xml_props_status or xml_props_status == 'none' or
527 xml_props_status == 'normal'):
528 pass
529 else:
maruel@chromium.org54019f32010-09-09 13:50:11 +0000530 raise gclient_utils.Error(
531 'Unknown props status "%s"; please implement me!' %
532 xml_props_status)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000533 # Col 2
534 if wc_status[0].getAttribute('wc-locked') == 'true':
535 statuses[2] = 'L'
536 # Col 3
537 if wc_status[0].getAttribute('copied') == 'true':
538 statuses[3] = '+'
539 # Col 4
540 if wc_status[0].getAttribute('switched') == 'true':
541 statuses[4] = 'S'
542 # TODO(maruel): Col 5 and 6
543 item = (''.join(statuses), file_path)
544 results.append(item)
545 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000546
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000547 @staticmethod
548 def IsMoved(filename):
549 """Determine if a file has been added through svn mv"""
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000550 return SVN.IsMovedInfo(SVN.CaptureInfo(filename))
551
552 @staticmethod
553 def IsMovedInfo(info):
554 """Determine if a file has been added through svn mv"""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000555 return (info.get('Copied From URL') and
556 info.get('Copied From Rev') and
557 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000558
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000559 @staticmethod
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000560 def GetFileProperty(filename, property_name):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000561 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000562
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000563 Args:
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000564 filename: The file to check
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000565 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000566
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000567 Returns:
568 The value of the property, which will be the empty string if the property
569 is not set on the file. If the file is not under version control, the
570 empty string is also returned.
571 """
maruel@chromium.org54019f32010-09-09 13:50:11 +0000572 try:
573 return SVN.Capture(['propget', property_name, filename])
574 except gclient_utils.Error:
575 return ''
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000576
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000577 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000578 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000579 """Diffs a single file.
580
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000581 Should be simple, eh? No it isn't.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000582 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000583 expected relative path.
584 full_move means that move or copy operations should completely recreate the
585 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000586 # If the user specified a custom diff command in their svn config file,
587 # then it'll be used when we do svn diff, which we don't want to happen
588 # since we want the unified diff. Using --diff-cmd=diff doesn't always
589 # work, since they can have another diff executable in their path that
590 # gives different line endings. So we use a bogus temp directory as the
591 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000592 bogus_dir = tempfile.mkdtemp()
593 try:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000594 # Use "svn info" output instead of os.path.isdir because the latter fails
595 # when the file is deleted.
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000596 return SVN._DiffItemInternal(filename, SVN.CaptureInfo(filename),
597 bogus_dir,
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000598 full_move=full_move, revision=revision)
599 finally:
600 shutil.rmtree(bogus_dir)
601
602 @staticmethod
603 def _DiffItemInternal(filename, info, bogus_dir, full_move=False,
604 revision=None):
605 """Grabs the diff data."""
606 command = ["diff", "--config-dir", bogus_dir, filename]
607 if revision:
608 command.extend(['--revision', revision])
609 data = None
610 if SVN.IsMovedInfo(info):
611 if full_move:
612 if info.get("Node Kind") == "directory":
613 # Things become tricky here. It's a directory copy/move. We need to
614 # diff all the files inside it.
615 # This will put a lot of pressure on the heap. This is why StringIO
616 # is used and converted back into a string at the end. The reason to
617 # return a string instead of a StringIO is that StringIO.write()
618 # doesn't accept a StringIO object. *sigh*.
619 for (dirpath, dirnames, filenames) in os.walk(filename):
620 # Cleanup all files starting with a '.'.
621 for d in dirnames:
622 if d.startswith('.'):
623 dirnames.remove(d)
624 for f in filenames:
625 if f.startswith('.'):
626 filenames.remove(f)
627 for f in filenames:
628 if data is None:
629 data = cStringIO.StringIO()
630 data.write(GenFakeDiff(os.path.join(dirpath, f)))
631 if data:
632 tmp = data.getvalue()
633 data.close()
634 data = tmp
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000635 else:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000636 data = GenFakeDiff(filename)
637 else:
638 if info.get("Node Kind") != "directory":
maruel@chromium.org0836c562010-01-22 01:10:06 +0000639 # svn diff on a mv/cp'd file outputs nothing if there was no change.
maruel@chromium.org54019f32010-09-09 13:50:11 +0000640 data = SVN.Capture(command)
maruel@chromium.org0836c562010-01-22 01:10:06 +0000641 if not data:
642 # We put in an empty Index entry so upload.py knows about them.
maruel@chromium.orgc6d170e2010-06-03 00:06:00 +0000643 data = "Index: %s\n" % filename.replace(os.sep, '/')
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000644 # Otherwise silently ignore directories.
645 else:
646 if info.get("Node Kind") != "directory":
647 # Normal simple case.
maruel@chromium.org54019f32010-09-09 13:50:11 +0000648 data = SVN.Capture(command)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000649 # Otherwise silently ignore directories.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000650 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000651
652 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000653 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000654 """Returns a string containing the diff for the given file list.
655
656 The files in the list should either be absolute paths or relative to the
657 given root. If no root directory is provided, the repository root will be
658 used.
659 The diff will always use relative paths.
660 """
661 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000662 root = root or SVN.GetCheckoutRoot(previous_cwd)
663 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000664 def RelativePath(path, root):
665 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000666 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000667 return path[len(root):]
668 return path
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000669 # If the user specified a custom diff command in their svn config file,
670 # then it'll be used when we do svn diff, which we don't want to happen
671 # since we want the unified diff. Using --diff-cmd=diff doesn't always
672 # work, since they can have another diff executable in their path that
673 # gives different line endings. So we use a bogus temp directory as the
674 # config directory, which gets around these problems.
675 bogus_dir = tempfile.mkdtemp()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000676 try:
677 os.chdir(root)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000678 # Cleanup filenames
679 filenames = [RelativePath(f, root) for f in filenames]
680 # Get information about the modified items (files and directories)
681 data = dict([(f, SVN.CaptureInfo(f)) for f in filenames])
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000682 diffs = []
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000683 if full_move:
684 # Eliminate modified files inside moved/copied directory.
685 for (filename, info) in data.iteritems():
686 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
687 # Remove files inside the directory.
688 filenames = [f for f in filenames
689 if not f.startswith(filename + os.path.sep)]
690 for filename in data.keys():
691 if not filename in filenames:
692 # Remove filtered out items.
693 del data[filename]
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000694 else:
695 metaheaders = []
696 for (filename, info) in data.iteritems():
697 if SVN.IsMovedInfo(info):
698 # for now, the most common case is a head copy,
699 # so let's just encode that as a straight up cp.
700 srcurl = info.get('Copied From URL')
701 root = info.get('Repository Root')
702 rev = int(info.get('Copied From Rev'))
703 assert srcurl.startswith(root)
704 src = srcurl[len(root)+1:]
705 srcinfo = SVN.CaptureInfo(srcurl)
706 if (srcinfo.get('Revision') != rev and
707 SVN.Capture(['diff', '-r', '%d:head' % rev, srcurl])):
708 metaheaders.append("#$ svn cp -r %d %s %s "
709 "### WARNING: note non-trunk copy\n" %
710 (rev, src, filename))
711 else:
712 metaheaders.append("#$ cp %s %s\n" % (src,
713 filename))
714
715 if metaheaders:
716 diffs.append("### BEGIN SVN COPY METADATA\n")
717 diffs.extend(metaheaders)
718 diffs.append("### END SVN COPY METADATA\n")
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000719 # Now ready to do the actual diff.
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000720 for filename in sorted(data.iterkeys()):
721 diffs.append(SVN._DiffItemInternal(filename, data[filename], bogus_dir,
722 full_move=full_move,
723 revision=revision))
724 # Use StringIO since it can be messy when diffing a directory move with
725 # full_move=True.
726 buf = cStringIO.StringIO()
727 for d in filter(None, diffs):
728 buf.write(d)
729 result = buf.getvalue()
730 buf.close()
731 return result
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000732 finally:
733 os.chdir(previous_cwd)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000734 shutil.rmtree(bogus_dir)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000735
736 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000737 def GetEmail(repo_root):
738 """Retrieves the svn account which we assume is an email address."""
maruel@chromium.org54019f32010-09-09 13:50:11 +0000739 try:
740 infos = SVN.CaptureInfo(repo_root)
741 except gclient_utils.Error:
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000742 return None
743
744 # Should check for uuid but it is incorrectly saved for https creds.
maruel@chromium.org54019f32010-09-09 13:50:11 +0000745 root = infos['Repository Root']
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000746 realm = root.rsplit('/', 1)[0]
maruel@chromium.org54019f32010-09-09 13:50:11 +0000747 uuid = infos['UUID']
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000748 if root.startswith('https') or not uuid:
749 regexp = re.compile(r'<%s:\d+>.*' % realm)
750 else:
751 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
752 if regexp is None:
753 return None
754 if sys.platform.startswith('win'):
755 if not 'APPDATA' in os.environ:
756 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000757 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
758 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000759 else:
760 if not 'HOME' in os.environ:
761 return None
762 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
763 'svn.simple')
764 for credfile in os.listdir(auth_dir):
765 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
766 if regexp.match(cred_info.get('svn:realmstring')):
767 return cred_info.get('username')
768
769 @staticmethod
770 def ReadSimpleAuth(filename):
771 f = open(filename, 'r')
772 values = {}
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000773 def ReadOneItem(item_type):
774 m = re.match(r'%s (\d+)' % item_type, f.readline())
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000775 if not m:
776 return None
777 data = f.read(int(m.group(1)))
778 if f.read(1) != '\n':
779 return None
780 return data
781
782 while True:
783 key = ReadOneItem('K')
784 if not key:
785 break
786 value = ReadOneItem('V')
787 if not value:
788 break
789 values[key] = value
790 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000791
792 @staticmethod
793 def GetCheckoutRoot(directory):
794 """Returns the top level directory of the current repository.
795
796 The directory is returned as an absolute path.
797 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000798 directory = os.path.abspath(directory)
maruel@chromium.org54019f32010-09-09 13:50:11 +0000799 try:
800 cur_dir_repo_root = SVN.CaptureInfo(directory)['Repository Root']
801 except gclient_utils.Error:
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000802 return None
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000803 while True:
804 parent = os.path.dirname(directory)
maruel@chromium.org54019f32010-09-09 13:50:11 +0000805 try:
806 if SVN.CaptureInfo(parent)['Repository Root'] != cur_dir_repo_root:
807 break
808 except gclient_utils.Error:
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000809 break
810 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000811 return GetCasedPath(directory)
tony@chromium.org57564662010-04-14 02:35:12 +0000812
813 @staticmethod
814 def AssertVersion(min_version):
815 """Asserts svn's version is at least min_version."""
816 def only_int(val):
817 if val.isdigit():
818 return int(val)
819 else:
820 return 0
821 if not SVN.current_version:
822 SVN.current_version = SVN.Capture(['--version']).split()[2]
823 current_version_list = map(only_int, SVN.current_version.split('.'))
824 for min_ver in map(int, min_version.split('.')):
825 ver = current_version_list.pop(0)
826 if ver < min_ver:
827 return (False, SVN.current_version)
828 elif ver > min_ver:
829 return (True, SVN.current_version)
830 return (True, SVN.current_version)