blob: 1edaf800a46563c104baf12c06cd32594b5eff4c [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)
evan@chromium.org400f3e72010-05-19 14:23:36 +0000280 command = ['diff', '-p', '--no-prefix', '--no-ext-diff',
281 branch + "..." + branch_head]
maruel@chromium.orga9371762009-12-22 18:27:38 +0000282 if not full_move:
283 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000284 # TODO(maruel): --binary support.
285 if files:
286 command.append('--')
287 command.extend(files)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000288 diff = GIT.Capture(command, cwd)[0].splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000289 for i in range(len(diff)):
290 # In the case of added files, replace /dev/null with the path to the
291 # file being added.
292 if diff[i].startswith('--- /dev/null'):
293 diff[i] = '--- %s' % diff[i+1][4:]
294 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000295
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000296 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000297 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
298 """Returns the list of modified files between two branches."""
299 if not branch:
maruel@chromium.org81e012c2010-04-29 16:07:24 +0000300 branch = GIT.GetUpstreamBranch(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000301 command = ['diff', '--name-only', branch + "..." + branch_head]
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000302 return GIT.Capture(command, cwd)[0].splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000303
304 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000305 def GetPatchName(cwd):
306 """Constructs a name for this patch."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000307 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd)[0].strip()
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000308 return "%s-%s" % (GIT.GetBranch(cwd), short_sha)
309
310 @staticmethod
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000311 def GetCheckoutRoot(path):
312 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000313 """
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000314 root = GIT.Capture(['rev-parse', '--show-cdup'], path)[0].strip()
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000315 return os.path.abspath(os.path.join(path, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000316
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000317 @staticmethod
318 def AssertVersion(min_version):
319 """Asserts git's version is at least min_version."""
320 def only_int(val):
321 if val.isdigit():
322 return int(val)
323 else:
324 return 0
325 current_version = GIT.Capture(['--version'])[0].split()[-1]
326 current_version_list = map(only_int, current_version.split('.'))
327 for min_ver in map(int, min_version.split('.')):
328 ver = current_version_list.pop(0)
329 if ver < min_ver:
330 return (False, current_version)
331 elif ver > min_ver:
332 return (True, current_version)
333 return (True, current_version)
334
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000335
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000336class SVN(object):
337 COMMAND = "svn"
tony@chromium.org57564662010-04-14 02:35:12 +0000338 current_version = None
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000339
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000340 @staticmethod
341 def Run(args, in_directory):
342 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000343
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000344 Args:
345 args: A sequence of command line parameters to be passed to svn.
346 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000347
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000348 Raises:
349 Error: An error occurred while running the svn command.
350 """
351 c = [SVN.COMMAND]
352 c.extend(args)
maruel@chromium.org2185f002009-12-18 21:03:47 +0000353 # TODO(maruel): This is very gclient-specific.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000354 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000355
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000356 @staticmethod
357 def Capture(args, in_directory=None, print_error=True):
358 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000359
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000360 Args:
361 args: A sequence of command line parameters to be passed to svn.
362 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000363
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000364 Returns:
365 The output sent to stdout as a string.
366 """
367 c = [SVN.COMMAND]
368 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000369
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000370 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
371 # the svn.exe executable, but shell=True makes subprocess on Linux fail
372 # when it's called with a list because it only tries to execute the
373 # first string ("svn").
374 stderr = None
375 if not print_error:
376 stderr = subprocess.PIPE
377 return subprocess.Popen(c,
378 cwd=in_directory,
379 shell=(sys.platform == 'win32'),
380 stdout=subprocess.PIPE,
381 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000382
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000383 @staticmethod
384 def RunAndGetFileList(options, args, in_directory, file_list):
385 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000386
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000387 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000388
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000389 svn's stdout is parsed to collect a list of files checked out or updated.
390 These files are appended to file_list. svn's stdout is also printed to
391 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000392
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000393 Args:
394 options: command line options to gclient
395 args: A sequence of command line parameters to be passed to svn.
396 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000397
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000398 Raises:
399 Error: An error occurred while running the svn command.
400 """
401 command = [SVN.COMMAND]
402 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000403
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000404 # svn update and svn checkout use the same pattern: the first three columns
405 # are for file status, property status, and lock status. This is followed
406 # by two spaces, and then the path to the file.
407 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000408
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000409 # The first three columns of svn status are the same as for svn update and
410 # svn checkout. The next three columns indicate addition-with-history,
411 # switch, and remote lock status. This is followed by one space, and then
412 # the path to the file.
413 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000414
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000415 # args[0] must be a supported command. This will blow up if it's something
416 # else, which is good. Note that the patterns are only effective when
417 # these commands are used in their ordinary forms, the patterns are invalid
418 # for "svn status --show-updates", for example.
419 pattern = {
420 'checkout': update_pattern,
421 'status': status_pattern,
422 'update': update_pattern,
423 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000424 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000425 # Place an upper limit.
maruel@chromium.orgfd876172010-04-30 14:01:05 +0000426 for _ in range(10):
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000427 previous_list_len = len(file_list)
428 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000429
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000430 def CaptureMatchingLines(line):
431 match = compiled_pattern.search(line)
432 if match:
433 file_list.append(match.group(1))
434 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000435 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000436
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000437 try:
438 SVN.RunAndFilterOutput(args,
439 in_directory,
440 options.verbose,
441 True,
442 CaptureMatchingLines)
443 except gclient_utils.Error:
maruel@chromium.org2de10252010-02-08 01:10:39 +0000444 # We enforce that some progress has been made or HTTP 502.
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000445 if (filter(lambda x: '502 Bad Gateway' in x, failure) or
maruel@chromium.org2de10252010-02-08 01:10:39 +0000446 (len(failure) and len(file_list) > previous_list_len)):
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000447 if args[0] == 'checkout':
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000448 # An aborted checkout is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000449 args = ['update'] + args[1:]
maruel@chromium.orgfd876172010-04-30 14:01:05 +0000450 print "Sleeping 15 seconds and retrying...."
451 time.sleep(15)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000452 continue
maruel@chromium.org2de10252010-02-08 01:10:39 +0000453 # No progress was made or an unknown error we aren't sure, bail out.
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000454 raise
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000455 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000456
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000457 @staticmethod
458 def RunAndFilterOutput(args,
459 in_directory,
460 print_messages,
461 print_stdout,
462 filter):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000463 """Runs a command, optionally outputting to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000464
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000465 stdout is passed line-by-line to the given filter function. If
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000466 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000467
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000468 Args:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000469 args: A sequence of command line parameters to be passed.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000470 in_directory: The directory where svn is to be run.
471 print_messages: Whether to print status messages to stdout about
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000472 which commands are being run.
473 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000474 filter: A function taking one argument (a string) which will be
475 passed each line (with the ending newline character removed) of
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000476 program's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000477
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000478 Raises:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000479 gclient_utils.Error: An error occurred while running the command.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000480 """
481 command = [SVN.COMMAND]
482 command.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000483 gclient_utils.SubprocessCallAndFilter(command,
484 in_directory,
485 print_messages,
486 print_stdout,
487 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000488
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000489 @staticmethod
490 def CaptureInfo(relpath, in_directory=None, print_error=True):
491 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000492
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000493 Args:
494 relpath: The directory where the working copy resides relative to
495 the directory given by in_directory.
496 in_directory: The directory where svn is to be run.
497 """
498 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
499 dom = gclient_utils.ParseXML(output)
500 result = {}
501 if dom:
502 GetNamedNodeText = gclient_utils.GetNamedNodeText
503 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
504 def C(item, f):
505 if item is not None: return f(item)
506 # /info/entry/
507 # url
508 # reposityory/(root|uuid)
509 # wc-info/(schedule|depth)
510 # commit/(author|date)
511 # str() the results because they may be returned as Unicode, which
512 # interferes with the higher layers matching up things in the deps
513 # dictionary.
514 # TODO(maruel): Fix at higher level instead (!)
515 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
516 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
517 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
518 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
519 'revision'),
520 int)
521 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
522 str)
523 # Differs across versions.
524 if result['Node Kind'] == 'dir':
525 result['Node Kind'] = 'directory'
526 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
527 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
528 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
529 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
530 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000531
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000532 @staticmethod
533 def CaptureHeadRevision(url):
534 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000535
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000536 Returns:
537 Int head revision
538 """
539 info = SVN.Capture(["info", "--xml", url], os.getcwd())
540 dom = xml.dom.minidom.parseString(info)
541 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000542
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000543 @staticmethod
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000544 def CaptureBaseRevision(cwd):
545 """Get the base revision of a SVN repository.
546
547 Returns:
548 Int base revision
549 """
550 info = SVN.Capture(["info", "--xml"], cwd)
551 dom = xml.dom.minidom.parseString(info)
552 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
553
554 @staticmethod
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000555 def CaptureStatus(files):
556 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000557
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000558 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000559
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000560 Returns an array of (status, file) tuples."""
561 command = ["status", "--xml"]
562 if not files:
563 pass
564 elif isinstance(files, basestring):
565 command.append(files)
566 else:
567 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000568
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000569 status_letter = {
570 None: ' ',
571 '': ' ',
572 'added': 'A',
573 'conflicted': 'C',
574 'deleted': 'D',
575 'external': 'X',
576 'ignored': 'I',
577 'incomplete': '!',
578 'merged': 'G',
579 'missing': '!',
580 'modified': 'M',
581 'none': ' ',
582 'normal': ' ',
583 'obstructed': '~',
584 'replaced': 'R',
585 'unversioned': '?',
586 }
587 dom = gclient_utils.ParseXML(SVN.Capture(command))
588 results = []
589 if dom:
590 # /status/target/entry/(wc-status|commit|author|date)
591 for target in dom.getElementsByTagName('target'):
592 #base_path = target.getAttribute('path')
593 for entry in target.getElementsByTagName('entry'):
594 file_path = entry.getAttribute('path')
595 wc_status = entry.getElementsByTagName('wc-status')
596 assert len(wc_status) == 1
597 # Emulate svn 1.5 status ouput...
598 statuses = [' '] * 7
599 # Col 0
600 xml_item_status = wc_status[0].getAttribute('item')
601 if xml_item_status in status_letter:
602 statuses[0] = status_letter[xml_item_status]
603 else:
604 raise Exception('Unknown item status "%s"; please implement me!' %
605 xml_item_status)
606 # Col 1
607 xml_props_status = wc_status[0].getAttribute('props')
608 if xml_props_status == 'modified':
609 statuses[1] = 'M'
610 elif xml_props_status == 'conflicted':
611 statuses[1] = 'C'
612 elif (not xml_props_status or xml_props_status == 'none' or
613 xml_props_status == 'normal'):
614 pass
615 else:
616 raise Exception('Unknown props status "%s"; please implement me!' %
617 xml_props_status)
618 # Col 2
619 if wc_status[0].getAttribute('wc-locked') == 'true':
620 statuses[2] = 'L'
621 # Col 3
622 if wc_status[0].getAttribute('copied') == 'true':
623 statuses[3] = '+'
624 # Col 4
625 if wc_status[0].getAttribute('switched') == 'true':
626 statuses[4] = 'S'
627 # TODO(maruel): Col 5 and 6
628 item = (''.join(statuses), file_path)
629 results.append(item)
630 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000631
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000632 @staticmethod
633 def IsMoved(filename):
634 """Determine if a file has been added through svn mv"""
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000635 return SVN.IsMovedInfo(SVN.CaptureInfo(filename))
636
637 @staticmethod
638 def IsMovedInfo(info):
639 """Determine if a file has been added through svn mv"""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000640 return (info.get('Copied From URL') and
641 info.get('Copied From Rev') and
642 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000643
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000644 @staticmethod
645 def GetFileProperty(file, property_name):
646 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000647
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000648 Args:
649 file: The file to check
650 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000651
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000652 Returns:
653 The value of the property, which will be the empty string if the property
654 is not set on the file. If the file is not under version control, the
655 empty string is also returned.
656 """
657 output = SVN.Capture(["propget", property_name, file])
658 if (output.startswith("svn: ") and
659 output.endswith("is not under version control")):
660 return ""
661 else:
662 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000663
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000664 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000665 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000666 """Diffs a single file.
667
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000668 Should be simple, eh? No it isn't.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000669 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000670 expected relative path.
671 full_move means that move or copy operations should completely recreate the
672 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000673 # If the user specified a custom diff command in their svn config file,
674 # then it'll be used when we do svn diff, which we don't want to happen
675 # since we want the unified diff. Using --diff-cmd=diff doesn't always
676 # work, since they can have another diff executable in their path that
677 # gives different line endings. So we use a bogus temp directory as the
678 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000679 bogus_dir = tempfile.mkdtemp()
680 try:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000681 # Use "svn info" output instead of os.path.isdir because the latter fails
682 # when the file is deleted.
683 return SVN._DiffItemInternal(SVN.CaptureInfo(filename),
684 full_move=full_move, revision=revision)
685 finally:
686 shutil.rmtree(bogus_dir)
687
688 @staticmethod
689 def _DiffItemInternal(filename, info, bogus_dir, full_move=False,
690 revision=None):
691 """Grabs the diff data."""
692 command = ["diff", "--config-dir", bogus_dir, filename]
693 if revision:
694 command.extend(['--revision', revision])
695 data = None
696 if SVN.IsMovedInfo(info):
697 if full_move:
698 if info.get("Node Kind") == "directory":
699 # Things become tricky here. It's a directory copy/move. We need to
700 # diff all the files inside it.
701 # This will put a lot of pressure on the heap. This is why StringIO
702 # is used and converted back into a string at the end. The reason to
703 # return a string instead of a StringIO is that StringIO.write()
704 # doesn't accept a StringIO object. *sigh*.
705 for (dirpath, dirnames, filenames) in os.walk(filename):
706 # Cleanup all files starting with a '.'.
707 for d in dirnames:
708 if d.startswith('.'):
709 dirnames.remove(d)
710 for f in filenames:
711 if f.startswith('.'):
712 filenames.remove(f)
713 for f in filenames:
714 if data is None:
715 data = cStringIO.StringIO()
716 data.write(GenFakeDiff(os.path.join(dirpath, f)))
717 if data:
718 tmp = data.getvalue()
719 data.close()
720 data = tmp
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000721 else:
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000722 data = GenFakeDiff(filename)
723 else:
724 if info.get("Node Kind") != "directory":
maruel@chromium.org0836c562010-01-22 01:10:06 +0000725 # svn diff on a mv/cp'd file outputs nothing if there was no change.
726 data = SVN.Capture(command, None)
727 if not data:
728 # We put in an empty Index entry so upload.py knows about them.
729 data = "Index: %s\n" % filename
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000730 # Otherwise silently ignore directories.
731 else:
732 if info.get("Node Kind") != "directory":
733 # Normal simple case.
maruel@chromium.org0836c562010-01-22 01:10:06 +0000734 data = SVN.Capture(command, None)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000735 # Otherwise silently ignore directories.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000736 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000737
738 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000739 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000740 """Returns a string containing the diff for the given file list.
741
742 The files in the list should either be absolute paths or relative to the
743 given root. If no root directory is provided, the repository root will be
744 used.
745 The diff will always use relative paths.
746 """
747 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000748 root = root or SVN.GetCheckoutRoot(previous_cwd)
749 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000750 def RelativePath(path, root):
751 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000752 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000753 return path[len(root):]
754 return path
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000755 # If the user specified a custom diff command in their svn config file,
756 # then it'll be used when we do svn diff, which we don't want to happen
757 # since we want the unified diff. Using --diff-cmd=diff doesn't always
758 # work, since they can have another diff executable in their path that
759 # gives different line endings. So we use a bogus temp directory as the
760 # config directory, which gets around these problems.
761 bogus_dir = tempfile.mkdtemp()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000762 try:
763 os.chdir(root)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000764 # Cleanup filenames
765 filenames = [RelativePath(f, root) for f in filenames]
766 # Get information about the modified items (files and directories)
767 data = dict([(f, SVN.CaptureInfo(f)) for f in filenames])
768 if full_move:
769 # Eliminate modified files inside moved/copied directory.
770 for (filename, info) in data.iteritems():
771 if SVN.IsMovedInfo(info) and info.get("Node Kind") == "directory":
772 # Remove files inside the directory.
773 filenames = [f for f in filenames
774 if not f.startswith(filename + os.path.sep)]
775 for filename in data.keys():
776 if not filename in filenames:
777 # Remove filtered out items.
778 del data[filename]
779 # Now ready to do the actual diff.
780 diffs = []
781 for filename in sorted(data.iterkeys()):
782 diffs.append(SVN._DiffItemInternal(filename, data[filename], bogus_dir,
783 full_move=full_move,
784 revision=revision))
785 # Use StringIO since it can be messy when diffing a directory move with
786 # full_move=True.
787 buf = cStringIO.StringIO()
788 for d in filter(None, diffs):
789 buf.write(d)
790 result = buf.getvalue()
791 buf.close()
792 return result
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000793 finally:
794 os.chdir(previous_cwd)
maruel@chromium.org3c55d982010-05-06 14:25:44 +0000795 shutil.rmtree(bogus_dir)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000796
797 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000798 def GetEmail(repo_root):
799 """Retrieves the svn account which we assume is an email address."""
800 infos = SVN.CaptureInfo(repo_root)
801 uuid = infos.get('UUID')
802 root = infos.get('Repository Root')
803 if not root:
804 return None
805
806 # Should check for uuid but it is incorrectly saved for https creds.
807 realm = root.rsplit('/', 1)[0]
808 if root.startswith('https') or not uuid:
809 regexp = re.compile(r'<%s:\d+>.*' % realm)
810 else:
811 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
812 if regexp is None:
813 return None
814 if sys.platform.startswith('win'):
815 if not 'APPDATA' in os.environ:
816 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000817 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
818 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000819 else:
820 if not 'HOME' in os.environ:
821 return None
822 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
823 'svn.simple')
824 for credfile in os.listdir(auth_dir):
825 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
826 if regexp.match(cred_info.get('svn:realmstring')):
827 return cred_info.get('username')
828
829 @staticmethod
830 def ReadSimpleAuth(filename):
831 f = open(filename, 'r')
832 values = {}
833 def ReadOneItem(type):
834 m = re.match(r'%s (\d+)' % type, f.readline())
835 if not m:
836 return None
837 data = f.read(int(m.group(1)))
838 if f.read(1) != '\n':
839 return None
840 return data
841
842 while True:
843 key = ReadOneItem('K')
844 if not key:
845 break
846 value = ReadOneItem('V')
847 if not value:
848 break
849 values[key] = value
850 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000851
852 @staticmethod
853 def GetCheckoutRoot(directory):
854 """Returns the top level directory of the current repository.
855
856 The directory is returned as an absolute path.
857 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000858 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000859 infos = SVN.CaptureInfo(directory, print_error=False)
860 cur_dir_repo_root = infos.get("Repository Root")
861 if not cur_dir_repo_root:
862 return None
863
864 while True:
865 parent = os.path.dirname(directory)
866 if (SVN.CaptureInfo(parent, print_error=False).get(
867 "Repository Root") != cur_dir_repo_root):
868 break
869 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000870 return GetCasedPath(directory)
tony@chromium.org57564662010-04-14 02:35:12 +0000871
872 @staticmethod
873 def AssertVersion(min_version):
874 """Asserts svn's version is at least min_version."""
875 def only_int(val):
876 if val.isdigit():
877 return int(val)
878 else:
879 return 0
880 if not SVN.current_version:
881 SVN.current_version = SVN.Capture(['--version']).split()[2]
882 current_version_list = map(only_int, SVN.current_version.split('.'))
883 for min_ver in map(int, min_version.split('.')):
884 ver = current_version_list.pop(0)
885 if ver < min_ver:
886 return (False, SVN.current_version)
887 elif ver > min_ver:
888 return (True, SVN.current_version)
889 return (True, SVN.current_version)