blob: f1894262b3fe17aa2b9874dacb9f735e642b2406 [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"
chase@chromium.org8e416c82009-10-06 04:30:44 +000026GIT_COMMAND = "git"
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000027
28
29### SCM abstraction layer
30
31
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000032# Factory Method for SCM wrapper creation
33
34def CreateSCM(url=None, root_dir=None, relpath=None, scm_name='svn'):
35 # TODO(maruel): Deduce the SCM from the url.
36 scm_map = {
37 'svn' : SVNWrapper,
msb@chromium.orge28e4982009-09-25 20:51:45 +000038 'git' : GitWrapper,
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000039 }
msb@chromium.orge28e4982009-09-25 20:51:45 +000040
41 if url and (url.startswith('git:') or
42 url.startswith('ssh:') or
43 url.endswith('.git')):
44 scm_name = 'git'
45
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000046 if not scm_name in scm_map:
47 raise gclient_utils.Error('Unsupported scm %s' % scm_name)
48 return scm_map[scm_name](url, root_dir, relpath, scm_name)
49
50
51# SCMWrapper base class
52
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000053class SCMWrapper(object):
54 """Add necessary glue between all the supported SCM.
55
56 This is the abstraction layer to bind to different SCM. Since currently only
57 subversion is supported, a lot of subersionism remains. This can be sorted out
58 once another SCM is supported."""
maruel@chromium.org5e73b0c2009-09-18 19:47:48 +000059 def __init__(self, url=None, root_dir=None, relpath=None,
60 scm_name='svn'):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000061 self.scm_name = scm_name
62 self.url = url
maruel@chromium.org5e73b0c2009-09-18 19:47:48 +000063 self._root_dir = root_dir
64 if self._root_dir:
65 self._root_dir = self._root_dir.replace('/', os.sep)
66 self.relpath = relpath
67 if self.relpath:
68 self.relpath = self.relpath.replace('/', os.sep)
msb@chromium.orge28e4982009-09-25 20:51:45 +000069 if self.relpath and self._root_dir:
70 self.checkout_path = os.path.join(self._root_dir, self.relpath)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000071
72 def FullUrlForRelativeUrl(self, url):
73 # Find the forth '/' and strip from there. A bit hackish.
74 return '/'.join(self.url.split('/')[:4]) + url
75
76 def RunCommand(self, command, options, args, file_list=None):
77 # file_list will have all files that are modified appended to it.
maruel@chromium.orgde754ac2009-09-17 18:04:50 +000078 if file_list is None:
79 file_list = []
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000080
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000081 commands = ['cleanup', 'export', 'update', 'revert',
82 'status', 'diff', 'pack', 'runhooks']
maruel@chromium.org5f3eee32009-09-17 00:34:30 +000083
84 if not command in commands:
85 raise gclient_utils.Error('Unknown command %s' % command)
86
msb@chromium.orgcb5442b2009-09-22 16:51:24 +000087 if not command in dir(self):
88 raise gclient_utils.Error('Command %s not implemnted in %s wrapper' % (
89 command, self.scm_name))
90
91 return getattr(self, command)(options, args, file_list)
92
93
msb@chromium.orge28e4982009-09-25 20:51:45 +000094class GitWrapper(SCMWrapper):
95 """Wrapper for Git"""
96
97 def cleanup(self, options, args, file_list):
98 """Cleanup working copy."""
99 self._RunGit(['prune'])
100 self._RunGit(['fsck'])
101 self._RunGit(['gc'])
102
103 def diff(self, options, args, file_list):
104 # NOTE: This function does not currently modify file_list.
105 merge_base = self._RunGit(['merge-base', 'HEAD', 'origin'])
106 print self._RunGit(['diff', merge_base])
107
108 def export(self, options, args, file_list):
109 assert len(args) == 1
110 export_path = os.path.abspath(os.path.join(args[0], self.relpath))
111 if not os.path.exists(export_path):
112 os.makedirs(export_path)
113 self._RunGit(['checkout-index', '-a', '--prefix=%s/' % export_path])
114
115 def update(self, options, args, file_list):
116 """Runs git to update or transparently checkout the working copy.
117
118 All updated files will be appended to file_list.
119
120 Raises:
121 Error: if can't get URL for relative path.
122 """
123
124 if args:
125 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
126
127 components = self.url.split("@")
128 url = components[0]
129 revision = None
130 if options.revision:
131 revision = options.revision
132 elif len(components) == 2:
133 revision = components[1]
134
135 if not os.path.exists(self.checkout_path):
136 self._RunGit(['clone', '-q', url, self.checkout_path], cwd=self._root_dir)
137 if revision:
138 self._RunGit(['reset', '--hard', revision])
139 files = self._RunGit(['ls-files']).split()
140 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
141 return
142
143 self._RunGit(['remote', 'update'])
144 new_base = 'origin'
145 if revision:
146 new_base = revision
147 files = self._RunGit(['diff', new_base, '--name-only']).split()
148 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
149 self._RunGit(['rebase', new_base])
150
151 def revert(self, options, args, file_list):
152 """Reverts local modifications.
153
154 All reverted files will be appended to file_list.
155 """
156 merge_base = self._RunGit(['merge-base', 'HEAD', 'origin'])
157 files = self._RunGit(['diff', merge_base, '--name-only']).split()
158 print self._RunGit(['reset', '--hard', merge_base])
159 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
160
161 def runhooks(self, options, args, file_list):
162 self.status(options, args, file_list)
163
164 def status(self, options, args, file_list):
165 """Display status information."""
166 if not os.path.isdir(self.checkout_path):
167 print('\n________ couldn\'t run status in %s:\nThe directory '
168 'does not exist.' % checkout_path)
169 else:
170 merge_base = self._RunGit(['merge-base', 'HEAD', 'origin'])
171 print self._RunGit(['diff', '--name-status', merge_base])
172 files = self._RunGit(['diff', '--name-only', merge_base]).split()
173 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
174
175 def _RunGit(self, args, cwd=None, checkrc=True):
176 if cwd == None:
177 cwd = self.checkout_path
178 cmd = ['git']
179 cmd.extend(args)
180 sp = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE)
181 if checkrc and sp.returncode:
182 raise gclient_utils.Error('git command %s returned %d' %
183 (args[0], sp.returncode))
184 return sp.communicate()[0].strip()
185
186
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000187class SVNWrapper(SCMWrapper):
188 """ Wrapper for SVN """
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000189
190 def cleanup(self, options, args, file_list):
191 """Cleanup working copy."""
192 command = ['cleanup']
193 command.extend(args)
194 RunSVN(command, os.path.join(self._root_dir, self.relpath))
195
196 def diff(self, options, args, file_list):
197 # NOTE: This function does not currently modify file_list.
198 command = ['diff']
199 command.extend(args)
200 RunSVN(command, os.path.join(self._root_dir, self.relpath))
201
202 def export(self, options, args, file_list):
203 assert len(args) == 1
204 export_path = os.path.abspath(os.path.join(args[0], self.relpath))
205 try:
206 os.makedirs(export_path)
207 except OSError:
208 pass
209 assert os.path.exists(export_path)
210 command = ['export', '--force', '.']
211 command.append(export_path)
212 RunSVN(command, os.path.join(self._root_dir, self.relpath))
213
214 def update(self, options, args, file_list):
215 """Runs SCM to update or transparently checkout the working copy.
216
217 All updated files will be appended to file_list.
218
219 Raises:
220 Error: if can't get URL for relative path.
221 """
222 # Only update if git is not controlling the directory.
223 checkout_path = os.path.join(self._root_dir, self.relpath)
224 git_path = os.path.join(self._root_dir, self.relpath, '.git')
225 if os.path.exists(git_path):
226 print("________ found .git directory; skipping %s" % self.relpath)
227 return
228
229 if args:
230 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
231
232 url = self.url
233 components = url.split("@")
234 revision = None
235 forced_revision = False
236 if options.revision:
237 # Override the revision number.
238 url = '%s@%s' % (components[0], str(options.revision))
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000239 revision = options.revision
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000240 forced_revision = True
241 elif len(components) == 2:
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000242 revision = components[1]
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000243 forced_revision = True
244
245 rev_str = ""
246 if revision:
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000247 rev_str = ' at %s' % revision
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000248
249 if not os.path.exists(checkout_path):
250 # We need to checkout.
251 command = ['checkout', url, checkout_path]
252 if revision:
253 command.extend(['--revision', str(revision)])
dpranke@google.com22e29d42009-10-28 00:48:26 +0000254 RunSVNAndGetFileList(options, command, self._root_dir, file_list)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000255 return
256
257 # Get the existing scm url and the revision number of the current checkout.
258 from_info = CaptureSVNInfo(os.path.join(checkout_path, '.'), '.')
259 if not from_info:
260 raise gclient_utils.Error("Can't update/checkout %r if an unversioned "
261 "directory is present. Delete the directory "
262 "and try again." %
263 checkout_path)
264
maruel@chromium.org7753d242009-10-07 17:40:24 +0000265 if options.manually_grab_svn_rev:
266 # Retrieve the current HEAD version because svn is slow at null updates.
267 if not revision:
268 from_info_live = CaptureSVNInfo(from_info['URL'], '.')
269 revision = str(from_info_live['Revision'])
270 rev_str = ' at %s' % revision
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000271
272 if from_info['URL'] != components[0]:
273 to_info = CaptureSVNInfo(url, '.')
maruel@chromium.orge2ce0c72009-09-23 16:14:18 +0000274 if not to_info.get('Repository Root') or not to_info.get('UUID'):
275 # The url is invalid or the server is not accessible, it's safer to bail
276 # out right now.
277 raise gclient_utils.Error('This url is unreachable: %s' % url)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000278 can_switch = ((from_info['Repository Root'] != to_info['Repository Root'])
279 and (from_info['UUID'] == to_info['UUID']))
280 if can_switch:
281 print("\n_____ relocating %s to a new checkout" % self.relpath)
282 # We have different roots, so check if we can switch --relocate.
283 # Subversion only permits this if the repository UUIDs match.
284 # Perform the switch --relocate, then rewrite the from_url
285 # to reflect where we "are now." (This is the same way that
286 # Subversion itself handles the metadata when switch --relocate
287 # is used.) This makes the checks below for whether we
288 # can update to a revision or have to switch to a different
289 # branch work as expected.
290 # TODO(maruel): TEST ME !
291 command = ["switch", "--relocate",
292 from_info['Repository Root'],
293 to_info['Repository Root'],
294 self.relpath]
295 RunSVN(command, self._root_dir)
296 from_info['URL'] = from_info['URL'].replace(
297 from_info['Repository Root'],
298 to_info['Repository Root'])
299 else:
300 if CaptureSVNStatus(checkout_path):
301 raise gclient_utils.Error("Can't switch the checkout to %s; UUID "
302 "don't match and there is local changes "
303 "in %s. Delete the directory and "
304 "try again." % (url, checkout_path))
305 # Ok delete it.
306 print("\n_____ switching %s to a new checkout" % self.relpath)
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +0000307 gclient_utils.RemoveDirectory(checkout_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000308 # We need to checkout.
309 command = ['checkout', url, checkout_path]
310 if revision:
311 command.extend(['--revision', str(revision)])
dpranke@google.com22e29d42009-10-28 00:48:26 +0000312 RunSVNAndGetFileList(options, command, self._root_dir, file_list)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000313 return
314
315
316 # If the provided url has a revision number that matches the revision
317 # number of the existing directory, then we don't need to bother updating.
maruel@chromium.org2e0c6852009-09-24 00:02:07 +0000318 if not options.force and str(from_info['Revision']) == revision:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000319 if options.verbose or not forced_revision:
320 print("\n_____ %s%s" % (self.relpath, rev_str))
321 return
322
323 command = ["update", checkout_path]
324 if revision:
325 command.extend(['--revision', str(revision)])
dpranke@google.com22e29d42009-10-28 00:48:26 +0000326 RunSVNAndGetFileList(options, command, self._root_dir, file_list)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000327
328 def revert(self, options, args, file_list):
329 """Reverts local modifications. Subversion specific.
330
331 All reverted files will be appended to file_list, even if Subversion
332 doesn't know about them.
333 """
334 path = os.path.join(self._root_dir, self.relpath)
335 if not os.path.isdir(path):
336 # svn revert won't work if the directory doesn't exist. It needs to
337 # checkout instead.
338 print("\n_____ %s is missing, synching instead" % self.relpath)
339 # Don't reuse the args.
340 return self.update(options, [], file_list)
341
maruel@chromium.org754960e2009-09-21 12:31:05 +0000342 for file in CaptureSVNStatus(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000343 file_path = os.path.join(path, file[1])
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000344 if file[0][0] == 'X':
maruel@chromium.org754960e2009-09-21 12:31:05 +0000345 # Ignore externals.
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000346 logging.info('Ignoring external %s' % file_path)
maruel@chromium.org754960e2009-09-21 12:31:05 +0000347 continue
348
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000349 if logging.getLogger().isEnabledFor(logging.INFO):
350 logging.info('%s%s' % (file[0], file[1]))
351 else:
352 print(file_path)
353 if file[0].isspace():
354 logging.error('No idea what is the status of %s.\n'
355 'You just found a bug in gclient, please ping '
356 'maruel@chromium.org ASAP!' % file_path)
357 # svn revert is really stupid. It fails on inconsistent line-endings,
358 # on switched directories, etc. So take no chance and delete everything!
359 try:
360 if not os.path.exists(file_path):
361 pass
362 elif os.path.isfile(file_path):
maruel@chromium.org754960e2009-09-21 12:31:05 +0000363 logging.info('os.remove(%s)' % file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000364 os.remove(file_path)
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000365 elif os.path.isdir(file_path):
maruel@chromium.org754960e2009-09-21 12:31:05 +0000366 logging.info('gclient_utils.RemoveDirectory(%s)' % file_path)
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +0000367 gclient_utils.RemoveDirectory(file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000368 else:
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000369 logging.error('no idea what is %s.\nYou just found a bug in gclient'
370 ', please ping maruel@chromium.org ASAP!' % file_path)
371 except EnvironmentError:
372 logging.error('Failed to remove %s.' % file_path)
373
maruel@chromium.org810a50b2009-10-05 23:03:18 +0000374 try:
375 # svn revert is so broken we don't even use it. Using
376 # "svn up --revision BASE" achieve the same effect.
dpranke@google.com22e29d42009-10-28 00:48:26 +0000377 RunSVNAndGetFileList(options, ['update', '--revision', 'BASE'], path,
maruel@chromium.org810a50b2009-10-05 23:03:18 +0000378 file_list)
379 except OSError, e:
380 # Maybe the directory disapeared meanwhile. We don't want it to throw an
381 # exception.
382 logging.error('Failed to update:\n%s' % str(e))
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000383
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000384 def runhooks(self, options, args, file_list):
385 self.status(options, args, file_list)
386
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000387 def status(self, options, args, file_list):
388 """Display status information."""
389 path = os.path.join(self._root_dir, self.relpath)
390 command = ['status']
391 command.extend(args)
392 if not os.path.isdir(path):
393 # svn status won't work if the directory doesn't exist.
394 print("\n________ couldn't run \'%s\' in \'%s\':\nThe directory "
395 "does not exist."
396 % (' '.join(command), path))
397 # There's no file list to retrieve.
398 else:
dpranke@google.com22e29d42009-10-28 00:48:26 +0000399 RunSVNAndGetFileList(options, command, path, file_list)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000400
401 def pack(self, options, args, file_list):
402 """Generates a patch file which can be applied to the root of the
403 repository."""
404 path = os.path.join(self._root_dir, self.relpath)
405 command = ['diff']
406 command.extend(args)
407 # Simple class which tracks which file is being diffed and
408 # replaces instances of its file name in the original and
409 # working copy lines of the svn diff output.
410 class DiffFilterer(object):
411 index_string = "Index: "
412 original_prefix = "--- "
413 working_prefix = "+++ "
414
415 def __init__(self, relpath):
416 # Note that we always use '/' as the path separator to be
417 # consistent with svn's cygwin-style output on Windows
418 self._relpath = relpath.replace("\\", "/")
419 self._current_file = ""
420 self._replacement_file = ""
421
422 def SetCurrentFile(self, file):
423 self._current_file = file
424 # Note that we always use '/' as the path separator to be
425 # consistent with svn's cygwin-style output on Windows
426 self._replacement_file = self._relpath + '/' + file
427
428 def ReplaceAndPrint(self, line):
429 print(line.replace(self._current_file, self._replacement_file))
430
431 def Filter(self, line):
432 if (line.startswith(self.index_string)):
433 self.SetCurrentFile(line[len(self.index_string):])
434 self.ReplaceAndPrint(line)
435 else:
436 if (line.startswith(self.original_prefix) or
437 line.startswith(self.working_prefix)):
438 self.ReplaceAndPrint(line)
439 else:
440 print line
441
442 filterer = DiffFilterer(self.relpath)
443 RunSVNAndFilterOutput(command, path, False, False, filterer.Filter)
444
445
446# -----------------------------------------------------------------------------
chase@chromium.org8e416c82009-10-06 04:30:44 +0000447# Git utils:
448
449
450def CaptureGit(args, in_directory=None, print_error=True):
451 """Runs git, capturing output sent to stdout as a string.
452
453 Args:
454 args: A sequence of command line parameters to be passed to git.
455 in_directory: The directory where git is to be run.
456
457 Returns:
458 The output sent to stdout as a string.
459 """
460 c = [GIT_COMMAND]
461 c.extend(args)
462
463 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
464 # the git.exe executable, but shell=True makes subprocess on Linux fail
465 # when it's called with a list because it only tries to execute the
466 # first string ("git").
467 stderr = None
468 if not print_error:
469 stderr = subprocess.PIPE
470 return subprocess.Popen(c,
471 cwd=in_directory,
472 shell=sys.platform.startswith('win'),
473 stdout=subprocess.PIPE,
474 stderr=stderr).communicate()[0]
475
476
477def CaptureGitStatus(files, upstream_branch='origin'):
478 """Returns git status.
479
480 @files can be a string (one file) or a list of files.
481
482 Returns an array of (status, file) tuples."""
483 command = ["diff", "--name-status", "-r", "%s.." % upstream_branch]
484 if not files:
485 pass
486 elif isinstance(files, basestring):
487 command.append(files)
488 else:
489 command.extend(files)
490
491 status = CaptureGit(command).rstrip()
492 results = []
493 if status:
494 for statusline in status.split('\n'):
495 m = re.match('^(\w)\t(.+)$', statusline)
496 if not m:
497 raise Exception("status currently unsupported: %s" % statusline)
498 results.append(('%s ' % m.group(1), m.group(2)))
499 return results
500
501
502# -----------------------------------------------------------------------------
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000503# SVN utils:
504
505
506def RunSVN(args, in_directory):
507 """Runs svn, sending output to stdout.
508
509 Args:
510 args: A sequence of command line parameters to be passed to svn.
511 in_directory: The directory where svn is to be run.
512
513 Raises:
514 Error: An error occurred while running the svn command.
515 """
516 c = [SVN_COMMAND]
517 c.extend(args)
518
519 gclient_utils.SubprocessCall(c, in_directory)
520
521
522def CaptureSVN(args, in_directory=None, print_error=True):
523 """Runs svn, capturing output sent to stdout as a string.
524
525 Args:
526 args: A sequence of command line parameters to be passed to svn.
527 in_directory: The directory where svn is to be run.
528
529 Returns:
530 The output sent to stdout as a string.
531 """
532 c = [SVN_COMMAND]
533 c.extend(args)
534
535 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
536 # the svn.exe executable, but shell=True makes subprocess on Linux fail
537 # when it's called with a list because it only tries to execute the
538 # first string ("svn").
539 stderr = None
540 if not print_error:
541 stderr = subprocess.PIPE
542 return subprocess.Popen(c,
543 cwd=in_directory,
544 shell=(sys.platform == 'win32'),
545 stdout=subprocess.PIPE,
546 stderr=stderr).communicate()[0]
547
548
dpranke@google.com22e29d42009-10-28 00:48:26 +0000549def RunSVNAndGetFileList(options, args, in_directory, file_list):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000550 """Runs svn checkout, update, or status, output to stdout.
551
552 The first item in args must be either "checkout", "update", or "status".
553
554 svn's stdout is parsed to collect a list of files checked out or updated.
555 These files are appended to file_list. svn's stdout is also printed to
556 sys.stdout as in RunSVN.
557
558 Args:
dpranke@google.com22e29d42009-10-28 00:48:26 +0000559 options: command line options to gclient
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000560 args: A sequence of command line parameters to be passed to svn.
561 in_directory: The directory where svn is to be run.
562
563 Raises:
564 Error: An error occurred while running the svn command.
565 """
566 command = [SVN_COMMAND]
567 command.extend(args)
568
569 # svn update and svn checkout use the same pattern: the first three columns
570 # are for file status, property status, and lock status. This is followed
571 # by two spaces, and then the path to the file.
572 update_pattern = '^... (.*)$'
573
574 # The first three columns of svn status are the same as for svn update and
575 # svn checkout. The next three columns indicate addition-with-history,
576 # switch, and remote lock status. This is followed by one space, and then
577 # the path to the file.
578 status_pattern = '^...... (.*)$'
579
580 # args[0] must be a supported command. This will blow up if it's something
581 # else, which is good. Note that the patterns are only effective when
582 # these commands are used in their ordinary forms, the patterns are invalid
583 # for "svn status --show-updates", for example.
584 pattern = {
585 'checkout': update_pattern,
586 'status': status_pattern,
587 'update': update_pattern,
588 }[args[0]]
589
590 compiled_pattern = re.compile(pattern)
591
592 def CaptureMatchingLines(line):
593 match = compiled_pattern.search(line)
594 if match:
595 file_list.append(match.group(1))
596
597 RunSVNAndFilterOutput(args,
598 in_directory,
dpranke@google.com22e29d42009-10-28 00:48:26 +0000599 options.verbose,
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000600 True,
601 CaptureMatchingLines)
602
603def RunSVNAndFilterOutput(args,
604 in_directory,
605 print_messages,
606 print_stdout,
607 filter):
608 """Runs svn checkout, update, status, or diff, optionally outputting
609 to stdout.
610
611 The first item in args must be either "checkout", "update",
612 "status", or "diff".
613
614 svn's stdout is passed line-by-line to the given filter function. If
615 print_stdout is true, it is also printed to sys.stdout as in RunSVN.
616
617 Args:
618 args: A sequence of command line parameters to be passed to svn.
619 in_directory: The directory where svn is to be run.
620 print_messages: Whether to print status messages to stdout about
621 which Subversion commands are being run.
622 print_stdout: Whether to forward Subversion's output to stdout.
623 filter: A function taking one argument (a string) which will be
624 passed each line (with the ending newline character removed) of
625 Subversion's output for filtering.
626
627 Raises:
628 Error: An error occurred while running the svn command.
629 """
630 command = [SVN_COMMAND]
631 command.extend(args)
632
633 gclient_utils.SubprocessCallAndFilter(command,
634 in_directory,
635 print_messages,
636 print_stdout,
637 filter=filter)
638
639def CaptureSVNInfo(relpath, in_directory=None, print_error=True):
640 """Returns a dictionary from the svn info output for the given file.
641
642 Args:
643 relpath: The directory where the working copy resides relative to
644 the directory given by in_directory.
645 in_directory: The directory where svn is to be run.
646 """
647 output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error)
648 dom = gclient_utils.ParseXML(output)
649 result = {}
650 if dom:
651 GetNamedNodeText = gclient_utils.GetNamedNodeText
652 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
653 def C(item, f):
654 if item is not None: return f(item)
655 # /info/entry/
656 # url
657 # reposityory/(root|uuid)
658 # wc-info/(schedule|depth)
659 # commit/(author|date)
660 # str() the results because they may be returned as Unicode, which
661 # interferes with the higher layers matching up things in the deps
662 # dictionary.
663 # TODO(maruel): Fix at higher level instead (!)
664 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
665 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
666 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
667 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry', 'revision'),
668 int)
669 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
670 str)
671 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
672 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
673 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
674 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
675 return result
676
677
678def CaptureSVNHeadRevision(url):
679 """Get the head revision of a SVN repository.
680
681 Returns:
682 Int head revision
683 """
684 info = CaptureSVN(["info", "--xml", url], os.getcwd())
685 dom = xml.dom.minidom.parseString(info)
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000686 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000687
688
689def CaptureSVNStatus(files):
690 """Returns the svn 1.5 svn status emulated output.
691
692 @files can be a string (one file) or a list of files.
693
694 Returns an array of (status, file) tuples."""
695 command = ["status", "--xml"]
696 if not files:
697 pass
698 elif isinstance(files, basestring):
699 command.append(files)
700 else:
701 command.extend(files)
702
703 status_letter = {
704 None: ' ',
705 '': ' ',
706 'added': 'A',
707 'conflicted': 'C',
708 'deleted': 'D',
709 'external': 'X',
710 'ignored': 'I',
711 'incomplete': '!',
712 'merged': 'G',
713 'missing': '!',
714 'modified': 'M',
715 'none': ' ',
716 'normal': ' ',
717 'obstructed': '~',
718 'replaced': 'R',
719 'unversioned': '?',
720 }
721 dom = gclient_utils.ParseXML(CaptureSVN(command))
722 results = []
723 if dom:
724 # /status/target/entry/(wc-status|commit|author|date)
725 for target in dom.getElementsByTagName('target'):
726 base_path = target.getAttribute('path')
727 for entry in target.getElementsByTagName('entry'):
728 file = entry.getAttribute('path')
729 wc_status = entry.getElementsByTagName('wc-status')
730 assert len(wc_status) == 1
731 # Emulate svn 1.5 status ouput...
732 statuses = [' ' for i in range(7)]
733 # Col 0
734 xml_item_status = wc_status[0].getAttribute('item')
735 if xml_item_status in status_letter:
736 statuses[0] = status_letter[xml_item_status]
737 else:
738 raise Exception('Unknown item status "%s"; please implement me!' %
739 xml_item_status)
740 # Col 1
741 xml_props_status = wc_status[0].getAttribute('props')
742 if xml_props_status == 'modified':
743 statuses[1] = 'M'
744 elif xml_props_status == 'conflicted':
745 statuses[1] = 'C'
746 elif (not xml_props_status or xml_props_status == 'none' or
747 xml_props_status == 'normal'):
748 pass
749 else:
750 raise Exception('Unknown props status "%s"; please implement me!' %
751 xml_props_status)
752 # Col 2
753 if wc_status[0].getAttribute('wc-locked') == 'true':
754 statuses[2] = 'L'
755 # Col 3
756 if wc_status[0].getAttribute('copied') == 'true':
757 statuses[3] = '+'
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000758 # Col 4
759 if wc_status[0].getAttribute('switched') == 'true':
760 statuses[4] = 'S'
761 # TODO(maruel): Col 5 and 6
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000762 item = (''.join(statuses), file)
763 results.append(item)
764 return results