blob: 12848152abcde2efa2de76c51085450f98d30ebc [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.org261eeb52009-11-16 18:25:45 +00005"""SCM-specific functions."""
maruel@chromium.orgd5800f12009-11-12 20:03:43 +00006
7import os
8import re
9import subprocess
10import sys
11import xml.dom.minidom
12
13import gclient_utils
14
15
maruel@chromium.org261eeb52009-11-16 18:25:45 +000016SVN_COMMAND = "svn"
17GIT_COMMAND = "git"
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000018
maruel@chromium.org261eeb52009-11-16 18:25:45 +000019# -----------------------------------------------------------------------------
20# Git utils:
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000021
22
maruel@chromium.org261eeb52009-11-16 18:25:45 +000023def CaptureGit(args, in_directory=None, print_error=True):
24 """Runs git, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000025
maruel@chromium.org261eeb52009-11-16 18:25:45 +000026 Args:
27 args: A sequence of command line parameters to be passed to git.
28 in_directory: The directory where git is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000029
maruel@chromium.org261eeb52009-11-16 18:25:45 +000030 Returns:
31 The output sent to stdout as a string.
32 """
33 c = [GIT_COMMAND]
34 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000035
maruel@chromium.org261eeb52009-11-16 18:25:45 +000036 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
37 # the git.exe executable, but shell=True makes subprocess on Linux fail
38 # when it's called with a list because it only tries to execute the
39 # first string ("git").
40 stderr = None
41 if not print_error:
42 stderr = subprocess.PIPE
43 return subprocess.Popen(c,
44 cwd=in_directory,
45 shell=sys.platform.startswith('win'),
46 stdout=subprocess.PIPE,
47 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000048
49
maruel@chromium.org261eeb52009-11-16 18:25:45 +000050def CaptureGitStatus(files, upstream_branch='origin'):
51 """Returns git status.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000052
maruel@chromium.org261eeb52009-11-16 18:25:45 +000053 @files can be a string (one file) or a list of files.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000054
maruel@chromium.org261eeb52009-11-16 18:25:45 +000055 Returns an array of (status, file) tuples."""
56 command = ["diff", "--name-status", "-r", "%s.." % upstream_branch]
57 if not files:
58 pass
59 elif isinstance(files, basestring):
60 command.append(files)
61 else:
62 command.extend(files)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000063
maruel@chromium.org261eeb52009-11-16 18:25:45 +000064 status = CaptureGit(command).rstrip()
65 results = []
66 if status:
67 for statusline in status.split('\n'):
68 m = re.match('^(\w)\t(.+)$', statusline)
69 if not m:
70 raise Exception("status currently unsupported: %s" % statusline)
71 results.append(('%s ' % m.group(1), m.group(2)))
72 return results
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000073
74
maruel@chromium.org261eeb52009-11-16 18:25:45 +000075# -----------------------------------------------------------------------------
76# SVN utils:
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000077
78
maruel@chromium.org261eeb52009-11-16 18:25:45 +000079def RunSVN(args, in_directory):
80 """Runs svn, sending output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000081
maruel@chromium.org261eeb52009-11-16 18:25:45 +000082 Args:
83 args: A sequence of command line parameters to be passed to svn.
84 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000085
maruel@chromium.org261eeb52009-11-16 18:25:45 +000086 Raises:
87 Error: An error occurred while running the svn command.
88 """
89 c = [SVN_COMMAND]
90 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000091
maruel@chromium.org261eeb52009-11-16 18:25:45 +000092 gclient_utils.SubprocessCall(c, in_directory)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000093
94
maruel@chromium.org261eeb52009-11-16 18:25:45 +000095def CaptureSVN(args, in_directory=None, print_error=True):
96 """Runs svn, capturing output sent to stdout as a string.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +000097
maruel@chromium.org261eeb52009-11-16 18:25:45 +000098 Args:
99 args: A sequence of command line parameters to be passed to svn.
100 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000101
maruel@chromium.org261eeb52009-11-16 18:25:45 +0000102 Returns:
103 The output sent to stdout as a string.
104 """
105 c = [SVN_COMMAND]
106 c.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000107
maruel@chromium.org261eeb52009-11-16 18:25:45 +0000108 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
109 # the svn.exe executable, but shell=True makes subprocess on Linux fail
110 # when it's called with a list because it only tries to execute the
111 # first string ("svn").
112 stderr = None
113 if not print_error:
114 stderr = subprocess.PIPE
115 return subprocess.Popen(c,
116 cwd=in_directory,
117 shell=(sys.platform == 'win32'),
118 stdout=subprocess.PIPE,
119 stderr=stderr).communicate()[0]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000120
121
maruel@chromium.org261eeb52009-11-16 18:25:45 +0000122def RunSVNAndGetFileList(options, args, in_directory, file_list):
123 """Runs svn checkout, update, or status, output to stdout.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000124
maruel@chromium.org261eeb52009-11-16 18:25:45 +0000125 The first item in args must be either "checkout", "update", or "status".
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000126
maruel@chromium.org261eeb52009-11-16 18:25:45 +0000127 svn's stdout is parsed to collect a list of files checked out or updated.
128 These files are appended to file_list. svn's stdout is also printed to
129 sys.stdout as in RunSVN.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000130
maruel@chromium.org261eeb52009-11-16 18:25:45 +0000131 Args:
132 options: command line options to gclient
133 args: A sequence of command line parameters to be passed to svn.
134 in_directory: The directory where svn is to be run.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000135
maruel@chromium.org261eeb52009-11-16 18:25:45 +0000136 Raises:
137 Error: An error occurred while running the svn command.
138 """
139 command = [SVN_COMMAND]
140 command.extend(args)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000141
maruel@chromium.org261eeb52009-11-16 18:25:45 +0000142 # svn update and svn checkout use the same pattern: the first three columns
143 # are for file status, property status, and lock status. This is followed
144 # by two spaces, and then the path to the file.
145 update_pattern = '^... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000146
maruel@chromium.org261eeb52009-11-16 18:25:45 +0000147 # The first three columns of svn status are the same as for svn update and
148 # svn checkout. The next three columns indicate addition-with-history,
149 # switch, and remote lock status. This is followed by one space, and then
150 # the path to the file.
151 status_pattern = '^...... (.*)$'
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000152
maruel@chromium.org261eeb52009-11-16 18:25:45 +0000153 # args[0] must be a supported command. This will blow up if it's something
154 # else, which is good. Note that the patterns are only effective when
155 # these commands are used in their ordinary forms, the patterns are invalid
156 # for "svn status --show-updates", for example.
157 pattern = {
158 'checkout': update_pattern,
159 'status': status_pattern,
160 'update': update_pattern,
161 }[args[0]]
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000162
maruel@chromium.org261eeb52009-11-16 18:25:45 +0000163 compiled_pattern = re.compile(pattern)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000164
maruel@chromium.org261eeb52009-11-16 18:25:45 +0000165 def CaptureMatchingLines(line):
166 match = compiled_pattern.search(line)
167 if match:
168 file_list.append(match.group(1))
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000169
maruel@chromium.org261eeb52009-11-16 18:25:45 +0000170 RunSVNAndFilterOutput(args,
171 in_directory,
172 options.verbose,
173 True,
174 CaptureMatchingLines)
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000175
maruel@chromium.org261eeb52009-11-16 18:25:45 +0000176def RunSVNAndFilterOutput(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.org261eeb52009-11-16 18:25:45 +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.org261eeb52009-11-16 18:25:45 +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 RunSVN.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000189
maruel@chromium.org261eeb52009-11-16 18:25:45 +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.org261eeb52009-11-16 18:25:45 +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.org261eeb52009-11-16 18:25:45 +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.org261eeb52009-11-16 18:25:45 +0000212def CaptureSVNInfo(relpath, in_directory=None, print_error=True):
213 """Returns a dictionary from the svn info output for the given file.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000214
maruel@chromium.org261eeb52009-11-16 18:25:45 +0000215 Args:
216 relpath: The directory where the working copy resides relative to
217 the directory given by in_directory.
218 in_directory: The directory where svn is to be run.
219 """
220 output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error)
221 dom = gclient_utils.ParseXML(output)
222 result = {}
223 if dom:
224 GetNamedNodeText = gclient_utils.GetNamedNodeText
225 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
226 def C(item, f):
227 if item is not None: return f(item)
228 # /info/entry/
229 # url
230 # reposityory/(root|uuid)
231 # wc-info/(schedule|depth)
232 # commit/(author|date)
233 # str() the results because they may be returned as Unicode, which
234 # interferes with the higher layers matching up things in the deps
235 # dictionary.
236 # TODO(maruel): Fix at higher level instead (!)
237 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
238 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
239 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
240 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry', 'revision'),
241 int)
242 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
243 str)
244 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
245 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
246 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
247 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
248 return result
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000249
250
maruel@chromium.org261eeb52009-11-16 18:25:45 +0000251def CaptureSVNHeadRevision(url):
252 """Get the head revision of a SVN repository.
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000253
maruel@chromium.org261eeb52009-11-16 18:25:45 +0000254 Returns:
255 Int head revision
256 """
257 info = CaptureSVN(["info", "--xml", url], os.getcwd())
258 dom = xml.dom.minidom.parseString(info)
259 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.orgd5800f12009-11-12 20:03:43 +0000260
maruel@chromium.org261eeb52009-11-16 18:25:45 +0000261
262def CaptureSVNStatus(files):
263 """Returns the svn 1.5 svn status emulated output.
264
265 @files can be a string (one file) or a list of files.
266
267 Returns an array of (status, file) tuples."""
268 command = ["status", "--xml"]
269 if not files:
270 pass
271 elif isinstance(files, basestring):
272 command.append(files)
273 else:
274 command.extend(files)
275
276 status_letter = {
277 None: ' ',
278 '': ' ',
279 'added': 'A',
280 'conflicted': 'C',
281 'deleted': 'D',
282 'external': 'X',
283 'ignored': 'I',
284 'incomplete': '!',
285 'merged': 'G',
286 'missing': '!',
287 'modified': 'M',
288 'none': ' ',
289 'normal': ' ',
290 'obstructed': '~',
291 'replaced': 'R',
292 'unversioned': '?',
293 }
294 dom = gclient_utils.ParseXML(CaptureSVN(command))
295 results = []
296 if dom:
297 # /status/target/entry/(wc-status|commit|author|date)
298 for target in dom.getElementsByTagName('target'):
299 for entry in target.getElementsByTagName('entry'):
300 file_path = entry.getAttribute('path')
301 wc_status = entry.getElementsByTagName('wc-status')
302 assert len(wc_status) == 1
303 # Emulate svn 1.5 status ouput...
304 statuses = [' '] * 7
305 # Col 0
306 xml_item_status = wc_status[0].getAttribute('item')
307 if xml_item_status in status_letter:
308 statuses[0] = status_letter[xml_item_status]
309 else:
310 raise Exception('Unknown item status "%s"; please implement me!' %
311 xml_item_status)
312 # Col 1
313 xml_props_status = wc_status[0].getAttribute('props')
314 if xml_props_status == 'modified':
315 statuses[1] = 'M'
316 elif xml_props_status == 'conflicted':
317 statuses[1] = 'C'
318 elif (not xml_props_status or xml_props_status == 'none' or
319 xml_props_status == 'normal'):
320 pass
321 else:
322 raise Exception('Unknown props status "%s"; please implement me!' %
323 xml_props_status)
324 # Col 2
325 if wc_status[0].getAttribute('wc-locked') == 'true':
326 statuses[2] = 'L'
327 # Col 3
328 if wc_status[0].getAttribute('copied') == 'true':
329 statuses[3] = '+'
330 # Col 4
331 if wc_status[0].getAttribute('switched') == 'true':
332 statuses[4] = 'S'
333 # TODO(maruel): Col 5 and 6
334 item = (''.join(statuses), file_path)
335 results.append(item)
336 return results