blob: e0c49764414fc98249617f54e639362ab77892e7 [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
maruel@chromium.org0563d6c2009-11-12 19:21:30 +000024
25SVN_COMMAND = "svn"
26GIT_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."""
maruel@chromium.orge3608df2009-11-10 20:22:57 +000099 __pychecker__ = 'unusednames=args,file_list,options'
msb@chromium.orge8e60e52009-11-02 21:50:56 +0000100 self._RunGit(['prune'], redirect_stdout=False)
101 self._RunGit(['fsck'], redirect_stdout=False)
102 self._RunGit(['gc'], redirect_stdout=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000103
104 def diff(self, options, args, file_list):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000105 __pychecker__ = 'unusednames=args,file_list,options'
msb@chromium.orge28e4982009-09-25 20:51:45 +0000106 merge_base = self._RunGit(['merge-base', 'HEAD', 'origin'])
msb@chromium.orge8e60e52009-11-02 21:50:56 +0000107 self._RunGit(['diff', merge_base], redirect_stdout=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000108
109 def export(self, options, args, file_list):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000110 __pychecker__ = 'unusednames=file_list,options'
msb@chromium.orge28e4982009-09-25 20:51:45 +0000111 assert len(args) == 1
112 export_path = os.path.abspath(os.path.join(args[0], self.relpath))
113 if not os.path.exists(export_path):
114 os.makedirs(export_path)
msb@chromium.orge8e60e52009-11-02 21:50:56 +0000115 self._RunGit(['checkout-index', '-a', '--prefix=%s/' % export_path],
116 redirect_stdout=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000117
118 def update(self, options, args, file_list):
119 """Runs git to update or transparently checkout the working copy.
120
121 All updated files will be appended to file_list.
122
123 Raises:
124 Error: if can't get URL for relative path.
125 """
126
127 if args:
128 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
129
msb@chromium.org7780c282009-11-09 17:54:17 +0000130 if self.url.startswith('ssh:'):
131 # Make sure ssh://test@example.com/test.git@stable works
132 regex = r"(ssh://(?:[\w]+@)?[-\w:\.]+/[-\w\.]+)(?:@([\w/]+))?"
133 components = re.search(regex, self.url).groups()
134 else:
135 components = self.url.split("@")
msb@chromium.orge28e4982009-09-25 20:51:45 +0000136 url = components[0]
137 revision = None
138 if options.revision:
139 revision = options.revision
140 elif len(components) == 2:
141 revision = components[1]
142
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000143 if options.verbose:
144 rev_str = ""
145 if revision:
146 rev_str = ' at %s' % revision
147 print("\n_____ %s%s" % (self.relpath, rev_str))
148
msb@chromium.orge28e4982009-09-25 20:51:45 +0000149 if not os.path.exists(self.checkout_path):
msb@chromium.orge8e60e52009-11-02 21:50:56 +0000150 self._RunGit(['clone', url, self.checkout_path],
151 cwd=self._root_dir, redirect_stdout=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000152 if revision:
msb@chromium.orge8e60e52009-11-02 21:50:56 +0000153 self._RunGit(['reset', '--hard', revision], redirect_stdout=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000154 files = self._RunGit(['ls-files']).split()
155 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
156 return
157
msb@chromium.orge8e60e52009-11-02 21:50:56 +0000158 self._RunGit(['remote', 'update'], redirect_stdout=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000159 new_base = 'origin'
160 if revision:
161 new_base = revision
162 files = self._RunGit(['diff', new_base, '--name-only']).split()
163 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
msb@chromium.orgb1a22bf2009-11-07 02:33:50 +0000164 self._RunGit(['rebase', '-v', new_base], redirect_stdout=False)
165 print "Checked out revision %s." % self.revinfo(options, (), None)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000166
167 def revert(self, options, args, file_list):
168 """Reverts local modifications.
169
170 All reverted files will be appended to file_list.
171 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000172 __pychecker__ = 'unusednames=args'
msb@chromium.org260c6532009-10-28 03:22:35 +0000173 path = os.path.join(self._root_dir, self.relpath)
174 if not os.path.isdir(path):
175 # revert won't work if the directory doesn't exist. It needs to
176 # checkout instead.
177 print("\n_____ %s is missing, synching instead" % self.relpath)
178 # Don't reuse the args.
179 return self.update(options, [], file_list)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000180 merge_base = self._RunGit(['merge-base', 'HEAD', 'origin'])
181 files = self._RunGit(['diff', merge_base, '--name-only']).split()
msb@chromium.orge8e60e52009-11-02 21:50:56 +0000182 self._RunGit(['reset', '--hard', merge_base], redirect_stdout=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000183 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
184
msb@chromium.org0f282062009-11-06 20:14:02 +0000185 def revinfo(self, options, args, file_list):
186 """Display revision"""
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000187 __pychecker__ = 'unusednames=args,file_list,options'
msb@chromium.org0f282062009-11-06 20:14:02 +0000188 return self._RunGit(['rev-parse', 'HEAD'])
189
msb@chromium.orge28e4982009-09-25 20:51:45 +0000190 def runhooks(self, options, args, file_list):
191 self.status(options, args, file_list)
192
193 def status(self, options, args, file_list):
194 """Display status information."""
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000195 __pychecker__ = 'unusednames=args,options'
msb@chromium.orge28e4982009-09-25 20:51:45 +0000196 if not os.path.isdir(self.checkout_path):
197 print('\n________ couldn\'t run status in %s:\nThe directory '
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000198 'does not exist.' % self.checkout_path)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000199 else:
200 merge_base = self._RunGit(['merge-base', 'HEAD', 'origin'])
msb@chromium.orge8e60e52009-11-02 21:50:56 +0000201 self._RunGit(['diff', '--name-status', merge_base], redirect_stdout=False)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000202 files = self._RunGit(['diff', '--name-only', merge_base]).split()
203 file_list.extend([os.path.join(self.checkout_path, f) for f in files])
204
msb@chromium.orge8e60e52009-11-02 21:50:56 +0000205 def _RunGit(self, args, cwd=None, checkrc=True, redirect_stdout=True):
206 stdout=None
207 if redirect_stdout:
208 stdout=subprocess.PIPE
msb@chromium.orge28e4982009-09-25 20:51:45 +0000209 if cwd == None:
210 cwd = self.checkout_path
211 cmd = ['git']
212 cmd.extend(args)
msb@chromium.orge8e60e52009-11-02 21:50:56 +0000213 sp = subprocess.Popen(cmd, cwd=cwd, stdout=stdout)
msb@chromium.orge28e4982009-09-25 20:51:45 +0000214 if checkrc and sp.returncode:
215 raise gclient_utils.Error('git command %s returned %d' %
216 (args[0], sp.returncode))
msb@chromium.orge8e60e52009-11-02 21:50:56 +0000217 output = sp.communicate()[0]
218 if output != None:
219 return output.strip()
msb@chromium.orge28e4982009-09-25 20:51:45 +0000220
221
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000222class SVNWrapper(SCMWrapper):
223 """ Wrapper for SVN """
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000224
225 def cleanup(self, options, args, file_list):
226 """Cleanup working copy."""
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000227 __pychecker__ = 'unusednames=file_list,options'
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000228 command = ['cleanup']
229 command.extend(args)
230 RunSVN(command, os.path.join(self._root_dir, self.relpath))
231
232 def diff(self, options, args, file_list):
233 # NOTE: This function does not currently modify file_list.
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000234 __pychecker__ = 'unusednames=file_list,options'
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000235 command = ['diff']
236 command.extend(args)
237 RunSVN(command, os.path.join(self._root_dir, self.relpath))
238
239 def export(self, options, args, file_list):
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000240 __pychecker__ = 'unusednames=file_list,options'
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000241 assert len(args) == 1
242 export_path = os.path.abspath(os.path.join(args[0], self.relpath))
243 try:
244 os.makedirs(export_path)
245 except OSError:
246 pass
247 assert os.path.exists(export_path)
248 command = ['export', '--force', '.']
249 command.append(export_path)
250 RunSVN(command, os.path.join(self._root_dir, self.relpath))
251
252 def update(self, options, args, file_list):
253 """Runs SCM to update or transparently checkout the working copy.
254
255 All updated files will be appended to file_list.
256
257 Raises:
258 Error: if can't get URL for relative path.
259 """
260 # Only update if git is not controlling the directory.
261 checkout_path = os.path.join(self._root_dir, self.relpath)
262 git_path = os.path.join(self._root_dir, self.relpath, '.git')
263 if os.path.exists(git_path):
264 print("________ found .git directory; skipping %s" % self.relpath)
265 return
266
267 if args:
268 raise gclient_utils.Error("Unsupported argument(s): %s" % ",".join(args))
269
270 url = self.url
271 components = url.split("@")
272 revision = None
273 forced_revision = False
274 if options.revision:
275 # Override the revision number.
276 url = '%s@%s' % (components[0], str(options.revision))
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000277 revision = options.revision
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000278 forced_revision = True
279 elif len(components) == 2:
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000280 revision = components[1]
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000281 forced_revision = True
282
283 rev_str = ""
284 if revision:
msb@chromium.org770ff9e2009-09-23 17:18:18 +0000285 rev_str = ' at %s' % revision
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000286
287 if not os.path.exists(checkout_path):
288 # We need to checkout.
289 command = ['checkout', url, checkout_path]
290 if revision:
291 command.extend(['--revision', str(revision)])
dpranke@google.com22e29d42009-10-28 00:48:26 +0000292 RunSVNAndGetFileList(options, command, self._root_dir, file_list)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000293 return
294
295 # Get the existing scm url and the revision number of the current checkout.
296 from_info = CaptureSVNInfo(os.path.join(checkout_path, '.'), '.')
297 if not from_info:
298 raise gclient_utils.Error("Can't update/checkout %r if an unversioned "
299 "directory is present. Delete the directory "
300 "and try again." %
301 checkout_path)
302
maruel@chromium.org7753d242009-10-07 17:40:24 +0000303 if options.manually_grab_svn_rev:
304 # Retrieve the current HEAD version because svn is slow at null updates.
305 if not revision:
306 from_info_live = CaptureSVNInfo(from_info['URL'], '.')
307 revision = str(from_info_live['Revision'])
308 rev_str = ' at %s' % revision
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000309
310 if from_info['URL'] != components[0]:
311 to_info = CaptureSVNInfo(url, '.')
maruel@chromium.orge2ce0c72009-09-23 16:14:18 +0000312 if not to_info.get('Repository Root') or not to_info.get('UUID'):
313 # The url is invalid or the server is not accessible, it's safer to bail
314 # out right now.
315 raise gclient_utils.Error('This url is unreachable: %s' % url)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000316 can_switch = ((from_info['Repository Root'] != to_info['Repository Root'])
317 and (from_info['UUID'] == to_info['UUID']))
318 if can_switch:
319 print("\n_____ relocating %s to a new checkout" % self.relpath)
320 # We have different roots, so check if we can switch --relocate.
321 # Subversion only permits this if the repository UUIDs match.
322 # Perform the switch --relocate, then rewrite the from_url
323 # to reflect where we "are now." (This is the same way that
324 # Subversion itself handles the metadata when switch --relocate
325 # is used.) This makes the checks below for whether we
326 # can update to a revision or have to switch to a different
327 # branch work as expected.
328 # TODO(maruel): TEST ME !
329 command = ["switch", "--relocate",
330 from_info['Repository Root'],
331 to_info['Repository Root'],
332 self.relpath]
333 RunSVN(command, self._root_dir)
334 from_info['URL'] = from_info['URL'].replace(
335 from_info['Repository Root'],
336 to_info['Repository Root'])
337 else:
338 if CaptureSVNStatus(checkout_path):
339 raise gclient_utils.Error("Can't switch the checkout to %s; UUID "
340 "don't match and there is local changes "
341 "in %s. Delete the directory and "
342 "try again." % (url, checkout_path))
343 # Ok delete it.
344 print("\n_____ switching %s to a new checkout" % self.relpath)
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +0000345 gclient_utils.RemoveDirectory(checkout_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000346 # We need to checkout.
347 command = ['checkout', url, checkout_path]
348 if revision:
349 command.extend(['--revision', str(revision)])
dpranke@google.com22e29d42009-10-28 00:48:26 +0000350 RunSVNAndGetFileList(options, command, self._root_dir, file_list)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000351 return
352
353
354 # If the provided url has a revision number that matches the revision
355 # number of the existing directory, then we don't need to bother updating.
maruel@chromium.org2e0c6852009-09-24 00:02:07 +0000356 if not options.force and str(from_info['Revision']) == revision:
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000357 if options.verbose or not forced_revision:
358 print("\n_____ %s%s" % (self.relpath, rev_str))
359 return
360
361 command = ["update", checkout_path]
362 if revision:
363 command.extend(['--revision', str(revision)])
dpranke@google.com22e29d42009-10-28 00:48:26 +0000364 RunSVNAndGetFileList(options, command, self._root_dir, file_list)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000365
366 def revert(self, options, args, file_list):
367 """Reverts local modifications. Subversion specific.
368
369 All reverted files will be appended to file_list, even if Subversion
370 doesn't know about them.
371 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000372 __pychecker__ = 'unusednames=args'
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000373 path = os.path.join(self._root_dir, self.relpath)
374 if not os.path.isdir(path):
375 # svn revert won't work if the directory doesn't exist. It needs to
376 # checkout instead.
377 print("\n_____ %s is missing, synching instead" % self.relpath)
378 # Don't reuse the args.
379 return self.update(options, [], file_list)
380
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000381 for file_status in CaptureSVNStatus(path):
382 file_path = os.path.join(path, file_status[1])
383 if file_status[0][0] == 'X':
maruel@chromium.org754960e2009-09-21 12:31:05 +0000384 # Ignore externals.
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000385 logging.info('Ignoring external %s' % file_path)
maruel@chromium.org754960e2009-09-21 12:31:05 +0000386 continue
387
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000388 if logging.getLogger().isEnabledFor(logging.INFO):
389 logging.info('%s%s' % (file[0], file[1]))
390 else:
391 print(file_path)
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000392 if file_status[0].isspace():
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000393 logging.error('No idea what is the status of %s.\n'
394 'You just found a bug in gclient, please ping '
395 'maruel@chromium.org ASAP!' % file_path)
396 # svn revert is really stupid. It fails on inconsistent line-endings,
397 # on switched directories, etc. So take no chance and delete everything!
398 try:
399 if not os.path.exists(file_path):
400 pass
401 elif os.path.isfile(file_path):
maruel@chromium.org754960e2009-09-21 12:31:05 +0000402 logging.info('os.remove(%s)' % file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000403 os.remove(file_path)
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000404 elif os.path.isdir(file_path):
maruel@chromium.org754960e2009-09-21 12:31:05 +0000405 logging.info('gclient_utils.RemoveDirectory(%s)' % file_path)
bradnelson@google.com8f9c69f2009-09-17 00:48:28 +0000406 gclient_utils.RemoveDirectory(file_path)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000407 else:
maruel@chromium.orgaa3dd472009-09-21 19:02:48 +0000408 logging.error('no idea what is %s.\nYou just found a bug in gclient'
409 ', please ping maruel@chromium.org ASAP!' % file_path)
410 except EnvironmentError:
411 logging.error('Failed to remove %s.' % file_path)
412
maruel@chromium.org810a50b2009-10-05 23:03:18 +0000413 try:
414 # svn revert is so broken we don't even use it. Using
415 # "svn up --revision BASE" achieve the same effect.
dpranke@google.com22e29d42009-10-28 00:48:26 +0000416 RunSVNAndGetFileList(options, ['update', '--revision', 'BASE'], path,
maruel@chromium.org810a50b2009-10-05 23:03:18 +0000417 file_list)
418 except OSError, e:
419 # Maybe the directory disapeared meanwhile. We don't want it to throw an
420 # exception.
421 logging.error('Failed to update:\n%s' % str(e))
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000422
msb@chromium.org0f282062009-11-06 20:14:02 +0000423 def revinfo(self, options, args, file_list):
424 """Display revision"""
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000425 __pychecker__ = 'unusednames=args,file_list,options'
msb@chromium.org0f282062009-11-06 20:14:02 +0000426 return CaptureSVNHeadRevision(self.url)
427
msb@chromium.orgcb5442b2009-09-22 16:51:24 +0000428 def runhooks(self, options, args, file_list):
429 self.status(options, args, file_list)
430
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000431 def status(self, options, args, file_list):
432 """Display status information."""
433 path = os.path.join(self._root_dir, self.relpath)
434 command = ['status']
435 command.extend(args)
436 if not os.path.isdir(path):
437 # svn status won't work if the directory doesn't exist.
438 print("\n________ couldn't run \'%s\' in \'%s\':\nThe directory "
439 "does not exist."
440 % (' '.join(command), path))
441 # There's no file list to retrieve.
442 else:
dpranke@google.com22e29d42009-10-28 00:48:26 +0000443 RunSVNAndGetFileList(options, command, path, file_list)
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000444
445 def pack(self, options, args, file_list):
446 """Generates a patch file which can be applied to the root of the
447 repository."""
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000448 __pychecker__ = 'unusednames=file_list,options'
maruel@chromium.org5f3eee32009-09-17 00:34:30 +0000449 path = os.path.join(self._root_dir, self.relpath)
450 command = ['diff']
451 command.extend(args)
452 # Simple class which tracks which file is being diffed and
453 # replaces instances of its file name in the original and
454 # working copy lines of the svn diff output.
455 class DiffFilterer(object):
456 index_string = "Index: "
457 original_prefix = "--- "
458 working_prefix = "+++ "
459
460 def __init__(self, relpath):
461 # Note that we always use '/' as the path separator to be
462 # consistent with svn's cygwin-style output on Windows
463 self._relpath = relpath.replace("\\", "/")
464 self._current_file = ""
465 self._replacement_file = ""
466
467 def SetCurrentFile(self, file):
468 self._current_file = file
469 # Note that we always use '/' as the path separator to be
470 # consistent with svn's cygwin-style output on Windows
471 self._replacement_file = self._relpath + '/' + file
472
473 def ReplaceAndPrint(self, line):
474 print(line.replace(self._current_file, self._replacement_file))
475
476 def Filter(self, line):
477 if (line.startswith(self.index_string)):
478 self.SetCurrentFile(line[len(self.index_string):])
479 self.ReplaceAndPrint(line)
480 else:
481 if (line.startswith(self.original_prefix) or
482 line.startswith(self.working_prefix)):
483 self.ReplaceAndPrint(line)
484 else:
485 print line
486
487 filterer = DiffFilterer(self.relpath)
488 RunSVNAndFilterOutput(command, path, False, False, filterer.Filter)
maruel@chromium.org0563d6c2009-11-12 19:21:30 +0000489
490
491# -----------------------------------------------------------------------------
492# Git utils:
493
494
495def CaptureGit(args, in_directory=None, print_error=True):
496 """Runs git, capturing output sent to stdout as a string.
497
498 Args:
499 args: A sequence of command line parameters to be passed to git.
500 in_directory: The directory where git is to be run.
501
502 Returns:
503 The output sent to stdout as a string.
504 """
505 c = [GIT_COMMAND]
506 c.extend(args)
507
508 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
509 # the git.exe executable, but shell=True makes subprocess on Linux fail
510 # when it's called with a list because it only tries to execute the
511 # first string ("git").
512 stderr = None
513 if not print_error:
514 stderr = subprocess.PIPE
515 return subprocess.Popen(c,
516 cwd=in_directory,
517 shell=sys.platform.startswith('win'),
518 stdout=subprocess.PIPE,
519 stderr=stderr).communicate()[0]
520
521
522def CaptureGitStatus(files, upstream_branch='origin'):
523 """Returns git status.
524
525 @files can be a string (one file) or a list of files.
526
527 Returns an array of (status, file) tuples."""
528 command = ["diff", "--name-status", "-r", "%s.." % upstream_branch]
529 if not files:
530 pass
531 elif isinstance(files, basestring):
532 command.append(files)
533 else:
534 command.extend(files)
535
536 status = CaptureGit(command).rstrip()
537 results = []
538 if status:
539 for statusline in status.split('\n'):
540 m = re.match('^(\w)\t(.+)$', statusline)
541 if not m:
542 raise Exception("status currently unsupported: %s" % statusline)
543 results.append(('%s ' % m.group(1), m.group(2)))
544 return results
545
546
547# -----------------------------------------------------------------------------
548# SVN utils:
549
550
551def RunSVN(args, in_directory):
552 """Runs svn, sending output to stdout.
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 Raises:
559 Error: An error occurred while running the svn command.
560 """
561 c = [SVN_COMMAND]
562 c.extend(args)
563
564 gclient_utils.SubprocessCall(c, in_directory)
565
566
567def CaptureSVN(args, in_directory=None, print_error=True):
568 """Runs svn, capturing output sent to stdout as a string.
569
570 Args:
571 args: A sequence of command line parameters to be passed to svn.
572 in_directory: The directory where svn is to be run.
573
574 Returns:
575 The output sent to stdout as a string.
576 """
577 c = [SVN_COMMAND]
578 c.extend(args)
579
580 # *Sigh*: Windows needs shell=True, or else it won't search %PATH% for
581 # the svn.exe executable, but shell=True makes subprocess on Linux fail
582 # when it's called with a list because it only tries to execute the
583 # first string ("svn").
584 stderr = None
585 if not print_error:
586 stderr = subprocess.PIPE
587 return subprocess.Popen(c,
588 cwd=in_directory,
589 shell=(sys.platform == 'win32'),
590 stdout=subprocess.PIPE,
591 stderr=stderr).communicate()[0]
592
593
594def RunSVNAndGetFileList(options, args, in_directory, file_list):
595 """Runs svn checkout, update, or status, output to stdout.
596
597 The first item in args must be either "checkout", "update", or "status".
598
599 svn's stdout is parsed to collect a list of files checked out or updated.
600 These files are appended to file_list. svn's stdout is also printed to
601 sys.stdout as in RunSVN.
602
603 Args:
604 options: command line options to gclient
605 args: A sequence of command line parameters to be passed to svn.
606 in_directory: The directory where svn is to be run.
607
608 Raises:
609 Error: An error occurred while running the svn command.
610 """
611 command = [SVN_COMMAND]
612 command.extend(args)
613
614 # svn update and svn checkout use the same pattern: the first three columns
615 # are for file status, property status, and lock status. This is followed
616 # by two spaces, and then the path to the file.
617 update_pattern = '^... (.*)$'
618
619 # The first three columns of svn status are the same as for svn update and
620 # svn checkout. The next three columns indicate addition-with-history,
621 # switch, and remote lock status. This is followed by one space, and then
622 # the path to the file.
623 status_pattern = '^...... (.*)$'
624
625 # args[0] must be a supported command. This will blow up if it's something
626 # else, which is good. Note that the patterns are only effective when
627 # these commands are used in their ordinary forms, the patterns are invalid
628 # for "svn status --show-updates", for example.
629 pattern = {
630 'checkout': update_pattern,
631 'status': status_pattern,
632 'update': update_pattern,
633 }[args[0]]
634
635 compiled_pattern = re.compile(pattern)
636
637 def CaptureMatchingLines(line):
638 match = compiled_pattern.search(line)
639 if match:
640 file_list.append(match.group(1))
641
642 RunSVNAndFilterOutput(args,
643 in_directory,
644 options.verbose,
645 True,
646 CaptureMatchingLines)
647
648def RunSVNAndFilterOutput(args,
649 in_directory,
650 print_messages,
651 print_stdout,
652 filter):
653 """Runs svn checkout, update, status, or diff, optionally outputting
654 to stdout.
655
656 The first item in args must be either "checkout", "update",
657 "status", or "diff".
658
659 svn's stdout is passed line-by-line to the given filter function. If
660 print_stdout is true, it is also printed to sys.stdout as in RunSVN.
661
662 Args:
663 args: A sequence of command line parameters to be passed to svn.
664 in_directory: The directory where svn is to be run.
665 print_messages: Whether to print status messages to stdout about
666 which Subversion commands are being run.
667 print_stdout: Whether to forward Subversion's output to stdout.
668 filter: A function taking one argument (a string) which will be
669 passed each line (with the ending newline character removed) of
670 Subversion's output for filtering.
671
672 Raises:
673 Error: An error occurred while running the svn command.
674 """
675 command = [SVN_COMMAND]
676 command.extend(args)
677
678 gclient_utils.SubprocessCallAndFilter(command,
679 in_directory,
680 print_messages,
681 print_stdout,
682 filter=filter)
683
684def CaptureSVNInfo(relpath, in_directory=None, print_error=True):
685 """Returns a dictionary from the svn info output for the given file.
686
687 Args:
688 relpath: The directory where the working copy resides relative to
689 the directory given by in_directory.
690 in_directory: The directory where svn is to be run.
691 """
692 output = CaptureSVN(["info", "--xml", relpath], in_directory, print_error)
693 dom = gclient_utils.ParseXML(output)
694 result = {}
695 if dom:
696 GetNamedNodeText = gclient_utils.GetNamedNodeText
697 GetNodeNamedAttributeText = gclient_utils.GetNodeNamedAttributeText
698 def C(item, f):
699 if item is not None: return f(item)
700 # /info/entry/
701 # url
702 # reposityory/(root|uuid)
703 # wc-info/(schedule|depth)
704 # commit/(author|date)
705 # str() the results because they may be returned as Unicode, which
706 # interferes with the higher layers matching up things in the deps
707 # dictionary.
708 # TODO(maruel): Fix at higher level instead (!)
709 result['Repository Root'] = C(GetNamedNodeText(dom, 'root'), str)
710 result['URL'] = C(GetNamedNodeText(dom, 'url'), str)
711 result['UUID'] = C(GetNamedNodeText(dom, 'uuid'), str)
712 result['Revision'] = C(GetNodeNamedAttributeText(dom, 'entry', 'revision'),
713 int)
714 result['Node Kind'] = C(GetNodeNamedAttributeText(dom, 'entry', 'kind'),
715 str)
716 result['Schedule'] = C(GetNamedNodeText(dom, 'schedule'), str)
717 result['Path'] = C(GetNodeNamedAttributeText(dom, 'entry', 'path'), str)
718 result['Copied From URL'] = C(GetNamedNodeText(dom, 'copy-from-url'), str)
719 result['Copied From Rev'] = C(GetNamedNodeText(dom, 'copy-from-rev'), str)
720 return result
721
722
723def CaptureSVNHeadRevision(url):
724 """Get the head revision of a SVN repository.
725
726 Returns:
727 Int head revision
728 """
729 info = CaptureSVN(["info", "--xml", url], os.getcwd())
730 dom = xml.dom.minidom.parseString(info)
731 return dom.getElementsByTagName('entry')[0].getAttribute('revision')
732
733
734def CaptureSVNStatus(files):
735 """Returns the svn 1.5 svn status emulated output.
736
737 @files can be a string (one file) or a list of files.
738
739 Returns an array of (status, file) tuples."""
740 command = ["status", "--xml"]
741 if not files:
742 pass
743 elif isinstance(files, basestring):
744 command.append(files)
745 else:
746 command.extend(files)
747
748 status_letter = {
749 None: ' ',
750 '': ' ',
751 'added': 'A',
752 'conflicted': 'C',
753 'deleted': 'D',
754 'external': 'X',
755 'ignored': 'I',
756 'incomplete': '!',
757 'merged': 'G',
758 'missing': '!',
759 'modified': 'M',
760 'none': ' ',
761 'normal': ' ',
762 'obstructed': '~',
763 'replaced': 'R',
764 'unversioned': '?',
765 }
766 dom = gclient_utils.ParseXML(CaptureSVN(command))
767 results = []
768 if dom:
769 # /status/target/entry/(wc-status|commit|author|date)
770 for target in dom.getElementsByTagName('target'):
771 for entry in target.getElementsByTagName('entry'):
772 file_path = entry.getAttribute('path')
773 wc_status = entry.getElementsByTagName('wc-status')
774 assert len(wc_status) == 1
775 # Emulate svn 1.5 status ouput...
776 statuses = [' '] * 7
777 # Col 0
778 xml_item_status = wc_status[0].getAttribute('item')
779 if xml_item_status in status_letter:
780 statuses[0] = status_letter[xml_item_status]
781 else:
782 raise Exception('Unknown item status "%s"; please implement me!' %
783 xml_item_status)
784 # Col 1
785 xml_props_status = wc_status[0].getAttribute('props')
786 if xml_props_status == 'modified':
787 statuses[1] = 'M'
788 elif xml_props_status == 'conflicted':
789 statuses[1] = 'C'
790 elif (not xml_props_status or xml_props_status == 'none' or
791 xml_props_status == 'normal'):
792 pass
793 else:
794 raise Exception('Unknown props status "%s"; please implement me!' %
795 xml_props_status)
796 # Col 2
797 if wc_status[0].getAttribute('wc-locked') == 'true':
798 statuses[2] = 'L'
799 # Col 3
800 if wc_status[0].getAttribute('copied') == 'true':
801 statuses[3] = '+'
802 # Col 4
803 if wc_status[0].getAttribute('switched') == 'true':
804 statuses[4] = 'S'
805 # TODO(maruel): Col 5 and 6
806 item = (''.join(statuses), file_path)
807 results.append(item)
808 return results