blob: 1b3b72f2c2cb731ac72a93861039ab108387d199 [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
nasser@codeaurora.org5d63eb82010-03-24 23:22:09 +0000497 def CaptureBaseRevision(cwd):
498 """Get the base revision of a SVN repository.
499
500 Returns:
501 Int base revision
502 """
503 info = SVN.Capture(["info", "--xml"], cwd)
504 dom = xml.dom.minidom.parseString(info)
505 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
506
507 @staticmethod
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000508 def CaptureStatus(files):
509 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000510
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000511 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000512
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000513 Returns an array of (status, file) tuples."""
514 command = ["status", "--xml"]
515 if not files:
516 pass
517 elif isinstance(files, basestring):
518 command.append(files)
519 else:
520 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000521
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000522 status_letter = {
523 None: ' ',
524 '': ' ',
525 'added': 'A',
526 'conflicted': 'C',
527 'deleted': 'D',
528 'external': 'X',
529 'ignored': 'I',
530 'incomplete': '!',
531 'merged': 'G',
532 'missing': '!',
533 'modified': 'M',
534 'none': ' ',
535 'normal': ' ',
536 'obstructed': '~',
537 'replaced': 'R',
538 'unversioned': '?',
539 }
540 dom = gclient_utils.ParseXML(SVN.Capture(command))
541 results = []
542 if dom:
543 # /status/target/entry/(wc-status|commit|author|date)
544 for target in dom.getElementsByTagName('target'):
545 #base_path = target.getAttribute('path')
546 for entry in target.getElementsByTagName('entry'):
547 file_path = entry.getAttribute('path')
548 wc_status = entry.getElementsByTagName('wc-status')
549 assert len(wc_status) == 1
550 # Emulate svn 1.5 status ouput...
551 statuses = [' '] * 7
552 # Col 0
553 xml_item_status = wc_status[0].getAttribute('item')
554 if xml_item_status in status_letter:
555 statuses[0] = status_letter[xml_item_status]
556 else:
557 raise Exception('Unknown item status "%s"; please implement me!' %
558 xml_item_status)
559 # Col 1
560 xml_props_status = wc_status[0].getAttribute('props')
561 if xml_props_status == 'modified':
562 statuses[1] = 'M'
563 elif xml_props_status == 'conflicted':
564 statuses[1] = 'C'
565 elif (not xml_props_status or xml_props_status == 'none' or
566 xml_props_status == 'normal'):
567 pass
568 else:
569 raise Exception('Unknown props status "%s"; please implement me!' %
570 xml_props_status)
571 # Col 2
572 if wc_status[0].getAttribute('wc-locked') == 'true':
573 statuses[2] = 'L'
574 # Col 3
575 if wc_status[0].getAttribute('copied') == 'true':
576 statuses[3] = '+'
577 # Col 4
578 if wc_status[0].getAttribute('switched') == 'true':
579 statuses[4] = 'S'
580 # TODO(maruel): Col 5 and 6
581 item = (''.join(statuses), file_path)
582 results.append(item)
583 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000584
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000585 @staticmethod
586 def IsMoved(filename):
587 """Determine if a file has been added through svn mv"""
588 info = SVN.CaptureInfo(filename)
589 return (info.get('Copied From URL') and
590 info.get('Copied From Rev') and
591 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000592
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000593 @staticmethod
594 def GetFileProperty(file, property_name):
595 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000596
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000597 Args:
598 file: The file to check
599 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000600
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000601 Returns:
602 The value of the property, which will be the empty string if the property
603 is not set on the file. If the file is not under version control, the
604 empty string is also returned.
605 """
606 output = SVN.Capture(["propget", property_name, file])
607 if (output.startswith("svn: ") and
608 output.endswith("is not under version control")):
609 return ""
610 else:
611 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000612
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000613 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000614 def DiffItem(filename, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000615 """Diffs a single file.
616
617 Be sure to be in the appropriate directory before calling to have the
maruel@chromium.orga9371762009-12-22 18:27:38 +0000618 expected relative path.
619 full_move means that move or copy operations should completely recreate the
620 files, usually in the prospect to apply the patch for a try job."""
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000621 # Use svn info output instead of os.path.isdir because the latter fails
622 # when the file is deleted.
623 if SVN.CaptureInfo(filename).get("Node Kind") == "directory":
624 return None
625 # If the user specified a custom diff command in their svn config file,
626 # then it'll be used when we do svn diff, which we don't want to happen
627 # since we want the unified diff. Using --diff-cmd=diff doesn't always
628 # work, since they can have another diff executable in their path that
629 # gives different line endings. So we use a bogus temp directory as the
630 # config directory, which gets around these problems.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000631 bogus_dir = tempfile.mkdtemp()
632 try:
633 # Grabs the diff data.
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000634 command = ["diff", "--config-dir", bogus_dir, filename]
maruel@chromium.orgce3c8622010-01-07 02:18:16 +0000635 if revision:
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000636 command.extend(['--revision', revision])
maruel@chromium.org0836c562010-01-22 01:10:06 +0000637 if SVN.IsMoved(filename):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000638 if full_move:
639 file_content = gclient_utils.FileRead(filename, 'rb')
640 # Prepend '+' to every lines.
641 file_content = ['+' + i for i in file_content.splitlines(True)]
642 nb_lines = len(file_content)
643 # We need to use / since patch on unix will fail otherwise.
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000644 data = "Index: %s\n" % filename
645 data += '=' * 67 + '\n'
646 # Note: Should we use /dev/null instead?
647 data += "--- %s\n" % filename
648 data += "+++ %s\n" % filename
649 data += "@@ -0,0 +1,%d @@\n" % nb_lines
650 data += ''.join(file_content)
651 else:
maruel@chromium.org0836c562010-01-22 01:10:06 +0000652 # svn diff on a mv/cp'd file outputs nothing if there was no change.
653 data = SVN.Capture(command, None)
654 if not data:
655 # We put in an empty Index entry so upload.py knows about them.
656 data = "Index: %s\n" % filename
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000657 else:
maruel@chromium.org0836c562010-01-22 01:10:06 +0000658 data = SVN.Capture(command, None)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000659 finally:
660 shutil.rmtree(bogus_dir)
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000661 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000662
663 @staticmethod
maruel@chromium.org1c7db8e2010-01-07 02:00:19 +0000664 def GenerateDiff(filenames, root=None, full_move=False, revision=None):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000665 """Returns a string containing the diff for the given file list.
666
667 The files in the list should either be absolute paths or relative to the
668 given root. If no root directory is provided, the repository root will be
669 used.
670 The diff will always use relative paths.
671 """
672 previous_cwd = os.getcwd()
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000673 root = root or SVN.GetCheckoutRoot(previous_cwd)
674 root = os.path.normcase(os.path.join(root, ''))
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000675 def RelativePath(path, root):
676 """We must use relative paths."""
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000677 if os.path.normcase(path).startswith(root):
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000678 return path[len(root):]
679 return path
680 try:
681 os.chdir(root)
682 diff = "".join(filter(None,
maruel@chromium.org0c401692010-01-07 02:44:20 +0000683 [SVN.DiffItem(RelativePath(f, root),
684 full_move=full_move,
685 revision=revision)
maruel@chromium.orgf2f9d552009-12-22 00:12:57 +0000686 for f in filenames]))
687 finally:
688 os.chdir(previous_cwd)
689 return diff
690
691
692 @staticmethod
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000693 def GetEmail(repo_root):
694 """Retrieves the svn account which we assume is an email address."""
695 infos = SVN.CaptureInfo(repo_root)
696 uuid = infos.get('UUID')
697 root = infos.get('Repository Root')
698 if not root:
699 return None
700
701 # Should check for uuid but it is incorrectly saved for https creds.
702 realm = root.rsplit('/', 1)[0]
703 if root.startswith('https') or not uuid:
704 regexp = re.compile(r'<%s:\d+>.*' % realm)
705 else:
706 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
707 if regexp is None:
708 return None
709 if sys.platform.startswith('win'):
710 if not 'APPDATA' in os.environ:
711 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000712 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
713 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000714 else:
715 if not 'HOME' in os.environ:
716 return None
717 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
718 'svn.simple')
719 for credfile in os.listdir(auth_dir):
720 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
721 if regexp.match(cred_info.get('svn:realmstring')):
722 return cred_info.get('username')
723
724 @staticmethod
725 def ReadSimpleAuth(filename):
726 f = open(filename, 'r')
727 values = {}
728 def ReadOneItem(type):
729 m = re.match(r'%s (\d+)' % type, f.readline())
730 if not m:
731 return None
732 data = f.read(int(m.group(1)))
733 if f.read(1) != '\n':
734 return None
735 return data
736
737 while True:
738 key = ReadOneItem('K')
739 if not key:
740 break
741 value = ReadOneItem('V')
742 if not value:
743 break
744 values[key] = value
745 return values
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000746
747 @staticmethod
748 def GetCheckoutRoot(directory):
749 """Returns the top level directory of the current repository.
750
751 The directory is returned as an absolute path.
752 """
maruel@chromium.orgf7ae6d52009-12-22 20:49:04 +0000753 directory = os.path.abspath(directory)
maruel@chromium.org94b1ee92009-12-19 20:27:20 +0000754 infos = SVN.CaptureInfo(directory, print_error=False)
755 cur_dir_repo_root = infos.get("Repository Root")
756 if not cur_dir_repo_root:
757 return None
758
759 while True:
760 parent = os.path.dirname(directory)
761 if (SVN.CaptureInfo(parent, print_error=False).get(
762 "Repository Root") != cur_dir_repo_root):
763 break
764 directory = parent
maruel@chromium.orgfd9cbbb2010-01-08 23:04:03 +0000765 return GetCasedPath(directory)