blob: 9cbcc35d49037d884d0ef08d2f60f0ca3f921e98 [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.orgf2f9d552009-12-22 00:12:57 +000069 def Capture(args, in_directory=None, print_error=True, error_ok=False):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000070 """Runs git, capturing output sent to stdout as a string.
71
72 Args:
73 args: A sequence of command line parameters to be passed to git.
74 in_directory: The directory where git is to be run.
75
76 Returns:
77 The output sent to stdout as a string.
78 """
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000079 try:
maruel@chromium.org17d01792010-09-01 18:07:10 +000080 return gclient_utils.CheckCall(['git'] + args, in_directory, print_error)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000081 except gclient_utils.CheckCallError:
82 if error_ok:
nasser@codeaurora.orgcd968c12010-02-01 06:05:00 +000083 return ('', '')
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000084 raise
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000085
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000086 @staticmethod
msb@chromium.org786fb682010-06-02 15:16:23 +000087 def CaptureStatus(files, upstream_branch=None):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000088 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000089
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000090 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000091
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000092 Returns an array of (status, file) tuples."""
msb@chromium.org786fb682010-06-02 15:16:23 +000093 if upstream_branch is None:
94 upstream_branch = GIT.GetUpstreamBranch(os.getcwd())
95 if upstream_branch is None:
96 raise Exception("Cannot determine upstream branch")
bauerb@chromium.org14ec5042010-03-30 18:19:09 +000097 command = ["diff", "--name-status", "-r", "%s..." % upstream_branch]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000098 if not files:
99 pass
100 elif isinstance(files, basestring):
101 command.append(files)
102 else:
103 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000104
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000105 status = GIT.Capture(command)[0].rstrip()
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000106 results = []
107 if status:
108 for statusline in status.split('\n'):
109 m = re.match('^(\w)\t(.+)$', statusline)
110 if not m:
111 raise Exception("status currently unsupported: %s" % statusline)
112 results.append(('%s ' % m.group(1), m.group(2)))
113 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000114
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000115 @staticmethod
116 def GetEmail(repo_root):
117 """Retrieves the user email address if known."""
118 # We could want to look at the svn cred when it has a svn remote but it
119 # should be fine for now, users should simply configure their git settings.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000120 return GIT.Capture(['config', 'user.email'],
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000121 repo_root, error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000122
123 @staticmethod
124 def ShortBranchName(branch):
125 """Converts a name like 'refs/heads/foo' to just 'foo'."""
126 return branch.replace('refs/heads/', '')
127
128 @staticmethod
129 def GetBranchRef(cwd):
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000130 """Returns the full branch reference, e.g. 'refs/heads/master'."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000131 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000132
133 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000134 def GetBranch(cwd):
135 """Returns the short branch name, e.g. 'master'."""
maruel@chromium.orgc308a742009-12-22 18:29:33 +0000136 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000137
138 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000139 def IsGitSvn(cwd):
140 """Returns true if this repo looks like it's using git-svn."""
141 # If you have any "svn-remote.*" config keys, we think you're using svn.
142 try:
143 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd)
144 return True
145 except gclient_utils.CheckCallError:
146 return False
147
148 @staticmethod
149 def GetSVNBranch(cwd):
150 """Returns the svn branch name if found."""
151 # Try to figure out which remote branch we're based on.
152 # Strategy:
153 # 1) find all git-svn branches and note their svn URLs.
154 # 2) iterate through our branch history and match up the URLs.
155
156 # regexp matching the git-svn line that contains the URL.
157 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
158
159 # Get the refname and svn url for all refs/remotes/*.
160 remotes = GIT.Capture(
161 ['for-each-ref', '--format=%(refname)', 'refs/remotes'],
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000162 cwd)[0].splitlines()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000163 svn_refs = {}
164 for ref in remotes:
165 match = git_svn_re.search(
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000166 GIT.Capture(['cat-file', '-p', ref], cwd)[0])
sky@chromium.org42d8da52010-04-23 18:25:07 +0000167 # Prefer origin/HEAD over all others.
168 if match and (match.group(1) not in svn_refs or
169 ref == "refs/remotes/origin/HEAD"):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000170 svn_refs[match.group(1)] = ref
171
172 svn_branch = ''
173 if len(svn_refs) == 1:
174 # Only one svn branch exists -- seems like a good candidate.
175 svn_branch = svn_refs.values()[0]
176 elif len(svn_refs) > 1:
177 # We have more than one remote branch available. We don't
178 # want to go through all of history, so read a line from the
179 # pipe at a time.
180 # The -100 is an arbitrary limit so we don't search forever.
181 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org3a292682010-08-23 18:54:55 +0000182 proc = gclient_utils.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000183 for line in proc.stdout:
184 match = git_svn_re.match(line)
185 if match:
186 url = match.group(1)
187 if url in svn_refs:
188 svn_branch = svn_refs[url]
189 proc.stdout.close() # Cut pipe.
190 break
191 return svn_branch
192
193 @staticmethod
194 def FetchUpstreamTuple(cwd):
195 """Returns a tuple containg remote and remote ref,
196 e.g. 'origin', 'refs/heads/master'
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000197 Tries to be intelligent and understand git-svn.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000198 """
199 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000200 branch = GIT.GetBranch(cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000201 upstream_branch = None
202 upstream_branch = GIT.Capture(
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000203 ['config', 'branch.%s.merge' % branch], in_directory=cwd,
204 error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000205 if upstream_branch:
206 remote = GIT.Capture(
207 ['config', 'branch.%s.remote' % branch],
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000208 in_directory=cwd, error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000209 else:
210 # Fall back on trying a git-svn upstream branch.
211 if GIT.IsGitSvn(cwd):
212 upstream_branch = GIT.GetSVNBranch(cwd)
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000213 else:
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000214 # Else, try to guess the origin remote.
215 remote_branches = GIT.Capture(
216 ['branch', '-r'], in_directory=cwd)[0].split()
217 if 'origin/master' in remote_branches:
218 # Fall back on origin/master if it exits.
219 remote = 'origin'
220 upstream_branch = 'refs/heads/master'
221 elif 'origin/trunk' in remote_branches:
222 # Fall back on origin/trunk if it exists. Generally a shared
223 # git-svn clone
224 remote = 'origin'
225 upstream_branch = 'refs/heads/trunk'
226 else:
227 # Give up.
228 remote = None
229 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000230 return remote, upstream_branch
231
232 @staticmethod
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000233 def GetUpstreamBranch(cwd):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000234 """Gets the current branch's upstream branch."""
235 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000236 if remote != '.' and upstream_branch:
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000237 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
238 return upstream_branch
239
240 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000241 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
242 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000243 """Diffs against the upstream branch or optionally another branch.
244
245 full_move means that move or copy operations should completely recreate the
246 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000247 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000248 branch = GIT.GetUpstreamBranch(cwd)
evan@chromium.org400f3e72010-05-19 14:23:36 +0000249 command = ['diff', '-p', '--no-prefix', '--no-ext-diff',
250 branch + "..." + branch_head]
maruel@chromium.orga9371762009-12-22 18:27:38 +0000251 if not full_move:
252 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000253 # TODO(maruel): --binary support.
254 if files:
255 command.append('--')
256 command.extend(files)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000257 diff = GIT.Capture(command, cwd)[0].splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000258 for i in range(len(diff)):
259 # In the case of added files, replace /dev/null with the path to the
260 # file being added.
261 if diff[i].startswith('--- /dev/null'):
262 diff[i] = '--- %s' % diff[i+1][4:]
263 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000264
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000265 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000266 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
267 """Returns the list of modified files between two branches."""
268 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000269 branch = GIT.GetUpstreamBranch(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000270 command = ['diff', '--name-only', branch + "..." + branch_head]
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000271 return GIT.Capture(command, cwd)[0].splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000272
273 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000274 def GetPatchName(cwd):
275 """Constructs a name for this patch."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000276 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd)[0].strip()
maruel@chromium.org862ff8e2010-08-06 15:29:16 +0000277 return "%s#%s" % (GIT.GetBranch(cwd), short_sha)
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000278
279 @staticmethod
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000280 def GetCheckoutRoot(path):
281 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000282 """
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000283 root = GIT.Capture(['rev-parse', '--show-cdup'], path)[0].strip()
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000284 return os.path.abspath(os.path.join(path, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000285
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000286 @staticmethod
287 def AssertVersion(min_version):
288 """Asserts git's version is at least min_version."""
289 def only_int(val):
290 if val.isdigit():
291 return int(val)
292 else:
293 return 0
294 current_version = GIT.Capture(['--version'])[0].split()[-1]
295 current_version_list = map(only_int, current_version.split('.'))
296 for min_ver in map(int, min_version.split('.')):
297 ver = current_version_list.pop(0)
298 if ver < min_ver:
299 return (False, current_version)
300 elif ver > min_ver:
301 return (True, current_version)
302 return (True, current_version)
303
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000304
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000305class SVN(object):
tony@chromium.org57564662010-04-14 02:35:12 +0000306 current_version = None
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000307
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000308 @staticmethod
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000309 def Capture(args, in_directory=None, print_error=True):
310 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000311
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000312 Args:
313 args: A sequence of command line parameters to be passed to svn.
314 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000315
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000316 Returns:
317 The output sent to stdout as a string.
318 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000319 stderr = None
320 if not print_error:
321 stderr = subprocess.PIPE
maruel@chromium.org17d01792010-09-01 18:07:10 +0000322 return gclient_utils.Popen(['svn'] + args, cwd=in_directory,
323 stdout=subprocess.PIPE, stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000324
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000325 @staticmethod
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000326 def RunAndGetFileList(verbose, args, cwd, file_list, stdout=None):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000327 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000328
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000329 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000330
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000331 svn's stdout is parsed to collect a list of files checked out or updated.
332 These files are appended to file_list. svn's stdout is also printed to
333 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000334
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000335 Args:
maruel@chromium.org03807072010-08-16 17:18:44 +0000336 verbose: If True, uses verbose output
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000337 args: A sequence of command line parameters to be passed to svn.
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000338 cwd: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000339
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000340 Raises:
341 Error: An error occurred while running the svn command.
342 """
maruel@chromium.org2b9aa8e2010-08-25 20:01:42 +0000343 stdout = stdout or sys.stdout
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000344
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000345 # svn update and svn checkout use the same pattern: the first three columns
346 # are for file status, property status, and lock status. This is followed
347 # by two spaces, and then the path to the file.
348 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000349
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000350 # The first three columns of svn status are the same as for svn update and
351 # svn checkout. The next three columns indicate addition-with-history,
352 # switch, and remote lock status. This is followed by one space, and then
353 # the path to the file.
354 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000355
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000356 # args[0] must be a supported command. This will blow up if it's something
357 # else, which is good. Note that the patterns are only effective when
358 # these commands are used in their ordinary forms, the patterns are invalid
359 # for "svn status --show-updates", for example.
360 pattern = {
361 'checkout': update_pattern,
362 'status': status_pattern,
363 'update': update_pattern,
364 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000365 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000366 # Place an upper limit.
cbentzel@chromium.org2aee2292010-09-03 14:15:25 +0000367 backoff_time = 5
maruel@chromium.orgfd876172010-04-30 14:01:05 +0000368 for _ in range(10):
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000369 previous_list_len = len(file_list)
370 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000371
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000372 def CaptureMatchingLines(line):
373 match = compiled_pattern.search(line)
374 if match:
375 file_list.append(match.group(1))
376 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000377 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000378
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000379 try:
maruel@chromium.org17d01792010-09-01 18:07:10 +0000380 gclient_utils.CheckCallAndFilterAndHeader(
381 ['svn'] + args,
382 cwd=cwd,
383 always=verbose,
384 filter_fn=CaptureMatchingLines,
385 stdout=stdout)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000386 except gclient_utils.Error:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000387 def IsKnownFailure():
388 for x in failure:
389 if (x.startswith('svn: OPTIONS of') or
390 x.startswith('svn: PROPFIND of') or
391 x.startswith('svn: REPORT of') or
maruel@chromium.orgf61fc932010-08-19 13:05:24 +0000392 x.startswith('svn: Unknown hostname') or
393 x.startswith('svn: Server sent unexpected return value')):
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000394 return True
395 return False
396
maruel@chromium.org953586a2010-06-15 14:22:24 +0000397 # Subversion client is really misbehaving with Google Code.
398 if args[0] == 'checkout':
399 # Ensure at least one file was checked out, otherwise *delete* the
400 # directory.
401 if len(file_list) == previous_list_len:
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000402 if not IsKnownFailure():
maruel@chromium.org953586a2010-06-15 14:22:24 +0000403 # No known svn error was found, bail out.
404 raise
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000405 # No file were checked out, so make sure the directory is
406 # deleted in case it's messed up and try again.
407 # Warning: It's bad, it assumes args[2] is the directory
408 # argument.
409 if os.path.isdir(args[2]):
410 gclient_utils.RemoveDirectory(args[2])
maruel@chromium.org953586a2010-06-15 14:22:24 +0000411 else:
412 # Progress was made, convert to update since an aborted checkout
413 # is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000414 args = ['update'] + args[1:]
maruel@chromium.org953586a2010-06-15 14:22:24 +0000415 else:
416 # It was an update or export.
maruel@chromium.org6133c5b2010-08-18 18:34:48 +0000417 # We enforce that some progress has been made or a known failure.
418 if len(file_list) == previous_list_len and not IsKnownFailure():
419 # No known svn error was found and no progress, bail out.
420 raise
cbentzel@chromium.org2aee2292010-09-03 14:15:25 +0000421 print "Sleeping %.1f seconds and retrying...." % backoff_time
422 time.sleep(backoff_time)
423 backoff_time *= 1.3
maruel@chromium.org953586a2010-06-15 14:22:24 +0000424 continue
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000425 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000426
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000427 @staticmethod
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000428 def CaptureInfo(relpath, in_directory=None, print_error=True):
429 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000430
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000431 Args:
432 relpath: The directory where the working copy resides relative to
433 the directory given by in_directory.
434 in_directory: The directory where svn is to be run.
435 """
436 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
437 dom = gclient_utils.ParseXML(output)
438 result = {}
439 if dom:
440 GetNamedNodeText = gclient_utils.GetNamedNodeText
441 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
442 def C(item, f):
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000443 if item is not None:
444 return f(item)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000445 # /info/entry/
446 # url
447 # reposityory/(root|uuid)
448 # wc-info/(schedule|depth)
449 # commit/(author|date)
450 # str() the results because they may be returned as Unicode, which
451 # interferes with the higher layers matching up things in the deps
452 # dictionary.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000453 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
454 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
455 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
456 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
457 'revision'),
458 int)
459 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
460 str)
461 # Differs across versions.
462 if result['Node Kind'] == 'dir':
463 result['Node Kind'] = 'directory'
464 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
465 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
466 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
467 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
468 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000469
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000470 @staticmethod
471 def CaptureHeadRevision(url):
472 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000473
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000474 Returns:
475 Int head revision
476 """
477 info = SVN.Capture(["info", "--xml", url], os.getcwd())
478 dom = xml.dom.minidom.parseString(info)
479 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000480
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000481 @staticmethod
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000482 def CaptureBaseRevision(cwd):
483 """Get the base revision of a SVN repository.
484
485 Returns:
486 Int base revision
487 """
488 info = SVN.Capture(["info", "--xml"], cwd)
489 dom = xml.dom.minidom.parseString(info)
490 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
491
492 @staticmethod
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000493 def CaptureStatus(files):
494 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000495
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000496 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000497
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000498 Returns an array of (status, file) tuples."""
499 command = ["status", "--xml"]
500 if not files:
501 pass
502 elif isinstance(files, basestring):
503 command.append(files)
504 else:
505 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000506
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000507 status_letter = {
508 None: ' ',
509 '': ' ',
510 'added': 'A',
511 'conflicted': 'C',
512 'deleted': 'D',
513 'external': 'X',
514 'ignored': 'I',
515 'incomplete': '!',
516 'merged': 'G',
517 'missing': '!',
518 'modified': 'M',
519 'none': ' ',
520 'normal': ' ',
521 'obstructed': '~',
522 'replaced': 'R',
523 'unversioned': '?',
524 }
525 dom = gclient_utils.ParseXML(SVN.Capture(command))
526 results = []
527 if dom:
528 # /status/target/entry/(wc-status|commit|author|date)
529 for target in dom.getElementsByTagName('target'):
530 #base_path = target.getAttribute('path')
531 for entry in target.getElementsByTagName('entry'):
532 file_path = entry.getAttribute('path')
533 wc_status = entry.getElementsByTagName('wc-status')
534 assert len(wc_status) == 1
535 # Emulate svn 1.5 status ouput...
536 statuses = [' '] * 7
537 # Col 0
538 xml_item_status = wc_status[0].getAttribute('item')
539 if xml_item_status in status_letter:
540 statuses[0] = status_letter[xml_item_status]
541 else:
542 raise Exception('Unknown item status "%s"; please implement me!' %
543 xml_item_status)
544 # Col 1
545 xml_props_status = wc_status[0].getAttribute('props')
546 if xml_props_status == 'modified':
547 statuses[1] = 'M'
548 elif xml_props_status == 'conflicted':
549 statuses[1] = 'C'
550 elif (not xml_props_status or xml_props_status == 'none' or
551 xml_props_status == 'normal'):
552 pass
553 else:
554 raise Exception('Unknown props status "%s"; please implement me!' %
555 xml_props_status)
556 # Col 2
557 if wc_status[0].getAttribute('wc-locked') == 'true':
558 statuses[2] = 'L'
559 # Col 3
560 if wc_status[0].getAttribute('copied') == 'true':
561 statuses[3] = '+'
562 # Col 4
563 if wc_status[0].getAttribute('switched') == 'true':
564 statuses[4] = 'S'
565 # TODO(maruel): Col 5 and 6
566 item = (''.join(statuses), file_path)
567 results.append(item)
568 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000569
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000570 @staticmethod
571 def IsMoved(filename):
572 """Determine if a file has been added through svn mv"""
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000573 return SVN.IsMovedInfo(SVN.CaptureInfo(filename))
574
575 @staticmethod
576 def IsMovedInfo(info):
577 """Determine if a file has been added through svn mv"""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000578 return (info.get('Copied From URL') and
579 info.get('Copied From Rev') and
580 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000581
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000582 @staticmethod
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000583 def GetFileProperty(filename, property_name):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000584 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000585
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000586 Args:
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000587 filename: The file to check
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000588 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000589
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000590 Returns:
591 The value of the property, which will be the empty string if the property
592 is not set on the file. If the file is not under version control, the
593 empty string is also returned.
594 """
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000595 output = SVN.Capture(["propget", property_name, filename])
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000596 if (output.startswith("svn: ") and
597 output.endswith("is not under version control")):
598 return ""
599 else:
600 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000601
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000602 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000603 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000604 """Diffs a single file.
605
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000606 Should be simple, eh? No it isn't.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000607 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000608 expected relative path.
609 full_move means that move or copy operations should completely recreate the
610 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000611 # If the user specified a custom diff command in their svn config file,
612 # then it'll be used when we do svn diff, which we don't want to happen
613 # since we want the unified diff. Using --diff-cmd=diff doesn't always
614 # work, since they can have another diff executable in their path that
615 # gives different line endings. So we use a bogus temp directory as the
616 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000617 bogus_dir = tempfile.mkdtemp()
618 try:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000619 # Use "svn info" output instead of os.path.isdir because the latter fails
620 # when the file is deleted.
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000621 return SVN._DiffItemInternal(filename, SVN.CaptureInfo(filename),
622 bogus_dir,
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000623 full_move=full_move, revision=revision)
624 finally:
625 shutil.rmtree(bogus_dir)
626
627 @staticmethod
628 def _DiffItemInternal(filename, info, bogus_dir, full_move=False,
629 revision=None):
630 """Grabs the diff data."""
631 command = ["diff", "--config-dir", bogus_dir, filename]
632 if revision:
633 command.extend(['--revision', revision])
634 data = None
635 if SVN.IsMovedInfo(info):
636 if full_move:
637 if info.get("Node Kind") == "directory":
638 # Things become tricky here. It's a directory copy/move. We need to
639 # diff all the files inside it.
640 # This will put a lot of pressure on the heap. This is why StringIO
641 # is used and converted back into a string at the end. The reason to
642 # return a string instead of a StringIO is that StringIO.write()
643 # doesn't accept a StringIO object. *sigh*.
644 for (dirpath, dirnames, filenames) in os.walk(filename):
645 # Cleanup all files starting with a '.'.
646 for d in dirnames:
647 if d.startswith('.'):
648 dirnames.remove(d)
649 for f in filenames:
650 if f.startswith('.'):
651 filenames.remove(f)
652 for f in filenames:
653 if data is None:
654 data = cStringIO.StringIO()
655 data.write(GenFakeDiff(os.path.join(dirpath, f)))
656 if data:
657 tmp = data.getvalue()
658 data.close()
659 data = tmp
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000660 else:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000661 data = GenFakeDiff(filename)
662 else:
663 if info.get("Node Kind") != "directory":
maruel@chromium.org0836c562010-01-22 01:10:06 +0000664 # svn diff on a mv/cp'd file outputs nothing if there was no change.
665 data = SVN.Capture(command, None)
666 if not data:
667 # We put in an empty Index entry so upload.py knows about them.
maruel@chromium.orgc6d170e2010-06-03 00:06:00 +0000668 data = "Index: %s\n" % filename.replace(os.sep, '/')
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000669 # Otherwise silently ignore directories.
670 else:
671 if info.get("Node Kind") != "directory":
672 # Normal simple case.
maruel@chromium.org0836c562010-01-22 01:10:06 +0000673 data = SVN.Capture(command, None)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000674 # Otherwise silently ignore directories.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000675 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000676
677 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000678 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000679 """Returns a string containing the diff for the given file list.
680
681 The files in the list should either be absolute paths or relative to the
682 given root. If no root directory is provided, the repository root will be
683 used.
684 The diff will always use relative paths.
685 """
686 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000687 root = root or SVN.GetCheckoutRoot(previous_cwd)
688 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000689 def RelativePath(path, root):
690 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000691 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000692 return path[len(root):]
693 return path
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000694 # If the user specified a custom diff command in their svn config file,
695 # then it'll be used when we do svn diff, which we don't want to happen
696 # since we want the unified diff. Using --diff-cmd=diff doesn't always
697 # work, since they can have another diff executable in their path that
698 # gives different line endings. So we use a bogus temp directory as the
699 # config directory, which gets around these problems.
700 bogus_dir = tempfile.mkdtemp()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000701 try:
702 os.chdir(root)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000703 # Cleanup filenames
704 filenames = [RelativePath(f, root) for f in filenames]
705 # Get information about the modified items (files and directories)
706 data = dict([(f, SVN.CaptureInfo(f)) for f in filenames])
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000707 diffs = []
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000708 if full_move:
709 # Eliminate modified files inside moved/copied directory.
710 for (filename, info) in data.iteritems():
711 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
712 # Remove files inside the directory.
713 filenames = [f for f in filenames
714 if not f.startswith(filename + os.path.sep)]
715 for filename in data.keys():
716 if not filename in filenames:
717 # Remove filtered out items.
718 del data[filename]
gavinp@google.com3fda4cc2010-06-29 13:29:27 +0000719 else:
720 metaheaders = []
721 for (filename, info) in data.iteritems():
722 if SVN.IsMovedInfo(info):
723 # for now, the most common case is a head copy,
724 # so let's just encode that as a straight up cp.
725 srcurl = info.get('Copied From URL')
726 root = info.get('Repository Root')
727 rev = int(info.get('Copied From Rev'))
728 assert srcurl.startswith(root)
729 src = srcurl[len(root)+1:]
730 srcinfo = SVN.CaptureInfo(srcurl)
731 if (srcinfo.get('Revision') != rev and
732 SVN.Capture(['diff', '-r', '%d:head' % rev, srcurl])):
733 metaheaders.append("#$ svn cp -r %d %s %s "
734 "### WARNING: note non-trunk copy\n" %
735 (rev, src, filename))
736 else:
737 metaheaders.append("#$ cp %s %s\n" % (src,
738 filename))
739
740 if metaheaders:
741 diffs.append("### BEGIN SVN COPY METADATA\n")
742 diffs.extend(metaheaders)
743 diffs.append("### END SVN COPY METADATA\n")
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000744 # Now ready to do the actual diff.
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000745 for filename in sorted(data.iterkeys()):
746 diffs.append(SVN._DiffItemInternal(filename, data[filename], bogus_dir,
747 full_move=full_move,
748 revision=revision))
749 # Use StringIO since it can be messy when diffing a directory move with
750 # full_move=True.
751 buf = cStringIO.StringIO()
752 for d in filter(None, diffs):
753 buf.write(d)
754 result = buf.getvalue()
755 buf.close()
756 return result
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000757 finally:
758 os.chdir(previous_cwd)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000759 shutil.rmtree(bogus_dir)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000760
761 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000762 def GetEmail(repo_root):
763 """Retrieves the svn account which we assume is an email address."""
764 infos = SVN.CaptureInfo(repo_root)
765 uuid = infos.get('UUID')
766 root = infos.get('Repository Root')
767 if not root:
768 return None
769
770 # Should check for uuid but it is incorrectly saved for https creds.
771 realm = root.rsplit('/', 1)[0]
772 if root.startswith('https') or not uuid:
773 regexp = re.compile(r'<%s:\d+>.*' % realm)
774 else:
775 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
776 if regexp is None:
777 return None
778 if sys.platform.startswith('win'):
779 if not 'APPDATA' in os.environ:
780 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000781 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
782 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000783 else:
784 if not 'HOME' in os.environ:
785 return None
786 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
787 'svn.simple')
788 for credfile in os.listdir(auth_dir):
789 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
790 if regexp.match(cred_info.get('svn:realmstring')):
791 return cred_info.get('username')
792
793 @staticmethod
794 def ReadSimpleAuth(filename):
795 f = open(filename, 'r')
796 values = {}
maruel@chromium.org6e29d572010-06-04 17:32:20 +0000797 def ReadOneItem(item_type):
798 m = re.match(r'%s (\d+)' % item_type, f.readline())
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000799 if not m:
800 return None
801 data = f.read(int(m.group(1)))
802 if f.read(1) != '\n':
803 return None
804 return data
805
806 while True:
807 key = ReadOneItem('K')
808 if not key:
809 break
810 value = ReadOneItem('V')
811 if not value:
812 break
813 values[key] = value
814 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000815
816 @staticmethod
817 def GetCheckoutRoot(directory):
818 """Returns the top level directory of the current repository.
819
820 The directory is returned as an absolute path.
821 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000822 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000823 infos = SVN.CaptureInfo(directory, print_error=False)
824 cur_dir_repo_root = infos.get("Repository Root")
825 if not cur_dir_repo_root:
826 return None
827
828 while True:
829 parent = os.path.dirname(directory)
830 if (SVN.CaptureInfo(parent, print_error=False).get(
831 "Repository Root") != cur_dir_repo_root):
832 break
833 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000834 return GetCasedPath(directory)
tony@chromium.org57564662010-04-14 02:35:12 +0000835
836 @staticmethod
837 def AssertVersion(min_version):
838 """Asserts svn's version is at least min_version."""
839 def only_int(val):
840 if val.isdigit():
841 return int(val)
842 else:
843 return 0
844 if not SVN.current_version:
845 SVN.current_version = SVN.Capture(['--version']).split()[2]
846 current_version_list = map(only_int, SVN.current_version.split('.'))
847 for min_ver in map(int, min_version.split('.')):
848 ver = current_version_list.pop(0)
849 if ver < min_ver:
850 return (False, SVN.current_version)
851 elif ver > min_ver:
852 return (True, SVN.current_version)
853 return (True, SVN.current_version)