blob: 99dccf97496c8334cf8f8ad5a588721093e7572c [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.orgd5800f12009-11-12 20:03:43 +000097
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.orgd5800f12009-11-12 20:03:43 +0000168
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000169 compiled_pattern = re.compile(pattern)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000170
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000171 def CaptureMatchingLines(line):
172 match = compiled_pattern.search(line)
173 if match:
174 file_list.append(match.group(1))
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000175
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000176 SVN.RunAndFilterOutput(args,
177 in_directory,
178 options.verbose,
179 True,
180 CaptureMatchingLines)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000181
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000182 @staticmethod
183 def RunAndFilterOutput(args,
184 in_directory,
185 print_messages,
186 print_stdout,
187 filter):
188 """Runs svn checkout, update, status, or diff, optionally outputting
189 to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000190
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000191 The first item in args must be either "checkout", "update",
192 "status", or "diff".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000193
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000194 svn's stdout is passed line-by-line to the given filter function. If
195 print_stdout is true, it is also printed to sys.stdout as in Run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000196
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000197 Args:
198 args: A sequence of command line parameters to be passed to svn.
199 in_directory: The directory where svn is to be run.
200 print_messages: Whether to print status messages to stdout about
201 which Subversion commands are being run.
202 print_stdout: Whether to forward Subversion's output to stdout.
203 filter: A function taking one argument (a string) which will be
204 passed each line (with the ending newline character removed) of
205 Subversion's output for filtering.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000206
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000207 Raises:
208 Error: An error occurred while running the svn command.
209 """
210 command = [SVN.COMMAND]
211 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000212
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000213 gclient_utils.SubprocessCallAndFilter(command,
214 in_directory,
215 print_messages,
216 print_stdout,
217 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000218
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000219 @staticmethod
220 def CaptureInfo(relpath, in_directory=None, print_error=True):
221 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000222
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000223 Args:
224 relpath: The directory where the working copy resides relative to
225 the directory given by in_directory.
226 in_directory: The directory where svn is to be run.
227 """
228 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
229 dom = gclient_utils.ParseXML(output)
230 result = {}
231 if dom:
232 GetNamedNodeText = gclient_utils.GetNamedNodeText
233 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
234 def C(item, f):
235 if item is not None: return f(item)
236 # /info/entry/
237 # url
238 # reposityory/(root|uuid)
239 # wc-info/(schedule|depth)
240 # commit/(author|date)
241 # str() the results because they may be returned as Unicode, which
242 # interferes with the higher layers matching up things in the deps
243 # dictionary.
244 # TODO(maruel): Fix at higher level instead (!)
245 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
246 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
247 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
248 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
249 'revision'),
250 int)
251 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
252 str)
253 # Differs across versions.
254 if result['Node Kind'] == 'dir':
255 result['Node Kind'] = 'directory'
256 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
257 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
258 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
259 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
260 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000261
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000262 @staticmethod
263 def CaptureHeadRevision(url):
264 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000265
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000266 Returns:
267 Int head revision
268 """
269 info = SVN.Capture(["info", "--xml", url], os.getcwd())
270 dom = xml.dom.minidom.parseString(info)
271 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000272
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000273 @staticmethod
274 def CaptureStatus(files):
275 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000276
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000277 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000278
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000279 Returns an array of (status, file) tuples."""
280 command = ["status", "--xml"]
281 if not files:
282 pass
283 elif isinstance(files, basestring):
284 command.append(files)
285 else:
286 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000287
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000288 status_letter = {
289 None: ' ',
290 '': ' ',
291 'added': 'A',
292 'conflicted': 'C',
293 'deleted': 'D',
294 'external': 'X',
295 'ignored': 'I',
296 'incomplete': '!',
297 'merged': 'G',
298 'missing': '!',
299 'modified': 'M',
300 'none': ' ',
301 'normal': ' ',
302 'obstructed': '~',
303 'replaced': 'R',
304 'unversioned': '?',
305 }
306 dom = gclient_utils.ParseXML(SVN.Capture(command))
307 results = []
308 if dom:
309 # /status/target/entry/(wc-status|commit|author|date)
310 for target in dom.getElementsByTagName('target'):
311 #base_path = target.getAttribute('path')
312 for entry in target.getElementsByTagName('entry'):
313 file_path = entry.getAttribute('path')
314 wc_status = entry.getElementsByTagName('wc-status')
315 assert len(wc_status) == 1
316 # Emulate svn 1.5 status ouput...
317 statuses = [' '] * 7
318 # Col 0
319 xml_item_status = wc_status[0].getAttribute('item')
320 if xml_item_status in status_letter:
321 statuses[0] = status_letter[xml_item_status]
322 else:
323 raise Exception('Unknown item status "%s"; please implement me!' %
324 xml_item_status)
325 # Col 1
326 xml_props_status = wc_status[0].getAttribute('props')
327 if xml_props_status == 'modified':
328 statuses[1] = 'M'
329 elif xml_props_status == 'conflicted':
330 statuses[1] = 'C'
331 elif (not xml_props_status or xml_props_status == 'none' or
332 xml_props_status == 'normal'):
333 pass
334 else:
335 raise Exception('Unknown props status "%s"; please implement me!' %
336 xml_props_status)
337 # Col 2
338 if wc_status[0].getAttribute('wc-locked') == 'true':
339 statuses[2] = 'L'
340 # Col 3
341 if wc_status[0].getAttribute('copied') == 'true':
342 statuses[3] = '+'
343 # Col 4
344 if wc_status[0].getAttribute('switched') == 'true':
345 statuses[4] = 'S'
346 # TODO(maruel): Col 5 and 6
347 item = (''.join(statuses), file_path)
348 results.append(item)
349 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000350
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000351 @staticmethod
352 def IsMoved(filename):
353 """Determine if a file has been added through svn mv"""
354 info = SVN.CaptureInfo(filename)
355 return (info.get('Copied From URL') and
356 info.get('Copied From Rev') and
357 info.get('Schedule') == 'add')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000358
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000359 @staticmethod
360 def GetFileProperty(file, property_name):
361 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000362
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000363 Args:
364 file: The file to check
365 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000366
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000367 Returns:
368 The value of the property, which will be the empty string if the property
369 is not set on the file. If the file is not under version control, the
370 empty string is also returned.
371 """
372 output = SVN.Capture(["propget", property_name, file])
373 if (output.startswith("svn: ") and
374 output.endswith("is not under version control")):
375 return ""
376 else:
377 return output
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000378
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000379 @staticmethod
380 def DiffItem(filename):
381 """Diff a single file"""
382 # Use svn info output instead of os.path.isdir because the latter fails
383 # when the file is deleted.
384 if SVN.CaptureInfo(filename).get("Node Kind") == "directory":
385 return None
386 # If the user specified a custom diff command in their svn config file,
387 # then it'll be used when we do svn diff, which we don't want to happen
388 # since we want the unified diff. Using --diff-cmd=diff doesn't always
389 # work, since they can have another diff executable in their path that
390 # gives different line endings. So we use a bogus temp directory as the
391 # config directory, which gets around these problems.
392 if sys.platform.startswith("win"):
393 parent_dir = tempfile.gettempdir()
394 else:
395 parent_dir = sys.path[0] # tempdir is not secure.
396 bogus_dir = os.path.join(parent_dir, "temp_svn_config")
397 if not os.path.exists(bogus_dir):
398 os.mkdir(bogus_dir)
399 # Grabs the diff data.
400 data = SVN.Capture(["diff", "--config-dir", bogus_dir, filename], None)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000401
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000402 # We know the diff will be incorrectly formatted. Fix it.
403 if SVN.IsMoved(filename):
404 # The file is "new" in the patch sense. Generate a homebrew diff.
405 # We can't use ReadFile() since it's not using binary mode.
406 file_handle = open(filename, 'rb')
407 file_content = file_handle.read()
408 file_handle.close()
409 # Prepend '+' to every lines.
410 file_content = ['+' + i for i in file_content.splitlines(True)]
411 nb_lines = len(file_content)
412 # We need to use / since patch on unix will fail otherwise.
413 filename = filename.replace('\\', '/')
414 data = "Index: %s\n" % filename
415 data += ("============================================================="
416 "======\n")
417 # Note: Should we use /dev/null instead?
418 data += "--- %s\n" % filename
419 data += "+++ %s\n" % filename
420 data += "@@ -0,0 +1,%d @@\n" % nb_lines
421 data += ''.join(file_content)
422 return data
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000423
424 @staticmethod
425 def GetEmail(repo_root):
426 """Retrieves the svn account which we assume is an email address."""
427 infos = SVN.CaptureInfo(repo_root)
428 uuid = infos.get('UUID')
429 root = infos.get('Repository Root')
430 if not root:
431 return None
432
433 # Should check for uuid but it is incorrectly saved for https creds.
434 realm = root.rsplit('/', 1)[0]
435 if root.startswith('https') or not uuid:
436 regexp = re.compile(r'<%s:\d+>.*' % realm)
437 else:
438 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
439 if regexp is None:
440 return None
441 if sys.platform.startswith('win'):
442 if not 'APPDATA' in os.environ:
443 return None
maruel@chromium.org720d9f32009-11-21 17:38:57 +0000444 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
445 'svn.simple')
maruel@chromium.orgc78f2462009-11-21 01:20:57 +0000446 else:
447 if not 'HOME' in os.environ:
448 return None
449 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
450 'svn.simple')
451 for credfile in os.listdir(auth_dir):
452 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
453 if regexp.match(cred_info.get('svn:realmstring')):
454 return cred_info.get('username')
455
456 @staticmethod
457 def ReadSimpleAuth(filename):
458 f = open(filename, 'r')
459 values = {}
460 def ReadOneItem(type):
461 m = re.match(r'%s (\d+)' % type, f.readline())
462 if not m:
463 return None
464 data = f.read(int(m.group(1)))
465 if f.read(1) != '\n':
466 return None
467 return data
468
469 while True:
470 key = ReadOneItem('K')
471 if not key:
472 break
473 value = ReadOneItem('V')
474 if not value:
475 break
476 values[key] = value
477 return values