blob: 00364cae4c82f2bf181db2f5ffca83626857a518 [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):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000068 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +000069 def Capture(args, **kwargs):
70 return gclient_utils.CheckCall(['git'] + args, print_error=False,
71 **kwargs)[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000072
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000073 @staticmethod
msb@chromium.org786fb682010-06-02 15:16:23 +000074 def CaptureStatus(files, upstream_branch=None):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000075 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000076
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000077 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000078
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000079 Returns an array of (status, file) tuples."""
msb@chromium.org786fb682010-06-02 15:16:23 +000080 if upstream_branch is None:
81 upstream_branch = GIT.GetUpstreamBranch(os.getcwd())
82 if upstream_branch is None:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +000083 raise gclient_utils.Error('Cannot determine upstream branch')
84 command = ['diff', '--name-status', '-r', '%s...' % upstream_branch]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000085 if not files:
86 pass
87 elif isinstance(files, basestring):
88 command.append(files)
89 else:
90 command.extend(files)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +000091 status = GIT.Capture(command).rstrip()
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000092 results = []
93 if status:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +000094 for statusline in status.splitlines():
maruel@chromium.orgcc1614b2010-09-20 17:13:17 +000095 # 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)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000099 if not m:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000100 raise gclient_utils.Error(
101 'status currently unsupported: %s' % statusline)
maruel@chromium.orgcc1614b2010-09-20 17:13:17 +0000102 # Only grab the first letter.
103 results.append(('%s ' % m.group(1)[0], m.group(2)))
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000104 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000105
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000106 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000107 def GetEmail(cwd):
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000108 """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.
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000111 try:
112 return GIT.Capture(['config', 'user.email'], cwd=cwd).strip()
113 except gclient_utils.CheckCallError:
114 return ''
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000115
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):
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000123 """Returns the full branch reference, e.g. 'refs/heads/master'."""
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000124 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd=cwd).strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000125
126 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000127 def GetBranch(cwd):
128 """Returns the short branch name, e.g. 'master'."""
maruel@chromium.orgc308a742009-12-22 18:29:33 +0000129 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000130
131 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000132 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:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000136 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd=cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000137 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'],
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000155 cwd=cwd).splitlines()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000156 svn_refs = {}
157 for ref in remotes:
158 match = git_svn_re.search(
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000159 GIT.Capture(['cat-file', '-p', ref], cwd=cwd))
sky@chromium.org42d8da52010-04-23 18:25:07 +0000160 # Prefer origin/HEAD over all others.
161 if match and (match.group(1) not in svn_refs or
162 ref == "refs/remotes/origin/HEAD"):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000163 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']
maruel@chromium.org3a292682010-08-23 18:54:55 +0000175 proc = gclient_utils.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000176 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'
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000190 Tries to be intelligent and understand git-svn.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000191 """
192 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000193 branch = GIT.GetBranch(cwd)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000194 try:
195 upstream_branch = GIT.Capture(
196 ['config', 'branch.%s.merge' % branch], cwd=cwd).strip()
197 except gclient_utils.Error:
198 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000199 if upstream_branch:
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000200 try:
201 remote = GIT.Capture(
202 ['config', 'branch.%s.remote' % branch], cwd=cwd).strip()
203 except gclient_utils.Error:
204 pass
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000205 else:
206 # Fall back on trying a git-svn upstream branch.
207 if GIT.IsGitSvn(cwd):
208 upstream_branch = GIT.GetSVNBranch(cwd)
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000209 else:
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000210 # Else, try to guess the origin remote.
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000211 remote_branches = GIT.Capture(['branch', '-r'], cwd=cwd).split()
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000212 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
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000225 return remote, upstream_branch
226
227 @staticmethod
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000228 def GetUpstreamBranch(cwd):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000229 """Gets the current branch's upstream branch."""
230 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000231 if remote != '.' and upstream_branch:
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000232 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
233 return upstream_branch
234
235 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000236 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
237 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000238 """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."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000242 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000243 branch = GIT.GetUpstreamBranch(cwd)
evan@chromium.org400f3e72010-05-19 14:23:36 +0000244 command = ['diff', '-p', '--no-prefix', '--no-ext-diff',
245 branch + "..." + branch_head]
maruel@chromium.orga9371762009-12-22 18:27:38 +0000246 if not full_move:
247 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000248 # TODO(maruel): --binary support.
249 if files:
250 command.append('--')
251 command.extend(files)
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000252 diff = GIT.Capture(command, cwd=cwd).splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000253 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)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000259
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000260 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000261 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
262 """Returns the list of modified files between two branches."""
263 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000264 branch = GIT.GetUpstreamBranch(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000265 command = ['diff', '--name-only', branch + "..." + branch_head]
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000266 return GIT.Capture(command, cwd=cwd).splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000267
268 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000269 def GetPatchName(cwd):
270 """Constructs a name for this patch."""
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000271 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd=cwd).strip()
maruel@chromium.org862ff8e2010-08-06 15:29:16 +0000272 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000273
274 @staticmethod
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000275 def GetCheckoutRoot(cwd):
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000276 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000277 """
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000278 root = GIT.Capture(['rev-parse', '--show-cdup'], cwd=cwd).strip()
279 return os.path.abspath(os.path.join(cwd, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000280
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000281 @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
maruel@chromium.orgad80e3b2010-09-09 14:18:28 +0000289 current_version = GIT.Capture(['--version']).split()[-1]
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000290 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
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000299
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000300class SVN(object):
tony@chromium.org57564662010-04-14 02:35:12 +0000301 current_version = None
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000302
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000303 @staticmethod
maruel@chromium.org54019f32010-09-09 13:50:11 +0000304 def Capture(args, **kwargs):
305 """Always redirect stderr.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000306
maruel@chromium.org54019f32010-09-09 13:50:11 +0000307 Throws an exception if non-0 is returned."""
308 return gclient_utils.CheckCall(['svn'] + args, print_error=False,
309 **kwargs)[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000310
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000311 @staticmethod
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000312 def RunAndGetFileList(verbose, args, cwd, file_list, stdout=None):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000313 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000314
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000315 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000316
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000317 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.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000320
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000321 Args:
maruel@chromium.org03807072010-08-16 17:18:44 +0000322 verbose: If True, uses verbose output
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000323 args: A sequence of command line parameters to be passed to svn.
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000324 cwd: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000325
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000326 Raises:
327 Error: An error occurred while running the svn command.
328 """
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000329 stdout = stdout or sys.stdout
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000330
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000331 # 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 = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000335
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000336 # 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 = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000341
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000342 # 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]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000351 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000352 # Place an upper limit.
cbentzel@chromium.org2aee2292010-09-03 14:15:25 +0000353 backoff_time = 5
maruel@chromium.orgfd876172010-04-30 14:01:05 +0000354 for _ in range(10):
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000355 previous_list_len = len(file_list)
356 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000357
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000358 def CaptureMatchingLines(line):
359 match = compiled_pattern.search(line)
360 if match:
361 file_list.append(match.group(1))
362 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000363 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000364
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000365 try:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000366 gclient_utils.CheckCallAndFilterAndHeader(
367 ['svn'] + args,
368 cwd=cwd,
369 always=verbose,
370 filter_fn=CaptureMatchingLines,
371 stdout=stdout)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000372 except gclient_utils.Error:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000373 def IsKnownFailure():
374 for x in failure:
375 if (x.startswith('svn: OPTIONS of') or
376 x.startswith('svn: PROPFIND of') or
377 x.startswith('svn: REPORT of') or
maruel@chromium.orgf61fc932010-08-19 13:05:24 +0000378 x.startswith('svn: Unknown hostname') or
379 x.startswith('svn: Server sent unexpected return value')):
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000380 return True
381 return False
382
maruel@chromium.org953586a2010-06-15 14:22:24 +0000383 # Subversion client is really misbehaving with Google Code.
384 if args[0] == 'checkout':
385 # Ensure at least one file was checked out, otherwise *delete* the
386 # directory.
387 if len(file_list) == previous_list_len:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000388 if not IsKnownFailure():
maruel@chromium.org953586a2010-06-15 14:22:24 +0000389 # No known svn error was found, bail out.
390 raise
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000391 # No file were checked out, so make sure the directory is
392 # deleted in case it's messed up and try again.
393 # Warning: It's bad, it assumes args[2] is the directory
394 # argument.
395 if os.path.isdir(args[2]):
396 gclient_utils.RemoveDirectory(args[2])
maruel@chromium.org953586a2010-06-15 14:22:24 +0000397 else:
398 # Progress was made, convert to update since an aborted checkout
399 # is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000400 args = ['update'] + args[1:]
maruel@chromium.org953586a2010-06-15 14:22:24 +0000401 else:
402 # It was an update or export.
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000403 # We enforce that some progress has been made or a known failure.
404 if len(file_list) == previous_list_len and not IsKnownFailure():
405 # No known svn error was found and no progress, bail out.
406 raise
cbentzel@chromium.org2aee2292010-09-03 14:15:25 +0000407 print "Sleeping %.1f seconds and retrying...." % backoff_time
408 time.sleep(backoff_time)
409 backoff_time *= 1.3
maruel@chromium.org953586a2010-06-15 14:22:24 +0000410 continue
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000411 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000412
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000413 @staticmethod
maruel@chromium.org54019f32010-09-09 13:50:11 +0000414 def CaptureInfo(cwd):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000415 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000416
maruel@chromium.org54019f32010-09-09 13:50:11 +0000417 Throws an exception if svn info fails."""
418 output = SVN.Capture(['info', '--xml', cwd])
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000419 dom = gclient_utils.ParseXML(output)
420 result = {}
421 if dom:
422 GetNamedNodeText = gclient_utils.GetNamedNodeText
423 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
424 def C(item, f):
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000425 if item is not None:
426 return f(item)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000427 # /info/entry/
428 # url
429 # reposityory/(root|uuid)
430 # wc-info/(schedule|depth)
431 # commit/(author|date)
432 # str() the results because they may be returned as Unicode, which
433 # interferes with the higher layers matching up things in the deps
434 # dictionary.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000435 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
436 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
437 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
438 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
439 'revision'),
440 int)
441 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
442 str)
443 # Differs across versions.
444 if result['Node Kind'] == 'dir':
445 result['Node Kind'] = 'directory'
446 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
447 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
448 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
449 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
450 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000451
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000452 @staticmethod
maruel@chromium.org54019f32010-09-09 13:50:11 +0000453 def CaptureRevision(cwd):
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000454 """Get the base revision of a SVN repository.
455
456 Returns:
457 Int base revision
458 """
maruel@chromium.org54019f32010-09-09 13:50:11 +0000459 info = SVN.Capture(['info', '--xml'], cwd=cwd)
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000460 dom = xml.dom.minidom.parseString(info)
461 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
462
463 @staticmethod
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000464 def CaptureStatus(files):
465 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000466
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000467 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000468
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000469 Returns an array of (status, file) tuples."""
470 command = ["status", "--xml"]
471 if not files:
472 pass
473 elif isinstance(files, basestring):
474 command.append(files)
475 else:
476 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000477
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000478 status_letter = {
479 None: ' ',
480 '': ' ',
481 'added': 'A',
482 'conflicted': 'C',
483 'deleted': 'D',
484 'external': 'X',
485 'ignored': 'I',
486 'incomplete': '!',
487 'merged': 'G',
488 'missing': '!',
489 'modified': 'M',
490 'none': ' ',
491 'normal': ' ',
492 'obstructed': '~',
493 'replaced': 'R',
494 'unversioned': '?',
495 }
496 dom = gclient_utils.ParseXML(SVN.Capture(command))
497 results = []
498 if dom:
499 # /status/target/entry/(wc-status|commit|author|date)
500 for target in dom.getElementsByTagName('target'):
501 #base_path = target.getAttribute('path')
502 for entry in target.getElementsByTagName('entry'):
503 file_path = entry.getAttribute('path')
504 wc_status = entry.getElementsByTagName('wc-status')
505 assert len(wc_status) == 1
506 # Emulate svn 1.5 status ouput...
507 statuses = [' '] * 7
508 # Col 0
509 xml_item_status = wc_status[0].getAttribute('item')
510 if xml_item_status in status_letter:
511 statuses[0] = status_letter[xml_item_status]
512 else:
maruel@chromium.org54019f32010-09-09 13:50:11 +0000513 raise gclient_utils.Error(
514 'Unknown item status "%s"; please implement me!' %
515 xml_item_status)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000516 # Col 1
517 xml_props_status = wc_status[0].getAttribute('props')
518 if xml_props_status == 'modified':
519 statuses[1] = 'M'
520 elif xml_props_status == 'conflicted':
521 statuses[1] = 'C'
522 elif (not xml_props_status or xml_props_status == 'none' or
523 xml_props_status == 'normal'):
524 pass
525 else:
maruel@chromium.org54019f32010-09-09 13:50:11 +0000526 raise gclient_utils.Error(
527 'Unknown props status "%s"; please implement me!' %
528 xml_props_status)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000529 # Col 2
530 if wc_status[0].getAttribute('wc-locked') == 'true':
531 statuses[2] = 'L'
532 # Col 3
533 if wc_status[0].getAttribute('copied') == 'true':
534 statuses[3] = '+'
535 # Col 4
536 if wc_status[0].getAttribute('switched') == 'true':
537 statuses[4] = 'S'
538 # TODO(maruel): Col 5 and 6
539 item = (''.join(statuses), file_path)
540 results.append(item)
541 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000542
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000543 @staticmethod
544 def IsMoved(filename):
545 """Determine if a file has been added through svn mv"""
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000546 return SVN.IsMovedInfo(SVN.CaptureInfo(filename))
547
548 @staticmethod
549 def IsMovedInfo(info):
550 """Determine if a file has been added through svn mv"""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000551 return (info.get('Copied From URL') and
552 info.get('Copied From Rev') and
553 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000554
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000555 @staticmethod
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000556 def GetFileProperty(filename, property_name):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000557 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000558
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000559 Args:
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000560 filename: The file to check
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000561 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000562
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000563 Returns:
564 The value of the property, which will be the empty string if the property
565 is not set on the file. If the file is not under version control, the
566 empty string is also returned.
567 """
maruel@chromium.org54019f32010-09-09 13:50:11 +0000568 try:
569 return SVN.Capture(['propget', property_name, filename])
570 except gclient_utils.Error:
571 return ''
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000572
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000573 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000574 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000575 """Diffs a single file.
576
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000577 Should be simple, eh? No it isn't.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000578 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000579 expected relative path.
580 full_move means that move or copy operations should completely recreate the
581 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000582 # If the user specified a custom diff command in their svn config file,
583 # then it'll be used when we do svn diff, which we don't want to happen
584 # since we want the unified diff. Using --diff-cmd=diff doesn't always
585 # work, since they can have another diff executable in their path that
586 # gives different line endings. So we use a bogus temp directory as the
587 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000588 bogus_dir = tempfile.mkdtemp()
589 try:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000590 # Use "svn info" output instead of os.path.isdir because the latter fails
591 # when the file is deleted.
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000592 return SVN._DiffItemInternal(filename, SVN.CaptureInfo(filename),
593 bogus_dir,
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000594 full_move=full_move, revision=revision)
595 finally:
596 shutil.rmtree(bogus_dir)
597
598 @staticmethod
599 def _DiffItemInternal(filename, info, bogus_dir, full_move=False,
600 revision=None):
601 """Grabs the diff data."""
602 command = ["diff", "--config-dir", bogus_dir, filename]
603 if revision:
604 command.extend(['--revision', revision])
605 data = None
606 if SVN.IsMovedInfo(info):
607 if full_move:
608 if info.get("Node Kind") == "directory":
609 # Things become tricky here. It's a directory copy/move. We need to
610 # diff all the files inside it.
611 # This will put a lot of pressure on the heap. This is why StringIO
612 # is used and converted back into a string at the end. The reason to
613 # return a string instead of a StringIO is that StringIO.write()
614 # doesn't accept a StringIO object. *sigh*.
615 for (dirpath, dirnames, filenames) in os.walk(filename):
616 # Cleanup all files starting with a '.'.
617 for d in dirnames:
618 if d.startswith('.'):
619 dirnames.remove(d)
620 for f in filenames:
621 if f.startswith('.'):
622 filenames.remove(f)
623 for f in filenames:
624 if data is None:
625 data = cStringIO.StringIO()
626 data.write(GenFakeDiff(os.path.join(dirpath, f)))
627 if data:
628 tmp = data.getvalue()
629 data.close()
630 data = tmp
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000631 else:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000632 data = GenFakeDiff(filename)
633 else:
634 if info.get("Node Kind") != "directory":
maruel@chromium.org0836c562010-01-22 01:10:06 +0000635 # svn diff on a mv/cp'd file outputs nothing if there was no change.
maruel@chromium.org54019f32010-09-09 13:50:11 +0000636 data = SVN.Capture(command)
maruel@chromium.org0836c562010-01-22 01:10:06 +0000637 if not data:
638 # We put in an empty Index entry so upload.py knows about them.
maruel@chromium.orgc6d170e2010-06-03 00:06:00 +0000639 data = "Index: %s\n" % filename.replace(os.sep, '/')
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000640 # Otherwise silently ignore directories.
641 else:
642 if info.get("Node Kind") != "directory":
643 # Normal simple case.
maruel@chromium.org54019f32010-09-09 13:50:11 +0000644 data = SVN.Capture(command)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000645 # Otherwise silently ignore directories.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000646 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000647
648 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000649 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000650 """Returns a string containing the diff for the given file list.
651
652 The files in the list should either be absolute paths or relative to the
653 given root. If no root directory is provided, the repository root will be
654 used.
655 The diff will always use relative paths.
656 """
657 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000658 root = root or SVN.GetCheckoutRoot(previous_cwd)
659 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000660 def RelativePath(path, root):
661 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000662 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000663 return path[len(root):]
664 return path
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000665 # If the user specified a custom diff command in their svn config file,
666 # then it'll be used when we do svn diff, which we don't want to happen
667 # since we want the unified diff. Using --diff-cmd=diff doesn't always
668 # work, since they can have another diff executable in their path that
669 # gives different line endings. So we use a bogus temp directory as the
670 # config directory, which gets around these problems.
671 bogus_dir = tempfile.mkdtemp()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000672 try:
673 os.chdir(root)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000674 # Cleanup filenames
675 filenames = [RelativePath(f, root) for f in filenames]
676 # Get information about the modified items (files and directories)
677 data = dict([(f, SVN.CaptureInfo(f)) for f in filenames])
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000678 diffs = []
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000679 if full_move:
680 # Eliminate modified files inside moved/copied directory.
681 for (filename, info) in data.iteritems():
682 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
683 # Remove files inside the directory.
684 filenames = [f for f in filenames
685 if not f.startswith(filename + os.path.sep)]
686 for filename in data.keys():
687 if not filename in filenames:
688 # Remove filtered out items.
689 del data[filename]
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000690 else:
691 metaheaders = []
692 for (filename, info) in data.iteritems():
693 if SVN.IsMovedInfo(info):
694 # for now, the most common case is a head copy,
695 # so let's just encode that as a straight up cp.
696 srcurl = info.get('Copied From URL')
697 root = info.get('Repository Root')
698 rev = int(info.get('Copied From Rev'))
699 assert srcurl.startswith(root)
700 src = srcurl[len(root)+1:]
701 srcinfo = SVN.CaptureInfo(srcurl)
702 if (srcinfo.get('Revision') != rev and
703 SVN.Capture(['diff', '-r', '%d:head' % rev, srcurl])):
704 metaheaders.append("#$ svn cp -r %d %s %s "
705 "### WARNING: note non-trunk copy\n" %
706 (rev, src, filename))
707 else:
708 metaheaders.append("#$ cp %s %s\n" % (src,
709 filename))
710
711 if metaheaders:
712 diffs.append("### BEGIN SVN COPY METADATA\n")
713 diffs.extend(metaheaders)
714 diffs.append("### END SVN COPY METADATA\n")
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000715 # Now ready to do the actual diff.
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000716 for filename in sorted(data.iterkeys()):
717 diffs.append(SVN._DiffItemInternal(filename, data[filename], bogus_dir,
718 full_move=full_move,
719 revision=revision))
720 # Use StringIO since it can be messy when diffing a directory move with
721 # full_move=True.
722 buf = cStringIO.StringIO()
723 for d in filter(None, diffs):
724 buf.write(d)
725 result = buf.getvalue()
726 buf.close()
727 return result
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000728 finally:
729 os.chdir(previous_cwd)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000730 shutil.rmtree(bogus_dir)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000731
732 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000733 def GetEmail(repo_root):
734 """Retrieves the svn account which we assume is an email address."""
maruel@chromium.org54019f32010-09-09 13:50:11 +0000735 try:
736 infos = SVN.CaptureInfo(repo_root)
737 except gclient_utils.Error:
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000738 return None
739
740 # Should check for uuid but it is incorrectly saved for https creds.
maruel@chromium.org54019f32010-09-09 13:50:11 +0000741 root = infos['Repository Root']
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000742 realm = root.rsplit('/', 1)[0]
maruel@chromium.org54019f32010-09-09 13:50:11 +0000743 uuid = infos['UUID']
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000744 if root.startswith('https') or not uuid:
745 regexp = re.compile(r'<%s:\d+>.*' % realm)
746 else:
747 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
748 if regexp is None:
749 return None
750 if sys.platform.startswith('win'):
751 if not 'APPDATA' in os.environ:
752 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000753 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
754 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000755 else:
756 if not 'HOME' in os.environ:
757 return None
758 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
759 'svn.simple')
760 for credfile in os.listdir(auth_dir):
761 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
762 if regexp.match(cred_info.get('svn:realmstring')):
763 return cred_info.get('username')
764
765 @staticmethod
766 def ReadSimpleAuth(filename):
767 f = open(filename, 'r')
768 values = {}
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000769 def ReadOneItem(item_type):
770 m = re.match(r'%s (\d+)' % item_type, f.readline())
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000771 if not m:
772 return None
773 data = f.read(int(m.group(1)))
774 if f.read(1) != '\n':
775 return None
776 return data
777
778 while True:
779 key = ReadOneItem('K')
780 if not key:
781 break
782 value = ReadOneItem('V')
783 if not value:
784 break
785 values[key] = value
786 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000787
788 @staticmethod
789 def GetCheckoutRoot(directory):
790 """Returns the top level directory of the current repository.
791
792 The directory is returned as an absolute path.
793 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000794 directory = os.path.abspath(directory)
maruel@chromium.org54019f32010-09-09 13:50:11 +0000795 try:
796 cur_dir_repo_root = SVN.CaptureInfo(directory)['Repository Root']
797 except gclient_utils.Error:
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000798 return None
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000799 while True:
800 parent = os.path.dirname(directory)
maruel@chromium.org54019f32010-09-09 13:50:11 +0000801 try:
802 if SVN.CaptureInfo(parent)['Repository Root'] != cur_dir_repo_root:
803 break
804 except gclient_utils.Error:
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000805 break
806 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000807 return GetCasedPath(directory)
tony@chromium.org57564662010-04-14 02:35:12 +0000808
809 @staticmethod
810 def AssertVersion(min_version):
811 """Asserts svn's version is at least min_version."""
812 def only_int(val):
813 if val.isdigit():
814 return int(val)
815 else:
816 return 0
817 if not SVN.current_version:
818 SVN.current_version = SVN.Capture(['--version']).split()[2]
819 current_version_list = map(only_int, SVN.current_version.split('.'))
820 for min_ver in map(int, min_version.split('.')):
821 ver = current_version_list.pop(0)
822 if ver < min_ver:
823 return (False, SVN.current_version)
824 elif ver > min_ver:
825 return (True, SVN.current_version)
826 return (True, SVN.current_version)