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