blob: ac674d695a88e50e53e26730e37d6622ad71d4f3 [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):
[email protected]6e29d572010-06-04 17:32:2021 return (re.match(r"^[a-zA-Z0-9._%-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", email)
22 is not None)
[email protected]b24a8e12009-12-22 13:45:4823
[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)
[email protected]c6d170e2010-06-03 00:06:0048 filename = filename.replace(os.sep, '/')
[email protected]3c55d982010-05-06 14:25:4449 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
[email protected]5aeb7dd2009-11-17 18:09:0167class GIT(object):
[email protected]5aeb7dd2009-11-17 18:09:0168 @staticmethod
[email protected]ad80e3b2010-09-09 14:18:2869 def Capture(args, **kwargs):
70 return gclient_utils.CheckCall(['git'] + args, print_error=False,
71 **kwargs)[0]
[email protected]d5800f12009-11-12 20:03:4372
[email protected]5aeb7dd2009-11-17 18:09:0173 @staticmethod
[email protected]786fb682010-06-02 15:16:2374 def CaptureStatus(files, upstream_branch=None):
[email protected]5aeb7dd2009-11-17 18:09:0175 """Returns git status.
[email protected]d5800f12009-11-12 20:03:4376
[email protected]5aeb7dd2009-11-17 18:09:0177 @files can be a string (one file) or a list of files.
[email protected]d5800f12009-11-12 20:03:4378
[email protected]5aeb7dd2009-11-17 18:09:0179 Returns an array of (status, file) tuples."""
[email protected]786fb682010-06-02 15:16:2380 if upstream_branch is None:
81 upstream_branch = GIT.GetUpstreamBranch(os.getcwd())
82 if upstream_branch is None:
[email protected]ad80e3b2010-09-09 14:18:2883 raise gclient_utils.Error('Cannot determine upstream branch')
84 command = ['diff', '--name-status', '-r', '%s...' % upstream_branch]
[email protected]5aeb7dd2009-11-17 18:09:0185 if not files:
86 pass
87 elif isinstance(files, basestring):
88 command.append(files)
89 else:
90 command.extend(files)
[email protected]ad80e3b2010-09-09 14:18:2891 status = GIT.Capture(command).rstrip()
[email protected]5aeb7dd2009-11-17 18:09:0192 results = []
93 if status:
[email protected]ad80e3b2010-09-09 14:18:2894 for statusline in status.splitlines():
[email protected]cc1614b2010-09-20 17:13:1795 # 3-way merges can cause the status can be 'MMM' instead of 'M'. This
96 # can happen when the user has 2 local branches and he diffs between
97 # these 2 branches instead diffing to upstream.
98 m = re.match('^(\w)+\t(.+)$', statusline)
[email protected]5aeb7dd2009-11-17 18:09:0199 if not m:
[email protected]ad80e3b2010-09-09 14:18:28100 raise gclient_utils.Error(
101 'status currently unsupported: %s' % statusline)
[email protected]cc1614b2010-09-20 17:13:17102 # Only grab the first letter.
103 results.append(('%s ' % m.group(1)[0], m.group(2)))
[email protected]5aeb7dd2009-11-17 18:09:01104 return results
[email protected]d5800f12009-11-12 20:03:43105
[email protected]c78f2462009-11-21 01:20:57106 @staticmethod
[email protected]ad80e3b2010-09-09 14:18:28107 def GetEmail(cwd):
[email protected]c78f2462009-11-21 01:20:57108 """Retrieves the user email address if known."""
109 # We could want to look at the svn cred when it has a svn remote but it
110 # should be fine for now, users should simply configure their git settings.
[email protected]ad80e3b2010-09-09 14:18:28111 try:
112 return GIT.Capture(['config', 'user.email'], cwd=cwd).strip()
113 except gclient_utils.CheckCallError:
114 return ''
[email protected]f2f9d552009-12-22 00:12:57115
116 @staticmethod
117 def ShortBranchName(branch):
118 """Converts a name like 'refs/heads/foo' to just 'foo'."""
119 return branch.replace('refs/heads/', '')
120
121 @staticmethod
122 def GetBranchRef(cwd):
[email protected]b24a8e12009-12-22 13:45:48123 """Returns the full branch reference, e.g. 'refs/heads/master'."""
[email protected]ad80e3b2010-09-09 14:18:28124 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd=cwd).strip()
[email protected]f2f9d552009-12-22 00:12:57125
126 @staticmethod
[email protected]b24a8e12009-12-22 13:45:48127 def GetBranch(cwd):
128 """Returns the short branch name, e.g. 'master'."""
[email protected]c308a742009-12-22 18:29:33129 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
[email protected]b24a8e12009-12-22 13:45:48130
131 @staticmethod
[email protected]f2f9d552009-12-22 00:12:57132 def IsGitSvn(cwd):
133 """Returns true if this repo looks like it's using git-svn."""
134 # If you have any "svn-remote.*" config keys, we think you're using svn.
135 try:
[email protected]ad80e3b2010-09-09 14:18:28136 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd=cwd)
[email protected]f2f9d552009-12-22 00:12:57137 return True
138 except gclient_utils.CheckCallError:
139 return False
140
141 @staticmethod
142 def GetSVNBranch(cwd):
143 """Returns the svn branch name if found."""
144 # Try to figure out which remote branch we're based on.
145 # Strategy:
146 # 1) find all git-svn branches and note their svn URLs.
147 # 2) iterate through our branch history and match up the URLs.
148
149 # regexp matching the git-svn line that contains the URL.
150 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
151
152 # Get the refname and svn url for all refs/remotes/*.
153 remotes = GIT.Capture(
154 ['for-each-ref', '--format=%(refname)', 'refs/remotes'],
[email protected]ad80e3b2010-09-09 14:18:28155 cwd=cwd).splitlines()
[email protected]f2f9d552009-12-22 00:12:57156 svn_refs = {}
157 for ref in remotes:
158 match = git_svn_re.search(
[email protected]ad80e3b2010-09-09 14:18:28159 GIT.Capture(['cat-file', '-p', ref], cwd=cwd))
[email protected]42d8da52010-04-23 18:25:07160 # Prefer origin/HEAD over all others.
161 if match and (match.group(1) not in svn_refs or
162 ref == "refs/remotes/origin/HEAD"):
[email protected]f2f9d552009-12-22 00:12:57163 svn_refs[match.group(1)] = ref
164
165 svn_branch = ''
166 if len(svn_refs) == 1:
167 # Only one svn branch exists -- seems like a good candidate.
168 svn_branch = svn_refs.values()[0]
169 elif len(svn_refs) > 1:
170 # We have more than one remote branch available. We don't
171 # want to go through all of history, so read a line from the
172 # pipe at a time.
173 # The -100 is an arbitrary limit so we don't search forever.
174 cmd = ['git', 'log', '-100', '--pretty=medium']
[email protected]3a292682010-08-23 18:54:55175 proc = gclient_utils.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd)
[email protected]f2f9d552009-12-22 00:12:57176 for line in proc.stdout:
177 match = git_svn_re.match(line)
178 if match:
179 url = match.group(1)
180 if url in svn_refs:
181 svn_branch = svn_refs[url]
182 proc.stdout.close() # Cut pipe.
183 break
184 return svn_branch
185
186 @staticmethod
187 def FetchUpstreamTuple(cwd):
188 """Returns a tuple containg remote and remote ref,
189 e.g. 'origin', 'refs/heads/master'
[email protected]81e012c2010-04-29 16:07:24190 Tries to be intelligent and understand git-svn.
[email protected]f2f9d552009-12-22 00:12:57191 """
192 remote = '.'
[email protected]b24a8e12009-12-22 13:45:48193 branch = GIT.GetBranch(cwd)
[email protected]ad80e3b2010-09-09 14:18:28194 try:
195 upstream_branch = GIT.Capture(
196 ['config', 'branch.%s.merge' % branch], cwd=cwd).strip()
197 except gclient_utils.Error:
198 upstream_branch = None
[email protected]f2f9d552009-12-22 00:12:57199 if upstream_branch:
[email protected]ad80e3b2010-09-09 14:18:28200 try:
201 remote = GIT.Capture(
202 ['config', 'branch.%s.remote' % branch], cwd=cwd).strip()
203 except gclient_utils.Error:
204 pass
[email protected]f2f9d552009-12-22 00:12:57205 else:
206 # Fall back on trying a git-svn upstream branch.
207 if GIT.IsGitSvn(cwd):
208 upstream_branch = GIT.GetSVNBranch(cwd)
[email protected]81e012c2010-04-29 16:07:24209 else:
[email protected]a630bd72010-04-29 23:32:34210 # Else, try to guess the origin remote.
[email protected]ad80e3b2010-09-09 14:18:28211 remote_branches = GIT.Capture(['branch', '-r'], cwd=cwd).split()
[email protected]a630bd72010-04-29 23:32:34212 if 'origin/master' in remote_branches:
213 # Fall back on origin/master if it exits.
214 remote = 'origin'
215 upstream_branch = 'refs/heads/master'
216 elif 'origin/trunk' in remote_branches:
217 # Fall back on origin/trunk if it exists. Generally a shared
218 # git-svn clone
219 remote = 'origin'
220 upstream_branch = 'refs/heads/trunk'
221 else:
222 # Give up.
223 remote = None
224 upstream_branch = None
[email protected]f2f9d552009-12-22 00:12:57225 return remote, upstream_branch
226
227 @staticmethod
[email protected]81e012c2010-04-29 16:07:24228 def GetUpstreamBranch(cwd):
[email protected]f2f9d552009-12-22 00:12:57229 """Gets the current branch's upstream branch."""
230 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
[email protected]a630bd72010-04-29 23:32:34231 if remote != '.' and upstream_branch:
[email protected]f2f9d552009-12-22 00:12:57232 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
233 return upstream_branch
234
235 @staticmethod
[email protected]8ede00e2010-01-12 14:35:28236 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
237 files=None):
[email protected]a9371762009-12-22 18:27:38238 """Diffs against the upstream branch or optionally another branch.
239
240 full_move means that move or copy operations should completely recreate the
241 files, usually in the prospect to apply the patch for a try job."""
[email protected]f2f9d552009-12-22 00:12:57242 if not branch:
[email protected]81e012c2010-04-29 16:07:24243 branch = GIT.GetUpstreamBranch(cwd)
[email protected]400f3e72010-05-19 14:23:36244 command = ['diff', '-p', '--no-prefix', '--no-ext-diff',
245 branch + "..." + branch_head]
[email protected]a9371762009-12-22 18:27:38246 if not full_move:
247 command.append('-C')
[email protected]8ede00e2010-01-12 14:35:28248 # TODO(maruel): --binary support.
249 if files:
250 command.append('--')
251 command.extend(files)
[email protected]ad80e3b2010-09-09 14:18:28252 diff = GIT.Capture(command, cwd=cwd).splitlines(True)
[email protected]f2f9d552009-12-22 00:12:57253 for i in range(len(diff)):
254 # In the case of added files, replace /dev/null with the path to the
255 # file being added.
256 if diff[i].startswith('--- /dev/null'):
257 diff[i] = '--- %s' % diff[i+1][4:]
258 return ''.join(diff)
[email protected]c78f2462009-11-21 01:20:57259
[email protected]b24a8e12009-12-22 13:45:48260 @staticmethod
[email protected]8ede00e2010-01-12 14:35:28261 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
262 """Returns the list of modified files between two branches."""
263 if not branch:
[email protected]81e012c2010-04-29 16:07:24264 branch = GIT.GetUpstreamBranch(cwd)
[email protected]838f0f22010-04-09 17:02:50265 command = ['diff', '--name-only', branch + "..." + branch_head]
[email protected]ad80e3b2010-09-09 14:18:28266 return GIT.Capture(command, cwd=cwd).splitlines(False)
[email protected]8ede00e2010-01-12 14:35:28267
268 @staticmethod
[email protected]b24a8e12009-12-22 13:45:48269 def GetPatchName(cwd):
270 """Constructs a name for this patch."""
[email protected]ad80e3b2010-09-09 14:18:28271 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd=cwd).strip()
[email protected]862ff8e2010-08-06 15:29:16272 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
[email protected]b24a8e12009-12-22 13:45:48273
274 @staticmethod
[email protected]ad80e3b2010-09-09 14:18:28275 def GetCheckoutRoot(cwd):
[email protected]01d8c1d2010-01-07 01:56:59276 """Returns the top level directory of a git checkout as an absolute path.
[email protected]b24a8e12009-12-22 13:45:48277 """
[email protected]ad80e3b2010-09-09 14:18:28278 root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd).strip()
279 return os.path.abspath(os.path.join(cwd, root))
[email protected]b24a8e12009-12-22 13:45:48280
[email protected]d0f854a2010-03-11 19:35:53281 @staticmethod
282 def AssertVersion(min_version):
283 """Asserts git's version is at least min_version."""
284 def only_int(val):
285 if val.isdigit():
286 return int(val)
287 else:
288 return 0
[email protected]ad80e3b2010-09-09 14:18:28289 current_version = GIT.Capture(['--version']).split()[-1]
[email protected]d0f854a2010-03-11 19:35:53290 current_version_list = map(only_int, current_version.split('.'))
291 for min_ver in map(int, min_version.split('.')):
292 ver = current_version_list.pop(0)
293 if ver < min_ver:
294 return (False, current_version)
295 elif ver > min_ver:
296 return (True, current_version)
297 return (True, current_version)
298
[email protected]d5800f12009-11-12 20:03:43299
[email protected]5aeb7dd2009-11-17 18:09:01300class SVN(object):
[email protected]57564662010-04-14 02:35:12301 current_version = None
[email protected]d5800f12009-11-12 20:03:43302
[email protected]5aeb7dd2009-11-17 18:09:01303 @staticmethod
[email protected]54019f32010-09-09 13:50:11304 def Capture(args, **kwargs):
305 """Always redirect stderr.
[email protected]d5800f12009-11-12 20:03:43306
[email protected]54019f32010-09-09 13:50:11307 Throws an exception if non-0 is returned."""
308 return gclient_utils.CheckCall(['svn'] + args, print_error=False,
309 **kwargs)[0]
[email protected]d5800f12009-11-12 20:03:43310
[email protected]5aeb7dd2009-11-17 18:09:01311 @staticmethod
[email protected]2b9aa8e2010-08-25 20:01:42312 def RunAndGetFileList(verbose, args, cwd, file_list, stdout=None):
[email protected]5aeb7dd2009-11-17 18:09:01313 """Runs svn checkout, update, or status, output to stdout.
[email protected]d5800f12009-11-12 20:03:43314
[email protected]5aeb7dd2009-11-17 18:09:01315 The first item in args must be either "checkout", "update", or "status".
[email protected]d5800f12009-11-12 20:03:43316
[email protected]5aeb7dd2009-11-17 18:09:01317 svn's stdout is parsed to collect a list of files checked out or updated.
318 These files are appended to file_list. svn's stdout is also printed to
319 sys.stdout as in Run.
[email protected]d5800f12009-11-12 20:03:43320
[email protected]5aeb7dd2009-11-17 18:09:01321 Args:
[email protected]03807072010-08-16 17:18:44322 verbose: If True, uses verbose output
[email protected]5aeb7dd2009-11-17 18:09:01323 args: A sequence of command line parameters to be passed to svn.
[email protected]2b9aa8e2010-08-25 20:01:42324 cwd: The directory where svn is to be run.
[email protected]d5800f12009-11-12 20:03:43325
[email protected]5aeb7dd2009-11-17 18:09:01326 Raises:
327 Error: An error occurred while running the svn command.
328 """
[email protected]2b9aa8e2010-08-25 20:01:42329 stdout = stdout or sys.stdout
[email protected]d5800f12009-11-12 20:03:43330
[email protected]5aeb7dd2009-11-17 18:09:01331 # svn update and svn checkout use the same pattern: the first three columns
332 # are for file status, property status, and lock status. This is followed
333 # by two spaces, and then the path to the file.
334 update_pattern = '^... (.*)$'
[email protected]d5800f12009-11-12 20:03:43335
[email protected]5aeb7dd2009-11-17 18:09:01336 # The first three columns of svn status are the same as for svn update and
337 # svn checkout. The next three columns indicate addition-with-history,
338 # switch, and remote lock status. This is followed by one space, and then
339 # the path to the file.
340 status_pattern = '^...... (.*)$'
[email protected]d5800f12009-11-12 20:03:43341
[email protected]5aeb7dd2009-11-17 18:09:01342 # args[0] must be a supported command. This will blow up if it's something
343 # else, which is good. Note that the patterns are only effective when
344 # these commands are used in their ordinary forms, the patterns are invalid
345 # for "svn status --show-updates", for example.
346 pattern = {
347 'checkout': update_pattern,
348 'status': status_pattern,
349 'update': update_pattern,
350 }[args[0]]
[email protected]5aeb7dd2009-11-17 18:09:01351 compiled_pattern = re.compile(pattern)
[email protected]b71b67e2009-11-24 20:48:19352 # Place an upper limit.
[email protected]2aee22982010-09-03 14:15:25353 backoff_time = 5
[email protected]03507062010-10-26 00:58:27354 i = 0
355 while True:
356 i += 1
[email protected]b71b67e2009-11-24 20:48:19357 previous_list_len = len(file_list)
358 failure = []
[email protected]54d1f1a2010-01-08 19:53:47359
[email protected]b71b67e2009-11-24 20:48:19360 def CaptureMatchingLines(line):
361 match = compiled_pattern.search(line)
362 if match:
363 file_list.append(match.group(1))
364 if line.startswith('svn: '):
[email protected]8599aa72010-02-08 20:27:14365 failure.append(line)
[email protected]54d1f1a2010-01-08 19:53:47366
[email protected]b71b67e2009-11-24 20:48:19367 try:
[email protected]17d01792010-09-01 18:07:10368 gclient_utils.CheckCallAndFilterAndHeader(
369 ['svn'] + args,
370 cwd=cwd,
371 always=verbose,
372 filter_fn=CaptureMatchingLines,
373 stdout=stdout)
[email protected]b71b67e2009-11-24 20:48:19374 except gclient_utils.Error:
[email protected]6133c5b2010-08-18 18:34:48375 def IsKnownFailure():
376 for x in failure:
377 if (x.startswith('svn: OPTIONS of') or
378 x.startswith('svn: PROPFIND of') or
379 x.startswith('svn: REPORT of') or
[email protected]f61fc932010-08-19 13:05:24380 x.startswith('svn: Unknown hostname') or
381 x.startswith('svn: Server sent unexpected return value')):
[email protected]6133c5b2010-08-18 18:34:48382 return True
383 return False
384
[email protected]953586a2010-06-15 14:22:24385 # Subversion client is really misbehaving with Google Code.
386 if args[0] == 'checkout':
387 # Ensure at least one file was checked out, otherwise *delete* the
388 # directory.
389 if len(file_list) == previous_list_len:
[email protected]6133c5b2010-08-18 18:34:48390 if not IsKnownFailure():
[email protected]953586a2010-06-15 14:22:24391 # No known svn error was found, bail out.
392 raise
[email protected]6133c5b2010-08-18 18:34:48393 # No file were checked out, so make sure the directory is
394 # deleted in case it's messed up and try again.
395 # Warning: It's bad, it assumes args[2] is the directory
396 # argument.
397 if os.path.isdir(args[2]):
398 gclient_utils.RemoveDirectory(args[2])
[email protected]953586a2010-06-15 14:22:24399 else:
400 # Progress was made, convert to update since an aborted checkout
401 # is now an update.
[email protected]2de10252010-02-08 01:10:39402 args = ['update'] + args[1:]
[email protected]953586a2010-06-15 14:22:24403 else:
404 # It was an update or export.
[email protected]6133c5b2010-08-18 18:34:48405 # We enforce that some progress has been made or a known failure.
406 if len(file_list) == previous_list_len and not IsKnownFailure():
407 # No known svn error was found and no progress, bail out.
408 raise
[email protected]03507062010-10-26 00:58:27409 if i == 10:
410 raise
[email protected]2aee22982010-09-03 14:15:25411 print "Sleeping %.1f seconds and retrying...." % backoff_time
412 time.sleep(backoff_time)
413 backoff_time *= 1.3
[email protected]953586a2010-06-15 14:22:24414 continue
[email protected]b71b67e2009-11-24 20:48:19415 break
[email protected]d5800f12009-11-12 20:03:43416
[email protected]5aeb7dd2009-11-17 18:09:01417 @staticmethod
[email protected]54019f32010-09-09 13:50:11418 def CaptureInfo(cwd):
[email protected]5aeb7dd2009-11-17 18:09:01419 """Returns a dictionary from the svn info output for the given file.
[email protected]d5800f12009-11-12 20:03:43420
[email protected]54019f32010-09-09 13:50:11421 Throws an exception if svn info fails."""
422 output = SVN.Capture(['info', '--xml', cwd])
[email protected]5aeb7dd2009-11-17 18:09:01423 dom = gclient_utils.ParseXML(output)
424 result = {}
425 if dom:
426 GetNamedNodeText = gclient_utils.GetNamedNodeText
427 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
428 def C(item, f):
[email protected]6e29d572010-06-04 17:32:20429 if item is not None:
430 return f(item)
[email protected]5aeb7dd2009-11-17 18:09:01431 # /info/entry/
432 # url
433 # reposityory/(root|uuid)
434 # wc-info/(schedule|depth)
435 # commit/(author|date)
436 # str() the results because they may be returned as Unicode, which
437 # interferes with the higher layers matching up things in the deps
438 # dictionary.
[email protected]5aeb7dd2009-11-17 18:09:01439 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
440 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
441 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
442 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
443 'revision'),
444 int)
445 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
446 str)
447 # Differs across versions.
448 if result['Node Kind'] == 'dir':
449 result['Node Kind'] = 'directory'
450 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
451 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
452 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
453 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
454 return result
[email protected]d5800f12009-11-12 20:03:43455
[email protected]5aeb7dd2009-11-17 18:09:01456 @staticmethod
[email protected]54019f32010-09-09 13:50:11457 def CaptureRevision(cwd):
[email protected]5d63eb82010-03-24 23:22:09458 """Get the base revision of a SVN repository.
459
460 Returns:
461 Int base revision
462 """
[email protected]54019f32010-09-09 13:50:11463 info = SVN.Capture(['info', '--xml'], cwd=cwd)
[email protected]5d63eb82010-03-24 23:22:09464 dom = xml.dom.minidom.parseString(info)
465 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
466
467 @staticmethod
[email protected]5aeb7dd2009-11-17 18:09:01468 def CaptureStatus(files):
469 """Returns the svn 1.5 svn status emulated output.
[email protected]d5800f12009-11-12 20:03:43470
[email protected]5aeb7dd2009-11-17 18:09:01471 @files can be a string (one file) or a list of files.
[email protected]d5800f12009-11-12 20:03:43472
[email protected]5aeb7dd2009-11-17 18:09:01473 Returns an array of (status, file) tuples."""
474 command = ["status", "--xml"]
475 if not files:
476 pass
477 elif isinstance(files, basestring):
478 command.append(files)
479 else:
480 command.extend(files)
[email protected]d5800f12009-11-12 20:03:43481
[email protected]5aeb7dd2009-11-17 18:09:01482 status_letter = {
483 None: ' ',
484 '': ' ',
485 'added': 'A',
486 'conflicted': 'C',
487 'deleted': 'D',
488 'external': 'X',
489 'ignored': 'I',
490 'incomplete': '!',
491 'merged': 'G',
492 'missing': '!',
493 'modified': 'M',
494 'none': ' ',
495 'normal': ' ',
496 'obstructed': '~',
497 'replaced': 'R',
498 'unversioned': '?',
499 }
500 dom = gclient_utils.ParseXML(SVN.Capture(command))
501 results = []
502 if dom:
503 # /status/target/entry/(wc-status|commit|author|date)
504 for target in dom.getElementsByTagName('target'):
505 #base_path = target.getAttribute('path')
506 for entry in target.getElementsByTagName('entry'):
507 file_path = entry.getAttribute('path')
508 wc_status = entry.getElementsByTagName('wc-status')
509 assert len(wc_status) == 1
510 # Emulate svn 1.5 status ouput...
511 statuses = [' '] * 7
512 # Col 0
513 xml_item_status = wc_status[0].getAttribute('item')
514 if xml_item_status in status_letter:
515 statuses[0] = status_letter[xml_item_status]
516 else:
[email protected]54019f32010-09-09 13:50:11517 raise gclient_utils.Error(
518 'Unknown item status "%s"; please implement me!' %
519 xml_item_status)
[email protected]5aeb7dd2009-11-17 18:09:01520 # Col 1
521 xml_props_status = wc_status[0].getAttribute('props')
522 if xml_props_status == 'modified':
523 statuses[1] = 'M'
524 elif xml_props_status == 'conflicted':
525 statuses[1] = 'C'
526 elif (not xml_props_status or xml_props_status == 'none' or
527 xml_props_status == 'normal'):
528 pass
529 else:
[email protected]54019f32010-09-09 13:50:11530 raise gclient_utils.Error(
531 'Unknown props status "%s"; please implement me!' %
532 xml_props_status)
[email protected]5aeb7dd2009-11-17 18:09:01533 # Col 2
534 if wc_status[0].getAttribute('wc-locked') == 'true':
535 statuses[2] = 'L'
536 # Col 3
537 if wc_status[0].getAttribute('copied') == 'true':
538 statuses[3] = '+'
539 # Col 4
540 if wc_status[0].getAttribute('switched') == 'true':
541 statuses[4] = 'S'
542 # TODO(maruel): Col 5 and 6
543 item = (''.join(statuses), file_path)
544 results.append(item)
545 return results
[email protected]d5800f12009-11-12 20:03:43546
[email protected]5aeb7dd2009-11-17 18:09:01547 @staticmethod
548 def IsMoved(filename):
549 """Determine if a file has been added through svn mv"""
[email protected]3c55d982010-05-06 14:25:44550 return SVN.IsMovedInfo(SVN.CaptureInfo(filename))
551
552 @staticmethod
553 def IsMovedInfo(info):
554 """Determine if a file has been added through svn mv"""
[email protected]5aeb7dd2009-11-17 18:09:01555 return (info.get('Copied From URL') and
556 info.get('Copied From Rev') and
557 info.get('Schedule') == 'add')
[email protected]d5800f12009-11-12 20:03:43558
[email protected]5aeb7dd2009-11-17 18:09:01559 @staticmethod
[email protected]6e29d572010-06-04 17:32:20560 def GetFileProperty(filename, property_name):
[email protected]5aeb7dd2009-11-17 18:09:01561 """Returns the value of an SVN property for the given file.
[email protected]d5800f12009-11-12 20:03:43562
[email protected]5aeb7dd2009-11-17 18:09:01563 Args:
[email protected]6e29d572010-06-04 17:32:20564 filename: The file to check
[email protected]5aeb7dd2009-11-17 18:09:01565 property_name: The name of the SVN property, e.g. "svn:mime-type"
[email protected]d5800f12009-11-12 20:03:43566
[email protected]5aeb7dd2009-11-17 18:09:01567 Returns:
568 The value of the property, which will be the empty string if the property
569 is not set on the file. If the file is not under version control, the
570 empty string is also returned.
571 """
[email protected]54019f32010-09-09 13:50:11572 try:
573 return SVN.Capture(['propget', property_name, filename])
574 except gclient_utils.Error:
575 return ''
[email protected]d5800f12009-11-12 20:03:43576
[email protected]5aeb7dd2009-11-17 18:09:01577 @staticmethod
[email protected]1c7db8e2010-01-07 02:00:19578 def DiffItem(filename, full_move=False, revision=None):
[email protected]f2f9d552009-12-22 00:12:57579 """Diffs a single file.
580
[email protected]3c55d982010-05-06 14:25:44581 Should be simple, eh? No it isn't.
[email protected]f2f9d552009-12-22 00:12:57582 Be sure to be in the appropriate directory before calling to have the
[email protected]a9371762009-12-22 18:27:38583 expected relative path.
584 full_move means that move or copy operations should completely recreate the
585 files, usually in the prospect to apply the patch for a try job."""
[email protected]5aeb7dd2009-11-17 18:09:01586 # If the user specified a custom diff command in their svn config file,
587 # then it'll be used when we do svn diff, which we don't want to happen
588 # since we want the unified diff. Using --diff-cmd=diff doesn't always
589 # work, since they can have another diff executable in their path that
590 # gives different line endings. So we use a bogus temp directory as the
591 # config directory, which gets around these problems.
[email protected]f2f9d552009-12-22 00:12:57592 bogus_dir = tempfile.mkdtemp()
593 try:
[email protected]3c55d982010-05-06 14:25:44594 # Use "svn info" output instead of os.path.isdir because the latter fails
595 # when the file is deleted.
[email protected]6e29d572010-06-04 17:32:20596 return SVN._DiffItemInternal(filename, SVN.CaptureInfo(filename),
597 bogus_dir,
[email protected]3c55d982010-05-06 14:25:44598 full_move=full_move, revision=revision)
599 finally:
600 shutil.rmtree(bogus_dir)
601
602 @staticmethod
603 def _DiffItemInternal(filename, info, bogus_dir, full_move=False,
604 revision=None):
605 """Grabs the diff data."""
606 command = ["diff", "--config-dir", bogus_dir, filename]
607 if revision:
608 command.extend(['--revision', revision])
609 data = None
610 if SVN.IsMovedInfo(info):
611 if full_move:
612 if info.get("Node Kind") == "directory":
613 # Things become tricky here. It's a directory copy/move. We need to
614 # diff all the files inside it.
615 # This will put a lot of pressure on the heap. This is why StringIO
616 # is used and converted back into a string at the end. The reason to
617 # return a string instead of a StringIO is that StringIO.write()
618 # doesn't accept a StringIO object. *sigh*.
619 for (dirpath, dirnames, filenames) in os.walk(filename):
620 # Cleanup all files starting with a '.'.
621 for d in dirnames:
622 if d.startswith('.'):
623 dirnames.remove(d)
624 for f in filenames:
625 if f.startswith('.'):
626 filenames.remove(f)
627 for f in filenames:
628 if data is None:
629 data = cStringIO.StringIO()
630 data.write(GenFakeDiff(os.path.join(dirpath, f)))
631 if data:
632 tmp = data.getvalue()
633 data.close()
634 data = tmp
[email protected]f2f9d552009-12-22 00:12:57635 else:
[email protected]3c55d982010-05-06 14:25:44636 data = GenFakeDiff(filename)
637 else:
638 if info.get("Node Kind") != "directory":
[email protected]0836c562010-01-22 01:10:06639 # svn diff on a mv/cp'd file outputs nothing if there was no change.
[email protected]54019f32010-09-09 13:50:11640 data = SVN.Capture(command)
[email protected]0836c562010-01-22 01:10:06641 if not data:
642 # We put in an empty Index entry so upload.py knows about them.
[email protected]c6d170e2010-06-03 00:06:00643 data = "Index: %s\n" % filename.replace(os.sep, '/')
[email protected]3c55d982010-05-06 14:25:44644 # Otherwise silently ignore directories.
645 else:
646 if info.get("Node Kind") != "directory":
647 # Normal simple case.
[email protected]54019f32010-09-09 13:50:11648 data = SVN.Capture(command)
[email protected]3c55d982010-05-06 14:25:44649 # Otherwise silently ignore directories.
[email protected]5aeb7dd2009-11-17 18:09:01650 return data
[email protected]c78f2462009-11-21 01:20:57651
652 @staticmethod
[email protected]1c7db8e2010-01-07 02:00:19653 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
[email protected]f2f9d552009-12-22 00:12:57654 """Returns a string containing the diff for the given file list.
655
656 The files in the list should either be absolute paths or relative to the
657 given root. If no root directory is provided, the repository root will be
658 used.
659 The diff will always use relative paths.
660 """
661 previous_cwd = os.getcwd()
[email protected]fd9cbbb2010-01-08 23:04:03662 root = root or SVN.GetCheckoutRoot(previous_cwd)
663 root = os.path.normcase(os.path.join(root, ''))
[email protected]f2f9d552009-12-22 00:12:57664 def RelativePath(path, root):
665 """We must use relative paths."""
[email protected]fd9cbbb2010-01-08 23:04:03666 if os.path.normcase(path).startswith(root):
[email protected]f2f9d552009-12-22 00:12:57667 return path[len(root):]
668 return path
[email protected]3c55d982010-05-06 14:25:44669 # If the user specified a custom diff command in their svn config file,
670 # then it'll be used when we do svn diff, which we don't want to happen
671 # since we want the unified diff. Using --diff-cmd=diff doesn't always
672 # work, since they can have another diff executable in their path that
673 # gives different line endings. So we use a bogus temp directory as the
674 # config directory, which gets around these problems.
675 bogus_dir = tempfile.mkdtemp()
[email protected]f2f9d552009-12-22 00:12:57676 try:
677 os.chdir(root)
[email protected]3c55d982010-05-06 14:25:44678 # Cleanup filenames
679 filenames = [RelativePath(f, root) for f in filenames]
680 # Get information about the modified items (files and directories)
681 data = dict([(f, SVN.CaptureInfo(f)) for f in filenames])
[email protected]3fda4cc2010-06-29 13:29:27682 diffs = []
[email protected]3c55d982010-05-06 14:25:44683 if full_move:
684 # Eliminate modified files inside moved/copied directory.
685 for (filename, info) in data.iteritems():
686 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
687 # Remove files inside the directory.
688 filenames = [f for f in filenames
689 if not f.startswith(filename + os.path.sep)]
690 for filename in data.keys():
691 if not filename in filenames:
692 # Remove filtered out items.
693 del data[filename]
[email protected]3fda4cc2010-06-29 13:29:27694 else:
695 metaheaders = []
696 for (filename, info) in data.iteritems():
697 if SVN.IsMovedInfo(info):
698 # for now, the most common case is a head copy,
699 # so let's just encode that as a straight up cp.
700 srcurl = info.get('Copied From URL')
701 root = info.get('Repository Root')
702 rev = int(info.get('Copied From Rev'))
703 assert srcurl.startswith(root)
704 src = srcurl[len(root)+1:]
705 srcinfo = SVN.CaptureInfo(srcurl)
706 if (srcinfo.get('Revision') != rev and
707 SVN.Capture(['diff', '-r', '%d:head' % rev, srcurl])):
708 metaheaders.append("#$ svn cp -r %d %s %s "
709 "### WARNING: note non-trunk copy\n" %
710 (rev, src, filename))
711 else:
712 metaheaders.append("#$ cp %s %s\n" % (src,
713 filename))
714
715 if metaheaders:
716 diffs.append("### BEGIN SVN COPY METADATA\n")
717 diffs.extend(metaheaders)
718 diffs.append("### END SVN COPY METADATA\n")
[email protected]3c55d982010-05-06 14:25:44719 # Now ready to do the actual diff.
[email protected]3c55d982010-05-06 14:25:44720 for filename in sorted(data.iterkeys()):
721 diffs.append(SVN._DiffItemInternal(filename, data[filename], bogus_dir,
722 full_move=full_move,
723 revision=revision))
724 # Use StringIO since it can be messy when diffing a directory move with
725 # full_move=True.
726 buf = cStringIO.StringIO()
727 for d in filter(None, diffs):
728 buf.write(d)
729 result = buf.getvalue()
730 buf.close()
731 return result
[email protected]f2f9d552009-12-22 00:12:57732 finally:
733 os.chdir(previous_cwd)
[email protected]3c55d982010-05-06 14:25:44734 shutil.rmtree(bogus_dir)
[email protected]f2f9d552009-12-22 00:12:57735
736 @staticmethod
[email protected]c78f2462009-11-21 01:20:57737 def GetEmail(repo_root):
738 """Retrieves the svn account which we assume is an email address."""
[email protected]54019f32010-09-09 13:50:11739 try:
740 infos = SVN.CaptureInfo(repo_root)
741 except gclient_utils.Error:
[email protected]c78f2462009-11-21 01:20:57742 return None
743
744 # Should check for uuid but it is incorrectly saved for https creds.
[email protected]54019f32010-09-09 13:50:11745 root = infos['Repository Root']
[email protected]c78f2462009-11-21 01:20:57746 realm = root.rsplit('/', 1)[0]
[email protected]54019f32010-09-09 13:50:11747 uuid = infos['UUID']
[email protected]c78f2462009-11-21 01:20:57748 if root.startswith('https') or not uuid:
749 regexp = re.compile(r'<%s:\d+>.*' % realm)
750 else:
751 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
752 if regexp is None:
753 return None
754 if sys.platform.startswith('win'):
755 if not 'APPDATA' in os.environ:
756 return None
[email protected]720d9f32009-11-21 17:38:57757 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
758 'svn.simple')
[email protected]c78f2462009-11-21 01:20:57759 else:
760 if not 'HOME' in os.environ:
761 return None
762 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
763 'svn.simple')
764 for credfile in os.listdir(auth_dir):
765 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
766 if regexp.match(cred_info.get('svn:realmstring')):
767 return cred_info.get('username')
768
769 @staticmethod
770 def ReadSimpleAuth(filename):
771 f = open(filename, 'r')
772 values = {}
[email protected]6e29d572010-06-04 17:32:20773 def ReadOneItem(item_type):
774 m = re.match(r'%s (\d+)' % item_type, f.readline())
[email protected]c78f2462009-11-21 01:20:57775 if not m:
776 return None
777 data = f.read(int(m.group(1)))
778 if f.read(1) != '\n':
779 return None
780 return data
781
782 while True:
783 key = ReadOneItem('K')
784 if not key:
785 break
786 value = ReadOneItem('V')
787 if not value:
788 break
789 values[key] = value
790 return values
[email protected]94b1ee92009-12-19 20:27:20791
792 @staticmethod
793 def GetCheckoutRoot(directory):
794 """Returns the top level directory of the current repository.
795
796 The directory is returned as an absolute path.
797 """
[email protected]f7ae6d52009-12-22 20:49:04798 directory = os.path.abspath(directory)
[email protected]54019f32010-09-09 13:50:11799 try:
800 cur_dir_repo_root = SVN.CaptureInfo(directory)['Repository Root']
801 except gclient_utils.Error:
[email protected]94b1ee92009-12-19 20:27:20802 return None
[email protected]94b1ee92009-12-19 20:27:20803 while True:
804 parent = os.path.dirname(directory)
[email protected]54019f32010-09-09 13:50:11805 try:
806 if SVN.CaptureInfo(parent)['Repository Root'] != cur_dir_repo_root:
807 break
808 except gclient_utils.Error:
[email protected]94b1ee92009-12-19 20:27:20809 break
810 directory = parent
[email protected]fd9cbbb2010-01-08 23:04:03811 return GetCasedPath(directory)
[email protected]57564662010-04-14 02:35:12812
813 @staticmethod
814 def AssertVersion(min_version):
815 """Asserts svn's version is at least min_version."""
816 def only_int(val):
817 if val.isdigit():
818 return int(val)
819 else:
820 return 0
821 if not SVN.current_version:
822 SVN.current_version = SVN.Capture(['--version']).split()[2]
823 current_version_list = map(only_int, SVN.current_version.split('.'))
824 for min_ver in map(int, min_version.split('.')):
825 ver = current_version_list.pop(0)
826 if ver < min_ver:
827 return (False, SVN.current_version)
828 elif ver > min_ver:
829 return (True, SVN.current_version)
830 return (True, SVN.current_version)