blob: ac3f03fce0d5e2f8d296f3cd49ed4a963866fcc6 [file] [log] [blame]
[email protected]d5800f12009-11-12 20:03:431# 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
[email protected]5aeb7dd2009-11-17 18:09:015"""SCM-specific utility classes."""
[email protected]d5800f12009-11-12 20:03:436
[email protected]3c55d982010-05-06 14:25:447import cStringIO
[email protected]fd9cbbb2010-01-08 23:04:038import glob
[email protected]d5800f12009-11-12 20:03:439import os
10import re
[email protected]f2f9d552009-12-22 00:12:5711import shutil
[email protected]d5800f12009-11-12 20:03:4312import subprocess
13import sys
[email protected]5aeb7dd2009-11-17 18:09:0114import tempfile
[email protected]fd876172010-04-30 14:01:0515import time
[email protected]d5800f12009-11-12 20:03:4316import xml.dom.minidom
17
18import gclient_utils
19
[email protected]b24a8e12009-12-22 13:45:4820def ValidateEmail(email):
21 return (re.match(r"^[a-zA-Z0-9._%-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", email)
22 is not None)
23
[email protected]d5800f12009-11-12 20:03:4324
[email protected]fd9cbbb2010-01-08 23:04:0325def 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
[email protected]3c55d982010-05-06 14:25:4445def GenFakeDiff(filename):
46 """Generates a fake diff from a file."""
47 file_content = gclient_utils.FileRead(filename, 'rb').splitlines(True)
48 nb_lines = len(file_content)
49 # We need to use / since patch on unix will fail otherwise.
50 data = cStringIO.StringIO()
51 data.write("Index: %s\n" % filename)
52 data.write('=' * 67 + '\n')
53 # Note: Should we use /dev/null instead?
54 data.write("--- %s\n" % filename)
55 data.write("+++ %s\n" % filename)
56 data.write("@@ -0,0 +1,%d @@\n" % nb_lines)
57 # Prepend '+' to every lines.
58 for line in file_content:
59 data.write('+')
60 data.write(line)
61 result = data.getvalue()
62 data.close()
63 return result
64
65
[email protected]5aeb7dd2009-11-17 18:09:0166class GIT(object):
67 COMMAND = "git"
[email protected]d5800f12009-11-12 20:03:4368
[email protected]5aeb7dd2009-11-17 18:09:0169 @staticmethod
[email protected]f2f9d552009-12-22 00:12:5770 def Capture(args, in_directory=None, print_error=True, error_ok=False):
[email protected]5aeb7dd2009-11-17 18:09:0171 """Runs git, capturing output sent to stdout as a string.
72
73 Args:
74 args: A sequence of command line parameters to be passed to git.
75 in_directory: The directory where git is to be run.
76
77 Returns:
78 The output sent to stdout as a string.
79 """
80 c = [GIT.COMMAND]
81 c.extend(args)
[email protected]f2f9d552009-12-22 00:12:5782 try:
83 return gclient_utils.CheckCall(c, in_directory, print_error)
84 except gclient_utils.CheckCallError:
85 if error_ok:
[email protected]cd968c12010-02-01 06:05:0086 return ('', '')
[email protected]f2f9d552009-12-22 00:12:5787 raise
[email protected]d5800f12009-11-12 20:03:4388
[email protected]5aeb7dd2009-11-17 18:09:0189 @staticmethod
90 def CaptureStatus(files, upstream_branch='origin'):
91 """Returns git status.
[email protected]d5800f12009-11-12 20:03:4392
[email protected]5aeb7dd2009-11-17 18:09:0193 @files can be a string (one file) or a list of files.
[email protected]d5800f12009-11-12 20:03:4394
[email protected]5aeb7dd2009-11-17 18:09:0195 Returns an array of (status, file) tuples."""
[email protected]14ec5042010-03-30 18:19:0996 command = ["diff", "--name-status", "-r", "%s..." % upstream_branch]
[email protected]5aeb7dd2009-11-17 18:09:0197 if not files:
98 pass
99 elif isinstance(files, basestring):
100 command.append(files)
101 else:
102 command.extend(files)
[email protected]d5800f12009-11-12 20:03:43103
[email protected]7be5ef22010-01-30 22:31:50104 status = GIT.Capture(command)[0].rstrip()
[email protected]5aeb7dd2009-11-17 18:09:01105 results = []
106 if status:
107 for statusline in status.split('\n'):
108 m = re.match('^(\w)\t(.+)$', statusline)
109 if not m:
110 raise Exception("status currently unsupported: %s" % statusline)
111 results.append(('%s ' % m.group(1), m.group(2)))
112 return results
[email protected]d5800f12009-11-12 20:03:43113
[email protected]c78f2462009-11-21 01:20:57114 @staticmethod
[email protected]ee4071d2009-12-22 22:25:37115 def RunAndFilterOutput(args,
116 in_directory,
117 print_messages,
118 print_stdout,
119 filter):
120 """Runs a command, optionally outputting to stdout.
121
122 stdout is passed line-by-line to the given filter function. If
123 print_stdout is true, it is also printed to sys.stdout as in Run.
124
125 Args:
126 args: A sequence of command line parameters to be passed.
[email protected]d6504212010-01-13 17:34:31127 in_directory: The directory where git is to be run.
[email protected]ee4071d2009-12-22 22:25:37128 print_messages: Whether to print status messages to stdout about
129 which commands are being run.
130 print_stdout: Whether to forward program's output to stdout.
131 filter: A function taking one argument (a string) which will be
132 passed each line (with the ending newline character removed) of
133 program's output for filtering.
134
135 Raises:
136 gclient_utils.Error: An error occurred while running the command.
137 """
138 command = [GIT.COMMAND]
139 command.extend(args)
140 gclient_utils.SubprocessCallAndFilter(command,
141 in_directory,
142 print_messages,
143 print_stdout,
144 filter=filter)
145
146 @staticmethod
[email protected]c78f2462009-11-21 01:20:57147 def GetEmail(repo_root):
148 """Retrieves the user email address if known."""
149 # We could want to look at the svn cred when it has a svn remote but it
150 # should be fine for now, users should simply configure their git settings.
[email protected]f2f9d552009-12-22 00:12:57151 return GIT.Capture(['config', 'user.email'],
[email protected]7be5ef22010-01-30 22:31:50152 repo_root, error_ok=True)[0].strip()
[email protected]f2f9d552009-12-22 00:12:57153
154 @staticmethod
155 def ShortBranchName(branch):
156 """Converts a name like 'refs/heads/foo' to just 'foo'."""
157 return branch.replace('refs/heads/', '')
158
159 @staticmethod
160 def GetBranchRef(cwd):
[email protected]b24a8e12009-12-22 13:45:48161 """Returns the full branch reference, e.g. 'refs/heads/master'."""
[email protected]7be5ef22010-01-30 22:31:50162 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd)[0].strip()
[email protected]f2f9d552009-12-22 00:12:57163
164 @staticmethod
[email protected]b24a8e12009-12-22 13:45:48165 def GetBranch(cwd):
166 """Returns the short branch name, e.g. 'master'."""
[email protected]c308a742009-12-22 18:29:33167 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
[email protected]b24a8e12009-12-22 13:45:48168
169 @staticmethod
[email protected]f2f9d552009-12-22 00:12:57170 def IsGitSvn(cwd):
171 """Returns true if this repo looks like it's using git-svn."""
172 # If you have any "svn-remote.*" config keys, we think you're using svn.
173 try:
174 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd)
175 return True
176 except gclient_utils.CheckCallError:
177 return False
178
179 @staticmethod
180 def GetSVNBranch(cwd):
181 """Returns the svn branch name if found."""
182 # Try to figure out which remote branch we're based on.
183 # Strategy:
184 # 1) find all git-svn branches and note their svn URLs.
185 # 2) iterate through our branch history and match up the URLs.
186
187 # regexp matching the git-svn line that contains the URL.
188 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
189
190 # Get the refname and svn url for all refs/remotes/*.
191 remotes = GIT.Capture(
192 ['for-each-ref', '--format=%(refname)', 'refs/remotes'],
[email protected]7be5ef22010-01-30 22:31:50193 cwd)[0].splitlines()
[email protected]f2f9d552009-12-22 00:12:57194 svn_refs = {}
195 for ref in remotes:
196 match = git_svn_re.search(
[email protected]7be5ef22010-01-30 22:31:50197 GIT.Capture(['cat-file', '-p', ref], cwd)[0])
[email protected]42d8da52010-04-23 18:25:07198 # Prefer origin/HEAD over all others.
199 if match and (match.group(1) not in svn_refs or
200 ref == "refs/remotes/origin/HEAD"):
[email protected]f2f9d552009-12-22 00:12:57201 svn_refs[match.group(1)] = ref
202
203 svn_branch = ''
204 if len(svn_refs) == 1:
205 # Only one svn branch exists -- seems like a good candidate.
206 svn_branch = svn_refs.values()[0]
207 elif len(svn_refs) > 1:
208 # We have more than one remote branch available. We don't
209 # want to go through all of history, so read a line from the
210 # pipe at a time.
211 # The -100 is an arbitrary limit so we don't search forever.
212 cmd = ['git', 'log', '-100', '--pretty=medium']
213 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd)
214 for line in proc.stdout:
215 match = git_svn_re.match(line)
216 if match:
217 url = match.group(1)
218 if url in svn_refs:
219 svn_branch = svn_refs[url]
220 proc.stdout.close() # Cut pipe.
221 break
222 return svn_branch
223
224 @staticmethod
225 def FetchUpstreamTuple(cwd):
226 """Returns a tuple containg remote and remote ref,
227 e.g. 'origin', 'refs/heads/master'
[email protected]81e012c2010-04-29 16:07:24228 Tries to be intelligent and understand git-svn.
[email protected]f2f9d552009-12-22 00:12:57229 """
230 remote = '.'
[email protected]b24a8e12009-12-22 13:45:48231 branch = GIT.GetBranch(cwd)
[email protected]f2f9d552009-12-22 00:12:57232 upstream_branch = None
233 upstream_branch = GIT.Capture(
[email protected]b65040a2010-02-01 16:29:14234 ['config', 'branch.%s.merge' % branch], in_directory=cwd,
235 error_ok=True)[0].strip()
[email protected]f2f9d552009-12-22 00:12:57236 if upstream_branch:
237 remote = GIT.Capture(
238 ['config', 'branch.%s.remote' % branch],
[email protected]b65040a2010-02-01 16:29:14239 in_directory=cwd, error_ok=True)[0].strip()
[email protected]f2f9d552009-12-22 00:12:57240 else:
241 # Fall back on trying a git-svn upstream branch.
242 if GIT.IsGitSvn(cwd):
243 upstream_branch = GIT.GetSVNBranch(cwd)
[email protected]81e012c2010-04-29 16:07:24244 else:
[email protected]a630bd72010-04-29 23:32:34245 # Else, try to guess the origin remote.
246 remote_branches = GIT.Capture(
247 ['branch', '-r'], in_directory=cwd)[0].split()
248 if 'origin/master' in remote_branches:
249 # Fall back on origin/master if it exits.
250 remote = 'origin'
251 upstream_branch = 'refs/heads/master'
252 elif 'origin/trunk' in remote_branches:
253 # Fall back on origin/trunk if it exists. Generally a shared
254 # git-svn clone
255 remote = 'origin'
256 upstream_branch = 'refs/heads/trunk'
257 else:
258 # Give up.
259 remote = None
260 upstream_branch = None
[email protected]f2f9d552009-12-22 00:12:57261 return remote, upstream_branch
262
263 @staticmethod
[email protected]81e012c2010-04-29 16:07:24264 def GetUpstreamBranch(cwd):
[email protected]f2f9d552009-12-22 00:12:57265 """Gets the current branch's upstream branch."""
266 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
[email protected]a630bd72010-04-29 23:32:34267 if remote != '.' and upstream_branch:
[email protected]f2f9d552009-12-22 00:12:57268 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
269 return upstream_branch
270
271 @staticmethod
[email protected]8ede00e2010-01-12 14:35:28272 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
273 files=None):
[email protected]a9371762009-12-22 18:27:38274 """Diffs against the upstream branch or optionally another branch.
275
276 full_move means that move or copy operations should completely recreate the
277 files, usually in the prospect to apply the patch for a try job."""
[email protected]f2f9d552009-12-22 00:12:57278 if not branch:
[email protected]81e012c2010-04-29 16:07:24279 branch = GIT.GetUpstreamBranch(cwd)
[email protected]838f0f22010-04-09 17:02:50280 command = ['diff', '-p', '--no-prefix', branch + "..." + branch_head]
[email protected]a9371762009-12-22 18:27:38281 if not full_move:
282 command.append('-C')
[email protected]8ede00e2010-01-12 14:35:28283 # TODO(maruel): --binary support.
284 if files:
285 command.append('--')
286 command.extend(files)
[email protected]7be5ef22010-01-30 22:31:50287 diff = GIT.Capture(command, cwd)[0].splitlines(True)
[email protected]f2f9d552009-12-22 00:12:57288 for i in range(len(diff)):
289 # In the case of added files, replace /dev/null with the path to the
290 # file being added.
291 if diff[i].startswith('--- /dev/null'):
292 diff[i] = '--- %s' % diff[i+1][4:]
293 return ''.join(diff)
[email protected]c78f2462009-11-21 01:20:57294
[email protected]b24a8e12009-12-22 13:45:48295 @staticmethod
[email protected]8ede00e2010-01-12 14:35:28296 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
297 """Returns the list of modified files between two branches."""
298 if not branch:
[email protected]81e012c2010-04-29 16:07:24299 branch = GIT.GetUpstreamBranch(cwd)
[email protected]838f0f22010-04-09 17:02:50300 command = ['diff', '--name-only', branch + "..." + branch_head]
[email protected]7be5ef22010-01-30 22:31:50301 return GIT.Capture(command, cwd)[0].splitlines(False)
[email protected]8ede00e2010-01-12 14:35:28302
303 @staticmethod
[email protected]b24a8e12009-12-22 13:45:48304 def GetPatchName(cwd):
305 """Constructs a name for this patch."""
[email protected]7be5ef22010-01-30 22:31:50306 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd)[0].strip()
[email protected]b24a8e12009-12-22 13:45:48307 return "%s-%s" % (GIT.GetBranch(cwd), short_sha)
308
309 @staticmethod
[email protected]01d8c1d2010-01-07 01:56:59310 def GetCheckoutRoot(path):
311 """Returns the top level directory of a git checkout as an absolute path.
[email protected]b24a8e12009-12-22 13:45:48312 """
[email protected]7be5ef22010-01-30 22:31:50313 root = GIT.Capture(['rev-parse', '--show-cdup'], path)[0].strip()
[email protected]01d8c1d2010-01-07 01:56:59314 return os.path.abspath(os.path.join(path, root))
[email protected]b24a8e12009-12-22 13:45:48315
[email protected]d0f854a2010-03-11 19:35:53316 @staticmethod
317 def AssertVersion(min_version):
318 """Asserts git's version is at least min_version."""
319 def only_int(val):
320 if val.isdigit():
321 return int(val)
322 else:
323 return 0
324 current_version = GIT.Capture(['--version'])[0].split()[-1]
325 current_version_list = map(only_int, current_version.split('.'))
326 for min_ver in map(int, min_version.split('.')):
327 ver = current_version_list.pop(0)
328 if ver < min_ver:
329 return (False, current_version)
330 elif ver > min_ver:
331 return (True, current_version)
332 return (True, current_version)
333
[email protected]d5800f12009-11-12 20:03:43334
[email protected]5aeb7dd2009-11-17 18:09:01335class SVN(object):
336 COMMAND = "svn"
[email protected]57564662010-04-14 02:35:12337 current_version = None
[email protected]d5800f12009-11-12 20:03:43338
[email protected]5aeb7dd2009-11-17 18:09:01339 @staticmethod
340 def Run(args, in_directory):
341 """Runs svn, sending output to stdout.
[email protected]d5800f12009-11-12 20:03:43342
[email protected]5aeb7dd2009-11-17 18:09:01343 Args:
344 args: A sequence of command line parameters to be passed to svn.
345 in_directory: The directory where svn is to be run.
[email protected]d5800f12009-11-12 20:03:43346
[email protected]5aeb7dd2009-11-17 18:09:01347 Raises:
348 Error: An error occurred while running the svn command.
349 """
350 c = [SVN.COMMAND]
351 c.extend(args)
[email protected]2185f002009-12-18 21:03:47352 # TODO(maruel): This is very gclient-specific.
[email protected]5aeb7dd2009-11-17 18:09:01353 gclient_utils.SubprocessCall(c, in_directory)
[email protected]d5800f12009-11-12 20:03:43354
[email protected]5aeb7dd2009-11-17 18:09:01355 @staticmethod
356 def Capture(args, in_directory=None, print_error=True):
357 """Runs svn, capturing output sent to stdout as a string.
[email protected]d5800f12009-11-12 20:03:43358
[email protected]5aeb7dd2009-11-17 18:09:01359 Args:
360 args: A sequence of command line parameters to be passed to svn.
361 in_directory: The directory where svn is to be run.
[email protected]d5800f12009-11-12 20:03:43362
[email protected]5aeb7dd2009-11-17 18:09:01363 Returns:
364 The output sent to stdout as a string.
365 """
366 c = [SVN.COMMAND]
367 c.extend(args)
[email protected]d5800f12009-11-12 20:03:43368
[email protected]5aeb7dd2009-11-17 18:09:01369 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
370 # the svn.exe executable, but shell=True makes subprocess on Linux fail
371 # when it's called with a list because it only tries to execute the
372 # first string ("svn").
373 stderr = None
374 if not print_error:
375 stderr = subprocess.PIPE
376 return subprocess.Popen(c,
377 cwd=in_directory,
378 shell=(sys.platform == 'win32'),
379 stdout=subprocess.PIPE,
380 stderr=stderr).communicate()[0]
[email protected]d5800f12009-11-12 20:03:43381
[email protected]5aeb7dd2009-11-17 18:09:01382 @staticmethod
383 def RunAndGetFileList(options, args, in_directory, file_list):
384 """Runs svn checkout, update, or status, output to stdout.
[email protected]d5800f12009-11-12 20:03:43385
[email protected]5aeb7dd2009-11-17 18:09:01386 The first item in args must be either "checkout", "update", or "status".
[email protected]d5800f12009-11-12 20:03:43387
[email protected]5aeb7dd2009-11-17 18:09:01388 svn's stdout is parsed to collect a list of files checked out or updated.
389 These files are appended to file_list. svn's stdout is also printed to
390 sys.stdout as in Run.
[email protected]d5800f12009-11-12 20:03:43391
[email protected]5aeb7dd2009-11-17 18:09:01392 Args:
393 options: command line options to gclient
394 args: A sequence of command line parameters to be passed to svn.
395 in_directory: The directory where svn is to be run.
[email protected]d5800f12009-11-12 20:03:43396
[email protected]5aeb7dd2009-11-17 18:09:01397 Raises:
398 Error: An error occurred while running the svn command.
399 """
400 command = [SVN.COMMAND]
401 command.extend(args)
[email protected]d5800f12009-11-12 20:03:43402
[email protected]5aeb7dd2009-11-17 18:09:01403 # svn update and svn checkout use the same pattern: the first three columns
404 # are for file status, property status, and lock status. This is followed
405 # by two spaces, and then the path to the file.
406 update_pattern = '^... (.*)$'
[email protected]d5800f12009-11-12 20:03:43407
[email protected]5aeb7dd2009-11-17 18:09:01408 # The first three columns of svn status are the same as for svn update and
409 # svn checkout. The next three columns indicate addition-with-history,
410 # switch, and remote lock status. This is followed by one space, and then
411 # the path to the file.
412 status_pattern = '^...... (.*)$'
[email protected]d5800f12009-11-12 20:03:43413
[email protected]5aeb7dd2009-11-17 18:09:01414 # args[0] must be a supported command. This will blow up if it's something
415 # else, which is good. Note that the patterns are only effective when
416 # these commands are used in their ordinary forms, the patterns are invalid
417 # for "svn status --show-updates", for example.
418 pattern = {
419 'checkout': update_pattern,
420 'status': status_pattern,
421 'update': update_pattern,
422 }[args[0]]
[email protected]5aeb7dd2009-11-17 18:09:01423 compiled_pattern = re.compile(pattern)
[email protected]b71b67e2009-11-24 20:48:19424 # Place an upper limit.
[email protected]fd876172010-04-30 14:01:05425 for _ in range(10):
[email protected]b71b67e2009-11-24 20:48:19426 previous_list_len = len(file_list)
427 failure = []
[email protected]54d1f1a2010-01-08 19:53:47428
[email protected]b71b67e2009-11-24 20:48:19429 def CaptureMatchingLines(line):
430 match = compiled_pattern.search(line)
431 if match:
432 file_list.append(match.group(1))
433 if line.startswith('svn: '):
[email protected]8599aa72010-02-08 20:27:14434 failure.append(line)
[email protected]54d1f1a2010-01-08 19:53:47435
[email protected]b71b67e2009-11-24 20:48:19436 try:
437 SVN.RunAndFilterOutput(args,
438 in_directory,
439 options.verbose,
440 True,
441 CaptureMatchingLines)
442 except gclient_utils.Error:
[email protected]2de10252010-02-08 01:10:39443 # We enforce that some progress has been made or HTTP 502.
[email protected]55e724e2010-03-11 19:36:49444 if (filter(lambda x: '502 Bad Gateway' in x, failure) or
[email protected]2de10252010-02-08 01:10:39445 (len(failure) and len(file_list) > previous_list_len)):
[email protected]b71b67e2009-11-24 20:48:19446 if args[0] == 'checkout':
[email protected]b71b67e2009-11-24 20:48:19447 # An aborted checkout is now an update.
[email protected]2de10252010-02-08 01:10:39448 args = ['update'] + args[1:]
[email protected]fd876172010-04-30 14:01:05449 print "Sleeping 15 seconds and retrying...."
450 time.sleep(15)
[email protected]b71b67e2009-11-24 20:48:19451 continue
[email protected]2de10252010-02-08 01:10:39452 # No progress was made or an unknown error we aren't sure, bail out.
[email protected]54d1f1a2010-01-08 19:53:47453 raise
[email protected]b71b67e2009-11-24 20:48:19454 break
[email protected]d5800f12009-11-12 20:03:43455
[email protected]5aeb7dd2009-11-17 18:09:01456 @staticmethod
457 def RunAndFilterOutput(args,
458 in_directory,
459 print_messages,
460 print_stdout,
461 filter):
[email protected]ee4071d2009-12-22 22:25:37462 """Runs a command, optionally outputting to stdout.
[email protected]d5800f12009-11-12 20:03:43463
[email protected]ee4071d2009-12-22 22:25:37464 stdout is passed line-by-line to the given filter function. If
[email protected]5aeb7dd2009-11-17 18:09:01465 print_stdout is true, it is also printed to sys.stdout as in Run.
[email protected]d5800f12009-11-12 20:03:43466
[email protected]5aeb7dd2009-11-17 18:09:01467 Args:
[email protected]ee4071d2009-12-22 22:25:37468 args: A sequence of command line parameters to be passed.
[email protected]5aeb7dd2009-11-17 18:09:01469 in_directory: The directory where svn is to be run.
470 print_messages: Whether to print status messages to stdout about
[email protected]ee4071d2009-12-22 22:25:37471 which commands are being run.
472 print_stdout: Whether to forward program's output to stdout.
[email protected]5aeb7dd2009-11-17 18:09:01473 filter: A function taking one argument (a string) which will be
474 passed each line (with the ending newline character removed) of
[email protected]ee4071d2009-12-22 22:25:37475 program's output for filtering.
[email protected]d5800f12009-11-12 20:03:43476
[email protected]5aeb7dd2009-11-17 18:09:01477 Raises:
[email protected]ee4071d2009-12-22 22:25:37478 gclient_utils.Error: An error occurred while running the command.
[email protected]5aeb7dd2009-11-17 18:09:01479 """
480 command = [SVN.COMMAND]
481 command.extend(args)
[email protected]5aeb7dd2009-11-17 18:09:01482 gclient_utils.SubprocessCallAndFilter(command,
483 in_directory,
484 print_messages,
485 print_stdout,
486 filter=filter)
[email protected]d5800f12009-11-12 20:03:43487
[email protected]5aeb7dd2009-11-17 18:09:01488 @staticmethod
489 def CaptureInfo(relpath, in_directory=None, print_error=True):
490 """Returns a dictionary from the svn info output for the given file.
[email protected]d5800f12009-11-12 20:03:43491
[email protected]5aeb7dd2009-11-17 18:09:01492 Args:
493 relpath: The directory where the working copy resides relative to
494 the directory given by in_directory.
495 in_directory: The directory where svn is to be run.
496 """
497 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
498 dom = gclient_utils.ParseXML(output)
499 result = {}
500 if dom:
501 GetNamedNodeText = gclient_utils.GetNamedNodeText
502 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
503 def C(item, f):
504 if item is not None: return f(item)
505 # /info/entry/
506 # url
507 # reposityory/(root|uuid)
508 # wc-info/(schedule|depth)
509 # commit/(author|date)
510 # str() the results because they may be returned as Unicode, which
511 # interferes with the higher layers matching up things in the deps
512 # dictionary.
513 # TODO(maruel): Fix at higher level instead (!)
514 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
515 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
516 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
517 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
518 'revision'),
519 int)
520 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
521 str)
522 # Differs across versions.
523 if result['Node Kind'] == 'dir':
524 result['Node Kind'] = 'directory'
525 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
526 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
527 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
528 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
529 return result
[email protected]d5800f12009-11-12 20:03:43530
[email protected]5aeb7dd2009-11-17 18:09:01531 @staticmethod
532 def CaptureHeadRevision(url):
533 """Get the head revision of a SVN repository.
[email protected]d5800f12009-11-12 20:03:43534
[email protected]5aeb7dd2009-11-17 18:09:01535 Returns:
536 Int head revision
537 """
538 info = SVN.Capture(["info", "--xml", url], os.getcwd())
539 dom = xml.dom.minidom.parseString(info)
540 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
[email protected]d5800f12009-11-12 20:03:43541
[email protected]5aeb7dd2009-11-17 18:09:01542 @staticmethod
[email protected]5d63eb82010-03-24 23:22:09543 def CaptureBaseRevision(cwd):
544 """Get the base revision of a SVN repository.
545
546 Returns:
547 Int base revision
548 """
549 info = SVN.Capture(["info", "--xml"], cwd)
550 dom = xml.dom.minidom.parseString(info)
551 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
552
553 @staticmethod
[email protected]5aeb7dd2009-11-17 18:09:01554 def CaptureStatus(files):
555 """Returns the svn 1.5 svn status emulated output.
[email protected]d5800f12009-11-12 20:03:43556
[email protected]5aeb7dd2009-11-17 18:09:01557 @files can be a string (one file) or a list of files.
[email protected]d5800f12009-11-12 20:03:43558
[email protected]5aeb7dd2009-11-17 18:09:01559 Returns an array of (status, file) tuples."""
560 command = ["status", "--xml"]
561 if not files:
562 pass
563 elif isinstance(files, basestring):
564 command.append(files)
565 else:
566 command.extend(files)
[email protected]d5800f12009-11-12 20:03:43567
[email protected]5aeb7dd2009-11-17 18:09:01568 status_letter = {
569 None: ' ',
570 '': ' ',
571 'added': 'A',
572 'conflicted': 'C',
573 'deleted': 'D',
574 'external': 'X',
575 'ignored': 'I',
576 'incomplete': '!',
577 'merged': 'G',
578 'missing': '!',
579 'modified': 'M',
580 'none': ' ',
581 'normal': ' ',
582 'obstructed': '~',
583 'replaced': 'R',
584 'unversioned': '?',
585 }
586 dom = gclient_utils.ParseXML(SVN.Capture(command))
587 results = []
588 if dom:
589 # /status/target/entry/(wc-status|commit|author|date)
590 for target in dom.getElementsByTagName('target'):
591 #base_path = target.getAttribute('path')
592 for entry in target.getElementsByTagName('entry'):
593 file_path = entry.getAttribute('path')
594 wc_status = entry.getElementsByTagName('wc-status')
595 assert len(wc_status) == 1
596 # Emulate svn 1.5 status ouput...
597 statuses = [' '] * 7
598 # Col 0
599 xml_item_status = wc_status[0].getAttribute('item')
600 if xml_item_status in status_letter:
601 statuses[0] = status_letter[xml_item_status]
602 else:
603 raise Exception('Unknown item status "%s"; please implement me!' %
604 xml_item_status)
605 # Col 1
606 xml_props_status = wc_status[0].getAttribute('props')
607 if xml_props_status == 'modified':
608 statuses[1] = 'M'
609 elif xml_props_status == 'conflicted':
610 statuses[1] = 'C'
611 elif (not xml_props_status or xml_props_status == 'none' or
612 xml_props_status == 'normal'):
613 pass
614 else:
615 raise Exception('Unknown props status "%s"; please implement me!' %
616 xml_props_status)
617 # Col 2
618 if wc_status[0].getAttribute('wc-locked') == 'true':
619 statuses[2] = 'L'
620 # Col 3
621 if wc_status[0].getAttribute('copied') == 'true':
622 statuses[3] = '+'
623 # Col 4
624 if wc_status[0].getAttribute('switched') == 'true':
625 statuses[4] = 'S'
626 # TODO(maruel): Col 5 and 6
627 item = (''.join(statuses), file_path)
628 results.append(item)
629 return results
[email protected]d5800f12009-11-12 20:03:43630
[email protected]5aeb7dd2009-11-17 18:09:01631 @staticmethod
632 def IsMoved(filename):
633 """Determine if a file has been added through svn mv"""
[email protected]3c55d982010-05-06 14:25:44634 return SVN.IsMovedInfo(SVN.CaptureInfo(filename))
635
636 @staticmethod
637 def IsMovedInfo(info):
638 """Determine if a file has been added through svn mv"""
[email protected]5aeb7dd2009-11-17 18:09:01639 return (info.get('Copied From URL') and
640 info.get('Copied From Rev') and
641 info.get('Schedule') == 'add')
[email protected]d5800f12009-11-12 20:03:43642
[email protected]5aeb7dd2009-11-17 18:09:01643 @staticmethod
644 def GetFileProperty(file, property_name):
645 """Returns the value of an SVN property for the given file.
[email protected]d5800f12009-11-12 20:03:43646
[email protected]5aeb7dd2009-11-17 18:09:01647 Args:
648 file: The file to check
649 property_name: The name of the SVN property, e.g. "svn:mime-type"
[email protected]d5800f12009-11-12 20:03:43650
[email protected]5aeb7dd2009-11-17 18:09:01651 Returns:
652 The value of the property, which will be the empty string if the property
653 is not set on the file. If the file is not under version control, the
654 empty string is also returned.
655 """
656 output = SVN.Capture(["propget", property_name, file])
657 if (output.startswith("svn: ") and
658 output.endswith("is not under version control")):
659 return ""
660 else:
661 return output
[email protected]d5800f12009-11-12 20:03:43662
[email protected]5aeb7dd2009-11-17 18:09:01663 @staticmethod
[email protected]1c7db8e2010-01-07 02:00:19664 def DiffItem(filename, full_move=False, revision=None):
[email protected]f2f9d552009-12-22 00:12:57665 """Diffs a single file.
666
[email protected]3c55d982010-05-06 14:25:44667 Should be simple, eh? No it isn't.
[email protected]f2f9d552009-12-22 00:12:57668 Be sure to be in the appropriate directory before calling to have the
[email protected]a9371762009-12-22 18:27:38669 expected relative path.
670 full_move means that move or copy operations should completely recreate the
671 files, usually in the prospect to apply the patch for a try job."""
[email protected]5aeb7dd2009-11-17 18:09:01672 # If the user specified a custom diff command in their svn config file,
673 # then it'll be used when we do svn diff, which we don't want to happen
674 # since we want the unified diff. Using --diff-cmd=diff doesn't always
675 # work, since they can have another diff executable in their path that
676 # gives different line endings. So we use a bogus temp directory as the
677 # config directory, which gets around these problems.
[email protected]f2f9d552009-12-22 00:12:57678 bogus_dir = tempfile.mkdtemp()
679 try:
[email protected]3c55d982010-05-06 14:25:44680 # Use "svn info" output instead of os.path.isdir because the latter fails
681 # when the file is deleted.
682 return SVN._DiffItemInternal(SVN.CaptureInfo(filename),
683 full_move=full_move, revision=revision)
684 finally:
685 shutil.rmtree(bogus_dir)
686
687 @staticmethod
688 def _DiffItemInternal(filename, info, bogus_dir, full_move=False,
689 revision=None):
690 """Grabs the diff data."""
691 command = ["diff", "--config-dir", bogus_dir, filename]
692 if revision:
693 command.extend(['--revision', revision])
694 data = None
695 if SVN.IsMovedInfo(info):
696 if full_move:
697 if info.get("Node Kind") == "directory":
698 # Things become tricky here. It's a directory copy/move. We need to
699 # diff all the files inside it.
700 # This will put a lot of pressure on the heap. This is why StringIO
701 # is used and converted back into a string at the end. The reason to
702 # return a string instead of a StringIO is that StringIO.write()
703 # doesn't accept a StringIO object. *sigh*.
704 for (dirpath, dirnames, filenames) in os.walk(filename):
705 # Cleanup all files starting with a '.'.
706 for d in dirnames:
707 if d.startswith('.'):
708 dirnames.remove(d)
709 for f in filenames:
710 if f.startswith('.'):
711 filenames.remove(f)
712 for f in filenames:
713 if data is None:
714 data = cStringIO.StringIO()
715 data.write(GenFakeDiff(os.path.join(dirpath, f)))
716 if data:
717 tmp = data.getvalue()
718 data.close()
719 data = tmp
[email protected]f2f9d552009-12-22 00:12:57720 else:
[email protected]3c55d982010-05-06 14:25:44721 data = GenFakeDiff(filename)
722 else:
723 if info.get("Node Kind") != "directory":
[email protected]0836c562010-01-22 01:10:06724 # svn diff on a mv/cp'd file outputs nothing if there was no change.
725 data = SVN.Capture(command, None)
726 if not data:
727 # We put in an empty Index entry so upload.py knows about them.
728 data = "Index: %s\n" % filename
[email protected]3c55d982010-05-06 14:25:44729 # Otherwise silently ignore directories.
730 else:
731 if info.get("Node Kind") != "directory":
732 # Normal simple case.
[email protected]0836c562010-01-22 01:10:06733 data = SVN.Capture(command, None)
[email protected]3c55d982010-05-06 14:25:44734 # Otherwise silently ignore directories.
[email protected]5aeb7dd2009-11-17 18:09:01735 return data
[email protected]c78f2462009-11-21 01:20:57736
737 @staticmethod
[email protected]1c7db8e2010-01-07 02:00:19738 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
[email protected]f2f9d552009-12-22 00:12:57739 """Returns a string containing the diff for the given file list.
740
741 The files in the list should either be absolute paths or relative to the
742 given root. If no root directory is provided, the repository root will be
743 used.
744 The diff will always use relative paths.
745 """
746 previous_cwd = os.getcwd()
[email protected]fd9cbbb2010-01-08 23:04:03747 root = root or SVN.GetCheckoutRoot(previous_cwd)
748 root = os.path.normcase(os.path.join(root, ''))
[email protected]f2f9d552009-12-22 00:12:57749 def RelativePath(path, root):
750 """We must use relative paths."""
[email protected]fd9cbbb2010-01-08 23:04:03751 if os.path.normcase(path).startswith(root):
[email protected]f2f9d552009-12-22 00:12:57752 return path[len(root):]
753 return path
[email protected]3c55d982010-05-06 14:25:44754 # If the user specified a custom diff command in their svn config file,
755 # then it'll be used when we do svn diff, which we don't want to happen
756 # since we want the unified diff. Using --diff-cmd=diff doesn't always
757 # work, since they can have another diff executable in their path that
758 # gives different line endings. So we use a bogus temp directory as the
759 # config directory, which gets around these problems.
760 bogus_dir = tempfile.mkdtemp()
[email protected]f2f9d552009-12-22 00:12:57761 try:
762 os.chdir(root)
[email protected]3c55d982010-05-06 14:25:44763 # Cleanup filenames
764 filenames = [RelativePath(f, root) for f in filenames]
765 # Get information about the modified items (files and directories)
766 data = dict([(f, SVN.CaptureInfo(f)) for f in filenames])
767 if full_move:
768 # Eliminate modified files inside moved/copied directory.
769 for (filename, info) in data.iteritems():
770 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
771 # Remove files inside the directory.
772 filenames = [f for f in filenames
773 if not f.startswith(filename + os.path.sep)]
774 for filename in data.keys():
775 if not filename in filenames:
776 # Remove filtered out items.
777 del data[filename]
778 # Now ready to do the actual diff.
779 diffs = []
780 for filename in sorted(data.iterkeys()):
781 diffs.append(SVN._DiffItemInternal(filename, data[filename], bogus_dir,
782 full_move=full_move,
783 revision=revision))
784 # Use StringIO since it can be messy when diffing a directory move with
785 # full_move=True.
786 buf = cStringIO.StringIO()
787 for d in filter(None, diffs):
788 buf.write(d)
789 result = buf.getvalue()
790 buf.close()
791 return result
[email protected]f2f9d552009-12-22 00:12:57792 finally:
793 os.chdir(previous_cwd)
[email protected]3c55d982010-05-06 14:25:44794 shutil.rmtree(bogus_dir)
[email protected]f2f9d552009-12-22 00:12:57795
796 @staticmethod
[email protected]c78f2462009-11-21 01:20:57797 def GetEmail(repo_root):
798 """Retrieves the svn account which we assume is an email address."""
799 infos = SVN.CaptureInfo(repo_root)
800 uuid = infos.get('UUID')
801 root = infos.get('Repository Root')
802 if not root:
803 return None
804
805 # Should check for uuid but it is incorrectly saved for https creds.
806 realm = root.rsplit('/', 1)[0]
807 if root.startswith('https') or not uuid:
808 regexp = re.compile(r'<%s:\d+>.*' % realm)
809 else:
810 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
811 if regexp is None:
812 return None
813 if sys.platform.startswith('win'):
814 if not 'APPDATA' in os.environ:
815 return None
[email protected]720d9f32009-11-21 17:38:57816 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
817 'svn.simple')
[email protected]c78f2462009-11-21 01:20:57818 else:
819 if not 'HOME' in os.environ:
820 return None
821 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
822 'svn.simple')
823 for credfile in os.listdir(auth_dir):
824 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
825 if regexp.match(cred_info.get('svn:realmstring')):
826 return cred_info.get('username')
827
828 @staticmethod
829 def ReadSimpleAuth(filename):
830 f = open(filename, 'r')
831 values = {}
832 def ReadOneItem(type):
833 m = re.match(r'%s (\d+)' % type, f.readline())
834 if not m:
835 return None
836 data = f.read(int(m.group(1)))
837 if f.read(1) != '\n':
838 return None
839 return data
840
841 while True:
842 key = ReadOneItem('K')
843 if not key:
844 break
845 value = ReadOneItem('V')
846 if not value:
847 break
848 values[key] = value
849 return values
[email protected]94b1ee92009-12-19 20:27:20850
851 @staticmethod
852 def GetCheckoutRoot(directory):
853 """Returns the top level directory of the current repository.
854
855 The directory is returned as an absolute path.
856 """
[email protected]f7ae6d52009-12-22 20:49:04857 directory = os.path.abspath(directory)
[email protected]94b1ee92009-12-19 20:27:20858 infos = SVN.CaptureInfo(directory, print_error=False)
859 cur_dir_repo_root = infos.get("Repository Root")
860 if not cur_dir_repo_root:
861 return None
862
863 while True:
864 parent = os.path.dirname(directory)
865 if (SVN.CaptureInfo(parent, print_error=False).get(
866 "Repository Root") != cur_dir_repo_root):
867 break
868 directory = parent
[email protected]fd9cbbb2010-01-08 23:04:03869 return GetCasedPath(directory)
[email protected]57564662010-04-14 02:35:12870
871 @staticmethod
872 def AssertVersion(min_version):
873 """Asserts svn's version is at least min_version."""
874 def only_int(val):
875 if val.isdigit():
876 return int(val)
877 else:
878 return 0
879 if not SVN.current_version:
880 SVN.current_version = SVN.Capture(['--version']).split()[2]
881 current_version_list = map(only_int, SVN.current_version.split('.'))
882 for min_ver in map(int, min_version.split('.')):
883 ver = current_version_list.pop(0)
884 if ver < min_ver:
885 return (False, SVN.current_version)
886 elif ver > min_ver:
887 return (True, SVN.current_version)
888 return (True, SVN.current_version)