blob: e2e7e3ab9c97659b58814a35e3f363ae8fdd6281 [file] [log] [blame]
[email protected]dfaecd22011-04-21 00:33:311# coding=utf8
[email protected]9799a072012-01-11 00:26:252# Copyright (c) 2012 The Chromium Authors. All rights reserved.
[email protected]dfaecd22011-04-21 00:33:313# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Manages a project checkout.
6
7Includes support for svn, git-svn and git.
8"""
9
[email protected]dfaecd22011-04-21 00:33:3110import ConfigParser
11import fnmatch
12import logging
13import os
14import re
[email protected]5e975632011-09-29 18:07:0615import shutil
[email protected]dfaecd22011-04-21 00:33:3116import subprocess
17import sys
18import tempfile
19
20import patch
21import scm
22import subprocess2
23
24
25def get_code_review_setting(path, key,
26 codereview_settings_file='codereview.settings'):
27 """Parses codereview.settings and return the value for the key if present.
28
29 Don't cache the values in case the file is changed."""
30 # TODO(maruel): Do not duplicate code.
31 settings = {}
32 try:
33 settings_file = open(os.path.join(path, codereview_settings_file), 'r')
34 try:
35 for line in settings_file.readlines():
36 if not line or line.startswith('#'):
37 continue
38 if not ':' in line:
39 # Invalid file.
40 return None
41 k, v = line.split(':', 1)
42 settings[k.strip()] = v.strip()
43 finally:
44 settings_file.close()
[email protected]004fb712011-06-21 20:02:1645 except IOError:
[email protected]dfaecd22011-04-21 00:33:3146 return None
47 return settings.get(key, None)
48
49
[email protected]4dd9f722012-10-01 16:23:0350def align_stdout(stdout):
51 """Returns the aligned output of multiple stdouts."""
52 output = ''
53 for item in stdout:
54 item = item.strip()
55 if not item:
56 continue
57 output += ''.join(' %s\n' % line for line in item.splitlines())
58 return output
59
60
[email protected]dfaecd22011-04-21 00:33:3161class PatchApplicationFailed(Exception):
62 """Patch failed to be applied."""
[email protected]34f68552012-05-09 19:18:3663 def __init__(self, p, status):
64 super(PatchApplicationFailed, self).__init__(p, status)
65 self.patch = p
[email protected]dfaecd22011-04-21 00:33:3166 self.status = status
67
[email protected]34f68552012-05-09 19:18:3668 @property
69 def filename(self):
70 if self.patch:
71 return self.patch.filename
72
73 def __str__(self):
74 out = []
75 if self.filename:
76 out.append('Failed to apply patch for %s:' % self.filename)
77 if self.status:
78 out.append(self.status)
[email protected]cb5667a2012-10-23 19:42:1079 if self.patch:
80 out.append('Patch: %s' % self.patch.dump())
[email protected]34f68552012-05-09 19:18:3681 return '\n'.join(out)
82
[email protected]dfaecd22011-04-21 00:33:3183
84class CheckoutBase(object):
85 # Set to None to have verbose output.
86 VOID = subprocess2.VOID
87
[email protected]6ed8b502011-06-12 01:05:3588 def __init__(self, root_dir, project_name, post_processors):
89 """
90 Args:
91 post_processor: list of lambda(checkout, patches) to call on each of the
92 modified files.
93 """
[email protected]a5129fb2011-06-20 18:36:2594 super(CheckoutBase, self).__init__()
[email protected]dfaecd22011-04-21 00:33:3195 self.root_dir = root_dir
96 self.project_name = project_name
[email protected]3cdb7f32011-05-05 16:37:2497 if self.project_name is None:
98 self.project_path = self.root_dir
99 else:
100 self.project_path = os.path.join(self.root_dir, self.project_name)
[email protected]dfaecd22011-04-21 00:33:31101 # Only used for logging purposes.
102 self._last_seen_revision = None
[email protected]a5129fb2011-06-20 18:36:25103 self.post_processors = post_processors
[email protected]dfaecd22011-04-21 00:33:31104 assert self.root_dir
[email protected]dfaecd22011-04-21 00:33:31105 assert self.project_path
[email protected]0aca0f92012-10-01 16:39:45106 assert os.path.isabs(self.project_path)
[email protected]dfaecd22011-04-21 00:33:31107
108 def get_settings(self, key):
109 return get_code_review_setting(self.project_path, key)
110
[email protected]51919772011-06-12 01:27:42111 def prepare(self, revision):
[email protected]dfaecd22011-04-21 00:33:31112 """Checks out a clean copy of the tree and removes any local modification.
113
114 This function shouldn't throw unless the remote repository is inaccessible,
115 there is no free disk space or hard issues like that.
[email protected]51919772011-06-12 01:27:42116
117 Args:
118 revision: The revision it should sync to, SCM specific.
[email protected]dfaecd22011-04-21 00:33:31119 """
120 raise NotImplementedError()
121
[email protected]4dd9f722012-10-01 16:23:03122 def apply_patch(self, patches, post_processors=None, verbose=False):
[email protected]dfaecd22011-04-21 00:33:31123 """Applies a patch and returns the list of modified files.
124
125 This function should throw patch.UnsupportedPatchFormat or
126 PatchApplicationFailed when relevant.
[email protected]8a1396c2011-04-22 00:14:24127
128 Args:
129 patches: patch.PatchSet object.
[email protected]dfaecd22011-04-21 00:33:31130 """
131 raise NotImplementedError()
132
133 def commit(self, commit_message, user):
134 """Commits the patch upstream, while impersonating 'user'."""
135 raise NotImplementedError()
136
[email protected]bc32ad12012-07-26 13:22:47137 def revisions(self, rev1, rev2):
138 """Returns the count of revisions from rev1 to rev2, e.g. len(]rev1, rev2]).
139
140 If rev2 is None, it means 'HEAD'.
141
142 Returns None if there is no link between the two.
143 """
144 raise NotImplementedError()
145
[email protected]dfaecd22011-04-21 00:33:31146
147class RawCheckout(CheckoutBase):
148 """Used to apply a patch locally without any intent to commit it.
149
150 To be used by the try server.
151 """
[email protected]51919772011-06-12 01:27:42152 def prepare(self, revision):
[email protected]dfaecd22011-04-21 00:33:31153 """Stubbed out."""
154 pass
155
[email protected]4dd9f722012-10-01 16:23:03156 def apply_patch(self, patches, post_processors=None, verbose=False):
[email protected]8a1396c2011-04-22 00:14:24157 """Ignores svn properties."""
[email protected]b1d1a782011-09-29 14:13:55158 post_processors = post_processors or self.post_processors or []
[email protected]dfaecd22011-04-21 00:33:31159 for p in patches:
[email protected]4dd9f722012-10-01 16:23:03160 stdout = []
[email protected]dfaecd22011-04-21 00:33:31161 try:
[email protected]4dd9f722012-10-01 16:23:03162 filepath = os.path.join(self.project_path, p.filename)
[email protected]dfaecd22011-04-21 00:33:31163 if p.is_delete:
[email protected]4dd9f722012-10-01 16:23:03164 os.remove(filepath)
165 stdout.append('Deleted.')
[email protected]dfaecd22011-04-21 00:33:31166 else:
167 dirname = os.path.dirname(p.filename)
168 full_dir = os.path.join(self.project_path, dirname)
169 if dirname and not os.path.isdir(full_dir):
170 os.makedirs(full_dir)
[email protected]4dd9f722012-10-01 16:23:03171 stdout.append('Created missing directory %s.' % dirname)
[email protected]4869bcf2011-06-04 01:14:32172
[email protected]dfaecd22011-04-21 00:33:31173 if p.is_binary:
[email protected]4dd9f722012-10-01 16:23:03174 content = p.get()
[email protected]4869bcf2011-06-04 01:14:32175 with open(filepath, 'wb') as f:
[email protected]4dd9f722012-10-01 16:23:03176 f.write(content)
177 stdout.append('Added binary file %d bytes.' % len(content))
[email protected]dfaecd22011-04-21 00:33:31178 else:
[email protected]5e975632011-09-29 18:07:06179 if p.source_filename:
180 if not p.is_new:
181 raise PatchApplicationFailed(
[email protected]34f68552012-05-09 19:18:36182 p,
[email protected]5e975632011-09-29 18:07:06183 'File has a source filename specified but is not new')
184 # Copy the file first.
185 if os.path.isfile(filepath):
186 raise PatchApplicationFailed(
[email protected]34f68552012-05-09 19:18:36187 p, 'File exist but was about to be overwriten')
[email protected]5e975632011-09-29 18:07:06188 shutil.copy2(
189 os.path.join(self.project_path, p.source_filename), filepath)
[email protected]4dd9f722012-10-01 16:23:03190 stdout.append('Copied %s -> %s' % (p.source_filename, p.filename))
[email protected]58fe6622011-06-03 20:59:27191 if p.diff_hunks:
[email protected]4dd9f722012-10-01 16:23:03192 cmd = ['patch', '-u', '--binary', '-p%s' % p.patchlevel]
193 if verbose:
194 cmd.append('--verbose')
195 stdout.append(
196 subprocess2.check_output(
197 cmd,
198 stdin=p.get(False),
199 stderr=subprocess2.STDOUT,
[email protected]44b21b92012-11-08 19:37:08200 cwd=self.project_path))
[email protected]4869bcf2011-06-04 01:14:32201 elif p.is_new and not os.path.exists(filepath):
[email protected]58fe6622011-06-03 20:59:27202 # There is only a header. Just create the file.
[email protected]4869bcf2011-06-04 01:14:32203 open(filepath, 'w').close()
[email protected]4dd9f722012-10-01 16:23:03204 stdout.append('Created an empty file.')
[email protected]b1d1a782011-09-29 14:13:55205 for post in post_processors:
[email protected]8a1396c2011-04-22 00:14:24206 post(self, p)
[email protected]4dd9f722012-10-01 16:23:03207 if verbose:
208 print p.filename
209 print align_stdout(stdout)
[email protected]dfaecd22011-04-21 00:33:31210 except OSError, e:
[email protected]4dd9f722012-10-01 16:23:03211 raise PatchApplicationFailed(p, '%s%s' % (align_stdout(stdout), e))
[email protected]dfaecd22011-04-21 00:33:31212 except subprocess.CalledProcessError, e:
213 raise PatchApplicationFailed(
[email protected]4dd9f722012-10-01 16:23:03214 p,
215 'While running %s;\n%s%s' % (
216 ' '.join(e.cmd),
217 align_stdout(stdout),
218 align_stdout([getattr(e, 'stdout', '')])))
[email protected]dfaecd22011-04-21 00:33:31219
220 def commit(self, commit_message, user):
221 """Stubbed out."""
222 raise NotImplementedError('RawCheckout can\'t commit')
223
[email protected]bc32ad12012-07-26 13:22:47224 def revisions(self, _rev1, _rev2):
225 return None
226
[email protected]dfaecd22011-04-21 00:33:31227
228class SvnConfig(object):
229 """Parses a svn configuration file."""
230 def __init__(self, svn_config_dir=None):
[email protected]a5129fb2011-06-20 18:36:25231 super(SvnConfig, self).__init__()
[email protected]dfaecd22011-04-21 00:33:31232 self.svn_config_dir = svn_config_dir
233 self.default = not bool(self.svn_config_dir)
234 if not self.svn_config_dir:
235 if sys.platform == 'win32':
236 self.svn_config_dir = os.path.join(os.environ['APPDATA'], 'Subversion')
237 else:
238 self.svn_config_dir = os.path.join(os.environ['HOME'], '.subversion')
239 svn_config_file = os.path.join(self.svn_config_dir, 'config')
240 parser = ConfigParser.SafeConfigParser()
241 if os.path.isfile(svn_config_file):
242 parser.read(svn_config_file)
243 else:
244 parser.add_section('auto-props')
245 self.auto_props = dict(parser.items('auto-props'))
246
247
248class SvnMixIn(object):
249 """MixIn class to add svn commands common to both svn and git-svn clients."""
250 # These members need to be set by the subclass.
251 commit_user = None
252 commit_pwd = None
253 svn_url = None
254 project_path = None
255 # Override at class level when necessary. If used, --non-interactive is
256 # implied.
257 svn_config = SvnConfig()
258 # Set to True when non-interactivity is necessary but a custom subversion
259 # configuration directory is not necessary.
260 non_interactive = False
261
[email protected]9842a0c2011-05-30 20:41:54262 def _add_svn_flags(self, args, non_interactive, credentials=True):
[email protected]dfaecd22011-04-21 00:33:31263 args = ['svn'] + args
264 if not self.svn_config.default:
265 args.extend(['--config-dir', self.svn_config.svn_config_dir])
266 if not self.svn_config.default or self.non_interactive or non_interactive:
267 args.append('--non-interactive')
[email protected]9842a0c2011-05-30 20:41:54268 if credentials:
269 if self.commit_user:
270 args.extend(['--username', self.commit_user])
271 if self.commit_pwd:
272 args.extend(['--password', self.commit_pwd])
[email protected]dfaecd22011-04-21 00:33:31273 return args
274
275 def _check_call_svn(self, args, **kwargs):
276 """Runs svn and throws an exception if the command failed."""
277 kwargs.setdefault('cwd', self.project_path)
278 kwargs.setdefault('stdout', self.VOID)
[email protected]0bcd1d32011-04-26 15:55:49279 return subprocess2.check_call_out(
[email protected]44b21b92012-11-08 19:37:08280 self._add_svn_flags(args, False), **kwargs)
[email protected]dfaecd22011-04-21 00:33:31281
[email protected]9842a0c2011-05-30 20:41:54282 def _check_output_svn(self, args, credentials=True, **kwargs):
[email protected]dfaecd22011-04-21 00:33:31283 """Runs svn and throws an exception if the command failed.
284
285 Returns the output.
286 """
287 kwargs.setdefault('cwd', self.project_path)
[email protected]9842a0c2011-05-30 20:41:54288 return subprocess2.check_output(
[email protected]87e6d332011-09-09 19:01:28289 self._add_svn_flags(args, True, credentials),
290 stderr=subprocess2.STDOUT,
291 **kwargs)
[email protected]dfaecd22011-04-21 00:33:31292
293 @staticmethod
294 def _parse_svn_info(output, key):
295 """Returns value for key from svn info output.
296
297 Case insensitive.
298 """
299 values = {}
300 key = key.lower()
301 for line in output.splitlines(False):
302 if not line:
303 continue
304 k, v = line.split(':', 1)
305 k = k.strip().lower()
306 v = v.strip()
307 assert not k in values
308 values[k] = v
309 return values.get(key, None)
310
311
312class SvnCheckout(CheckoutBase, SvnMixIn):
313 """Manages a subversion checkout."""
[email protected]6ed8b502011-06-12 01:05:35314 def __init__(self, root_dir, project_name, commit_user, commit_pwd, svn_url,
315 post_processors=None):
[email protected]a5129fb2011-06-20 18:36:25316 CheckoutBase.__init__(self, root_dir, project_name, post_processors)
317 SvnMixIn.__init__(self)
[email protected]dfaecd22011-04-21 00:33:31318 self.commit_user = commit_user
319 self.commit_pwd = commit_pwd
320 self.svn_url = svn_url
321 assert bool(self.commit_user) >= bool(self.commit_pwd)
[email protected]dfaecd22011-04-21 00:33:31322
[email protected]51919772011-06-12 01:27:42323 def prepare(self, revision):
[email protected]dfaecd22011-04-21 00:33:31324 # Will checkout if the directory is not present.
[email protected]3cdb7f32011-05-05 16:37:24325 assert self.svn_url
[email protected]dfaecd22011-04-21 00:33:31326 if not os.path.isdir(self.project_path):
327 logging.info('Checking out %s in %s' %
328 (self.project_name, self.project_path))
[email protected]51919772011-06-12 01:27:42329 return self._revert(revision)
[email protected]dfaecd22011-04-21 00:33:31330
[email protected]4dd9f722012-10-01 16:23:03331 def apply_patch(self, patches, post_processors=None, verbose=False):
[email protected]b1d1a782011-09-29 14:13:55332 post_processors = post_processors or self.post_processors or []
[email protected]dfaecd22011-04-21 00:33:31333 for p in patches:
[email protected]4dd9f722012-10-01 16:23:03334 stdout = []
[email protected]dfaecd22011-04-21 00:33:31335 try:
[email protected]4dd9f722012-10-01 16:23:03336 filepath = os.path.join(self.project_path, p.filename)
[email protected]9842a0c2011-05-30 20:41:54337 # It is important to use credentials=False otherwise credentials could
338 # leak in the error message. Credentials are not necessary here for the
339 # following commands anyway.
[email protected]dfaecd22011-04-21 00:33:31340 if p.is_delete:
[email protected]4dd9f722012-10-01 16:23:03341 stdout.append(self._check_output_svn(
342 ['delete', p.filename, '--force'], credentials=False))
343 stdout.append('Deleted.')
[email protected]dfaecd22011-04-21 00:33:31344 else:
[email protected]dfaecd22011-04-21 00:33:31345 # svn add while creating directories otherwise svn add on the
346 # contained files will silently fail.
347 # First, find the root directory that exists.
348 dirname = os.path.dirname(p.filename)
349 dirs_to_create = []
350 while (dirname and
351 not os.path.isdir(os.path.join(self.project_path, dirname))):
352 dirs_to_create.append(dirname)
353 dirname = os.path.dirname(dirname)
354 for dir_to_create in reversed(dirs_to_create):
355 os.mkdir(os.path.join(self.project_path, dir_to_create))
[email protected]4dd9f722012-10-01 16:23:03356 stdout.append(
357 self._check_output_svn(
358 ['add', dir_to_create, '--force'], credentials=False))
359 stdout.append('Created missing directory %s.' % dir_to_create)
[email protected]dfaecd22011-04-21 00:33:31360
361 if p.is_binary:
[email protected]4dd9f722012-10-01 16:23:03362 content = p.get()
[email protected]4869bcf2011-06-04 01:14:32363 with open(filepath, 'wb') as f:
[email protected]4dd9f722012-10-01 16:23:03364 f.write(content)
365 stdout.append('Added binary file %d bytes.' % len(content))
[email protected]dfaecd22011-04-21 00:33:31366 else:
[email protected]5e975632011-09-29 18:07:06367 if p.source_filename:
368 if not p.is_new:
369 raise PatchApplicationFailed(
[email protected]34f68552012-05-09 19:18:36370 p,
[email protected]5e975632011-09-29 18:07:06371 'File has a source filename specified but is not new')
372 # Copy the file first.
373 if os.path.isfile(filepath):
374 raise PatchApplicationFailed(
[email protected]34f68552012-05-09 19:18:36375 p, 'File exist but was about to be overwriten')
[email protected]4dd9f722012-10-01 16:23:03376 stdout.append(
377 self._check_output_svn(
378 ['copy', p.source_filename, p.filename]))
379 stdout.append('Copied %s -> %s' % (p.source_filename, p.filename))
[email protected]58fe6622011-06-03 20:59:27380 if p.diff_hunks:
[email protected]ec4a9182012-09-28 20:39:45381 cmd = [
382 'patch',
383 '-p%s' % p.patchlevel,
384 '--forward',
385 '--force',
386 '--no-backup-if-mismatch',
387 ]
[email protected]4dd9f722012-10-01 16:23:03388 stdout.append(
389 subprocess2.check_output(
[email protected]44b21b92012-11-08 19:37:08390 cmd, stdin=p.get(False), cwd=self.project_path))
[email protected]4869bcf2011-06-04 01:14:32391 elif p.is_new and not os.path.exists(filepath):
392 # There is only a header. Just create the file if it doesn't
393 # exist.
394 open(filepath, 'w').close()
[email protected]4dd9f722012-10-01 16:23:03395 stdout.append('Created an empty file.')
[email protected]3da83172012-05-07 16:17:20396 if p.is_new and not p.source_filename:
397 # Do not run it if p.source_filename is defined, since svn copy was
398 # using above.
[email protected]4dd9f722012-10-01 16:23:03399 stdout.append(
400 self._check_output_svn(
401 ['add', p.filename, '--force'], credentials=False))
[email protected]d7ca6162012-08-29 17:22:22402 for name, value in p.svn_properties:
403 if value is None:
[email protected]4dd9f722012-10-01 16:23:03404 stdout.append(
405 self._check_output_svn(
406 ['propdel', '--quiet', name, p.filename],
407 credentials=False))
408 stdout.append('Property %s deleted.' % name)
[email protected]d7ca6162012-08-29 17:22:22409 else:
[email protected]4dd9f722012-10-01 16:23:03410 stdout.append(
411 self._check_output_svn(
412 ['propset', name, value, p.filename], credentials=False))
413 stdout.append('Property %s=%s' % (name, value))
[email protected]9842a0c2011-05-30 20:41:54414 for prop, values in self.svn_config.auto_props.iteritems():
[email protected]dfaecd22011-04-21 00:33:31415 if fnmatch.fnmatch(p.filename, prop):
[email protected]9842a0c2011-05-30 20:41:54416 for value in values.split(';'):
417 if '=' not in value:
[email protected]e1a03762012-09-24 15:28:52418 params = [value, '.']
[email protected]9842a0c2011-05-30 20:41:54419 else:
420 params = value.split('=', 1)
[email protected]e1a03762012-09-24 15:28:52421 if params[1] == '*':
422 # Works around crbug.com/150960 on Windows.
423 params[1] = '.'
[email protected]4dd9f722012-10-01 16:23:03424 stdout.append(
425 self._check_output_svn(
426 ['propset'] + params + [p.filename], credentials=False))
427 stdout.append('Property (auto) %s' % '='.join(params))
[email protected]b1d1a782011-09-29 14:13:55428 for post in post_processors:
[email protected]8a1396c2011-04-22 00:14:24429 post(self, p)
[email protected]4dd9f722012-10-01 16:23:03430 if verbose:
431 print p.filename
432 print align_stdout(stdout)
[email protected]dfaecd22011-04-21 00:33:31433 except OSError, e:
[email protected]4dd9f722012-10-01 16:23:03434 raise PatchApplicationFailed(p, '%s%s' % (align_stdout(stdout), e))
[email protected]dfaecd22011-04-21 00:33:31435 except subprocess.CalledProcessError, e:
436 raise PatchApplicationFailed(
[email protected]34f68552012-05-09 19:18:36437 p,
[email protected]9842a0c2011-05-30 20:41:54438 'While running %s;\n%s%s' % (
[email protected]4dd9f722012-10-01 16:23:03439 ' '.join(e.cmd),
440 align_stdout(stdout),
441 align_stdout([getattr(e, 'stdout', '')])))
[email protected]dfaecd22011-04-21 00:33:31442
443 def commit(self, commit_message, user):
444 logging.info('Committing patch for %s' % user)
445 assert self.commit_user
[email protected]1bf50972011-05-05 19:57:21446 assert isinstance(commit_message, unicode)
[email protected]dfaecd22011-04-21 00:33:31447 handle, commit_filename = tempfile.mkstemp(text=True)
448 try:
[email protected]1bf50972011-05-05 19:57:21449 # Shouldn't assume default encoding is UTF-8. But really, if you are using
450 # anything else, you are living in another world.
451 os.write(handle, commit_message.encode('utf-8'))
[email protected]dfaecd22011-04-21 00:33:31452 os.close(handle)
453 # When committing, svn won't update the Revision metadata of the checkout,
454 # so if svn commit returns "Committed revision 3.", svn info will still
455 # return "Revision: 2". Since running svn update right after svn commit
456 # creates a race condition with other committers, this code _must_ parse
457 # the output of svn commit and use a regexp to grab the revision number.
458 # Note that "Committed revision N." is localized but subprocess2 forces
459 # LANGUAGE=en.
460 args = ['commit', '--file', commit_filename]
461 # realauthor is parsed by a server-side hook.
462 if user and user != self.commit_user:
463 args.extend(['--with-revprop', 'realauthor=%s' % user])
464 out = self._check_output_svn(args)
465 finally:
466 os.remove(commit_filename)
467 lines = filter(None, out.splitlines())
468 match = re.match(r'^Committed revision (\d+).$', lines[-1])
469 if not match:
470 raise PatchApplicationFailed(
471 None,
472 'Couldn\'t make sense out of svn commit message:\n' + out)
473 return int(match.group(1))
474
[email protected]51919772011-06-12 01:27:42475 def _revert(self, revision):
[email protected]dfaecd22011-04-21 00:33:31476 """Reverts local modifications or checks out if the directory is not
477 present. Use depot_tools's functionality to do this.
478 """
479 flags = ['--ignore-externals']
[email protected]51919772011-06-12 01:27:42480 if revision:
481 flags.extend(['--revision', str(revision)])
[email protected]6e904b42012-12-19 14:21:14482 if os.path.isdir(self.project_path):
483 # This may remove any part (or all) of the checkout.
484 scm.SVN.Revert(self.project_path, no_ignore=True)
485
486 if os.path.isdir(self.project_path):
487 # Revive files that were deleted in scm.SVN.Revert().
488 self._check_call_svn(['update', '--force'] + flags)
489 else:
[email protected]dfaecd22011-04-21 00:33:31490 logging.info(
491 'Directory %s is not present, checking it out.' % self.project_path)
492 self._check_call_svn(
[email protected]44b21b92012-11-08 19:37:08493 ['checkout', self.svn_url, self.project_path] + flags, cwd=None)
[email protected]51919772011-06-12 01:27:42494 return self._get_revision()
[email protected]dfaecd22011-04-21 00:33:31495
[email protected]51919772011-06-12 01:27:42496 def _get_revision(self):
[email protected]dfaecd22011-04-21 00:33:31497 out = self._check_output_svn(['info', '.'])
[email protected]51919772011-06-12 01:27:42498 revision = int(self._parse_svn_info(out, 'revision'))
499 if revision != self._last_seen_revision:
500 logging.info('Updated to revision %d' % revision)
501 self._last_seen_revision = revision
502 return revision
[email protected]dfaecd22011-04-21 00:33:31503
[email protected]bc32ad12012-07-26 13:22:47504 def revisions(self, rev1, rev2):
505 """Returns the number of actual commits, not just the difference between
506 numbers.
507 """
508 rev2 = rev2 or 'HEAD'
509 # Revision range is inclusive and ordering doesn't matter, they'll appear in
510 # the order specified.
511 try:
512 out = self._check_output_svn(
513 ['log', '-q', self.svn_url, '-r', '%s:%s' % (rev1, rev2)])
514 except subprocess.CalledProcessError:
515 return None
516 # Ignore the '----' lines.
517 return len([l for l in out.splitlines() if l.startswith('r')]) - 1
518
[email protected]dfaecd22011-04-21 00:33:31519
520class GitCheckoutBase(CheckoutBase):
521 """Base class for git checkout. Not to be used as-is."""
[email protected]6ed8b502011-06-12 01:05:35522 def __init__(self, root_dir, project_name, remote_branch,
523 post_processors=None):
524 super(GitCheckoutBase, self).__init__(
525 root_dir, project_name, post_processors)
[email protected]dfaecd22011-04-21 00:33:31526 # There is no reason to not hardcode it.
527 self.remote = 'origin'
528 self.remote_branch = remote_branch
529 self.working_branch = 'working_branch'
[email protected]dfaecd22011-04-21 00:33:31530
[email protected]51919772011-06-12 01:27:42531 def prepare(self, revision):
[email protected]dfaecd22011-04-21 00:33:31532 """Resets the git repository in a clean state.
533
534 Checks it out if not present and deletes the working branch.
535 """
[email protected]3cdb7f32011-05-05 16:37:24536 assert self.remote_branch
[email protected]dfaecd22011-04-21 00:33:31537 assert os.path.isdir(self.project_path)
538 self._check_call_git(['reset', '--hard', '--quiet'])
[email protected]51919772011-06-12 01:27:42539 if revision:
540 try:
541 revision = self._check_output_git(['rev-parse', revision])
542 except subprocess.CalledProcessError:
[email protected]44b21b92012-11-08 19:37:08543 self._check_call_git(
544 ['fetch', self.remote, self.remote_branch, '--quiet'])
[email protected]51919772011-06-12 01:27:42545 revision = self._check_output_git(['rev-parse', revision])
546 self._check_call_git(['checkout', '--force', '--quiet', revision])
547 else:
548 branches, active = self._branches()
549 if active != 'master':
550 self._check_call_git(['checkout', '--force', '--quiet', 'master'])
551 self._check_call_git(['pull', self.remote, self.remote_branch, '--quiet'])
552 if self.working_branch in branches:
553 self._call_git(['branch', '-D', self.working_branch])
[email protected]dfaecd22011-04-21 00:33:31554
[email protected]4dd9f722012-10-01 16:23:03555 def apply_patch(self, patches, post_processors=None, verbose=False):
[email protected]8a1396c2011-04-22 00:14:24556 """Applies a patch on 'working_branch' and switch to it.
557
558 Also commits the changes on the local branch.
559
560 Ignores svn properties and raise an exception on unexpected ones.
561 """
[email protected]b1d1a782011-09-29 14:13:55562 post_processors = post_processors or self.post_processors or []
[email protected]dfaecd22011-04-21 00:33:31563 # It this throws, the checkout is corrupted. Maybe worth deleting it and
564 # trying again?
[email protected]3cdb7f32011-05-05 16:37:24565 if self.remote_branch:
566 self._check_call_git(
567 ['checkout', '-b', self.working_branch,
568 '%s/%s' % (self.remote, self.remote_branch), '--quiet'])
[email protected]5e975632011-09-29 18:07:06569 for index, p in enumerate(patches):
[email protected]4dd9f722012-10-01 16:23:03570 stdout = []
[email protected]dfaecd22011-04-21 00:33:31571 try:
[email protected]4dd9f722012-10-01 16:23:03572 filepath = os.path.join(self.project_path, p.filename)
[email protected]dfaecd22011-04-21 00:33:31573 if p.is_delete:
[email protected]4dd9f722012-10-01 16:23:03574 if (not os.path.exists(filepath) and
[email protected]5e975632011-09-29 18:07:06575 any(p1.source_filename == p.filename for p1 in patches[0:index])):
[email protected]4dd9f722012-10-01 16:23:03576 # The file was already deleted if a prior patch with file rename
577 # was already processed because 'git apply' did it for us.
[email protected]5e975632011-09-29 18:07:06578 pass
579 else:
[email protected]4dd9f722012-10-01 16:23:03580 stdout.append(self._check_output_git(['rm', p.filename]))
581 stdout.append('Deleted.')
[email protected]dfaecd22011-04-21 00:33:31582 else:
583 dirname = os.path.dirname(p.filename)
584 full_dir = os.path.join(self.project_path, dirname)
585 if dirname and not os.path.isdir(full_dir):
586 os.makedirs(full_dir)
[email protected]4dd9f722012-10-01 16:23:03587 stdout.append('Created missing directory %s.' % dirname)
[email protected]dfaecd22011-04-21 00:33:31588 if p.is_binary:
[email protected]4dd9f722012-10-01 16:23:03589 content = p.get()
590 with open(filepath, 'wb') as f:
591 f.write(content)
592 stdout.append('Added binary file %d bytes' % len(content))
593 cmd = ['add', p.filename]
594 if verbose:
595 cmd.append('--verbose')
596 stdout.append(self._check_output_git(cmd))
[email protected]dfaecd22011-04-21 00:33:31597 else:
[email protected]58fe6622011-06-03 20:59:27598 # No need to do anything special with p.is_new or if not
599 # p.diff_hunks. git apply manages all that already.
[email protected]4dd9f722012-10-01 16:23:03600 cmd = ['apply', '--index', '-p%s' % p.patchlevel]
601 if verbose:
602 cmd.append('--verbose')
603 stdout.append(self._check_output_git(cmd, stdin=p.get(True)))
604 for name, value in p.svn_properties:
[email protected]dfaecd22011-04-21 00:33:31605 # Ignore some known auto-props flags through .subversion/config,
606 # bails out on the other ones.
607 # TODO(maruel): Read ~/.subversion/config and detect the rules that
608 # applies here to figure out if the property will be correctly
609 # handled.
[email protected]4dd9f722012-10-01 16:23:03610 stdout.append('Property %s=%s' % (name, value))
[email protected]d7ca6162012-08-29 17:22:22611 if not name in (
[email protected]9799a072012-01-11 00:26:25612 'svn:eol-style', 'svn:executable', 'svn:mime-type'):
[email protected]dfaecd22011-04-21 00:33:31613 raise patch.UnsupportedPatchFormat(
614 p.filename,
615 'Cannot apply svn property %s to file %s.' % (
[email protected]d7ca6162012-08-29 17:22:22616 name, p.filename))
[email protected]b1d1a782011-09-29 14:13:55617 for post in post_processors:
[email protected]8a1396c2011-04-22 00:14:24618 post(self, p)
[email protected]4dd9f722012-10-01 16:23:03619 if verbose:
620 print p.filename
621 print align_stdout(stdout)
[email protected]dfaecd22011-04-21 00:33:31622 except OSError, e:
[email protected]4dd9f722012-10-01 16:23:03623 raise PatchApplicationFailed(p, '%s%s' % (align_stdout(stdout), e))
[email protected]dfaecd22011-04-21 00:33:31624 except subprocess.CalledProcessError, e:
625 raise PatchApplicationFailed(
[email protected]4dd9f722012-10-01 16:23:03626 p,
627 'While running %s;\n%s%s' % (
628 ' '.join(e.cmd),
629 align_stdout(stdout),
630 align_stdout([getattr(e, 'stdout', '')])))
[email protected]dfaecd22011-04-21 00:33:31631 # Once all the patches are processed and added to the index, commit the
632 # index.
[email protected]4dd9f722012-10-01 16:23:03633 cmd = ['commit', '-m', 'Committed patch']
634 if verbose:
635 cmd.append('--verbose')
636 self._check_call_git(cmd)
[email protected]dfaecd22011-04-21 00:33:31637 # TODO(maruel): Weirdly enough they don't match, need to investigate.
638 #found_files = self._check_output_git(
639 # ['diff', 'master', '--name-only']).splitlines(False)
640 #assert sorted(patches.filenames) == sorted(found_files), (
641 # sorted(out), sorted(found_files))
642
643 def commit(self, commit_message, user):
644 """Updates the commit message.
645
646 Subclass needs to dcommit or push.
647 """
[email protected]1bf50972011-05-05 19:57:21648 assert isinstance(commit_message, unicode)
[email protected]dfaecd22011-04-21 00:33:31649 self._check_call_git(['commit', '--amend', '-m', commit_message])
650 return self._check_output_git(['rev-parse', 'HEAD']).strip()
651
652 def _check_call_git(self, args, **kwargs):
653 kwargs.setdefault('cwd', self.project_path)
654 kwargs.setdefault('stdout', self.VOID)
[email protected]44b21b92012-11-08 19:37:08655 return subprocess2.check_call_out(['git'] + args, **kwargs)
[email protected]dfaecd22011-04-21 00:33:31656
657 def _call_git(self, args, **kwargs):
658 """Like check_call but doesn't throw on failure."""
659 kwargs.setdefault('cwd', self.project_path)
660 kwargs.setdefault('stdout', self.VOID)
[email protected]44b21b92012-11-08 19:37:08661 return subprocess2.call(['git'] + args, **kwargs)
[email protected]dfaecd22011-04-21 00:33:31662
663 def _check_output_git(self, args, **kwargs):
664 kwargs.setdefault('cwd', self.project_path)
[email protected]87e6d332011-09-09 19:01:28665 return subprocess2.check_output(
[email protected]44b21b92012-11-08 19:37:08666 ['git'] + args, stderr=subprocess2.STDOUT, **kwargs)
[email protected]dfaecd22011-04-21 00:33:31667
668 def _branches(self):
669 """Returns the list of branches and the active one."""
670 out = self._check_output_git(['branch']).splitlines(False)
671 branches = [l[2:] for l in out]
672 active = None
673 for l in out:
674 if l.startswith('*'):
675 active = l[2:]
676 break
677 return branches, active
678
[email protected]bc32ad12012-07-26 13:22:47679 def revisions(self, rev1, rev2):
680 """Returns the number of actual commits between both hash."""
681 self._fetch_remote()
682
683 rev2 = rev2 or '%s/%s' % (self.remote, self.remote_branch)
684 # Revision range is ]rev1, rev2] and ordering matters.
685 try:
686 out = self._check_output_git(
687 ['log', '--format="%H"' , '%s..%s' % (rev1, rev2)])
688 except subprocess.CalledProcessError:
689 return None
690 return len(out.splitlines())
691
692 def _fetch_remote(self):
693 """Fetches the remote without rebasing."""
694 raise NotImplementedError()
695
696
697class GitCheckout(GitCheckoutBase):
698 """Git checkout implementation."""
699 def _fetch_remote(self):
700 # git fetch is always verbose even with -q -q so redirect its output.
[email protected]44b21b92012-11-08 19:37:08701 self._check_output_git(['fetch', self.remote, self.remote_branch])
[email protected]bc32ad12012-07-26 13:22:47702
[email protected]dfaecd22011-04-21 00:33:31703
[email protected]dfaecd22011-04-21 00:33:31704class ReadOnlyCheckout(object):
705 """Converts a checkout into a read-only one."""
[email protected]b1d1a782011-09-29 14:13:55706 def __init__(self, checkout, post_processors=None):
[email protected]a5129fb2011-06-20 18:36:25707 super(ReadOnlyCheckout, self).__init__()
[email protected]dfaecd22011-04-21 00:33:31708 self.checkout = checkout
[email protected]b1d1a782011-09-29 14:13:55709 self.post_processors = (post_processors or []) + (
710 self.checkout.post_processors or [])
[email protected]dfaecd22011-04-21 00:33:31711
[email protected]51919772011-06-12 01:27:42712 def prepare(self, revision):
713 return self.checkout.prepare(revision)
[email protected]dfaecd22011-04-21 00:33:31714
715 def get_settings(self, key):
716 return self.checkout.get_settings(key)
717
[email protected]4dd9f722012-10-01 16:23:03718 def apply_patch(self, patches, post_processors=None, verbose=False):
[email protected]b1d1a782011-09-29 14:13:55719 return self.checkout.apply_patch(
[email protected]4dd9f722012-10-01 16:23:03720 patches, post_processors or self.post_processors, verbose)
[email protected]dfaecd22011-04-21 00:33:31721
722 def commit(self, message, user): # pylint: disable=R0201
723 logging.info('Would have committed for %s with message: %s' % (
724 user, message))
725 return 'FAKE'
726
[email protected]bc32ad12012-07-26 13:22:47727 def revisions(self, rev1, rev2):
728 return self.checkout.revisions(rev1, rev2)
729
[email protected]dfaecd22011-04-21 00:33:31730 @property
731 def project_name(self):
732 return self.checkout.project_name
733
734 @property
735 def project_path(self):
736 return self.checkout.project_path