blob: 592074e87e0e9ee5c48c88ca1b173435d3ed7838 [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)])
maruel@chromium.org9344e972009-10-05 22:52:57 +0000253 RunSVNAndGetFileList(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
maruel@chromium.org9344e972009-10-05 22:52:57 +0000264 if options.manually_grab_svn_rev:
265 # Retrieve the current HEAD version because svn is slow at null updates.
266 if not revision:
267 from_info_live = CaptureSVNInfo(from_info['URL'], '.')
268 revision = str(from_info_live['Revision'])
269 rev_str = ' at %s' % revision
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)])
maruel@chromium.org9344e972009-10-05 22:52:57 +0000311 RunSVNAndGetFileList(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)])
maruel@chromium.org9344e972009-10-05 22:52:57 +0000325 RunSVNAndGetFileList(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
maruel@chromium.org810a50b2009-10-05 23:03:18 +0000373 try:
374 # svn revert is so broken we don't even use it. Using
375 # "svn up --revision BASE" achieve the same effect.
376 RunSVNAndGetFileList(['update', '--revision', 'BASE'], path,
377 file_list)
378 except OSError, e:
379 # Maybe the directory disapeared meanwhile. We don't want it to throw an
380 # exception.
381 logging.error('Failed to update:\n%s' % str(e))
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000382
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000383 def runhooks(self, options, args, file_list):
384 self.status(options, args, file_list)
385
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000386 def status(self, options, args, file_list):
387 """Display status information."""
388 path = os.path.join(self._root_dir, self.relpath)
389 command = ['status']
390 command.extend(args)
391 if not os.path.isdir(path):
392 # svn status won't work if the directory doesn't exist.
393 print("\n________ couldn't run \'%s\' in \'%s\':\nThe directory "
394 "does not exist."
395 % (' '.join(command), path))
396 # There's no file list to retrieve.
397 else:
maruel@chromium.org9344e972009-10-05 22:52:57 +0000398 RunSVNAndGetFileList(command, path, file_list)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000399
400 def pack(self, options, args, file_list):
401 """Generates a patch file which can be applied to the root of the
402 repository."""
403 path = os.path.join(self._root_dir, self.relpath)
404 command = ['diff']
405 command.extend(args)
406 # Simple class which tracks which file is being diffed and
407 # replaces instances of its file name in the original and
408 # working copy lines of the svn diff output.
409 class DiffFilterer(object):
410 index_string = "Index: "
411 original_prefix = "--- "
412 working_prefix = "+++ "
413
414 def __init__(self, relpath):
415 # Note that we always use '/' as the path separator to be
416 # consistent with svn's cygwin-style output on Windows
417 self._relpath = relpath.replace("\\", "/")
418 self._current_file = ""
419 self._replacement_file = ""
420
421 def SetCurrentFile(self, file):
422 self._current_file = file
423 # Note that we always use '/' as the path separator to be
424 # consistent with svn's cygwin-style output on Windows
425 self._replacement_file = self._relpath + '/' + file
426
427 def ReplaceAndPrint(self, line):
428 print(line.replace(self._current_file, self._replacement_file))
429
430 def Filter(self, line):
431 if (line.startswith(self.index_string)):
432 self.SetCurrentFile(line[len(self.index_string):])
433 self.ReplaceAndPrint(line)
434 else:
435 if (line.startswith(self.original_prefix) or
436 line.startswith(self.working_prefix)):
437 self.ReplaceAndPrint(line)
438 else:
439 print line
440
441 filterer = DiffFilterer(self.relpath)
442 RunSVNAndFilterOutput(command, path, False, False, filterer.Filter)
443
444
445# -----------------------------------------------------------------------------
446# SVN utils:
447
448
449def RunSVN(args, in_directory):
450 """Runs svn, sending output to stdout.
451
452 Args:
453 args: A sequence of command line parameters to be passed to svn.
454 in_directory: The directory where svn is to be run.
455
456 Raises:
457 Error: An error occurred while running the svn command.
458 """
459 c = [SVN_COMMAND]
460 c.extend(args)
461
462 gclient_utils.SubprocessCall(c, in_directory)
463
464
465def CaptureSVN(args, in_directory=None, print_error=True):
466 """Runs svn, capturing output sent to stdout as a string.
467
468 Args:
469 args: A sequence of command line parameters to be passed to svn.
470 in_directory: The directory where svn is to be run.
471
472 Returns:
473 The output sent to stdout as a string.
474 """
475 c = [SVN_COMMAND]
476 c.extend(args)
477
478 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
479 # the svn.exe executable, but shell=True makes subprocess on Linux fail
480 # when it's called with a list because it only tries to execute the
481 # first string ("svn").
482 stderr = None
483 if not print_error:
484 stderr = subprocess.PIPE
485 return subprocess.Popen(c,
486 cwd=in_directory,
487 shell=(sys.platform == 'win32'),
488 stdout=subprocess.PIPE,
489 stderr=stderr).communicate()[0]
490
491
maruel@chromium.org9344e972009-10-05 22:52:57 +0000492def RunSVNAndGetFileList(args, in_directory, file_list):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000493 """Runs svn checkout, update, or status, output to stdout.
494
495 The first item in args must be either "checkout", "update", or "status".
496
497 svn's stdout is parsed to collect a list of files checked out or updated.
498 These files are appended to file_list. svn's stdout is also printed to
499 sys.stdout as in RunSVN.
500
501 Args:
502 args: A sequence of command line parameters to be passed to svn.
503 in_directory: The directory where svn is to be run.
504
505 Raises:
506 Error: An error occurred while running the svn command.
507 """
508 command = [SVN_COMMAND]
509 command.extend(args)
510
511 # svn update and svn checkout use the same pattern: the first three columns
512 # are for file status, property status, and lock status. This is followed
513 # by two spaces, and then the path to the file.
514 update_pattern = '^... (.*)$'
515
516 # The first three columns of svn status are the same as for svn update and
517 # svn checkout. The next three columns indicate addition-with-history,
518 # switch, and remote lock status. This is followed by one space, and then
519 # the path to the file.
520 status_pattern = '^...... (.*)$'
521
522 # args[0] must be a supported command. This will blow up if it's something
523 # else, which is good. Note that the patterns are only effective when
524 # these commands are used in their ordinary forms, the patterns are invalid
525 # for "svn status --show-updates", for example.
526 pattern = {
527 'checkout': update_pattern,
528 'status': status_pattern,
529 'update': update_pattern,
530 }[args[0]]
531
532 compiled_pattern = re.compile(pattern)
533
534 def CaptureMatchingLines(line):
535 match = compiled_pattern.search(line)
536 if match:
537 file_list.append(match.group(1))
538
539 RunSVNAndFilterOutput(args,
540 in_directory,
maruel@chromium.org9344e972009-10-05 22:52:57 +0000541 True,
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000542 True,
543 CaptureMatchingLines)
544
545def RunSVNAndFilterOutput(args,
546 in_directory,
547 print_messages,
548 print_stdout,
549 filter):
550 """Runs svn checkout, update, status, or diff, optionally outputting
551 to stdout.
552
553 The first item in args must be either "checkout", "update",
554 "status", or "diff".
555
556 svn's stdout is passed line-by-line to the given filter function. If
557 print_stdout is true, it is also printed to sys.stdout as in RunSVN.
558
559 Args:
560 args: A sequence of command line parameters to be passed to svn.
561 in_directory: The directory where svn is to be run.
562 print_messages: Whether to print status messages to stdout about
563 which Subversion commands are being run.
564 print_stdout: Whether to forward Subversion's output to stdout.
565 filter: A function taking one argument (a string) which will be
566 passed each line (with the ending newline character removed) of
567 Subversion's output for filtering.
568
569 Raises:
570 Error: An error occurred while running the svn command.
571 """
572 command = [SVN_COMMAND]
573 command.extend(args)
574
575 gclient_utils.SubprocessCallAndFilter(command,
576 in_directory,
577 print_messages,
578 print_stdout,
579 filter=filter)
580
581def CaptureSVNInfo(relpath, in_directory=None, print_error=True):
582 """Returns a dictionary from the svn info output for the given file.
583
584 Args:
585 relpath: The directory where the working copy resides relative to
586 the directory given by in_directory.
587 in_directory: The directory where svn is to be run.
588 """
589 output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error)
590 dom = gclient_utils.ParseXML(output)
591 result = {}
592 if dom:
593 GetNamedNodeText = gclient_utils.GetNamedNodeText
594 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
595 def C(item, f):
596 if item is not None: return f(item)
597 # /info/entry/
598 # url
599 # reposityory/(root|uuid)
600 # wc-info/(schedule|depth)
601 # commit/(author|date)
602 # str() the results because they may be returned as Unicode, which
603 # interferes with the higher layers matching up things in the deps
604 # dictionary.
605 # TODO(maruel): Fix at higher level instead (!)
606 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
607 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
608 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
609 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry', 'revision'),
610 int)
611 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
612 str)
613 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
614 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
615 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
616 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
617 return result
618
619
620def CaptureSVNHeadRevision(url):
621 """Get the head revision of a SVN repository.
622
623 Returns:
624 Int head revision
625 """
626 info = CaptureSVN(["info", "--xml", url], os.getcwd())
627 dom = xml.dom.minidom.parseString(info)
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000628 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000629
630
631def CaptureSVNStatus(files):
632 """Returns the svn 1.5 svn status emulated output.
633
634 @files can be a string (one file) or a list of files.
635
636 Returns an array of (status, file) tuples."""
637 command = ["status", "--xml"]
638 if not files:
639 pass
640 elif isinstance(files, basestring):
641 command.append(files)
642 else:
643 command.extend(files)
644
645 status_letter = {
646 None: ' ',
647 '': ' ',
648 'added': 'A',
649 'conflicted': 'C',
650 'deleted': 'D',
651 'external': 'X',
652 'ignored': 'I',
653 'incomplete': '!',
654 'merged': 'G',
655 'missing': '!',
656 'modified': 'M',
657 'none': ' ',
658 'normal': ' ',
659 'obstructed': '~',
660 'replaced': 'R',
661 'unversioned': '?',
662 }
663 dom = gclient_utils.ParseXML(CaptureSVN(command))
664 results = []
665 if dom:
666 # /status/target/entry/(wc-status|commit|author|date)
667 for target in dom.getElementsByTagName('target'):
668 base_path = target.getAttribute('path')
669 for entry in target.getElementsByTagName('entry'):
670 file = entry.getAttribute('path')
671 wc_status = entry.getElementsByTagName('wc-status')
672 assert len(wc_status) == 1
673 # Emulate svn 1.5 status ouput...
674 statuses = [' ' for i in range(7)]
675 # Col 0
676 xml_item_status = wc_status[0].getAttribute('item')
677 if xml_item_status in status_letter:
678 statuses[0] = status_letter[xml_item_status]
679 else:
680 raise Exception('Unknown item status "%s"; please implement me!' %
681 xml_item_status)
682 # Col 1
683 xml_props_status = wc_status[0].getAttribute('props')
684 if xml_props_status == 'modified':
685 statuses[1] = 'M'
686 elif xml_props_status == 'conflicted':
687 statuses[1] = 'C'
688 elif (not xml_props_status or xml_props_status == 'none' or
689 xml_props_status == 'normal'):
690 pass
691 else:
692 raise Exception('Unknown props status "%s"; please implement me!' %
693 xml_props_status)
694 # Col 2
695 if wc_status[0].getAttribute('wc-locked') == 'true':
696 statuses[2] = 'L'
697 # Col 3
698 if wc_status[0].getAttribute('copied') == 'true':
699 statuses[3] = '+'
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000700 # Col 4
701 if wc_status[0].getAttribute('switched') == 'true':
702 statuses[4] = 'S'
703 # TODO(maruel): Col 5 and 6
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000704 item = (''.join(statuses), file)
705 results.append(item)
706 return results