blob: f0fa40fcb8b4500cd3084a13b3121ee6697227f8 [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.org07ab60e2011-02-08 21:54:00 +00009import logging
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000010import os
11import re
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000012import shutil
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000013import subprocess
14import sys
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000015import tempfile
maruel@chromium.orgfd876172010-04-30 14:01:05 +000016import time
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000017import xml.dom.minidom
18
19import gclient_utils
maruel@chromium.org31cb48a2011-04-04 18:01:36 +000020import subprocess2
21
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000022
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000023def ValidateEmail(email):
maruel@chromium.org6e29d572010-06-04 17:32:20 +000024 return (re.match(r"^[a-zA-Z0-9._%-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", email)
25 is not None)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000026
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000027
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +000028def GetCasedPath(path):
29 """Elcheapos way to get the real path case on Windows."""
30 if sys.platform.startswith('win') and os.path.exists(path):
31 # Reconstruct the path.
32 path = os.path.abspath(path)
33 paths = path.split('\\')
34 for i in range(len(paths)):
35 if i == 0:
36 # Skip drive letter.
37 continue
38 subpath = '\\'.join(paths[:i+1])
39 prev = len('\\'.join(paths[:i]))
40 # glob.glob will return the cased path for the last item only. This is why
41 # we are calling it in a loop. Extract the data we want and put it back
42 # into the list.
43 paths[i] = glob.glob(subpath + '*')[0][prev+1:len(subpath)]
44 path = '\\'.join(paths)
45 return path
46
47
maruel@chromium.org3c55d982010-05-06 14:25:44 +000048def GenFakeDiff(filename):
49 """Generates a fake diff from a file."""
50 file_content = gclient_utils.FileRead(filename, 'rb').splitlines(True)
maruel@chromium.orgc6d170e2010-06-03 00:06:00 +000051 filename = filename.replace(os.sep, '/')
maruel@chromium.org3c55d982010-05-06 14:25:44 +000052 nb_lines = len(file_content)
53 # We need to use / since patch on unix will fail otherwise.
54 data = cStringIO.StringIO()
55 data.write("Index: %s\n" % filename)
56 data.write('=' * 67 + '\n')
57 # Note: Should we use /dev/null instead?
58 data.write("--- %s\n" % filename)
59 data.write("+++ %s\n" % filename)
60 data.write("@@ -0,0 +1,%d @@\n" % nb_lines)
61 # Prepend '+' to every lines.
62 for line in file_content:
63 data.write('+')
64 data.write(line)
65 result = data.getvalue()
66 data.close()
67 return result
68
69
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +000070def determine_scm(root):
71 """Similar to upload.py's version but much simpler.
72
73 Returns 'svn', 'git' or None.
74 """
75 if os.path.isdir(os.path.join(root, '.svn')):
76 return 'svn'
77 elif os.path.isdir(os.path.join(root, '.svn')):
78 return 'git'
79 else:
80 if (0 == subprocess.call(
81 ['git', 'rev-parse', '--show-cdup'],
82 stdout=subprocess.PIPE, cwd=root)):
83 return 'git'
84 else:
85 return None
86
87
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000088class GIT(object):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000089 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +000090 def Capture(args, **kwargs):
91 return gclient_utils.CheckCall(['git'] + args, print_error=False,
92 **kwargs)[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000093
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000094 @staticmethod
msb@chromium.org786fb682010-06-02 15:16:23 +000095 def CaptureStatus(files, upstream_branch=None):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000096 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000097
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000098 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000099
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000100 Returns an array of (status, file) tuples."""
msb@chromium.org786fb682010-06-02 15:16:23 +0000101 if upstream_branch is None:
102 upstream_branch = GIT.GetUpstreamBranch(os.getcwd())
103 if upstream_branch is None:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000104 raise gclient_utils.Error('Cannot determine upstream branch')
105 command = ['diff', '--name-status', '-r', '%s...' % upstream_branch]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000106 if not files:
107 pass
108 elif isinstance(files, basestring):
109 command.append(files)
110 else:
111 command.extend(files)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000112 status = GIT.Capture(command).rstrip()
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000113 results = []
114 if status:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000115 for statusline in status.splitlines():
maruel@chromium.orgcc1614b2010-09-20 17:13:17 +0000116 # 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)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000120 if not m:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000121 raise gclient_utils.Error(
122 'status currently unsupported: %s' % statusline)
maruel@chromium.orgcc1614b2010-09-20 17:13:17 +0000123 # Only grab the first letter.
124 results.append(('%s ' % m.group(1)[0], m.group(2)))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000125 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000126
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000127 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000128 def GetEmail(cwd):
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000129 """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.
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000132 try:
133 return GIT.Capture(['config', 'user.email'], cwd=cwd).strip()
134 except gclient_utils.CheckCallError:
135 return ''
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000136
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):
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000144 """Returns the full branch reference, e.g. 'refs/heads/master'."""
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000145 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd=cwd).strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000146
147 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000148 def GetBranch(cwd):
149 """Returns the short branch name, e.g. 'master'."""
maruel@chromium.orgc308a742009-12-22 18:29:33 +0000150 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000151
152 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000153 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:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000157 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd=cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000158 return True
159 except gclient_utils.CheckCallError:
160 return False
161
162 @staticmethod
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000163 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
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000205 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:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000209 # 1) iterate through our branch history and find the svn URL.
210 # 2) find the svn-remote that fetches from the URL.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000211
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
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000215 # 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']
219 proc = gclient_utils.Popen(cmd, stdout=subprocess.PIPE)
220 for line in proc.stdout:
221 match = git_svn_re.match(line)
222 if match:
223 url = match.group(1)
224 proc.stdout.close() # Cut pipe.
225 break
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000226
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000227 if url:
228 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
229 remotes = GIT.Capture(['config', '--get-regexp',
230 r'^svn-remote\..*\.url'], cwd=cwd).splitlines()
231 for remote in remotes:
232 match = svn_remote_re.match(remote)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000233 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000234 remote = match.group(1)
235 base_url = match.group(2)
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000236 try:
237 fetch_spec = GIT.Capture(
238 ['config', 'svn-remote.%s.fetch' % remote],
239 cwd=cwd).strip()
240 branch = GIT.MatchSvnGlob(url, base_url, fetch_spec, False)
241 except gclient_utils.CheckCallError:
242 branch = None
243 if branch:
244 return branch
245 try:
246 branch_spec = GIT.Capture(
247 ['config', 'svn-remote.%s.branches' % remote],
248 cwd=cwd).strip()
249 branch = GIT.MatchSvnGlob(url, base_url, branch_spec, True)
250 except gclient_utils.CheckCallError:
251 branch = None
252 if branch:
253 return branch
254 try:
255 tag_spec = GIT.Capture(
256 ['config', 'svn-remote.%s.tags' % remote],
257 cwd=cwd).strip()
258 branch = GIT.MatchSvnGlob(url, base_url, tag_spec, True)
259 except gclient_utils.CheckCallError:
260 branch = None
261 if branch:
262 return branch
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000263
264 @staticmethod
265 def FetchUpstreamTuple(cwd):
266 """Returns a tuple containg remote and remote ref,
267 e.g. 'origin', 'refs/heads/master'
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000268 Tries to be intelligent and understand git-svn.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000269 """
270 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000271 branch = GIT.GetBranch(cwd)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000272 try:
273 upstream_branch = GIT.Capture(
274 ['config', 'branch.%s.merge' % branch], cwd=cwd).strip()
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000275 except (gclient_utils.Error, subprocess2.CalledProcessError):
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000276 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000277 if upstream_branch:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000278 try:
279 remote = GIT.Capture(
280 ['config', 'branch.%s.remote' % branch], cwd=cwd).strip()
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000281 except (gclient_utils.Error, subprocess2.CalledProcessError):
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000282 pass
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000283 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000284 try:
285 upstream_branch = GIT.Capture(
286 ['config', 'rietveld.upstream-branch'], cwd=cwd).strip()
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000287 except (gclient_utils.Error, subprocess2.CalledProcessError):
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000288 upstream_branch = None
289 if upstream_branch:
290 try:
291 remote = GIT.Capture(
292 ['config', 'rietveld.upstream-remote'], cwd=cwd).strip()
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000293 except (gclient_utils.Error, subprocess2.CalledProcessError):
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000294 pass
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000295 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000296 # Fall back on trying a git-svn upstream branch.
297 if GIT.IsGitSvn(cwd):
298 upstream_branch = GIT.GetSVNBranch(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000299 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000300 # Else, try to guess the origin remote.
301 remote_branches = GIT.Capture(['branch', '-r'], cwd=cwd).split()
302 if 'origin/master' in remote_branches:
303 # Fall back on origin/master if it exits.
304 remote = 'origin'
305 upstream_branch = 'refs/heads/master'
306 elif 'origin/trunk' in remote_branches:
307 # Fall back on origin/trunk if it exists. Generally a shared
308 # git-svn clone
309 remote = 'origin'
310 upstream_branch = 'refs/heads/trunk'
311 else:
312 # Give up.
313 remote = None
314 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000315 return remote, upstream_branch
316
317 @staticmethod
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000318 def GetUpstreamBranch(cwd):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000319 """Gets the current branch's upstream branch."""
320 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000321 if remote != '.' and upstream_branch:
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000322 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
323 return upstream_branch
324
325 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000326 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
327 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000328 """Diffs against the upstream branch or optionally another branch.
329
330 full_move means that move or copy operations should completely recreate the
331 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000332 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000333 branch = GIT.GetUpstreamBranch(cwd)
evan@chromium.org400f3e72010-05-19 14:23:36 +0000334 command = ['diff', '-p', '--no-prefix', '--no-ext-diff',
335 branch + "..." + branch_head]
maruel@chromium.orga9371762009-12-22 18:27:38 +0000336 if not full_move:
337 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000338 # TODO(maruel): --binary support.
339 if files:
340 command.append('--')
341 command.extend(files)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000342 diff = GIT.Capture(command, cwd=cwd).splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000343 for i in range(len(diff)):
344 # In the case of added files, replace /dev/null with the path to the
345 # file being added.
346 if diff[i].startswith('--- /dev/null'):
347 diff[i] = '--- %s' % diff[i+1][4:]
348 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000349
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000350 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000351 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
352 """Returns the list of modified files between two branches."""
353 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000354 branch = GIT.GetUpstreamBranch(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000355 command = ['diff', '--name-only', branch + "..." + branch_head]
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000356 return GIT.Capture(command, cwd=cwd).splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000357
358 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000359 def GetPatchName(cwd):
360 """Constructs a name for this patch."""
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000361 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd=cwd).strip()
maruel@chromium.org862ff8e2010-08-06 15:29:16 +0000362 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000363
364 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000365 def GetCheckoutRoot(cwd):
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000366 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000367 """
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000368 root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd).strip()
369 return os.path.abspath(os.path.join(cwd, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000370
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000371 @staticmethod
372 def AssertVersion(min_version):
373 """Asserts git's version is at least min_version."""
374 def only_int(val):
375 if val.isdigit():
376 return int(val)
377 else:
378 return 0
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000379 current_version = GIT.Capture(['--version']).split()[-1]
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000380 current_version_list = map(only_int, current_version.split('.'))
381 for min_ver in map(int, min_version.split('.')):
382 ver = current_version_list.pop(0)
383 if ver < min_ver:
384 return (False, current_version)
385 elif ver > min_ver:
386 return (True, current_version)
387 return (True, current_version)
388
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000389
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000390class SVN(object):
tony@chromium.org57564662010-04-14 02:35:12 +0000391 current_version = None
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000392
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000393 @staticmethod
maruel@chromium.org54019f32010-09-09 13:50:11 +0000394 def Capture(args, **kwargs):
395 """Always redirect stderr.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000396
maruel@chromium.org54019f32010-09-09 13:50:11 +0000397 Throws an exception if non-0 is returned."""
398 return gclient_utils.CheckCall(['svn'] + args, print_error=False,
399 **kwargs)[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000400
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000401 @staticmethod
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000402 def RunAndGetFileList(verbose, args, cwd, file_list, stdout=None):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000403 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000404
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000405 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000406
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000407 svn's stdout is parsed to collect a list of files checked out or updated.
408 These files are appended to file_list. svn's stdout is also printed to
409 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000410
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000411 Args:
maruel@chromium.org03807072010-08-16 17:18:44 +0000412 verbose: If True, uses verbose output
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000413 args: A sequence of command line parameters to be passed to svn.
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000414 cwd: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000415
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000416 Raises:
417 Error: An error occurred while running the svn command.
418 """
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000419 stdout = stdout or sys.stdout
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000420
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000421 # svn update and svn checkout use the same pattern: the first three columns
422 # are for file status, property status, and lock status. This is followed
423 # by two spaces, and then the path to the file.
424 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000425
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000426 # The first three columns of svn status are the same as for svn update and
427 # svn checkout. The next three columns indicate addition-with-history,
428 # switch, and remote lock status. This is followed by one space, and then
429 # the path to the file.
430 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000431
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000432 # args[0] must be a supported command. This will blow up if it's something
433 # else, which is good. Note that the patterns are only effective when
434 # these commands are used in their ordinary forms, the patterns are invalid
435 # for "svn status --show-updates", for example.
436 pattern = {
437 'checkout': update_pattern,
438 'status': status_pattern,
439 'update': update_pattern,
440 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000441 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000442 # Place an upper limit.
cbentzel@chromium.org2aee2292010-09-03 14:15:25 +0000443 backoff_time = 5
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000444 retries = 0
maruel@chromium.org03507062010-10-26 00:58:27 +0000445 while True:
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000446 retries += 1
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000447 previous_list_len = len(file_list)
448 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000449
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000450 def CaptureMatchingLines(line):
451 match = compiled_pattern.search(line)
452 if match:
453 file_list.append(match.group(1))
454 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000455 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000456
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000457 try:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000458 gclient_utils.CheckCallAndFilterAndHeader(
459 ['svn'] + args,
460 cwd=cwd,
461 always=verbose,
462 filter_fn=CaptureMatchingLines,
463 stdout=stdout)
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000464 except (gclient_utils.Error, subprocess2.CalledProcessError):
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000465 def IsKnownFailure():
466 for x in failure:
467 if (x.startswith('svn: OPTIONS of') or
468 x.startswith('svn: PROPFIND of') or
469 x.startswith('svn: REPORT of') or
maruel@chromium.orgf61fc932010-08-19 13:05:24 +0000470 x.startswith('svn: Unknown hostname') or
471 x.startswith('svn: Server sent unexpected return value')):
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000472 return True
473 return False
474
maruel@chromium.org953586a2010-06-15 14:22:24 +0000475 # Subversion client is really misbehaving with Google Code.
476 if args[0] == 'checkout':
477 # Ensure at least one file was checked out, otherwise *delete* the
478 # directory.
479 if len(file_list) == previous_list_len:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000480 if not IsKnownFailure():
maruel@chromium.org953586a2010-06-15 14:22:24 +0000481 # No known svn error was found, bail out.
482 raise
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000483 # No file were checked out, so make sure the directory is
484 # deleted in case it's messed up and try again.
485 # Warning: It's bad, it assumes args[2] is the directory
486 # argument.
487 if os.path.isdir(args[2]):
488 gclient_utils.RemoveDirectory(args[2])
maruel@chromium.org953586a2010-06-15 14:22:24 +0000489 else:
490 # Progress was made, convert to update since an aborted checkout
491 # is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000492 args = ['update'] + args[1:]
maruel@chromium.org953586a2010-06-15 14:22:24 +0000493 else:
494 # It was an update or export.
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000495 # We enforce that some progress has been made or a known failure.
496 if len(file_list) == previous_list_len and not IsKnownFailure():
497 # No known svn error was found and no progress, bail out.
498 raise
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000499 if retries == 10:
maruel@chromium.org03507062010-10-26 00:58:27 +0000500 raise
cbentzel@chromium.org2aee2292010-09-03 14:15:25 +0000501 print "Sleeping %.1f seconds and retrying...." % backoff_time
502 time.sleep(backoff_time)
503 backoff_time *= 1.3
maruel@chromium.org953586a2010-06-15 14:22:24 +0000504 continue
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000505 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000506
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000507 @staticmethod
maruel@chromium.org54019f32010-09-09 13:50:11 +0000508 def CaptureInfo(cwd):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000509 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000510
maruel@chromium.org54019f32010-09-09 13:50:11 +0000511 Throws an exception if svn info fails."""
512 output = SVN.Capture(['info', '--xml', cwd])
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000513 dom = gclient_utils.ParseXML(output)
514 result = {}
515 if dom:
516 GetNamedNodeText = gclient_utils.GetNamedNodeText
517 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
518 def C(item, f):
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000519 if item is not None:
520 return f(item)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000521 # /info/entry/
522 # url
523 # reposityory/(root|uuid)
524 # wc-info/(schedule|depth)
525 # commit/(author|date)
526 # str() the results because they may be returned as Unicode, which
527 # interferes with the higher layers matching up things in the deps
528 # dictionary.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000529 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
530 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
531 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
532 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
533 'revision'),
534 int)
535 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
536 str)
537 # Differs across versions.
538 if result['Node Kind'] == 'dir':
539 result['Node Kind'] = 'directory'
540 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
541 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
542 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
543 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
544 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000545
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000546 @staticmethod
maruel@chromium.org54019f32010-09-09 13:50:11 +0000547 def CaptureRevision(cwd):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000548 """Get the base revision of a SVN repository.
549
550 Returns:
551 Int base revision
552 """
maruel@chromium.org54019f32010-09-09 13:50:11 +0000553 info = SVN.Capture(['info', '--xml'], cwd=cwd)
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000554 dom = xml.dom.minidom.parseString(info)
555 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
556
557 @staticmethod
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000558 def CaptureStatus(files):
559 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000560
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000561 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000562
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000563 Returns an array of (status, file) tuples."""
564 command = ["status", "--xml"]
565 if not files:
566 pass
567 elif isinstance(files, basestring):
568 command.append(files)
569 else:
570 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000571
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000572 status_letter = {
573 None: ' ',
574 '': ' ',
575 'added': 'A',
576 'conflicted': 'C',
577 'deleted': 'D',
578 'external': 'X',
579 'ignored': 'I',
580 'incomplete': '!',
581 'merged': 'G',
582 'missing': '!',
583 'modified': 'M',
584 'none': ' ',
585 'normal': ' ',
586 'obstructed': '~',
587 'replaced': 'R',
588 'unversioned': '?',
589 }
590 dom = gclient_utils.ParseXML(SVN.Capture(command))
591 results = []
592 if dom:
593 # /status/target/entry/(wc-status|commit|author|date)
594 for target in dom.getElementsByTagName('target'):
595 #base_path = target.getAttribute('path')
596 for entry in target.getElementsByTagName('entry'):
597 file_path = entry.getAttribute('path')
598 wc_status = entry.getElementsByTagName('wc-status')
599 assert len(wc_status) == 1
600 # Emulate svn 1.5 status ouput...
601 statuses = [' '] * 7
602 # Col 0
603 xml_item_status = wc_status[0].getAttribute('item')
604 if xml_item_status in status_letter:
605 statuses[0] = status_letter[xml_item_status]
606 else:
maruel@chromium.org54019f32010-09-09 13:50:11 +0000607 raise gclient_utils.Error(
608 'Unknown item status "%s"; please implement me!' %
609 xml_item_status)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000610 # Col 1
611 xml_props_status = wc_status[0].getAttribute('props')
612 if xml_props_status == 'modified':
613 statuses[1] = 'M'
614 elif xml_props_status == 'conflicted':
615 statuses[1] = 'C'
616 elif (not xml_props_status or xml_props_status == 'none' or
617 xml_props_status == 'normal'):
618 pass
619 else:
maruel@chromium.org54019f32010-09-09 13:50:11 +0000620 raise gclient_utils.Error(
621 'Unknown props status "%s"; please implement me!' %
622 xml_props_status)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000623 # Col 2
624 if wc_status[0].getAttribute('wc-locked') == 'true':
625 statuses[2] = 'L'
626 # Col 3
627 if wc_status[0].getAttribute('copied') == 'true':
628 statuses[3] = '+'
629 # Col 4
630 if wc_status[0].getAttribute('switched') == 'true':
631 statuses[4] = 'S'
632 # TODO(maruel): Col 5 and 6
633 item = (''.join(statuses), file_path)
634 results.append(item)
635 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000636
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000637 @staticmethod
638 def IsMoved(filename):
639 """Determine if a file has been added through svn mv"""
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000640 return SVN.IsMovedInfo(SVN.CaptureInfo(filename))
641
642 @staticmethod
643 def IsMovedInfo(info):
644 """Determine if a file has been added through svn mv"""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000645 return (info.get('Copied From URL') and
646 info.get('Copied From Rev') and
647 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000648
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000649 @staticmethod
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000650 def GetFileProperty(filename, property_name):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000651 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000652
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000653 Args:
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000654 filename: The file to check
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000655 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000656
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000657 Returns:
658 The value of the property, which will be the empty string if the property
659 is not set on the file. If the file is not under version control, the
660 empty string is also returned.
661 """
maruel@chromium.org54019f32010-09-09 13:50:11 +0000662 try:
663 return SVN.Capture(['propget', property_name, filename])
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000664 except (gclient_utils.Error, subprocess2.CalledProcessError):
maruel@chromium.org54019f32010-09-09 13:50:11 +0000665 return ''
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000666
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000667 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000668 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000669 """Diffs a single file.
670
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000671 Should be simple, eh? No it isn't.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000672 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000673 expected relative path.
674 full_move means that move or copy operations should completely recreate the
675 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000676 # If the user specified a custom diff command in their svn config file,
677 # then it'll be used when we do svn diff, which we don't want to happen
678 # since we want the unified diff. Using --diff-cmd=diff doesn't always
679 # work, since they can have another diff executable in their path that
680 # gives different line endings. So we use a bogus temp directory as the
681 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000682 bogus_dir = tempfile.mkdtemp()
683 try:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000684 # Use "svn info" output instead of os.path.isdir because the latter fails
685 # when the file is deleted.
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000686 return SVN._DiffItemInternal(filename, SVN.CaptureInfo(filename),
687 bogus_dir,
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000688 full_move=full_move, revision=revision)
689 finally:
690 shutil.rmtree(bogus_dir)
691
692 @staticmethod
693 def _DiffItemInternal(filename, info, bogus_dir, full_move=False,
694 revision=None):
695 """Grabs the diff data."""
696 command = ["diff", "--config-dir", bogus_dir, filename]
697 if revision:
698 command.extend(['--revision', revision])
699 data = None
700 if SVN.IsMovedInfo(info):
701 if full_move:
702 if info.get("Node Kind") == "directory":
703 # Things become tricky here. It's a directory copy/move. We need to
704 # diff all the files inside it.
705 # This will put a lot of pressure on the heap. This is why StringIO
706 # is used and converted back into a string at the end. The reason to
707 # return a string instead of a StringIO is that StringIO.write()
708 # doesn't accept a StringIO object. *sigh*.
709 for (dirpath, dirnames, filenames) in os.walk(filename):
710 # Cleanup all files starting with a '.'.
711 for d in dirnames:
712 if d.startswith('.'):
713 dirnames.remove(d)
714 for f in filenames:
715 if f.startswith('.'):
716 filenames.remove(f)
717 for f in filenames:
718 if data is None:
719 data = cStringIO.StringIO()
720 data.write(GenFakeDiff(os.path.join(dirpath, f)))
721 if data:
722 tmp = data.getvalue()
723 data.close()
724 data = tmp
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000725 else:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000726 data = GenFakeDiff(filename)
727 else:
728 if info.get("Node Kind") != "directory":
maruel@chromium.org0836c562010-01-22 01:10:06 +0000729 # svn diff on a mv/cp'd file outputs nothing if there was no change.
maruel@chromium.org54019f32010-09-09 13:50:11 +0000730 data = SVN.Capture(command)
maruel@chromium.org0836c562010-01-22 01:10:06 +0000731 if not data:
732 # We put in an empty Index entry so upload.py knows about them.
maruel@chromium.orgc6d170e2010-06-03 00:06:00 +0000733 data = "Index: %s\n" % filename.replace(os.sep, '/')
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000734 # Otherwise silently ignore directories.
735 else:
736 if info.get("Node Kind") != "directory":
737 # Normal simple case.
maruel@chromium.orgf8b3f942011-03-24 17:33:50 +0000738 try:
739 data = SVN.Capture(command)
740 except gclient_utils.CheckCallError:
741 if revision:
742 data = GenFakeDiff(filename)
743 else:
744 raise
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000745 # Otherwise silently ignore directories.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000746 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000747
748 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000749 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000750 """Returns a string containing the diff for the given file list.
751
752 The files in the list should either be absolute paths or relative to the
753 given root. If no root directory is provided, the repository root will be
754 used.
755 The diff will always use relative paths.
756 """
maruel@chromium.org00fdcb32011-02-24 01:41:02 +0000757 assert isinstance(filenames, (list, tuple))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000758 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000759 root = root or SVN.GetCheckoutRoot(previous_cwd)
760 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000761 def RelativePath(path, root):
762 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000763 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000764 return path[len(root):]
765 return path
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000766 # If the user specified a custom diff command in their svn config file,
767 # then it'll be used when we do svn diff, which we don't want to happen
768 # since we want the unified diff. Using --diff-cmd=diff doesn't always
769 # work, since they can have another diff executable in their path that
770 # gives different line endings. So we use a bogus temp directory as the
771 # config directory, which gets around these problems.
772 bogus_dir = tempfile.mkdtemp()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000773 try:
774 os.chdir(root)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000775 # Cleanup filenames
776 filenames = [RelativePath(f, root) for f in filenames]
777 # Get information about the modified items (files and directories)
778 data = dict([(f, SVN.CaptureInfo(f)) for f in filenames])
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000779 diffs = []
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000780 if full_move:
781 # Eliminate modified files inside moved/copied directory.
782 for (filename, info) in data.iteritems():
783 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
784 # Remove files inside the directory.
785 filenames = [f for f in filenames
786 if not f.startswith(filename + os.path.sep)]
787 for filename in data.keys():
788 if not filename in filenames:
789 # Remove filtered out items.
790 del data[filename]
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000791 else:
792 metaheaders = []
793 for (filename, info) in data.iteritems():
794 if SVN.IsMovedInfo(info):
795 # for now, the most common case is a head copy,
796 # so let's just encode that as a straight up cp.
797 srcurl = info.get('Copied From URL')
798 root = info.get('Repository Root')
799 rev = int(info.get('Copied From Rev'))
800 assert srcurl.startswith(root)
801 src = srcurl[len(root)+1:]
maruel@chromium.org00fdcb32011-02-24 01:41:02 +0000802 try:
803 srcinfo = SVN.CaptureInfo(srcurl)
804 except gclient_utils.CheckCallError, e:
805 if not 'Not a valid URL' in e.stderr:
806 raise
807 # Assume the file was deleted. No idea how to figure out at which
808 # revision the file was deleted.
809 srcinfo = {'Revision': rev}
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000810 if (srcinfo.get('Revision') != rev and
811 SVN.Capture(['diff', '-r', '%d:head' % rev, srcurl])):
812 metaheaders.append("#$ svn cp -r %d %s %s "
813 "### WARNING: note non-trunk copy\n" %
814 (rev, src, filename))
815 else:
816 metaheaders.append("#$ cp %s %s\n" % (src,
817 filename))
818
819 if metaheaders:
820 diffs.append("### BEGIN SVN COPY METADATA\n")
821 diffs.extend(metaheaders)
822 diffs.append("### END SVN COPY METADATA\n")
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000823 # Now ready to do the actual diff.
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000824 for filename in sorted(data.iterkeys()):
825 diffs.append(SVN._DiffItemInternal(filename, data[filename], bogus_dir,
826 full_move=full_move,
827 revision=revision))
828 # Use StringIO since it can be messy when diffing a directory move with
829 # full_move=True.
830 buf = cStringIO.StringIO()
831 for d in filter(None, diffs):
832 buf.write(d)
833 result = buf.getvalue()
834 buf.close()
835 return result
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000836 finally:
837 os.chdir(previous_cwd)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000838 shutil.rmtree(bogus_dir)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000839
840 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000841 def GetEmail(repo_root):
842 """Retrieves the svn account which we assume is an email address."""
maruel@chromium.org54019f32010-09-09 13:50:11 +0000843 try:
844 infos = SVN.CaptureInfo(repo_root)
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000845 except (gclient_utils.Error, subprocess2.CalledProcessError):
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000846 return None
847
848 # Should check for uuid but it is incorrectly saved for https creds.
maruel@chromium.org54019f32010-09-09 13:50:11 +0000849 root = infos['Repository Root']
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000850 realm = root.rsplit('/', 1)[0]
maruel@chromium.org54019f32010-09-09 13:50:11 +0000851 uuid = infos['UUID']
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000852 if root.startswith('https') or not uuid:
853 regexp = re.compile(r'<%s:\d+>.*' % realm)
854 else:
855 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
856 if regexp is None:
857 return None
858 if sys.platform.startswith('win'):
859 if not 'APPDATA' in os.environ:
860 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000861 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
862 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000863 else:
864 if not 'HOME' in os.environ:
865 return None
866 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
867 'svn.simple')
868 for credfile in os.listdir(auth_dir):
869 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
870 if regexp.match(cred_info.get('svn:realmstring')):
871 return cred_info.get('username')
872
873 @staticmethod
874 def ReadSimpleAuth(filename):
875 f = open(filename, 'r')
876 values = {}
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000877 def ReadOneItem(item_type):
878 m = re.match(r'%s (\d+)' % item_type, f.readline())
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000879 if not m:
880 return None
881 data = f.read(int(m.group(1)))
882 if f.read(1) != '\n':
883 return None
884 return data
885
886 while True:
887 key = ReadOneItem('K')
888 if not key:
889 break
890 value = ReadOneItem('V')
891 if not value:
892 break
893 values[key] = value
894 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000895
896 @staticmethod
897 def GetCheckoutRoot(directory):
898 """Returns the top level directory of the current repository.
899
900 The directory is returned as an absolute path.
901 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000902 directory = os.path.abspath(directory)
maruel@chromium.org54019f32010-09-09 13:50:11 +0000903 try:
maruel@chromium.org885d6e82011-02-24 20:21:46 +0000904 info = SVN.CaptureInfo(directory)
905 cur_dir_repo_root = info['Repository Root']
906 url = info['URL']
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000907 except (gclient_utils.Error, subprocess2.CalledProcessError):
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000908 return None
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000909 while True:
910 parent = os.path.dirname(directory)
maruel@chromium.org54019f32010-09-09 13:50:11 +0000911 try:
maruel@chromium.org885d6e82011-02-24 20:21:46 +0000912 info = SVN.CaptureInfo(parent)
913 if (info['Repository Root'] != cur_dir_repo_root or
914 info['URL'] != os.path.dirname(url)):
maruel@chromium.org54019f32010-09-09 13:50:11 +0000915 break
maruel@chromium.org885d6e82011-02-24 20:21:46 +0000916 url = info['URL']
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000917 except (gclient_utils.Error, subprocess2.CalledProcessError):
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000918 break
919 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000920 return GetCasedPath(directory)
tony@chromium.org57564662010-04-14 02:35:12 +0000921
922 @staticmethod
923 def AssertVersion(min_version):
924 """Asserts svn's version is at least min_version."""
925 def only_int(val):
926 if val.isdigit():
927 return int(val)
928 else:
929 return 0
930 if not SVN.current_version:
931 SVN.current_version = SVN.Capture(['--version']).split()[2]
932 current_version_list = map(only_int, SVN.current_version.split('.'))
933 for min_ver in map(int, min_version.split('.')):
934 ver = current_version_list.pop(0)
935 if ver < min_ver:
936 return (False, SVN.current_version)
937 elif ver > min_ver:
938 return (True, SVN.current_version)
939 return (True, SVN.current_version)
maruel@chromium.org07ab60e2011-02-08 21:54:00 +0000940
941 @staticmethod
942 def Revert(repo_root, callback=None, ignore_externals=False):
943 """Reverts all svn modifications in repo_root, including properties.
944
945 Deletes any modified files or directory.
946
947 A "svn update --revision BASE" call is required after to revive deleted
948 files.
949 """
950 for file_status in SVN.CaptureStatus(repo_root):
951 file_path = os.path.join(repo_root, file_status[1])
maruel@chromium.org8c415122011-03-15 17:14:27 +0000952 if (ignore_externals and
953 file_status[0][0] == 'X' and
954 file_status[0][1:].isspace()):
maruel@chromium.org07ab60e2011-02-08 21:54:00 +0000955 # Ignore externals.
956 logging.info('Ignoring external %s' % file_status[1])
957 continue
958
959 if callback:
960 callback(file_status)
961
maruel@chromium.org8c415122011-03-15 17:14:27 +0000962 if os.path.exists(file_path):
963 # svn revert is really stupid. It fails on inconsistent line-endings,
964 # on switched directories, etc. So take no chance and delete everything!
965 # In theory, it wouldn't be necessary for property-only change but then
966 # it'd have to look for switched directories, etc so it's not worth
967 # optimizing this use case.
968 if os.path.isfile(file_path) or os.path.islink(file_path):
969 logging.info('os.remove(%s)' % file_path)
970 os.remove(file_path)
971 elif os.path.isdir(file_path):
972 logging.info('gclient_utils.RemoveDirectory(%s)' % file_path)
973 gclient_utils.RemoveDirectory(file_path)
974 else:
975 logging.critical(
976 ('No idea what is %s.\nYou just found a bug in gclient'
977 ', please ping maruel@chromium.org ASAP!') % file_path)
maruel@chromium.org07ab60e2011-02-08 21:54:00 +0000978
maruel@chromium.org8c415122011-03-15 17:14:27 +0000979 if (file_status[0][0] in ('D', 'A', '!') or
980 not file_status[0][1:].isspace()):
maruel@chromium.orgaf453492011-03-03 21:04:09 +0000981 # Added, deleted file requires manual intervention and require calling
maruel@chromium.org07ab60e2011-02-08 21:54:00 +0000982 # revert, like for properties.
maruel@chromium.orgaf453492011-03-03 21:04:09 +0000983 try:
984 SVN.Capture(['revert', file_status[1]], cwd=repo_root)
985 except gclient_utils.CheckCallError:
986 if not os.path.exists(file_path):
987 continue
988 raise