blob: 58f26c6a9855f4b85ea8132d7f535d1bb7a6a2a7 [file] [log] [blame]
[email protected]d5800f12009-11-12 20:03:431# 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
[email protected]5aeb7dd2009-11-17 18:09:015"""SCM-specific utility classes."""
[email protected]d5800f12009-11-12 20:03:436
7import os
8import re
9import subprocess
10import sys
[email protected]5aeb7dd2009-11-17 18:09:0111import tempfile
[email protected]d5800f12009-11-12 20:03:4312import xml.dom.minidom
13
14import gclient_utils
15
16
[email protected]5aeb7dd2009-11-17 18:09:0117class GIT(object):
18 COMMAND = "git"
[email protected]d5800f12009-11-12 20:03:4319
[email protected]5aeb7dd2009-11-17 18:09:0120 @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]
[email protected]d5800f12009-11-12 20:03:4346
47
[email protected]5aeb7dd2009-11-17 18:09:0148 @staticmethod
49 def CaptureStatus(files, upstream_branch='origin'):
50 """Returns git status.
[email protected]d5800f12009-11-12 20:03:4351
[email protected]5aeb7dd2009-11-17 18:09:0152 @files can be a string (one file) or a list of files.
[email protected]d5800f12009-11-12 20:03:4353
[email protected]5aeb7dd2009-11-17 18:09:0154 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)
[email protected]d5800f12009-11-12 20:03:4362
[email protected]5aeb7dd2009-11-17 18:09:0163 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
[email protected]d5800f12009-11-12 20:03:4372
[email protected]c78f2462009-11-21 01:20:5773 @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
[email protected]d5800f12009-11-12 20:03:4380
[email protected]5aeb7dd2009-11-17 18:09:0181class SVN(object):
82 COMMAND = "svn"
[email protected]d5800f12009-11-12 20:03:4383
[email protected]5aeb7dd2009-11-17 18:09:0184 @staticmethod
85 def Run(args, in_directory):
86 """Runs svn, sending output to stdout.
[email protected]d5800f12009-11-12 20:03:4387
[email protected]5aeb7dd2009-11-17 18:09:0188 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.
[email protected]d5800f12009-11-12 20:03:4391
[email protected]5aeb7dd2009-11-17 18:09:0192 Raises:
93 Error: An error occurred while running the svn command.
94 """
95 c = [SVN.COMMAND]
96 c.extend(args)
[email protected]d5800f12009-11-12 20:03:4397
[email protected]5aeb7dd2009-11-17 18:09:0198 gclient_utils.SubprocessCall(c, in_directory)
[email protected]d5800f12009-11-12 20:03:4399
[email protected]5aeb7dd2009-11-17 18:09:01100 @staticmethod
101 def Capture(args, in_directory=None, print_error=True):
102 """Runs svn, capturing output sent to stdout as a string.
[email protected]d5800f12009-11-12 20:03:43103
[email protected]5aeb7dd2009-11-17 18:09:01104 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.
[email protected]d5800f12009-11-12 20:03:43107
[email protected]5aeb7dd2009-11-17 18:09:01108 Returns:
109 The output sent to stdout as a string.
110 """
111 c = [SVN.COMMAND]
112 c.extend(args)
[email protected]d5800f12009-11-12 20:03:43113
[email protected]5aeb7dd2009-11-17 18:09:01114 # *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]
[email protected]d5800f12009-11-12 20:03:43126
[email protected]5aeb7dd2009-11-17 18:09:01127 @staticmethod
128 def RunAndGetFileList(options, args, in_directory, file_list):
129 """Runs svn checkout, update, or status, output to stdout.
[email protected]d5800f12009-11-12 20:03:43130
[email protected]5aeb7dd2009-11-17 18:09:01131 The first item in args must be either "checkout", "update", or "status".
[email protected]d5800f12009-11-12 20:03:43132
[email protected]5aeb7dd2009-11-17 18:09:01133 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.
[email protected]d5800f12009-11-12 20:03:43136
[email protected]5aeb7dd2009-11-17 18:09:01137 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.
[email protected]d5800f12009-11-12 20:03:43141
[email protected]5aeb7dd2009-11-17 18:09:01142 Raises:
143 Error: An error occurred while running the svn command.
144 """
145 command = [SVN.COMMAND]
146 command.extend(args)
[email protected]d5800f12009-11-12 20:03:43147
[email protected]5aeb7dd2009-11-17 18:09:01148 # 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 = '^... (.*)$'
[email protected]d5800f12009-11-12 20:03:43152
[email protected]5aeb7dd2009-11-17 18:09:01153 # 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 = '^...... (.*)$'
[email protected]d5800f12009-11-12 20:03:43158
[email protected]5aeb7dd2009-11-17 18:09:01159 # 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]]
[email protected]5aeb7dd2009-11-17 18:09:01168 compiled_pattern = re.compile(pattern)
[email protected]b71b67e2009-11-24 20:48:19169 # 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
[email protected]d5800f12009-11-12 20:03:43196
[email protected]5aeb7dd2009-11-17 18:09:01197 @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.
[email protected]d5800f12009-11-12 20:03:43205
[email protected]5aeb7dd2009-11-17 18:09:01206 The first item in args must be either "checkout", "update",
207 "status", or "diff".
[email protected]d5800f12009-11-12 20:03:43208
[email protected]5aeb7dd2009-11-17 18:09:01209 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.
[email protected]d5800f12009-11-12 20:03:43211
[email protected]5aeb7dd2009-11-17 18:09:01212 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.
[email protected]d5800f12009-11-12 20:03:43221
[email protected]5aeb7dd2009-11-17 18:09:01222 Raises:
223 Error: An error occurred while running the svn command.
224 """
225 command = [SVN.COMMAND]
226 command.extend(args)
[email protected]d5800f12009-11-12 20:03:43227
[email protected]5aeb7dd2009-11-17 18:09:01228 gclient_utils.SubprocessCallAndFilter(command,
229 in_directory,
230 print_messages,
231 print_stdout,
232 filter=filter)
[email protected]d5800f12009-11-12 20:03:43233
[email protected]5aeb7dd2009-11-17 18:09:01234 @staticmethod
235 def CaptureInfo(relpath, in_directory=None, print_error=True):
236 """Returns a dictionary from the svn info output for the given file.
[email protected]d5800f12009-11-12 20:03:43237
[email protected]5aeb7dd2009-11-17 18:09:01238 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
[email protected]d5800f12009-11-12 20:03:43276
[email protected]5aeb7dd2009-11-17 18:09:01277 @staticmethod
278 def CaptureHeadRevision(url):
279 """Get the head revision of a SVN repository.
[email protected]d5800f12009-11-12 20:03:43280
[email protected]5aeb7dd2009-11-17 18:09:01281 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')
[email protected]d5800f12009-11-12 20:03:43287
[email protected]5aeb7dd2009-11-17 18:09:01288 @staticmethod
289 def CaptureStatus(files):
290 """Returns the svn 1.5 svn status emulated output.
[email protected]d5800f12009-11-12 20:03:43291
[email protected]5aeb7dd2009-11-17 18:09:01292 @files can be a string (one file) or a list of files.
[email protected]d5800f12009-11-12 20:03:43293
[email protected]5aeb7dd2009-11-17 18:09:01294 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)
[email protected]d5800f12009-11-12 20:03:43302
[email protected]5aeb7dd2009-11-17 18:09:01303 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
[email protected]d5800f12009-11-12 20:03:43365
[email protected]5aeb7dd2009-11-17 18:09:01366 @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')
[email protected]d5800f12009-11-12 20:03:43373
[email protected]5aeb7dd2009-11-17 18:09:01374 @staticmethod
375 def GetFileProperty(file, property_name):
376 """Returns the value of an SVN property for the given file.
[email protected]d5800f12009-11-12 20:03:43377
[email protected]5aeb7dd2009-11-17 18:09:01378 Args:
379 file: The file to check
380 property_name: The name of the SVN property, e.g. "svn:mime-type"
[email protected]d5800f12009-11-12 20:03:43381
[email protected]5aeb7dd2009-11-17 18:09:01382 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
[email protected]d5800f12009-11-12 20:03:43393
[email protected]5aeb7dd2009-11-17 18:09:01394 @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)
[email protected]d5800f12009-11-12 20:03:43416
[email protected]5aeb7dd2009-11-17 18:09:01417 # We know the diff will be incorrectly formatted. Fix it.
418 if SVN.IsMoved(filename):
419 # The file is "new" in the patch sense. Generate a homebrew diff.
420 # We can't use ReadFile() since it's not using binary mode.
421 file_handle = open(filename, 'rb')
422 file_content = file_handle.read()
423 file_handle.close()
424 # Prepend '+' to every lines.
425 file_content = ['+' + i for i in file_content.splitlines(True)]
426 nb_lines = len(file_content)
427 # We need to use / since patch on unix will fail otherwise.
428 filename = filename.replace('\\', '/')
429 data = "Index: %s\n" % filename
430 data += ("============================================================="
431 "======\n")
432 # Note: Should we use /dev/null instead?
433 data += "--- %s\n" % filename
434 data += "+++ %s\n" % filename
435 data += "@@ -0,0 +1,%d @@\n" % nb_lines
436 data += ''.join(file_content)
437 return data
[email protected]c78f2462009-11-21 01:20:57438
439 @staticmethod
440 def GetEmail(repo_root):
441 """Retrieves the svn account which we assume is an email address."""
442 infos = SVN.CaptureInfo(repo_root)
443 uuid = infos.get('UUID')
444 root = infos.get('Repository Root')
445 if not root:
446 return None
447
448 # Should check for uuid but it is incorrectly saved for https creds.
449 realm = root.rsplit('/', 1)[0]
450 if root.startswith('https') or not uuid:
451 regexp = re.compile(r'<%s:\d+>.*' % realm)
452 else:
453 regexp = re.compile(r'<%s:\d+> %s' % (realm, uuid))
454 if regexp is None:
455 return None
456 if sys.platform.startswith('win'):
457 if not 'APPDATA' in os.environ:
458 return None
[email protected]720d9f32009-11-21 17:38:57459 auth_dir = os.path.join(os.environ['APPDATA'], 'Subversion', 'auth',
460 'svn.simple')
[email protected]c78f2462009-11-21 01:20:57461 else:
462 if not 'HOME' in os.environ:
463 return None
464 auth_dir = os.path.join(os.environ['HOME'], '.subversion', 'auth',
465 'svn.simple')
466 for credfile in os.listdir(auth_dir):
467 cred_info = SVN.ReadSimpleAuth(os.path.join(auth_dir, credfile))
468 if regexp.match(cred_info.get('svn:realmstring')):
469 return cred_info.get('username')
470
471 @staticmethod
472 def ReadSimpleAuth(filename):
473 f = open(filename, 'r')
474 values = {}
475 def ReadOneItem(type):
476 m = re.match(r'%s (\d+)' % type, f.readline())
477 if not m:
478 return None
479 data = f.read(int(m.group(1)))
480 if f.read(1) != '\n':
481 return None
482 return data
483
484 while True:
485 key = ReadOneItem('K')
486 if not key:
487 break
488 value = ReadOneItem('V')
489 if not value:
490 break
491 values[key] = value
492 return values