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