blob: a5cf5801f1a8251e1b26f971c7a2dbfb1ba6a486 [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:
130 stdout = subprocess2.check_output(
131 ['patch', '-p%s' % p.patchlevel],
132 stdin=p.get(),
133 cwd=self.project_path)
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000134 for post in post_processor:
135 post(self, p)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000136 except OSError, e:
137 raise PatchApplicationFailed(p.filename, '%s%s' % (stdout, e))
138 except subprocess.CalledProcessError, e:
139 raise PatchApplicationFailed(
140 p.filename, '%s%s' % (stdout, getattr(e, 'stdout', None)))
141
142 def commit(self, commit_message, user):
143 """Stubbed out."""
144 raise NotImplementedError('RawCheckout can\'t commit')
145
146
147class SvnConfig(object):
148 """Parses a svn configuration file."""
149 def __init__(self, svn_config_dir=None):
150 self.svn_config_dir = svn_config_dir
151 self.default = not bool(self.svn_config_dir)
152 if not self.svn_config_dir:
153 if sys.platform == 'win32':
154 self.svn_config_dir = os.path.join(os.environ['APPDATA'], 'Subversion')
155 else:
156 self.svn_config_dir = os.path.join(os.environ['HOME'], '.subversion')
157 svn_config_file = os.path.join(self.svn_config_dir, 'config')
158 parser = ConfigParser.SafeConfigParser()
159 if os.path.isfile(svn_config_file):
160 parser.read(svn_config_file)
161 else:
162 parser.add_section('auto-props')
163 self.auto_props = dict(parser.items('auto-props'))
164
165
166class SvnMixIn(object):
167 """MixIn class to add svn commands common to both svn and git-svn clients."""
168 # These members need to be set by the subclass.
169 commit_user = None
170 commit_pwd = None
171 svn_url = None
172 project_path = None
173 # Override at class level when necessary. If used, --non-interactive is
174 # implied.
175 svn_config = SvnConfig()
176 # Set to True when non-interactivity is necessary but a custom subversion
177 # configuration directory is not necessary.
178 non_interactive = False
179
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000180 def _add_svn_flags(self, args, non_interactive, credentials=True):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000181 args = ['svn'] + args
182 if not self.svn_config.default:
183 args.extend(['--config-dir', self.svn_config.svn_config_dir])
184 if not self.svn_config.default or self.non_interactive or non_interactive:
185 args.append('--non-interactive')
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000186 if credentials:
187 if self.commit_user:
188 args.extend(['--username', self.commit_user])
189 if self.commit_pwd:
190 args.extend(['--password', self.commit_pwd])
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000191 return args
192
193 def _check_call_svn(self, args, **kwargs):
194 """Runs svn and throws an exception if the command failed."""
195 kwargs.setdefault('cwd', self.project_path)
196 kwargs.setdefault('stdout', self.VOID)
maruel@chromium.org0bcd1d32011-04-26 15:55:49 +0000197 return subprocess2.check_call_out(
198 self._add_svn_flags(args, False), **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000199
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000200 def _check_output_svn(self, args, credentials=True, **kwargs):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000201 """Runs svn and throws an exception if the command failed.
202
203 Returns the output.
204 """
205 kwargs.setdefault('cwd', self.project_path)
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000206 return subprocess2.check_output(
207 self._add_svn_flags(args, True, credentials), **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000208
209 @staticmethod
210 def _parse_svn_info(output, key):
211 """Returns value for key from svn info output.
212
213 Case insensitive.
214 """
215 values = {}
216 key = key.lower()
217 for line in output.splitlines(False):
218 if not line:
219 continue
220 k, v = line.split(':', 1)
221 k = k.strip().lower()
222 v = v.strip()
223 assert not k in values
224 values[k] = v
225 return values.get(key, None)
226
227
228class SvnCheckout(CheckoutBase, SvnMixIn):
229 """Manages a subversion checkout."""
230 def __init__(self, root_dir, project_name, commit_user, commit_pwd, svn_url):
231 super(SvnCheckout, self).__init__(root_dir, project_name)
232 self.commit_user = commit_user
233 self.commit_pwd = commit_pwd
234 self.svn_url = svn_url
235 assert bool(self.commit_user) >= bool(self.commit_pwd)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000236
237 def prepare(self):
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000238 # Will checkout if the directory is not present.
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000239 assert self.svn_url
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000240 if not os.path.isdir(self.project_path):
241 logging.info('Checking out %s in %s' %
242 (self.project_name, self.project_path))
243 revision = self._revert()
244 if revision != self._last_seen_revision:
245 logging.info('Updated at revision %d' % revision)
246 self._last_seen_revision = revision
247 return revision
248
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000249 def apply_patch(self, patches, post_processor=None):
250 post_processor = post_processor or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000251 for p in patches:
252 try:
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000253 # It is important to use credentials=False otherwise credentials could
254 # leak in the error message. Credentials are not necessary here for the
255 # following commands anyway.
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000256 stdout = ''
257 if p.is_delete:
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000258 stdout += self._check_output_svn(
259 ['delete', p.filename, '--force'], credentials=False)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000260 else:
261 new = not os.path.exists(p.filename)
262
263 # svn add while creating directories otherwise svn add on the
264 # contained files will silently fail.
265 # First, find the root directory that exists.
266 dirname = os.path.dirname(p.filename)
267 dirs_to_create = []
268 while (dirname and
269 not os.path.isdir(os.path.join(self.project_path, dirname))):
270 dirs_to_create.append(dirname)
271 dirname = os.path.dirname(dirname)
272 for dir_to_create in reversed(dirs_to_create):
273 os.mkdir(os.path.join(self.project_path, dir_to_create))
274 stdout += self._check_output_svn(
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000275 ['add', dir_to_create, '--force'], credentials=False)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000276
277 if p.is_binary:
278 with open(os.path.join(self.project_path, p.filename), 'wb') as f:
279 f.write(p.get())
280 else:
281 cmd = ['patch', '-p%s' % p.patchlevel, '--forward', '--force']
282 stdout += subprocess2.check_output(
283 cmd, stdin=p.get(), cwd=self.project_path)
284 if new:
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000285 stdout += self._check_output_svn(
286 ['add', p.filename, '--force'], credentials=False)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000287 for prop in p.svn_properties:
288 stdout += self._check_output_svn(
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000289 ['propset', prop[0], prop[1], p.filename], credentials=False)
290 for prop, values in self.svn_config.auto_props.iteritems():
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000291 if fnmatch.fnmatch(p.filename, prop):
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000292 for value in values.split(';'):
293 if '=' not in value:
294 params = [value, '*']
295 else:
296 params = value.split('=', 1)
297 stdout += self._check_output_svn(
298 ['propset'] + params + [p.filename], credentials=False)
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000299 for post in post_processor:
300 post(self, p)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000301 except OSError, e:
302 raise PatchApplicationFailed(p.filename, '%s%s' % (stdout, e))
303 except subprocess.CalledProcessError, e:
304 raise PatchApplicationFailed(
maruel@chromium.org9842a0c2011-05-30 20:41:54 +0000305 p.filename,
306 'While running %s;\n%s%s' % (
307 ' '.join(e.cmd), stdout, getattr(e, 'stdout', '')))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000308
309 def commit(self, commit_message, user):
310 logging.info('Committing patch for %s' % user)
311 assert self.commit_user
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000312 assert isinstance(commit_message, unicode)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000313 handle, commit_filename = tempfile.mkstemp(text=True)
314 try:
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000315 # Shouldn't assume default encoding is UTF-8. But really, if you are using
316 # anything else, you are living in another world.
317 os.write(handle, commit_message.encode('utf-8'))
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000318 os.close(handle)
319 # When committing, svn won't update the Revision metadata of the checkout,
320 # so if svn commit returns "Committed revision 3.", svn info will still
321 # return "Revision: 2". Since running svn update right after svn commit
322 # creates a race condition with other committers, this code _must_ parse
323 # the output of svn commit and use a regexp to grab the revision number.
324 # Note that "Committed revision N." is localized but subprocess2 forces
325 # LANGUAGE=en.
326 args = ['commit', '--file', commit_filename]
327 # realauthor is parsed by a server-side hook.
328 if user and user != self.commit_user:
329 args.extend(['--with-revprop', 'realauthor=%s' % user])
330 out = self._check_output_svn(args)
331 finally:
332 os.remove(commit_filename)
333 lines = filter(None, out.splitlines())
334 match = re.match(r'^Committed revision (\d+).$', lines[-1])
335 if not match:
336 raise PatchApplicationFailed(
337 None,
338 'Couldn\'t make sense out of svn commit message:\n' + out)
339 return int(match.group(1))
340
341 def _revert(self):
342 """Reverts local modifications or checks out if the directory is not
343 present. Use depot_tools's functionality to do this.
344 """
345 flags = ['--ignore-externals']
346 if not os.path.isdir(self.project_path):
347 logging.info(
348 'Directory %s is not present, checking it out.' % self.project_path)
349 self._check_call_svn(
350 ['checkout', self.svn_url, self.project_path] + flags, cwd=None)
351 else:
352 scm.SVN.Revert(self.project_path)
353 # Revive files that were deleted in scm.SVN.Revert().
354 self._check_call_svn(['update', '--force'] + flags)
355
356 out = self._check_output_svn(['info', '.'])
357 return int(self._parse_svn_info(out, 'revision'))
358
359
360class GitCheckoutBase(CheckoutBase):
361 """Base class for git checkout. Not to be used as-is."""
362 def __init__(self, root_dir, project_name, remote_branch):
363 super(GitCheckoutBase, self).__init__(root_dir, project_name)
364 # There is no reason to not hardcode it.
365 self.remote = 'origin'
366 self.remote_branch = remote_branch
367 self.working_branch = 'working_branch'
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000368
369 def prepare(self):
370 """Resets the git repository in a clean state.
371
372 Checks it out if not present and deletes the working branch.
373 """
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000374 assert self.remote_branch
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000375 assert os.path.isdir(self.project_path)
376 self._check_call_git(['reset', '--hard', '--quiet'])
377 branches, active = self._branches()
378 if active != 'master':
379 self._check_call_git(['checkout', 'master', '--force', '--quiet'])
380 self._check_call_git(['pull', self.remote, self.remote_branch, '--quiet'])
381 if self.working_branch in branches:
382 self._call_git(['branch', '-D', self.working_branch])
383
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000384 def apply_patch(self, patches, post_processor=None):
385 """Applies a patch on 'working_branch' and switch to it.
386
387 Also commits the changes on the local branch.
388
389 Ignores svn properties and raise an exception on unexpected ones.
390 """
391 post_processor = post_processor or []
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000392 # It this throws, the checkout is corrupted. Maybe worth deleting it and
393 # trying again?
maruel@chromium.org3cdb7f32011-05-05 16:37:24 +0000394 if self.remote_branch:
395 self._check_call_git(
396 ['checkout', '-b', self.working_branch,
397 '%s/%s' % (self.remote, self.remote_branch), '--quiet'])
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000398 for p in patches:
399 try:
400 stdout = ''
401 if p.is_delete:
402 stdout += self._check_output_git(['rm', p.filename])
403 else:
404 dirname = os.path.dirname(p.filename)
405 full_dir = os.path.join(self.project_path, dirname)
406 if dirname and not os.path.isdir(full_dir):
407 os.makedirs(full_dir)
408 if p.is_binary:
409 with open(os.path.join(self.project_path, p.filename), 'wb') as f:
410 f.write(p.get())
411 stdout += self._check_output_git(['add', p.filename])
412 else:
413 stdout += self._check_output_git(
414 ['apply', '--index', '-p%s' % p.patchlevel], stdin=p.get())
415 for prop in p.svn_properties:
416 # Ignore some known auto-props flags through .subversion/config,
417 # bails out on the other ones.
418 # TODO(maruel): Read ~/.subversion/config and detect the rules that
419 # applies here to figure out if the property will be correctly
420 # handled.
421 if not prop[0] in ('svn:eol-style', 'svn:executable'):
422 raise patch.UnsupportedPatchFormat(
423 p.filename,
424 'Cannot apply svn property %s to file %s.' % (
425 prop[0], p.filename))
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000426 for post in post_processor:
427 post(self, p)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000428 except OSError, e:
429 raise PatchApplicationFailed(p.filename, '%s%s' % (stdout, e))
430 except subprocess.CalledProcessError, e:
431 raise PatchApplicationFailed(
432 p.filename, '%s%s' % (stdout, getattr(e, 'stdout', None)))
433 # Once all the patches are processed and added to the index, commit the
434 # index.
435 self._check_call_git(['commit', '-m', 'Committed patch'])
436 # TODO(maruel): Weirdly enough they don't match, need to investigate.
437 #found_files = self._check_output_git(
438 # ['diff', 'master', '--name-only']).splitlines(False)
439 #assert sorted(patches.filenames) == sorted(found_files), (
440 # sorted(out), sorted(found_files))
441
442 def commit(self, commit_message, user):
443 """Updates the commit message.
444
445 Subclass needs to dcommit or push.
446 """
maruel@chromium.org1bf50972011-05-05 19:57:21 +0000447 assert isinstance(commit_message, unicode)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000448 self._check_call_git(['commit', '--amend', '-m', commit_message])
449 return self._check_output_git(['rev-parse', 'HEAD']).strip()
450
451 def _check_call_git(self, args, **kwargs):
452 kwargs.setdefault('cwd', self.project_path)
453 kwargs.setdefault('stdout', self.VOID)
maruel@chromium.org0bcd1d32011-04-26 15:55:49 +0000454 return subprocess2.check_call_out(['git'] + args, **kwargs)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000455
456 def _call_git(self, args, **kwargs):
457 """Like check_call but doesn't throw on failure."""
458 kwargs.setdefault('cwd', self.project_path)
459 kwargs.setdefault('stdout', self.VOID)
460 return subprocess2.call(['git'] + args, **kwargs)
461
462 def _check_output_git(self, args, **kwargs):
463 kwargs.setdefault('cwd', self.project_path)
464 return subprocess2.check_output(['git'] + args, **kwargs)
465
466 def _branches(self):
467 """Returns the list of branches and the active one."""
468 out = self._check_output_git(['branch']).splitlines(False)
469 branches = [l[2:] for l in out]
470 active = None
471 for l in out:
472 if l.startswith('*'):
473 active = l[2:]
474 break
475 return branches, active
476
477
478class GitSvnCheckoutBase(GitCheckoutBase, SvnMixIn):
479 """Base class for git-svn checkout. Not to be used as-is."""
480 def __init__(self,
481 root_dir, project_name, remote_branch,
482 commit_user, commit_pwd,
483 svn_url, trunk):
484 """trunk is optional."""
485 super(GitSvnCheckoutBase, self).__init__(
486 root_dir, project_name + '.git', remote_branch)
487 self.commit_user = commit_user
488 self.commit_pwd = commit_pwd
489 # svn_url in this case is the root of the svn repository.
490 self.svn_url = svn_url
491 self.trunk = trunk
492 assert bool(self.commit_user) >= bool(self.commit_pwd)
493 assert self.svn_url
494 assert self.trunk
495 self._cache_svn_auth()
496
497 def prepare(self):
498 """Resets the git repository in a clean state."""
499 self._check_call_git(['reset', '--hard', '--quiet'])
500 branches, active = self._branches()
501 if active != 'master':
502 if not 'master' in branches:
503 self._check_call_git(
504 ['checkout', '--quiet', '-b', 'master',
505 '%s/%s' % (self.remote, self.remote_branch)])
506 else:
507 self._check_call_git(['checkout', 'master', '--force', '--quiet'])
508 # git svn rebase --quiet --quiet doesn't work, use two steps to silence it.
509 self._check_call_git_svn(['fetch', '--quiet', '--quiet'])
510 self._check_call_git(
511 ['rebase', '--quiet', '--quiet',
512 '%s/%s' % (self.remote, self.remote_branch)])
513 if self.working_branch in branches:
514 self._call_git(['branch', '-D', self.working_branch])
515 return int(self._git_svn_info('revision'))
516
517 def _git_svn_info(self, key):
518 """Calls git svn info. This doesn't support nor need --config-dir."""
519 return self._parse_svn_info(self._check_output_git(['svn', 'info']), key)
520
521 def commit(self, commit_message, user):
522 """Commits a patch."""
523 logging.info('Committing patch for %s' % user)
524 # Fix the commit message and author. It returns the git hash, which we
525 # ignore unless it's None.
526 if not super(GitSvnCheckoutBase, self).commit(commit_message, user):
527 return None
528 # TODO(maruel): git-svn ignores --config-dir as of git-svn version 1.7.4 and
529 # doesn't support --with-revprop.
530 # Either learn perl and upstream or suck it.
531 kwargs = {}
532 if self.commit_pwd:
533 kwargs['stdin'] = self.commit_pwd + '\n'
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000534 kwargs['stderr'] = subprocess2.STDOUT
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000535 self._check_call_git_svn(
536 ['dcommit', '--rmdir', '--find-copies-harder',
537 '--username', self.commit_user],
538 **kwargs)
539 revision = int(self._git_svn_info('revision'))
540 return revision
541
542 def _cache_svn_auth(self):
543 """Caches the svn credentials. It is necessary since git-svn doesn't prompt
544 for it."""
545 if not self.commit_user or not self.commit_pwd:
546 return
547 # Use capture to lower noise in logs.
548 self._check_output_svn(['ls', self.svn_url], cwd=None)
549
550 def _check_call_git_svn(self, args, **kwargs):
551 """Handles svn authentication while calling git svn."""
552 args = ['svn'] + args
553 if not self.svn_config.default:
554 args.extend(['--config-dir', self.svn_config.svn_config_dir])
555 return self._check_call_git(args, **kwargs)
556
557 def _get_revision(self):
558 revision = int(self._git_svn_info('revision'))
559 if revision != self._last_seen_revision:
560 logging.info('Updated at revision %d' % revision)
561 self._last_seen_revision = revision
562 return revision
563
564
565class GitSvnPremadeCheckout(GitSvnCheckoutBase):
566 """Manages a git-svn clone made out from an initial git-svn seed.
567
568 This class is very similar to GitSvnCheckout but is faster to bootstrap
569 because it starts right off with an existing git-svn clone.
570 """
571 def __init__(self,
572 root_dir, project_name, remote_branch,
573 commit_user, commit_pwd,
574 svn_url, trunk, git_url):
575 super(GitSvnPremadeCheckout, self).__init__(
576 root_dir, project_name, remote_branch,
577 commit_user, commit_pwd,
578 svn_url, trunk)
579 self.git_url = git_url
580 assert self.git_url
581
582 def prepare(self):
583 """Creates the initial checkout for the repo."""
584 if not os.path.isdir(self.project_path):
585 logging.info('Checking out %s in %s' %
586 (self.project_name, self.project_path))
587 assert self.remote == 'origin'
588 # self.project_path doesn't exist yet.
589 self._check_call_git(
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000590 ['clone', self.git_url, self.project_name, '--quiet'],
591 cwd=self.root_dir,
592 stderr=subprocess2.STDOUT)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000593 try:
594 configured_svn_url = self._check_output_git(
595 ['config', 'svn-remote.svn.url']).strip()
596 except subprocess.CalledProcessError:
597 configured_svn_url = ''
598
599 if configured_svn_url.strip() != self.svn_url:
600 self._check_call_git_svn(
601 ['init',
602 '--prefix', self.remote + '/',
603 '-T', self.trunk,
604 self.svn_url])
605 self._check_call_git_svn(['fetch'])
606 super(GitSvnPremadeCheckout, self).prepare()
607 return self._get_revision()
608
609
610class GitSvnCheckout(GitSvnCheckoutBase):
611 """Manages a git-svn clone.
612
613 Using git-svn hides some of the complexity of using a svn checkout.
614 """
615 def __init__(self,
616 root_dir, project_name,
617 commit_user, commit_pwd,
618 svn_url, trunk):
619 super(GitSvnCheckout, self).__init__(
620 root_dir, project_name, 'trunk',
621 commit_user, commit_pwd,
622 svn_url, trunk)
623
624 def prepare(self):
625 """Creates the initial checkout for the repo."""
626 if not os.path.isdir(self.project_path):
627 logging.info('Checking out %s in %s' %
628 (self.project_name, self.project_path))
629 # TODO: Create a shallow clone.
630 # self.project_path doesn't exist yet.
631 self._check_call_git_svn(
632 ['clone',
633 '--prefix', self.remote + '/',
634 '-T', self.trunk,
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000635 self.svn_url, self.project_path,
636 '--quiet'],
637 cwd=self.root_dir,
638 stderr=subprocess2.STDOUT)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000639 super(GitSvnCheckout, self).prepare()
640 return self._get_revision()
641
642
643class ReadOnlyCheckout(object):
644 """Converts a checkout into a read-only one."""
645 def __init__(self, checkout):
646 self.checkout = checkout
647
648 def prepare(self):
649 return self.checkout.prepare()
650
651 def get_settings(self, key):
652 return self.checkout.get_settings(key)
653
maruel@chromium.org8a1396c2011-04-22 00:14:24 +0000654 def apply_patch(self, patches, post_processor=None):
655 return self.checkout.apply_patch(patches, post_processor)
maruel@chromium.orgdfaecd22011-04-21 00:33:31 +0000656
657 def commit(self, message, user): # pylint: disable=R0201
658 logging.info('Would have committed for %s with message: %s' % (
659 user, message))
660 return 'FAKE'
661
662 @property
663 def project_name(self):
664 return self.checkout.project_name
665
666 @property
667 def project_path(self):
668 return self.checkout.project_path