blob: 94d41ffe864a77b44652ca62e503026feb816f40 [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 """
msb@chromium.org260c6532009-10-28 03:22:35 +0000156 path = os.path.join(self._root_dir, self.relpath)
157 if not os.path.isdir(path):
158 # revert won't work if the directory doesn't exist. It needs to
159 # checkout instead.
160 print("\n_____ %s is missing, synching instead" % self.relpath)
161 # Don't reuse the args.
162 return self.update(options, [], file_list)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000163 merge_base = self._RunGit(['merge-base', 'HEAD', 'origin'])
164 files = self._RunGit(['diff', merge_base, '--name-only']).split()
165 print self._RunGit(['reset', '--hard', merge_base])
166 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
167
168 def runhooks(self, options, args, file_list):
169 self.status(options, args, file_list)
170
171 def status(self, options, args, file_list):
172 """Display status information."""
173 if not os.path.isdir(self.checkout_path):
174 print('\n________ couldn\'t run status in %s:\nThe directory '
175 'does not exist.' % checkout_path)
176 else:
177 merge_base = self._RunGit(['merge-base', 'HEAD', 'origin'])
178 print self._RunGit(['diff', '--name-status', merge_base])
179 files = self._RunGit(['diff', '--name-only', merge_base]).split()
180 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
181
182 def _RunGit(self, args, cwd=None, checkrc=True):
183 if cwd == None:
184 cwd = self.checkout_path
185 cmd = ['git']
186 cmd.extend(args)
187 sp = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE)
188 if checkrc and sp.returncode:
189 raise gclient_utils.Error('git command %s returned %d' %
190 (args[0], sp.returncode))
191 return sp.communicate()[0].strip()
192
193
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000194class SVNWrapper(SCMWrapper):
195 """ Wrapper for SVN """
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000196
197 def cleanup(self, options, args, file_list):
198 """Cleanup working copy."""
199 command = ['cleanup']
200 command.extend(args)
201 RunSVN(command, os.path.join(self._root_dir, self.relpath))
202
203 def diff(self, options, args, file_list):
204 # NOTE: This function does not currently modify file_list.
205 command = ['diff']
206 command.extend(args)
207 RunSVN(command, os.path.join(self._root_dir, self.relpath))
208
209 def export(self, options, args, file_list):
210 assert len(args) == 1
211 export_path = os.path.abspath(os.path.join(args[0], self.relpath))
212 try:
213 os.makedirs(export_path)
214 except OSError:
215 pass
216 assert os.path.exists(export_path)
217 command = ['export', '--force', '.']
218 command.append(export_path)
219 RunSVN(command, os.path.join(self._root_dir, self.relpath))
220
221 def update(self, options, args, file_list):
222 """Runs SCM to update or transparently checkout the working copy.
223
224 All updated files will be appended to file_list.
225
226 Raises:
227 Error: if can't get URL for relative path.
228 """
229 # Only update if git is not controlling the directory.
230 checkout_path = os.path.join(self._root_dir, self.relpath)
231 git_path = os.path.join(self._root_dir, self.relpath, '.git')
232 if os.path.exists(git_path):
233 print("________ found .git directory; skipping %s" % self.relpath)
234 return
235
236 if args:
237 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
238
239 url = self.url
240 components = url.split("@")
241 revision = None
242 forced_revision = False
243 if options.revision:
244 # Override the revision number.
245 url = '%s@%s' % (components[0], str(options.revision))
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000246 revision = options.revision
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000247 forced_revision = True
248 elif len(components) == 2:
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000249 revision = components[1]
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000250 forced_revision = True
251
252 rev_str = ""
253 if revision:
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000254 rev_str = ' at %s' % revision
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000255
256 if not os.path.exists(checkout_path):
257 # We need to checkout.
258 command = ['checkout', url, checkout_path]
259 if revision:
260 command.extend(['--revision', str(revision)])
dpranke@google.com22e29d42009-10-28 00:48:26 +0000261 RunSVNAndGetFileList(options, command, self._root_dir, file_list)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000262 return
263
264 # Get the existing scm url and the revision number of the current checkout.
265 from_info = CaptureSVNInfo(os.path.join(checkout_path, '.'), '.')
266 if not from_info:
267 raise gclient_utils.Error("Can't update/checkout %r if an unversioned "
268 "directory is present. Delete the directory "
269 "and try again." %
270 checkout_path)
271
maruel@chromium.org7753d242009-10-07 17:40:24 +0000272 if options.manually_grab_svn_rev:
273 # Retrieve the current HEAD version because svn is slow at null updates.
274 if not revision:
275 from_info_live = CaptureSVNInfo(from_info['URL'], '.')
276 revision = str(from_info_live['Revision'])
277 rev_str = ' at %s' % revision
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000278
279 if from_info['URL'] != components[0]:
280 to_info = CaptureSVNInfo(url, '.')
maruel@chromium.orge2ce0c72009-09-23 16:14:18 +0000281 if not to_info.get('Repository Root') or not to_info.get('UUID'):
282 # The url is invalid or the server is not accessible, it's safer to bail
283 # out right now.
284 raise gclient_utils.Error('This url is unreachable: %s' % url)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000285 can_switch = ((from_info['Repository Root'] != to_info['Repository Root'])
286 and (from_info['UUID'] == to_info['UUID']))
287 if can_switch:
288 print("\n_____ relocating %s to a new checkout" % self.relpath)
289 # We have different roots, so check if we can switch --relocate.
290 # Subversion only permits this if the repository UUIDs match.
291 # Perform the switch --relocate, then rewrite the from_url
292 # to reflect where we "are now." (This is the same way that
293 # Subversion itself handles the metadata when switch --relocate
294 # is used.) This makes the checks below for whether we
295 # can update to a revision or have to switch to a different
296 # branch work as expected.
297 # TODO(maruel): TEST ME !
298 command = ["switch", "--relocate",
299 from_info['Repository Root'],
300 to_info['Repository Root'],
301 self.relpath]
302 RunSVN(command, self._root_dir)
303 from_info['URL'] = from_info['URL'].replace(
304 from_info['Repository Root'],
305 to_info['Repository Root'])
306 else:
307 if CaptureSVNStatus(checkout_path):
308 raise gclient_utils.Error("Can't switch the checkout to %s; UUID "
309 "don't match and there is local changes "
310 "in %s. Delete the directory and "
311 "try again." % (url, checkout_path))
312 # Ok delete it.
313 print("\n_____ switching %s to a new checkout" % self.relpath)
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +0000314 gclient_utils.RemoveDirectory(checkout_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000315 # We need to checkout.
316 command = ['checkout', url, checkout_path]
317 if revision:
318 command.extend(['--revision', str(revision)])
dpranke@google.com22e29d42009-10-28 00:48:26 +0000319 RunSVNAndGetFileList(options, command, self._root_dir, file_list)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000320 return
321
322
323 # If the provided url has a revision number that matches the revision
324 # number of the existing directory, then we don't need to bother updating.
maruel@chromium.org2e0c6852009-09-24 00:02:07 +0000325 if not options.force and str(from_info['Revision']) == revision:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000326 if options.verbose or not forced_revision:
327 print("\n_____ %s%s" % (self.relpath, rev_str))
328 return
329
330 command = ["update", checkout_path]
331 if revision:
332 command.extend(['--revision', str(revision)])
dpranke@google.com22e29d42009-10-28 00:48:26 +0000333 RunSVNAndGetFileList(options, command, self._root_dir, file_list)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000334
335 def revert(self, options, args, file_list):
336 """Reverts local modifications. Subversion specific.
337
338 All reverted files will be appended to file_list, even if Subversion
339 doesn't know about them.
340 """
341 path = os.path.join(self._root_dir, self.relpath)
342 if not os.path.isdir(path):
343 # svn revert won't work if the directory doesn't exist. It needs to
344 # checkout instead.
345 print("\n_____ %s is missing, synching instead" % self.relpath)
346 # Don't reuse the args.
347 return self.update(options, [], file_list)
348
maruel@chromium.org754960e2009-09-21 12:31:05 +0000349 for file in CaptureSVNStatus(path):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000350 file_path = os.path.join(path, file[1])
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000351 if file[0][0] == 'X':
maruel@chromium.org754960e2009-09-21 12:31:05 +0000352 # Ignore externals.
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000353 logging.info('Ignoring external %s' % file_path)
maruel@chromium.org754960e2009-09-21 12:31:05 +0000354 continue
355
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000356 if logging.getLogger().isEnabledFor(logging.INFO):
357 logging.info('%s%s' % (file[0], file[1]))
358 else:
359 print(file_path)
360 if file[0].isspace():
361 logging.error('No idea what is the status of %s.\n'
362 'You just found a bug in gclient, please ping '
363 'maruel@chromium.org ASAP!' % file_path)
364 # svn revert is really stupid. It fails on inconsistent line-endings,
365 # on switched directories, etc. So take no chance and delete everything!
366 try:
367 if not os.path.exists(file_path):
368 pass
369 elif os.path.isfile(file_path):
maruel@chromium.org754960e2009-09-21 12:31:05 +0000370 logging.info('os.remove(%s)' % file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000371 os.remove(file_path)
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000372 elif os.path.isdir(file_path):
maruel@chromium.org754960e2009-09-21 12:31:05 +0000373 logging.info('gclient_utils.RemoveDirectory(%s)' % file_path)
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +0000374 gclient_utils.RemoveDirectory(file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000375 else:
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000376 logging.error('no idea what is %s.\nYou just found a bug in gclient'
377 ', please ping maruel@chromium.org ASAP!' % file_path)
378 except EnvironmentError:
379 logging.error('Failed to remove %s.' % file_path)
380
maruel@chromium.org810a50b2009-10-05 23:03:18 +0000381 try:
382 # svn revert is so broken we don't even use it. Using
383 # "svn up --revision BASE" achieve the same effect.
dpranke@google.com22e29d42009-10-28 00:48:26 +0000384 RunSVNAndGetFileList(options, ['update', '--revision', 'BASE'], path,
maruel@chromium.org810a50b2009-10-05 23:03:18 +0000385 file_list)
386 except OSError, e:
387 # Maybe the directory disapeared meanwhile. We don't want it to throw an
388 # exception.
389 logging.error('Failed to update:\n%s' % str(e))
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000390
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000391 def runhooks(self, options, args, file_list):
392 self.status(options, args, file_list)
393
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000394 def status(self, options, args, file_list):
395 """Display status information."""
396 path = os.path.join(self._root_dir, self.relpath)
397 command = ['status']
398 command.extend(args)
399 if not os.path.isdir(path):
400 # svn status won't work if the directory doesn't exist.
401 print("\n________ couldn't run \'%s\' in \'%s\':\nThe directory "
402 "does not exist."
403 % (' '.join(command), path))
404 # There's no file list to retrieve.
405 else:
dpranke@google.com22e29d42009-10-28 00:48:26 +0000406 RunSVNAndGetFileList(options, command, path, file_list)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000407
408 def pack(self, options, args, file_list):
409 """Generates a patch file which can be applied to the root of the
410 repository."""
411 path = os.path.join(self._root_dir, self.relpath)
412 command = ['diff']
413 command.extend(args)
414 # Simple class which tracks which file is being diffed and
415 # replaces instances of its file name in the original and
416 # working copy lines of the svn diff output.
417 class DiffFilterer(object):
418 index_string = "Index: "
419 original_prefix = "--- "
420 working_prefix = "+++ "
421
422 def __init__(self, relpath):
423 # Note that we always use '/' as the path separator to be
424 # consistent with svn's cygwin-style output on Windows
425 self._relpath = relpath.replace("\\", "/")
426 self._current_file = ""
427 self._replacement_file = ""
428
429 def SetCurrentFile(self, file):
430 self._current_file = file
431 # Note that we always use '/' as the path separator to be
432 # consistent with svn's cygwin-style output on Windows
433 self._replacement_file = self._relpath + '/' + file
434
435 def ReplaceAndPrint(self, line):
436 print(line.replace(self._current_file, self._replacement_file))
437
438 def Filter(self, line):
439 if (line.startswith(self.index_string)):
440 self.SetCurrentFile(line[len(self.index_string):])
441 self.ReplaceAndPrint(line)
442 else:
443 if (line.startswith(self.original_prefix) or
444 line.startswith(self.working_prefix)):
445 self.ReplaceAndPrint(line)
446 else:
447 print line
448
449 filterer = DiffFilterer(self.relpath)
450 RunSVNAndFilterOutput(command, path, False, False, filterer.Filter)
451
452
453# -----------------------------------------------------------------------------
chase@chromium.org8e416c82009-10-06 04:30:44 +0000454# Git utils:
455
456
457def CaptureGit(args, in_directory=None, print_error=True):
458 """Runs git, capturing output sent to stdout as a string.
459
460 Args:
461 args: A sequence of command line parameters to be passed to git.
462 in_directory: The directory where git is to be run.
463
464 Returns:
465 The output sent to stdout as a string.
466 """
467 c = [GIT_COMMAND]
468 c.extend(args)
469
470 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
471 # the git.exe executable, but shell=True makes subprocess on Linux fail
472 # when it's called with a list because it only tries to execute the
473 # first string ("git").
474 stderr = None
475 if not print_error:
476 stderr = subprocess.PIPE
477 return subprocess.Popen(c,
478 cwd=in_directory,
479 shell=sys.platform.startswith('win'),
480 stdout=subprocess.PIPE,
481 stderr=stderr).communicate()[0]
482
483
484def CaptureGitStatus(files, upstream_branch='origin'):
485 """Returns git status.
486
487 @files can be a string (one file) or a list of files.
488
489 Returns an array of (status, file) tuples."""
490 command = ["diff", "--name-status", "-r", "%s.." % upstream_branch]
491 if not files:
492 pass
493 elif isinstance(files, basestring):
494 command.append(files)
495 else:
496 command.extend(files)
497
498 status = CaptureGit(command).rstrip()
499 results = []
500 if status:
501 for statusline in status.split('\n'):
502 m = re.match('^(\w)\t(.+)$', statusline)
503 if not m:
504 raise Exception("status currently unsupported: %s" % statusline)
505 results.append(('%s ' % m.group(1), m.group(2)))
506 return results
507
508
509# -----------------------------------------------------------------------------
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000510# SVN utils:
511
512
513def RunSVN(args, in_directory):
514 """Runs svn, sending output to stdout.
515
516 Args:
517 args: A sequence of command line parameters to be passed to svn.
518 in_directory: The directory where svn is to be run.
519
520 Raises:
521 Error: An error occurred while running the svn command.
522 """
523 c = [SVN_COMMAND]
524 c.extend(args)
525
526 gclient_utils.SubprocessCall(c, in_directory)
527
528
529def CaptureSVN(args, in_directory=None, print_error=True):
530 """Runs svn, capturing output sent to stdout as a string.
531
532 Args:
533 args: A sequence of command line parameters to be passed to svn.
534 in_directory: The directory where svn is to be run.
535
536 Returns:
537 The output sent to stdout as a string.
538 """
539 c = [SVN_COMMAND]
540 c.extend(args)
541
542 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
543 # the svn.exe executable, but shell=True makes subprocess on Linux fail
544 # when it's called with a list because it only tries to execute the
545 # first string ("svn").
546 stderr = None
547 if not print_error:
548 stderr = subprocess.PIPE
549 return subprocess.Popen(c,
550 cwd=in_directory,
551 shell=(sys.platform == 'win32'),
552 stdout=subprocess.PIPE,
553 stderr=stderr).communicate()[0]
554
555
dpranke@google.com22e29d42009-10-28 00:48:26 +0000556def RunSVNAndGetFileList(options, args, in_directory, file_list):
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000557 """Runs svn checkout, update, or status, output to stdout.
558
559 The first item in args must be either "checkout", "update", or "status".
560
561 svn's stdout is parsed to collect a list of files checked out or updated.
562 These files are appended to file_list. svn's stdout is also printed to
563 sys.stdout as in RunSVN.
564
565 Args:
dpranke@google.com22e29d42009-10-28 00:48:26 +0000566 options: command line options to gclient
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000567 args: A sequence of command line parameters to be passed to svn.
568 in_directory: The directory where svn is to be run.
569
570 Raises:
571 Error: An error occurred while running the svn command.
572 """
573 command = [SVN_COMMAND]
574 command.extend(args)
575
576 # svn update and svn checkout use the same pattern: the first three columns
577 # are for file status, property status, and lock status. This is followed
578 # by two spaces, and then the path to the file.
579 update_pattern = '^... (.*)$'
580
581 # The first three columns of svn status are the same as for svn update and
582 # svn checkout. The next three columns indicate addition-with-history,
583 # switch, and remote lock status. This is followed by one space, and then
584 # the path to the file.
585 status_pattern = '^...... (.*)$'
586
587 # args[0] must be a supported command. This will blow up if it's something
588 # else, which is good. Note that the patterns are only effective when
589 # these commands are used in their ordinary forms, the patterns are invalid
590 # for "svn status --show-updates", for example.
591 pattern = {
592 'checkout': update_pattern,
593 'status': status_pattern,
594 'update': update_pattern,
595 }[args[0]]
596
597 compiled_pattern = re.compile(pattern)
598
599 def CaptureMatchingLines(line):
600 match = compiled_pattern.search(line)
601 if match:
602 file_list.append(match.group(1))
603
604 RunSVNAndFilterOutput(args,
605 in_directory,
dpranke@google.com22e29d42009-10-28 00:48:26 +0000606 options.verbose,
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000607 True,
608 CaptureMatchingLines)
609
610def RunSVNAndFilterOutput(args,
611 in_directory,
612 print_messages,
613 print_stdout,
614 filter):
615 """Runs svn checkout, update, status, or diff, optionally outputting
616 to stdout.
617
618 The first item in args must be either "checkout", "update",
619 "status", or "diff".
620
621 svn's stdout is passed line-by-line to the given filter function. If
622 print_stdout is true, it is also printed to sys.stdout as in RunSVN.
623
624 Args:
625 args: A sequence of command line parameters to be passed to svn.
626 in_directory: The directory where svn is to be run.
627 print_messages: Whether to print status messages to stdout about
628 which Subversion commands are being run.
629 print_stdout: Whether to forward Subversion's output to stdout.
630 filter: A function taking one argument (a string) which will be
631 passed each line (with the ending newline character removed) of
632 Subversion's output for filtering.
633
634 Raises:
635 Error: An error occurred while running the svn command.
636 """
637 command = [SVN_COMMAND]
638 command.extend(args)
639
640 gclient_utils.SubprocessCallAndFilter(command,
641 in_directory,
642 print_messages,
643 print_stdout,
644 filter=filter)
645
646def CaptureSVNInfo(relpath, in_directory=None, print_error=True):
647 """Returns a dictionary from the svn info output for the given file.
648
649 Args:
650 relpath: The directory where the working copy resides relative to
651 the directory given by in_directory.
652 in_directory: The directory where svn is to be run.
653 """
654 output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error)
655 dom = gclient_utils.ParseXML(output)
656 result = {}
657 if dom:
658 GetNamedNodeText = gclient_utils.GetNamedNodeText
659 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
660 def C(item, f):
661 if item is not None: return f(item)
662 # /info/entry/
663 # url
664 # reposityory/(root|uuid)
665 # wc-info/(schedule|depth)
666 # commit/(author|date)
667 # str() the results because they may be returned as Unicode, which
668 # interferes with the higher layers matching up things in the deps
669 # dictionary.
670 # TODO(maruel): Fix at higher level instead (!)
671 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
672 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
673 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
674 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry', 'revision'),
675 int)
676 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
677 str)
678 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
679 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
680 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
681 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
682 return result
683
684
685def CaptureSVNHeadRevision(url):
686 """Get the head revision of a SVN repository.
687
688 Returns:
689 Int head revision
690 """
691 info = CaptureSVN(["info", "--xml", url], os.getcwd())
692 dom = xml.dom.minidom.parseString(info)
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000693 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000694
695
696def CaptureSVNStatus(files):
697 """Returns the svn 1.5 svn status emulated output.
698
699 @files can be a string (one file) or a list of files.
700
701 Returns an array of (status, file) tuples."""
702 command = ["status", "--xml"]
703 if not files:
704 pass
705 elif isinstance(files, basestring):
706 command.append(files)
707 else:
708 command.extend(files)
709
710 status_letter = {
711 None: ' ',
712 '': ' ',
713 'added': 'A',
714 'conflicted': 'C',
715 'deleted': 'D',
716 'external': 'X',
717 'ignored': 'I',
718 'incomplete': '!',
719 'merged': 'G',
720 'missing': '!',
721 'modified': 'M',
722 'none': ' ',
723 'normal': ' ',
724 'obstructed': '~',
725 'replaced': 'R',
726 'unversioned': '?',
727 }
728 dom = gclient_utils.ParseXML(CaptureSVN(command))
729 results = []
730 if dom:
731 # /status/target/entry/(wc-status|commit|author|date)
732 for target in dom.getElementsByTagName('target'):
733 base_path = target.getAttribute('path')
734 for entry in target.getElementsByTagName('entry'):
735 file = entry.getAttribute('path')
736 wc_status = entry.getElementsByTagName('wc-status')
737 assert len(wc_status) == 1
738 # Emulate svn 1.5 status ouput...
739 statuses = [' ' for i in range(7)]
740 # Col 0
741 xml_item_status = wc_status[0].getAttribute('item')
742 if xml_item_status in status_letter:
743 statuses[0] = status_letter[xml_item_status]
744 else:
745 raise Exception('Unknown item status "%s"; please implement me!' %
746 xml_item_status)
747 # Col 1
748 xml_props_status = wc_status[0].getAttribute('props')
749 if xml_props_status == 'modified':
750 statuses[1] = 'M'
751 elif xml_props_status == 'conflicted':
752 statuses[1] = 'C'
753 elif (not xml_props_status or xml_props_status == 'none' or
754 xml_props_status == 'normal'):
755 pass
756 else:
757 raise Exception('Unknown props status "%s"; please implement me!' %
758 xml_props_status)
759 # Col 2
760 if wc_status[0].getAttribute('wc-locked') == 'true':
761 statuses[2] = 'L'
762 # Col 3
763 if wc_status[0].getAttribute('copied') == 'true':
764 statuses[3] = '+'
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000765 # Col 4
766 if wc_status[0].getAttribute('switched') == 'true':
767 statuses[4] = 'S'
768 # TODO(maruel): Col 5 and 6
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000769 item = (''.join(statuses), file)
770 results.append(item)
771 return results