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