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