blob: 21c283e8cbd4e3860f8a52fbe01f2e7bcbc9ea1f [file] [log] [blame]
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +00001# coding=utf8
2# Copyright (c) 2011 The Chromium Authors. All rights reserved.
3# 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
10from __future__ import with_statement
11import ConfigParser
12import fnmatch
13import logging
14import os
15import re
16import 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()
45 except OSError:
46 return None
47 return settings.get(key, None)
48
49
50class PatchApplicationFailed(Exception):
51 """Patch failed to be applied."""
52 def __init__(self, filename, status):
53 super(PatchApplicationFailed, self).__init__(filename, status)
54 self.filename = filename
55 self.status = status
56
57
58class CheckoutBase(object):
59 # Set to None to have verbose output.
60 VOID = subprocess2.VOID
61
62 def __init__(self, root_dir, project_name):
63 self.root_dir = root_dir
64 self.project_name = project_name
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +000065 if self.project_name is None:
66 self.project_path = self.root_dir
67 else:
68 self.project_path = os.path.join(self.root_dir, self.project_name)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000069 # Only used for logging purposes.
70 self._last_seen_revision = None
71 assert self.root_dir
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000072 assert self.project_path
73
74 def get_settings(self, key):
75 return get_code_review_setting(self.project_path, key)
76
77 def prepare(self):
78 """Checks out a clean copy of the tree and removes any local modification.
79
80 This function shouldn't throw unless the remote repository is inaccessible,
81 there is no free disk space or hard issues like that.
82 """
83 raise NotImplementedError()
84
maruel@chromium.org8a1396c2011-04-22 00:14:24 +000085 def apply_patch(self, patches, post_processor=None):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000086 """Applies a patch and returns the list of modified files.
87
88 This function should throw patch.UnsupportedPatchFormat or
89 PatchApplicationFailed when relevant.
maruel@chromium.org8a1396c2011-04-22 00:14:24 +000090
91 Args:
92 patches: patch.PatchSet object.
93 post_processor: list of lambda(checkout, patches) to call on each of the
94 modified files.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +000095 """
96 raise NotImplementedError()
97
98 def commit(self, commit_message, user):
99 """Commits the patch upstream, while impersonating 'user'."""
100 raise NotImplementedError()
101
102
103class RawCheckout(CheckoutBase):
104 """Used to apply a patch locally without any intent to commit it.
105
106 To be used by the try server.
107 """
108 def prepare(self):
109 """Stubbed out."""
110 pass
111
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000112 def apply_patch(self, patches, post_processor=None):
113 """Ignores svn properties."""
114 post_processor = post_processor or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000115 for p in patches:
116 try:
117 stdout = ''
118 filename = os.path.join(self.project_path, p.filename)
119 if p.is_delete:
120 os.remove(filename)
121 else:
122 dirname = os.path.dirname(p.filename)
123 full_dir = os.path.join(self.project_path, dirname)
124 if dirname and not os.path.isdir(full_dir):
125 os.makedirs(full_dir)
126 if p.is_binary:
127 with open(os.path.join(filename), 'wb') as f:
128 f.write(p.get())
129 else:
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000130 if p.diff_hunks:
131 stdout = subprocess2.check_output(
132 ['patch', '-p%s' % p.patchlevel],
133 stdin=p.get(),
134 cwd=self.project_path)
135 elif p.is_new:
136 # There is only a header. Just create the file.
137 open(os.path.join(self.project_path, p.filename), 'w').close()
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000138 for post in post_processor:
139 post(self, p)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000140 except OSError, e:
141 raise PatchApplicationFailed(p.filename, '%s%s' % (stdout, e))
142 except subprocess.CalledProcessError, e:
143 raise PatchApplicationFailed(
144 p.filename, '%s%s' % (stdout, getattr(e, 'stdout', None)))
145
146 def commit(self, commit_message, user):
147 """Stubbed out."""
148 raise NotImplementedError('RawCheckout can\'t commit')
149
150
151class SvnConfig(object):
152 """Parses a svn configuration file."""
153 def __init__(self, svn_config_dir=None):
154 self.svn_config_dir = svn_config_dir
155 self.default = not bool(self.svn_config_dir)
156 if not self.svn_config_dir:
157 if sys.platform == 'win32':
158 self.svn_config_dir = os.path.join(os.environ['APPDATA'], 'Subversion')
159 else:
160 self.svn_config_dir = os.path.join(os.environ['HOME'], '.subversion')
161 svn_config_file = os.path.join(self.svn_config_dir, 'config')
162 parser = ConfigParser.SafeConfigParser()
163 if os.path.isfile(svn_config_file):
164 parser.read(svn_config_file)
165 else:
166 parser.add_section('auto-props')
167 self.auto_props = dict(parser.items('auto-props'))
168
169
170class SvnMixIn(object):
171 """MixIn class to add svn commands common to both svn and git-svn clients."""
172 # These members need to be set by the subclass.
173 commit_user = None
174 commit_pwd = None
175 svn_url = None
176 project_path = None
177 # Override at class level when necessary. If used, --non-interactive is
178 # implied.
179 svn_config = SvnConfig()
180 # Set to True when non-interactivity is necessary but a custom subversion
181 # configuration directory is not necessary.
182 non_interactive = False
183
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000184 def _add_svn_flags(self, args, non_interactive, credentials=True):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000185 args = ['svn'] + args
186 if not self.svn_config.default:
187 args.extend(['--config-dir', self.svn_config.svn_config_dir])
188 if not self.svn_config.default or self.non_interactive or non_interactive:
189 args.append('--non-interactive')
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000190 if credentials:
191 if self.commit_user:
192 args.extend(['--username', self.commit_user])
193 if self.commit_pwd:
194 args.extend(['--password', self.commit_pwd])
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000195 return args
196
197 def _check_call_svn(self, args, **kwargs):
198 """Runs svn and throws an exception if the command failed."""
199 kwargs.setdefault('cwd', self.project_path)
200 kwargs.setdefault('stdout', self.VOID)
maruel@chromium.org0bcd1d32011-04-26 15:55:49 +0000201 return subprocess2.check_call_out(
202 self._add_svn_flags(args, False), **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000203
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000204 def _check_output_svn(self, args, credentials=True, **kwargs):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000205 """Runs svn and throws an exception if the command failed.
206
207 Returns the output.
208 """
209 kwargs.setdefault('cwd', self.project_path)
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000210 return subprocess2.check_output(
211 self._add_svn_flags(args, True, credentials), **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000212
213 @staticmethod
214 def _parse_svn_info(output, key):
215 """Returns value for key from svn info output.
216
217 Case insensitive.
218 """
219 values = {}
220 key = key.lower()
221 for line in output.splitlines(False):
222 if not line:
223 continue
224 k, v = line.split(':', 1)
225 k = k.strip().lower()
226 v = v.strip()
227 assert not k in values
228 values[k] = v
229 return values.get(key, None)
230
231
232class SvnCheckout(CheckoutBase, SvnMixIn):
233 """Manages a subversion checkout."""
234 def __init__(self, root_dir, project_name, commit_user, commit_pwd, svn_url):
235 super(SvnCheckout, self).__init__(root_dir, project_name)
236 self.commit_user = commit_user
237 self.commit_pwd = commit_pwd
238 self.svn_url = svn_url
239 assert bool(self.commit_user) >= bool(self.commit_pwd)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000240
241 def prepare(self):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000242 # Will checkout if the directory is not present.
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000243 assert self.svn_url
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000244 if not os.path.isdir(self.project_path):
245 logging.info('Checking out %s in %s' %
246 (self.project_name, self.project_path))
247 revision = self._revert()
248 if revision != self._last_seen_revision:
249 logging.info('Updated at revision %d' % revision)
250 self._last_seen_revision = revision
251 return revision
252
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000253 def apply_patch(self, patches, post_processor=None):
254 post_processor = post_processor or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000255 for p in patches:
256 try:
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000257 # It is important to use credentials=False otherwise credentials could
258 # leak in the error message. Credentials are not necessary here for the
259 # following commands anyway.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000260 stdout = ''
261 if p.is_delete:
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000262 stdout += self._check_output_svn(
263 ['delete', p.filename, '--force'], credentials=False)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000264 else:
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000265 # svn add while creating directories otherwise svn add on the
266 # contained files will silently fail.
267 # First, find the root directory that exists.
268 dirname = os.path.dirname(p.filename)
269 dirs_to_create = []
270 while (dirname and
271 not os.path.isdir(os.path.join(self.project_path, dirname))):
272 dirs_to_create.append(dirname)
273 dirname = os.path.dirname(dirname)
274 for dir_to_create in reversed(dirs_to_create):
275 os.mkdir(os.path.join(self.project_path, dir_to_create))
276 stdout += self._check_output_svn(
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000277 ['add', dir_to_create, '--force'], credentials=False)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000278
279 if p.is_binary:
280 with open(os.path.join(self.project_path, p.filename), 'wb') as f:
281 f.write(p.get())
282 else:
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000283 if p.diff_hunks:
284 cmd = ['patch', '-p%s' % p.patchlevel, '--forward', '--force']
285 stdout += subprocess2.check_output(
286 cmd, stdin=p.get(), cwd=self.project_path)
287 elif p.is_new:
288 # There is only a header. Just create the file.
289 open(os.path.join(self.project_path, p.filename), 'w').close()
290 if p.is_new:
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000291 stdout += self._check_output_svn(
292 ['add', p.filename, '--force'], credentials=False)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000293 for prop in p.svn_properties:
294 stdout += self._check_output_svn(
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000295 ['propset', prop[0], prop[1], p.filename], credentials=False)
296 for prop, values in self.svn_config.auto_props.iteritems():
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000297 if fnmatch.fnmatch(p.filename, prop):
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000298 for value in values.split(';'):
299 if '=' not in value:
300 params = [value, '*']
301 else:
302 params = value.split('=', 1)
303 stdout += self._check_output_svn(
304 ['propset'] + params + [p.filename], credentials=False)
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000305 for post in post_processor:
306 post(self, p)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000307 except OSError, e:
308 raise PatchApplicationFailed(p.filename, '%s%s' % (stdout, e))
309 except subprocess.CalledProcessError, e:
310 raise PatchApplicationFailed(
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000311 p.filename,
312 'While running %s;\n%s%s' % (
313 ' '.join(e.cmd), stdout, getattr(e, 'stdout', '')))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000314
315 def commit(self, commit_message, user):
316 logging.info('Committing patch for %s' % user)
317 assert self.commit_user
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000318 assert isinstance(commit_message, unicode)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000319 handle, commit_filename = tempfile.mkstemp(text=True)
320 try:
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000321 # Shouldn't assume default encoding is UTF-8. But really, if you are using
322 # anything else, you are living in another world.
323 os.write(handle, commit_message.encode('utf-8'))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000324 os.close(handle)
325 # When committing, svn won't update the Revision metadata of the checkout,
326 # so if svn commit returns "Committed revision 3.", svn info will still
327 # return "Revision: 2". Since running svn update right after svn commit
328 # creates a race condition with other committers, this code _must_ parse
329 # the output of svn commit and use a regexp to grab the revision number.
330 # Note that "Committed revision N." is localized but subprocess2 forces
331 # LANGUAGE=en.
332 args = ['commit', '--file', commit_filename]
333 # realauthor is parsed by a server-side hook.
334 if user and user != self.commit_user:
335 args.extend(['--with-revprop', 'realauthor=%s' % user])
336 out = self._check_output_svn(args)
337 finally:
338 os.remove(commit_filename)
339 lines = filter(None, out.splitlines())
340 match = re.match(r'^Committed revision (\d+).$', lines[-1])
341 if not match:
342 raise PatchApplicationFailed(
343 None,
344 'Couldn\'t make sense out of svn commit message:\n' + out)
345 return int(match.group(1))
346
347 def _revert(self):
348 """Reverts local modifications or checks out if the directory is not
349 present. Use depot_tools's functionality to do this.
350 """
351 flags = ['--ignore-externals']
352 if not os.path.isdir(self.project_path):
353 logging.info(
354 'Directory %s is not present, checking it out.' % self.project_path)
355 self._check_call_svn(
356 ['checkout', self.svn_url, self.project_path] + flags, cwd=None)
357 else:
358 scm.SVN.Revert(self.project_path)
359 # Revive files that were deleted in scm.SVN.Revert().
360 self._check_call_svn(['update', '--force'] + flags)
361
362 out = self._check_output_svn(['info', '.'])
363 return int(self._parse_svn_info(out, 'revision'))
364
365
366class GitCheckoutBase(CheckoutBase):
367 """Base class for git checkout. Not to be used as-is."""
368 def __init__(self, root_dir, project_name, remote_branch):
369 super(GitCheckoutBase, self).__init__(root_dir, project_name)
370 # There is no reason to not hardcode it.
371 self.remote = 'origin'
372 self.remote_branch = remote_branch
373 self.working_branch = 'working_branch'
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000374
375 def prepare(self):
376 """Resets the git repository in a clean state.
377
378 Checks it out if not present and deletes the working branch.
379 """
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000380 assert self.remote_branch
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000381 assert os.path.isdir(self.project_path)
382 self._check_call_git(['reset', '--hard', '--quiet'])
383 branches, active = self._branches()
384 if active != 'master':
385 self._check_call_git(['checkout', 'master', '--force', '--quiet'])
386 self._check_call_git(['pull', self.remote, self.remote_branch, '--quiet'])
387 if self.working_branch in branches:
388 self._call_git(['branch', '-D', self.working_branch])
389
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000390 def apply_patch(self, patches, post_processor=None):
391 """Applies a patch on 'working_branch' and switch to it.
392
393 Also commits the changes on the local branch.
394
395 Ignores svn properties and raise an exception on unexpected ones.
396 """
397 post_processor = post_processor or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000398 # It this throws, the checkout is corrupted. Maybe worth deleting it and
399 # trying again?
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000400 if self.remote_branch:
401 self._check_call_git(
402 ['checkout', '-b', self.working_branch,
403 '%s/%s' % (self.remote, self.remote_branch), '--quiet'])
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000404 for p in patches:
405 try:
406 stdout = ''
407 if p.is_delete:
408 stdout += self._check_output_git(['rm', p.filename])
409 else:
410 dirname = os.path.dirname(p.filename)
411 full_dir = os.path.join(self.project_path, dirname)
412 if dirname and not os.path.isdir(full_dir):
413 os.makedirs(full_dir)
414 if p.is_binary:
415 with open(os.path.join(self.project_path, p.filename), 'wb') as f:
416 f.write(p.get())
417 stdout += self._check_output_git(['add', p.filename])
418 else:
maruel@chromium.org58fe6622011-06-03 20:59:27 +0000419 # No need to do anything special with p.is_new or if not
420 # p.diff_hunks. git apply manages all that already.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000421 stdout += self._check_output_git(
422 ['apply', '--index', '-p%s' % p.patchlevel], stdin=p.get())
423 for prop in p.svn_properties:
424 # Ignore some known auto-props flags through .subversion/config,
425 # bails out on the other ones.
426 # TODO(maruel): Read ~/.subversion/config and detect the rules that
427 # applies here to figure out if the property will be correctly
428 # handled.
429 if not prop[0] in ('svn:eol-style', 'svn:executable'):
430 raise patch.UnsupportedPatchFormat(
431 p.filename,
432 'Cannot apply svn property %s to file %s.' % (
433 prop[0], p.filename))
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000434 for post in post_processor:
435 post(self, p)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000436 except OSError, e:
437 raise PatchApplicationFailed(p.filename, '%s%s' % (stdout, e))
438 except subprocess.CalledProcessError, e:
439 raise PatchApplicationFailed(
440 p.filename, '%s%s' % (stdout, getattr(e, 'stdout', None)))
441 # Once all the patches are processed and added to the index, commit the
442 # index.
443 self._check_call_git(['commit', '-m', 'Committed patch'])
444 # TODO(maruel): Weirdly enough they don't match, need to investigate.
445 #found_files = self._check_output_git(
446 # ['diff', 'master', '--name-only']).splitlines(False)
447 #assert sorted(patches.filenames) == sorted(found_files), (
448 # sorted(out), sorted(found_files))
449
450 def commit(self, commit_message, user):
451 """Updates the commit message.
452
453 Subclass needs to dcommit or push.
454 """
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000455 assert isinstance(commit_message, unicode)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000456 self._check_call_git(['commit', '--amend', '-m', commit_message])
457 return self._check_output_git(['rev-parse', 'HEAD']).strip()
458
459 def _check_call_git(self, args, **kwargs):
460 kwargs.setdefault('cwd', self.project_path)
461 kwargs.setdefault('stdout', self.VOID)
maruel@chromium.org0bcd1d32011-04-26 15:55:49 +0000462 return subprocess2.check_call_out(['git'] + args, **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000463
464 def _call_git(self, args, **kwargs):
465 """Like check_call but doesn't throw on failure."""
466 kwargs.setdefault('cwd', self.project_path)
467 kwargs.setdefault('stdout', self.VOID)
468 return subprocess2.call(['git'] + args, **kwargs)
469
470 def _check_output_git(self, args, **kwargs):
471 kwargs.setdefault('cwd', self.project_path)
472 return subprocess2.check_output(['git'] + args, **kwargs)
473
474 def _branches(self):
475 """Returns the list of branches and the active one."""
476 out = self._check_output_git(['branch']).splitlines(False)
477 branches = [l[2:] for l in out]
478 active = None
479 for l in out:
480 if l.startswith('*'):
481 active = l[2:]
482 break
483 return branches, active
484
485
486class GitSvnCheckoutBase(GitCheckoutBase, SvnMixIn):
487 """Base class for git-svn checkout. Not to be used as-is."""
488 def __init__(self,
489 root_dir, project_name, remote_branch,
490 commit_user, commit_pwd,
491 svn_url, trunk):
492 """trunk is optional."""
493 super(GitSvnCheckoutBase, self).__init__(
494 root_dir, project_name + '.git', remote_branch)
495 self.commit_user = commit_user
496 self.commit_pwd = commit_pwd
497 # svn_url in this case is the root of the svn repository.
498 self.svn_url = svn_url
499 self.trunk = trunk
500 assert bool(self.commit_user) >= bool(self.commit_pwd)
501 assert self.svn_url
502 assert self.trunk
503 self._cache_svn_auth()
504
505 def prepare(self):
506 """Resets the git repository in a clean state."""
507 self._check_call_git(['reset', '--hard', '--quiet'])
508 branches, active = self._branches()
509 if active != 'master':
510 if not 'master' in branches:
511 self._check_call_git(
512 ['checkout', '--quiet', '-b', 'master',
513 '%s/%s' % (self.remote, self.remote_branch)])
514 else:
515 self._check_call_git(['checkout', 'master', '--force', '--quiet'])
516 # git svn rebase --quiet --quiet doesn't work, use two steps to silence it.
517 self._check_call_git_svn(['fetch', '--quiet', '--quiet'])
518 self._check_call_git(
519 ['rebase', '--quiet', '--quiet',
520 '%s/%s' % (self.remote, self.remote_branch)])
521 if self.working_branch in branches:
522 self._call_git(['branch', '-D', self.working_branch])
523 return int(self._git_svn_info('revision'))
524
525 def _git_svn_info(self, key):
526 """Calls git svn info. This doesn't support nor need --config-dir."""
527 return self._parse_svn_info(self._check_output_git(['svn', 'info']), key)
528
529 def commit(self, commit_message, user):
530 """Commits a patch."""
531 logging.info('Committing patch for %s' % user)
532 # Fix the commit message and author. It returns the git hash, which we
533 # ignore unless it's None.
534 if not super(GitSvnCheckoutBase, self).commit(commit_message, user):
535 return None
536 # TODO(maruel): git-svn ignores --config-dir as of git-svn version 1.7.4 and
537 # doesn't support --with-revprop.
538 # Either learn perl and upstream or suck it.
539 kwargs = {}
540 if self.commit_pwd:
541 kwargs['stdin'] = self.commit_pwd + '\n'
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000542 kwargs['stderr'] = subprocess2.STDOUT
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000543 self._check_call_git_svn(
544 ['dcommit', '--rmdir', '--find-copies-harder',
545 '--username', self.commit_user],
546 **kwargs)
547 revision = int(self._git_svn_info('revision'))
548 return revision
549
550 def _cache_svn_auth(self):
551 """Caches the svn credentials. It is necessary since git-svn doesn't prompt
552 for it."""
553 if not self.commit_user or not self.commit_pwd:
554 return
555 # Use capture to lower noise in logs.
556 self._check_output_svn(['ls', self.svn_url], cwd=None)
557
558 def _check_call_git_svn(self, args, **kwargs):
559 """Handles svn authentication while calling git svn."""
560 args = ['svn'] + args
561 if not self.svn_config.default:
562 args.extend(['--config-dir', self.svn_config.svn_config_dir])
563 return self._check_call_git(args, **kwargs)
564
565 def _get_revision(self):
566 revision = int(self._git_svn_info('revision'))
567 if revision != self._last_seen_revision:
568 logging.info('Updated at revision %d' % revision)
569 self._last_seen_revision = revision
570 return revision
571
572
573class GitSvnPremadeCheckout(GitSvnCheckoutBase):
574 """Manages a git-svn clone made out from an initial git-svn seed.
575
576 This class is very similar to GitSvnCheckout but is faster to bootstrap
577 because it starts right off with an existing git-svn clone.
578 """
579 def __init__(self,
580 root_dir, project_name, remote_branch,
581 commit_user, commit_pwd,
582 svn_url, trunk, git_url):
583 super(GitSvnPremadeCheckout, self).__init__(
584 root_dir, project_name, remote_branch,
585 commit_user, commit_pwd,
586 svn_url, trunk)
587 self.git_url = git_url
588 assert self.git_url
589
590 def prepare(self):
591 """Creates the initial checkout for the repo."""
592 if not os.path.isdir(self.project_path):
593 logging.info('Checking out %s in %s' %
594 (self.project_name, self.project_path))
595 assert self.remote == 'origin'
596 # self.project_path doesn't exist yet.
597 self._check_call_git(
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000598 ['clone', self.git_url, self.project_name, '--quiet'],
599 cwd=self.root_dir,
600 stderr=subprocess2.STDOUT)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000601 try:
602 configured_svn_url = self._check_output_git(
603 ['config', 'svn-remote.svn.url']).strip()
604 except subprocess.CalledProcessError:
605 configured_svn_url = ''
606
607 if configured_svn_url.strip() != self.svn_url:
608 self._check_call_git_svn(
609 ['init',
610 '--prefix', self.remote + '/',
611 '-T', self.trunk,
612 self.svn_url])
613 self._check_call_git_svn(['fetch'])
614 super(GitSvnPremadeCheckout, self).prepare()
615 return self._get_revision()
616
617
618class GitSvnCheckout(GitSvnCheckoutBase):
619 """Manages a git-svn clone.
620
621 Using git-svn hides some of the complexity of using a svn checkout.
622 """
623 def __init__(self,
624 root_dir, project_name,
625 commit_user, commit_pwd,
626 svn_url, trunk):
627 super(GitSvnCheckout, self).__init__(
628 root_dir, project_name, 'trunk',
629 commit_user, commit_pwd,
630 svn_url, trunk)
631
632 def prepare(self):
633 """Creates the initial checkout for the repo."""
634 if not os.path.isdir(self.project_path):
635 logging.info('Checking out %s in %s' %
636 (self.project_name, self.project_path))
637 # TODO: Create a shallow clone.
638 # self.project_path doesn't exist yet.
639 self._check_call_git_svn(
640 ['clone',
641 '--prefix', self.remote + '/',
642 '-T', self.trunk,
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000643 self.svn_url, self.project_path,
644 '--quiet'],
645 cwd=self.root_dir,
646 stderr=subprocess2.STDOUT)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000647 super(GitSvnCheckout, self).prepare()
648 return self._get_revision()
649
650
651class ReadOnlyCheckout(object):
652 """Converts a checkout into a read-only one."""
653 def __init__(self, checkout):
654 self.checkout = checkout
655
656 def prepare(self):
657 return self.checkout.prepare()
658
659 def get_settings(self, key):
660 return self.checkout.get_settings(key)
661
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000662 def apply_patch(self, patches, post_processor=None):
663 return self.checkout.apply_patch(patches, post_processor)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000664
665 def commit(self, message, user): # pylint: disable=R0201
666 logging.info('Would have committed for %s with message: %s' % (
667 user, message))
668 return 'FAKE'
669
670 @property
671 def project_name(self):
672 return self.checkout.project_name
673
674 @property
675 def project_path(self):
676 return self.checkout.project_path