blob: ac3f03fce0d5e2f8d296f3cd49ed4a963866fcc6 [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):
21 return (re.match(r"^[a-zA-Z0-9._%-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", email)
22 is not None)
23
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)
48 nb_lines = len(file_content)
49 # We need to use / since patch on unix will fail otherwise.
50 data = cStringIO.StringIO()
51 data.write("Index: %s\n" % filename)
52 data.write('=' * 67 + '\n')
53 # Note: Should we use /dev/null instead?
54 data.write("--- %s\n" % filename)
55 data.write("+++ %s\n" % filename)
56 data.write("@@ -0,0 +1,%d @@\n" % nb_lines)
57 # Prepend '+' to every lines.
58 for line in file_content:
59 data.write('+')
60 data.write(line)
61 result = data.getvalue()
62 data.close()
63 return result
64
65
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000066class GIT(object):
67 COMMAND = "git"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000068
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000069 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000070 def Capture(args, in_directory=None, print_error=True, error_ok=False):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000071 """Runs git, capturing output sent to stdout as a string.
72
73 Args:
74 args: A sequence of command line parameters to be passed to git.
75 in_directory: The directory where git is to be run.
76
77 Returns:
78 The output sent to stdout as a string.
79 """
80 c = [GIT.COMMAND]
81 c.extend(args)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000082 try:
83 return gclient_utils.CheckCall(c, in_directory, print_error)
84 except gclient_utils.CheckCallError:
85 if error_ok:
nasser@codeaurora.orgcd968c12010-02-01 06:05:00 +000086 return ('', '')
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000087 raise
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000088
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000089 @staticmethod
90 def CaptureStatus(files, upstream_branch='origin'):
91 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000092
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000093 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000094
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000095 Returns an array of (status, file) tuples."""
bauerb@chromium.org14ec5042010-03-30 18:19:09 +000096 command = ["diff", "--name-status", "-r", "%s..." % upstream_branch]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000097 if not files:
98 pass
99 elif isinstance(files, basestring):
100 command.append(files)
101 else:
102 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000103
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000104 status = GIT.Capture(command)[0].rstrip()
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000105 results = []
106 if status:
107 for statusline in status.split('\n'):
108 m = re.match('^(\w)\t(.+)$', statusline)
109 if not m:
110 raise Exception("status currently unsupported: %s" % statusline)
111 results.append(('%s ' % m.group(1), m.group(2)))
112 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000113
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000114 @staticmethod
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000115 def RunAndFilterOutput(args,
116 in_directory,
117 print_messages,
118 print_stdout,
119 filter):
120 """Runs a command, optionally outputting to stdout.
121
122 stdout is passed line-by-line to the given filter function. If
123 print_stdout is true, it is also printed to sys.stdout as in Run.
124
125 Args:
126 args: A sequence of command line parameters to be passed.
msb@chromium.orgd6504212010-01-13 17:34:31 +0000127 in_directory: The directory where git is to be run.
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000128 print_messages: Whether to print status messages to stdout about
129 which commands are being run.
130 print_stdout: Whether to forward program's output to stdout.
131 filter: A function taking one argument (a string) which will be
132 passed each line (with the ending newline character removed) of
133 program's output for filtering.
134
135 Raises:
136 gclient_utils.Error: An error occurred while running the command.
137 """
138 command = [GIT.COMMAND]
139 command.extend(args)
140 gclient_utils.SubprocessCallAndFilter(command,
141 in_directory,
142 print_messages,
143 print_stdout,
144 filter=filter)
145
146 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000147 def GetEmail(repo_root):
148 """Retrieves the user email address if known."""
149 # We could want to look at the svn cred when it has a svn remote but it
150 # should be fine for now, users should simply configure their git settings.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000151 return GIT.Capture(['config', 'user.email'],
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000152 repo_root, error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000153
154 @staticmethod
155 def ShortBranchName(branch):
156 """Converts a name like 'refs/heads/foo' to just 'foo'."""
157 return branch.replace('refs/heads/', '')
158
159 @staticmethod
160 def GetBranchRef(cwd):
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000161 """Returns the full branch reference, e.g. 'refs/heads/master'."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000162 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000163
164 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000165 def GetBranch(cwd):
166 """Returns the short branch name, e.g. 'master'."""
maruel@chromium.orgc308a742009-12-22 18:29:33 +0000167 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000168
169 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000170 def IsGitSvn(cwd):
171 """Returns true if this repo looks like it's using git-svn."""
172 # If you have any "svn-remote.*" config keys, we think you're using svn.
173 try:
174 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd)
175 return True
176 except gclient_utils.CheckCallError:
177 return False
178
179 @staticmethod
180 def GetSVNBranch(cwd):
181 """Returns the svn branch name if found."""
182 # Try to figure out which remote branch we're based on.
183 # Strategy:
184 # 1) find all git-svn branches and note their svn URLs.
185 # 2) iterate through our branch history and match up the URLs.
186
187 # regexp matching the git-svn line that contains the URL.
188 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
189
190 # Get the refname and svn url for all refs/remotes/*.
191 remotes = GIT.Capture(
192 ['for-each-ref', '--format=%(refname)', 'refs/remotes'],
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000193 cwd)[0].splitlines()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000194 svn_refs = {}
195 for ref in remotes:
196 match = git_svn_re.search(
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000197 GIT.Capture(['cat-file', '-p', ref], cwd)[0])
sky@chromium.org42d8da52010-04-23 18:25:07 +0000198 # Prefer origin/HEAD over all others.
199 if match and (match.group(1) not in svn_refs or
200 ref == "refs/remotes/origin/HEAD"):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000201 svn_refs[match.group(1)] = ref
202
203 svn_branch = ''
204 if len(svn_refs) == 1:
205 # Only one svn branch exists -- seems like a good candidate.
206 svn_branch = svn_refs.values()[0]
207 elif len(svn_refs) > 1:
208 # We have more than one remote branch available. We don't
209 # want to go through all of history, so read a line from the
210 # pipe at a time.
211 # The -100 is an arbitrary limit so we don't search forever.
212 cmd = ['git', 'log', '-100', '--pretty=medium']
213 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd)
214 for line in proc.stdout:
215 match = git_svn_re.match(line)
216 if match:
217 url = match.group(1)
218 if url in svn_refs:
219 svn_branch = svn_refs[url]
220 proc.stdout.close() # Cut pipe.
221 break
222 return svn_branch
223
224 @staticmethod
225 def FetchUpstreamTuple(cwd):
226 """Returns a tuple containg remote and remote ref,
227 e.g. 'origin', 'refs/heads/master'
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000228 Tries to be intelligent and understand git-svn.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000229 """
230 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000231 branch = GIT.GetBranch(cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000232 upstream_branch = None
233 upstream_branch = GIT.Capture(
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000234 ['config', 'branch.%s.merge' % branch], in_directory=cwd,
235 error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000236 if upstream_branch:
237 remote = GIT.Capture(
238 ['config', 'branch.%s.remote' % branch],
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000239 in_directory=cwd, error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000240 else:
241 # Fall back on trying a git-svn upstream branch.
242 if GIT.IsGitSvn(cwd):
243 upstream_branch = GIT.GetSVNBranch(cwd)
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000244 else:
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000245 # Else, try to guess the origin remote.
246 remote_branches = GIT.Capture(
247 ['branch', '-r'], in_directory=cwd)[0].split()
248 if 'origin/master' in remote_branches:
249 # Fall back on origin/master if it exits.
250 remote = 'origin'
251 upstream_branch = 'refs/heads/master'
252 elif 'origin/trunk' in remote_branches:
253 # Fall back on origin/trunk if it exists. Generally a shared
254 # git-svn clone
255 remote = 'origin'
256 upstream_branch = 'refs/heads/trunk'
257 else:
258 # Give up.
259 remote = None
260 upstream_branch = None
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000261 return remote, upstream_branch
262
263 @staticmethod
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000264 def GetUpstreamBranch(cwd):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000265 """Gets the current branch's upstream branch."""
266 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
maruel@chromium.orga630bd72010-04-29 23:32:34 +0000267 if remote != '.' and upstream_branch:
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000268 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
269 return upstream_branch
270
271 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000272 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
273 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000274 """Diffs against the upstream branch or optionally another branch.
275
276 full_move means that move or copy operations should completely recreate the
277 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000278 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000279 branch = GIT.GetUpstreamBranch(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000280 command = ['diff', '-p', '--no-prefix', branch + "..." + branch_head]
maruel@chromium.orga9371762009-12-22 18:27:38 +0000281 if not full_move:
282 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000283 # TODO(maruel): --binary support.
284 if files:
285 command.append('--')
286 command.extend(files)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000287 diff = GIT.Capture(command, cwd)[0].splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000288 for i in range(len(diff)):
289 # In the case of added files, replace /dev/null with the path to the
290 # file being added.
291 if diff[i].startswith('--- /dev/null'):
292 diff[i] = '--- %s' % diff[i+1][4:]
293 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000294
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000295 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000296 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
297 """Returns the list of modified files between two branches."""
298 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000299 branch = GIT.GetUpstreamBranch(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000300 command = ['diff', '--name-only', branch + "..." + branch_head]
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000301 return GIT.Capture(command, cwd)[0].splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000302
303 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000304 def GetPatchName(cwd):
305 """Constructs a name for this patch."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000306 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd)[0].strip()
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000307 return "%s-%s" % (GIT.GetBranch(cwd), short_sha)
308
309 @staticmethod
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000310 def GetCheckoutRoot(path):
311 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000312 """
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000313 root = GIT.Capture(['rev-parse', '--show-cdup'], path)[0].strip()
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000314 return os.path.abspath(os.path.join(path, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000315
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000316 @staticmethod
317 def AssertVersion(min_version):
318 """Asserts git's version is at least min_version."""
319 def only_int(val):
320 if val.isdigit():
321 return int(val)
322 else:
323 return 0
324 current_version = GIT.Capture(['--version'])[0].split()[-1]
325 current_version_list = map(only_int, current_version.split('.'))
326 for min_ver in map(int, min_version.split('.')):
327 ver = current_version_list.pop(0)
328 if ver < min_ver:
329 return (False, current_version)
330 elif ver > min_ver:
331 return (True, current_version)
332 return (True, current_version)
333
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000334
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000335class SVN(object):
336 COMMAND = "svn"
tony@chromium.org57564662010-04-14 02:35:12 +0000337 current_version = None
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000338
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000339 @staticmethod
340 def Run(args, in_directory):
341 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000342
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000343 Args:
344 args: A sequence of command line parameters to be passed to svn.
345 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000346
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000347 Raises:
348 Error: An error occurred while running the svn command.
349 """
350 c = [SVN.COMMAND]
351 c.extend(args)
maruel@chromium.org2185f002009-12-18 21:03:47 +0000352 # TODO(maruel): This is very gclient-specific.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000353 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000354
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000355 @staticmethod
356 def Capture(args, in_directory=None, print_error=True):
357 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000358
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000359 Args:
360 args: A sequence of command line parameters to be passed to svn.
361 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000362
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000363 Returns:
364 The output sent to stdout as a string.
365 """
366 c = [SVN.COMMAND]
367 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000368
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000369 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
370 # the svn.exe executable, but shell=True makes subprocess on Linux fail
371 # when it's called with a list because it only tries to execute the
372 # first string ("svn").
373 stderr = None
374 if not print_error:
375 stderr = subprocess.PIPE
376 return subprocess.Popen(c,
377 cwd=in_directory,
378 shell=(sys.platform == 'win32'),
379 stdout=subprocess.PIPE,
380 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000381
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000382 @staticmethod
383 def RunAndGetFileList(options, args, in_directory, file_list):
384 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000385
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000386 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000387
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000388 svn's stdout is parsed to collect a list of files checked out or updated.
389 These files are appended to file_list. svn's stdout is also printed to
390 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000391
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000392 Args:
393 options: command line options to gclient
394 args: A sequence of command line parameters to be passed to svn.
395 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000396
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000397 Raises:
398 Error: An error occurred while running the svn command.
399 """
400 command = [SVN.COMMAND]
401 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000402
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000403 # svn update and svn checkout use the same pattern: the first three columns
404 # are for file status, property status, and lock status. This is followed
405 # by two spaces, and then the path to the file.
406 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000407
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000408 # The first three columns of svn status are the same as for svn update and
409 # svn checkout. The next three columns indicate addition-with-history,
410 # switch, and remote lock status. This is followed by one space, and then
411 # the path to the file.
412 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000413
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000414 # args[0] must be a supported command. This will blow up if it's something
415 # else, which is good. Note that the patterns are only effective when
416 # these commands are used in their ordinary forms, the patterns are invalid
417 # for "svn status --show-updates", for example.
418 pattern = {
419 'checkout': update_pattern,
420 'status': status_pattern,
421 'update': update_pattern,
422 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000423 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000424 # Place an upper limit.
maruel@chromium.orgfd876172010-04-30 14:01:05 +0000425 for _ in range(10):
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000426 previous_list_len = len(file_list)
427 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000428
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000429 def CaptureMatchingLines(line):
430 match = compiled_pattern.search(line)
431 if match:
432 file_list.append(match.group(1))
433 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000434 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000435
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000436 try:
437 SVN.RunAndFilterOutput(args,
438 in_directory,
439 options.verbose,
440 True,
441 CaptureMatchingLines)
442 except gclient_utils.Error:
maruel@chromium.org2de10252010-02-08 01:10:39 +0000443 # We enforce that some progress has been made or HTTP 502.
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000444 if (filter(lambda x: '502 Bad Gateway' in x, failure) or
maruel@chromium.org2de10252010-02-08 01:10:39 +0000445 (len(failure) and len(file_list) > previous_list_len)):
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000446 if args[0] == 'checkout':
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000447 # An aborted checkout is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000448 args = ['update'] + args[1:]
maruel@chromium.orgfd876172010-04-30 14:01:05 +0000449 print "Sleeping 15 seconds and retrying...."
450 time.sleep(15)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000451 continue
maruel@chromium.org2de10252010-02-08 01:10:39 +0000452 # No progress was made or an unknown error we aren't sure, bail out.
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000453 raise
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000454 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000455
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000456 @staticmethod
457 def RunAndFilterOutput(args,
458 in_directory,
459 print_messages,
460 print_stdout,
461 filter):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000462 """Runs a command, optionally outputting to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000463
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000464 stdout is passed line-by-line to the given filter function. If
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000465 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000466
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000467 Args:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000468 args: A sequence of command line parameters to be passed.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000469 in_directory: The directory where svn is to be run.
470 print_messages: Whether to print status messages to stdout about
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000471 which commands are being run.
472 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000473 filter: A function taking one argument (a string) which will be
474 passed each line (with the ending newline character removed) of
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000475 program's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000476
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000477 Raises:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000478 gclient_utils.Error: An error occurred while running the command.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000479 """
480 command = [SVN.COMMAND]
481 command.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000482 gclient_utils.SubprocessCallAndFilter(command,
483 in_directory,
484 print_messages,
485 print_stdout,
486 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000487
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000488 @staticmethod
489 def CaptureInfo(relpath, in_directory=None, print_error=True):
490 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000491
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000492 Args:
493 relpath: The directory where the working copy resides relative to
494 the directory given by in_directory.
495 in_directory: The directory where svn is to be run.
496 """
497 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
498 dom = gclient_utils.ParseXML(output)
499 result = {}
500 if dom:
501 GetNamedNodeText = gclient_utils.GetNamedNodeText
502 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
503 def C(item, f):
504 if item is not None: return f(item)
505 # /info/entry/
506 # url
507 # reposityory/(root|uuid)
508 # wc-info/(schedule|depth)
509 # commit/(author|date)
510 # str() the results because they may be returned as Unicode, which
511 # interferes with the higher layers matching up things in the deps
512 # dictionary.
513 # TODO(maruel): Fix at higher level instead (!)
514 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
515 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
516 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
517 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
518 'revision'),
519 int)
520 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
521 str)
522 # Differs across versions.
523 if result['Node Kind'] == 'dir':
524 result['Node Kind'] = 'directory'
525 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
526 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
527 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
528 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
529 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000530
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000531 @staticmethod
532 def CaptureHeadRevision(url):
533 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000534
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000535 Returns:
536 Int head revision
537 """
538 info = SVN.Capture(["info", "--xml", url], os.getcwd())
539 dom = xml.dom.minidom.parseString(info)
540 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000541
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000542 @staticmethod
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000543 def CaptureBaseRevision(cwd):
544 """Get the base revision of a SVN repository.
545
546 Returns:
547 Int base revision
548 """
549 info = SVN.Capture(["info", "--xml"], cwd)
550 dom = xml.dom.minidom.parseString(info)
551 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
552
553 @staticmethod
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000554 def CaptureStatus(files):
555 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000556
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000557 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000558
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000559 Returns an array of (status, file) tuples."""
560 command = ["status", "--xml"]
561 if not files:
562 pass
563 elif isinstance(files, basestring):
564 command.append(files)
565 else:
566 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000567
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000568 status_letter = {
569 None: ' ',
570 '': ' ',
571 'added': 'A',
572 'conflicted': 'C',
573 'deleted': 'D',
574 'external': 'X',
575 'ignored': 'I',
576 'incomplete': '!',
577 'merged': 'G',
578 'missing': '!',
579 'modified': 'M',
580 'none': ' ',
581 'normal': ' ',
582 'obstructed': '~',
583 'replaced': 'R',
584 'unversioned': '?',
585 }
586 dom = gclient_utils.ParseXML(SVN.Capture(command))
587 results = []
588 if dom:
589 # /status/target/entry/(wc-status|commit|author|date)
590 for target in dom.getElementsByTagName('target'):
591 #base_path = target.getAttribute('path')
592 for entry in target.getElementsByTagName('entry'):
593 file_path = entry.getAttribute('path')
594 wc_status = entry.getElementsByTagName('wc-status')
595 assert len(wc_status) == 1
596 # Emulate svn 1.5 status ouput...
597 statuses = [' '] * 7
598 # Col 0
599 xml_item_status = wc_status[0].getAttribute('item')
600 if xml_item_status in status_letter:
601 statuses[0] = status_letter[xml_item_status]
602 else:
603 raise Exception('Unknown item status "%s"; please implement me!' %
604 xml_item_status)
605 # Col 1
606 xml_props_status = wc_status[0].getAttribute('props')
607 if xml_props_status == 'modified':
608 statuses[1] = 'M'
609 elif xml_props_status == 'conflicted':
610 statuses[1] = 'C'
611 elif (not xml_props_status or xml_props_status == 'none' or
612 xml_props_status == 'normal'):
613 pass
614 else:
615 raise Exception('Unknown props status "%s"; please implement me!' %
616 xml_props_status)
617 # Col 2
618 if wc_status[0].getAttribute('wc-locked') == 'true':
619 statuses[2] = 'L'
620 # Col 3
621 if wc_status[0].getAttribute('copied') == 'true':
622 statuses[3] = '+'
623 # Col 4
624 if wc_status[0].getAttribute('switched') == 'true':
625 statuses[4] = 'S'
626 # TODO(maruel): Col 5 and 6
627 item = (''.join(statuses), file_path)
628 results.append(item)
629 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000630
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000631 @staticmethod
632 def IsMoved(filename):
633 """Determine if a file has been added through svn mv"""
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000634 return SVN.IsMovedInfo(SVN.CaptureInfo(filename))
635
636 @staticmethod
637 def IsMovedInfo(info):
638 """Determine if a file has been added through svn mv"""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000639 return (info.get('Copied From URL') and
640 info.get('Copied From Rev') and
641 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000642
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000643 @staticmethod
644 def GetFileProperty(file, property_name):
645 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000646
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000647 Args:
648 file: The file to check
649 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000650
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000651 Returns:
652 The value of the property, which will be the empty string if the property
653 is not set on the file. If the file is not under version control, the
654 empty string is also returned.
655 """
656 output = SVN.Capture(["propget", property_name, file])
657 if (output.startswith("svn: ") and
658 output.endswith("is not under version control")):
659 return ""
660 else:
661 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000662
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000663 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000664 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000665 """Diffs a single file.
666
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000667 Should be simple, eh? No it isn't.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000668 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000669 expected relative path.
670 full_move means that move or copy operations should completely recreate the
671 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000672 # If the user specified a custom diff command in their svn config file,
673 # then it'll be used when we do svn diff, which we don't want to happen
674 # since we want the unified diff. Using --diff-cmd=diff doesn't always
675 # work, since they can have another diff executable in their path that
676 # gives different line endings. So we use a bogus temp directory as the
677 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000678 bogus_dir = tempfile.mkdtemp()
679 try:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000680 # Use "svn info" output instead of os.path.isdir because the latter fails
681 # when the file is deleted.
682 return SVN._DiffItemInternal(SVN.CaptureInfo(filename),
683 full_move=full_move, revision=revision)
684 finally:
685 shutil.rmtree(bogus_dir)
686
687 @staticmethod
688 def _DiffItemInternal(filename, info, bogus_dir, full_move=False,
689 revision=None):
690 """Grabs the diff data."""
691 command = ["diff", "--config-dir", bogus_dir, filename]
692 if revision:
693 command.extend(['--revision', revision])
694 data = None
695 if SVN.IsMovedInfo(info):
696 if full_move:
697 if info.get("Node Kind") == "directory":
698 # Things become tricky here. It's a directory copy/move. We need to
699 # diff all the files inside it.
700 # This will put a lot of pressure on the heap. This is why StringIO
701 # is used and converted back into a string at the end. The reason to
702 # return a string instead of a StringIO is that StringIO.write()
703 # doesn't accept a StringIO object. *sigh*.
704 for (dirpath, dirnames, filenames) in os.walk(filename):
705 # Cleanup all files starting with a '.'.
706 for d in dirnames:
707 if d.startswith('.'):
708 dirnames.remove(d)
709 for f in filenames:
710 if f.startswith('.'):
711 filenames.remove(f)
712 for f in filenames:
713 if data is None:
714 data = cStringIO.StringIO()
715 data.write(GenFakeDiff(os.path.join(dirpath, f)))
716 if data:
717 tmp = data.getvalue()
718 data.close()
719 data = tmp
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000720 else:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000721 data = GenFakeDiff(filename)
722 else:
723 if info.get("Node Kind") != "directory":
maruel@chromium.org0836c562010-01-22 01:10:06 +0000724 # svn diff on a mv/cp'd file outputs nothing if there was no change.
725 data = SVN.Capture(command, None)
726 if not data:
727 # We put in an empty Index entry so upload.py knows about them.
728 data = "Index: %s\n" % filename
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000729 # Otherwise silently ignore directories.
730 else:
731 if info.get("Node Kind") != "directory":
732 # Normal simple case.
maruel@chromium.org0836c562010-01-22 01:10:06 +0000733 data = SVN.Capture(command, None)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000734 # Otherwise silently ignore directories.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000735 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000736
737 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000738 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000739 """Returns a string containing the diff for the given file list.
740
741 The files in the list should either be absolute paths or relative to the
742 given root. If no root directory is provided, the repository root will be
743 used.
744 The diff will always use relative paths.
745 """
746 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000747 root = root or SVN.GetCheckoutRoot(previous_cwd)
748 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000749 def RelativePath(path, root):
750 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000751 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000752 return path[len(root):]
753 return path
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000754 # If the user specified a custom diff command in their svn config file,
755 # then it'll be used when we do svn diff, which we don't want to happen
756 # since we want the unified diff. Using --diff-cmd=diff doesn't always
757 # work, since they can have another diff executable in their path that
758 # gives different line endings. So we use a bogus temp directory as the
759 # config directory, which gets around these problems.
760 bogus_dir = tempfile.mkdtemp()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000761 try:
762 os.chdir(root)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000763 # Cleanup filenames
764 filenames = [RelativePath(f, root) for f in filenames]
765 # Get information about the modified items (files and directories)
766 data = dict([(f, SVN.CaptureInfo(f)) for f in filenames])
767 if full_move:
768 # Eliminate modified files inside moved/copied directory.
769 for (filename, info) in data.iteritems():
770 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
771 # Remove files inside the directory.
772 filenames = [f for f in filenames
773 if not f.startswith(filename + os.path.sep)]
774 for filename in data.keys():
775 if not filename in filenames:
776 # Remove filtered out items.
777 del data[filename]
778 # Now ready to do the actual diff.
779 diffs = []
780 for filename in sorted(data.iterkeys()):
781 diffs.append(SVN._DiffItemInternal(filename, data[filename], bogus_dir,
782 full_move=full_move,
783 revision=revision))
784 # Use StringIO since it can be messy when diffing a directory move with
785 # full_move=True.
786 buf = cStringIO.StringIO()
787 for d in filter(None, diffs):
788 buf.write(d)
789 result = buf.getvalue()
790 buf.close()
791 return result
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000792 finally:
793 os.chdir(previous_cwd)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000794 shutil.rmtree(bogus_dir)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000795
796 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000797 def GetEmail(repo_root):
798 """Retrieves the svn account which we assume is an email address."""
799 infos = SVN.CaptureInfo(repo_root)
800 uuid = infos.get('UUID')
801 root = infos.get('Repository Root')
802 if not root:
803 return None
804
805 # Should check for uuid but it is incorrectly saved for https creds.
806 realm = root.rsplit('/', 1)[0]
807 if root.startswith('https') or not uuid:
808 regexp = re.compile(r'<%s:\d+>.*' % realm)
809 else:
810 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
811 if regexp is None:
812 return None
813 if sys.platform.startswith('win'):
814 if not 'APPDATA' in os.environ:
815 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000816 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
817 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000818 else:
819 if not 'HOME' in os.environ:
820 return None
821 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
822 'svn.simple')
823 for credfile in os.listdir(auth_dir):
824 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
825 if regexp.match(cred_info.get('svn:realmstring')):
826 return cred_info.get('username')
827
828 @staticmethod
829 def ReadSimpleAuth(filename):
830 f = open(filename, 'r')
831 values = {}
832 def ReadOneItem(type):
833 m = re.match(r'%s (\d+)' % type, f.readline())
834 if not m:
835 return None
836 data = f.read(int(m.group(1)))
837 if f.read(1) != '\n':
838 return None
839 return data
840
841 while True:
842 key = ReadOneItem('K')
843 if not key:
844 break
845 value = ReadOneItem('V')
846 if not value:
847 break
848 values[key] = value
849 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000850
851 @staticmethod
852 def GetCheckoutRoot(directory):
853 """Returns the top level directory of the current repository.
854
855 The directory is returned as an absolute path.
856 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000857 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000858 infos = SVN.CaptureInfo(directory, print_error=False)
859 cur_dir_repo_root = infos.get("Repository Root")
860 if not cur_dir_repo_root:
861 return None
862
863 while True:
864 parent = os.path.dirname(directory)
865 if (SVN.CaptureInfo(parent, print_error=False).get(
866 "Repository Root") != cur_dir_repo_root):
867 break
868 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000869 return GetCasedPath(directory)
tony@chromium.org57564662010-04-14 02:35:12 +0000870
871 @staticmethod
872 def AssertVersion(min_version):
873 """Asserts svn's version is at least min_version."""
874 def only_int(val):
875 if val.isdigit():
876 return int(val)
877 else:
878 return 0
879 if not SVN.current_version:
880 SVN.current_version = SVN.Capture(['--version']).split()[2]
881 current_version_list = map(only_int, SVN.current_version.split('.'))
882 for min_ver in map(int, min_version.split('.')):
883 ver = current_version_list.pop(0)
884 if ver < min_ver:
885 return (False, SVN.current_version)
886 elif ver > min_ver:
887 return (True, SVN.current_version)
888 return (True, SVN.current_version)