blob: 03e28f68caf5557edb7a74ce75297bf5eedb3010 [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
50class PatchApplicationFailed(Exception):
51 """Patch failed to be applied."""
[email protected]34f68552012-05-09 19:18:3652 def __init__(self, p, status):
53 super(PatchApplicationFailed, self).__init__(p, status)
54 self.patch = p
[email protected]dfaecd22011-04-21 00:33:3155 self.status = status
56
[email protected]34f68552012-05-09 19:18:3657 @property
58 def filename(self):
59 if self.patch:
60 return self.patch.filename
61
62 def __str__(self):
63 out = []
64 if self.filename:
65 out.append('Failed to apply patch for %s:' % self.filename)
66 if self.status:
67 out.append(self.status)
68 return '\n'.join(out)
69
[email protected]dfaecd22011-04-21 00:33:3170
71class CheckoutBase(object):
72 # Set to None to have verbose output.
73 VOID = subprocess2.VOID
74
[email protected]6ed8b502011-06-12 01:05:3575 def __init__(self, root_dir, project_name, post_processors):
76 """
77 Args:
78 post_processor: list of lambda(checkout, patches) to call on each of the
79 modified files.
80 """
[email protected]a5129fb2011-06-20 18:36:2581 super(CheckoutBase, self).__init__()
[email protected]dfaecd22011-04-21 00:33:3182 self.root_dir = root_dir
83 self.project_name = project_name
[email protected]3cdb7f32011-05-05 16:37:2484 if self.project_name is None:
85 self.project_path = self.root_dir
86 else:
87 self.project_path = os.path.join(self.root_dir, self.project_name)
[email protected]dfaecd22011-04-21 00:33:3188 # Only used for logging purposes.
89 self._last_seen_revision = None
[email protected]a5129fb2011-06-20 18:36:2590 self.post_processors = post_processors
[email protected]dfaecd22011-04-21 00:33:3191 assert self.root_dir
[email protected]dfaecd22011-04-21 00:33:3192 assert self.project_path
93
94 def get_settings(self, key):
95 return get_code_review_setting(self.project_path, key)
96
[email protected]51919772011-06-12 01:27:4297 def prepare(self, revision):
[email protected]dfaecd22011-04-21 00:33:3198 """Checks out a clean copy of the tree and removes any local modification.
99
100 This function shouldn't throw unless the remote repository is inaccessible,
101 there is no free disk space or hard issues like that.
[email protected]51919772011-06-12 01:27:42102
103 Args:
104 revision: The revision it should sync to, SCM specific.
[email protected]dfaecd22011-04-21 00:33:31105 """
106 raise NotImplementedError()
107
[email protected]b1d1a782011-09-29 14:13:55108 def apply_patch(self, patches, post_processors=None):
[email protected]dfaecd22011-04-21 00:33:31109 """Applies a patch and returns the list of modified files.
110
111 This function should throw patch.UnsupportedPatchFormat or
112 PatchApplicationFailed when relevant.
[email protected]8a1396c2011-04-22 00:14:24113
114 Args:
115 patches: patch.PatchSet object.
[email protected]dfaecd22011-04-21 00:33:31116 """
117 raise NotImplementedError()
118
119 def commit(self, commit_message, user):
120 """Commits the patch upstream, while impersonating 'user'."""
121 raise NotImplementedError()
122
[email protected]bc32ad12012-07-26 13:22:47123 def revisions(self, rev1, rev2):
124 """Returns the count of revisions from rev1 to rev2, e.g. len(]rev1, rev2]).
125
126 If rev2 is None, it means 'HEAD'.
127
128 Returns None if there is no link between the two.
129 """
130 raise NotImplementedError()
131
[email protected]dfaecd22011-04-21 00:33:31132
133class RawCheckout(CheckoutBase):
134 """Used to apply a patch locally without any intent to commit it.
135
136 To be used by the try server.
137 """
[email protected]51919772011-06-12 01:27:42138 def prepare(self, revision):
[email protected]dfaecd22011-04-21 00:33:31139 """Stubbed out."""
140 pass
141
[email protected]b1d1a782011-09-29 14:13:55142 def apply_patch(self, patches, post_processors=None):
[email protected]8a1396c2011-04-22 00:14:24143 """Ignores svn properties."""
[email protected]b1d1a782011-09-29 14:13:55144 post_processors = post_processors or self.post_processors or []
[email protected]dfaecd22011-04-21 00:33:31145 for p in patches:
[email protected]de800ff2012-09-12 19:25:24146 logging.debug('Applying %s' % p.filename)
[email protected]dfaecd22011-04-21 00:33:31147 try:
148 stdout = ''
149 filename = os.path.join(self.project_path, p.filename)
150 if p.is_delete:
151 os.remove(filename)
152 else:
153 dirname = os.path.dirname(p.filename)
154 full_dir = os.path.join(self.project_path, dirname)
155 if dirname and not os.path.isdir(full_dir):
156 os.makedirs(full_dir)
[email protected]4869bcf2011-06-04 01:14:32157
158 filepath = os.path.join(self.project_path, p.filename)
[email protected]dfaecd22011-04-21 00:33:31159 if p.is_binary:
[email protected]4869bcf2011-06-04 01:14:32160 with open(filepath, 'wb') as f:
[email protected]dfaecd22011-04-21 00:33:31161 f.write(p.get())
162 else:
[email protected]5e975632011-09-29 18:07:06163 if p.source_filename:
164 if not p.is_new:
165 raise PatchApplicationFailed(
[email protected]34f68552012-05-09 19:18:36166 p,
[email protected]5e975632011-09-29 18:07:06167 'File has a source filename specified but is not new')
168 # Copy the file first.
169 if os.path.isfile(filepath):
170 raise PatchApplicationFailed(
[email protected]34f68552012-05-09 19:18:36171 p, 'File exist but was about to be overwriten')
[email protected]5e975632011-09-29 18:07:06172 shutil.copy2(
173 os.path.join(self.project_path, p.source_filename), filepath)
[email protected]58fe6622011-06-03 20:59:27174 if p.diff_hunks:
175 stdout = subprocess2.check_output(
[email protected]5e975632011-09-29 18:07:06176 ['patch', '-u', '--binary', '-p%s' % p.patchlevel],
177 stdin=p.get(False),
[email protected]87e6d332011-09-09 19:01:28178 stderr=subprocess2.STDOUT,
[email protected]58fe6622011-06-03 20:59:27179 cwd=self.project_path)
[email protected]4869bcf2011-06-04 01:14:32180 elif p.is_new and not os.path.exists(filepath):
[email protected]58fe6622011-06-03 20:59:27181 # There is only a header. Just create the file.
[email protected]4869bcf2011-06-04 01:14:32182 open(filepath, 'w').close()
[email protected]b1d1a782011-09-29 14:13:55183 for post in post_processors:
[email protected]8a1396c2011-04-22 00:14:24184 post(self, p)
[email protected]dfaecd22011-04-21 00:33:31185 except OSError, e:
[email protected]34f68552012-05-09 19:18:36186 raise PatchApplicationFailed(p, '%s%s' % (stdout, e))
[email protected]dfaecd22011-04-21 00:33:31187 except subprocess.CalledProcessError, e:
188 raise PatchApplicationFailed(
[email protected]34f68552012-05-09 19:18:36189 p, '%s%s' % (stdout, getattr(e, 'stdout', None)))
[email protected]dfaecd22011-04-21 00:33:31190
191 def commit(self, commit_message, user):
192 """Stubbed out."""
193 raise NotImplementedError('RawCheckout can\'t commit')
194
[email protected]bc32ad12012-07-26 13:22:47195 def revisions(self, _rev1, _rev2):
196 return None
197
[email protected]dfaecd22011-04-21 00:33:31198
199class SvnConfig(object):
200 """Parses a svn configuration file."""
201 def __init__(self, svn_config_dir=None):
[email protected]a5129fb2011-06-20 18:36:25202 super(SvnConfig, self).__init__()
[email protected]dfaecd22011-04-21 00:33:31203 self.svn_config_dir = svn_config_dir
204 self.default = not bool(self.svn_config_dir)
205 if not self.svn_config_dir:
206 if sys.platform == 'win32':
207 self.svn_config_dir = os.path.join(os.environ['APPDATA'], 'Subversion')
208 else:
209 self.svn_config_dir = os.path.join(os.environ['HOME'], '.subversion')
210 svn_config_file = os.path.join(self.svn_config_dir, 'config')
211 parser = ConfigParser.SafeConfigParser()
212 if os.path.isfile(svn_config_file):
213 parser.read(svn_config_file)
214 else:
215 parser.add_section('auto-props')
216 self.auto_props = dict(parser.items('auto-props'))
217
218
219class SvnMixIn(object):
220 """MixIn class to add svn commands common to both svn and git-svn clients."""
221 # These members need to be set by the subclass.
222 commit_user = None
223 commit_pwd = None
224 svn_url = None
225 project_path = None
226 # Override at class level when necessary. If used, --non-interactive is
227 # implied.
228 svn_config = SvnConfig()
229 # Set to True when non-interactivity is necessary but a custom subversion
230 # configuration directory is not necessary.
231 non_interactive = False
232
[email protected]9842a0c2011-05-30 20:41:54233 def _add_svn_flags(self, args, non_interactive, credentials=True):
[email protected]dfaecd22011-04-21 00:33:31234 args = ['svn'] + args
235 if not self.svn_config.default:
236 args.extend(['--config-dir', self.svn_config.svn_config_dir])
237 if not self.svn_config.default or self.non_interactive or non_interactive:
238 args.append('--non-interactive')
[email protected]9842a0c2011-05-30 20:41:54239 if credentials:
240 if self.commit_user:
241 args.extend(['--username', self.commit_user])
242 if self.commit_pwd:
243 args.extend(['--password', self.commit_pwd])
[email protected]dfaecd22011-04-21 00:33:31244 return args
245
246 def _check_call_svn(self, args, **kwargs):
247 """Runs svn and throws an exception if the command failed."""
248 kwargs.setdefault('cwd', self.project_path)
249 kwargs.setdefault('stdout', self.VOID)
[email protected]0bcd1d32011-04-26 15:55:49250 return subprocess2.check_call_out(
251 self._add_svn_flags(args, False), **kwargs)
[email protected]dfaecd22011-04-21 00:33:31252
[email protected]9842a0c2011-05-30 20:41:54253 def _check_output_svn(self, args, credentials=True, **kwargs):
[email protected]dfaecd22011-04-21 00:33:31254 """Runs svn and throws an exception if the command failed.
255
256 Returns the output.
257 """
258 kwargs.setdefault('cwd', self.project_path)
[email protected]9842a0c2011-05-30 20:41:54259 return subprocess2.check_output(
[email protected]87e6d332011-09-09 19:01:28260 self._add_svn_flags(args, True, credentials),
261 stderr=subprocess2.STDOUT,
262 **kwargs)
[email protected]dfaecd22011-04-21 00:33:31263
264 @staticmethod
265 def _parse_svn_info(output, key):
266 """Returns value for key from svn info output.
267
268 Case insensitive.
269 """
270 values = {}
271 key = key.lower()
272 for line in output.splitlines(False):
273 if not line:
274 continue
275 k, v = line.split(':', 1)
276 k = k.strip().lower()
277 v = v.strip()
278 assert not k in values
279 values[k] = v
280 return values.get(key, None)
281
282
283class SvnCheckout(CheckoutBase, SvnMixIn):
284 """Manages a subversion checkout."""
[email protected]6ed8b502011-06-12 01:05:35285 def __init__(self, root_dir, project_name, commit_user, commit_pwd, svn_url,
286 post_processors=None):
[email protected]a5129fb2011-06-20 18:36:25287 CheckoutBase.__init__(self, root_dir, project_name, post_processors)
288 SvnMixIn.__init__(self)
[email protected]dfaecd22011-04-21 00:33:31289 self.commit_user = commit_user
290 self.commit_pwd = commit_pwd
291 self.svn_url = svn_url
292 assert bool(self.commit_user) >= bool(self.commit_pwd)
[email protected]dfaecd22011-04-21 00:33:31293
[email protected]51919772011-06-12 01:27:42294 def prepare(self, revision):
[email protected]dfaecd22011-04-21 00:33:31295 # Will checkout if the directory is not present.
[email protected]3cdb7f32011-05-05 16:37:24296 assert self.svn_url
[email protected]dfaecd22011-04-21 00:33:31297 if not os.path.isdir(self.project_path):
298 logging.info('Checking out %s in %s' %
299 (self.project_name, self.project_path))
[email protected]51919772011-06-12 01:27:42300 return self._revert(revision)
[email protected]dfaecd22011-04-21 00:33:31301
[email protected]b1d1a782011-09-29 14:13:55302 def apply_patch(self, patches, post_processors=None):
303 post_processors = post_processors or self.post_processors or []
[email protected]dfaecd22011-04-21 00:33:31304 for p in patches:
[email protected]de800ff2012-09-12 19:25:24305 logging.debug('Applying %s' % p.filename)
[email protected]dfaecd22011-04-21 00:33:31306 try:
[email protected]9842a0c2011-05-30 20:41:54307 # It is important to use credentials=False otherwise credentials could
308 # leak in the error message. Credentials are not necessary here for the
309 # following commands anyway.
[email protected]dfaecd22011-04-21 00:33:31310 stdout = ''
311 if p.is_delete:
[email protected]9842a0c2011-05-30 20:41:54312 stdout += self._check_output_svn(
313 ['delete', p.filename, '--force'], credentials=False)
[email protected]dfaecd22011-04-21 00:33:31314 else:
[email protected]dfaecd22011-04-21 00:33:31315 # svn add while creating directories otherwise svn add on the
316 # contained files will silently fail.
317 # First, find the root directory that exists.
318 dirname = os.path.dirname(p.filename)
319 dirs_to_create = []
320 while (dirname and
321 not os.path.isdir(os.path.join(self.project_path, dirname))):
322 dirs_to_create.append(dirname)
323 dirname = os.path.dirname(dirname)
324 for dir_to_create in reversed(dirs_to_create):
325 os.mkdir(os.path.join(self.project_path, dir_to_create))
326 stdout += self._check_output_svn(
[email protected]9842a0c2011-05-30 20:41:54327 ['add', dir_to_create, '--force'], credentials=False)
[email protected]dfaecd22011-04-21 00:33:31328
[email protected]4869bcf2011-06-04 01:14:32329 filepath = os.path.join(self.project_path, p.filename)
[email protected]dfaecd22011-04-21 00:33:31330 if p.is_binary:
[email protected]4869bcf2011-06-04 01:14:32331 with open(filepath, 'wb') as f:
[email protected]dfaecd22011-04-21 00:33:31332 f.write(p.get())
333 else:
[email protected]5e975632011-09-29 18:07:06334 if p.source_filename:
335 if not p.is_new:
336 raise PatchApplicationFailed(
[email protected]34f68552012-05-09 19:18:36337 p,
[email protected]5e975632011-09-29 18:07:06338 'File has a source filename specified but is not new')
339 # Copy the file first.
340 if os.path.isfile(filepath):
341 raise PatchApplicationFailed(
[email protected]34f68552012-05-09 19:18:36342 p, 'File exist but was about to be overwriten')
[email protected]3da83172012-05-07 16:17:20343 self._check_output_svn(
[email protected]b0f852f2012-09-15 01:37:21344 ['copy', p.source_filename, p.filename])
[email protected]58fe6622011-06-03 20:59:27345 if p.diff_hunks:
[email protected]ec4a9182012-09-28 20:39:45346 cmd = [
347 'patch',
348 '-p%s' % p.patchlevel,
349 '--forward',
350 '--force',
351 '--no-backup-if-mismatch',
352 ]
[email protected]58fe6622011-06-03 20:59:27353 stdout += subprocess2.check_output(
[email protected]5e975632011-09-29 18:07:06354 cmd, stdin=p.get(False), cwd=self.project_path)
[email protected]4869bcf2011-06-04 01:14:32355 elif p.is_new and not os.path.exists(filepath):
356 # There is only a header. Just create the file if it doesn't
357 # exist.
358 open(filepath, 'w').close()
[email protected]3da83172012-05-07 16:17:20359 if p.is_new and not p.source_filename:
360 # Do not run it if p.source_filename is defined, since svn copy was
361 # using above.
[email protected]9842a0c2011-05-30 20:41:54362 stdout += self._check_output_svn(
363 ['add', p.filename, '--force'], credentials=False)
[email protected]d7ca6162012-08-29 17:22:22364 for name, value in p.svn_properties:
365 if value is None:
366 stdout += self._check_output_svn(
367 ['propdel', '--quiet', name, p.filename], credentials=False)
368 else:
369 stdout += self._check_output_svn(
370 ['propset', name, value, p.filename], credentials=False)
[email protected]9842a0c2011-05-30 20:41:54371 for prop, values in self.svn_config.auto_props.iteritems():
[email protected]dfaecd22011-04-21 00:33:31372 if fnmatch.fnmatch(p.filename, prop):
[email protected]9842a0c2011-05-30 20:41:54373 for value in values.split(';'):
374 if '=' not in value:
[email protected]e1a03762012-09-24 15:28:52375 params = [value, '.']
[email protected]9842a0c2011-05-30 20:41:54376 else:
377 params = value.split('=', 1)
[email protected]e1a03762012-09-24 15:28:52378 if params[1] == '*':
379 # Works around crbug.com/150960 on Windows.
380 params[1] = '.'
[email protected]9842a0c2011-05-30 20:41:54381 stdout += self._check_output_svn(
382 ['propset'] + params + [p.filename], credentials=False)
[email protected]b1d1a782011-09-29 14:13:55383 for post in post_processors:
[email protected]8a1396c2011-04-22 00:14:24384 post(self, p)
[email protected]dfaecd22011-04-21 00:33:31385 except OSError, e:
[email protected]34f68552012-05-09 19:18:36386 raise PatchApplicationFailed(p, '%s%s' % (stdout, e))
[email protected]dfaecd22011-04-21 00:33:31387 except subprocess.CalledProcessError, e:
388 raise PatchApplicationFailed(
[email protected]34f68552012-05-09 19:18:36389 p,
[email protected]9842a0c2011-05-30 20:41:54390 'While running %s;\n%s%s' % (
391 ' '.join(e.cmd), stdout, getattr(e, 'stdout', '')))
[email protected]dfaecd22011-04-21 00:33:31392
393 def commit(self, commit_message, user):
394 logging.info('Committing patch for %s' % user)
395 assert self.commit_user
[email protected]1bf50972011-05-05 19:57:21396 assert isinstance(commit_message, unicode)
[email protected]dfaecd22011-04-21 00:33:31397 handle, commit_filename = tempfile.mkstemp(text=True)
398 try:
[email protected]1bf50972011-05-05 19:57:21399 # Shouldn't assume default encoding is UTF-8. But really, if you are using
400 # anything else, you are living in another world.
401 os.write(handle, commit_message.encode('utf-8'))
[email protected]dfaecd22011-04-21 00:33:31402 os.close(handle)
403 # When committing, svn won't update the Revision metadata of the checkout,
404 # so if svn commit returns "Committed revision 3.", svn info will still
405 # return "Revision: 2". Since running svn update right after svn commit
406 # creates a race condition with other committers, this code _must_ parse
407 # the output of svn commit and use a regexp to grab the revision number.
408 # Note that "Committed revision N." is localized but subprocess2 forces
409 # LANGUAGE=en.
410 args = ['commit', '--file', commit_filename]
411 # realauthor is parsed by a server-side hook.
412 if user and user != self.commit_user:
413 args.extend(['--with-revprop', 'realauthor=%s' % user])
414 out = self._check_output_svn(args)
415 finally:
416 os.remove(commit_filename)
417 lines = filter(None, out.splitlines())
418 match = re.match(r'^Committed revision (\d+).$', lines[-1])
419 if not match:
420 raise PatchApplicationFailed(
421 None,
422 'Couldn\'t make sense out of svn commit message:\n' + out)
423 return int(match.group(1))
424
[email protected]51919772011-06-12 01:27:42425 def _revert(self, revision):
[email protected]dfaecd22011-04-21 00:33:31426 """Reverts local modifications or checks out if the directory is not
427 present. Use depot_tools's functionality to do this.
428 """
429 flags = ['--ignore-externals']
[email protected]51919772011-06-12 01:27:42430 if revision:
431 flags.extend(['--revision', str(revision)])
[email protected]dfaecd22011-04-21 00:33:31432 if not os.path.isdir(self.project_path):
433 logging.info(
434 'Directory %s is not present, checking it out.' % self.project_path)
435 self._check_call_svn(
436 ['checkout', self.svn_url, self.project_path] + flags, cwd=None)
437 else:
[email protected]ea15cb72012-05-04 14:16:31438 scm.SVN.Revert(self.project_path, no_ignore=True)
[email protected]dfaecd22011-04-21 00:33:31439 # Revive files that were deleted in scm.SVN.Revert().
440 self._check_call_svn(['update', '--force'] + flags)
[email protected]51919772011-06-12 01:27:42441 return self._get_revision()
[email protected]dfaecd22011-04-21 00:33:31442
[email protected]51919772011-06-12 01:27:42443 def _get_revision(self):
[email protected]dfaecd22011-04-21 00:33:31444 out = self._check_output_svn(['info', '.'])
[email protected]51919772011-06-12 01:27:42445 revision = int(self._parse_svn_info(out, 'revision'))
446 if revision != self._last_seen_revision:
447 logging.info('Updated to revision %d' % revision)
448 self._last_seen_revision = revision
449 return revision
[email protected]dfaecd22011-04-21 00:33:31450
[email protected]bc32ad12012-07-26 13:22:47451 def revisions(self, rev1, rev2):
452 """Returns the number of actual commits, not just the difference between
453 numbers.
454 """
455 rev2 = rev2 or 'HEAD'
456 # Revision range is inclusive and ordering doesn't matter, they'll appear in
457 # the order specified.
458 try:
459 out = self._check_output_svn(
460 ['log', '-q', self.svn_url, '-r', '%s:%s' % (rev1, rev2)])
461 except subprocess.CalledProcessError:
462 return None
463 # Ignore the '----' lines.
464 return len([l for l in out.splitlines() if l.startswith('r')]) - 1
465
[email protected]dfaecd22011-04-21 00:33:31466
467class GitCheckoutBase(CheckoutBase):
468 """Base class for git checkout. Not to be used as-is."""
[email protected]6ed8b502011-06-12 01:05:35469 def __init__(self, root_dir, project_name, remote_branch,
470 post_processors=None):
471 super(GitCheckoutBase, self).__init__(
472 root_dir, project_name, post_processors)
[email protected]dfaecd22011-04-21 00:33:31473 # There is no reason to not hardcode it.
474 self.remote = 'origin'
475 self.remote_branch = remote_branch
476 self.working_branch = 'working_branch'
[email protected]dfaecd22011-04-21 00:33:31477
[email protected]51919772011-06-12 01:27:42478 def prepare(self, revision):
[email protected]dfaecd22011-04-21 00:33:31479 """Resets the git repository in a clean state.
480
481 Checks it out if not present and deletes the working branch.
482 """
[email protected]3cdb7f32011-05-05 16:37:24483 assert self.remote_branch
[email protected]dfaecd22011-04-21 00:33:31484 assert os.path.isdir(self.project_path)
485 self._check_call_git(['reset', '--hard', '--quiet'])
[email protected]51919772011-06-12 01:27:42486 if revision:
487 try:
488 revision = self._check_output_git(['rev-parse', revision])
489 except subprocess.CalledProcessError:
490 self._check_call_git(
491 ['fetch', self.remote, self.remote_branch, '--quiet'])
492 revision = self._check_output_git(['rev-parse', revision])
493 self._check_call_git(['checkout', '--force', '--quiet', revision])
494 else:
495 branches, active = self._branches()
496 if active != 'master':
497 self._check_call_git(['checkout', '--force', '--quiet', 'master'])
498 self._check_call_git(['pull', self.remote, self.remote_branch, '--quiet'])
499 if self.working_branch in branches:
500 self._call_git(['branch', '-D', self.working_branch])
[email protected]dfaecd22011-04-21 00:33:31501
[email protected]b1d1a782011-09-29 14:13:55502 def apply_patch(self, patches, post_processors=None):
[email protected]8a1396c2011-04-22 00:14:24503 """Applies a patch on 'working_branch' and switch to it.
504
505 Also commits the changes on the local branch.
506
507 Ignores svn properties and raise an exception on unexpected ones.
508 """
[email protected]b1d1a782011-09-29 14:13:55509 post_processors = post_processors or self.post_processors or []
[email protected]dfaecd22011-04-21 00:33:31510 # It this throws, the checkout is corrupted. Maybe worth deleting it and
511 # trying again?
[email protected]3cdb7f32011-05-05 16:37:24512 if self.remote_branch:
513 self._check_call_git(
514 ['checkout', '-b', self.working_branch,
515 '%s/%s' % (self.remote, self.remote_branch), '--quiet'])
[email protected]5e975632011-09-29 18:07:06516 for index, p in enumerate(patches):
[email protected]de800ff2012-09-12 19:25:24517 logging.debug('Applying %s' % p.filename)
[email protected]dfaecd22011-04-21 00:33:31518 try:
519 stdout = ''
520 if p.is_delete:
[email protected]5e975632011-09-29 18:07:06521 if (not os.path.exists(p.filename) and
522 any(p1.source_filename == p.filename for p1 in patches[0:index])):
523 # The file could already be deleted if a prior patch with file
524 # rename was already processed. To be sure, look at all the previous
525 # patches to see if they were a file rename.
526 pass
527 else:
528 stdout += self._check_output_git(['rm', p.filename])
[email protected]dfaecd22011-04-21 00:33:31529 else:
530 dirname = os.path.dirname(p.filename)
531 full_dir = os.path.join(self.project_path, dirname)
532 if dirname and not os.path.isdir(full_dir):
533 os.makedirs(full_dir)
534 if p.is_binary:
535 with open(os.path.join(self.project_path, p.filename), 'wb') as f:
536 f.write(p.get())
537 stdout += self._check_output_git(['add', p.filename])
538 else:
[email protected]58fe6622011-06-03 20:59:27539 # No need to do anything special with p.is_new or if not
540 # p.diff_hunks. git apply manages all that already.
[email protected]dfaecd22011-04-21 00:33:31541 stdout += self._check_output_git(
[email protected]5e975632011-09-29 18:07:06542 ['apply', '--index', '-p%s' % p.patchlevel], stdin=p.get(True))
[email protected]d7ca6162012-08-29 17:22:22543 for name, _ in p.svn_properties:
[email protected]dfaecd22011-04-21 00:33:31544 # Ignore some known auto-props flags through .subversion/config,
545 # bails out on the other ones.
546 # TODO(maruel): Read ~/.subversion/config and detect the rules that
547 # applies here to figure out if the property will be correctly
548 # handled.
[email protected]d7ca6162012-08-29 17:22:22549 if not name in (
[email protected]9799a072012-01-11 00:26:25550 'svn:eol-style', 'svn:executable', 'svn:mime-type'):
[email protected]dfaecd22011-04-21 00:33:31551 raise patch.UnsupportedPatchFormat(
552 p.filename,
553 'Cannot apply svn property %s to file %s.' % (
[email protected]d7ca6162012-08-29 17:22:22554 name, p.filename))
[email protected]b1d1a782011-09-29 14:13:55555 for post in post_processors:
[email protected]8a1396c2011-04-22 00:14:24556 post(self, p)
[email protected]dfaecd22011-04-21 00:33:31557 except OSError, e:
[email protected]34f68552012-05-09 19:18:36558 raise PatchApplicationFailed(p, '%s%s' % (stdout, e))
[email protected]dfaecd22011-04-21 00:33:31559 except subprocess.CalledProcessError, e:
560 raise PatchApplicationFailed(
[email protected]34f68552012-05-09 19:18:36561 p, '%s%s' % (stdout, getattr(e, 'stdout', None)))
[email protected]dfaecd22011-04-21 00:33:31562 # Once all the patches are processed and added to the index, commit the
563 # index.
564 self._check_call_git(['commit', '-m', 'Committed patch'])
565 # TODO(maruel): Weirdly enough they don't match, need to investigate.
566 #found_files = self._check_output_git(
567 # ['diff', 'master', '--name-only']).splitlines(False)
568 #assert sorted(patches.filenames) == sorted(found_files), (
569 # sorted(out), sorted(found_files))
570
571 def commit(self, commit_message, user):
572 """Updates the commit message.
573
574 Subclass needs to dcommit or push.
575 """
[email protected]1bf50972011-05-05 19:57:21576 assert isinstance(commit_message, unicode)
[email protected]dfaecd22011-04-21 00:33:31577 self._check_call_git(['commit', '--amend', '-m', commit_message])
578 return self._check_output_git(['rev-parse', 'HEAD']).strip()
579
580 def _check_call_git(self, args, **kwargs):
581 kwargs.setdefault('cwd', self.project_path)
582 kwargs.setdefault('stdout', self.VOID)
[email protected]0bcd1d32011-04-26 15:55:49583 return subprocess2.check_call_out(['git'] + args, **kwargs)
[email protected]dfaecd22011-04-21 00:33:31584
585 def _call_git(self, args, **kwargs):
586 """Like check_call but doesn't throw on failure."""
587 kwargs.setdefault('cwd', self.project_path)
588 kwargs.setdefault('stdout', self.VOID)
589 return subprocess2.call(['git'] + args, **kwargs)
590
591 def _check_output_git(self, args, **kwargs):
592 kwargs.setdefault('cwd', self.project_path)
[email protected]87e6d332011-09-09 19:01:28593 return subprocess2.check_output(
594 ['git'] + args, stderr=subprocess2.STDOUT, **kwargs)
[email protected]dfaecd22011-04-21 00:33:31595
596 def _branches(self):
597 """Returns the list of branches and the active one."""
598 out = self._check_output_git(['branch']).splitlines(False)
599 branches = [l[2:] for l in out]
600 active = None
601 for l in out:
602 if l.startswith('*'):
603 active = l[2:]
604 break
605 return branches, active
606
[email protected]bc32ad12012-07-26 13:22:47607 def revisions(self, rev1, rev2):
608 """Returns the number of actual commits between both hash."""
609 self._fetch_remote()
610
611 rev2 = rev2 or '%s/%s' % (self.remote, self.remote_branch)
612 # Revision range is ]rev1, rev2] and ordering matters.
613 try:
614 out = self._check_output_git(
615 ['log', '--format="%H"' , '%s..%s' % (rev1, rev2)])
616 except subprocess.CalledProcessError:
617 return None
618 return len(out.splitlines())
619
620 def _fetch_remote(self):
621 """Fetches the remote without rebasing."""
622 raise NotImplementedError()
623
624
625class GitCheckout(GitCheckoutBase):
626 """Git checkout implementation."""
627 def _fetch_remote(self):
628 # git fetch is always verbose even with -q -q so redirect its output.
629 self._check_output_git(['fetch', self.remote, self.remote_branch])
630
[email protected]dfaecd22011-04-21 00:33:31631
[email protected]dfaecd22011-04-21 00:33:31632class ReadOnlyCheckout(object):
633 """Converts a checkout into a read-only one."""
[email protected]b1d1a782011-09-29 14:13:55634 def __init__(self, checkout, post_processors=None):
[email protected]a5129fb2011-06-20 18:36:25635 super(ReadOnlyCheckout, self).__init__()
[email protected]dfaecd22011-04-21 00:33:31636 self.checkout = checkout
[email protected]b1d1a782011-09-29 14:13:55637 self.post_processors = (post_processors or []) + (
638 self.checkout.post_processors or [])
[email protected]dfaecd22011-04-21 00:33:31639
[email protected]51919772011-06-12 01:27:42640 def prepare(self, revision):
641 return self.checkout.prepare(revision)
[email protected]dfaecd22011-04-21 00:33:31642
643 def get_settings(self, key):
644 return self.checkout.get_settings(key)
645
[email protected]b1d1a782011-09-29 14:13:55646 def apply_patch(self, patches, post_processors=None):
647 return self.checkout.apply_patch(
648 patches, post_processors or self.post_processors)
[email protected]dfaecd22011-04-21 00:33:31649
650 def commit(self, message, user): # pylint: disable=R0201
651 logging.info('Would have committed for %s with message: %s' % (
652 user, message))
653 return 'FAKE'
654
[email protected]bc32ad12012-07-26 13:22:47655 def revisions(self, rev1, rev2):
656 return self.checkout.revisions(rev1, rev2)
657
[email protected]dfaecd22011-04-21 00:33:31658 @property
659 def project_name(self):
660 return self.checkout.project_name
661
662 @property
663 def project_path(self):
664 return self.checkout.project_path