blob: 8bd0ba3a61ca3aa667aa224360546ca44e7f4904 [file] [log] [blame]
[email protected]da64d632011-09-08 17:41:151# Copyright (c) 2011 The Chromium Authors. All rights reserved.
[email protected]d5800f12009-11-12 20:03:432# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
[email protected]5aeb7dd2009-11-17 18:09:015"""SCM-specific utility classes."""
[email protected]d5800f12009-11-12 20:03:436
[email protected]3c55d982010-05-06 14:25:447import cStringIO
[email protected]fd9cbbb2010-01-08 23:04:038import glob
[email protected]07ab60e2011-02-08 21:54:009import logging
[email protected]d5800f12009-11-12 20:03:4310import os
11import re
[email protected]d5800f12009-11-12 20:03:4312import sys
[email protected]5aeb7dd2009-11-17 18:09:0113import tempfile
[email protected]fd876172010-04-30 14:01:0514import time
[email protected]ade9c592011-04-07 15:59:1115from xml.etree import ElementTree
[email protected]d5800f12009-11-12 20:03:4316
17import gclient_utils
[email protected]31cb48a2011-04-04 18:01:3618import subprocess2
19
[email protected]d5800f12009-11-12 20:03:4320
[email protected]b24a8e12009-12-22 13:45:4821def ValidateEmail(email):
[email protected]6e29d572010-06-04 17:32:2022 return (re.match(r"^[a-zA-Z0-9._%-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", email)
23 is not None)
[email protected]b24a8e12009-12-22 13:45:4824
[email protected]d5800f12009-11-12 20:03:4325
[email protected]fd9cbbb2010-01-08 23:04:0326def GetCasedPath(path):
27 """Elcheapos way to get the real path case on Windows."""
28 if sys.platform.startswith('win') and os.path.exists(path):
29 # Reconstruct the path.
30 path = os.path.abspath(path)
31 paths = path.split('\\')
32 for i in range(len(paths)):
33 if i == 0:
34 # Skip drive letter.
35 continue
36 subpath = '\\'.join(paths[:i+1])
37 prev = len('\\'.join(paths[:i]))
38 # glob.glob will return the cased path for the last item only. This is why
39 # we are calling it in a loop. Extract the data we want and put it back
40 # into the list.
41 paths[i] = glob.glob(subpath + '*')[0][prev+1:len(subpath)]
42 path = '\\'.join(paths)
43 return path
44
45
[email protected]3c55d982010-05-06 14:25:4446def GenFakeDiff(filename):
47 """Generates a fake diff from a file."""
48 file_content = gclient_utils.FileRead(filename, 'rb').splitlines(True)
[email protected]c6d170e2010-06-03 00:06:0049 filename = filename.replace(os.sep, '/')
[email protected]3c55d982010-05-06 14:25:4450 nb_lines = len(file_content)
51 # We need to use / since patch on unix will fail otherwise.
52 data = cStringIO.StringIO()
53 data.write("Index: %s\n" % filename)
54 data.write('=' * 67 + '\n')
55 # Note: Should we use /dev/null instead?
56 data.write("--- %s\n" % filename)
57 data.write("+++ %s\n" % filename)
58 data.write("@@ -0,0 +1,%d @@\n" % nb_lines)
59 # Prepend '+' to every lines.
60 for line in file_content:
61 data.write('+')
62 data.write(line)
63 result = data.getvalue()
64 data.close()
65 return result
66
67
[email protected]5c8c6de2011-03-18 16:20:1868def determine_scm(root):
69 """Similar to upload.py's version but much simpler.
70
71 Returns 'svn', 'git' or None.
72 """
73 if os.path.isdir(os.path.join(root, '.svn')):
74 return 'svn'
[email protected]c98c0c52011-04-06 13:39:4375 elif os.path.isdir(os.path.join(root, '.git')):
[email protected]5c8c6de2011-03-18 16:20:1876 return 'git'
77 else:
[email protected]c98c0c52011-04-06 13:39:4378 try:
79 subprocess2.check_output(
[email protected]5c8c6de2011-03-18 16:20:1880 ['git', 'rev-parse', '--show-cdup'],
[email protected]c98c0c52011-04-06 13:39:4381 stdout=subprocess2.VOID,
82 cwd=root)
[email protected]5c8c6de2011-03-18 16:20:1883 return 'git'
[email protected]c98c0c52011-04-06 13:39:4384 except (OSError, subprocess2.CalledProcessError):
[email protected]5c8c6de2011-03-18 16:20:1885 return None
86
87
[email protected]5aeb7dd2009-11-17 18:09:0188class GIT(object):
[email protected]5aeb7dd2009-11-17 18:09:0189 @staticmethod
[email protected]ad80e3b2010-09-09 14:18:2890 def Capture(args, **kwargs):
[email protected]da64d632011-09-08 17:41:1591 return subprocess2.check_output(
92 ['git'] + args, stderr=subprocess2.VOID, **kwargs)
[email protected]d5800f12009-11-12 20:03:4393
[email protected]5aeb7dd2009-11-17 18:09:0194 @staticmethod
[email protected]786fb682010-06-02 15:16:2395 def CaptureStatus(files, upstream_branch=None):
[email protected]5aeb7dd2009-11-17 18:09:0196 """Returns git status.
[email protected]d5800f12009-11-12 20:03:4397
[email protected]5aeb7dd2009-11-17 18:09:0198 @files can be a string (one file) or a list of files.
[email protected]d5800f12009-11-12 20:03:4399
[email protected]5aeb7dd2009-11-17 18:09:01100 Returns an array of (status, file) tuples."""
[email protected]786fb682010-06-02 15:16:23101 if upstream_branch is None:
102 upstream_branch = GIT.GetUpstreamBranch(os.getcwd())
103 if upstream_branch is None:
[email protected]ad80e3b2010-09-09 14:18:28104 raise gclient_utils.Error('Cannot determine upstream branch')
105 command = ['diff', '--name-status', '-r', '%s...' % upstream_branch]
[email protected]5aeb7dd2009-11-17 18:09:01106 if not files:
107 pass
108 elif isinstance(files, basestring):
109 command.append(files)
110 else:
111 command.extend(files)
[email protected]ad80e3b2010-09-09 14:18:28112 status = GIT.Capture(command).rstrip()
[email protected]5aeb7dd2009-11-17 18:09:01113 results = []
114 if status:
[email protected]ad80e3b2010-09-09 14:18:28115 for statusline in status.splitlines():
[email protected]cc1614b2010-09-20 17:13:17116 # 3-way merges can cause the status can be 'MMM' instead of 'M'. This
117 # can happen when the user has 2 local branches and he diffs between
118 # these 2 branches instead diffing to upstream.
119 m = re.match('^(\w)+\t(.+)$', statusline)
[email protected]5aeb7dd2009-11-17 18:09:01120 if not m:
[email protected]ad80e3b2010-09-09 14:18:28121 raise gclient_utils.Error(
122 'status currently unsupported: %s' % statusline)
[email protected]cc1614b2010-09-20 17:13:17123 # Only grab the first letter.
124 results.append(('%s ' % m.group(1)[0], m.group(2)))
[email protected]5aeb7dd2009-11-17 18:09:01125 return results
[email protected]d5800f12009-11-12 20:03:43126
[email protected]c78f2462009-11-21 01:20:57127 @staticmethod
[email protected]ad80e3b2010-09-09 14:18:28128 def GetEmail(cwd):
[email protected]c78f2462009-11-21 01:20:57129 """Retrieves the user email address if known."""
130 # We could want to look at the svn cred when it has a svn remote but it
131 # should be fine for now, users should simply configure their git settings.
[email protected]ad80e3b2010-09-09 14:18:28132 try:
133 return GIT.Capture(['config', 'user.email'], cwd=cwd).strip()
[email protected]da64d632011-09-08 17:41:15134 except subprocess2.CalledProcessError:
[email protected]ad80e3b2010-09-09 14:18:28135 return ''
[email protected]f2f9d552009-12-22 00:12:57136
137 @staticmethod
138 def ShortBranchName(branch):
139 """Converts a name like 'refs/heads/foo' to just 'foo'."""
140 return branch.replace('refs/heads/', '')
141
142 @staticmethod
143 def GetBranchRef(cwd):
[email protected]b24a8e12009-12-22 13:45:48144 """Returns the full branch reference, e.g. 'refs/heads/master'."""
[email protected]ad80e3b2010-09-09 14:18:28145 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd=cwd).strip()
[email protected]f2f9d552009-12-22 00:12:57146
147 @staticmethod
[email protected]b24a8e12009-12-22 13:45:48148 def GetBranch(cwd):
149 """Returns the short branch name, e.g. 'master'."""
[email protected]c308a742009-12-22 18:29:33150 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
[email protected]b24a8e12009-12-22 13:45:48151
152 @staticmethod
[email protected]f2f9d552009-12-22 00:12:57153 def IsGitSvn(cwd):
154 """Returns true if this repo looks like it's using git-svn."""
155 # If you have any "svn-remote.*" config keys, we think you're using svn.
156 try:
[email protected]ad80e3b2010-09-09 14:18:28157 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd=cwd)
[email protected]f2f9d552009-12-22 00:12:57158 return True
[email protected]da64d632011-09-08 17:41:15159 except subprocess2.CalledProcessError:
[email protected]f2f9d552009-12-22 00:12:57160 return False
161
162 @staticmethod
[email protected]866276c2011-03-18 20:09:31163 def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
164 """Return the corresponding git ref if |base_url| together with |glob_spec|
165 matches the full |url|.
166
167 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
168 """
169 fetch_suburl, as_ref = glob_spec.split(':')
170 if allow_wildcards:
171 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
172 if glob_match:
173 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
174 # "branches/{472,597,648}/src:refs/remotes/svn/*".
175 branch_re = re.escape(base_url)
176 if glob_match.group(1):
177 branch_re += '/' + re.escape(glob_match.group(1))
178 wildcard = glob_match.group(2)
179 if wildcard == '*':
180 branch_re += '([^/]*)'
181 else:
182 # Escape and replace surrounding braces with parentheses and commas
183 # with pipe symbols.
184 wildcard = re.escape(wildcard)
185 wildcard = re.sub('^\\\\{', '(', wildcard)
186 wildcard = re.sub('\\\\,', '|', wildcard)
187 wildcard = re.sub('\\\\}$', ')', wildcard)
188 branch_re += wildcard
189 if glob_match.group(3):
190 branch_re += re.escape(glob_match.group(3))
191 match = re.match(branch_re, url)
192 if match:
193 return re.sub('\*$', match.group(1), as_ref)
194
195 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
196 if fetch_suburl:
197 full_url = base_url + '/' + fetch_suburl
198 else:
199 full_url = base_url
200 if full_url == url:
201 return as_ref
202 return None
203
204 @staticmethod
[email protected]f2f9d552009-12-22 00:12:57205 def GetSVNBranch(cwd):
206 """Returns the svn branch name if found."""
207 # Try to figure out which remote branch we're based on.
208 # Strategy:
[email protected]ade368c2011-03-01 08:57:50209 # 1) iterate through our branch history and find the svn URL.
210 # 2) find the svn-remote that fetches from the URL.
[email protected]f2f9d552009-12-22 00:12:57211
212 # regexp matching the git-svn line that contains the URL.
213 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
214
[email protected]ade368c2011-03-01 08:57:50215 # We don't want to go through all of history, so read a line from the
216 # pipe at a time.
217 # The -100 is an arbitrary limit so we don't search forever.
218 cmd = ['git', 'log', '-100', '--pretty=medium']
[email protected]da64d632011-09-08 17:41:15219 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
[email protected]e8c28622011-04-05 14:41:44220 url = None
[email protected]ade368c2011-03-01 08:57:50221 for line in proc.stdout:
222 match = git_svn_re.match(line)
223 if match:
224 url = match.group(1)
225 proc.stdout.close() # Cut pipe.
226 break
[email protected]f2f9d552009-12-22 00:12:57227
[email protected]ade368c2011-03-01 08:57:50228 if url:
229 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
230 remotes = GIT.Capture(['config', '--get-regexp',
231 r'^svn-remote\..*\.url'], cwd=cwd).splitlines()
232 for remote in remotes:
233 match = svn_remote_re.match(remote)
[email protected]f2f9d552009-12-22 00:12:57234 if match:
[email protected]ade368c2011-03-01 08:57:50235 remote = match.group(1)
236 base_url = match.group(2)
[email protected]866276c2011-03-18 20:09:31237 try:
238 fetch_spec = GIT.Capture(
239 ['config', 'svn-remote.%s.fetch' % remote],
240 cwd=cwd).strip()
241 branch = GIT.MatchSvnGlob(url, base_url, fetch_spec, False)
[email protected]da64d632011-09-08 17:41:15242 except subprocess2.CalledProcessError:
[email protected]866276c2011-03-18 20:09:31243 branch = None
244 if branch:
245 return branch
246 try:
247 branch_spec = GIT.Capture(
248 ['config', 'svn-remote.%s.branches' % remote],
249 cwd=cwd).strip()
250 branch = GIT.MatchSvnGlob(url, base_url, branch_spec, True)
[email protected]da64d632011-09-08 17:41:15251 except subprocess2.CalledProcessError:
[email protected]866276c2011-03-18 20:09:31252 branch = None
253 if branch:
254 return branch
255 try:
256 tag_spec = GIT.Capture(
257 ['config', 'svn-remote.%s.tags' % remote],
258 cwd=cwd).strip()
259 branch = GIT.MatchSvnGlob(url, base_url, tag_spec, True)
[email protected]da64d632011-09-08 17:41:15260 except subprocess2.CalledProcessError:
[email protected]866276c2011-03-18 20:09:31261 branch = None
262 if branch:
263 return branch
[email protected]f2f9d552009-12-22 00:12:57264
265 @staticmethod
266 def FetchUpstreamTuple(cwd):
267 """Returns a tuple containg remote and remote ref,
268 e.g. 'origin', 'refs/heads/master'
[email protected]81e012c2010-04-29 16:07:24269 Tries to be intelligent and understand git-svn.
[email protected]f2f9d552009-12-22 00:12:57270 """
271 remote = '.'
[email protected]b24a8e12009-12-22 13:45:48272 branch = GIT.GetBranch(cwd)
[email protected]ad80e3b2010-09-09 14:18:28273 try:
274 upstream_branch = GIT.Capture(
275 ['config', 'branch.%s.merge' % branch], cwd=cwd).strip()
[email protected]da64d632011-09-08 17:41:15276 except subprocess2.CalledProcessError:
[email protected]ad80e3b2010-09-09 14:18:28277 upstream_branch = None
[email protected]f2f9d552009-12-22 00:12:57278 if upstream_branch:
[email protected]ad80e3b2010-09-09 14:18:28279 try:
280 remote = GIT.Capture(
281 ['config', 'branch.%s.remote' % branch], cwd=cwd).strip()
[email protected]da64d632011-09-08 17:41:15282 except subprocess2.CalledProcessError:
[email protected]ad80e3b2010-09-09 14:18:28283 pass
[email protected]f2f9d552009-12-22 00:12:57284 else:
[email protected]ade368c2011-03-01 08:57:50285 try:
286 upstream_branch = GIT.Capture(
287 ['config', 'rietveld.upstream-branch'], cwd=cwd).strip()
[email protected]da64d632011-09-08 17:41:15288 except subprocess2.CalledProcessError:
[email protected]ade368c2011-03-01 08:57:50289 upstream_branch = None
290 if upstream_branch:
291 try:
292 remote = GIT.Capture(
293 ['config', 'rietveld.upstream-remote'], cwd=cwd).strip()
[email protected]da64d632011-09-08 17:41:15294 except subprocess2.CalledProcessError:
[email protected]ade368c2011-03-01 08:57:50295 pass
[email protected]81e012c2010-04-29 16:07:24296 else:
[email protected]ade368c2011-03-01 08:57:50297 # Fall back on trying a git-svn upstream branch.
298 if GIT.IsGitSvn(cwd):
299 upstream_branch = GIT.GetSVNBranch(cwd)
[email protected]a630bd72010-04-29 23:32:34300 else:
[email protected]ade368c2011-03-01 08:57:50301 # Else, try to guess the origin remote.
302 remote_branches = GIT.Capture(['branch', '-r'], cwd=cwd).split()
303 if 'origin/master' in remote_branches:
304 # Fall back on origin/master if it exits.
305 remote = 'origin'
306 upstream_branch = 'refs/heads/master'
307 elif 'origin/trunk' in remote_branches:
308 # Fall back on origin/trunk if it exists. Generally a shared
309 # git-svn clone
310 remote = 'origin'
311 upstream_branch = 'refs/heads/trunk'
312 else:
313 # Give up.
314 remote = None
315 upstream_branch = None
[email protected]f2f9d552009-12-22 00:12:57316 return remote, upstream_branch
317
318 @staticmethod
[email protected]81e012c2010-04-29 16:07:24319 def GetUpstreamBranch(cwd):
[email protected]f2f9d552009-12-22 00:12:57320 """Gets the current branch's upstream branch."""
321 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
[email protected]a630bd72010-04-29 23:32:34322 if remote != '.' and upstream_branch:
[email protected]f2f9d552009-12-22 00:12:57323 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
324 return upstream_branch
325
326 @staticmethod
[email protected]8ede00e2010-01-12 14:35:28327 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
328 files=None):
[email protected]a9371762009-12-22 18:27:38329 """Diffs against the upstream branch or optionally another branch.
330
331 full_move means that move or copy operations should completely recreate the
332 files, usually in the prospect to apply the patch for a try job."""
[email protected]f2f9d552009-12-22 00:12:57333 if not branch:
[email protected]81e012c2010-04-29 16:07:24334 branch = GIT.GetUpstreamBranch(cwd)
[email protected]400f3e72010-05-19 14:23:36335 command = ['diff', '-p', '--no-prefix', '--no-ext-diff',
336 branch + "..." + branch_head]
[email protected]a9371762009-12-22 18:27:38337 if not full_move:
338 command.append('-C')
[email protected]8ede00e2010-01-12 14:35:28339 # TODO(maruel): --binary support.
340 if files:
341 command.append('--')
342 command.extend(files)
[email protected]ad80e3b2010-09-09 14:18:28343 diff = GIT.Capture(command, cwd=cwd).splitlines(True)
[email protected]f2f9d552009-12-22 00:12:57344 for i in range(len(diff)):
345 # In the case of added files, replace /dev/null with the path to the
346 # file being added.
347 if diff[i].startswith('--- /dev/null'):
348 diff[i] = '--- %s' % diff[i+1][4:]
349 return ''.join(diff)
[email protected]c78f2462009-11-21 01:20:57350
[email protected]b24a8e12009-12-22 13:45:48351 @staticmethod
[email protected]8ede00e2010-01-12 14:35:28352 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
353 """Returns the list of modified files between two branches."""
354 if not branch:
[email protected]81e012c2010-04-29 16:07:24355 branch = GIT.GetUpstreamBranch(cwd)
[email protected]838f0f22010-04-09 17:02:50356 command = ['diff', '--name-only', branch + "..." + branch_head]
[email protected]ad80e3b2010-09-09 14:18:28357 return GIT.Capture(command, cwd=cwd).splitlines(False)
[email protected]8ede00e2010-01-12 14:35:28358
359 @staticmethod
[email protected]b24a8e12009-12-22 13:45:48360 def GetPatchName(cwd):
361 """Constructs a name for this patch."""
[email protected]ad80e3b2010-09-09 14:18:28362 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd=cwd).strip()
[email protected]862ff8e2010-08-06 15:29:16363 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
[email protected]b24a8e12009-12-22 13:45:48364
365 @staticmethod
[email protected]ad80e3b2010-09-09 14:18:28366 def GetCheckoutRoot(cwd):
[email protected]01d8c1d2010-01-07 01:56:59367 """Returns the top level directory of a git checkout as an absolute path.
[email protected]b24a8e12009-12-22 13:45:48368 """
[email protected]ad80e3b2010-09-09 14:18:28369 root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd).strip()
370 return os.path.abspath(os.path.join(cwd, root))
[email protected]b24a8e12009-12-22 13:45:48371
[email protected]d0f854a2010-03-11 19:35:53372 @staticmethod
373 def AssertVersion(min_version):
374 """Asserts git's version is at least min_version."""
375 def only_int(val):
376 if val.isdigit():
377 return int(val)
378 else:
379 return 0
[email protected]ad80e3b2010-09-09 14:18:28380 current_version = GIT.Capture(['--version']).split()[-1]
[email protected]d0f854a2010-03-11 19:35:53381 current_version_list = map(only_int, current_version.split('.'))
382 for min_ver in map(int, min_version.split('.')):
383 ver = current_version_list.pop(0)
384 if ver < min_ver:
385 return (False, current_version)
386 elif ver > min_ver:
387 return (True, current_version)
388 return (True, current_version)
389
[email protected]d5800f12009-11-12 20:03:43390
[email protected]5aeb7dd2009-11-17 18:09:01391class SVN(object):
[email protected]57564662010-04-14 02:35:12392 current_version = None
[email protected]d5800f12009-11-12 20:03:43393
[email protected]5aeb7dd2009-11-17 18:09:01394 @staticmethod
[email protected]54019f32010-09-09 13:50:11395 def Capture(args, **kwargs):
396 """Always redirect stderr.
[email protected]d5800f12009-11-12 20:03:43397
[email protected]da64d632011-09-08 17:41:15398 Throws an exception if non-0 is returned.
399 """
400 return subprocess2.check_output(['svn'] + args, **kwargs)
[email protected]d5800f12009-11-12 20:03:43401
[email protected]5aeb7dd2009-11-17 18:09:01402 @staticmethod
[email protected]2b9aa8e2010-08-25 20:01:42403 def RunAndGetFileList(verbose, args, cwd, file_list, stdout=None):
[email protected]5aeb7dd2009-11-17 18:09:01404 """Runs svn checkout, update, or status, output to stdout.
[email protected]d5800f12009-11-12 20:03:43405
[email protected]5aeb7dd2009-11-17 18:09:01406 The first item in args must be either "checkout", "update", or "status".
[email protected]d5800f12009-11-12 20:03:43407
[email protected]5aeb7dd2009-11-17 18:09:01408 svn's stdout is parsed to collect a list of files checked out or updated.
409 These files are appended to file_list. svn's stdout is also printed to
410 sys.stdout as in Run.
[email protected]d5800f12009-11-12 20:03:43411
[email protected]5aeb7dd2009-11-17 18:09:01412 Args:
[email protected]03807072010-08-16 17:18:44413 verbose: If True, uses verbose output
[email protected]5aeb7dd2009-11-17 18:09:01414 args: A sequence of command line parameters to be passed to svn.
[email protected]2b9aa8e2010-08-25 20:01:42415 cwd: The directory where svn is to be run.
[email protected]d5800f12009-11-12 20:03:43416
[email protected]5aeb7dd2009-11-17 18:09:01417 Raises:
418 Error: An error occurred while running the svn command.
419 """
[email protected]2b9aa8e2010-08-25 20:01:42420 stdout = stdout or sys.stdout
[email protected]d5800f12009-11-12 20:03:43421
[email protected]5aeb7dd2009-11-17 18:09:01422 # svn update and svn checkout use the same pattern: the first three columns
423 # are for file status, property status, and lock status. This is followed
424 # by two spaces, and then the path to the file.
425 update_pattern = '^... (.*)$'
[email protected]d5800f12009-11-12 20:03:43426
[email protected]5aeb7dd2009-11-17 18:09:01427 # The first three columns of svn status are the same as for svn update and
428 # svn checkout. The next three columns indicate addition-with-history,
429 # switch, and remote lock status. This is followed by one space, and then
430 # the path to the file.
431 status_pattern = '^...... (.*)$'
[email protected]d5800f12009-11-12 20:03:43432
[email protected]5aeb7dd2009-11-17 18:09:01433 # args[0] must be a supported command. This will blow up if it's something
434 # else, which is good. Note that the patterns are only effective when
435 # these commands are used in their ordinary forms, the patterns are invalid
436 # for "svn status --show-updates", for example.
437 pattern = {
438 'checkout': update_pattern,
439 'status': status_pattern,
440 'update': update_pattern,
441 }[args[0]]
[email protected]5aeb7dd2009-11-17 18:09:01442 compiled_pattern = re.compile(pattern)
[email protected]b71b67e2009-11-24 20:48:19443 # Place an upper limit.
[email protected]2aee22982010-09-03 14:15:25444 backoff_time = 5
[email protected]bec588d2010-10-26 13:50:25445 retries = 0
[email protected]03507062010-10-26 00:58:27446 while True:
[email protected]bec588d2010-10-26 13:50:25447 retries += 1
[email protected]b71b67e2009-11-24 20:48:19448 previous_list_len = len(file_list)
449 failure = []
[email protected]54d1f1a2010-01-08 19:53:47450
[email protected]b71b67e2009-11-24 20:48:19451 def CaptureMatchingLines(line):
452 match = compiled_pattern.search(line)
453 if match:
454 file_list.append(match.group(1))
455 if line.startswith('svn: '):
[email protected]8599aa72010-02-08 20:27:14456 failure.append(line)
[email protected]54d1f1a2010-01-08 19:53:47457
[email protected]b71b67e2009-11-24 20:48:19458 try:
[email protected]17d01792010-09-01 18:07:10459 gclient_utils.CheckCallAndFilterAndHeader(
460 ['svn'] + args,
461 cwd=cwd,
462 always=verbose,
463 filter_fn=CaptureMatchingLines,
464 stdout=stdout)
[email protected]da64d632011-09-08 17:41:15465 except subprocess2.CalledProcessError:
[email protected]6133c5b2010-08-18 18:34:48466 def IsKnownFailure():
467 for x in failure:
468 if (x.startswith('svn: OPTIONS of') or
469 x.startswith('svn: PROPFIND of') or
470 x.startswith('svn: REPORT of') or
[email protected]f61fc932010-08-19 13:05:24471 x.startswith('svn: Unknown hostname') or
472 x.startswith('svn: Server sent unexpected return value')):
[email protected]6133c5b2010-08-18 18:34:48473 return True
474 return False
475
[email protected]953586a2010-06-15 14:22:24476 # Subversion client is really misbehaving with Google Code.
477 if args[0] == 'checkout':
478 # Ensure at least one file was checked out, otherwise *delete* the
479 # directory.
480 if len(file_list) == previous_list_len:
[email protected]6133c5b2010-08-18 18:34:48481 if not IsKnownFailure():
[email protected]953586a2010-06-15 14:22:24482 # No known svn error was found, bail out.
483 raise
[email protected]6133c5b2010-08-18 18:34:48484 # No file were checked out, so make sure the directory is
485 # deleted in case it's messed up and try again.
486 # Warning: It's bad, it assumes args[2] is the directory
487 # argument.
488 if os.path.isdir(args[2]):
489 gclient_utils.RemoveDirectory(args[2])
[email protected]953586a2010-06-15 14:22:24490 else:
491 # Progress was made, convert to update since an aborted checkout
492 # is now an update.
[email protected]2de10252010-02-08 01:10:39493 args = ['update'] + args[1:]
[email protected]953586a2010-06-15 14:22:24494 else:
495 # It was an update or export.
[email protected]6133c5b2010-08-18 18:34:48496 # We enforce that some progress has been made or a known failure.
497 if len(file_list) == previous_list_len and not IsKnownFailure():
498 # No known svn error was found and no progress, bail out.
499 raise
[email protected]bec588d2010-10-26 13:50:25500 if retries == 10:
[email protected]03507062010-10-26 00:58:27501 raise
[email protected]2aee22982010-09-03 14:15:25502 print "Sleeping %.1f seconds and retrying...." % backoff_time
503 time.sleep(backoff_time)
504 backoff_time *= 1.3
[email protected]953586a2010-06-15 14:22:24505 continue
[email protected]b71b67e2009-11-24 20:48:19506 break
[email protected]d5800f12009-11-12 20:03:43507
[email protected]5aeb7dd2009-11-17 18:09:01508 @staticmethod
[email protected]54019f32010-09-09 13:50:11509 def CaptureInfo(cwd):
[email protected]5aeb7dd2009-11-17 18:09:01510 """Returns a dictionary from the svn info output for the given file.
[email protected]d5800f12009-11-12 20:03:43511
[email protected]54019f32010-09-09 13:50:11512 Throws an exception if svn info fails."""
[email protected]d25fb8f2011-04-07 13:40:15513 result = {}
[email protected]ade9c592011-04-07 15:59:11514 output = SVN.Capture(['info', '--xml', cwd])
515 info = ElementTree.XML(output)
516 if info is None:
517 return result
518 entry = info.find('entry')
[email protected]6f323bb2011-04-26 15:42:53519 if entry is None:
520 return result
[email protected]ade9c592011-04-07 15:59:11521
522 # Use .text when the item is not optional.
523 result['Path'] = entry.attrib['path']
524 result['Revision'] = int(entry.attrib['revision'])
525 result['Node Kind'] = entry.attrib['kind']
526 # Differs across versions.
527 if result['Node Kind'] == 'dir':
528 result['Node Kind'] = 'directory'
529 result['URL'] = entry.find('url').text
530 repository = entry.find('repository')
531 result['Repository Root'] = repository.find('root').text
532 result['UUID'] = repository.find('uuid')
533 wc_info = entry.find('wc-info')
534 if wc_info is not None:
535 result['Schedule'] = wc_info.find('schedule').text
536 result['Copied From URL'] = wc_info.find('copy-from-url')
537 result['Copied From Rev'] = wc_info.find('copy-from-rev')
538 else:
539 result['Schedule'] = None
540 result['Copied From URL'] = None
541 result['Copied From Rev'] = None
542 for key in result.keys():
543 if isinstance(result[key], unicode):
544 # Unicode results interferes with the higher layers matching up things
545 # in the deps dictionary.
546 result[key] = result[key].encode()
547 # Automatic conversion of optional parameters.
548 result[key] = getattr(result[key], 'text', result[key])
[email protected]5aeb7dd2009-11-17 18:09:01549 return result
[email protected]d5800f12009-11-12 20:03:43550
[email protected]5aeb7dd2009-11-17 18:09:01551 @staticmethod
[email protected]54019f32010-09-09 13:50:11552 def CaptureRevision(cwd):
[email protected]5d63eb82010-03-24 23:22:09553 """Get the base revision of a SVN repository.
554
555 Returns:
556 Int base revision
557 """
[email protected]ade9c592011-04-07 15:59:11558 return SVN.CaptureInfo(cwd).get('Revision')
[email protected]5d63eb82010-03-24 23:22:09559
560 @staticmethod
[email protected]5aeb7dd2009-11-17 18:09:01561 def CaptureStatus(files):
562 """Returns the svn 1.5 svn status emulated output.
[email protected]d5800f12009-11-12 20:03:43563
[email protected]5aeb7dd2009-11-17 18:09:01564 @files can be a string (one file) or a list of files.
[email protected]d5800f12009-11-12 20:03:43565
[email protected]5aeb7dd2009-11-17 18:09:01566 Returns an array of (status, file) tuples."""
567 command = ["status", "--xml"]
568 if not files:
569 pass
570 elif isinstance(files, basestring):
571 command.append(files)
572 else:
573 command.extend(files)
[email protected]d5800f12009-11-12 20:03:43574
[email protected]5aeb7dd2009-11-17 18:09:01575 status_letter = {
576 None: ' ',
577 '': ' ',
578 'added': 'A',
579 'conflicted': 'C',
580 'deleted': 'D',
581 'external': 'X',
582 'ignored': 'I',
583 'incomplete': '!',
584 'merged': 'G',
585 'missing': '!',
586 'modified': 'M',
587 'none': ' ',
588 'normal': ' ',
589 'obstructed': '~',
590 'replaced': 'R',
591 'unversioned': '?',
592 }
[email protected]ade9c592011-04-07 15:59:11593 dom = ElementTree.XML(SVN.Capture(command))
[email protected]5aeb7dd2009-11-17 18:09:01594 results = []
[email protected]ade9c592011-04-07 15:59:11595 if dom is None:
596 return results
597 # /status/target/entry/(wc-status|commit|author|date)
598 for target in dom.findall('target'):
599 for entry in target.findall('entry'):
600 file_path = entry.attrib['path']
601 wc_status = entry.find('wc-status')
602 # Emulate svn 1.5 status ouput...
603 statuses = [' '] * 7
604 # Col 0
605 xml_item_status = wc_status.attrib['item']
606 if xml_item_status in status_letter:
607 statuses[0] = status_letter[xml_item_status]
608 else:
609 raise gclient_utils.Error(
610 'Unknown item status "%s"; please implement me!' %
611 xml_item_status)
612 # Col 1
613 xml_props_status = wc_status.attrib['props']
614 if xml_props_status == 'modified':
615 statuses[1] = 'M'
616 elif xml_props_status == 'conflicted':
617 statuses[1] = 'C'
618 elif (not xml_props_status or xml_props_status == 'none' or
619 xml_props_status == 'normal'):
620 pass
621 else:
622 raise gclient_utils.Error(
623 'Unknown props status "%s"; please implement me!' %
624 xml_props_status)
625 # Col 2
626 if wc_status.attrib.get('wc-locked') == 'true':
627 statuses[2] = 'L'
628 # Col 3
629 if wc_status.attrib.get('copied') == 'true':
630 statuses[3] = '+'
631 # Col 4
632 if wc_status.attrib.get('switched') == 'true':
633 statuses[4] = 'S'
634 # TODO(maruel): Col 5 and 6
635 item = (''.join(statuses), file_path)
636 results.append(item)
[email protected]5aeb7dd2009-11-17 18:09:01637 return results
[email protected]d5800f12009-11-12 20:03:43638
[email protected]5aeb7dd2009-11-17 18:09:01639 @staticmethod
640 def IsMoved(filename):
641 """Determine if a file has been added through svn mv"""
[email protected]3c55d982010-05-06 14:25:44642 return SVN.IsMovedInfo(SVN.CaptureInfo(filename))
643
644 @staticmethod
645 def IsMovedInfo(info):
646 """Determine if a file has been added through svn mv"""
[email protected]5aeb7dd2009-11-17 18:09:01647 return (info.get('Copied From URL') and
648 info.get('Copied From Rev') and
649 info.get('Schedule') == 'add')
[email protected]d5800f12009-11-12 20:03:43650
[email protected]5aeb7dd2009-11-17 18:09:01651 @staticmethod
[email protected]6e29d572010-06-04 17:32:20652 def GetFileProperty(filename, property_name):
[email protected]5aeb7dd2009-11-17 18:09:01653 """Returns the value of an SVN property for the given file.
[email protected]d5800f12009-11-12 20:03:43654
[email protected]5aeb7dd2009-11-17 18:09:01655 Args:
[email protected]6e29d572010-06-04 17:32:20656 filename: The file to check
[email protected]5aeb7dd2009-11-17 18:09:01657 property_name: The name of the SVN property, e.g. "svn:mime-type"
[email protected]d5800f12009-11-12 20:03:43658
[email protected]5aeb7dd2009-11-17 18:09:01659 Returns:
660 The value of the property, which will be the empty string if the property
661 is not set on the file. If the file is not under version control, the
662 empty string is also returned.
663 """
[email protected]54019f32010-09-09 13:50:11664 try:
665 return SVN.Capture(['propget', property_name, filename])
[email protected]da64d632011-09-08 17:41:15666 except subprocess2.CalledProcessError:
[email protected]54019f32010-09-09 13:50:11667 return ''
[email protected]d5800f12009-11-12 20:03:43668
[email protected]5aeb7dd2009-11-17 18:09:01669 @staticmethod
[email protected]1c7db8e2010-01-07 02:00:19670 def DiffItem(filename, full_move=False, revision=None):
[email protected]f2f9d552009-12-22 00:12:57671 """Diffs a single file.
672
[email protected]3c55d982010-05-06 14:25:44673 Should be simple, eh? No it isn't.
[email protected]f2f9d552009-12-22 00:12:57674 Be sure to be in the appropriate directory before calling to have the
[email protected]a9371762009-12-22 18:27:38675 expected relative path.
676 full_move means that move or copy operations should completely recreate the
677 files, usually in the prospect to apply the patch for a try job."""
[email protected]5aeb7dd2009-11-17 18:09:01678 # If the user specified a custom diff command in their svn config file,
679 # then it'll be used when we do svn diff, which we don't want to happen
680 # since we want the unified diff. Using --diff-cmd=diff doesn't always
681 # work, since they can have another diff executable in their path that
682 # gives different line endings. So we use a bogus temp directory as the
683 # config directory, which gets around these problems.
[email protected]f2f9d552009-12-22 00:12:57684 bogus_dir = tempfile.mkdtemp()
685 try:
[email protected]3c55d982010-05-06 14:25:44686 # Use "svn info" output instead of os.path.isdir because the latter fails
687 # when the file is deleted.
[email protected]6e29d572010-06-04 17:32:20688 return SVN._DiffItemInternal(filename, SVN.CaptureInfo(filename),
689 bogus_dir,
[email protected]3c55d982010-05-06 14:25:44690 full_move=full_move, revision=revision)
691 finally:
[email protected]da64d632011-09-08 17:41:15692 gclient_utils.RemoveDirectory(bogus_dir)
[email protected]3c55d982010-05-06 14:25:44693
694 @staticmethod
695 def _DiffItemInternal(filename, info, bogus_dir, full_move=False,
696 revision=None):
697 """Grabs the diff data."""
698 command = ["diff", "--config-dir", bogus_dir, filename]
699 if revision:
700 command.extend(['--revision', revision])
701 data = None
702 if SVN.IsMovedInfo(info):
703 if full_move:
704 if info.get("Node Kind") == "directory":
705 # Things become tricky here. It's a directory copy/move. We need to
706 # diff all the files inside it.
707 # This will put a lot of pressure on the heap. This is why StringIO
708 # is used and converted back into a string at the end. The reason to
709 # return a string instead of a StringIO is that StringIO.write()
710 # doesn't accept a StringIO object. *sigh*.
711 for (dirpath, dirnames, filenames) in os.walk(filename):
712 # Cleanup all files starting with a '.'.
713 for d in dirnames:
714 if d.startswith('.'):
715 dirnames.remove(d)
716 for f in filenames:
717 if f.startswith('.'):
718 filenames.remove(f)
719 for f in filenames:
720 if data is None:
721 data = cStringIO.StringIO()
722 data.write(GenFakeDiff(os.path.join(dirpath, f)))
723 if data:
724 tmp = data.getvalue()
725 data.close()
726 data = tmp
[email protected]f2f9d552009-12-22 00:12:57727 else:
[email protected]3c55d982010-05-06 14:25:44728 data = GenFakeDiff(filename)
729 else:
730 if info.get("Node Kind") != "directory":
[email protected]0836c562010-01-22 01:10:06731 # svn diff on a mv/cp'd file outputs nothing if there was no change.
[email protected]54019f32010-09-09 13:50:11732 data = SVN.Capture(command)
[email protected]0836c562010-01-22 01:10:06733 if not data:
734 # We put in an empty Index entry so upload.py knows about them.
[email protected]c6d170e2010-06-03 00:06:00735 data = "Index: %s\n" % filename.replace(os.sep, '/')
[email protected]3c55d982010-05-06 14:25:44736 # Otherwise silently ignore directories.
737 else:
738 if info.get("Node Kind") != "directory":
739 # Normal simple case.
[email protected]f8b3f942011-03-24 17:33:50740 try:
741 data = SVN.Capture(command)
[email protected]da64d632011-09-08 17:41:15742 except subprocess2.CalledProcessError:
[email protected]f8b3f942011-03-24 17:33:50743 if revision:
744 data = GenFakeDiff(filename)
745 else:
746 raise
[email protected]3c55d982010-05-06 14:25:44747 # Otherwise silently ignore directories.
[email protected]5aeb7dd2009-11-17 18:09:01748 return data
[email protected]c78f2462009-11-21 01:20:57749
750 @staticmethod
[email protected]1c7db8e2010-01-07 02:00:19751 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
[email protected]f2f9d552009-12-22 00:12:57752 """Returns a string containing the diff for the given file list.
753
754 The files in the list should either be absolute paths or relative to the
755 given root. If no root directory is provided, the repository root will be
756 used.
757 The diff will always use relative paths.
758 """
[email protected]00fdcb32011-02-24 01:41:02759 assert isinstance(filenames, (list, tuple))
[email protected]f2f9d552009-12-22 00:12:57760 previous_cwd = os.getcwd()
[email protected]fd9cbbb2010-01-08 23:04:03761 root = root or SVN.GetCheckoutRoot(previous_cwd)
762 root = os.path.normcase(os.path.join(root, ''))
[email protected]f2f9d552009-12-22 00:12:57763 def RelativePath(path, root):
764 """We must use relative paths."""
[email protected]fd9cbbb2010-01-08 23:04:03765 if os.path.normcase(path).startswith(root):
[email protected]f2f9d552009-12-22 00:12:57766 return path[len(root):]
767 return path
[email protected]3c55d982010-05-06 14:25:44768 # If the user specified a custom diff command in their svn config file,
769 # then it'll be used when we do svn diff, which we don't want to happen
770 # since we want the unified diff. Using --diff-cmd=diff doesn't always
771 # work, since they can have another diff executable in their path that
772 # gives different line endings. So we use a bogus temp directory as the
773 # config directory, which gets around these problems.
774 bogus_dir = tempfile.mkdtemp()
[email protected]f2f9d552009-12-22 00:12:57775 try:
776 os.chdir(root)
[email protected]3c55d982010-05-06 14:25:44777 # Cleanup filenames
778 filenames = [RelativePath(f, root) for f in filenames]
779 # Get information about the modified items (files and directories)
780 data = dict([(f, SVN.CaptureInfo(f)) for f in filenames])
[email protected]3fda4cc2010-06-29 13:29:27781 diffs = []
[email protected]3c55d982010-05-06 14:25:44782 if full_move:
783 # Eliminate modified files inside moved/copied directory.
784 for (filename, info) in data.iteritems():
785 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
786 # Remove files inside the directory.
787 filenames = [f for f in filenames
788 if not f.startswith(filename + os.path.sep)]
789 for filename in data.keys():
790 if not filename in filenames:
791 # Remove filtered out items.
792 del data[filename]
[email protected]3fda4cc2010-06-29 13:29:27793 else:
794 metaheaders = []
795 for (filename, info) in data.iteritems():
796 if SVN.IsMovedInfo(info):
797 # for now, the most common case is a head copy,
798 # so let's just encode that as a straight up cp.
799 srcurl = info.get('Copied From URL')
800 root = info.get('Repository Root')
801 rev = int(info.get('Copied From Rev'))
802 assert srcurl.startswith(root)
803 src = srcurl[len(root)+1:]
[email protected]00fdcb32011-02-24 01:41:02804 try:
805 srcinfo = SVN.CaptureInfo(srcurl)
[email protected]da64d632011-09-08 17:41:15806 except subprocess2.CalledProcessError, e:
[email protected]00fdcb32011-02-24 01:41:02807 if not 'Not a valid URL' in e.stderr:
808 raise
809 # Assume the file was deleted. No idea how to figure out at which
810 # revision the file was deleted.
811 srcinfo = {'Revision': rev}
[email protected]3fda4cc2010-06-29 13:29:27812 if (srcinfo.get('Revision') != rev and
813 SVN.Capture(['diff', '-r', '%d:head' % rev, srcurl])):
814 metaheaders.append("#$ svn cp -r %d %s %s "
815 "### WARNING: note non-trunk copy\n" %
816 (rev, src, filename))
817 else:
818 metaheaders.append("#$ cp %s %s\n" % (src,
819 filename))
820
821 if metaheaders:
822 diffs.append("### BEGIN SVN COPY METADATA\n")
823 diffs.extend(metaheaders)
824 diffs.append("### END SVN COPY METADATA\n")
[email protected]3c55d982010-05-06 14:25:44825 # Now ready to do the actual diff.
[email protected]3c55d982010-05-06 14:25:44826 for filename in sorted(data.iterkeys()):
827 diffs.append(SVN._DiffItemInternal(filename, data[filename], bogus_dir,
828 full_move=full_move,
829 revision=revision))
830 # Use StringIO since it can be messy when diffing a directory move with
831 # full_move=True.
832 buf = cStringIO.StringIO()
833 for d in filter(None, diffs):
834 buf.write(d)
835 result = buf.getvalue()
836 buf.close()
837 return result
[email protected]f2f9d552009-12-22 00:12:57838 finally:
839 os.chdir(previous_cwd)
[email protected]da64d632011-09-08 17:41:15840 gclient_utils.RemoveDirectory(bogus_dir)
[email protected]f2f9d552009-12-22 00:12:57841
842 @staticmethod
[email protected]c78f2462009-11-21 01:20:57843 def GetEmail(repo_root):
844 """Retrieves the svn account which we assume is an email address."""
[email protected]54019f32010-09-09 13:50:11845 try:
846 infos = SVN.CaptureInfo(repo_root)
[email protected]da64d632011-09-08 17:41:15847 except subprocess2.CalledProcessError:
[email protected]c78f2462009-11-21 01:20:57848 return None
849
850 # Should check for uuid but it is incorrectly saved for https creds.
[email protected]54019f32010-09-09 13:50:11851 root = infos['Repository Root']
[email protected]c78f2462009-11-21 01:20:57852 realm = root.rsplit('/', 1)[0]
[email protected]54019f32010-09-09 13:50:11853 uuid = infos['UUID']
[email protected]c78f2462009-11-21 01:20:57854 if root.startswith('https') or not uuid:
855 regexp = re.compile(r'<%s:\d+>.*' % realm)
856 else:
857 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
858 if regexp is None:
859 return None
860 if sys.platform.startswith('win'):
861 if not 'APPDATA' in os.environ:
862 return None
[email protected]720d9f32009-11-21 17:38:57863 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
864 'svn.simple')
[email protected]c78f2462009-11-21 01:20:57865 else:
866 if not 'HOME' in os.environ:
867 return None
868 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
869 'svn.simple')
870 for credfile in os.listdir(auth_dir):
871 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
872 if regexp.match(cred_info.get('svn:realmstring')):
873 return cred_info.get('username')
874
875 @staticmethod
876 def ReadSimpleAuth(filename):
877 f = open(filename, 'r')
878 values = {}
[email protected]6e29d572010-06-04 17:32:20879 def ReadOneItem(item_type):
880 m = re.match(r'%s (\d+)' % item_type, f.readline())
[email protected]c78f2462009-11-21 01:20:57881 if not m:
882 return None
883 data = f.read(int(m.group(1)))
884 if f.read(1) != '\n':
885 return None
886 return data
887
888 while True:
889 key = ReadOneItem('K')
890 if not key:
891 break
892 value = ReadOneItem('V')
893 if not value:
894 break
895 values[key] = value
896 return values
[email protected]94b1ee92009-12-19 20:27:20897
898 @staticmethod
899 def GetCheckoutRoot(directory):
900 """Returns the top level directory of the current repository.
901
902 The directory is returned as an absolute path.
903 """
[email protected]f7ae6d52009-12-22 20:49:04904 directory = os.path.abspath(directory)
[email protected]54019f32010-09-09 13:50:11905 try:
[email protected]885d6e82011-02-24 20:21:46906 info = SVN.CaptureInfo(directory)
907 cur_dir_repo_root = info['Repository Root']
908 url = info['URL']
[email protected]da64d632011-09-08 17:41:15909 except subprocess2.CalledProcessError:
[email protected]94b1ee92009-12-19 20:27:20910 return None
[email protected]94b1ee92009-12-19 20:27:20911 while True:
912 parent = os.path.dirname(directory)
[email protected]54019f32010-09-09 13:50:11913 try:
[email protected]885d6e82011-02-24 20:21:46914 info = SVN.CaptureInfo(parent)
915 if (info['Repository Root'] != cur_dir_repo_root or
916 info['URL'] != os.path.dirname(url)):
[email protected]54019f32010-09-09 13:50:11917 break
[email protected]885d6e82011-02-24 20:21:46918 url = info['URL']
[email protected]da64d632011-09-08 17:41:15919 except subprocess2.CalledProcessError:
[email protected]94b1ee92009-12-19 20:27:20920 break
921 directory = parent
[email protected]fd9cbbb2010-01-08 23:04:03922 return GetCasedPath(directory)
[email protected]57564662010-04-14 02:35:12923
924 @staticmethod
925 def AssertVersion(min_version):
926 """Asserts svn's version is at least min_version."""
927 def only_int(val):
928 if val.isdigit():
929 return int(val)
930 else:
931 return 0
932 if not SVN.current_version:
933 SVN.current_version = SVN.Capture(['--version']).split()[2]
934 current_version_list = map(only_int, SVN.current_version.split('.'))
935 for min_ver in map(int, min_version.split('.')):
936 ver = current_version_list.pop(0)
937 if ver < min_ver:
938 return (False, SVN.current_version)
939 elif ver > min_ver:
940 return (True, SVN.current_version)
941 return (True, SVN.current_version)
[email protected]07ab60e2011-02-08 21:54:00942
943 @staticmethod
944 def Revert(repo_root, callback=None, ignore_externals=False):
945 """Reverts all svn modifications in repo_root, including properties.
946
947 Deletes any modified files or directory.
948
949 A "svn update --revision BASE" call is required after to revive deleted
950 files.
951 """
952 for file_status in SVN.CaptureStatus(repo_root):
953 file_path = os.path.join(repo_root, file_status[1])
[email protected]8c415122011-03-15 17:14:27954 if (ignore_externals and
955 file_status[0][0] == 'X' and
956 file_status[0][1:].isspace()):
[email protected]07ab60e2011-02-08 21:54:00957 # Ignore externals.
958 logging.info('Ignoring external %s' % file_status[1])
959 continue
960
961 if callback:
962 callback(file_status)
963
[email protected]8c415122011-03-15 17:14:27964 if os.path.exists(file_path):
965 # svn revert is really stupid. It fails on inconsistent line-endings,
966 # on switched directories, etc. So take no chance and delete everything!
967 # In theory, it wouldn't be necessary for property-only change but then
968 # it'd have to look for switched directories, etc so it's not worth
969 # optimizing this use case.
970 if os.path.isfile(file_path) or os.path.islink(file_path):
971 logging.info('os.remove(%s)' % file_path)
972 os.remove(file_path)
973 elif os.path.isdir(file_path):
[email protected]da64d632011-09-08 17:41:15974 logging.info('RemoveDirectory(%s)' % file_path)
[email protected]8c415122011-03-15 17:14:27975 gclient_utils.RemoveDirectory(file_path)
976 else:
977 logging.critical(
978 ('No idea what is %s.\nYou just found a bug in gclient'
979 ', please ping [email protected] ASAP!') % file_path)
[email protected]07ab60e2011-02-08 21:54:00980
[email protected]8c415122011-03-15 17:14:27981 if (file_status[0][0] in ('D', 'A', '!') or
982 not file_status[0][1:].isspace()):
[email protected]af453492011-03-03 21:04:09983 # Added, deleted file requires manual intervention and require calling
[email protected]07ab60e2011-02-08 21:54:00984 # revert, like for properties.
[email protected]af453492011-03-03 21:04:09985 try:
986 SVN.Capture(['revert', file_status[1]], cwd=repo_root)
[email protected]da64d632011-09-08 17:41:15987 except subprocess2.CalledProcessError:
[email protected]af453492011-03-03 21:04:09988 if not os.path.exists(file_path):
989 continue
990 raise