blob: 313ff783911c5f4b55f8ce4e8ffb91e92396a712 [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."""
73 command = ["diff", "--name-status", "-r", "%s.." % upstream_branch]
74 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])
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000175 if match:
176 svn_refs[match.group(1)] = ref
177
178 svn_branch = ''
179 if len(svn_refs) == 1:
180 # Only one svn branch exists -- seems like a good candidate.
181 svn_branch = svn_refs.values()[0]
182 elif len(svn_refs) > 1:
183 # We have more than one remote branch available. We don't
184 # want to go through all of history, so read a line from the
185 # pipe at a time.
186 # The -100 is an arbitrary limit so we don't search forever.
187 cmd = ['git', 'log', '-100', '--pretty=medium']
188 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=cwd)
189 for line in proc.stdout:
190 match = git_svn_re.match(line)
191 if match:
192 url = match.group(1)
193 if url in svn_refs:
194 svn_branch = svn_refs[url]
195 proc.stdout.close() # Cut pipe.
196 break
197 return svn_branch
198
199 @staticmethod
200 def FetchUpstreamTuple(cwd):
201 """Returns a tuple containg remote and remote ref,
202 e.g. 'origin', 'refs/heads/master'
203 """
204 remote = '.'
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000205 branch = GIT.GetBranch(cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000206 upstream_branch = None
207 upstream_branch = GIT.Capture(
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000208 ['config', 'branch.%s.merge' % branch], in_directory=cwd,
209 error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000210 if upstream_branch:
211 remote = GIT.Capture(
212 ['config', 'branch.%s.remote' % branch],
nasser@codeaurora.orgb65040a2010-02-01 16:29:14 +0000213 in_directory=cwd, error_ok=True)[0].strip()
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000214 else:
215 # Fall back on trying a git-svn upstream branch.
216 if GIT.IsGitSvn(cwd):
217 upstream_branch = GIT.GetSVNBranch(cwd)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000218 return remote, upstream_branch
219
220 @staticmethod
221 def GetUpstream(cwd):
222 """Gets the current branch's upstream branch."""
223 remote, upstream_branch = GIT.FetchUpstreamTuple(cwd)
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000224 if remote != '.':
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000225 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
226 return upstream_branch
227
228 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000229 def GenerateDiff(cwd, branch=None, branch_head='HEAD', full_move=False,
230 files=None):
maruel@chromium.orga9371762009-12-22 18:27:38 +0000231 """Diffs against the upstream branch or optionally another branch.
232
233 full_move means that move or copy operations should completely recreate the
234 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000235 if not branch:
236 branch = GIT.GetUpstream(cwd)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000237 command = ['diff-tree', '-p', '--no-prefix', branch, branch_head]
maruel@chromium.orga9371762009-12-22 18:27:38 +0000238 if not full_move:
239 command.append('-C')
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000240 # TODO(maruel): --binary support.
241 if files:
242 command.append('--')
243 command.extend(files)
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000244 diff = GIT.Capture(command, cwd)[0].splitlines(True)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000245 for i in range(len(diff)):
246 # In the case of added files, replace /dev/null with the path to the
247 # file being added.
248 if diff[i].startswith('--- /dev/null'):
249 diff[i] = '--- %s' % diff[i+1][4:]
250 return ''.join(diff)
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000251
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000252 @staticmethod
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000253 def GetDifferentFiles(cwd, branch=None, branch_head='HEAD'):
254 """Returns the list of modified files between two branches."""
255 if not branch:
256 branch = GIT.GetUpstream(cwd)
257 command = ['diff', '--name-only', branch, branch_head]
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000258 return GIT.Capture(command, cwd)[0].splitlines(False)
maruel@chromium.org8ede00e2010-01-12 14:35:28 +0000259
260 @staticmethod
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000261 def GetPatchName(cwd):
262 """Constructs a name for this patch."""
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000263 short_sha = GIT.Capture(['rev-parse', '--short=4', 'HEAD'], cwd)[0].strip()
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000264 return "%s-%s" % (GIT.GetBranch(cwd), short_sha)
265
266 @staticmethod
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000267 def GetCheckoutRoot(path):
268 """Returns the top level directory of a git checkout as an absolute path.
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000269 """
maruel@chromium.org7be5ef22010-01-30 22:31:50 +0000270 root = GIT.Capture(['rev-parse', '--show-cdup'], path)[0].strip()
maruel@chromium.org01d8c1d2010-01-07 01:56:59 +0000271 return os.path.abspath(os.path.join(path, root))
maruel@chromium.orgb24a8e12009-12-22 13:45:48 +0000272
maruel@chromium.orgd0f854a2010-03-11 19:35:53 +0000273 @staticmethod
274 def AssertVersion(min_version):
275 """Asserts git's version is at least min_version."""
276 def only_int(val):
277 if val.isdigit():
278 return int(val)
279 else:
280 return 0
281 current_version = GIT.Capture(['--version'])[0].split()[-1]
282 current_version_list = map(only_int, current_version.split('.'))
283 for min_ver in map(int, min_version.split('.')):
284 ver = current_version_list.pop(0)
285 if ver < min_ver:
286 return (False, current_version)
287 elif ver > min_ver:
288 return (True, current_version)
289 return (True, current_version)
290
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000291
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000292class SVN(object):
293 COMMAND = "svn"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000294
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000295 @staticmethod
296 def Run(args, in_directory):
297 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000298
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000299 Args:
300 args: A sequence of command line parameters to be passed to svn.
301 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000302
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000303 Raises:
304 Error: An error occurred while running the svn command.
305 """
306 c = [SVN.COMMAND]
307 c.extend(args)
maruel@chromium.org2185f002009-12-18 21:03:47 +0000308 # TODO(maruel): This is very gclient-specific.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000309 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000310
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000311 @staticmethod
312 def Capture(args, in_directory=None, print_error=True):
313 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000314
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000315 Args:
316 args: A sequence of command line parameters to be passed to svn.
317 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000318
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000319 Returns:
320 The output sent to stdout as a string.
321 """
322 c = [SVN.COMMAND]
323 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000324
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000325 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
326 # the svn.exe executable, but shell=True makes subprocess on Linux fail
327 # when it's called with a list because it only tries to execute the
328 # first string ("svn").
329 stderr = None
330 if not print_error:
331 stderr = subprocess.PIPE
332 return subprocess.Popen(c,
333 cwd=in_directory,
334 shell=(sys.platform == 'win32'),
335 stdout=subprocess.PIPE,
336 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000337
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000338 @staticmethod
339 def RunAndGetFileList(options, args, in_directory, file_list):
340 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000341
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000342 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000343
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000344 svn's stdout is parsed to collect a list of files checked out or updated.
345 These files are appended to file_list. svn's stdout is also printed to
346 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000347
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000348 Args:
349 options: command line options to gclient
350 args: A sequence of command line parameters to be passed to svn.
351 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000352
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000353 Raises:
354 Error: An error occurred while running the svn command.
355 """
356 command = [SVN.COMMAND]
357 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000358
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000359 # svn update and svn checkout use the same pattern: the first three columns
360 # are for file status, property status, and lock status. This is followed
361 # by two spaces, and then the path to the file.
362 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000363
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000364 # The first three columns of svn status are the same as for svn update and
365 # svn checkout. The next three columns indicate addition-with-history,
366 # switch, and remote lock status. This is followed by one space, and then
367 # the path to the file.
368 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000369
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000370 # args[0] must be a supported command. This will blow up if it's something
371 # else, which is good. Note that the patterns are only effective when
372 # these commands are used in their ordinary forms, the patterns are invalid
373 # for "svn status --show-updates", for example.
374 pattern = {
375 'checkout': update_pattern,
376 'status': status_pattern,
377 'update': update_pattern,
378 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000379 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000380 # Place an upper limit.
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000381 for _ in range(1, 10):
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000382 previous_list_len = len(file_list)
383 failure = []
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000384
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000385 def CaptureMatchingLines(line):
386 match = compiled_pattern.search(line)
387 if match:
388 file_list.append(match.group(1))
389 if line.startswith('svn: '):
maruel@chromium.org8599aa72010-02-08 20:27:14 +0000390 failure.append(line)
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000391
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000392 try:
393 SVN.RunAndFilterOutput(args,
394 in_directory,
395 options.verbose,
396 True,
397 CaptureMatchingLines)
398 except gclient_utils.Error:
maruel@chromium.org2de10252010-02-08 01:10:39 +0000399 # We enforce that some progress has been made or HTTP 502.
maruel@chromium.org55e724e2010-03-11 19:36:49 +0000400 if (filter(lambda x: '502 Bad Gateway' in x, failure) or
maruel@chromium.org2de10252010-02-08 01:10:39 +0000401 (len(failure) and len(file_list) > previous_list_len)):
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000402 if args[0] == 'checkout':
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000403 # An aborted checkout is now an update.
maruel@chromium.org2de10252010-02-08 01:10:39 +0000404 args = ['update'] + args[1:]
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000405 continue
maruel@chromium.org2de10252010-02-08 01:10:39 +0000406 # No progress was made or an unknown error we aren't sure, bail out.
maruel@chromium.org54d1f1a2010-01-08 19:53:47 +0000407 raise
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000408 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000409
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000410 @staticmethod
411 def RunAndFilterOutput(args,
412 in_directory,
413 print_messages,
414 print_stdout,
415 filter):
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000416 """Runs a command, optionally outputting to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000417
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000418 stdout is passed line-by-line to the given filter function. If
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000419 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000420
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000421 Args:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000422 args: A sequence of command line parameters to be passed.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000423 in_directory: The directory where svn is to be run.
424 print_messages: Whether to print status messages to stdout about
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000425 which commands are being run.
426 print_stdout: Whether to forward program's output to stdout.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000427 filter: A function taking one argument (a string) which will be
428 passed each line (with the ending newline character removed) of
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000429 program's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000430
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000431 Raises:
maruel@chromium.orgee4071d2009-12-22 22:25:37 +0000432 gclient_utils.Error: An error occurred while running the command.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000433 """
434 command = [SVN.COMMAND]
435 command.extend(args)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000436 gclient_utils.SubprocessCallAndFilter(command,
437 in_directory,
438 print_messages,
439 print_stdout,
440 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000441
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000442 @staticmethod
443 def CaptureInfo(relpath, in_directory=None, print_error=True):
444 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000445
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000446 Args:
447 relpath: The directory where the working copy resides relative to
448 the directory given by in_directory.
449 in_directory: The directory where svn is to be run.
450 """
451 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
452 dom = gclient_utils.ParseXML(output)
453 result = {}
454 if dom:
455 GetNamedNodeText = gclient_utils.GetNamedNodeText
456 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
457 def C(item, f):
458 if item is not None: return f(item)
459 # /info/entry/
460 # url
461 # reposityory/(root|uuid)
462 # wc-info/(schedule|depth)
463 # commit/(author|date)
464 # str() the results because they may be returned as Unicode, which
465 # interferes with the higher layers matching up things in the deps
466 # dictionary.
467 # TODO(maruel): Fix at higher level instead (!)
468 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
469 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
470 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
471 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
472 'revision'),
473 int)
474 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
475 str)
476 # Differs across versions.
477 if result['Node Kind'] == 'dir':
478 result['Node Kind'] = 'directory'
479 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
480 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
481 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
482 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
483 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000484
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000485 @staticmethod
486 def CaptureHeadRevision(url):
487 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000488
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000489 Returns:
490 Int head revision
491 """
492 info = SVN.Capture(["info", "--xml", url], os.getcwd())
493 dom = xml.dom.minidom.parseString(info)
494 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000495
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000496 @staticmethod
497 def CaptureStatus(files):
498 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000499
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000500 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000501
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000502 Returns an array of (status, file) tuples."""
503 command = ["status", "--xml"]
504 if not files:
505 pass
506 elif isinstance(files, basestring):
507 command.append(files)
508 else:
509 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000510
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000511 status_letter = {
512 None: ' ',
513 '': ' ',
514 'added': 'A',
515 'conflicted': 'C',
516 'deleted': 'D',
517 'external': 'X',
518 'ignored': 'I',
519 'incomplete': '!',
520 'merged': 'G',
521 'missing': '!',
522 'modified': 'M',
523 'none': ' ',
524 'normal': ' ',
525 'obstructed': '~',
526 'replaced': 'R',
527 'unversioned': '?',
528 }
529 dom = gclient_utils.ParseXML(SVN.Capture(command))
530 results = []
531 if dom:
532 # /status/target/entry/(wc-status|commit|author|date)
533 for target in dom.getElementsByTagName('target'):
534 #base_path = target.getAttribute('path')
535 for entry in target.getElementsByTagName('entry'):
536 file_path = entry.getAttribute('path')
537 wc_status = entry.getElementsByTagName('wc-status')
538 assert len(wc_status) == 1
539 # Emulate svn 1.5 status ouput...
540 statuses = [' '] * 7
541 # Col 0
542 xml_item_status = wc_status[0].getAttribute('item')
543 if xml_item_status in status_letter:
544 statuses[0] = status_letter[xml_item_status]
545 else:
546 raise Exception('Unknown item status "%s"; please implement me!' %
547 xml_item_status)
548 # Col 1
549 xml_props_status = wc_status[0].getAttribute('props')
550 if xml_props_status == 'modified':
551 statuses[1] = 'M'
552 elif xml_props_status == 'conflicted':
553 statuses[1] = 'C'
554 elif (not xml_props_status or xml_props_status == 'none' or
555 xml_props_status == 'normal'):
556 pass
557 else:
558 raise Exception('Unknown props status "%s"; please implement me!' %
559 xml_props_status)
560 # Col 2
561 if wc_status[0].getAttribute('wc-locked') == 'true':
562 statuses[2] = 'L'
563 # Col 3
564 if wc_status[0].getAttribute('copied') == 'true':
565 statuses[3] = '+'
566 # Col 4
567 if wc_status[0].getAttribute('switched') == 'true':
568 statuses[4] = 'S'
569 # TODO(maruel): Col 5 and 6
570 item = (''.join(statuses), file_path)
571 results.append(item)
572 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000573
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000574 @staticmethod
575 def IsMoved(filename):
576 """Determine if a file has been added through svn mv"""
577 info = SVN.CaptureInfo(filename)
578 return (info.get('Copied From URL') and
579 info.get('Copied From Rev') and
580 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000581
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000582 @staticmethod
583 def GetFileProperty(file, property_name):
584 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000585
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000586 Args:
587 file: The file to check
588 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000589
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000590 Returns:
591 The value of the property, which will be the empty string if the property
592 is not set on the file. If the file is not under version control, the
593 empty string is also returned.
594 """
595 output = SVN.Capture(["propget", property_name, file])
596 if (output.startswith("svn: ") and
597 output.endswith("is not under version control")):
598 return ""
599 else:
600 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000601
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000602 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000603 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000604 """Diffs a single file.
605
606 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000607 expected relative path.
608 full_move means that move or copy operations should completely recreate the
609 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000610 # Use svn info output instead of os.path.isdir because the latter fails
611 # when the file is deleted.
612 if SVN.CaptureInfo(filename).get("Node Kind") == "directory":
613 return None
614 # If the user specified a custom diff command in their svn config file,
615 # then it'll be used when we do svn diff, which we don't want to happen
616 # since we want the unified diff. Using --diff-cmd=diff doesn't always
617 # work, since they can have another diff executable in their path that
618 # gives different line endings. So we use a bogus temp directory as the
619 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000620 bogus_dir = tempfile.mkdtemp()
621 try:
622 # Grabs the diff data.
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000623 command = ["diff", "--config-dir", bogus_dir, filename]
maruel@chromium.orgce3c8622010-01-07 02:18:16 +0000624 if revision:
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000625 command.extend(['--revision', revision])
maruel@chromium.org0836c562010-01-22 01:10:06 +0000626 if SVN.IsMoved(filename):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000627 if full_move:
628 file_content = gclient_utils.FileRead(filename, 'rb')
629 # Prepend '+' to every lines.
630 file_content = ['+' + i for i in file_content.splitlines(True)]
631 nb_lines = len(file_content)
632 # We need to use / since patch on unix will fail otherwise.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000633 data = "Index: %s\n" % filename
634 data += '=' * 67 + '\n'
635 # Note: Should we use /dev/null instead?
636 data += "--- %s\n" % filename
637 data += "+++ %s\n" % filename
638 data += "@@ -0,0 +1,%d @@\n" % nb_lines
639 data += ''.join(file_content)
640 else:
maruel@chromium.org0836c562010-01-22 01:10:06 +0000641 # svn diff on a mv/cp'd file outputs nothing if there was no change.
642 data = SVN.Capture(command, None)
643 if not data:
644 # We put in an empty Index entry so upload.py knows about them.
645 data = "Index: %s\n" % filename
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000646 else:
maruel@chromium.org0836c562010-01-22 01:10:06 +0000647 data = SVN.Capture(command, None)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000648 finally:
649 shutil.rmtree(bogus_dir)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000650 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000651
652 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000653 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000654 """Returns a string containing the diff for the given file list.
655
656 The files in the list should either be absolute paths or relative to the
657 given root. If no root directory is provided, the repository root will be
658 used.
659 The diff will always use relative paths.
660 """
661 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000662 root = root or SVN.GetCheckoutRoot(previous_cwd)
663 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000664 def RelativePath(path, root):
665 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000666 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000667 return path[len(root):]
668 return path
669 try:
670 os.chdir(root)
671 diff = "".join(filter(None,
maruel@chromium.org0c401692010-01-07 02:44:20 +0000672 [SVN.DiffItem(RelativePath(f, root),
673 full_move=full_move,
674 revision=revision)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000675 for f in filenames]))
676 finally:
677 os.chdir(previous_cwd)
678 return diff
679
680
681 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000682 def GetEmail(repo_root):
683 """Retrieves the svn account which we assume is an email address."""
684 infos = SVN.CaptureInfo(repo_root)
685 uuid = infos.get('UUID')
686 root = infos.get('Repository Root')
687 if not root:
688 return None
689
690 # Should check for uuid but it is incorrectly saved for https creds.
691 realm = root.rsplit('/', 1)[0]
692 if root.startswith('https') or not uuid:
693 regexp = re.compile(r'<%s:\d+>.*' % realm)
694 else:
695 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
696 if regexp is None:
697 return None
698 if sys.platform.startswith('win'):
699 if not 'APPDATA' in os.environ:
700 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000701 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
702 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000703 else:
704 if not 'HOME' in os.environ:
705 return None
706 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
707 'svn.simple')
708 for credfile in os.listdir(auth_dir):
709 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
710 if regexp.match(cred_info.get('svn:realmstring')):
711 return cred_info.get('username')
712
713 @staticmethod
714 def ReadSimpleAuth(filename):
715 f = open(filename, 'r')
716 values = {}
717 def ReadOneItem(type):
718 m = re.match(r'%s (\d+)' % type, f.readline())
719 if not m:
720 return None
721 data = f.read(int(m.group(1)))
722 if f.read(1) != '\n':
723 return None
724 return data
725
726 while True:
727 key = ReadOneItem('K')
728 if not key:
729 break
730 value = ReadOneItem('V')
731 if not value:
732 break
733 values[key] = value
734 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000735
736 @staticmethod
737 def GetCheckoutRoot(directory):
738 """Returns the top level directory of the current repository.
739
740 The directory is returned as an absolute path.
741 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000742 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000743 infos = SVN.CaptureInfo(directory, print_error=False)
744 cur_dir_repo_root = infos.get("Repository Root")
745 if not cur_dir_repo_root:
746 return None
747
748 while True:
749 parent = os.path.dirname(directory)
750 if (SVN.CaptureInfo(parent, print_error=False).get(
751 "Repository Root") != cur_dir_repo_root):
752 break
753 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000754 return GetCasedPath(directory)