blob: 6f82383db37000d65d2e6b9c2c7c983ae15a7bec [file] [log] [blame]
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +00001# coding=utf8
maruel@chromium.org9799a072012-01-11 00:26:25 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +00003# 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
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000010import ConfigParser
11import fnmatch
12import logging
13import os
14import re
maruel@chromium.org5e975632011-09-29 18:07:06 +000015import shutil
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000016import 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()
maruel@chromium.org004fb712011-06-21 20:02:16 +000045 except IOError:
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000046 return None
47 return settings.get(key, None)
48
49
50class PatchApplicationFailed(Exception):
51 """Patch failed to be applied."""
maruel@chromium.org34f68552012-05-09 19:18:36 +000052 def __init__(self, p, status):
53 super(PatchApplicationFailed, self).__init__(p, status)
54 self.patch = p
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000055 self.status = status
56
maruel@chromium.org34f68552012-05-09 19:18:36 +000057 @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
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000070
71class CheckoutBase(object):
72 # Set to None to have verbose output.
73 VOID = subprocess2.VOID
74
maruel@chromium.org6ed8b502011-06-12 01:05:35 +000075 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 """
maruel@chromium.orga5129fb2011-06-20 18:36:25 +000081 super(CheckoutBase, self).__init__()
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000082 self.root_dir = root_dir
83 self.project_name = project_name
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +000084 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)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000088 # Only used for logging purposes.
89 self._last_seen_revision = None
maruel@chromium.orga5129fb2011-06-20 18:36:25 +000090 self.post_processors = post_processors
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000091 assert self.root_dir
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000092 assert self.project_path
93
94 def get_settings(self, key):
95 return get_code_review_setting(self.project_path, key)
96
maruel@chromium.org51919772011-06-12 01:27:42 +000097 def prepare(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000098 """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.
maruel@chromium.org51919772011-06-12 01:27:42 +0000102
103 Args:
104 revision: The revision it should sync to, SCM specific.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000105 """
106 raise NotImplementedError()
107
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000108 def apply_patch(self, patches, post_processors=None):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000109 """Applies a patch and returns the list of modified files.
110
111 This function should throw patch.UnsupportedPatchFormat or
112 PatchApplicationFailed when relevant.
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000113
114 Args:
115 patches: patch.PatchSet object.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000116 """
117 raise NotImplementedError()
118
119 def commit(self, commit_message, user):
120 """Commits the patch upstream, while impersonating 'user'."""
121 raise NotImplementedError()
122
123
124class RawCheckout(CheckoutBase):
125 """Used to apply a patch locally without any intent to commit it.
126
127 To be used by the try server.
128 """
maruel@chromium.org51919772011-06-12 01:27:42 +0000129 def prepare(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000130 """Stubbed out."""
131 pass
132
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000133 def apply_patch(self, patches, post_processors=None):
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000134 """Ignores svn properties."""
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000135 post_processors = post_processors or self.post_processors or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000136 for p in patches:
137 try:
138 stdout = ''
139 filename = os.path.join(self.project_path, p.filename)
140 if p.is_delete:
141 os.remove(filename)
142 else:
143 dirname = os.path.dirname(p.filename)
144 full_dir = os.path.join(self.project_path, dirname)
145 if dirname and not os.path.isdir(full_dir):
146 os.makedirs(full_dir)
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000147
148 filepath = os.path.join(self.project_path, p.filename)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000149 if p.is_binary:
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000150 with open(filepath, 'wb') as f:
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000151 f.write(p.get())
152 else:
maruel@chromium.org5e975632011-09-29 18:07:06 +0000153 if p.source_filename:
154 if not p.is_new:
155 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000156 p,
maruel@chromium.org5e975632011-09-29 18:07:06 +0000157 'File has a source filename specified but is not new')
158 # Copy the file first.
159 if os.path.isfile(filepath):
160 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000161 p, 'File exist but was about to be overwriten')
maruel@chromium.org5e975632011-09-29 18:07:06 +0000162 shutil.copy2(
163 os.path.join(self.project_path, p.source_filename), filepath)
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000164 if p.diff_hunks:
165 stdout = subprocess2.check_output(
maruel@chromium.org5e975632011-09-29 18:07:06 +0000166 ['patch', '-u', '--binary', '-p%s' % p.patchlevel],
167 stdin=p.get(False),
maruel@chromium.org87e6d332011-09-09 19:01:28 +0000168 stderr=subprocess2.STDOUT,
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000169 cwd=self.project_path)
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000170 elif p.is_new and not os.path.exists(filepath):
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000171 # There is only a header. Just create the file.
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000172 open(filepath, 'w').close()
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000173 for post in post_processors:
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000174 post(self, p)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000175 except OSError, e:
maruel@chromium.org34f68552012-05-09 19:18:36 +0000176 raise PatchApplicationFailed(p, '%s%s' % (stdout, e))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000177 except subprocess.CalledProcessError, e:
178 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000179 p, '%s%s' % (stdout, getattr(e, 'stdout', None)))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000180
181 def commit(self, commit_message, user):
182 """Stubbed out."""
183 raise NotImplementedError('RawCheckout can\'t commit')
184
185
186class SvnConfig(object):
187 """Parses a svn configuration file."""
188 def __init__(self, svn_config_dir=None):
maruel@chromium.orga5129fb2011-06-20 18:36:25 +0000189 super(SvnConfig, self).__init__()
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000190 self.svn_config_dir = svn_config_dir
191 self.default = not bool(self.svn_config_dir)
192 if not self.svn_config_dir:
193 if sys.platform == 'win32':
194 self.svn_config_dir = os.path.join(os.environ['APPDATA'], 'Subversion')
195 else:
196 self.svn_config_dir = os.path.join(os.environ['HOME'], '.subversion')
197 svn_config_file = os.path.join(self.svn_config_dir, 'config')
198 parser = ConfigParser.SafeConfigParser()
199 if os.path.isfile(svn_config_file):
200 parser.read(svn_config_file)
201 else:
202 parser.add_section('auto-props')
203 self.auto_props = dict(parser.items('auto-props'))
204
205
206class SvnMixIn(object):
207 """MixIn class to add svn commands common to both svn and git-svn clients."""
208 # These members need to be set by the subclass.
209 commit_user = None
210 commit_pwd = None
211 svn_url = None
212 project_path = None
213 # Override at class level when necessary. If used, --non-interactive is
214 # implied.
215 svn_config = SvnConfig()
216 # Set to True when non-interactivity is necessary but a custom subversion
217 # configuration directory is not necessary.
218 non_interactive = False
219
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000220 def _add_svn_flags(self, args, non_interactive, credentials=True):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000221 args = ['svn'] + args
222 if not self.svn_config.default:
223 args.extend(['--config-dir', self.svn_config.svn_config_dir])
224 if not self.svn_config.default or self.non_interactive or non_interactive:
225 args.append('--non-interactive')
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000226 if credentials:
227 if self.commit_user:
228 args.extend(['--username', self.commit_user])
229 if self.commit_pwd:
230 args.extend(['--password', self.commit_pwd])
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000231 return args
232
233 def _check_call_svn(self, args, **kwargs):
234 """Runs svn and throws an exception if the command failed."""
235 kwargs.setdefault('cwd', self.project_path)
236 kwargs.setdefault('stdout', self.VOID)
maruel@chromium.org0bcd1d32011-04-26 15:55:49 +0000237 return subprocess2.check_call_out(
238 self._add_svn_flags(args, False), **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000239
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000240 def _check_output_svn(self, args, credentials=True, **kwargs):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000241 """Runs svn and throws an exception if the command failed.
242
243 Returns the output.
244 """
245 kwargs.setdefault('cwd', self.project_path)
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000246 return subprocess2.check_output(
maruel@chromium.org87e6d332011-09-09 19:01:28 +0000247 self._add_svn_flags(args, True, credentials),
248 stderr=subprocess2.STDOUT,
249 **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000250
251 @staticmethod
252 def _parse_svn_info(output, key):
253 """Returns value for key from svn info output.
254
255 Case insensitive.
256 """
257 values = {}
258 key = key.lower()
259 for line in output.splitlines(False):
260 if not line:
261 continue
262 k, v = line.split(':', 1)
263 k = k.strip().lower()
264 v = v.strip()
265 assert not k in values
266 values[k] = v
267 return values.get(key, None)
268
269
270class SvnCheckout(CheckoutBase, SvnMixIn):
271 """Manages a subversion checkout."""
maruel@chromium.org6ed8b502011-06-12 01:05:35 +0000272 def __init__(self, root_dir, project_name, commit_user, commit_pwd, svn_url,
273 post_processors=None):
maruel@chromium.orga5129fb2011-06-20 18:36:25 +0000274 CheckoutBase.__init__(self, root_dir, project_name, post_processors)
275 SvnMixIn.__init__(self)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000276 self.commit_user = commit_user
277 self.commit_pwd = commit_pwd
278 self.svn_url = svn_url
279 assert bool(self.commit_user) >= bool(self.commit_pwd)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000280
maruel@chromium.org51919772011-06-12 01:27:42 +0000281 def prepare(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000282 # Will checkout if the directory is not present.
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000283 assert self.svn_url
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000284 if not os.path.isdir(self.project_path):
285 logging.info('Checking out %s in %s' %
286 (self.project_name, self.project_path))
maruel@chromium.org51919772011-06-12 01:27:42 +0000287 return self._revert(revision)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000288
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000289 def apply_patch(self, patches, post_processors=None):
290 post_processors = post_processors or self.post_processors or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000291 for p in patches:
292 try:
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000293 # It is important to use credentials=False otherwise credentials could
294 # leak in the error message. Credentials are not necessary here for the
295 # following commands anyway.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000296 stdout = ''
297 if p.is_delete:
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000298 stdout += self._check_output_svn(
299 ['delete', p.filename, '--force'], credentials=False)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000300 else:
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000301 # svn add while creating directories otherwise svn add on the
302 # contained files will silently fail.
303 # First, find the root directory that exists.
304 dirname = os.path.dirname(p.filename)
305 dirs_to_create = []
306 while (dirname and
307 not os.path.isdir(os.path.join(self.project_path, dirname))):
308 dirs_to_create.append(dirname)
309 dirname = os.path.dirname(dirname)
310 for dir_to_create in reversed(dirs_to_create):
311 os.mkdir(os.path.join(self.project_path, dir_to_create))
312 stdout += self._check_output_svn(
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000313 ['add', dir_to_create, '--force'], credentials=False)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000314
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000315 filepath = os.path.join(self.project_path, p.filename)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000316 if p.is_binary:
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000317 with open(filepath, 'wb') as f:
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000318 f.write(p.get())
319 else:
maruel@chromium.org5e975632011-09-29 18:07:06 +0000320 if p.source_filename:
321 if not p.is_new:
322 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000323 p,
maruel@chromium.org5e975632011-09-29 18:07:06 +0000324 'File has a source filename specified but is not new')
325 # Copy the file first.
326 if os.path.isfile(filepath):
327 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000328 p, 'File exist but was about to be overwriten')
maruel@chromium.org3da83172012-05-07 16:17:20 +0000329 self._check_output_svn(
330 [
331 'copy',
332 os.path.join(self.project_path, p.source_filename),
333 filepath
334 ])
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000335 if p.diff_hunks:
336 cmd = ['patch', '-p%s' % p.patchlevel, '--forward', '--force']
337 stdout += subprocess2.check_output(
maruel@chromium.org5e975632011-09-29 18:07:06 +0000338 cmd, stdin=p.get(False), cwd=self.project_path)
maruel@chromium.org4869bcf2011-06-04 01:14:32 +0000339 elif p.is_new and not os.path.exists(filepath):
340 # There is only a header. Just create the file if it doesn't
341 # exist.
342 open(filepath, 'w').close()
maruel@chromium.org3da83172012-05-07 16:17:20 +0000343 if p.is_new and not p.source_filename:
344 # Do not run it if p.source_filename is defined, since svn copy was
345 # using above.
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000346 stdout += self._check_output_svn(
347 ['add', p.filename, '--force'], credentials=False)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000348 for prop in p.svn_properties:
349 stdout += self._check_output_svn(
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000350 ['propset', prop[0], prop[1], p.filename], credentials=False)
351 for prop, values in self.svn_config.auto_props.iteritems():
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000352 if fnmatch.fnmatch(p.filename, prop):
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000353 for value in values.split(';'):
354 if '=' not in value:
355 params = [value, '*']
356 else:
357 params = value.split('=', 1)
358 stdout += self._check_output_svn(
359 ['propset'] + params + [p.filename], credentials=False)
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000360 for post in post_processors:
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000361 post(self, p)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000362 except OSError, e:
maruel@chromium.org34f68552012-05-09 19:18:36 +0000363 raise PatchApplicationFailed(p, '%s%s' % (stdout, e))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000364 except subprocess.CalledProcessError, e:
365 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000366 p,
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000367 'While running %s;\n%s%s' % (
368 ' '.join(e.cmd), stdout, getattr(e, 'stdout', '')))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000369
370 def commit(self, commit_message, user):
371 logging.info('Committing patch for %s' % user)
372 assert self.commit_user
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000373 assert isinstance(commit_message, unicode)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000374 handle, commit_filename = tempfile.mkstemp(text=True)
375 try:
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000376 # Shouldn't assume default encoding is UTF-8. But really, if you are using
377 # anything else, you are living in another world.
378 os.write(handle, commit_message.encode('utf-8'))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000379 os.close(handle)
380 # When committing, svn won't update the Revision metadata of the checkout,
381 # so if svn commit returns "Committed revision 3.", svn info will still
382 # return "Revision: 2". Since running svn update right after svn commit
383 # creates a race condition with other committers, this code _must_ parse
384 # the output of svn commit and use a regexp to grab the revision number.
385 # Note that "Committed revision N." is localized but subprocess2 forces
386 # LANGUAGE=en.
387 args = ['commit', '--file', commit_filename]
388 # realauthor is parsed by a server-side hook.
389 if user and user != self.commit_user:
390 args.extend(['--with-revprop', 'realauthor=%s' % user])
391 out = self._check_output_svn(args)
392 finally:
393 os.remove(commit_filename)
394 lines = filter(None, out.splitlines())
395 match = re.match(r'^Committed revision (\d+).$', lines[-1])
396 if not match:
397 raise PatchApplicationFailed(
398 None,
399 'Couldn\'t make sense out of svn commit message:\n' + out)
400 return int(match.group(1))
401
maruel@chromium.org51919772011-06-12 01:27:42 +0000402 def _revert(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000403 """Reverts local modifications or checks out if the directory is not
404 present. Use depot_tools's functionality to do this.
405 """
406 flags = ['--ignore-externals']
maruel@chromium.org51919772011-06-12 01:27:42 +0000407 if revision:
408 flags.extend(['--revision', str(revision)])
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000409 if not os.path.isdir(self.project_path):
410 logging.info(
411 'Directory %s is not present, checking it out.' % self.project_path)
412 self._check_call_svn(
413 ['checkout', self.svn_url, self.project_path] + flags, cwd=None)
414 else:
maruel@chromium.orgea15cb72012-05-04 14:16:31 +0000415 scm.SVN.Revert(self.project_path, no_ignore=True)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000416 # Revive files that were deleted in scm.SVN.Revert().
417 self._check_call_svn(['update', '--force'] + flags)
maruel@chromium.org51919772011-06-12 01:27:42 +0000418 return self._get_revision()
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000419
maruel@chromium.org51919772011-06-12 01:27:42 +0000420 def _get_revision(self):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000421 out = self._check_output_svn(['info', '.'])
maruel@chromium.org51919772011-06-12 01:27:42 +0000422 revision = int(self._parse_svn_info(out, 'revision'))
423 if revision != self._last_seen_revision:
424 logging.info('Updated to revision %d' % revision)
425 self._last_seen_revision = revision
426 return revision
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000427
428
429class GitCheckoutBase(CheckoutBase):
430 """Base class for git checkout. Not to be used as-is."""
maruel@chromium.org6ed8b502011-06-12 01:05:35 +0000431 def __init__(self, root_dir, project_name, remote_branch,
432 post_processors=None):
433 super(GitCheckoutBase, self).__init__(
434 root_dir, project_name, post_processors)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000435 # There is no reason to not hardcode it.
436 self.remote = 'origin'
437 self.remote_branch = remote_branch
438 self.working_branch = 'working_branch'
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000439
maruel@chromium.org51919772011-06-12 01:27:42 +0000440 def prepare(self, revision):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000441 """Resets the git repository in a clean state.
442
443 Checks it out if not present and deletes the working branch.
444 """
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000445 assert self.remote_branch
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000446 assert os.path.isdir(self.project_path)
447 self._check_call_git(['reset', '--hard', '--quiet'])
maruel@chromium.org51919772011-06-12 01:27:42 +0000448 if revision:
449 try:
450 revision = self._check_output_git(['rev-parse', revision])
451 except subprocess.CalledProcessError:
452 self._check_call_git(
453 ['fetch', self.remote, self.remote_branch, '--quiet'])
454 revision = self._check_output_git(['rev-parse', revision])
455 self._check_call_git(['checkout', '--force', '--quiet', revision])
456 else:
457 branches, active = self._branches()
458 if active != 'master':
459 self._check_call_git(['checkout', '--force', '--quiet', 'master'])
460 self._check_call_git(['pull', self.remote, self.remote_branch, '--quiet'])
461 if self.working_branch in branches:
462 self._call_git(['branch', '-D', self.working_branch])
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000463
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000464 def apply_patch(self, patches, post_processors=None):
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000465 """Applies a patch on 'working_branch' and switch to it.
466
467 Also commits the changes on the local branch.
468
469 Ignores svn properties and raise an exception on unexpected ones.
470 """
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000471 post_processors = post_processors or self.post_processors or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000472 # It this throws, the checkout is corrupted. Maybe worth deleting it and
473 # trying again?
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000474 if self.remote_branch:
475 self._check_call_git(
476 ['checkout', '-b', self.working_branch,
477 '%s/%s' % (self.remote, self.remote_branch), '--quiet'])
maruel@chromium.org5e975632011-09-29 18:07:06 +0000478 for index, p in enumerate(patches):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000479 try:
480 stdout = ''
481 if p.is_delete:
maruel@chromium.org5e975632011-09-29 18:07:06 +0000482 if (not os.path.exists(p.filename) and
483 any(p1.source_filename == p.filename for p1 in patches[0:index])):
484 # The file could already be deleted if a prior patch with file
485 # rename was already processed. To be sure, look at all the previous
486 # patches to see if they were a file rename.
487 pass
488 else:
489 stdout += self._check_output_git(['rm', p.filename])
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000490 else:
491 dirname = os.path.dirname(p.filename)
492 full_dir = os.path.join(self.project_path, dirname)
493 if dirname and not os.path.isdir(full_dir):
494 os.makedirs(full_dir)
495 if p.is_binary:
496 with open(os.path.join(self.project_path, p.filename), 'wb') as f:
497 f.write(p.get())
498 stdout += self._check_output_git(['add', p.filename])
499 else:
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000500 # No need to do anything special with p.is_new or if not
501 # p.diff_hunks. git apply manages all that already.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000502 stdout += self._check_output_git(
maruel@chromium.org5e975632011-09-29 18:07:06 +0000503 ['apply', '--index', '-p%s' % p.patchlevel], stdin=p.get(True))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000504 for prop in p.svn_properties:
505 # Ignore some known auto-props flags through .subversion/config,
506 # bails out on the other ones.
507 # TODO(maruel): Read ~/.subversion/config and detect the rules that
508 # applies here to figure out if the property will be correctly
509 # handled.
maruel@chromium.org9799a072012-01-11 00:26:25 +0000510 if not prop[0] in (
511 'svn:eol-style', 'svn:executable', 'svn:mime-type'):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000512 raise patch.UnsupportedPatchFormat(
513 p.filename,
514 'Cannot apply svn property %s to file %s.' % (
515 prop[0], p.filename))
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000516 for post in post_processors:
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000517 post(self, p)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000518 except OSError, e:
maruel@chromium.org34f68552012-05-09 19:18:36 +0000519 raise PatchApplicationFailed(p, '%s%s' % (stdout, e))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000520 except subprocess.CalledProcessError, e:
521 raise PatchApplicationFailed(
maruel@chromium.org34f68552012-05-09 19:18:36 +0000522 p, '%s%s' % (stdout, getattr(e, 'stdout', None)))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000523 # Once all the patches are processed and added to the index, commit the
524 # index.
525 self._check_call_git(['commit', '-m', 'Committed patch'])
526 # TODO(maruel): Weirdly enough they don't match, need to investigate.
527 #found_files = self._check_output_git(
528 # ['diff', 'master', '--name-only']).splitlines(False)
529 #assert sorted(patches.filenames) == sorted(found_files), (
530 # sorted(out), sorted(found_files))
531
532 def commit(self, commit_message, user):
533 """Updates the commit message.
534
535 Subclass needs to dcommit or push.
536 """
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000537 assert isinstance(commit_message, unicode)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000538 self._check_call_git(['commit', '--amend', '-m', commit_message])
539 return self._check_output_git(['rev-parse', 'HEAD']).strip()
540
541 def _check_call_git(self, args, **kwargs):
542 kwargs.setdefault('cwd', self.project_path)
543 kwargs.setdefault('stdout', self.VOID)
maruel@chromium.org0bcd1d32011-04-26 15:55:49 +0000544 return subprocess2.check_call_out(['git'] + args, **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000545
546 def _call_git(self, args, **kwargs):
547 """Like check_call but doesn't throw on failure."""
548 kwargs.setdefault('cwd', self.project_path)
549 kwargs.setdefault('stdout', self.VOID)
550 return subprocess2.call(['git'] + args, **kwargs)
551
552 def _check_output_git(self, args, **kwargs):
553 kwargs.setdefault('cwd', self.project_path)
maruel@chromium.org87e6d332011-09-09 19:01:28 +0000554 return subprocess2.check_output(
555 ['git'] + args, stderr=subprocess2.STDOUT, **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000556
557 def _branches(self):
558 """Returns the list of branches and the active one."""
559 out = self._check_output_git(['branch']).splitlines(False)
560 branches = [l[2:] for l in out]
561 active = None
562 for l in out:
563 if l.startswith('*'):
564 active = l[2:]
565 break
566 return branches, active
567
568
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000569class ReadOnlyCheckout(object):
570 """Converts a checkout into a read-only one."""
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000571 def __init__(self, checkout, post_processors=None):
maruel@chromium.orga5129fb2011-06-20 18:36:25 +0000572 super(ReadOnlyCheckout, self).__init__()
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000573 self.checkout = checkout
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000574 self.post_processors = (post_processors or []) + (
575 self.checkout.post_processors or [])
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000576
maruel@chromium.org51919772011-06-12 01:27:42 +0000577 def prepare(self, revision):
578 return self.checkout.prepare(revision)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000579
580 def get_settings(self, key):
581 return self.checkout.get_settings(key)
582
maruel@chromium.orgb1d1a782011-09-29 14:13:55 +0000583 def apply_patch(self, patches, post_processors=None):
584 return self.checkout.apply_patch(
585 patches, post_processors or self.post_processors)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000586
587 def commit(self, message, user): # pylint: disable=R0201
588 logging.info('Would have committed for %s with message: %s' % (
589 user, message))
590 return 'FAKE'
591
592 @property
593 def project_name(self):
594 return self.checkout.project_name
595
596 @property
597 def project_path(self):
598 return self.checkout.project_path