blob: 3febbe495d6632e8503f03653c761a52903af014 [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)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000239 command = ['diff-tree', '-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)
259 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"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000296
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000297 @staticmethod
298 def Run(args, in_directory):
299 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000300
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000301 Args:
302 args: A sequence of command line parameters to be passed to svn.
303 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000304
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000305 Raises:
306 Error: An error occurred while running the svn command.
307 """
308 c = [SVN.COMMAND]
309 c.extend(args)
maruel@chromium.org2185f002009-12-18 21:03:47 +0000310 # TODO(maruel): This is very gclient-specific.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000311 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000312
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000313 @staticmethod
314 def Capture(args, in_directory=None, print_error=True):
315 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000316
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000317 Args:
318 args: A sequence of command line parameters to be passed to svn.
319 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000320
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000321 Returns:
322 The output sent to stdout as a string.
323 """
324 c = [SVN.COMMAND]
325 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000326
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000327 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
328 # the svn.exe executable, but shell=True makes subprocess on Linux fail
329 # when it's called with a list because it only tries to execute the
330 # first string ("svn").
331 stderr = None
332 if not print_error:
333 stderr = subprocess.PIPE
334 return subprocess.Popen(c,
335 cwd=in_directory,
336 shell=(sys.platform == 'win32'),
337 stdout=subprocess.PIPE,
338 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000339
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000340 @staticmethod
341 def RunAndGetFileList(options, args, in_directory, file_list):
342 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000343
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000344 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000345
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000346 svn's stdout is parsed to collect a list of files checked out or updated.
347 These files are appended to file_list. svn's stdout is also printed to
348 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000349
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000350 Args:
351 options: command line options to gclient
352 args: A sequence of command line parameters to be passed to svn.
353 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000354
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000355 Raises:
356 Error: An error occurred while running the svn command.
357 """
358 command = [SVN.COMMAND]
359 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000360
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000361 # svn update and svn checkout use the same pattern: the first three columns
362 # are for file status, property status, and lock status. This is followed
363 # by two spaces, and then the path to the file.
364 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000365
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000366 # The first three columns of svn status are the same as for svn update and
367 # svn checkout. The next three columns indicate addition-with-history,
368 # switch, and remote lock status. This is followed by one space, and then
369 # the path to the file.
370 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000371
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000372 # args[0] must be a supported command. This will blow up if it's something
373 # else, which is good. Note that the patterns are only effective when
374 # these commands are used in their ordinary forms, the patterns are invalid
375 # for "svn status --show-updates", for example.
376 pattern = {
377 'checkout': update_pattern,
378 'status': status_pattern,
379 'update': update_pattern,
380 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000381 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000382 # Place an upper limit.
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000383 for _ in range(1, 10):
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000384 previous_list_len = len(file_list)
385 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000386
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000387 def CaptureMatchingLines(line):
388 match = compiled_pattern.search(line)
389 if match:
390 file_list.append(match.group(1))
391 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000392 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000393
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000394 try:
395 SVN.RunAndFilterOutput(args,
396 in_directory,
397 options.verbose,
398 True,
399 CaptureMatchingLines)
400 except gclient_utils.Error:
maruel@chromium.org2de10252010-02-08 01:10:39 +0000401 # We enforce that some progress has been made or HTTP 502.
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000402 if (filter(lambda x: '502 Bad Gateway' in x, failure) or
maruel@chromium.org2de10252010-02-08 01:10:39 +0000403 (len(failure) and len(file_list) > previous_list_len)):
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000404 if args[0] == 'checkout':
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000405 # An aborted checkout is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000406 args = ['update'] + args[1:]
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000407 continue
maruel@chromium.org2de10252010-02-08 01:10:39 +0000408 # No progress was made or an unknown error we aren't sure, bail out.
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000409 raise
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000410 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000411
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000412 @staticmethod
413 def RunAndFilterOutput(args,
414 in_directory,
415 print_messages,
416 print_stdout,
417 filter):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000418 """Runs a command, optionally outputting to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000419
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000420 stdout is passed line-by-line to the given filter function. If
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000421 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000422
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000423 Args:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000424 args: A sequence of command line parameters to be passed.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000425 in_directory: The directory where svn is to be run.
426 print_messages: Whether to print status messages to stdout about
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000427 which commands are being run.
428 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000429 filter: A function taking one argument (a string) which will be
430 passed each line (with the ending newline character removed) of
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000431 program's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000432
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000433 Raises:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000434 gclient_utils.Error: An error occurred while running the command.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000435 """
436 command = [SVN.COMMAND]
437 command.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000438 gclient_utils.SubprocessCallAndFilter(command,
439 in_directory,
440 print_messages,
441 print_stdout,
442 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000443
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000444 @staticmethod
445 def CaptureInfo(relpath, in_directory=None, print_error=True):
446 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000447
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000448 Args:
449 relpath: The directory where the working copy resides relative to
450 the directory given by in_directory.
451 in_directory: The directory where svn is to be run.
452 """
453 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
454 dom = gclient_utils.ParseXML(output)
455 result = {}
456 if dom:
457 GetNamedNodeText = gclient_utils.GetNamedNodeText
458 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
459 def C(item, f):
460 if item is not None: return f(item)
461 # /info/entry/
462 # url
463 # reposityory/(root|uuid)
464 # wc-info/(schedule|depth)
465 # commit/(author|date)
466 # str() the results because they may be returned as Unicode, which
467 # interferes with the higher layers matching up things in the deps
468 # dictionary.
469 # TODO(maruel): Fix at higher level instead (!)
470 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
471 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
472 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
473 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
474 'revision'),
475 int)
476 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
477 str)
478 # Differs across versions.
479 if result['Node Kind'] == 'dir':
480 result['Node Kind'] = 'directory'
481 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
482 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
483 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
484 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
485 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000486
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000487 @staticmethod
488 def CaptureHeadRevision(url):
489 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000490
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000491 Returns:
492 Int head revision
493 """
494 info = SVN.Capture(["info", "--xml", url], os.getcwd())
495 dom = xml.dom.minidom.parseString(info)
496 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000497
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000498 @staticmethod
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000499 def CaptureBaseRevision(cwd):
500 """Get the base revision of a SVN repository.
501
502 Returns:
503 Int base revision
504 """
505 info = SVN.Capture(["info", "--xml"], cwd)
506 dom = xml.dom.minidom.parseString(info)
507 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
508
509 @staticmethod
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000510 def CaptureStatus(files):
511 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000512
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000513 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000514
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000515 Returns an array of (status, file) tuples."""
516 command = ["status", "--xml"]
517 if not files:
518 pass
519 elif isinstance(files, basestring):
520 command.append(files)
521 else:
522 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000523
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000524 status_letter = {
525 None: ' ',
526 '': ' ',
527 'added': 'A',
528 'conflicted': 'C',
529 'deleted': 'D',
530 'external': 'X',
531 'ignored': 'I',
532 'incomplete': '!',
533 'merged': 'G',
534 'missing': '!',
535 'modified': 'M',
536 'none': ' ',
537 'normal': ' ',
538 'obstructed': '~',
539 'replaced': 'R',
540 'unversioned': '?',
541 }
542 dom = gclient_utils.ParseXML(SVN.Capture(command))
543 results = []
544 if dom:
545 # /status/target/entry/(wc-status|commit|author|date)
546 for target in dom.getElementsByTagName('target'):
547 #base_path = target.getAttribute('path')
548 for entry in target.getElementsByTagName('entry'):
549 file_path = entry.getAttribute('path')
550 wc_status = entry.getElementsByTagName('wc-status')
551 assert len(wc_status) == 1
552 # Emulate svn 1.5 status ouput...
553 statuses = [' '] * 7
554 # Col 0
555 xml_item_status = wc_status[0].getAttribute('item')
556 if xml_item_status in status_letter:
557 statuses[0] = status_letter[xml_item_status]
558 else:
559 raise Exception('Unknown item status "%s"; please implement me!' %
560 xml_item_status)
561 # Col 1
562 xml_props_status = wc_status[0].getAttribute('props')
563 if xml_props_status == 'modified':
564 statuses[1] = 'M'
565 elif xml_props_status == 'conflicted':
566 statuses[1] = 'C'
567 elif (not xml_props_status or xml_props_status == 'none' or
568 xml_props_status == 'normal'):
569 pass
570 else:
571 raise Exception('Unknown props status "%s"; please implement me!' %
572 xml_props_status)
573 # Col 2
574 if wc_status[0].getAttribute('wc-locked') == 'true':
575 statuses[2] = 'L'
576 # Col 3
577 if wc_status[0].getAttribute('copied') == 'true':
578 statuses[3] = '+'
579 # Col 4
580 if wc_status[0].getAttribute('switched') == 'true':
581 statuses[4] = 'S'
582 # TODO(maruel): Col 5 and 6
583 item = (''.join(statuses), file_path)
584 results.append(item)
585 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000586
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000587 @staticmethod
588 def IsMoved(filename):
589 """Determine if a file has been added through svn mv"""
590 info = SVN.CaptureInfo(filename)
591 return (info.get('Copied From URL') and
592 info.get('Copied From Rev') and
593 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000594
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000595 @staticmethod
596 def GetFileProperty(file, property_name):
597 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000598
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000599 Args:
600 file: The file to check
601 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000602
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000603 Returns:
604 The value of the property, which will be the empty string if the property
605 is not set on the file. If the file is not under version control, the
606 empty string is also returned.
607 """
608 output = SVN.Capture(["propget", property_name, file])
609 if (output.startswith("svn: ") and
610 output.endswith("is not under version control")):
611 return ""
612 else:
613 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000614
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000615 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000616 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000617 """Diffs a single file.
618
619 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000620 expected relative path.
621 full_move means that move or copy operations should completely recreate the
622 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000623 # Use svn info output instead of os.path.isdir because the latter fails
624 # when the file is deleted.
625 if SVN.CaptureInfo(filename).get("Node Kind") == "directory":
626 return None
627 # If the user specified a custom diff command in their svn config file,
628 # then it'll be used when we do svn diff, which we don't want to happen
629 # since we want the unified diff. Using --diff-cmd=diff doesn't always
630 # work, since they can have another diff executable in their path that
631 # gives different line endings. So we use a bogus temp directory as the
632 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000633 bogus_dir = tempfile.mkdtemp()
634 try:
635 # Grabs the diff data.
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000636 command = ["diff", "--config-dir", bogus_dir, filename]
maruel@chromium.orgce3c8622010-01-07 02:18:16 +0000637 if revision:
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000638 command.extend(['--revision', revision])
maruel@chromium.org0836c562010-01-22 01:10:06 +0000639 if SVN.IsMoved(filename):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000640 if full_move:
641 file_content = gclient_utils.FileRead(filename, 'rb')
642 # Prepend '+' to every lines.
643 file_content = ['+' + i for i in file_content.splitlines(True)]
644 nb_lines = len(file_content)
645 # We need to use / since patch on unix will fail otherwise.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000646 data = "Index: %s\n" % filename
647 data += '=' * 67 + '\n'
648 # Note: Should we use /dev/null instead?
649 data += "--- %s\n" % filename
650 data += "+++ %s\n" % filename
651 data += "@@ -0,0 +1,%d @@\n" % nb_lines
652 data += ''.join(file_content)
653 else:
maruel@chromium.org0836c562010-01-22 01:10:06 +0000654 # svn diff on a mv/cp'd file outputs nothing if there was no change.
655 data = SVN.Capture(command, None)
656 if not data:
657 # We put in an empty Index entry so upload.py knows about them.
658 data = "Index: %s\n" % filename
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000659 else:
maruel@chromium.org0836c562010-01-22 01:10:06 +0000660 data = SVN.Capture(command, None)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000661 finally:
662 shutil.rmtree(bogus_dir)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000663 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000664
665 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000666 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000667 """Returns a string containing the diff for the given file list.
668
669 The files in the list should either be absolute paths or relative to the
670 given root. If no root directory is provided, the repository root will be
671 used.
672 The diff will always use relative paths.
673 """
674 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000675 root = root or SVN.GetCheckoutRoot(previous_cwd)
676 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000677 def RelativePath(path, root):
678 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000679 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000680 return path[len(root):]
681 return path
682 try:
683 os.chdir(root)
684 diff = "".join(filter(None,
maruel@chromium.org0c401692010-01-07 02:44:20 +0000685 [SVN.DiffItem(RelativePath(f, root),
686 full_move=full_move,
687 revision=revision)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000688 for f in filenames]))
689 finally:
690 os.chdir(previous_cwd)
691 return diff
692
693
694 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000695 def GetEmail(repo_root):
696 """Retrieves the svn account which we assume is an email address."""
697 infos = SVN.CaptureInfo(repo_root)
698 uuid = infos.get('UUID')
699 root = infos.get('Repository Root')
700 if not root:
701 return None
702
703 # Should check for uuid but it is incorrectly saved for https creds.
704 realm = root.rsplit('/', 1)[0]
705 if root.startswith('https') or not uuid:
706 regexp = re.compile(r'<%s:\d+>.*' % realm)
707 else:
708 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
709 if regexp is None:
710 return None
711 if sys.platform.startswith('win'):
712 if not 'APPDATA' in os.environ:
713 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000714 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
715 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000716 else:
717 if not 'HOME' in os.environ:
718 return None
719 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
720 'svn.simple')
721 for credfile in os.listdir(auth_dir):
722 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
723 if regexp.match(cred_info.get('svn:realmstring')):
724 return cred_info.get('username')
725
726 @staticmethod
727 def ReadSimpleAuth(filename):
728 f = open(filename, 'r')
729 values = {}
730 def ReadOneItem(type):
731 m = re.match(r'%s (\d+)' % type, f.readline())
732 if not m:
733 return None
734 data = f.read(int(m.group(1)))
735 if f.read(1) != '\n':
736 return None
737 return data
738
739 while True:
740 key = ReadOneItem('K')
741 if not key:
742 break
743 value = ReadOneItem('V')
744 if not value:
745 break
746 values[key] = value
747 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000748
749 @staticmethod
750 def GetCheckoutRoot(directory):
751 """Returns the top level directory of the current repository.
752
753 The directory is returned as an absolute path.
754 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000755 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000756 infos = SVN.CaptureInfo(directory, print_error=False)
757 cur_dir_repo_root = infos.get("Repository Root")
758 if not cur_dir_repo_root:
759 return None
760
761 while True:
762 parent = os.path.dirname(directory)
763 if (SVN.CaptureInfo(parent, print_error=False).get(
764 "Repository Root") != cur_dir_repo_root):
765 break
766 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000767 return GetCasedPath(directory)