blob: 21363d38c847700b6b2185b882f4141b98151a5d [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
73
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000074class SVN(object):
75 COMMAND = "svn"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000076
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000077 @staticmethod
78 def Run(args, in_directory):
79 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000080
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000081 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.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000084
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000085 Raises:
86 Error: An error occurred while running the svn command.
87 """
88 c = [SVN.COMMAND]
89 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000090
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000091 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000092
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000093 @staticmethod
94 def Capture(args, in_directory=None, print_error=True):
95 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000096
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000097 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.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000100
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000101 Returns:
102 The output sent to stdout as a string.
103 """
104 c = [SVN.COMMAND]
105 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000106
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000107 # *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]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000119
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000120 @staticmethod
121 def RunAndGetFileList(options, args, in_directory, file_list):
122 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000123
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000124 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000125
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000126 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.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000129
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000130 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.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000134
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000135 Raises:
136 Error: An error occurred while running the svn command.
137 """
138 command = [SVN.COMMAND]
139 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000140
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000141 # 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 = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000145
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000146 # 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 = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000151
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000152 # 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]]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000161
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000162 compiled_pattern = re.compile(pattern)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000163
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000164 def CaptureMatchingLines(line):
165 match = compiled_pattern.search(line)
166 if match:
167 file_list.append(match.group(1))
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000168
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000169 SVN.RunAndFilterOutput(args,
170 in_directory,
171 options.verbose,
172 True,
173 CaptureMatchingLines)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000174
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000175 @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.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000183
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000184 The first item in args must be either "checkout", "update",
185 "status", or "diff".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000186
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000187 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.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000189
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000190 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.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000199
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000200 Raises:
201 Error: An error occurred while running the svn command.
202 """
203 command = [SVN.COMMAND]
204 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000205
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000206 gclient_utils.SubprocessCallAndFilter(command,
207 in_directory,
208 print_messages,
209 print_stdout,
210 filter=filter)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000211
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000212 @staticmethod
213 def CaptureInfo(relpath, in_directory=None, print_error=True):
214 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000215
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000216 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
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000254
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000255 @staticmethod
256 def CaptureHeadRevision(url):
257 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000258
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000259 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')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000265
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000266 @staticmethod
267 def CaptureStatus(files):
268 """Returns the svn 1.5 svn status emulated output.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000269
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000270 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000271
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000272 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)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000280
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000281 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
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000343
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000344 @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')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000351
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000352 @staticmethod
353 def GetFileProperty(file, property_name):
354 """Returns the value of an SVN property for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000355
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000356 Args:
357 file: The file to check
358 property_name: The name of the SVN property, e.g. "svn:mime-type"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000359
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000360 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
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000371
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000372 @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)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000394
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000395 # 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