blob: ce616816c1b822f1865006316055dba96601992a [file] [log] [blame]
maruel@chromium.org5f3eee32009-09-17 00:34:30 +00001# Copyright 2009 Google Inc. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15
maruel@chromium.org754960e2009-09-21 12:31:05 +000016import logging
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000017import os
18import re
19import subprocess
20import sys
21import xml.dom.minidom
22
23import gclient_utils
24
25SVN_COMMAND = "svn"
26
27
28### SCM abstraction layer
29
30
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000031# Factory Method for SCM wrapper creation
32
33def CreateSCM(url=None, root_dir=None, relpath=None, scm_name='svn'):
34 # TODO(maruel): Deduce the SCM from the url.
35 scm_map = {
36 'svn' : SVNWrapper,
msb@chromium.orge28e4982009-09-25 20:51:45 +000037 'git' : GitWrapper,
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000038 }
msb@chromium.orge28e4982009-09-25 20:51:45 +000039
40 if url and (url.startswith('git:') or
41 url.startswith('ssh:') or
42 url.endswith('.git')):
43 scm_name = 'git'
44
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000045 if not scm_name in scm_map:
46 raise gclient_utils.Error('Unsupported scm %s' % scm_name)
47 return scm_map[scm_name](url, root_dir, relpath, scm_name)
48
49
50# SCMWrapper base class
51
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000052class SCMWrapper(object):
53 """Add necessary glue between all the supported SCM.
54
55 This is the abstraction layer to bind to different SCM. Since currently only
56 subversion is supported, a lot of subersionism remains. This can be sorted out
57 once another SCM is supported."""
maruel@chromium.org5e73b0c2009-09-18 19:47:48 +000058 def __init__(self, url=None, root_dir=None, relpath=None,
59 scm_name='svn'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000060 self.scm_name = scm_name
61 self.url = url
maruel@chromium.org5e73b0c2009-09-18 19:47:48 +000062 self._root_dir = root_dir
63 if self._root_dir:
64 self._root_dir = self._root_dir.replace('/', os.sep)
65 self.relpath = relpath
66 if self.relpath:
67 self.relpath = self.relpath.replace('/', os.sep)
msb@chromium.orge28e4982009-09-25 20:51:45 +000068 if self.relpath and self._root_dir:
69 self.checkout_path = os.path.join(self._root_dir, self.relpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000070
71 def FullUrlForRelativeUrl(self, url):
72 # Find the forth '/' and strip from there. A bit hackish.
73 return '/'.join(self.url.split('/')[:4]) + url
74
75 def RunCommand(self, command, options, args, file_list=None):
76 # file_list will have all files that are modified appended to it.
maruel@chromium.orgde754ac2009-09-17 18:04:50 +000077 if file_list is None:
78 file_list = []
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000079
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000080 commands = ['cleanup', 'export', 'update', 'revert',
81 'status', 'diff', 'pack', 'runhooks']
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000082
83 if not command in commands:
84 raise gclient_utils.Error('Unknown command %s' % command)
85
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000086 if not command in dir(self):
87 raise gclient_utils.Error('Command %s not implemnted in %s wrapper' % (
88 command, self.scm_name))
89
90 return getattr(self, command)(options, args, file_list)
91
92
msb@chromium.orge28e4982009-09-25 20:51:45 +000093class GitWrapper(SCMWrapper):
94 """Wrapper for Git"""
95
96 def cleanup(self, options, args, file_list):
97 """Cleanup working copy."""
98 self._RunGit(['prune'])
99 self._RunGit(['fsck'])
100 self._RunGit(['gc'])
101
102 def diff(self, options, args, file_list):
103 # NOTE: This function does not currently modify file_list.
104 merge_base = self._RunGit(['merge-base', 'HEAD', 'origin'])
105 print self._RunGit(['diff', merge_base])
106
107 def export(self, options, args, file_list):
108 assert len(args) == 1
109 export_path = os.path.abspath(os.path.join(args[0], self.relpath))
110 if not os.path.exists(export_path):
111 os.makedirs(export_path)
112 self._RunGit(['checkout-index', '-a', '--prefix=%s/' % export_path])
113
114 def update(self, options, args, file_list):
115 """Runs git to update or transparently checkout the working copy.
116
117 All updated files will be appended to file_list.
118
119 Raises:
120 Error: if can't get URL for relative path.
121 """
122
123 if args:
124 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
125
126 components = self.url.split("@")
127 url = components[0]
128 revision = None
129 if options.revision:
130 revision = options.revision
131 elif len(components) == 2:
132 revision = components[1]
133
134 if not os.path.exists(self.checkout_path):
135 self._RunGit(['clone', '-q', url, self.checkout_path], cwd=self._root_dir)
136 if revision:
137 self._RunGit(['reset', '--hard', revision])
138 files = self._RunGit(['ls-files']).split()
139 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
140 return
141
142 self._RunGit(['remote', 'update'])
143 new_base = 'origin'
144 if revision:
145 new_base = revision
146 files = self._RunGit(['diff', new_base, '--name-only']).split()
147 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
148 self._RunGit(['rebase', new_base])
149
150 def revert(self, options, args, file_list):
151 """Reverts local modifications.
152
153 All reverted files will be appended to file_list.
154 """
155 merge_base = self._RunGit(['merge-base', 'HEAD', 'origin'])
156 files = self._RunGit(['diff', merge_base, '--name-only']).split()
157 print self._RunGit(['reset', '--hard', merge_base])
158 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
159
160 def runhooks(self, options, args, file_list):
161 self.status(options, args, file_list)
162
163 def status(self, options, args, file_list):
164 """Display status information."""
165 if not os.path.isdir(self.checkout_path):
166 print('\n________ couldn\'t run status in %s:\nThe directory '
167 'does not exist.' % checkout_path)
168 else:
169 merge_base = self._RunGit(['merge-base', 'HEAD', 'origin'])
170 print self._RunGit(['diff', '--name-status', merge_base])
171 files = self._RunGit(['diff', '--name-only', merge_base]).split()
172 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
173
174 def _RunGit(self, args, cwd=None, checkrc=True):
175 if cwd == None:
176 cwd = self.checkout_path
177 cmd = ['git']
178 cmd.extend(args)
179 sp = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE)
180 if checkrc and sp.returncode:
181 raise gclient_utils.Error('git command %s returned %d' %
182 (args[0], sp.returncode))
183 return sp.communicate()[0].strip()
184
185
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000186class SVNWrapper(SCMWrapper):
187 """ Wrapper for SVN """
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000188
189 def cleanup(self, options, args, file_list):
190 """Cleanup working copy."""
191 command = ['cleanup']
192 command.extend(args)
193 RunSVN(command, os.path.join(self._root_dir, self.relpath))
194
195 def diff(self, options, args, file_list):
196 # NOTE: This function does not currently modify file_list.
197 command = ['diff']
198 command.extend(args)
199 RunSVN(command, os.path.join(self._root_dir, self.relpath))
200
201 def export(self, options, args, file_list):
202 assert len(args) == 1
203 export_path = os.path.abspath(os.path.join(args[0], self.relpath))
204 try:
205 os.makedirs(export_path)
206 except OSError:
207 pass
208 assert os.path.exists(export_path)
209 command = ['export', '--force', '.']
210 command.append(export_path)
211 RunSVN(command, os.path.join(self._root_dir, self.relpath))
212
213 def update(self, options, args, file_list):
214 """Runs SCM to update or transparently checkout the working copy.
215
216 All updated files will be appended to file_list.
217
218 Raises:
219 Error: if can't get URL for relative path.
220 """
221 # Only update if git is not controlling the directory.
222 checkout_path = os.path.join(self._root_dir, self.relpath)
223 git_path = os.path.join(self._root_dir, self.relpath, '.git')
224 if os.path.exists(git_path):
225 print("________ found .git directory; skipping %s" % self.relpath)
226 return
227
228 if args:
229 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
230
231 url = self.url
232 components = url.split("@")
233 revision = None
234 forced_revision = False
235 if options.revision:
236 # Override the revision number.
237 url = '%s@%s' % (components[0], str(options.revision))
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000238 revision = options.revision
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000239 forced_revision = True
240 elif len(components) == 2:
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000241 revision = components[1]
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000242 forced_revision = True
243
244 rev_str = ""
245 if revision:
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000246 rev_str = ' at %s' % revision
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000247
248 if not os.path.exists(checkout_path):
249 # We need to checkout.
250 command = ['checkout', url, checkout_path]
251 if revision:
252 command.extend(['--revision', str(revision)])
dpranke@google.com1a77f1d2009-10-05 21:05:16 +0000253 RunSVNAndGetFileList(options, command, self._root_dir, file_list)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000254 return
255
256 # Get the existing scm url and the revision number of the current checkout.
257 from_info = CaptureSVNInfo(os.path.join(checkout_path, '.'), '.')
258 if not from_info:
259 raise gclient_utils.Error("Can't update/checkout %r if an unversioned "
260 "directory is present. Delete the directory "
261 "and try again." %
262 checkout_path)
263
dpranke@google.com1a77f1d2009-10-05 21:05:16 +0000264 # Retrieve the current HEAD version because svn is slow at null updates.
265 if not revision:
266 from_info_live = CaptureSVNInfo(from_info['URL'], '.')
267 revision = str(from_info_live['Revision'])
268 rev_str = ' at %s' % revision
269 forced_revision = True
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000270
271 if from_info['URL'] != components[0]:
272 to_info = CaptureSVNInfo(url, '.')
maruel@chromium.orge2ce0c72009-09-23 16:14:18 +0000273 if not to_info.get('Repository Root') or not to_info.get('UUID'):
274 # The url is invalid or the server is not accessible, it's safer to bail
275 # out right now.
276 raise gclient_utils.Error('This url is unreachable: %s' % url)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000277 can_switch = ((from_info['Repository Root'] != to_info['Repository Root'])
278 and (from_info['UUID'] == to_info['UUID']))
279 if can_switch:
280 print("\n_____ relocating %s to a new checkout" % self.relpath)
281 # We have different roots, so check if we can switch --relocate.
282 # Subversion only permits this if the repository UUIDs match.
283 # Perform the switch --relocate, then rewrite the from_url
284 # to reflect where we "are now." (This is the same way that
285 # Subversion itself handles the metadata when switch --relocate
286 # is used.) This makes the checks below for whether we
287 # can update to a revision or have to switch to a different
288 # branch work as expected.
289 # TODO(maruel): TEST ME !
290 command = ["switch", "--relocate",
291 from_info['Repository Root'],
292 to_info['Repository Root'],
293 self.relpath]
294 RunSVN(command, self._root_dir)
295 from_info['URL'] = from_info['URL'].replace(
296 from_info['Repository Root'],
297 to_info['Repository Root'])
298 else:
299 if CaptureSVNStatus(checkout_path):
300 raise gclient_utils.Error("Can't switch the checkout to %s; UUID "
301 "don't match and there is local changes "
302 "in %s. Delete the directory and "
303 "try again." % (url, checkout_path))
304 # Ok delete it.
305 print("\n_____ switching %s to a new checkout" % self.relpath)
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +0000306 gclient_utils.RemoveDirectory(checkout_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000307 # We need to checkout.
308 command = ['checkout', url, checkout_path]
309 if revision:
310 command.extend(['--revision', str(revision)])
dpranke@google.com1a77f1d2009-10-05 21:05:16 +0000311 RunSVNAndGetFileList(options, command, self._root_dir, file_list)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000312 return
313
314
315 # If the provided url has a revision number that matches the revision
316 # number of the existing directory, then we don't need to bother updating.
maruel@chromium.org2e0c6852009-09-24 00:02:07 +0000317 if not options.force and str(from_info['Revision']) == revision:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000318 if options.verbose or not forced_revision:
319 print("\n_____ %s%s" % (self.relpath, rev_str))
320 return
321
322 command = ["update", checkout_path]
323 if revision:
324 command.extend(['--revision', str(revision)])
dpranke@google.com1a77f1d2009-10-05 21:05:16 +0000325 RunSVNAndGetFileList(options, command, self._root_dir, file_list)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000326
327 def revert(self, options, args, file_list):
328 """Reverts local modifications. Subversion specific.
329
330 All reverted files will be appended to file_list, even if Subversion
331 doesn't know about them.
332 """
333 path = os.path.join(self._root_dir, self.relpath)
334 if not os.path.isdir(path):
335 # svn revert won't work if the directory doesn't exist. It needs to
336 # checkout instead.
337 print("\n_____ %s is missing, synching instead" % self.relpath)
338 # Don't reuse the args.
339 return self.update(options, [], file_list)
340
maruel@chromium.org754960e2009-09-21 12:31:05 +0000341 for file in CaptureSVNStatus(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000342 file_path = os.path.join(path, file[1])
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000343 if file[0][0] == 'X':
maruel@chromium.org754960e2009-09-21 12:31:05 +0000344 # Ignore externals.
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000345 logging.info('Ignoring external %s' % file_path)
maruel@chromium.org754960e2009-09-21 12:31:05 +0000346 continue
347
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000348 if logging.getLogger().isEnabledFor(logging.INFO):
349 logging.info('%s%s' % (file[0], file[1]))
350 else:
351 print(file_path)
352 if file[0].isspace():
353 logging.error('No idea what is the status of %s.\n'
354 'You just found a bug in gclient, please ping '
355 'maruel@chromium.org ASAP!' % file_path)
356 # svn revert is really stupid. It fails on inconsistent line-endings,
357 # on switched directories, etc. So take no chance and delete everything!
358 try:
359 if not os.path.exists(file_path):
360 pass
361 elif os.path.isfile(file_path):
maruel@chromium.org754960e2009-09-21 12:31:05 +0000362 logging.info('os.remove(%s)' % file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000363 os.remove(file_path)
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000364 elif os.path.isdir(file_path):
maruel@chromium.org754960e2009-09-21 12:31:05 +0000365 logging.info('gclient_utils.RemoveDirectory(%s)' % file_path)
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +0000366 gclient_utils.RemoveDirectory(file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000367 else:
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000368 logging.error('no idea what is %s.\nYou just found a bug in gclient'
369 ', please ping maruel@chromium.org ASAP!' % file_path)
370 except EnvironmentError:
371 logging.error('Failed to remove %s.' % file_path)
372
373 # svn revert is so broken we don't even use it. Using
374 # "svn up --revision BASE" achieve the same effect.
dpranke@google.com1a77f1d2009-10-05 21:05:16 +0000375 RunSVNAndGetFileList(options, ['update', '--revision', 'BASE'], path,
376 file_list)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000377
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000378 def runhooks(self, options, args, file_list):
379 self.status(options, args, file_list)
380
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000381 def status(self, options, args, file_list):
382 """Display status information."""
383 path = os.path.join(self._root_dir, self.relpath)
384 command = ['status']
385 command.extend(args)
386 if not os.path.isdir(path):
387 # svn status won't work if the directory doesn't exist.
388 print("\n________ couldn't run \'%s\' in \'%s\':\nThe directory "
389 "does not exist."
390 % (' '.join(command), path))
391 # There's no file list to retrieve.
392 else:
dpranke@google.com1a77f1d2009-10-05 21:05:16 +0000393 RunSVNAndGetFileList(options, command, path, file_list)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000394
395 def pack(self, options, args, file_list):
396 """Generates a patch file which can be applied to the root of the
397 repository."""
398 path = os.path.join(self._root_dir, self.relpath)
399 command = ['diff']
400 command.extend(args)
401 # Simple class which tracks which file is being diffed and
402 # replaces instances of its file name in the original and
403 # working copy lines of the svn diff output.
404 class DiffFilterer(object):
405 index_string = "Index: "
406 original_prefix = "--- "
407 working_prefix = "+++ "
408
409 def __init__(self, relpath):
410 # Note that we always use '/' as the path separator to be
411 # consistent with svn's cygwin-style output on Windows
412 self._relpath = relpath.replace("\\", "/")
413 self._current_file = ""
414 self._replacement_file = ""
415
416 def SetCurrentFile(self, file):
417 self._current_file = file
418 # Note that we always use '/' as the path separator to be
419 # consistent with svn's cygwin-style output on Windows
420 self._replacement_file = self._relpath + '/' + file
421
422 def ReplaceAndPrint(self, line):
423 print(line.replace(self._current_file, self._replacement_file))
424
425 def Filter(self, line):
426 if (line.startswith(self.index_string)):
427 self.SetCurrentFile(line[len(self.index_string):])
428 self.ReplaceAndPrint(line)
429 else:
430 if (line.startswith(self.original_prefix) or
431 line.startswith(self.working_prefix)):
432 self.ReplaceAndPrint(line)
433 else:
434 print line
435
436 filterer = DiffFilterer(self.relpath)
437 RunSVNAndFilterOutput(command, path, False, False, filterer.Filter)
438
439
440# -----------------------------------------------------------------------------
441# SVN utils:
442
443
444def RunSVN(args, in_directory):
445 """Runs svn, sending output to stdout.
446
447 Args:
448 args: A sequence of command line parameters to be passed to svn.
449 in_directory: The directory where svn is to be run.
450
451 Raises:
452 Error: An error occurred while running the svn command.
453 """
454 c = [SVN_COMMAND]
455 c.extend(args)
456
457 gclient_utils.SubprocessCall(c, in_directory)
458
459
460def CaptureSVN(args, in_directory=None, print_error=True):
461 """Runs svn, capturing output sent to stdout as a string.
462
463 Args:
464 args: A sequence of command line parameters to be passed to svn.
465 in_directory: The directory where svn is to be run.
466
467 Returns:
468 The output sent to stdout as a string.
469 """
470 c = [SVN_COMMAND]
471 c.extend(args)
472
473 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
474 # the svn.exe executable, but shell=True makes subprocess on Linux fail
475 # when it's called with a list because it only tries to execute the
476 # first string ("svn").
477 stderr = None
478 if not print_error:
479 stderr = subprocess.PIPE
480 return subprocess.Popen(c,
481 cwd=in_directory,
482 shell=(sys.platform == 'win32'),
483 stdout=subprocess.PIPE,
484 stderr=stderr).communicate()[0]
485
486
dpranke@google.com1a77f1d2009-10-05 21:05:16 +0000487def RunSVNAndGetFileList(options, args, in_directory, file_list):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000488 """Runs svn checkout, update, or status, output to stdout.
489
490 The first item in args must be either "checkout", "update", or "status".
491
492 svn's stdout is parsed to collect a list of files checked out or updated.
493 These files are appended to file_list. svn's stdout is also printed to
494 sys.stdout as in RunSVN.
495
496 Args:
dpranke@google.com1a77f1d2009-10-05 21:05:16 +0000497 options: command line options to gclient
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000498 args: A sequence of command line parameters to be passed to svn.
499 in_directory: The directory where svn is to be run.
500
501 Raises:
502 Error: An error occurred while running the svn command.
503 """
504 command = [SVN_COMMAND]
505 command.extend(args)
506
507 # svn update and svn checkout use the same pattern: the first three columns
508 # are for file status, property status, and lock status. This is followed
509 # by two spaces, and then the path to the file.
510 update_pattern = '^... (.*)$'
511
512 # The first three columns of svn status are the same as for svn update and
513 # svn checkout. The next three columns indicate addition-with-history,
514 # switch, and remote lock status. This is followed by one space, and then
515 # the path to the file.
516 status_pattern = '^...... (.*)$'
517
518 # args[0] must be a supported command. This will blow up if it's something
519 # else, which is good. Note that the patterns are only effective when
520 # these commands are used in their ordinary forms, the patterns are invalid
521 # for "svn status --show-updates", for example.
522 pattern = {
523 'checkout': update_pattern,
524 'status': status_pattern,
525 'update': update_pattern,
526 }[args[0]]
527
528 compiled_pattern = re.compile(pattern)
529
530 def CaptureMatchingLines(line):
531 match = compiled_pattern.search(line)
532 if match:
533 file_list.append(match.group(1))
534
535 RunSVNAndFilterOutput(args,
536 in_directory,
dpranke@google.com1a77f1d2009-10-05 21:05:16 +0000537 options.verbose,
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000538 True,
539 CaptureMatchingLines)
540
541def RunSVNAndFilterOutput(args,
542 in_directory,
543 print_messages,
544 print_stdout,
545 filter):
546 """Runs svn checkout, update, status, or diff, optionally outputting
547 to stdout.
548
549 The first item in args must be either "checkout", "update",
550 "status", or "diff".
551
552 svn's stdout is passed line-by-line to the given filter function. If
553 print_stdout is true, it is also printed to sys.stdout as in RunSVN.
554
555 Args:
556 args: A sequence of command line parameters to be passed to svn.
557 in_directory: The directory where svn is to be run.
558 print_messages: Whether to print status messages to stdout about
559 which Subversion commands are being run.
560 print_stdout: Whether to forward Subversion's output to stdout.
561 filter: A function taking one argument (a string) which will be
562 passed each line (with the ending newline character removed) of
563 Subversion's output for filtering.
564
565 Raises:
566 Error: An error occurred while running the svn command.
567 """
568 command = [SVN_COMMAND]
569 command.extend(args)
570
571 gclient_utils.SubprocessCallAndFilter(command,
572 in_directory,
573 print_messages,
574 print_stdout,
575 filter=filter)
576
577def CaptureSVNInfo(relpath, in_directory=None, print_error=True):
578 """Returns a dictionary from the svn info output for the given file.
579
580 Args:
581 relpath: The directory where the working copy resides relative to
582 the directory given by in_directory.
583 in_directory: The directory where svn is to be run.
584 """
585 output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error)
586 dom = gclient_utils.ParseXML(output)
587 result = {}
588 if dom:
589 GetNamedNodeText = gclient_utils.GetNamedNodeText
590 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
591 def C(item, f):
592 if item is not None: return f(item)
593 # /info/entry/
594 # url
595 # reposityory/(root|uuid)
596 # wc-info/(schedule|depth)
597 # commit/(author|date)
598 # str() the results because they may be returned as Unicode, which
599 # interferes with the higher layers matching up things in the deps
600 # dictionary.
601 # TODO(maruel): Fix at higher level instead (!)
602 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
603 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
604 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
605 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry', 'revision'),
606 int)
607 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
608 str)
609 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
610 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
611 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
612 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
613 return result
614
615
616def CaptureSVNHeadRevision(url):
617 """Get the head revision of a SVN repository.
618
619 Returns:
620 Int head revision
621 """
622 info = CaptureSVN(["info", "--xml", url], os.getcwd())
623 dom = xml.dom.minidom.parseString(info)
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000624 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000625
626
627def CaptureSVNStatus(files):
628 """Returns the svn 1.5 svn status emulated output.
629
630 @files can be a string (one file) or a list of files.
631
632 Returns an array of (status, file) tuples."""
633 command = ["status", "--xml"]
634 if not files:
635 pass
636 elif isinstance(files, basestring):
637 command.append(files)
638 else:
639 command.extend(files)
640
641 status_letter = {
642 None: ' ',
643 '': ' ',
644 'added': 'A',
645 'conflicted': 'C',
646 'deleted': 'D',
647 'external': 'X',
648 'ignored': 'I',
649 'incomplete': '!',
650 'merged': 'G',
651 'missing': '!',
652 'modified': 'M',
653 'none': ' ',
654 'normal': ' ',
655 'obstructed': '~',
656 'replaced': 'R',
657 'unversioned': '?',
658 }
659 dom = gclient_utils.ParseXML(CaptureSVN(command))
660 results = []
661 if dom:
662 # /status/target/entry/(wc-status|commit|author|date)
663 for target in dom.getElementsByTagName('target'):
664 base_path = target.getAttribute('path')
665 for entry in target.getElementsByTagName('entry'):
666 file = entry.getAttribute('path')
667 wc_status = entry.getElementsByTagName('wc-status')
668 assert len(wc_status) == 1
669 # Emulate svn 1.5 status ouput...
670 statuses = [' ' for i in range(7)]
671 # Col 0
672 xml_item_status = wc_status[0].getAttribute('item')
673 if xml_item_status in status_letter:
674 statuses[0] = status_letter[xml_item_status]
675 else:
676 raise Exception('Unknown item status "%s"; please implement me!' %
677 xml_item_status)
678 # Col 1
679 xml_props_status = wc_status[0].getAttribute('props')
680 if xml_props_status == 'modified':
681 statuses[1] = 'M'
682 elif xml_props_status == 'conflicted':
683 statuses[1] = 'C'
684 elif (not xml_props_status or xml_props_status == 'none' or
685 xml_props_status == 'normal'):
686 pass
687 else:
688 raise Exception('Unknown props status "%s"; please implement me!' %
689 xml_props_status)
690 # Col 2
691 if wc_status[0].getAttribute('wc-locked') == 'true':
692 statuses[2] = 'L'
693 # Col 3
694 if wc_status[0].getAttribute('copied') == 'true':
695 statuses[3] = '+'
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000696 # Col 4
697 if wc_status[0].getAttribute('switched') == 'true':
698 statuses[4] = 'S'
699 # TODO(maruel): Col 5 and 6
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000700 item = (''.join(statuses), file)
701 results.append(item)
702 return results