blob: 87514beacbd00a7c6d891768dcbfdb686e808072 [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.orgfd9cbbb2010-01-08 23:04:03 +00007import glob
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00008import os
9import re
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000010import shutil
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000011import subprocess
12import sys
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000013import tempfile
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000014import xml.dom.minidom
15
16import gclient_utils
17
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +000018def ValidateEmail(email):
19 return (re.match(r"^[a-zA-Z0-9._%-+]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,6}$", email)
20 is not None)
21
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000022
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +000023def GetCasedPath(path):
24 """Elcheapos way to get the real path case on Windows."""
25 if sys.platform.startswith('win') and os.path.exists(path):
26 # Reconstruct the path.
27 path = os.path.abspath(path)
28 paths = path.split('\\')
29 for i in range(len(paths)):
30 if i == 0:
31 # Skip drive letter.
32 continue
33 subpath = '\\'.join(paths[:i+1])
34 prev = len('\\'.join(paths[:i]))
35 # glob.glob will return the cased path for the last item only. This is why
36 # we are calling it in a loop. Extract the data we want and put it back
37 # into the list.
38 paths[i] = glob.glob(subpath + '*')[0][prev+1:len(subpath)]
39 path = '\\'.join(paths)
40 return path
41
42
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000043class GIT(object):
44 COMMAND = "git"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000045
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000046 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000047 def Capture(args, in_directory=None, print_error=True, error_ok=False):
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000048 """Runs git, capturing output sent to stdout as a string.
49
50 Args:
51 args: A sequence of command line parameters to be passed to git.
52 in_directory: The directory where git is to be run.
53
54 Returns:
55 The output sent to stdout as a string.
56 """
57 c = [GIT.COMMAND]
58 c.extend(args)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000059 try:
60 return gclient_utils.CheckCall(c, in_directory, print_error)
61 except gclient_utils.CheckCallError:
62 if error_ok:
nasser@codeaurora.orgcd968c12010-02-01 06:05:00 +000063 return ('', '')
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +000064 raise
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000065
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000066 @staticmethod
67 def CaptureStatus(files, upstream_branch='origin'):
68 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000069
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000070 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000071
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000072 Returns an array of (status, file) tuples."""
bauerb@chromium.org14ec5042010-03-30 18:19:09 +000073 command = ["diff", "--name-status", "-r", "%s..." % upstream_branch]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000074 if not files:
75 pass
76 elif isinstance(files, basestring):
77 command.append(files)
78 else:
79 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000080
maruel@chromium.org7be5ef22010-01-30 22:31:50 +000081 status = GIT.Capture(command)[0].rstrip()
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000082 results = []
83 if status:
84 for statusline in status.split('\n'):
85 m = re.match('^(\w)\t(.+)$', statusline)
86 if not m:
87 raise Exception("status currently unsupported: %s" % statusline)
88 results.append(('%s ' % m.group(1), m.group(2)))
89 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000090
maruel@chromium.orgc78f2462009-11-21 01:20:57 +000091 @staticmethod
maruel@chromium.orgee4071d2009-12-22 22:25:37 +000092 def RunAndFilterOutput(args,
93 in_directory,
94 print_messages,
95 print_stdout,
96 filter):
97 """Runs a command, optionally outputting to stdout.
98
99 stdout is passed line-by-line to the given filter function. If
100 print_stdout is true, it is also printed to sys.stdout as in Run.
101
102 Args:
103 args: A sequence of command line parameters to be passed.
msb@chromium.orgd6504212010-01-13 17:34:31 +0000104 in_directory: The directory where git is to be run.
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000105 print_messages: Whether to print status messages to stdout about
106 which commands are being run.
107 print_stdout: Whether to forward program's output to stdout.
108 filter: A function taking one argument (a string) which will be
109 passed each line (with the ending newline character removed) of
110 program's output for filtering.
111
112 Raises:
113 gclient_utils.Error: An error occurred while running the command.
114 """
115 command = [GIT.COMMAND]
116 command.extend(args)
117 gclient_utils.SubprocessCallAndFilter(command,
118 in_directory,
119 print_messages,
120 print_stdout,
121 filter=filter)
122
123 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000124 def GetEmail(repo_root):
125 """Retrieves the user email address if known."""
126 # We could want to look at the svn cred when it has a svn remote but it
127 # should be fine for now, users should simply configure their git settings.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000128 return GIT.Capture(['config', 'user.email'],
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000129 repo_root, error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000130
131 @staticmethod
132 def ShortBranchName(branch):
133 """Converts a name like 'refs/heads/foo' to just 'foo'."""
134 return branch.replace('refs/heads/', '')
135
136 @staticmethod
137 def GetBranchRef(cwd):
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000138 """Returns the full branch reference, e.g. 'refs/heads/master'."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000139 return GIT.Capture(['symbolic-ref', 'HEAD'], cwd)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000140
141 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000142 def GetBranch(cwd):
143 """Returns the short branch name, e.g. 'master'."""
maruel@chromium.orgc308a742009-12-22 18:29:33 +0000144 return GIT.ShortBranchName(GIT.GetBranchRef(cwd))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000145
146 @staticmethod
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000147 def IsGitSvn(cwd):
148 """Returns true if this repo looks like it's using git-svn."""
149 # If you have any "svn-remote.*" config keys, we think you're using svn.
150 try:
151 GIT.Capture(['config', '--get-regexp', r'^svn-remote\.'], cwd)
152 return True
153 except gclient_utils.CheckCallError:
154 return False
155
156 @staticmethod
157 def GetSVNBranch(cwd):
158 """Returns the svn branch name if found."""
159 # Try to figure out which remote branch we're based on.
160 # Strategy:
161 # 1) find all git-svn branches and note their svn URLs.
162 # 2) iterate through our branch history and match up the URLs.
163
164 # regexp matching the git-svn line that contains the URL.
165 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
166
167 # Get the refname and svn url for all refs/remotes/*.
168 remotes = GIT.Capture(
169 ['for-each-ref', '--format=%(refname)', 'refs/remotes'],
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000170 cwd)[0].splitlines()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000171 svn_refs = {}
172 for ref in remotes:
173 match = git_svn_re.search(
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000174 GIT.Capture(['cat-file', '-p', ref], cwd)[0])
sky@chromium.orgd8da2c12010-04-09 16:28:09 +0000175 if match and match.group(1) not in svn_refs:
176 # To prefer local refs over remote ones we only set the first occurence.
177 # The assumption being local refs are usually first.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000178 svn_refs[match.group(1)] = ref
179
180 svn_branch = ''
181 if len(svn_refs) == 1:
182 # Only one svn branch exists -- seems like a good candidate.
183 svn_branch = svn_refs.values()[0]
184 elif len(svn_refs) > 1:
185 # We have more than one remote branch available. We don't
186 # want to go through all of history, so read a line from the
187 # pipe at a time.
188 # The -100 is an arbitrary limit so we don't search forever.
189 cmd = ['git', 'log', '-100', '--pretty=medium']
190 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd)
191 for line in proc.stdout:
192 match = git_svn_re.match(line)
193 if match:
194 url = match.group(1)
195 if url in svn_refs:
196 svn_branch = svn_refs[url]
197 proc.stdout.close() # Cut pipe.
198 break
199 return svn_branch
200
201 @staticmethod
202 def FetchUpstreamTuple(cwd):
203 """Returns a tuple containg remote and remote ref,
204 e.g. 'origin', 'refs/heads/master'
205 """
206 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000207 branch = GIT.GetBranch(cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000208 upstream_branch = None
209 upstream_branch = GIT.Capture(
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000210 ['config', 'branch.%s.merge' % branch], in_directory=cwd,
211 error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000212 if upstream_branch:
213 remote = GIT.Capture(
214 ['config', 'branch.%s.remote' % branch],
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000215 in_directory=cwd, error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000216 else:
217 # Fall back on trying a git-svn upstream branch.
218 if GIT.IsGitSvn(cwd):
219 upstream_branch = GIT.GetSVNBranch(cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000220 return remote, upstream_branch
221
222 @staticmethod
223 def GetUpstream(cwd):
224 """Gets the current branch's upstream branch."""
225 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000226 if remote != '.':
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000227 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
228 return upstream_branch
229
230 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000231 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
232 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000233 """Diffs against the upstream branch or optionally another branch.
234
235 full_move means that move or copy operations should completely recreate the
236 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000237 if not branch:
238 branch = GIT.GetUpstream(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000239 command = ['diff', '-p', '--no-prefix', branch + "..." + branch_head]
maruel@chromium.orga9371762009-12-22 18:27:38 +0000240 if not full_move:
241 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000242 # TODO(maruel): --binary support.
243 if files:
244 command.append('--')
245 command.extend(files)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000246 diff = GIT.Capture(command, cwd)[0].splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000247 for i in range(len(diff)):
248 # In the case of added files, replace /dev/null with the path to the
249 # file being added.
250 if diff[i].startswith('--- /dev/null'):
251 diff[i] = '--- %s' % diff[i+1][4:]
252 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000253
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000254 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000255 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
256 """Returns the list of modified files between two branches."""
257 if not branch:
258 branch = GIT.GetUpstream(cwd)
bauerb@chromium.org838f0f22010-04-09 17:02:50 +0000259 command = ['diff', '--name-only', branch + "..." + branch_head]
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000260 return GIT.Capture(command, cwd)[0].splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000261
262 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000263 def GetPatchName(cwd):
264 """Constructs a name for this patch."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000265 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd)[0].strip()
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000266 return "%s-%s" % (GIT.GetBranch(cwd), short_sha)
267
268 @staticmethod
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000269 def GetCheckoutRoot(path):
270 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000271 """
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000272 root = GIT.Capture(['rev-parse', '--show-cdup'], path)[0].strip()
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000273 return os.path.abspath(os.path.join(path, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000274
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000275 @staticmethod
276 def AssertVersion(min_version):
277 """Asserts git's version is at least min_version."""
278 def only_int(val):
279 if val.isdigit():
280 return int(val)
281 else:
282 return 0
283 current_version = GIT.Capture(['--version'])[0].split()[-1]
284 current_version_list = map(only_int, current_version.split('.'))
285 for min_ver in map(int, min_version.split('.')):
286 ver = current_version_list.pop(0)
287 if ver < min_ver:
288 return (False, current_version)
289 elif ver > min_ver:
290 return (True, current_version)
291 return (True, current_version)
292
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000293
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000294class SVN(object):
295 COMMAND = "svn"
tony@chromium.org57564662010-04-14 02:35:12 +0000296 current_version = None
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000297
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000298 @staticmethod
299 def Run(args, in_directory):
300 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000301
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000302 Args:
303 args: A sequence of command line parameters to be passed to svn.
304 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000305
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000306 Raises:
307 Error: An error occurred while running the svn command.
308 """
309 c = [SVN.COMMAND]
310 c.extend(args)
maruel@chromium.org2185f002009-12-18 21:03:47 +0000311 # TODO(maruel): This is very gclient-specific.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000312 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000313
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000314 @staticmethod
315 def Capture(args, in_directory=None, print_error=True):
316 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000317
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000318 Args:
319 args: A sequence of command line parameters to be passed to svn.
320 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000321
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000322 Returns:
323 The output sent to stdout as a string.
324 """
325 c = [SVN.COMMAND]
326 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000327
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000328 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
329 # the svn.exe executable, but shell=True makes subprocess on Linux fail
330 # when it's called with a list because it only tries to execute the
331 # first string ("svn").
332 stderr = None
333 if not print_error:
334 stderr = subprocess.PIPE
335 return subprocess.Popen(c,
336 cwd=in_directory,
337 shell=(sys.platform == 'win32'),
338 stdout=subprocess.PIPE,
339 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000340
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000341 @staticmethod
342 def RunAndGetFileList(options, args, in_directory, file_list):
343 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000344
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000345 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000346
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000347 svn's stdout is parsed to collect a list of files checked out or updated.
348 These files are appended to file_list. svn's stdout is also printed to
349 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000350
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000351 Args:
352 options: command line options to gclient
353 args: A sequence of command line parameters to be passed to svn.
354 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000355
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000356 Raises:
357 Error: An error occurred while running the svn command.
358 """
359 command = [SVN.COMMAND]
360 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000361
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000362 # svn update and svn checkout use the same pattern: the first three columns
363 # are for file status, property status, and lock status. This is followed
364 # by two spaces, and then the path to the file.
365 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000366
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000367 # The first three columns of svn status are the same as for svn update and
368 # svn checkout. The next three columns indicate addition-with-history,
369 # switch, and remote lock status. This is followed by one space, and then
370 # the path to the file.
371 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000372
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000373 # args[0] must be a supported command. This will blow up if it's something
374 # else, which is good. Note that the patterns are only effective when
375 # these commands are used in their ordinary forms, the patterns are invalid
376 # for "svn status --show-updates", for example.
377 pattern = {
378 'checkout': update_pattern,
379 'status': status_pattern,
380 'update': update_pattern,
381 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000382 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000383 # Place an upper limit.
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000384 for _ in range(1, 10):
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000385 previous_list_len = len(file_list)
386 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000387
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000388 def CaptureMatchingLines(line):
389 match = compiled_pattern.search(line)
390 if match:
391 file_list.append(match.group(1))
392 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000393 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000394
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000395 try:
396 SVN.RunAndFilterOutput(args,
397 in_directory,
398 options.verbose,
399 True,
400 CaptureMatchingLines)
401 except gclient_utils.Error:
maruel@chromium.org2de10252010-02-08 01:10:39 +0000402 # We enforce that some progress has been made or HTTP 502.
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000403 if (filter(lambda x: '502 Bad Gateway' in x, failure) or
maruel@chromium.org2de10252010-02-08 01:10:39 +0000404 (len(failure) and len(file_list) > previous_list_len)):
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000405 if args[0] == 'checkout':
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000406 # An aborted checkout is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000407 args = ['update'] + args[1:]
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000408 continue
maruel@chromium.org2de10252010-02-08 01:10:39 +0000409 # No progress was made or an unknown error we aren't sure, bail out.
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000410 raise
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000411 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000412
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000413 @staticmethod
414 def RunAndFilterOutput(args,
415 in_directory,
416 print_messages,
417 print_stdout,
418 filter):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000419 """Runs a command, optionally outputting to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000420
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000421 stdout is passed line-by-line to the given filter function. If
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000422 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000423
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000424 Args:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000425 args: A sequence of command line parameters to be passed.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000426 in_directory: The directory where svn is to be run.
427 print_messages: Whether to print status messages to stdout about
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000428 which commands are being run.
429 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000430 filter: A function taking one argument (a string) which will be
431 passed each line (with the ending newline character removed) of
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000432 program's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000433
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000434 Raises:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000435 gclient_utils.Error: An error occurred while running the command.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000436 """
437 command = [SVN.COMMAND]
438 command.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000439 gclient_utils.SubprocessCallAndFilter(command,
440 in_directory,
441 print_messages,
442 print_stdout,
443 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000444
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000445 @staticmethod
446 def CaptureInfo(relpath, in_directory=None, print_error=True):
447 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000448
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000449 Args:
450 relpath: The directory where the working copy resides relative to
451 the directory given by in_directory.
452 in_directory: The directory where svn is to be run.
453 """
454 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
455 dom = gclient_utils.ParseXML(output)
456 result = {}
457 if dom:
458 GetNamedNodeText = gclient_utils.GetNamedNodeText
459 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
460 def C(item, f):
461 if item is not None: return f(item)
462 # /info/entry/
463 # url
464 # reposityory/(root|uuid)
465 # wc-info/(schedule|depth)
466 # commit/(author|date)
467 # str() the results because they may be returned as Unicode, which
468 # interferes with the higher layers matching up things in the deps
469 # dictionary.
470 # TODO(maruel): Fix at higher level instead (!)
471 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
472 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
473 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
474 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
475 'revision'),
476 int)
477 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
478 str)
479 # Differs across versions.
480 if result['Node Kind'] == 'dir':
481 result['Node Kind'] = 'directory'
482 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
483 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
484 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
485 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
486 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000487
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000488 @staticmethod
489 def CaptureHeadRevision(url):
490 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000491
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000492 Returns:
493 Int head revision
494 """
495 info = SVN.Capture(["info", "--xml", url], os.getcwd())
496 dom = xml.dom.minidom.parseString(info)
497 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000498
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000499 @staticmethod
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000500 def CaptureBaseRevision(cwd):
501 """Get the base revision of a SVN repository.
502
503 Returns:
504 Int base revision
505 """
506 info = SVN.Capture(["info", "--xml"], cwd)
507 dom = xml.dom.minidom.parseString(info)
508 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
509
510 @staticmethod
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000511 def CaptureStatus(files):
512 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000513
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000514 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000515
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000516 Returns an array of (status, file) tuples."""
517 command = ["status", "--xml"]
518 if not files:
519 pass
520 elif isinstance(files, basestring):
521 command.append(files)
522 else:
523 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000524
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000525 status_letter = {
526 None: ' ',
527 '': ' ',
528 'added': 'A',
529 'conflicted': 'C',
530 'deleted': 'D',
531 'external': 'X',
532 'ignored': 'I',
533 'incomplete': '!',
534 'merged': 'G',
535 'missing': '!',
536 'modified': 'M',
537 'none': ' ',
538 'normal': ' ',
539 'obstructed': '~',
540 'replaced': 'R',
541 'unversioned': '?',
542 }
543 dom = gclient_utils.ParseXML(SVN.Capture(command))
544 results = []
545 if dom:
546 # /status/target/entry/(wc-status|commit|author|date)
547 for target in dom.getElementsByTagName('target'):
548 #base_path = target.getAttribute('path')
549 for entry in target.getElementsByTagName('entry'):
550 file_path = entry.getAttribute('path')
551 wc_status = entry.getElementsByTagName('wc-status')
552 assert len(wc_status) == 1
553 # Emulate svn 1.5 status ouput...
554 statuses = [' '] * 7
555 # Col 0
556 xml_item_status = wc_status[0].getAttribute('item')
557 if xml_item_status in status_letter:
558 statuses[0] = status_letter[xml_item_status]
559 else:
560 raise Exception('Unknown item status "%s"; please implement me!' %
561 xml_item_status)
562 # Col 1
563 xml_props_status = wc_status[0].getAttribute('props')
564 if xml_props_status == 'modified':
565 statuses[1] = 'M'
566 elif xml_props_status == 'conflicted':
567 statuses[1] = 'C'
568 elif (not xml_props_status or xml_props_status == 'none' or
569 xml_props_status == 'normal'):
570 pass
571 else:
572 raise Exception('Unknown props status "%s"; please implement me!' %
573 xml_props_status)
574 # Col 2
575 if wc_status[0].getAttribute('wc-locked') == 'true':
576 statuses[2] = 'L'
577 # Col 3
578 if wc_status[0].getAttribute('copied') == 'true':
579 statuses[3] = '+'
580 # Col 4
581 if wc_status[0].getAttribute('switched') == 'true':
582 statuses[4] = 'S'
583 # TODO(maruel): Col 5 and 6
584 item = (''.join(statuses), file_path)
585 results.append(item)
586 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000587
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000588 @staticmethod
589 def IsMoved(filename):
590 """Determine if a file has been added through svn mv"""
591 info = SVN.CaptureInfo(filename)
592 return (info.get('Copied From URL') and
593 info.get('Copied From Rev') and
594 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000595
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000596 @staticmethod
597 def GetFileProperty(file, property_name):
598 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000599
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000600 Args:
601 file: The file to check
602 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000603
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000604 Returns:
605 The value of the property, which will be the empty string if the property
606 is not set on the file. If the file is not under version control, the
607 empty string is also returned.
608 """
609 output = SVN.Capture(["propget", property_name, file])
610 if (output.startswith("svn: ") and
611 output.endswith("is not under version control")):
612 return ""
613 else:
614 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000615
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000616 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000617 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000618 """Diffs a single file.
619
620 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000621 expected relative path.
622 full_move means that move or copy operations should completely recreate the
623 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000624 # Use svn info output instead of os.path.isdir because the latter fails
625 # when the file is deleted.
626 if SVN.CaptureInfo(filename).get("Node Kind") == "directory":
627 return None
628 # If the user specified a custom diff command in their svn config file,
629 # then it'll be used when we do svn diff, which we don't want to happen
630 # since we want the unified diff. Using --diff-cmd=diff doesn't always
631 # work, since they can have another diff executable in their path that
632 # gives different line endings. So we use a bogus temp directory as the
633 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000634 bogus_dir = tempfile.mkdtemp()
635 try:
636 # Grabs the diff data.
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000637 command = ["diff", "--config-dir", bogus_dir, filename]
maruel@chromium.orgce3c8622010-01-07 02:18:16 +0000638 if revision:
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000639 command.extend(['--revision', revision])
maruel@chromium.org0836c562010-01-22 01:10:06 +0000640 if SVN.IsMoved(filename):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000641 if full_move:
642 file_content = gclient_utils.FileRead(filename, 'rb')
643 # Prepend '+' to every lines.
644 file_content = ['+' + i for i in file_content.splitlines(True)]
645 nb_lines = len(file_content)
646 # We need to use / since patch on unix will fail otherwise.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000647 data = "Index: %s\n" % filename
648 data += '=' * 67 + '\n'
649 # Note: Should we use /dev/null instead?
650 data += "--- %s\n" % filename
651 data += "+++ %s\n" % filename
652 data += "@@ -0,0 +1,%d @@\n" % nb_lines
653 data += ''.join(file_content)
654 else:
maruel@chromium.org0836c562010-01-22 01:10:06 +0000655 # svn diff on a mv/cp'd file outputs nothing if there was no change.
656 data = SVN.Capture(command, None)
657 if not data:
658 # We put in an empty Index entry so upload.py knows about them.
659 data = "Index: %s\n" % filename
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000660 else:
maruel@chromium.org0836c562010-01-22 01:10:06 +0000661 data = SVN.Capture(command, None)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000662 finally:
663 shutil.rmtree(bogus_dir)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000664 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000665
666 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000667 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000668 """Returns a string containing the diff for the given file list.
669
670 The files in the list should either be absolute paths or relative to the
671 given root. If no root directory is provided, the repository root will be
672 used.
673 The diff will always use relative paths.
674 """
675 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000676 root = root or SVN.GetCheckoutRoot(previous_cwd)
677 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000678 def RelativePath(path, root):
679 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000680 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000681 return path[len(root):]
682 return path
683 try:
684 os.chdir(root)
685 diff = "".join(filter(None,
maruel@chromium.org0c401692010-01-07 02:44:20 +0000686 [SVN.DiffItem(RelativePath(f, root),
687 full_move=full_move,
688 revision=revision)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000689 for f in filenames]))
690 finally:
691 os.chdir(previous_cwd)
692 return diff
693
694
695 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000696 def GetEmail(repo_root):
697 """Retrieves the svn account which we assume is an email address."""
698 infos = SVN.CaptureInfo(repo_root)
699 uuid = infos.get('UUID')
700 root = infos.get('Repository Root')
701 if not root:
702 return None
703
704 # Should check for uuid but it is incorrectly saved for https creds.
705 realm = root.rsplit('/', 1)[0]
706 if root.startswith('https') or not uuid:
707 regexp = re.compile(r'<%s:\d+>.*' % realm)
708 else:
709 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
710 if regexp is None:
711 return None
712 if sys.platform.startswith('win'):
713 if not 'APPDATA' in os.environ:
714 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000715 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
716 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000717 else:
718 if not 'HOME' in os.environ:
719 return None
720 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
721 'svn.simple')
722 for credfile in os.listdir(auth_dir):
723 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
724 if regexp.match(cred_info.get('svn:realmstring')):
725 return cred_info.get('username')
726
727 @staticmethod
728 def ReadSimpleAuth(filename):
729 f = open(filename, 'r')
730 values = {}
731 def ReadOneItem(type):
732 m = re.match(r'%s (\d+)' % type, f.readline())
733 if not m:
734 return None
735 data = f.read(int(m.group(1)))
736 if f.read(1) != '\n':
737 return None
738 return data
739
740 while True:
741 key = ReadOneItem('K')
742 if not key:
743 break
744 value = ReadOneItem('V')
745 if not value:
746 break
747 values[key] = value
748 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000749
750 @staticmethod
751 def GetCheckoutRoot(directory):
752 """Returns the top level directory of the current repository.
753
754 The directory is returned as an absolute path.
755 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000756 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000757 infos = SVN.CaptureInfo(directory, print_error=False)
758 cur_dir_repo_root = infos.get("Repository Root")
759 if not cur_dir_repo_root:
760 return None
761
762 while True:
763 parent = os.path.dirname(directory)
764 if (SVN.CaptureInfo(parent, print_error=False).get(
765 "Repository Root") != cur_dir_repo_root):
766 break
767 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000768 return GetCasedPath(directory)
tony@chromium.org57564662010-04-14 02:35:12 +0000769
770 @staticmethod
771 def AssertVersion(min_version):
772 """Asserts svn's version is at least min_version."""
773 def only_int(val):
774 if val.isdigit():
775 return int(val)
776 else:
777 return 0
778 if not SVN.current_version:
779 SVN.current_version = SVN.Capture(['--version']).split()[2]
780 current_version_list = map(only_int, SVN.current_version.split('.'))
781 for min_ver in map(int, min_version.split('.')):
782 ver = current_version_list.pop(0)
783 if ver < min_ver:
784 return (False, SVN.current_version)
785 elif ver > min_ver:
786 return (True, SVN.current_version)
787 return (True, SVN.current_version)