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