blob: 924f1264166032b21fda748b7de066ce9e3a4c2f [file] [log] [blame]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00001# Copyright (c) 2006-2009 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00005"""SCM-specific utility classes."""
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00006
maruel@chromium.org3c55d982010-05-06 14:25:44 +00007import cStringIO
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +00008import glob
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00009import os
10import re
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000011import shutil
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000012import subprocess
13import sys
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000014import tempfile
maruel@chromium.orgfd876172010-04-30 14:01:05 +000015import time
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000016import xml.dom.minidom
17
18import gclient_utils
19
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000020def ValidateEmail(email):
maruel@chromium.org6e29d572010-06-04 17:32:20 +000021 return (re.match(r"^[a-zA-Z0-9._%-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", email)
22 is not None)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000023
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000024
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +000025def GetCasedPath(path):
26 """Elcheapos way to get the real path case on Windows."""
27 if sys.platform.startswith('win') and os.path.exists(path):
28 # Reconstruct the path.
29 path = os.path.abspath(path)
30 paths = path.split('\\')
31 for i in range(len(paths)):
32 if i == 0:
33 # Skip drive letter.
34 continue
35 subpath = '\\'.join(paths[:i+1])
36 prev = len('\\'.join(paths[:i]))
37 # glob.glob will return the cased path for the last item only. This is why
38 # we are calling it in a loop. Extract the data we want and put it back
39 # into the list.
40 paths[i] = glob.glob(subpath + '*')[0][prev+1:len(subpath)]
41 path = '\\'.join(paths)
42 return path
43
44
maruel@chromium.org3c55d982010-05-06 14:25:44 +000045def GenFakeDiff(filename):
46 """Generates a fake diff from a file."""
47 file_content = gclient_utils.FileRead(filename, 'rb').splitlines(True)
maruel@chromium.orgc6d170e2010-06-03 00:06:00 +000048 filename = filename.replace(os.sep, '/')
maruel@chromium.org3c55d982010-05-06 14:25:44 +000049 nb_lines = len(file_content)
50 # We need to use / since patch on unix will fail otherwise.
51 data = cStringIO.StringIO()
52 data.write("Index: %s\n" % filename)
53 data.write('=' * 67 + '\n')
54 # Note: Should we use /dev/null instead?
55 data.write("--- %s\n" % filename)
56 data.write("+++ %s\n" % filename)
57 data.write("@@ -0,0 +1,%d @@\n" % nb_lines)
58 # Prepend '+' to every lines.
59 for line in file_content:
60 data.write('+')
61 data.write(line)
62 result = data.getvalue()
63 data.close()
64 return result
65
66
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000067class GIT(object):
68 COMMAND = "git"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000069
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000070 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000071 def Capture(args, in_directory=None, print_error=True, error_ok=False):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000072 """Runs git, capturing output sent to stdout as a string.
73
74 Args:
75 args: A sequence of command line parameters to be passed to git.
76 in_directory: The directory where git is to be run.
77
78 Returns:
79 The output sent to stdout as a string.
80 """
81 c = [GIT.COMMAND]
82 c.extend(args)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000083 try:
84 return gclient_utils.CheckCall(c, in_directory, print_error)
85 except gclient_utils.CheckCallError:
86 if error_ok:
nasser@codeaurora.orgcd968c12010-02-01 06:05:00 +000087 return ('', '')
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000088 raise
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000089
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000090 @staticmethod
msb@chromium.org786fb682010-06-02 15:16:23 +000091 def CaptureStatus(files, upstream_branch=None):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000092 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000093
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000094 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000095
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000096 Returns an array of (status, file) tuples."""
msb@chromium.org786fb682010-06-02 15:16:23 +000097 if upstream_branch is None:
98 upstream_branch = GIT.GetUpstreamBranch(os.getcwd())
99 if upstream_branch is None:
100 raise Exception("Cannot determine upstream branch")
bauerb@chromium.org14ec5042010-03-30 18:19:09 +0000101 command = ["diff", "--name-status", "-r", "%s..." % upstream_branch]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000102 if not files:
103 pass
104 elif isinstance(files, basestring):
105 command.append(files)
106 else:
107 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000108
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000109 status = GIT.Capture(command)[0].rstrip()
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000110 results = []
111 if status:
112 for statusline in status.split('\n'):
113 m = re.match('^(\w)\t(.+)$', statusline)
114 if not m:
115 raise Exception("status currently unsupported: %s" % statusline)
116 results.append(('%s ' % m.group(1), m.group(2)))
117 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000118
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000119 @staticmethod
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000120 def RunAndFilterOutput(args,
121 in_directory,
122 print_messages,
123 print_stdout,
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000124 filter_fn):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000125 """Runs a command, optionally outputting to stdout.
126
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000127 stdout is passed line-by-line to the given filter_fn function. If
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000128 print_stdout is true, it is also printed to sys.stdout as in Run.
129
130 Args:
131 args: A sequence of command line parameters to be passed.
msb@chromium.orgd6504212010-01-13 17:34:31 +0000132 in_directory: The directory where git is to be run.
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000133 print_messages: Whether to print status messages to stdout about
134 which commands are being run.
135 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000136 filter_fn: A function taking one argument (a string) which will be
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000137 passed each line (with the ending newline character removed) of
138 program's output for filtering.
139
140 Raises:
141 gclient_utils.Error: An error occurred while running the command.
142 """
143 command = [GIT.COMMAND]
144 command.extend(args)
145 gclient_utils.SubprocessCallAndFilter(command,
146 in_directory,
147 print_messages,
148 print_stdout,
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000149 filter_fn=filter_fn)
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000150
151 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000152 def GetEmail(repo_root):
153 """Retrieves the user email address if known."""
154 # We could want to look at the svn cred when it has a svn remote but it
155 # should be fine for now, users should simply configure their git settings.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000156 return GIT.Capture(['config', 'user.email'],
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000157 repo_root, error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000158
159 @staticmethod
160 def ShortBranchName(branch):
161 """Converts a name like 'refs/heads/foo' to just 'foo'."""
162 return branch.replace('refs/heads/', '')
163
164 @staticmethod
165 def GetBranchRef(cwd):
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000166 """Returns the full branch reference, e.g. 'refs/heads/master'."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000167 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000168
169 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000170 def GetBranch(cwd):
171 """Returns the short branch name, e.g. 'master'."""
maruel@chromium.orgc308a742009-12-22 18:29:33 +0000172 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000173
174 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000175 def IsGitSvn(cwd):
176 """Returns true if this repo looks like it's using git-svn."""
177 # If you have any "svn-remote.*" config keys, we think you're using svn.
178 try:
179 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd)
180 return True
181 except gclient_utils.CheckCallError:
182 return False
183
184 @staticmethod
185 def GetSVNBranch(cwd):
186 """Returns the svn branch name if found."""
187 # Try to figure out which remote branch we're based on.
188 # Strategy:
189 # 1) find all git-svn branches and note their svn URLs.
190 # 2) iterate through our branch history and match up the URLs.
191
192 # regexp matching the git-svn line that contains the URL.
193 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
194
195 # Get the refname and svn url for all refs/remotes/*.
196 remotes = GIT.Capture(
197 ['for-each-ref', '--format=%(refname)', 'refs/remotes'],
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000198 cwd)[0].splitlines()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000199 svn_refs = {}
200 for ref in remotes:
201 match = git_svn_re.search(
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000202 GIT.Capture(['cat-file', '-p', ref], cwd)[0])
sky@chromium.org42d8da52010-04-23 18:25:07 +0000203 # Prefer origin/HEAD over all others.
204 if match and (match.group(1) not in svn_refs or
205 ref == "refs/remotes/origin/HEAD"):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000206 svn_refs[match.group(1)] = ref
207
208 svn_branch = ''
209 if len(svn_refs) == 1:
210 # Only one svn branch exists -- seems like a good candidate.
211 svn_branch = svn_refs.values()[0]
212 elif len(svn_refs) > 1:
213 # We have more than one remote branch available. We don't
214 # want to go through all of history, so read a line from the
215 # pipe at a time.
216 # The -100 is an arbitrary limit so we don't search forever.
217 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org3a292682010-08-23 18:54:55 +0000218 proc = gclient_utils.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000219 for line in proc.stdout:
220 match = git_svn_re.match(line)
221 if match:
222 url = match.group(1)
223 if url in svn_refs:
224 svn_branch = svn_refs[url]
225 proc.stdout.close() # Cut pipe.
226 break
227 return svn_branch
228
229 @staticmethod
230 def FetchUpstreamTuple(cwd):
231 """Returns a tuple containg remote and remote ref,
232 e.g. 'origin', 'refs/heads/master'
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000233 Tries to be intelligent and understand git-svn.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000234 """
235 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000236 branch = GIT.GetBranch(cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000237 upstream_branch = None
238 upstream_branch = GIT.Capture(
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000239 ['config', 'branch.%s.merge' % branch], in_directory=cwd,
240 error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000241 if upstream_branch:
242 remote = GIT.Capture(
243 ['config', 'branch.%s.remote' % branch],
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000244 in_directory=cwd, error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000245 else:
246 # Fall back on trying a git-svn upstream branch.
247 if GIT.IsGitSvn(cwd):
248 upstream_branch = GIT.GetSVNBranch(cwd)
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000249 else:
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000250 # Else, try to guess the origin remote.
251 remote_branches = GIT.Capture(
252 ['branch', '-r'], in_directory=cwd)[0].split()
253 if 'origin/master' in remote_branches:
254 # Fall back on origin/master if it exits.
255 remote = 'origin'
256 upstream_branch = 'refs/heads/master'
257 elif 'origin/trunk' in remote_branches:
258 # Fall back on origin/trunk if it exists. Generally a shared
259 # git-svn clone
260 remote = 'origin'
261 upstream_branch = 'refs/heads/trunk'
262 else:
263 # Give up.
264 remote = None
265 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000266 return remote, upstream_branch
267
268 @staticmethod
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000269 def GetUpstreamBranch(cwd):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000270 """Gets the current branch's upstream branch."""
271 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000272 if remote != '.' and upstream_branch:
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000273 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
274 return upstream_branch
275
276 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000277 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
278 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000279 """Diffs against the upstream branch or optionally another branch.
280
281 full_move means that move or copy operations should completely recreate the
282 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000283 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000284 branch = GIT.GetUpstreamBranch(cwd)
evan@chromium.org400f3e72010-05-19 14:23:36 +0000285 command = ['diff', '-p', '--no-prefix', '--no-ext-diff',
286 branch + "..." + branch_head]
maruel@chromium.orga9371762009-12-22 18:27:38 +0000287 if not full_move:
288 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000289 # TODO(maruel): --binary support.
290 if files:
291 command.append('--')
292 command.extend(files)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000293 diff = GIT.Capture(command, cwd)[0].splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000294 for i in range(len(diff)):
295 # In the case of added files, replace /dev/null with the path to the
296 # file being added.
297 if diff[i].startswith('--- /dev/null'):
298 diff[i] = '--- %s' % diff[i+1][4:]
299 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000300
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000301 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000302 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
303 """Returns the list of modified files between two branches."""
304 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000305 branch = GIT.GetUpstreamBranch(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000306 command = ['diff', '--name-only', branch + "..." + branch_head]
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000307 return GIT.Capture(command, cwd)[0].splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000308
309 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000310 def GetPatchName(cwd):
311 """Constructs a name for this patch."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000312 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd)[0].strip()
maruel@chromium.org862ff8e2010-08-06 15:29:16 +0000313 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000314
315 @staticmethod
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000316 def GetCheckoutRoot(path):
317 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000318 """
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000319 root = GIT.Capture(['rev-parse', '--show-cdup'], path)[0].strip()
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000320 return os.path.abspath(os.path.join(path, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000321
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000322 @staticmethod
323 def AssertVersion(min_version):
324 """Asserts git's version is at least min_version."""
325 def only_int(val):
326 if val.isdigit():
327 return int(val)
328 else:
329 return 0
330 current_version = GIT.Capture(['--version'])[0].split()[-1]
331 current_version_list = map(only_int, current_version.split('.'))
332 for min_ver in map(int, min_version.split('.')):
333 ver = current_version_list.pop(0)
334 if ver < min_ver:
335 return (False, current_version)
336 elif ver > min_ver:
337 return (True, current_version)
338 return (True, current_version)
339
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000340
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000341class SVN(object):
342 COMMAND = "svn"
tony@chromium.org57564662010-04-14 02:35:12 +0000343 current_version = None
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000344
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000345 @staticmethod
346 def Run(args, in_directory):
347 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000348
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000349 Args:
350 args: A sequence of command line parameters to be passed to svn.
351 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000352
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000353 Raises:
354 Error: An error occurred while running the svn command.
355 """
356 c = [SVN.COMMAND]
357 c.extend(args)
maruel@chromium.org2185f002009-12-18 21:03:47 +0000358 # TODO(maruel): This is very gclient-specific.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000359 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000360
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000361 @staticmethod
362 def Capture(args, in_directory=None, print_error=True):
363 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000364
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000365 Args:
366 args: A sequence of command line parameters to be passed to svn.
367 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000368
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000369 Returns:
370 The output sent to stdout as a string.
371 """
372 c = [SVN.COMMAND]
373 c.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000374 stderr = None
375 if not print_error:
376 stderr = subprocess.PIPE
maruel@chromium.org3a292682010-08-23 18:54:55 +0000377 return gclient_utils.Popen(c, cwd=in_directory, stdout=subprocess.PIPE,
378 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000379
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000380 @staticmethod
maruel@chromium.org03807072010-08-16 17:18:44 +0000381 def RunAndGetFileList(verbose, args, in_directory, file_list):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000382 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000383
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000384 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000385
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000386 svn's stdout is parsed to collect a list of files checked out or updated.
387 These files are appended to file_list. svn's stdout is also printed to
388 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000389
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000390 Args:
maruel@chromium.org03807072010-08-16 17:18:44 +0000391 verbose: If True, uses verbose output
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000392 args: A sequence of command line parameters to be passed to svn.
393 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000394
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000395 Raises:
396 Error: An error occurred while running the svn command.
397 """
398 command = [SVN.COMMAND]
399 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000400
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000401 # svn update and svn checkout use the same pattern: the first three columns
402 # are for file status, property status, and lock status. This is followed
403 # by two spaces, and then the path to the file.
404 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000405
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000406 # The first three columns of svn status are the same as for svn update and
407 # svn checkout. The next three columns indicate addition-with-history,
408 # switch, and remote lock status. This is followed by one space, and then
409 # the path to the file.
410 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000411
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000412 # args[0] must be a supported command. This will blow up if it's something
413 # else, which is good. Note that the patterns are only effective when
414 # these commands are used in their ordinary forms, the patterns are invalid
415 # for "svn status --show-updates", for example.
416 pattern = {
417 'checkout': update_pattern,
418 'status': status_pattern,
419 'update': update_pattern,
420 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000421 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000422 # Place an upper limit.
maruel@chromium.orgfd876172010-04-30 14:01:05 +0000423 for _ in range(10):
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000424 previous_list_len = len(file_list)
425 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000426
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000427 def CaptureMatchingLines(line):
428 match = compiled_pattern.search(line)
429 if match:
430 file_list.append(match.group(1))
431 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000432 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000433
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000434 try:
435 SVN.RunAndFilterOutput(args,
436 in_directory,
maruel@chromium.org03807072010-08-16 17:18:44 +0000437 verbose,
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000438 True,
439 CaptureMatchingLines)
440 except gclient_utils.Error:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000441 def IsKnownFailure():
442 for x in failure:
443 if (x.startswith('svn: OPTIONS of') or
444 x.startswith('svn: PROPFIND of') or
445 x.startswith('svn: REPORT of') or
maruel@chromium.orgf61fc932010-08-19 13:05:24 +0000446 x.startswith('svn: Unknown hostname') or
447 x.startswith('svn: Server sent unexpected return value')):
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000448 return True
449 return False
450
maruel@chromium.org953586a2010-06-15 14:22:24 +0000451 # Subversion client is really misbehaving with Google Code.
452 if args[0] == 'checkout':
453 # Ensure at least one file was checked out, otherwise *delete* the
454 # directory.
455 if len(file_list) == previous_list_len:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000456 if not IsKnownFailure():
maruel@chromium.org953586a2010-06-15 14:22:24 +0000457 # No known svn error was found, bail out.
458 raise
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000459 # No file were checked out, so make sure the directory is
460 # deleted in case it's messed up and try again.
461 # Warning: It's bad, it assumes args[2] is the directory
462 # argument.
463 if os.path.isdir(args[2]):
464 gclient_utils.RemoveDirectory(args[2])
maruel@chromium.org953586a2010-06-15 14:22:24 +0000465 else:
466 # Progress was made, convert to update since an aborted checkout
467 # is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000468 args = ['update'] + args[1:]
maruel@chromium.org953586a2010-06-15 14:22:24 +0000469 else:
470 # It was an update or export.
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000471 # We enforce that some progress has been made or a known failure.
472 if len(file_list) == previous_list_len and not IsKnownFailure():
473 # No known svn error was found and no progress, bail out.
474 raise
maruel@chromium.org953586a2010-06-15 14:22:24 +0000475 print "Sleeping 15 seconds and retrying...."
476 time.sleep(15)
477 continue
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000478 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000479
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000480 @staticmethod
481 def RunAndFilterOutput(args,
482 in_directory,
483 print_messages,
484 print_stdout,
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000485 filter_fn):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000486 """Runs a command, optionally outputting to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000487
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000488 stdout is passed line-by-line to the given filter_fn function. If
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000489 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000490
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000491 Args:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000492 args: A sequence of command line parameters to be passed.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000493 in_directory: The directory where svn is to be run.
494 print_messages: Whether to print status messages to stdout about
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000495 which commands are being run.
496 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000497 filter_fn: A function taking one argument (a string) which will be
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000498 passed each line (with the ending newline character removed) of
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000499 program's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000500
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000501 Raises:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000502 gclient_utils.Error: An error occurred while running the command.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000503 """
504 command = [SVN.COMMAND]
505 command.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000506 gclient_utils.SubprocessCallAndFilter(command,
507 in_directory,
508 print_messages,
509 print_stdout,
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000510 filter_fn=filter_fn)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000511
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000512 @staticmethod
513 def CaptureInfo(relpath, in_directory=None, print_error=True):
514 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000515
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000516 Args:
517 relpath: The directory where the working copy resides relative to
518 the directory given by in_directory.
519 in_directory: The directory where svn is to be run.
520 """
521 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
522 dom = gclient_utils.ParseXML(output)
523 result = {}
524 if dom:
525 GetNamedNodeText = gclient_utils.GetNamedNodeText
526 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
527 def C(item, f):
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000528 if item is not None:
529 return f(item)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000530 # /info/entry/
531 # url
532 # reposityory/(root|uuid)
533 # wc-info/(schedule|depth)
534 # commit/(author|date)
535 # str() the results because they may be returned as Unicode, which
536 # interferes with the higher layers matching up things in the deps
537 # dictionary.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000538 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
539 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
540 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
541 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
542 'revision'),
543 int)
544 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
545 str)
546 # Differs across versions.
547 if result['Node Kind'] == 'dir':
548 result['Node Kind'] = 'directory'
549 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
550 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
551 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
552 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
553 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000554
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000555 @staticmethod
556 def CaptureHeadRevision(url):
557 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000558
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000559 Returns:
560 Int head revision
561 """
562 info = SVN.Capture(["info", "--xml", url], os.getcwd())
563 dom = xml.dom.minidom.parseString(info)
564 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000565
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000566 @staticmethod
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000567 def CaptureBaseRevision(cwd):
568 """Get the base revision of a SVN repository.
569
570 Returns:
571 Int base revision
572 """
573 info = SVN.Capture(["info", "--xml"], cwd)
574 dom = xml.dom.minidom.parseString(info)
575 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
576
577 @staticmethod
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000578 def CaptureStatus(files):
579 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000580
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000581 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000582
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000583 Returns an array of (status, file) tuples."""
584 command = ["status", "--xml"]
585 if not files:
586 pass
587 elif isinstance(files, basestring):
588 command.append(files)
589 else:
590 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000591
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000592 status_letter = {
593 None: ' ',
594 '': ' ',
595 'added': 'A',
596 'conflicted': 'C',
597 'deleted': 'D',
598 'external': 'X',
599 'ignored': 'I',
600 'incomplete': '!',
601 'merged': 'G',
602 'missing': '!',
603 'modified': 'M',
604 'none': ' ',
605 'normal': ' ',
606 'obstructed': '~',
607 'replaced': 'R',
608 'unversioned': '?',
609 }
610 dom = gclient_utils.ParseXML(SVN.Capture(command))
611 results = []
612 if dom:
613 # /status/target/entry/(wc-status|commit|author|date)
614 for target in dom.getElementsByTagName('target'):
615 #base_path = target.getAttribute('path')
616 for entry in target.getElementsByTagName('entry'):
617 file_path = entry.getAttribute('path')
618 wc_status = entry.getElementsByTagName('wc-status')
619 assert len(wc_status) == 1
620 # Emulate svn 1.5 status ouput...
621 statuses = [' '] * 7
622 # Col 0
623 xml_item_status = wc_status[0].getAttribute('item')
624 if xml_item_status in status_letter:
625 statuses[0] = status_letter[xml_item_status]
626 else:
627 raise Exception('Unknown item status "%s"; please implement me!' %
628 xml_item_status)
629 # Col 1
630 xml_props_status = wc_status[0].getAttribute('props')
631 if xml_props_status == 'modified':
632 statuses[1] = 'M'
633 elif xml_props_status == 'conflicted':
634 statuses[1] = 'C'
635 elif (not xml_props_status or xml_props_status == 'none' or
636 xml_props_status == 'normal'):
637 pass
638 else:
639 raise Exception('Unknown props status "%s"; please implement me!' %
640 xml_props_status)
641 # Col 2
642 if wc_status[0].getAttribute('wc-locked') == 'true':
643 statuses[2] = 'L'
644 # Col 3
645 if wc_status[0].getAttribute('copied') == 'true':
646 statuses[3] = '+'
647 # Col 4
648 if wc_status[0].getAttribute('switched') == 'true':
649 statuses[4] = 'S'
650 # TODO(maruel): Col 5 and 6
651 item = (''.join(statuses), file_path)
652 results.append(item)
653 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000654
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000655 @staticmethod
656 def IsMoved(filename):
657 """Determine if a file has been added through svn mv"""
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000658 return SVN.IsMovedInfo(SVN.CaptureInfo(filename))
659
660 @staticmethod
661 def IsMovedInfo(info):
662 """Determine if a file has been added through svn mv"""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000663 return (info.get('Copied From URL') and
664 info.get('Copied From Rev') and
665 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000666
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000667 @staticmethod
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000668 def GetFileProperty(filename, property_name):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000669 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000670
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000671 Args:
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000672 filename: The file to check
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000673 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000674
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000675 Returns:
676 The value of the property, which will be the empty string if the property
677 is not set on the file. If the file is not under version control, the
678 empty string is also returned.
679 """
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000680 output = SVN.Capture(["propget", property_name, filename])
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000681 if (output.startswith("svn: ") and
682 output.endswith("is not under version control")):
683 return ""
684 else:
685 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000686
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000687 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000688 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000689 """Diffs a single file.
690
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000691 Should be simple, eh? No it isn't.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000692 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000693 expected relative path.
694 full_move means that move or copy operations should completely recreate the
695 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000696 # If the user specified a custom diff command in their svn config file,
697 # then it'll be used when we do svn diff, which we don't want to happen
698 # since we want the unified diff. Using --diff-cmd=diff doesn't always
699 # work, since they can have another diff executable in their path that
700 # gives different line endings. So we use a bogus temp directory as the
701 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000702 bogus_dir = tempfile.mkdtemp()
703 try:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000704 # Use "svn info" output instead of os.path.isdir because the latter fails
705 # when the file is deleted.
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000706 return SVN._DiffItemInternal(filename, SVN.CaptureInfo(filename),
707 bogus_dir,
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000708 full_move=full_move, revision=revision)
709 finally:
710 shutil.rmtree(bogus_dir)
711
712 @staticmethod
713 def _DiffItemInternal(filename, info, bogus_dir, full_move=False,
714 revision=None):
715 """Grabs the diff data."""
716 command = ["diff", "--config-dir", bogus_dir, filename]
717 if revision:
718 command.extend(['--revision', revision])
719 data = None
720 if SVN.IsMovedInfo(info):
721 if full_move:
722 if info.get("Node Kind") == "directory":
723 # Things become tricky here. It's a directory copy/move. We need to
724 # diff all the files inside it.
725 # This will put a lot of pressure on the heap. This is why StringIO
726 # is used and converted back into a string at the end. The reason to
727 # return a string instead of a StringIO is that StringIO.write()
728 # doesn't accept a StringIO object. *sigh*.
729 for (dirpath, dirnames, filenames) in os.walk(filename):
730 # Cleanup all files starting with a '.'.
731 for d in dirnames:
732 if d.startswith('.'):
733 dirnames.remove(d)
734 for f in filenames:
735 if f.startswith('.'):
736 filenames.remove(f)
737 for f in filenames:
738 if data is None:
739 data = cStringIO.StringIO()
740 data.write(GenFakeDiff(os.path.join(dirpath, f)))
741 if data:
742 tmp = data.getvalue()
743 data.close()
744 data = tmp
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000745 else:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000746 data = GenFakeDiff(filename)
747 else:
748 if info.get("Node Kind") != "directory":
maruel@chromium.org0836c562010-01-22 01:10:06 +0000749 # svn diff on a mv/cp'd file outputs nothing if there was no change.
750 data = SVN.Capture(command, None)
751 if not data:
752 # We put in an empty Index entry so upload.py knows about them.
maruel@chromium.orgc6d170e2010-06-03 00:06:00 +0000753 data = "Index: %s\n" % filename.replace(os.sep, '/')
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000754 # Otherwise silently ignore directories.
755 else:
756 if info.get("Node Kind") != "directory":
757 # Normal simple case.
maruel@chromium.org0836c562010-01-22 01:10:06 +0000758 data = SVN.Capture(command, None)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000759 # Otherwise silently ignore directories.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000760 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000761
762 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000763 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000764 """Returns a string containing the diff for the given file list.
765
766 The files in the list should either be absolute paths or relative to the
767 given root. If no root directory is provided, the repository root will be
768 used.
769 The diff will always use relative paths.
770 """
771 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000772 root = root or SVN.GetCheckoutRoot(previous_cwd)
773 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000774 def RelativePath(path, root):
775 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000776 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000777 return path[len(root):]
778 return path
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000779 # If the user specified a custom diff command in their svn config file,
780 # then it'll be used when we do svn diff, which we don't want to happen
781 # since we want the unified diff. Using --diff-cmd=diff doesn't always
782 # work, since they can have another diff executable in their path that
783 # gives different line endings. So we use a bogus temp directory as the
784 # config directory, which gets around these problems.
785 bogus_dir = tempfile.mkdtemp()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000786 try:
787 os.chdir(root)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000788 # Cleanup filenames
789 filenames = [RelativePath(f, root) for f in filenames]
790 # Get information about the modified items (files and directories)
791 data = dict([(f, SVN.CaptureInfo(f)) for f in filenames])
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000792 diffs = []
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000793 if full_move:
794 # Eliminate modified files inside moved/copied directory.
795 for (filename, info) in data.iteritems():
796 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
797 # Remove files inside the directory.
798 filenames = [f for f in filenames
799 if not f.startswith(filename + os.path.sep)]
800 for filename in data.keys():
801 if not filename in filenames:
802 # Remove filtered out items.
803 del data[filename]
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000804 else:
805 metaheaders = []
806 for (filename, info) in data.iteritems():
807 if SVN.IsMovedInfo(info):
808 # for now, the most common case is a head copy,
809 # so let's just encode that as a straight up cp.
810 srcurl = info.get('Copied From URL')
811 root = info.get('Repository Root')
812 rev = int(info.get('Copied From Rev'))
813 assert srcurl.startswith(root)
814 src = srcurl[len(root)+1:]
815 srcinfo = SVN.CaptureInfo(srcurl)
816 if (srcinfo.get('Revision') != rev and
817 SVN.Capture(['diff', '-r', '%d:head' % rev, srcurl])):
818 metaheaders.append("#$ svn cp -r %d %s %s "
819 "### WARNING: note non-trunk copy\n" %
820 (rev, src, filename))
821 else:
822 metaheaders.append("#$ cp %s %s\n" % (src,
823 filename))
824
825 if metaheaders:
826 diffs.append("### BEGIN SVN COPY METADATA\n")
827 diffs.extend(metaheaders)
828 diffs.append("### END SVN COPY METADATA\n")
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000829 # Now ready to do the actual diff.
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000830 for filename in sorted(data.iterkeys()):
831 diffs.append(SVN._DiffItemInternal(filename, data[filename], bogus_dir,
832 full_move=full_move,
833 revision=revision))
834 # Use StringIO since it can be messy when diffing a directory move with
835 # full_move=True.
836 buf = cStringIO.StringIO()
837 for d in filter(None, diffs):
838 buf.write(d)
839 result = buf.getvalue()
840 buf.close()
841 return result
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000842 finally:
843 os.chdir(previous_cwd)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000844 shutil.rmtree(bogus_dir)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000845
846 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000847 def GetEmail(repo_root):
848 """Retrieves the svn account which we assume is an email address."""
849 infos = SVN.CaptureInfo(repo_root)
850 uuid = infos.get('UUID')
851 root = infos.get('Repository Root')
852 if not root:
853 return None
854
855 # Should check for uuid but it is incorrectly saved for https creds.
856 realm = root.rsplit('/', 1)[0]
857 if root.startswith('https') or not uuid:
858 regexp = re.compile(r'<%s:\d+>.*' % realm)
859 else:
860 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
861 if regexp is None:
862 return None
863 if sys.platform.startswith('win'):
864 if not 'APPDATA' in os.environ:
865 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000866 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
867 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000868 else:
869 if not 'HOME' in os.environ:
870 return None
871 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
872 'svn.simple')
873 for credfile in os.listdir(auth_dir):
874 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
875 if regexp.match(cred_info.get('svn:realmstring')):
876 return cred_info.get('username')
877
878 @staticmethod
879 def ReadSimpleAuth(filename):
880 f = open(filename, 'r')
881 values = {}
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000882 def ReadOneItem(item_type):
883 m = re.match(r'%s (\d+)' % item_type, f.readline())
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000884 if not m:
885 return None
886 data = f.read(int(m.group(1)))
887 if f.read(1) != '\n':
888 return None
889 return data
890
891 while True:
892 key = ReadOneItem('K')
893 if not key:
894 break
895 value = ReadOneItem('V')
896 if not value:
897 break
898 values[key] = value
899 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000900
901 @staticmethod
902 def GetCheckoutRoot(directory):
903 """Returns the top level directory of the current repository.
904
905 The directory is returned as an absolute path.
906 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000907 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000908 infos = SVN.CaptureInfo(directory, print_error=False)
909 cur_dir_repo_root = infos.get("Repository Root")
910 if not cur_dir_repo_root:
911 return None
912
913 while True:
914 parent = os.path.dirname(directory)
915 if (SVN.CaptureInfo(parent, print_error=False).get(
916 "Repository Root") != cur_dir_repo_root):
917 break
918 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000919 return GetCasedPath(directory)
tony@chromium.org57564662010-04-14 02:35:12 +0000920
921 @staticmethod
922 def AssertVersion(min_version):
923 """Asserts svn's version is at least min_version."""
924 def only_int(val):
925 if val.isdigit():
926 return int(val)
927 else:
928 return 0
929 if not SVN.current_version:
930 SVN.current_version = SVN.Capture(['--version']).split()[2]
931 current_version_list = map(only_int, SVN.current_version.split('.'))
932 for min_ver in map(int, min_version.split('.')):
933 ver = current_version_list.pop(0)
934 if ver < min_ver:
935 return (False, SVN.current_version)
936 elif ver > min_ver:
937 return (True, SVN.current_version)
938 return (True, SVN.current_version)