blob: 67dd747d4d47938c823c58314b1697a87664ee9e [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)
maruel@chromium.orge8c28622011-04-05 14:41:44 +0000220 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000221 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
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000227
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000228 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)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000234 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000235 remote = match.group(1)
236 base_url = match.group(2)
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000237 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)
242 except gclient_utils.CheckCallError:
243 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)
251 except gclient_utils.CheckCallError:
252 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)
260 except gclient_utils.CheckCallError:
261 branch = None
262 if branch:
263 return branch
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000264
265 @staticmethod
266 def FetchUpstreamTuple(cwd):
267 """Returns a tuple containg remote and remote ref,
268 e.g. 'origin', 'refs/heads/master'
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000269 Tries to be intelligent and understand git-svn.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000270 """
271 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000272 branch = GIT.GetBranch(cwd)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000273 try:
274 upstream_branch = GIT.Capture(
275 ['config', 'branch.%s.merge' % branch], cwd=cwd).strip()
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000276 except (gclient_utils.Error, subprocess2.CalledProcessError):
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000277 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000278 if upstream_branch:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000279 try:
280 remote = GIT.Capture(
281 ['config', 'branch.%s.remote' % branch], cwd=cwd).strip()
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000282 except (gclient_utils.Error, subprocess2.CalledProcessError):
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000283 pass
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000284 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000285 try:
286 upstream_branch = GIT.Capture(
287 ['config', 'rietveld.upstream-branch'], cwd=cwd).strip()
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000288 except (gclient_utils.Error, subprocess2.CalledProcessError):
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000289 upstream_branch = None
290 if upstream_branch:
291 try:
292 remote = GIT.Capture(
293 ['config', 'rietveld.upstream-remote'], cwd=cwd).strip()
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000294 except (gclient_utils.Error, subprocess2.CalledProcessError):
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000295 pass
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000296 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000297 # Fall back on trying a git-svn upstream branch.
298 if GIT.IsGitSvn(cwd):
299 upstream_branch = GIT.GetSVNBranch(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000300 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000301 # 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
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000316 return remote, upstream_branch
317
318 @staticmethod
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000319 def GetUpstreamBranch(cwd):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000320 """Gets the current branch's upstream branch."""
321 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000322 if remote != '.' and upstream_branch:
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000323 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
324 return upstream_branch
325
326 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000327 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
328 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000329 """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."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000333 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000334 branch = GIT.GetUpstreamBranch(cwd)
evan@chromium.org400f3e72010-05-19 14:23:36 +0000335 command = ['diff', '-p', '--no-prefix', '--no-ext-diff',
336 branch + "..." + branch_head]
maruel@chromium.orga9371762009-12-22 18:27:38 +0000337 if not full_move:
338 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000339 # TODO(maruel): --binary support.
340 if files:
341 command.append('--')
342 command.extend(files)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000343 diff = GIT.Capture(command, cwd=cwd).splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000344 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)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000350
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000351 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000352 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
353 """Returns the list of modified files between two branches."""
354 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000355 branch = GIT.GetUpstreamBranch(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000356 command = ['diff', '--name-only', branch + "..." + branch_head]
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000357 return GIT.Capture(command, cwd=cwd).splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000358
359 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000360 def GetPatchName(cwd):
361 """Constructs a name for this patch."""
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000362 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd=cwd).strip()
maruel@chromium.org862ff8e2010-08-06 15:29:16 +0000363 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000364
365 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000366 def GetCheckoutRoot(cwd):
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000367 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000368 """
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000369 root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd).strip()
370 return os.path.abspath(os.path.join(cwd, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000371
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000372 @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
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000380 current_version = GIT.Capture(['--version']).split()[-1]
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000381 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
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000390
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000391class SVN(object):
tony@chromium.org57564662010-04-14 02:35:12 +0000392 current_version = None
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000393
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000394 @staticmethod
maruel@chromium.org54019f32010-09-09 13:50:11 +0000395 def Capture(args, **kwargs):
396 """Always redirect stderr.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000397
maruel@chromium.org54019f32010-09-09 13:50:11 +0000398 Throws an exception if non-0 is returned."""
399 return gclient_utils.CheckCall(['svn'] + args, print_error=False,
400 **kwargs)[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000401
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000402 @staticmethod
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000403 def RunAndGetFileList(verbose, args, cwd, file_list, stdout=None):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000404 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000405
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000406 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000407
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000408 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.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000411
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000412 Args:
maruel@chromium.org03807072010-08-16 17:18:44 +0000413 verbose: If True, uses verbose output
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000414 args: A sequence of command line parameters to be passed to svn.
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000415 cwd: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000416
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000417 Raises:
418 Error: An error occurred while running the svn command.
419 """
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000420 stdout = stdout or sys.stdout
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000421
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000422 # 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 = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000426
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000427 # 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 = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000432
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000433 # 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]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000442 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000443 # Place an upper limit.
cbentzel@chromium.org2aee2292010-09-03 14:15:25 +0000444 backoff_time = 5
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000445 retries = 0
maruel@chromium.org03507062010-10-26 00:58:27 +0000446 while True:
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000447 retries += 1
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000448 previous_list_len = len(file_list)
449 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000450
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000451 def CaptureMatchingLines(line):
452 match = compiled_pattern.search(line)
453 if match:
454 file_list.append(match.group(1))
455 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000456 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000457
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000458 try:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000459 gclient_utils.CheckCallAndFilterAndHeader(
460 ['svn'] + args,
461 cwd=cwd,
462 always=verbose,
463 filter_fn=CaptureMatchingLines,
464 stdout=stdout)
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000465 except (gclient_utils.Error, subprocess2.CalledProcessError):
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000466 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
maruel@chromium.orgf61fc932010-08-19 13:05:24 +0000471 x.startswith('svn: Unknown hostname') or
472 x.startswith('svn: Server sent unexpected return value')):
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000473 return True
474 return False
475
maruel@chromium.org953586a2010-06-15 14:22:24 +0000476 # 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:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000481 if not IsKnownFailure():
maruel@chromium.org953586a2010-06-15 14:22:24 +0000482 # No known svn error was found, bail out.
483 raise
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000484 # 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])
maruel@chromium.org953586a2010-06-15 14:22:24 +0000490 else:
491 # Progress was made, convert to update since an aborted checkout
492 # is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000493 args = ['update'] + args[1:]
maruel@chromium.org953586a2010-06-15 14:22:24 +0000494 else:
495 # It was an update or export.
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000496 # 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
maruel@chromium.orgbec588d2010-10-26 13:50:25 +0000500 if retries == 10:
maruel@chromium.org03507062010-10-26 00:58:27 +0000501 raise
cbentzel@chromium.org2aee2292010-09-03 14:15:25 +0000502 print "Sleeping %.1f seconds and retrying...." % backoff_time
503 time.sleep(backoff_time)
504 backoff_time *= 1.3
maruel@chromium.org953586a2010-06-15 14:22:24 +0000505 continue
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000506 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000507
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000508 @staticmethod
maruel@chromium.org54019f32010-09-09 13:50:11 +0000509 def CaptureInfo(cwd):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000510 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000511
maruel@chromium.org54019f32010-09-09 13:50:11 +0000512 Throws an exception if svn info fails."""
513 output = SVN.Capture(['info', '--xml', cwd])
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000514 dom = gclient_utils.ParseXML(output)
515 result = {}
516 if dom:
517 GetNamedNodeText = gclient_utils.GetNamedNodeText
518 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
519 def C(item, f):
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000520 if item is not None:
521 return f(item)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000522 # /info/entry/
523 # url
524 # reposityory/(root|uuid)
525 # wc-info/(schedule|depth)
526 # commit/(author|date)
527 # str() the results because they may be returned as Unicode, which
528 # interferes with the higher layers matching up things in the deps
529 # dictionary.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000530 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
531 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
532 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
533 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
534 'revision'),
535 int)
536 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
537 str)
538 # Differs across versions.
539 if result['Node Kind'] == 'dir':
540 result['Node Kind'] = 'directory'
541 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
542 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
543 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
544 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
545 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000546
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000547 @staticmethod
maruel@chromium.org54019f32010-09-09 13:50:11 +0000548 def CaptureRevision(cwd):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000549 """Get the base revision of a SVN repository.
550
551 Returns:
552 Int base revision
553 """
maruel@chromium.org54019f32010-09-09 13:50:11 +0000554 info = SVN.Capture(['info', '--xml'], cwd=cwd)
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000555 dom = xml.dom.minidom.parseString(info)
556 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
557
558 @staticmethod
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000559 def CaptureStatus(files):
560 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000561
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000562 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000563
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000564 Returns an array of (status, file) tuples."""
565 command = ["status", "--xml"]
566 if not files:
567 pass
568 elif isinstance(files, basestring):
569 command.append(files)
570 else:
571 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000572
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000573 status_letter = {
574 None: ' ',
575 '': ' ',
576 'added': 'A',
577 'conflicted': 'C',
578 'deleted': 'D',
579 'external': 'X',
580 'ignored': 'I',
581 'incomplete': '!',
582 'merged': 'G',
583 'missing': '!',
584 'modified': 'M',
585 'none': ' ',
586 'normal': ' ',
587 'obstructed': '~',
588 'replaced': 'R',
589 'unversioned': '?',
590 }
591 dom = gclient_utils.ParseXML(SVN.Capture(command))
592 results = []
593 if dom:
594 # /status/target/entry/(wc-status|commit|author|date)
595 for target in dom.getElementsByTagName('target'):
596 #base_path = target.getAttribute('path')
597 for entry in target.getElementsByTagName('entry'):
598 file_path = entry.getAttribute('path')
599 wc_status = entry.getElementsByTagName('wc-status')
600 assert len(wc_status) == 1
601 # Emulate svn 1.5 status ouput...
602 statuses = [' '] * 7
603 # Col 0
604 xml_item_status = wc_status[0].getAttribute('item')
605 if xml_item_status in status_letter:
606 statuses[0] = status_letter[xml_item_status]
607 else:
maruel@chromium.org54019f32010-09-09 13:50:11 +0000608 raise gclient_utils.Error(
609 'Unknown item status "%s"; please implement me!' %
610 xml_item_status)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000611 # Col 1
612 xml_props_status = wc_status[0].getAttribute('props')
613 if xml_props_status == 'modified':
614 statuses[1] = 'M'
615 elif xml_props_status == 'conflicted':
616 statuses[1] = 'C'
617 elif (not xml_props_status or xml_props_status == 'none' or
618 xml_props_status == 'normal'):
619 pass
620 else:
maruel@chromium.org54019f32010-09-09 13:50:11 +0000621 raise gclient_utils.Error(
622 'Unknown props status "%s"; please implement me!' %
623 xml_props_status)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000624 # Col 2
625 if wc_status[0].getAttribute('wc-locked') == 'true':
626 statuses[2] = 'L'
627 # Col 3
628 if wc_status[0].getAttribute('copied') == 'true':
629 statuses[3] = '+'
630 # Col 4
631 if wc_status[0].getAttribute('switched') == 'true':
632 statuses[4] = 'S'
633 # TODO(maruel): Col 5 and 6
634 item = (''.join(statuses), file_path)
635 results.append(item)
636 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000637
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000638 @staticmethod
639 def IsMoved(filename):
640 """Determine if a file has been added through svn mv"""
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000641 return SVN.IsMovedInfo(SVN.CaptureInfo(filename))
642
643 @staticmethod
644 def IsMovedInfo(info):
645 """Determine if a file has been added through svn mv"""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000646 return (info.get('Copied From URL') and
647 info.get('Copied From Rev') and
648 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000649
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000650 @staticmethod
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000651 def GetFileProperty(filename, property_name):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000652 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000653
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000654 Args:
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000655 filename: The file to check
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000656 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000657
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000658 Returns:
659 The value of the property, which will be the empty string if the property
660 is not set on the file. If the file is not under version control, the
661 empty string is also returned.
662 """
maruel@chromium.org54019f32010-09-09 13:50:11 +0000663 try:
664 return SVN.Capture(['propget', property_name, filename])
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000665 except (gclient_utils.Error, subprocess2.CalledProcessError):
maruel@chromium.org54019f32010-09-09 13:50:11 +0000666 return ''
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000667
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000668 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000669 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000670 """Diffs a single file.
671
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000672 Should be simple, eh? No it isn't.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000673 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000674 expected relative path.
675 full_move means that move or copy operations should completely recreate the
676 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000677 # If the user specified a custom diff command in their svn config file,
678 # then it'll be used when we do svn diff, which we don't want to happen
679 # since we want the unified diff. Using --diff-cmd=diff doesn't always
680 # work, since they can have another diff executable in their path that
681 # gives different line endings. So we use a bogus temp directory as the
682 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000683 bogus_dir = tempfile.mkdtemp()
684 try:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000685 # Use "svn info" output instead of os.path.isdir because the latter fails
686 # when the file is deleted.
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000687 return SVN._DiffItemInternal(filename, SVN.CaptureInfo(filename),
688 bogus_dir,
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000689 full_move=full_move, revision=revision)
690 finally:
691 shutil.rmtree(bogus_dir)
692
693 @staticmethod
694 def _DiffItemInternal(filename, info, bogus_dir, full_move=False,
695 revision=None):
696 """Grabs the diff data."""
697 command = ["diff", "--config-dir", bogus_dir, filename]
698 if revision:
699 command.extend(['--revision', revision])
700 data = None
701 if SVN.IsMovedInfo(info):
702 if full_move:
703 if info.get("Node Kind") == "directory":
704 # Things become tricky here. It's a directory copy/move. We need to
705 # diff all the files inside it.
706 # This will put a lot of pressure on the heap. This is why StringIO
707 # is used and converted back into a string at the end. The reason to
708 # return a string instead of a StringIO is that StringIO.write()
709 # doesn't accept a StringIO object. *sigh*.
710 for (dirpath, dirnames, filenames) in os.walk(filename):
711 # Cleanup all files starting with a '.'.
712 for d in dirnames:
713 if d.startswith('.'):
714 dirnames.remove(d)
715 for f in filenames:
716 if f.startswith('.'):
717 filenames.remove(f)
718 for f in filenames:
719 if data is None:
720 data = cStringIO.StringIO()
721 data.write(GenFakeDiff(os.path.join(dirpath, f)))
722 if data:
723 tmp = data.getvalue()
724 data.close()
725 data = tmp
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000726 else:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000727 data = GenFakeDiff(filename)
728 else:
729 if info.get("Node Kind") != "directory":
maruel@chromium.org0836c562010-01-22 01:10:06 +0000730 # svn diff on a mv/cp'd file outputs nothing if there was no change.
maruel@chromium.org54019f32010-09-09 13:50:11 +0000731 data = SVN.Capture(command)
maruel@chromium.org0836c562010-01-22 01:10:06 +0000732 if not data:
733 # We put in an empty Index entry so upload.py knows about them.
maruel@chromium.orgc6d170e2010-06-03 00:06:00 +0000734 data = "Index: %s\n" % filename.replace(os.sep, '/')
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000735 # Otherwise silently ignore directories.
736 else:
737 if info.get("Node Kind") != "directory":
738 # Normal simple case.
maruel@chromium.orgf8b3f942011-03-24 17:33:50 +0000739 try:
740 data = SVN.Capture(command)
741 except gclient_utils.CheckCallError:
742 if revision:
743 data = GenFakeDiff(filename)
744 else:
745 raise
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000746 # Otherwise silently ignore directories.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000747 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000748
749 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000750 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000751 """Returns a string containing the diff for the given file list.
752
753 The files in the list should either be absolute paths or relative to the
754 given root. If no root directory is provided, the repository root will be
755 used.
756 The diff will always use relative paths.
757 """
maruel@chromium.org00fdcb32011-02-24 01:41:02 +0000758 assert isinstance(filenames, (list, tuple))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000759 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000760 root = root or SVN.GetCheckoutRoot(previous_cwd)
761 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000762 def RelativePath(path, root):
763 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000764 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000765 return path[len(root):]
766 return path
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000767 # If the user specified a custom diff command in their svn config file,
768 # then it'll be used when we do svn diff, which we don't want to happen
769 # since we want the unified diff. Using --diff-cmd=diff doesn't always
770 # work, since they can have another diff executable in their path that
771 # gives different line endings. So we use a bogus temp directory as the
772 # config directory, which gets around these problems.
773 bogus_dir = tempfile.mkdtemp()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000774 try:
775 os.chdir(root)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000776 # Cleanup filenames
777 filenames = [RelativePath(f, root) for f in filenames]
778 # Get information about the modified items (files and directories)
779 data = dict([(f, SVN.CaptureInfo(f)) for f in filenames])
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000780 diffs = []
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000781 if full_move:
782 # Eliminate modified files inside moved/copied directory.
783 for (filename, info) in data.iteritems():
784 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
785 # Remove files inside the directory.
786 filenames = [f for f in filenames
787 if not f.startswith(filename + os.path.sep)]
788 for filename in data.keys():
789 if not filename in filenames:
790 # Remove filtered out items.
791 del data[filename]
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000792 else:
793 metaheaders = []
794 for (filename, info) in data.iteritems():
795 if SVN.IsMovedInfo(info):
796 # for now, the most common case is a head copy,
797 # so let's just encode that as a straight up cp.
798 srcurl = info.get('Copied From URL')
799 root = info.get('Repository Root')
800 rev = int(info.get('Copied From Rev'))
801 assert srcurl.startswith(root)
802 src = srcurl[len(root)+1:]
maruel@chromium.org00fdcb32011-02-24 01:41:02 +0000803 try:
804 srcinfo = SVN.CaptureInfo(srcurl)
805 except gclient_utils.CheckCallError, e:
806 if not 'Not a valid URL' in e.stderr:
807 raise
808 # Assume the file was deleted. No idea how to figure out at which
809 # revision the file was deleted.
810 srcinfo = {'Revision': rev}
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000811 if (srcinfo.get('Revision') != rev and
812 SVN.Capture(['diff', '-r', '%d:head' % rev, srcurl])):
813 metaheaders.append("#$ svn cp -r %d %s %s "
814 "### WARNING: note non-trunk copy\n" %
815 (rev, src, filename))
816 else:
817 metaheaders.append("#$ cp %s %s\n" % (src,
818 filename))
819
820 if metaheaders:
821 diffs.append("### BEGIN SVN COPY METADATA\n")
822 diffs.extend(metaheaders)
823 diffs.append("### END SVN COPY METADATA\n")
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000824 # Now ready to do the actual diff.
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000825 for filename in sorted(data.iterkeys()):
826 diffs.append(SVN._DiffItemInternal(filename, data[filename], bogus_dir,
827 full_move=full_move,
828 revision=revision))
829 # Use StringIO since it can be messy when diffing a directory move with
830 # full_move=True.
831 buf = cStringIO.StringIO()
832 for d in filter(None, diffs):
833 buf.write(d)
834 result = buf.getvalue()
835 buf.close()
836 return result
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000837 finally:
838 os.chdir(previous_cwd)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000839 shutil.rmtree(bogus_dir)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000840
841 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000842 def GetEmail(repo_root):
843 """Retrieves the svn account which we assume is an email address."""
maruel@chromium.org54019f32010-09-09 13:50:11 +0000844 try:
845 infos = SVN.CaptureInfo(repo_root)
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000846 except (gclient_utils.Error, subprocess2.CalledProcessError):
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000847 return None
848
849 # Should check for uuid but it is incorrectly saved for https creds.
maruel@chromium.org54019f32010-09-09 13:50:11 +0000850 root = infos['Repository Root']
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000851 realm = root.rsplit('/', 1)[0]
maruel@chromium.org54019f32010-09-09 13:50:11 +0000852 uuid = infos['UUID']
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000853 if root.startswith('https') or not uuid:
854 regexp = re.compile(r'<%s:\d+>.*' % realm)
855 else:
856 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
857 if regexp is None:
858 return None
859 if sys.platform.startswith('win'):
860 if not 'APPDATA' in os.environ:
861 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000862 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
863 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000864 else:
865 if not 'HOME' in os.environ:
866 return None
867 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
868 'svn.simple')
869 for credfile in os.listdir(auth_dir):
870 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
871 if regexp.match(cred_info.get('svn:realmstring')):
872 return cred_info.get('username')
873
874 @staticmethod
875 def ReadSimpleAuth(filename):
876 f = open(filename, 'r')
877 values = {}
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000878 def ReadOneItem(item_type):
879 m = re.match(r'%s (\d+)' % item_type, f.readline())
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000880 if not m:
881 return None
882 data = f.read(int(m.group(1)))
883 if f.read(1) != '\n':
884 return None
885 return data
886
887 while True:
888 key = ReadOneItem('K')
889 if not key:
890 break
891 value = ReadOneItem('V')
892 if not value:
893 break
894 values[key] = value
895 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000896
897 @staticmethod
898 def GetCheckoutRoot(directory):
899 """Returns the top level directory of the current repository.
900
901 The directory is returned as an absolute path.
902 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000903 directory = os.path.abspath(directory)
maruel@chromium.org54019f32010-09-09 13:50:11 +0000904 try:
maruel@chromium.org885d6e82011-02-24 20:21:46 +0000905 info = SVN.CaptureInfo(directory)
906 cur_dir_repo_root = info['Repository Root']
907 url = info['URL']
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000908 except (gclient_utils.Error, subprocess2.CalledProcessError):
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000909 return None
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000910 while True:
911 parent = os.path.dirname(directory)
maruel@chromium.org54019f32010-09-09 13:50:11 +0000912 try:
maruel@chromium.org885d6e82011-02-24 20:21:46 +0000913 info = SVN.CaptureInfo(parent)
914 if (info['Repository Root'] != cur_dir_repo_root or
915 info['URL'] != os.path.dirname(url)):
maruel@chromium.org54019f32010-09-09 13:50:11 +0000916 break
maruel@chromium.org885d6e82011-02-24 20:21:46 +0000917 url = info['URL']
maruel@chromium.org31cb48a2011-04-04 18:01:36 +0000918 except (gclient_utils.Error, subprocess2.CalledProcessError):
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000919 break
920 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000921 return GetCasedPath(directory)
tony@chromium.org57564662010-04-14 02:35:12 +0000922
923 @staticmethod
924 def AssertVersion(min_version):
925 """Asserts svn's version is at least min_version."""
926 def only_int(val):
927 if val.isdigit():
928 return int(val)
929 else:
930 return 0
931 if not SVN.current_version:
932 SVN.current_version = SVN.Capture(['--version']).split()[2]
933 current_version_list = map(only_int, SVN.current_version.split('.'))
934 for min_ver in map(int, min_version.split('.')):
935 ver = current_version_list.pop(0)
936 if ver < min_ver:
937 return (False, SVN.current_version)
938 elif ver > min_ver:
939 return (True, SVN.current_version)
940 return (True, SVN.current_version)
maruel@chromium.org07ab60e2011-02-08 21:54:00 +0000941
942 @staticmethod
943 def Revert(repo_root, callback=None, ignore_externals=False):
944 """Reverts all svn modifications in repo_root, including properties.
945
946 Deletes any modified files or directory.
947
948 A "svn update --revision BASE" call is required after to revive deleted
949 files.
950 """
951 for file_status in SVN.CaptureStatus(repo_root):
952 file_path = os.path.join(repo_root, file_status[1])
maruel@chromium.org8c415122011-03-15 17:14:27 +0000953 if (ignore_externals and
954 file_status[0][0] == 'X' and
955 file_status[0][1:].isspace()):
maruel@chromium.org07ab60e2011-02-08 21:54:00 +0000956 # Ignore externals.
957 logging.info('Ignoring external %s' % file_status[1])
958 continue
959
960 if callback:
961 callback(file_status)
962
maruel@chromium.org8c415122011-03-15 17:14:27 +0000963 if os.path.exists(file_path):
964 # svn revert is really stupid. It fails on inconsistent line-endings,
965 # on switched directories, etc. So take no chance and delete everything!
966 # In theory, it wouldn't be necessary for property-only change but then
967 # it'd have to look for switched directories, etc so it's not worth
968 # optimizing this use case.
969 if os.path.isfile(file_path) or os.path.islink(file_path):
970 logging.info('os.remove(%s)' % file_path)
971 os.remove(file_path)
972 elif os.path.isdir(file_path):
973 logging.info('gclient_utils.RemoveDirectory(%s)' % file_path)
974 gclient_utils.RemoveDirectory(file_path)
975 else:
976 logging.critical(
977 ('No idea what is %s.\nYou just found a bug in gclient'
978 ', please ping maruel@chromium.org ASAP!') % file_path)
maruel@chromium.org07ab60e2011-02-08 21:54:00 +0000979
maruel@chromium.org8c415122011-03-15 17:14:27 +0000980 if (file_status[0][0] in ('D', 'A', '!') or
981 not file_status[0][1:].isspace()):
maruel@chromium.orgaf453492011-03-03 21:04:09 +0000982 # Added, deleted file requires manual intervention and require calling
maruel@chromium.org07ab60e2011-02-08 21:54:00 +0000983 # revert, like for properties.
maruel@chromium.orgaf453492011-03-03 21:04:09 +0000984 try:
985 SVN.Capture(['revert', file_status[1]], cwd=repo_root)
986 except gclient_utils.CheckCallError:
987 if not os.path.exists(file_path):
988 continue
989 raise