Use common parent id functions.
[jelmer/subvertpy.git] / __init__.py
1 # Copyright (C) 2005-2007 Jelmer Vernooij <jelmer@samba.org>
2
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 3 of the License, or
6 # (at your option) any later version.
7
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
17 """Support for Subversion branches
18
19 Bazaar can be used with Subversion branches through the bzr-svn plugin.
20
21 Most Bazaar commands should work fine with Subversion branches. The following 
22 commands at the moment do not:
23
24  - bzr uncommit
25  - bzr push --overwrite
26
27 bzr-svn also adds two new commands to Bazaar:
28
29  - bzr svn-import
30  - bzr dpush
31
32 For more information about bzr-svn, see the bzr-svn FAQ.
33
34 """
35 import bzrlib
36 from bzrlib import log
37 from bzrlib.bzrdir import BzrDirFormat, format_registry
38 from bzrlib.errors import BzrError
39 from bzrlib.commands import Command, register_command, display_command, Option
40 from bzrlib.help_topics import topic_registry
41 from bzrlib.revisionspec import SPEC_TYPES
42 from bzrlib.trace import warning, mutter
43 from bzrlib.transport import register_lazy_transport, register_transport_proto
44
45 import os
46
47 # versions ending in 'exp' mean experimental mappings
48 # versions ending in 'dev' mean development version
49 # versions ending in 'final' mean release (well tested, etc)
50 version_info = (0, 4, 11, 'exp', 0)
51
52 if version_info[3] == 'final':
53     version_string = '%d.%d.%d' % version_info[:3]
54 else:
55     version_string = '%d.%d.%d%s%d' % version_info
56 __version__ = version_string
57
58 COMPATIBLE_BZR_VERSIONS = [(1, 6)]
59
60 def check_bzrlib_version(desired):
61     """Check that bzrlib is compatible.
62
63     If version is < all compatible version, assume incompatible.
64     If version is compatible version + 1, assume compatible, with deprecations
65     Otherwise, assume incompatible.
66     """
67     import bzrlib
68     bzrlib_version = bzrlib.version_info[:2]
69     if (bzrlib_version in desired or 
70         ((bzrlib_version[0], bzrlib_version[1]-1) in desired and 
71          bzrlib.version_info[3] in ('dev', 'exp'))):
72         return
73     if bzrlib_version < desired[0]:
74         raise BzrError('Installed bzr version %s is too old to be used with bzr-svn, at least %s.%s required' % (bzrlib.__version__, desired[0][0], desired[0][1]))
75     else:
76         warning('bzr-svn is not up to date with installed bzr version %s.'
77                 ' \nThere should be a newer version of bzr-svn available.',
78                 bzrlib.__version__)
79         if not (bzrlib_version[0], bzrlib_version[1]-1) in desired:
80             raise BzrError('Version mismatch')
81
82 def check_subversion_version():
83     """Check that Subversion is compatible.
84
85     """
86     def check_mtime(m):
87         (base, _) = os.path.splitext(m.__file__)
88         c_file = "%s.c" % base
89         if not os.path.exists(c_file):
90             return True
91         if os.path.getmtime(m.__file__) < os.path.getmtime(c_file):
92             return False
93         return True
94     try:
95         from bzrlib.plugins.svn import client, ra, repos, wc
96         for x in client, ra, repos, wc:
97             if not check_mtime(x):
98                 warning("bzr-svn extensions are outdated and need to be rebuilt")
99                 break
100     except ImportError:
101         warning("Unable to load bzr-svn extensions - did you build it?")
102         raise
103     ra_version = ra.version()
104     if (ra_version[0] >= 5 and getattr(ra, 'SVN_REVISION', None) and 27729 <= ra.SVN_REVISION < 31470):
105         warning('Installed Subversion has buggy svn.ra.get_log() implementation, please install newer.')
106
107     mutter("bzr-svn: using Subversion %d.%d.%d (%s)" % ra_version)
108
109
110 def check_rebase_version(min_version):
111     """Check what version of bzr-rebase is installed.
112
113     Raises an exception when the version installed is older than 
114     min_version.
115
116     :raises RebaseNotPresent: Raised if bzr-rebase is not installed or too old.
117     """
118     from bzrlib.plugins.svn.errors import RebaseNotPresent
119     try:
120         from bzrlib.plugins.rebase import version_info as rebase_version_info
121         if rebase_version_info[:2] < min_version:
122             raise RebaseNotPresent("Version %r present, at least %r required" 
123                                    % (rebase_version_info, min_version))
124     except ImportError, e:
125         raise RebaseNotPresent(e)
126
127
128 check_subversion_version()
129
130 from bzrlib.plugins.svn import format, revspec
131
132 register_transport_proto('svn+ssh://', 
133     help="Access using the Subversion smart server tunneled over SSH.")
134 register_transport_proto('svn+file://', 
135     help="Access of local Subversion repositories.")
136 register_transport_proto('svn+http://',
137     help="Access of Subversion smart servers over HTTP.")
138 register_transport_proto('svn+https://',
139     help="Access of Subversion smart servers over secure HTTP.")
140 register_transport_proto('svn://', 
141     help="Access using the Subversion smart server.")
142 register_lazy_transport('svn://', 'bzrlib.plugins.svn.transport', 
143                         'SvnRaTransport')
144 register_lazy_transport('svn+', 'bzrlib.plugins.svn.transport', 
145                         'SvnRaTransport')
146 topic_registry.register_lazy('svn-branching-schemes', 
147                              'bzrlib.plugins.svn.mapping3.scheme',
148                              'help_schemes', 'Subversion branching schemes')
149
150 BzrDirFormat.register_control_format(format.SvnRemoteFormat)
151 BzrDirFormat.register_control_format(format.SvnWorkingTreeDirFormat)
152 format_registry.register("subversion", format.SvnRemoteFormat, 
153                          "Subversion repository. ", 
154                          native=False)
155 format_registry.register("subversion-wc", format.SvnWorkingTreeDirFormat, 
156                          "Subversion working copy. ", 
157                          native=False, hidden=True)
158 SPEC_TYPES.append(revspec.RevisionSpec_svn)
159
160 if getattr(log, "properties_handler_registry", None) is not None:
161     log.properties_handler_registry.register_lazy("subversion",
162                                                   "bzrlib.plugins.svn.log",
163                                                   "show_subversion_properties")
164
165 versions_checked = False
166 def lazy_check_versions():
167     """Check whether all dependencies have the right versions.
168     
169     :note: Only checks once, caches the result."""
170     global versions_checked
171     if versions_checked:
172         return
173     versions_checked = True
174     check_bzrlib_version(COMPATIBLE_BZR_VERSIONS)
175
176 optimizers_registered = False
177 def lazy_register_optimizers():
178     """Register optimizers for fetching between Subversion and Bazaar 
179     repositories.
180     
181     :note: Only registers on the first call."""
182     global optimizers_registered
183     if optimizers_registered:
184         return
185     from bzrlib.repository import InterRepository
186     from bzrlib.plugins.svn import commit, fetch
187     optimizers_registered = True
188     InterRepository.register_optimiser(fetch.InterFromSvnRepository)
189     InterRepository.register_optimiser(commit.InterToSvnRepository)
190
191
192 def get_scheme(schemename):
193     """Parse scheme identifier and return a branching scheme.
194     
195     :param schemename: Name of the scheme to retrieve.
196     """
197     if isinstance(schemename, unicode):
198         schemename = schemename.encode("ascii")
199     from bzrlib.plugins.svn.mapping3.scheme import BranchingScheme
200     from bzrlib.errors import BzrCommandError
201     
202     ret = BranchingScheme.find_scheme(schemename)
203     if ret is None:
204         raise BzrCommandError('No such branching scheme %r' % schemename)
205     return ret
206
207
208 class cmd_svn_import(Command):
209     """Convert a Subversion repository to a Bazaar repository.
210     
211     To save disk space, only branches will be created by default 
212     (no working trees). To create a tree for a branch, run "bzr co" in 
213     it.
214     """
215     takes_args = ['from_location', 'to_location?']
216     takes_options = [Option('trees', help='Create working trees.'),
217                      Option('standalone', help='Create standalone branches.'),
218                      Option('all', 
219                          help='Convert all revisions, even those not in '
220                               'current branch history (forbids --standalone).'),
221                      Option('scheme', type=get_scheme,
222                          help='Branching scheme (none, trunk, etc). '
223                               'Default: auto.'),
224                      Option('prefix', type=str, 
225                          help='Only consider branches of which path starts '
226                               'with prefix.')
227                     ]
228
229     @display_command
230     def run(self, from_location, to_location=None, trees=False, 
231             standalone=False, scheme=None, all=False, prefix=None):
232         from bzrlib.branch import Branch
233         from bzrlib.bzrdir import BzrDir
234         from bzrlib.errors import BzrCommandError, NoRepositoryPresent, NotBranchError
235         from bzrlib import urlutils
236         from bzrlib.plugins.svn.convert import convert_repository
237         from bzrlib.plugins.svn.mapping3 import repository_guess_scheme
238         from bzrlib.plugins.svn.repository import SvnRepository
239         import os
240
241         if to_location is None:
242             to_location = os.path.basename(from_location.rstrip("/\\"))
243
244         if all:
245             # All implies shared repository 
246             # (otherwise there is no repository to store revisions in)
247             standalone = False
248
249         if os.path.isfile(from_location):
250             from bzrlib.plugins.svn.convert import load_dumpfile
251             import tempfile
252             tmp_repos = tempfile.mkdtemp(prefix='bzr-svn-dump-')
253             load_dumpfile(from_location, tmp_repos)
254             from_location = tmp_repos
255         else:
256             tmp_repos = None
257
258         from_dir = BzrDir.open(from_location)
259         try:
260             from_repos = from_dir.open_repository()
261         except NoRepositoryPresent, e:
262             if prefix is not None:
263                 raise BzrCommandError("Path inside repository specified and --prefix specified")
264             from_repos = from_dir.find_repository()
265             prefix = urlutils.relative_url(from_repos.base, from_location)
266             prefix = prefix.encode("utf-8")
267
268         from_repos.lock_read()
269         try:
270             (guessed_scheme, scheme) = repository_guess_scheme(from_repos, from_repos.get_latest_revnum())
271
272             if prefix is not None:
273                 prefix = prefix.strip("/") + "/"
274                 if guessed_scheme.is_branch(prefix):
275                     raise BzrCommandError("%s appears to contain a branch. " 
276                             "For individual branches, use 'bzr branch'." % from_location)
277
278                 self.outf.write("Importing branches with prefix /%s\n" % 
279                     urlutils.unescape_for_display(prefix, self.outf.encoding))
280
281             if not isinstance(from_repos, SvnRepository):
282                 raise BzrCommandError(
283                         "Not a Subversion repository: %s" % from_location)
284
285             def filter_branch(branch):
286                 if prefix is not None and not branch.get_branch_path().startswith(prefix):
287                     return False
288                 return True
289
290             convert_repository(from_repos, to_location, scheme, None, 
291                                not standalone, trees, all, filter_branch=filter_branch)
292
293             if tmp_repos is not None:
294                 from bzrlib import osutils
295                 osutils.rmtree(tmp_repos)
296         finally:
297             from_repos.unlock()
298
299
300 register_command(cmd_svn_import)
301
302 class cmd_svn_upgrade(Command):
303     """Upgrade revisions mapped from Subversion in a Bazaar branch.
304     
305     This will change the revision ids of revisions whose parents 
306     were mapped from svn revisions.
307     """
308     takes_args = ['from_repository?']
309     takes_options = ['verbose']
310
311     @display_command
312     def run(self, from_repository=None, verbose=False):
313         from bzrlib.plugins.svn.upgrade import upgrade_branch, upgrade_workingtree
314         from bzrlib.branch import Branch
315         from bzrlib.errors import NoWorkingTree, BzrCommandError
316         from bzrlib.repository import Repository
317         from bzrlib.trace import info
318         from bzrlib.workingtree import WorkingTree
319         try:
320             wt_to = WorkingTree.open(".")
321             branch_to = wt_to.branch
322         except NoWorkingTree:
323             wt_to = None
324             branch_to = Branch.open(".")
325
326         stored_loc = branch_to.get_parent()
327         if from_repository is None:
328             if stored_loc is None:
329                 raise BzrCommandError("No pull location known or"
330                                              " specified.")
331             else:
332                 import bzrlib.urlutils as urlutils
333                 display_url = urlutils.unescape_for_display(stored_loc,
334                         self.outf.encoding)
335                 self.outf.write("Using saved location: %s\n" % display_url)
336                 from_repository = Branch.open(stored_loc).repository
337         else:
338             from_repository = Repository.open(from_repository)
339
340         if wt_to is not None:
341             renames = upgrade_workingtree(wt_to, from_repository, 
342                                           allow_changes=True, verbose=verbose)
343         else:
344             renames = upgrade_branch(branch_to, from_repository, 
345                                      allow_changes=True, verbose=verbose)
346
347         if renames == {}:
348             info("Nothing to do.")
349
350         if wt_to is not None:
351             wt_to.set_last_revision(branch_to.last_revision())
352
353 register_command(cmd_svn_upgrade)
354
355 class cmd_svn_push(Command):
356     """Push revisions to Subversion, creating a new branch if necessary.
357
358     The behaviour of this command is the same as that of "bzr push", except 
359     that it also creates new branches.
360     
361     This command is experimental and will be removed in the future when all 
362     functionality is included in "bzr push".
363     """
364     takes_args = ['location?']
365     takes_options = ['revision', 'remember', Option('directory',
366             help='Branch to push from, '
367                  'rather than the one containing the working directory.',
368             short_name='d',
369             type=unicode,
370             )]
371
372     def run(self, location=None, revision=None, remember=False, 
373             directory=None):
374         from bzrlib.bzrdir import BzrDir
375         from bzrlib.branch import Branch
376         from bzrlib.errors import NotBranchError, BzrCommandError
377         from bzrlib import urlutils
378
379         if directory is None:
380             directory = "."
381         source_branch = Branch.open_containing(directory)[0]
382         stored_loc = source_branch.get_push_location()
383         if location is None:
384             if stored_loc is None:
385                 raise BzrCommandError("No push location known or specified.")
386             else:
387                 display_url = urlutils.unescape_for_display(stored_loc,
388                         self.outf.encoding)
389                 self.outf.write("Using saved location: %s\n" % display_url)
390                 location = stored_loc
391
392         source_branch.lock_read()
393         try:
394             bzrdir = BzrDir.open(location)
395             if revision is not None:
396                 if len(revision) > 1:
397                     raise BzrCommandError(
398                         'bzr svn-push --revision takes exactly one revision' 
399                         ' identifier')
400                 revision_id = revision[0].as_revision_id(source_branch)
401             else:
402                 revision_id = None
403             try:
404                 target_branch = bzrdir.open_branch()
405                 target_branch.lock_write()
406                 try:
407                     target_branch.pull(source_branch, stop_revision=revision_id)
408                 finally:
409                     target_branch.unlock()
410             except NotBranchError:
411                 target_branch = bzrdir.import_branch(source_branch, revision_id)
412             # We successfully created the target, remember it
413             if source_branch.get_push_location() is None or remember:
414                 source_branch.set_push_location(target_branch.base)
415         finally:
416             source_branch.unlock()
417
418 register_command(cmd_svn_push)
419
420 class cmd_dpush(Command):
421     """Push diffs into Subversion avoiding the use of any Bazaar-specific properties.
422
423     This will afterwards rebase the local Bazaar branch on the Subversion 
424     branch unless the --no-rebase option is used, in which case 
425     the two branches will be out of sync. 
426     """
427     takes_args = ['location?']
428     takes_options = ['remember', Option('directory',
429             help='Branch to push from, '
430                  'rather than the one containing the working directory.',
431             short_name='d',
432             type=unicode,
433             ),
434             Option('no-rebase', help="Don't rebase after push")]
435
436     def run(self, location=None, remember=False, directory=None, no_rebase=False):
437         from bzrlib import urlutils
438         from bzrlib.bzrdir import BzrDir
439         from bzrlib.branch import Branch
440         from bzrlib.errors import NotBranchError, BzrCommandError, NoWorkingTree
441         from bzrlib.workingtree import WorkingTree
442
443         from bzrlib.plugins.svn.commit import dpush
444
445         if directory is None:
446             directory = "."
447         try:
448             source_wt = WorkingTree.open_containing(directory)[0]
449             source_branch = source_wt.branch
450         except NoWorkingTree:
451             source_branch = Branch.open_containing(directory)[0]
452             source_wt = None
453         stored_loc = source_branch.get_push_location()
454         if location is None:
455             if stored_loc is None:
456                 raise BzrCommandError("No push location known or specified.")
457             else:
458                 display_url = urlutils.unescape_for_display(stored_loc,
459                         self.outf.encoding)
460                 self.outf.write("Using saved location: %s\n" % display_url)
461                 location = stored_loc
462
463         bzrdir = BzrDir.open(location)
464         target_branch = bzrdir.open_branch()
465         target_branch.lock_write()
466         revid_map = dpush(target_branch, source_branch)
467         # We successfully created the target, remember it
468         if source_branch.get_push_location() is None or remember:
469             source_branch.set_push_location(target_branch.base)
470         if not no_rebase:
471             revno, old_last_revid = source_branch.last_revision_info()
472             new_last_revid = revid_map[old_last_revid]
473             if source_wt is not None:
474                 source_wt.pull(target_branch, overwrite=True, 
475                                stop_revision=new_last_revid)
476             else:
477                 source_branch.pull(target_branch, overwrite=True, 
478                                    stop_revision=new_last_revid)
479
480
481 register_command(cmd_dpush)
482
483
484 class cmd_svn_branching_scheme(Command):
485     """Show or change the branching scheme for a Subversion repository.
486
487     See 'bzr help svn-branching-schemes' for details.
488     """
489     takes_args = ['location?']
490     takes_options = [
491         Option('set', help="Change the branching scheme. "),
492         Option('repository-wide', 
493             help="Act on repository-wide setting rather than local.")
494         ]
495
496     def run(self, location=".", set=False, repository_wide=False):
497         from bzrlib.bzrdir import BzrDir
498         from bzrlib.errors import BzrCommandError
499         from bzrlib.msgeditor import edit_commit_message
500         from bzrlib.repository import Repository
501         from bzrlib.trace import info
502         from bzrlib.plugins.svn.repository import SvnRepository
503         from bzrlib.plugins.svn.mapping3.scheme import scheme_from_branch_list
504         from bzrlib.plugins.svn.mapping3 import config_set_scheme, get_property_scheme, set_property_scheme
505         def scheme_str(scheme):
506             if scheme is None:
507                 return ""
508             return "".join(map(lambda x: x+"\n", scheme.to_lines()))
509         dir = BzrDir.open_containing(location)[0]
510         repos = dir.find_repository()
511         if not isinstance(repos, SvnRepository):
512             raise BzrCommandError("Not a Subversion repository: %s" % location)
513         if repository_wide:
514             scheme = get_property_scheme(repos)
515         else:
516             scheme = repos.get_mapping().scheme
517         if set:
518             schemestr = edit_commit_message("", 
519                                             start_message=scheme_str(scheme))
520             scheme = scheme_from_branch_list(
521                 map(lambda x:x.strip("\n"), schemestr.splitlines()))
522             if repository_wide:
523                 set_property_scheme(repos, scheme)
524             else:
525                 config_set_scheme(repos, scheme, None, mandatory=True)
526         elif scheme is not None:
527             info(scheme_str(scheme))
528
529
530 register_command(cmd_svn_branching_scheme)
531
532
533 class cmd_svn_set_revprops(Command):
534     """Migrate Bazaar metadata to Subversion revision properties.
535
536     This requires that you have permission to change the 
537     revision properties on the repository.
538
539     To change these permissions, edit the hooks/pre-revprop-change 
540     file in the Subversion repository.
541     """
542     takes_args = ['location']
543
544     def run(self, location="."):
545         raise NotImplementedError(self.run)
546
547
548 register_command(cmd_svn_set_revprops)
549
550
551 def test_suite():
552     """Returns the testsuite for bzr-svn."""
553     from unittest import TestSuite
554     from bzrlib.plugins.svn import tests
555     suite = TestSuite()
556     suite.addTest(tests.test_suite())
557     return suite
558
559
560 if __name__ == '__main__':
561     print ("This is a Bazaar plugin. Copy this directory to ~/.bazaar/plugins "
562           "to use it.\n")
563 elif __name__ != 'bzrlib.plugins.svn':
564     raise ImportError('The Subversion plugin must be installed as'
565                       ' bzrlib.plugins.svn not %s' % __name__)