blob: 8dc127e33eafea38c44e6f8ea59acea2a251ba35 [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
7import os
8import re
9import subprocess
10import sys
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000011import tempfile
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000012import xml.dom.minidom
13
14import gclient_utils
15
16
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000017class GIT(object):
18 COMMAND = "git"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000019
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000020 @staticmethod
21 def Capture(args, in_directory=None, print_error=True):
22 """Runs git, capturing output sent to stdout as a string.
23
24 Args:
25 args: A sequence of command line parameters to be passed to git.
26 in_directory: The directory where git is to be run.
27
28 Returns:
29 The output sent to stdout as a string.
30 """
31 c = [GIT.COMMAND]
32 c.extend(args)
33
34 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
35 # the git.exe executable, but shell=True makes subprocess on Linux fail
36 # when it's called with a list because it only tries to execute the
37 # first string ("git").
38 stderr = None
39 if not print_error:
40 stderr = subprocess.PIPE
41 return subprocess.Popen(c,
42 cwd=in_directory,
43 shell=sys.platform.startswith('win'),
44 stdout=subprocess.PIPE,
45 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000046
47
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000048 @staticmethod
49 def CaptureStatus(files, upstream_branch='origin'):
50 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000051
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000052 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000053
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000054 Returns an array of (status, file) tuples."""
55 command = ["diff", "--name-status", "-r", "%s.." % upstream_branch]
56 if not files:
57 pass
58 elif isinstance(files, basestring):
59 command.append(files)
60 else:
61 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000062
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000063 status = GIT.Capture(command).rstrip()
64 results = []
65 if status:
66 for statusline in status.split('\n'):
67 m = re.match('^(\w)\t(.+)$', statusline)
68 if not m:
69 raise Exception("status currently unsupported: %s" % statusline)
70 results.append(('%s ' % m.group(1), m.group(2)))
71 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000072
maruel@chromium.orgc78f2462009-11-21 01:20:57 +000073 @staticmethod
74 def GetEmail(repo_root):
75 """Retrieves the user email address if known."""
76 # We could want to look at the svn cred when it has a svn remote but it
77 # should be fine for now, users should simply configure their git settings.
78 return GIT.Capture(['config', 'user.email'], repo_root).strip()
79
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000080
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000081class SVN(object):
82 COMMAND = "svn"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000083
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000084 @staticmethod
85 def Run(args, in_directory):
86 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000087
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000088 Args:
89 args: A sequence of command line parameters to be passed to svn.
90 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000091
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000092 Raises:
93 Error: An error occurred while running the svn command.
94 """
95 c = [SVN.COMMAND]
96 c.extend(args)
maruel@chromium.org2185f002009-12-18 21:03:47 +000097 # TODO(maruel): This is very gclient-specific.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000098 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000099
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000100 @staticmethod
101 def Capture(args, in_directory=None, print_error=True):
102 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000103
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000104 Args:
105 args: A sequence of command line parameters to be passed to svn.
106 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000107
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000108 Returns:
109 The output sent to stdout as a string.
110 """
111 c = [SVN.COMMAND]
112 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000113
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000114 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
115 # the svn.exe executable, but shell=True makes subprocess on Linux fail
116 # when it's called with a list because it only tries to execute the
117 # first string ("svn").
118 stderr = None
119 if not print_error:
120 stderr = subprocess.PIPE
121 return subprocess.Popen(c,
122 cwd=in_directory,
123 shell=(sys.platform == 'win32'),
124 stdout=subprocess.PIPE,
125 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000126
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000127 @staticmethod
128 def RunAndGetFileList(options, args, in_directory, file_list):
129 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000130
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000131 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000132
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000133 svn's stdout is parsed to collect a list of files checked out or updated.
134 These files are appended to file_list. svn's stdout is also printed to
135 sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000136
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000137 Args:
138 options: command line options to gclient
139 args: A sequence of command line parameters to be passed to svn.
140 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000141
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000142 Raises:
143 Error: An error occurred while running the svn command.
144 """
145 command = [SVN.COMMAND]
146 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000147
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000148 # svn update and svn checkout use the same pattern: the first three columns
149 # are for file status, property status, and lock status. This is followed
150 # by two spaces, and then the path to the file.
151 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000152
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000153 # The first three columns of svn status are the same as for svn update and
154 # svn checkout. The next three columns indicate addition-with-history,
155 # switch, and remote lock status. This is followed by one space, and then
156 # the path to the file.
157 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000158
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000159 # args[0] must be a supported command. This will blow up if it's something
160 # else, which is good. Note that the patterns are only effective when
161 # these commands are used in their ordinary forms, the patterns are invalid
162 # for "svn status --show-updates", for example.
163 pattern = {
164 'checkout': update_pattern,
165 'status': status_pattern,
166 'update': update_pattern,
167 }[args[0]]
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000168 compiled_pattern = re.compile(pattern)
maruel@chromium.orgb71b67e2009-11-24 20:48:19 +0000169 # Place an upper limit.
170 for i in range(1, 10):
171 previous_list_len = len(file_list)
172 failure = []
173 def CaptureMatchingLines(line):
174 match = compiled_pattern.search(line)
175 if match:
176 file_list.append(match.group(1))
177 if line.startswith('svn: '):
178 # We can't raise an exception. We can't alias a variable. Use a cheap
179 # way.
180 failure.append(True)
181 try:
182 SVN.RunAndFilterOutput(args,
183 in_directory,
184 options.verbose,
185 True,
186 CaptureMatchingLines)
187 except gclient_utils.Error:
188 # We enforce that some progress has been made.
189 if len(failure) and len(file_list) > previous_list_len:
190 if args[0] == 'checkout':
191 args = args[:]
192 # An aborted checkout is now an update.
193 args[0] = 'update'
194 continue
195 break
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000196
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000197 @staticmethod
198 def RunAndFilterOutput(args,
199 in_directory,
200 print_messages,
201 print_stdout,
202 filter):
203 """Runs svn checkout, update, status, or diff, optionally outputting
204 to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000205
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000206 The first item in args must be either "checkout", "update",
207 "status", or "diff".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000208
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000209 svn's stdout is passed line-by-line to the given filter function. If
210 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000211
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000212 Args:
213 args: A sequence of command line parameters to be passed to svn.
214 in_directory: The directory where svn is to be run.
215 print_messages: Whether to print status messages to stdout about
216 which Subversion commands are being run.
217 print_stdout: Whether to forward Subversion's output to stdout.
218 filter: A function taking one argument (a string) which will be
219 passed each line (with the ending newline character removed) of
220 Subversion's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000221
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000222 Raises:
223 Error: An error occurred while running the svn command.
224 """
225 command = [SVN.COMMAND]
226 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000227
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000228 gclient_utils.SubprocessCallAndFilter(command,
229 in_directory,
230 print_messages,
231 print_stdout,
232 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000233
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000234 @staticmethod
235 def CaptureInfo(relpath, in_directory=None, print_error=True):
236 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000237
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000238 Args:
239 relpath: The directory where the working copy resides relative to
240 the directory given by in_directory.
241 in_directory: The directory where svn is to be run.
242 """
243 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
244 dom = gclient_utils.ParseXML(output)
245 result = {}
246 if dom:
247 GetNamedNodeText = gclient_utils.GetNamedNodeText
248 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
249 def C(item, f):
250 if item is not None: return f(item)
251 # /info/entry/
252 # url
253 # reposityory/(root|uuid)
254 # wc-info/(schedule|depth)
255 # commit/(author|date)
256 # str() the results because they may be returned as Unicode, which
257 # interferes with the higher layers matching up things in the deps
258 # dictionary.
259 # TODO(maruel): Fix at higher level instead (!)
260 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
261 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
262 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
263 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
264 'revision'),
265 int)
266 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
267 str)
268 # Differs across versions.
269 if result['Node Kind'] == 'dir':
270 result['Node Kind'] = 'directory'
271 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
272 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
273 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
274 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
275 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000276
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000277 @staticmethod
278 def CaptureHeadRevision(url):
279 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000280
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000281 Returns:
282 Int head revision
283 """
284 info = SVN.Capture(["info", "--xml", url], os.getcwd())
285 dom = xml.dom.minidom.parseString(info)
286 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000287
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000288 @staticmethod
289 def CaptureStatus(files):
290 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000291
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000292 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000293
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000294 Returns an array of (status, file) tuples."""
295 command = ["status", "--xml"]
296 if not files:
297 pass
298 elif isinstance(files, basestring):
299 command.append(files)
300 else:
301 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000302
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000303 status_letter = {
304 None: ' ',
305 '': ' ',
306 'added': 'A',
307 'conflicted': 'C',
308 'deleted': 'D',
309 'external': 'X',
310 'ignored': 'I',
311 'incomplete': '!',
312 'merged': 'G',
313 'missing': '!',
314 'modified': 'M',
315 'none': ' ',
316 'normal': ' ',
317 'obstructed': '~',
318 'replaced': 'R',
319 'unversioned': '?',
320 }
321 dom = gclient_utils.ParseXML(SVN.Capture(command))
322 results = []
323 if dom:
324 # /status/target/entry/(wc-status|commit|author|date)
325 for target in dom.getElementsByTagName('target'):
326 #base_path = target.getAttribute('path')
327 for entry in target.getElementsByTagName('entry'):
328 file_path = entry.getAttribute('path')
329 wc_status = entry.getElementsByTagName('wc-status')
330 assert len(wc_status) == 1
331 # Emulate svn 1.5 status ouput...
332 statuses = [' '] * 7
333 # Col 0
334 xml_item_status = wc_status[0].getAttribute('item')
335 if xml_item_status in status_letter:
336 statuses[0] = status_letter[xml_item_status]
337 else:
338 raise Exception('Unknown item status "%s"; please implement me!' %
339 xml_item_status)
340 # Col 1
341 xml_props_status = wc_status[0].getAttribute('props')
342 if xml_props_status == 'modified':
343 statuses[1] = 'M'
344 elif xml_props_status == 'conflicted':
345 statuses[1] = 'C'
346 elif (not xml_props_status or xml_props_status == 'none' or
347 xml_props_status == 'normal'):
348 pass
349 else:
350 raise Exception('Unknown props status "%s"; please implement me!' %
351 xml_props_status)
352 # Col 2
353 if wc_status[0].getAttribute('wc-locked') == 'true':
354 statuses[2] = 'L'
355 # Col 3
356 if wc_status[0].getAttribute('copied') == 'true':
357 statuses[3] = '+'
358 # Col 4
359 if wc_status[0].getAttribute('switched') == 'true':
360 statuses[4] = 'S'
361 # TODO(maruel): Col 5 and 6
362 item = (''.join(statuses), file_path)
363 results.append(item)
364 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000365
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000366 @staticmethod
367 def IsMoved(filename):
368 """Determine if a file has been added through svn mv"""
369 info = SVN.CaptureInfo(filename)
370 return (info.get('Copied From URL') and
371 info.get('Copied From Rev') and
372 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000373
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000374 @staticmethod
375 def GetFileProperty(file, property_name):
376 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000377
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000378 Args:
379 file: The file to check
380 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000381
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000382 Returns:
383 The value of the property, which will be the empty string if the property
384 is not set on the file. If the file is not under version control, the
385 empty string is also returned.
386 """
387 output = SVN.Capture(["propget", property_name, file])
388 if (output.startswith("svn: ") and
389 output.endswith("is not under version control")):
390 return ""
391 else:
392 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000393
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000394 @staticmethod
395 def DiffItem(filename):
396 """Diff a single file"""
397 # Use svn info output instead of os.path.isdir because the latter fails
398 # when the file is deleted.
399 if SVN.CaptureInfo(filename).get("Node Kind") == "directory":
400 return None
401 # If the user specified a custom diff command in their svn config file,
402 # then it'll be used when we do svn diff, which we don't want to happen
403 # since we want the unified diff. Using --diff-cmd=diff doesn't always
404 # work, since they can have another diff executable in their path that
405 # gives different line endings. So we use a bogus temp directory as the
406 # config directory, which gets around these problems.
407 if sys.platform.startswith("win"):
408 parent_dir = tempfile.gettempdir()
409 else:
410 parent_dir = sys.path[0] # tempdir is not secure.
411 bogus_dir = os.path.join(parent_dir, "temp_svn_config")
412 if not os.path.exists(bogus_dir):
413 os.mkdir(bogus_dir)
414 # Grabs the diff data.
415 data = SVN.Capture(["diff", "--config-dir", bogus_dir, filename], None)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000416
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000417 # We know the diff will be incorrectly formatted. Fix it.
418 if SVN.IsMoved(filename):
maruel@chromium.org0fca4f32009-12-18 15:14:34 +0000419 file_content = gclient_utils.FileRead(filename, 'rb')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000420 # Prepend '+' to every lines.
421 file_content = ['+' + i for i in file_content.splitlines(True)]
422 nb_lines = len(file_content)
423 # We need to use / since patch on unix will fail otherwise.
424 filename = filename.replace('\\', '/')
425 data = "Index: %s\n" % filename
426 data += ("============================================================="
427 "======\n")
428 # Note: Should we use /dev/null instead?
429 data += "--- %s\n" % filename
430 data += "+++ %s\n" % filename
431 data += "@@ -0,0 +1,%d @@\n" % nb_lines
432 data += ''.join(file_content)
433 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000434
435 @staticmethod
436 def GetEmail(repo_root):
437 """Retrieves the svn account which we assume is an email address."""
438 infos = SVN.CaptureInfo(repo_root)
439 uuid = infos.get('UUID')
440 root = infos.get('Repository Root')
441 if not root:
442 return None
443
444 # Should check for uuid but it is incorrectly saved for https creds.
445 realm = root.rsplit('/', 1)[0]
446 if root.startswith('https') or not uuid:
447 regexp = re.compile(r'<%s:\d+>.*' % realm)
448 else:
449 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
450 if regexp is None:
451 return None
452 if sys.platform.startswith('win'):
453 if not 'APPDATA' in os.environ:
454 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000455 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
456 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000457 else:
458 if not 'HOME' in os.environ:
459 return None
460 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
461 'svn.simple')
462 for credfile in os.listdir(auth_dir):
463 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
464 if regexp.match(cred_info.get('svn:realmstring')):
465 return cred_info.get('username')
466
467 @staticmethod
468 def ReadSimpleAuth(filename):
469 f = open(filename, 'r')
470 values = {}
471 def ReadOneItem(type):
472 m = re.match(r'%s (\d+)' % type, f.readline())
473 if not m:
474 return None
475 data = f.read(int(m.group(1)))
476 if f.read(1) != '\n':
477 return None
478 return data
479
480 while True:
481 key = ReadOneItem('K')
482 if not key:
483 break
484 value = ReadOneItem('V')
485 if not value:
486 break
487 values[key] = value
488 return values