blob: fba971cea1e93c628b150308796fde4889df52bb [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)])
maruel@chromium.org9344e972009-10-05 22:52:57 +0000254 RunSVNAndGetFileList(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.org9344e972009-10-05 22:52:57 +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)])
maruel@chromium.org9344e972009-10-05 22:52:57 +0000312 RunSVNAndGetFileList(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)])
maruel@chromium.org9344e972009-10-05 22:52:57 +0000326 RunSVNAndGetFileList(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.
377 RunSVNAndGetFileList(['update', '--revision', 'BASE'], path,
378 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:
maruel@chromium.org9344e972009-10-05 22:52:57 +0000399 RunSVNAndGetFileList(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
maruel@chromium.org9344e972009-10-05 22:52:57 +0000549def RunSVNAndGetFileList(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:
559 args: A sequence of command line parameters to be passed to svn.
560 in_directory: The directory where svn is to be run.
561
562 Raises:
563 Error: An error occurred while running the svn command.
564 """
565 command = [SVN_COMMAND]
566 command.extend(args)
567
568 # svn update and svn checkout use the same pattern: the first three columns
569 # are for file status, property status, and lock status. This is followed
570 # by two spaces, and then the path to the file.
571 update_pattern = '^... (.*)$'
572
573 # The first three columns of svn status are the same as for svn update and
574 # svn checkout. The next three columns indicate addition-with-history,
575 # switch, and remote lock status. This is followed by one space, and then
576 # the path to the file.
577 status_pattern = '^...... (.*)$'
578
579 # args[0] must be a supported command. This will blow up if it's something
580 # else, which is good. Note that the patterns are only effective when
581 # these commands are used in their ordinary forms, the patterns are invalid
582 # for "svn status --show-updates", for example.
583 pattern = {
584 'checkout': update_pattern,
585 'status': status_pattern,
586 'update': update_pattern,
587 }[args[0]]
588
589 compiled_pattern = re.compile(pattern)
590
591 def CaptureMatchingLines(line):
592 match = compiled_pattern.search(line)
593 if match:
594 file_list.append(match.group(1))
595
596 RunSVNAndFilterOutput(args,
597 in_directory,
maruel@chromium.org9344e972009-10-05 22:52:57 +0000598 True,
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000599 True,
600 CaptureMatchingLines)
601
602def RunSVNAndFilterOutput(args,
603 in_directory,
604 print_messages,
605 print_stdout,
606 filter):
607 """Runs svn checkout, update, status, or diff, optionally outputting
608 to stdout.
609
610 The first item in args must be either "checkout", "update",
611 "status", or "diff".
612
613 svn's stdout is passed line-by-line to the given filter function. If
614 print_stdout is true, it is also printed to sys.stdout as in RunSVN.
615
616 Args:
617 args: A sequence of command line parameters to be passed to svn.
618 in_directory: The directory where svn is to be run.
619 print_messages: Whether to print status messages to stdout about
620 which Subversion commands are being run.
621 print_stdout: Whether to forward Subversion's output to stdout.
622 filter: A function taking one argument (a string) which will be
623 passed each line (with the ending newline character removed) of
624 Subversion's output for filtering.
625
626 Raises:
627 Error: An error occurred while running the svn command.
628 """
629 command = [SVN_COMMAND]
630 command.extend(args)
631
632 gclient_utils.SubprocessCallAndFilter(command,
633 in_directory,
634 print_messages,
635 print_stdout,
636 filter=filter)
637
638def CaptureSVNInfo(relpath, in_directory=None, print_error=True):
639 """Returns a dictionary from the svn info output for the given file.
640
641 Args:
642 relpath: The directory where the working copy resides relative to
643 the directory given by in_directory.
644 in_directory: The directory where svn is to be run.
645 """
646 output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error)
647 dom = gclient_utils.ParseXML(output)
648 result = {}
649 if dom:
650 GetNamedNodeText = gclient_utils.GetNamedNodeText
651 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
652 def C(item, f):
653 if item is not None: return f(item)
654 # /info/entry/
655 # url
656 # reposityory/(root|uuid)
657 # wc-info/(schedule|depth)
658 # commit/(author|date)
659 # str() the results because they may be returned as Unicode, which
660 # interferes with the higher layers matching up things in the deps
661 # dictionary.
662 # TODO(maruel): Fix at higher level instead (!)
663 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
664 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
665 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
666 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry', 'revision'),
667 int)
668 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
669 str)
670 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
671 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
672 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
673 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
674 return result
675
676
677def CaptureSVNHeadRevision(url):
678 """Get the head revision of a SVN repository.
679
680 Returns:
681 Int head revision
682 """
683 info = CaptureSVN(["info", "--xml", url], os.getcwd())
684 dom = xml.dom.minidom.parseString(info)
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000685 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000686
687
688def CaptureSVNStatus(files):
689 """Returns the svn 1.5 svn status emulated output.
690
691 @files can be a string (one file) or a list of files.
692
693 Returns an array of (status, file) tuples."""
694 command = ["status", "--xml"]
695 if not files:
696 pass
697 elif isinstance(files, basestring):
698 command.append(files)
699 else:
700 command.extend(files)
701
702 status_letter = {
703 None: ' ',
704 '': ' ',
705 'added': 'A',
706 'conflicted': 'C',
707 'deleted': 'D',
708 'external': 'X',
709 'ignored': 'I',
710 'incomplete': '!',
711 'merged': 'G',
712 'missing': '!',
713 'modified': 'M',
714 'none': ' ',
715 'normal': ' ',
716 'obstructed': '~',
717 'replaced': 'R',
718 'unversioned': '?',
719 }
720 dom = gclient_utils.ParseXML(CaptureSVN(command))
721 results = []
722 if dom:
723 # /status/target/entry/(wc-status|commit|author|date)
724 for target in dom.getElementsByTagName('target'):
725 base_path = target.getAttribute('path')
726 for entry in target.getElementsByTagName('entry'):
727 file = entry.getAttribute('path')
728 wc_status = entry.getElementsByTagName('wc-status')
729 assert len(wc_status) == 1
730 # Emulate svn 1.5 status ouput...
731 statuses = [' ' for i in range(7)]
732 # Col 0
733 xml_item_status = wc_status[0].getAttribute('item')
734 if xml_item_status in status_letter:
735 statuses[0] = status_letter[xml_item_status]
736 else:
737 raise Exception('Unknown item status "%s"; please implement me!' %
738 xml_item_status)
739 # Col 1
740 xml_props_status = wc_status[0].getAttribute('props')
741 if xml_props_status == 'modified':
742 statuses[1] = 'M'
743 elif xml_props_status == 'conflicted':
744 statuses[1] = 'C'
745 elif (not xml_props_status or xml_props_status == 'none' or
746 xml_props_status == 'normal'):
747 pass
748 else:
749 raise Exception('Unknown props status "%s"; please implement me!' %
750 xml_props_status)
751 # Col 2
752 if wc_status[0].getAttribute('wc-locked') == 'true':
753 statuses[2] = 'L'
754 # Col 3
755 if wc_status[0].getAttribute('copied') == 'true':
756 statuses[3] = '+'
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000757 # Col 4
758 if wc_status[0].getAttribute('switched') == 'true':
759 statuses[4] = 'S'
760 # TODO(maruel): Col 5 and 6
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000761 item = (''.join(statuses), file)
762 results.append(item)
763 return results