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