blob: 21363d38c847700b6b2185b882f4141b98151a5d [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
73
[email protected]5aeb7dd2009-11-17 18:09:0174class SVN(object):
75 COMMAND = "svn"
[email protected]d5800f12009-11-12 20:03:4376
[email protected]5aeb7dd2009-11-17 18:09:0177 @staticmethod
78 def Run(args, in_directory):
79 """Runs svn, sending output to stdout.
[email protected]d5800f12009-11-12 20:03:4380
[email protected]5aeb7dd2009-11-17 18:09:0181 Args:
82 args: A sequence of command line parameters to be passed to svn.
83 in_directory: The directory where svn is to be run.
[email protected]d5800f12009-11-12 20:03:4384
[email protected]5aeb7dd2009-11-17 18:09:0185 Raises:
86 Error: An error occurred while running the svn command.
87 """
88 c = [SVN.COMMAND]
89 c.extend(args)
[email protected]d5800f12009-11-12 20:03:4390
[email protected]5aeb7dd2009-11-17 18:09:0191 gclient_utils.SubprocessCall(c, in_directory)
[email protected]d5800f12009-11-12 20:03:4392
[email protected]5aeb7dd2009-11-17 18:09:0193 @staticmethod
94 def Capture(args, in_directory=None, print_error=True):
95 """Runs svn, capturing output sent to stdout as a string.
[email protected]d5800f12009-11-12 20:03:4396
[email protected]5aeb7dd2009-11-17 18:09:0197 Args:
98 args: A sequence of command line parameters to be passed to svn.
99 in_directory: The directory where svn is to be run.
[email protected]d5800f12009-11-12 20:03:43100
[email protected]5aeb7dd2009-11-17 18:09:01101 Returns:
102 The output sent to stdout as a string.
103 """
104 c = [SVN.COMMAND]
105 c.extend(args)
[email protected]d5800f12009-11-12 20:03:43106
[email protected]5aeb7dd2009-11-17 18:09:01107 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
108 # the svn.exe executable, but shell=True makes subprocess on Linux fail
109 # when it's called with a list because it only tries to execute the
110 # first string ("svn").
111 stderr = None
112 if not print_error:
113 stderr = subprocess.PIPE
114 return subprocess.Popen(c,
115 cwd=in_directory,
116 shell=(sys.platform == 'win32'),
117 stdout=subprocess.PIPE,
118 stderr=stderr).communicate()[0]
[email protected]d5800f12009-11-12 20:03:43119
[email protected]5aeb7dd2009-11-17 18:09:01120 @staticmethod
121 def RunAndGetFileList(options, args, in_directory, file_list):
122 """Runs svn checkout, update, or status, output to stdout.
[email protected]d5800f12009-11-12 20:03:43123
[email protected]5aeb7dd2009-11-17 18:09:01124 The first item in args must be either "checkout", "update", or "status".
[email protected]d5800f12009-11-12 20:03:43125
[email protected]5aeb7dd2009-11-17 18:09:01126 svn's stdout is parsed to collect a list of files checked out or updated.
127 These files are appended to file_list. svn's stdout is also printed to
128 sys.stdout as in Run.
[email protected]d5800f12009-11-12 20:03:43129
[email protected]5aeb7dd2009-11-17 18:09:01130 Args:
131 options: command line options to gclient
132 args: A sequence of command line parameters to be passed to svn.
133 in_directory: The directory where svn is to be run.
[email protected]d5800f12009-11-12 20:03:43134
[email protected]5aeb7dd2009-11-17 18:09:01135 Raises:
136 Error: An error occurred while running the svn command.
137 """
138 command = [SVN.COMMAND]
139 command.extend(args)
[email protected]d5800f12009-11-12 20:03:43140
[email protected]5aeb7dd2009-11-17 18:09:01141 # svn update and svn checkout use the same pattern: the first three columns
142 # are for file status, property status, and lock status. This is followed
143 # by two spaces, and then the path to the file.
144 update_pattern = '^... (.*)$'
[email protected]d5800f12009-11-12 20:03:43145
[email protected]5aeb7dd2009-11-17 18:09:01146 # The first three columns of svn status are the same as for svn update and
147 # svn checkout. The next three columns indicate addition-with-history,
148 # switch, and remote lock status. This is followed by one space, and then
149 # the path to the file.
150 status_pattern = '^...... (.*)$'
[email protected]d5800f12009-11-12 20:03:43151
[email protected]5aeb7dd2009-11-17 18:09:01152 # args[0] must be a supported command. This will blow up if it's something
153 # else, which is good. Note that the patterns are only effective when
154 # these commands are used in their ordinary forms, the patterns are invalid
155 # for "svn status --show-updates", for example.
156 pattern = {
157 'checkout': update_pattern,
158 'status': status_pattern,
159 'update': update_pattern,
160 }[args[0]]
[email protected]d5800f12009-11-12 20:03:43161
[email protected]5aeb7dd2009-11-17 18:09:01162 compiled_pattern = re.compile(pattern)
[email protected]d5800f12009-11-12 20:03:43163
[email protected]5aeb7dd2009-11-17 18:09:01164 def CaptureMatchingLines(line):
165 match = compiled_pattern.search(line)
166 if match:
167 file_list.append(match.group(1))
[email protected]d5800f12009-11-12 20:03:43168
[email protected]5aeb7dd2009-11-17 18:09:01169 SVN.RunAndFilterOutput(args,
170 in_directory,
171 options.verbose,
172 True,
173 CaptureMatchingLines)
[email protected]d5800f12009-11-12 20:03:43174
[email protected]5aeb7dd2009-11-17 18:09:01175 @staticmethod
176 def RunAndFilterOutput(args,
177 in_directory,
178 print_messages,
179 print_stdout,
180 filter):
181 """Runs svn checkout, update, status, or diff, optionally outputting
182 to stdout.
[email protected]d5800f12009-11-12 20:03:43183
[email protected]5aeb7dd2009-11-17 18:09:01184 The first item in args must be either "checkout", "update",
185 "status", or "diff".
[email protected]d5800f12009-11-12 20:03:43186
[email protected]5aeb7dd2009-11-17 18:09:01187 svn's stdout is passed line-by-line to the given filter function. If
188 print_stdout is true, it is also printed to sys.stdout as in Run.
[email protected]d5800f12009-11-12 20:03:43189
[email protected]5aeb7dd2009-11-17 18:09:01190 Args:
191 args: A sequence of command line parameters to be passed to svn.
192 in_directory: The directory where svn is to be run.
193 print_messages: Whether to print status messages to stdout about
194 which Subversion commands are being run.
195 print_stdout: Whether to forward Subversion's output to stdout.
196 filter: A function taking one argument (a string) which will be
197 passed each line (with the ending newline character removed) of
198 Subversion's output for filtering.
[email protected]d5800f12009-11-12 20:03:43199
[email protected]5aeb7dd2009-11-17 18:09:01200 Raises:
201 Error: An error occurred while running the svn command.
202 """
203 command = [SVN.COMMAND]
204 command.extend(args)
[email protected]d5800f12009-11-12 20:03:43205
[email protected]5aeb7dd2009-11-17 18:09:01206 gclient_utils.SubprocessCallAndFilter(command,
207 in_directory,
208 print_messages,
209 print_stdout,
210 filter=filter)
[email protected]d5800f12009-11-12 20:03:43211
[email protected]5aeb7dd2009-11-17 18:09:01212 @staticmethod
213 def CaptureInfo(relpath, in_directory=None, print_error=True):
214 """Returns a dictionary from the svn info output for the given file.
[email protected]d5800f12009-11-12 20:03:43215
[email protected]5aeb7dd2009-11-17 18:09:01216 Args:
217 relpath: The directory where the working copy resides relative to
218 the directory given by in_directory.
219 in_directory: The directory where svn is to be run.
220 """
221 output = SVN.Capture(["info", "--xml", relpath], in_directory, print_error)
222 dom = gclient_utils.ParseXML(output)
223 result = {}
224 if dom:
225 GetNamedNodeText = gclient_utils.GetNamedNodeText
226 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
227 def C(item, f):
228 if item is not None: return f(item)
229 # /info/entry/
230 # url
231 # reposityory/(root|uuid)
232 # wc-info/(schedule|depth)
233 # commit/(author|date)
234 # str() the results because they may be returned as Unicode, which
235 # interferes with the higher layers matching up things in the deps
236 # dictionary.
237 # TODO(maruel): Fix at higher level instead (!)
238 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
239 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
240 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
241 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry',
242 'revision'),
243 int)
244 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
245 str)
246 # Differs across versions.
247 if result['Node Kind'] == 'dir':
248 result['Node Kind'] = 'directory'
249 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
250 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
251 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
252 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
253 return result
[email protected]d5800f12009-11-12 20:03:43254
[email protected]5aeb7dd2009-11-17 18:09:01255 @staticmethod
256 def CaptureHeadRevision(url):
257 """Get the head revision of a SVN repository.
[email protected]d5800f12009-11-12 20:03:43258
[email protected]5aeb7dd2009-11-17 18:09:01259 Returns:
260 Int head revision
261 """
262 info = SVN.Capture(["info", "--xml", url], os.getcwd())
263 dom = xml.dom.minidom.parseString(info)
264 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
[email protected]d5800f12009-11-12 20:03:43265
[email protected]5aeb7dd2009-11-17 18:09:01266 @staticmethod
267 def CaptureStatus(files):
268 """Returns the svn 1.5 svn status emulated output.
[email protected]d5800f12009-11-12 20:03:43269
[email protected]5aeb7dd2009-11-17 18:09:01270 @files can be a string (one file) or a list of files.
[email protected]d5800f12009-11-12 20:03:43271
[email protected]5aeb7dd2009-11-17 18:09:01272 Returns an array of (status, file) tuples."""
273 command = ["status", "--xml"]
274 if not files:
275 pass
276 elif isinstance(files, basestring):
277 command.append(files)
278 else:
279 command.extend(files)
[email protected]d5800f12009-11-12 20:03:43280
[email protected]5aeb7dd2009-11-17 18:09:01281 status_letter = {
282 None: ' ',
283 '': ' ',
284 'added': 'A',
285 'conflicted': 'C',
286 'deleted': 'D',
287 'external': 'X',
288 'ignored': 'I',
289 'incomplete': '!',
290 'merged': 'G',
291 'missing': '!',
292 'modified': 'M',
293 'none': ' ',
294 'normal': ' ',
295 'obstructed': '~',
296 'replaced': 'R',
297 'unversioned': '?',
298 }
299 dom = gclient_utils.ParseXML(SVN.Capture(command))
300 results = []
301 if dom:
302 # /status/target/entry/(wc-status|commit|author|date)
303 for target in dom.getElementsByTagName('target'):
304 #base_path = target.getAttribute('path')
305 for entry in target.getElementsByTagName('entry'):
306 file_path = entry.getAttribute('path')
307 wc_status = entry.getElementsByTagName('wc-status')
308 assert len(wc_status) == 1
309 # Emulate svn 1.5 status ouput...
310 statuses = [' '] * 7
311 # Col 0
312 xml_item_status = wc_status[0].getAttribute('item')
313 if xml_item_status in status_letter:
314 statuses[0] = status_letter[xml_item_status]
315 else:
316 raise Exception('Unknown item status "%s"; please implement me!' %
317 xml_item_status)
318 # Col 1
319 xml_props_status = wc_status[0].getAttribute('props')
320 if xml_props_status == 'modified':
321 statuses[1] = 'M'
322 elif xml_props_status == 'conflicted':
323 statuses[1] = 'C'
324 elif (not xml_props_status or xml_props_status == 'none' or
325 xml_props_status == 'normal'):
326 pass
327 else:
328 raise Exception('Unknown props status "%s"; please implement me!' %
329 xml_props_status)
330 # Col 2
331 if wc_status[0].getAttribute('wc-locked') == 'true':
332 statuses[2] = 'L'
333 # Col 3
334 if wc_status[0].getAttribute('copied') == 'true':
335 statuses[3] = '+'
336 # Col 4
337 if wc_status[0].getAttribute('switched') == 'true':
338 statuses[4] = 'S'
339 # TODO(maruel): Col 5 and 6
340 item = (''.join(statuses), file_path)
341 results.append(item)
342 return results
[email protected]d5800f12009-11-12 20:03:43343
[email protected]5aeb7dd2009-11-17 18:09:01344 @staticmethod
345 def IsMoved(filename):
346 """Determine if a file has been added through svn mv"""
347 info = SVN.CaptureInfo(filename)
348 return (info.get('Copied From URL') and
349 info.get('Copied From Rev') and
350 info.get('Schedule') == 'add')
[email protected]d5800f12009-11-12 20:03:43351
[email protected]5aeb7dd2009-11-17 18:09:01352 @staticmethod
353 def GetFileProperty(file, property_name):
354 """Returns the value of an SVN property for the given file.
[email protected]d5800f12009-11-12 20:03:43355
[email protected]5aeb7dd2009-11-17 18:09:01356 Args:
357 file: The file to check
358 property_name: The name of the SVN property, e.g. "svn:mime-type"
[email protected]d5800f12009-11-12 20:03:43359
[email protected]5aeb7dd2009-11-17 18:09:01360 Returns:
361 The value of the property, which will be the empty string if the property
362 is not set on the file. If the file is not under version control, the
363 empty string is also returned.
364 """
365 output = SVN.Capture(["propget", property_name, file])
366 if (output.startswith("svn: ") and
367 output.endswith("is not under version control")):
368 return ""
369 else:
370 return output
[email protected]d5800f12009-11-12 20:03:43371
[email protected]5aeb7dd2009-11-17 18:09:01372 @staticmethod
373 def DiffItem(filename):
374 """Diff a single file"""
375 # Use svn info output instead of os.path.isdir because the latter fails
376 # when the file is deleted.
377 if SVN.CaptureInfo(filename).get("Node Kind") == "directory":
378 return None
379 # If the user specified a custom diff command in their svn config file,
380 # then it'll be used when we do svn diff, which we don't want to happen
381 # since we want the unified diff. Using --diff-cmd=diff doesn't always
382 # work, since they can have another diff executable in their path that
383 # gives different line endings. So we use a bogus temp directory as the
384 # config directory, which gets around these problems.
385 if sys.platform.startswith("win"):
386 parent_dir = tempfile.gettempdir()
387 else:
388 parent_dir = sys.path[0] # tempdir is not secure.
389 bogus_dir = os.path.join(parent_dir, "temp_svn_config")
390 if not os.path.exists(bogus_dir):
391 os.mkdir(bogus_dir)
392 # Grabs the diff data.
393 data = SVN.Capture(["diff", "--config-dir", bogus_dir, filename], None)
[email protected]d5800f12009-11-12 20:03:43394
[email protected]5aeb7dd2009-11-17 18:09:01395 # We know the diff will be incorrectly formatted. Fix it.
396 if SVN.IsMoved(filename):
397 # The file is "new" in the patch sense. Generate a homebrew diff.
398 # We can't use ReadFile() since it's not using binary mode.
399 file_handle = open(filename, 'rb')
400 file_content = file_handle.read()
401 file_handle.close()
402 # Prepend '+' to every lines.
403 file_content = ['+' + i for i in file_content.splitlines(True)]
404 nb_lines = len(file_content)
405 # We need to use / since patch on unix will fail otherwise.
406 filename = filename.replace('\\', '/')
407 data = "Index: %s\n" % filename
408 data += ("============================================================="
409 "======\n")
410 # Note: Should we use /dev/null instead?
411 data += "--- %s\n" % filename
412 data += "+++ %s\n" % filename
413 data += "@@ -0,0 +1,%d @@\n" % nb_lines
414 data += ''.join(file_content)
415 return data