Fix existing tests to work.
authorJelmer Vernooij <jelmer@samba.org>
Wed, 21 Jun 2006 18:05:08 +0000 (20:05 +0200)
committerJelmer Vernooij <jelmer@samba.org>
Wed, 21 Jun 2006 18:05:08 +0000 (20:05 +0200)
18 files changed:
.bzrignore [new file with mode: 0644]
COPYING [new file with mode: 0644]
README [new file with mode: 0644]
__init__.py [new file with mode: 0644]
branch.py [new file with mode: 0644]
commit.py [new file with mode: 0644]
dumpfile.py [new file with mode: 0644]
format.py [new file with mode: 0644]
repository.py [new file with mode: 0644]
scheme.py [new file with mode: 0644]
setup.py [new file with mode: 0755]
submit.py [new file with mode: 0644]
tests/__init__.py [new file with mode: 0644]
tests/svntest.py [moved from svntest.py with 72% similarity]
tests/test_branch.py [new file with mode: 0644]
tests/test_repos.py [new file with mode: 0644]
transport.py [new file with mode: 0644]
workingtree.py [new file with mode: 0644]

diff --git a/.bzrignore b/.bzrignore
new file mode 100644 (file)
index 0000000..378eac2
--- /dev/null
@@ -0,0 +1 @@
+build
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..d60c31a
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,340 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+     59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+\f
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+\f
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+\f
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+\f
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+\f
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year  name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..4d29bac
--- /dev/null
+++ b/README
@@ -0,0 +1,15 @@
+This directory contains a simple plugin that adds 
+Subversion (http://subversion.tigris.org/) branch support to 
+Bazaar (http://www.bazaar-vcs.org/)
+
+You will need a recent version of Bazaar-NG, most likely bzr.dev. If you have 
+an older version of Bazaar and don't want to upgrade, try the svn-0.8 branch. 
+This contains some hacks to make the Subversion plugin work with Bazaar 0.8.
+
+You also need the Subversion bindings for python, with my patch for svn_info_t,
+which has been in Subversions' repository since r19413.
+
+Simply place this directory in ~/.bazaar/plugins and you should be able 
+to check out branches from Subversion using bzr.
+
+Jelmer Vernooij <jelmer@samba.org>, April 2006.
diff --git a/__init__.py b/__init__.py
new file mode 100644 (file)
index 0000000..a2c5dfd
--- /dev/null
@@ -0,0 +1,52 @@
+# Copyright (C) 2005-2006 Jelmer Vernooij <jelmer@samba.org>
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+"""
+Support for foreign branches (Subversion)
+"""
+import sys
+import os.path
+import transport
+import dumpfile
+import format
+import branch
+import submit
+import workingtree
+
+sys.path.append(os.path.dirname(__file__))
+
+from bzrlib.transport import register_transport
+register_transport('svn:', transport.SvnRaTransport)
+register_transport('svn+', transport.SvnRaTransport)
+
+from bzrlib.bzrdir import BzrDirFormat
+
+BzrDirFormat.register_control_format(format.SvnFormat)
+
+BzrDirFormat.register_control_format(workingtree.SvnWorkingTreeDirFormat)
+
+BzrDirFormat.register_control_format(dumpfile.SvnDumpFileFormat)
+
+def test_suite():
+    from unittest import TestSuite, TestLoader
+    import tests
+
+    suite = TestSuite()
+
+    suite.addTest(tests.test_suite())
+
+    return suite
+
diff --git a/branch.py b/branch.py
new file mode 100644 (file)
index 0000000..9886b46
--- /dev/null
+++ b/branch.py
@@ -0,0 +1,224 @@
+# Copyright (C) 2005-2006 Jelmer Vernooij <jelmer@samba.org>
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from bzrlib.branch import Branch, BranchFormat, BranchCheckResult, BzrBranch
+from bzrlib.errors import NotBranchError, NoWorkingTree, NoSuchRevision, \
+        NoSuchFile
+from bzrlib.inventory import Inventory, InventoryFile, InventoryDirectory, \
+            ROOT_ID
+from bzrlib.revision import Revision, NULL_REVISION
+from bzrlib.tree import Tree, EmptyTree
+from bzrlib.trace import mutter, note
+from bzrlib.workingtree import WorkingTree
+from bzrlib.delta import compare_trees
+import bzrlib
+
+import svn.core, svn.ra
+import os
+from libsvn.core import SubversionException
+
+
+svn.ra.initialize()
+
+_global_pool = svn.core.svn_pool_create(None)
+
+class FakeControlFiles(object):
+    def get_utf8(self, name):
+        raise NoSuchFile(name)
+
+
+class SvnBranch(Branch):
+    """Maps to a Branch in a Subversion repository """
+    def __init__(self, repos, branch_path):
+        self.repository = repos
+        self.branch_path = branch_path
+        self.base_revnum = svn.ra.get_latest_revnum(self.repository.ra)
+        self.control_files = FakeControlFiles()
+        self._generate_revnum_map()
+        self.base = "%s/%s" % (repos.url, branch_path)
+        self._format = SvnBranchFormat()
+        mutter("Connected to branch at %s" % branch_path)
+
+    def check(self):
+        return BranchCheckResult(self)
+        
+    def path_from_file_id(self, revision_id, file_id):
+        """Generate a full Subversion path from a bzr file id.
+        
+        :param revision_id: 
+        :param file_id: 
+        :return: Subversion 
+        """
+        return self.base+"/"+self.filename_from_file_id(revision_id, file_id)
+
+    def _generate_revnum_map(self):
+        self._revision_history = []
+
+        def rcvr(paths, rev, author, date, message, pool):
+            revid = self.repository.generate_revision_id(rev, self.branch_path)
+            self._revision_history.append(revid)
+
+        self.repository._get_branch_log(self.branch_path.encode('utf8'), 0, 
+                                 self.base_revnum, 0, False, rcvr)
+
+    def set_root_id(self, file_id):
+        raise NotImplementedError(self.set_root_id)
+            
+    def get_root_id(self):
+        inv = self.repository.get_inventory(self.last_revision())
+        return inv.root.file_id
+
+    def _get_nick(self):
+        return self.branch_path
+
+    nick = property(_get_nick)
+
+    def abspath(self, name):
+        return self.base+"/"+name
+
+    def push_stores(self, branch_to):
+        raise NotImplementedError(self.push_stores)
+
+    def get_revision_inventory(self, revision_id):
+        raise NotImplementedError(self.get_revision_inventory)
+
+    def sign_revision(self, revision_id, gpg_strategy):
+        raise NotImplementedError(self.sign_revision)
+
+    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
+        raise NotImplementedError(self.store_revision_signature)
+
+    def set_revision_history(self, rev_history):
+        raise NotImplementedError(self.set_revision_history)
+
+    def set_push_location(self, location):
+        raise NotImplementedError(self.set_push_location)
+
+    def get_push_location(self):
+        # get_push_location not supported on Subversion
+        return None
+
+    def revision_history(self):
+        return self._revision_history
+
+    def has_revision(self, revision_id):
+        return self.revision_history().has_key(revision_id)
+
+    def get_parents(self, revision_id):
+        revnum = self.get_revnum(revision_id)
+        parents = []
+        if not revision_id is None:
+            parent_id = self.revnum_map[revnum.value.number-1]
+            if not parent_id is None:
+                parents.append(parent_id)
+        # FIXME: Figure out if there are any merges here and where they come 
+        # from
+        return parents
+
+    def get_ancestry(self, revision_id):
+        try:
+            i = self.revision_history().index(revision_id)
+        except ValueError:
+            raise NoSuchRevision(revision_id, self)
+
+        # FIXME: Figure out if there are any merges here and where they come 
+        # from
+        return self.revision_history()[0:i+1]
+
+    def pull(self, source, overwrite=False):
+        raise NotImplementedError(self.pull)
+
+    def update_revisions(self, other, stop_revision=None):
+        raise NotImplementedError(self.update_revisions)
+
+    def pullable_revisions(self, other, stop_revision):
+        raise NotImplementedError(self.pullable_revisions)
+        
+    # The remote server handles all this for us
+    def lock_write(self):
+        pass
+        
+    def lock_read(self):
+        pass
+
+    def unlock(self):
+        pass
+
+    def get_parent(self):
+        return None
+
+    def set_parent(self, url):
+        raise NotImplementedError(self.set_parent, 
+                                  'can not change parent of SVN branch')
+
+    def get_transaction(self):
+        raise NotImplementedError(self.get_transaction)
+
+    def append_revision(self, *revision_ids):
+        # FIXME: raise NotImplementedError(self.append_revision)
+        pass
+
+    def get_physical_lock_status(self):
+        return False
+
+    def sprout(self, to_bzrdir, revision_id=None):
+        result = BranchFormat.get_default_format().initialize(to_bzrdir)
+        self.copy_content_into(result, revision_id=revision_id)
+        result.set_parent(self.bzrdir.root_transport.base)
+        return result
+
+    def copy_content_into(self, destination, revision_id=None):
+        new_history = self.revision_history()
+        if revision_id is not None:
+            try:
+                new_history = new_history[:new_history.index(revision_id) + 1]
+            except ValueError:
+                rev = self.repository.get_revision(revision_id)
+                new_history = rev.get_history(self.repository)[1:]
+        destination.set_revision_history(new_history)
+        parent = self.get_parent()
+        if parent:
+            destination.set_parent(parent)
+
+    def submit(self, from_branch, stop_revision):
+        if stop_revision is None:
+            stop_revision = from_branch.last_revision()
+
+        revisions = self.missing_revisions(from_branch, \
+                from_branch.revision_id_to_revno(stop_revision))
+
+        for revid in revisions:
+            rev = from_branch.repository.get_revision(revid)
+            self.commit(rev.message)
+
+        print revisions
+
+
+class SvnBranchFormat(BranchFormat):
+    """ Branch format for Subversion Branches."""
+    def __init__(self):
+        BranchFormat.__init__(self)
+
+    def get_format_description(self):
+        """See Branch.get_format_description."""
+        return 'Subversion Smart Server'
+
+    def get_format_string(self):
+        return 'Subversion Smart Server'
+
+    def initialize(self, to_bzrdir):
+        raise NotImplementedError(self.initialize)
+
diff --git a/commit.py b/commit.py
new file mode 100644 (file)
index 0000000..99ff0f8
--- /dev/null
+++ b/commit.py
@@ -0,0 +1,80 @@
+# Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import svn.ra
+import svn.delta
+
+from bzrlib.repository import CommitBuilder
+from bzrlib.errors import UnsupportedOperation, BzrError
+
+class SvnCommitBuilder(CommitBuilder):
+    def __init__(self, repository, branch, parents, config, revprops):
+        super(SvnCommitBuilder, self).__init__(repository, parents, 
+            config, None, None, None, revprops, None)
+        self.branch = branch
+
+        # TODO: Allow revision id to be specified, but only if it 
+        # matches the format for Subversion revision ids, the UUID
+        # matches and the revnum is in the future. Set the 
+        # revision num on the delta editor using set_target_revision
+
+    def _generate_revision_if_needed(self):
+        pass
+
+    def set_message(self, message):
+        self.message = message
+
+    def finish_inventory(self):
+        # Subversion doesn't have an inventory
+        pass
+
+    def record_entry_contents(self, ie, parent_invs, path, tree):
+        # Subversion doesn't have an inventory
+        pass
+
+    def modified_file_text(self, file_id, file_parents,
+                           get_content_byte_lines, text_sha1=None,
+                           text_size=None):
+        # FIXME
+        pass
+
+    def modified_link(self, file_id, file_parents, link_target):
+        # FIXME
+        pass
+
+    def modified_directory(self, file_id, file_parents):
+        # FIXME
+        pass
+
+    def commit(self):
+        def done(info, pool):
+            if not info.post_commit_err is None:
+                raise BzrError(info.post_commit_err)
+
+            self.revnum = info.revision
+
+        editor, editor_baton = svn.ra.get_commit_editor2(
+            self.repository.ra, self.message, done, None, False)
+
+        root = svn.delta.editor_invoke_open_root(editor, editor_baton, 4)
+
+        svn.delta.editor_invoke_close_edit(editor, editor_baton)
+
+        # Throw away the cache of revision ids
+        self.branch._generate_revnum_map()
+
+        return self.repository.generate_revision_id(self.revnum, 
+                                                    self.branch.branch_path)
diff --git a/dumpfile.py b/dumpfile.py
new file mode 100644 (file)
index 0000000..bf266b2
--- /dev/null
@@ -0,0 +1,96 @@
+# Copyright (C) 2005-2006 Jelmer Vernooij <jelmer@samba.org>
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from transport import SvnRaTransport
+from format import SvnRemoteAccess
+
+import bzrlib
+from bzrlib.bzrdir import BzrDirFormat, BzrDir
+from bzrlib.errors import NotBranchError
+from bzrlib.inventory import Inventory
+from bzrlib.lockable_files import TransportLock
+from bzrlib.progress import DummyProgress
+import bzrlib.urlutils as urlutils
+import bzrlib.osutils as osutils
+from bzrlib.workingtree import WorkingTree, WorkingTreeFormat
+
+import tempfile
+from cStringIO import StringIO
+
+import svn.repos, svn.core
+from libsvn.core import SubversionException
+
+class SvnDumpFile(SvnRemoteAccess):
+    def __init__(self, nested_transport, format):
+        self.tmp_repos = None
+
+        transport = nested_transport
+
+        dumpfile = None
+
+        while not dumpfile:
+            last_name = urlutils.basename(transport.base)
+            parent_transport = transport
+            transport = transport.clone('..')
+            try:
+                dumpfile = transport.get(last_name)
+            except:
+                pass
+
+        repos_path = parent_transport.relpath(nested_transport.base)
+
+        self.tmp_repos = tempfile.mkdtemp(prefix='bzr-svn-dump-')
+        repos = svn.repos.create(self.tmp_repos, '', '', None, None)
+        try:
+            svn.repos.load_fs2(repos, dumpfile, StringIO(), 
+                svn.repos.load_uuid_default, '', 0, 0, None)
+        except SubversionException, (svn.core.SVN_ERR_STREAM_MALFORMED_DATA, _):
+            raise NotBranchError(path=nested_transport.base)
+
+        svn_url = 'svn+file://%s/%s' % (self.tmp_repos, repos_path)
+        remote_transport = SvnRaTransport(svn_url)
+
+        super(SvnDumpFile, self).__init__(remote_transport, format)
+
+    def __del__(self):
+        if self.tmp_repos:
+            osutils.rmtree(self.tmp_repos)
+            self.tmp_repos = None
+
+class SvnDumpFileFormat(BzrDirFormat):
+    _lock_class = TransportLock
+
+    @classmethod
+    def probe_transport(klass, transport):
+        format = klass()
+
+        # FIXME: This is way inefficient over remote transports..
+        if SvnDumpFile(transport, format):
+            return format
+
+        raise NotBranchError(path=transport.base)
+
+    def _open(self, transport):
+        return SvnDumpFile(transport, self)
+
+    def get_format_string(self):
+        return 'Subversion Dump File'
+
+    def get_format_description(self):
+        return 'Subversion Dump File'
+
+    def initialize(self,url):
+        raise NotImplementedError(SvnFormat.initialize)
diff --git a/format.py b/format.py
new file mode 100644 (file)
index 0000000..539581b
--- /dev/null
+++ b/format.py
@@ -0,0 +1,104 @@
+# Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from bzrlib.bzrdir import BzrDirFormat, BzrDir
+from repository import SvnRepository
+from branch import SvnBranch
+from libsvn.core import SubversionException
+from bzrlib.errors import NotBranchError, NotLocalUrl
+from bzrlib.lockable_files import TransportLock
+import svn.core
+from transport import SvnRaTransport
+
+class SvnRemoteAccess(BzrDir):
+    def __init__(self, _transport, _format):
+        self.root_transport = self.transport = _transport
+        self._format = _format
+
+        assert isinstance(_transport, SvnRaTransport)
+
+        self.url = _transport.base
+        self.branch_path = _transport.path
+
+    def clone(self, url, revision_id=None, basis=None, force_new_repo=False):
+        raise NotImplementedError(SvnRemoteAccess.clone)
+
+    def sprout(self, url, revision_id=None, basis=None, force_new_repo=False):
+        # FIXME: honor force_new_repo
+        result = BzrDirFormat.get_default_format().initialize(url)
+        repo = self.open_repository()
+        result_repo = repo.clone(result, revision_id, basis)
+        branch = self.open_branch()
+        branch.sprout(result, revision_id)
+        result.create_workingtree()
+        return result
+
+    def open_repository(self):
+        repos = SvnRepository(self, self.transport.root_url)
+        repos._format = self._format
+        return repos
+
+    # Subversion has all-in-one, so a repository is always present
+    find_repository = open_repository
+
+    # Working trees never exist on Subversion repositories
+    def open_workingtree(self):
+        raise NotLocalUrl(self.url)
+
+    def create_workingtree(self, revision_id=None):
+        raise NotImplementedError(self.create_workingtree)
+
+    def open_branch(self, unsupported=True):
+        repos = self.open_repository()
+
+        try:
+            branch = SvnBranch(repos, self.branch_path)
+        except SubversionException, (msg, num):
+            if num == svn.core.SVN_ERR_RA_ILLEGAL_URL or \
+               num == svn.core.SVN_ERR_WC_NOT_DIRECTORY or \
+               num == svn.core.SVN_ERR_RA_NO_REPOS_UUID or \
+               num == svn.core.SVN_ERR_RA_SVN_REPOS_NOT_FOUND or \
+               num == svn.core.SVN_ERR_FS_NOT_FOUND or \
+               num == svn.core.SVN_ERR_RA_DAV_REQUEST_FAILED:
+               raise NotBranchError(path=self.url)
+            raise
+        branch.bzrdir = self
+        return branch
+
+class SvnFormat(BzrDirFormat):
+    _lock_class = TransportLock
+
+    @classmethod
+    def probe_transport(klass, transport):
+        format = klass()
+
+        if isinstance(transport, SvnRaTransport):
+            return format
+
+        raise NotBranchError(path=transport.base)
+
+    def _open(self, transport):
+        return SvnRemoteAccess(transport, self)
+
+    def get_format_string(self):
+        return 'Subversion Smart Server'
+
+    def get_format_description(self):
+        return 'Subversion Smart Server'
+
+    def initialize(self, url):
+        raise NotImplementedError(self.initialize)
diff --git a/repository.py b/repository.py
new file mode 100644 (file)
index 0000000..8f60548
--- /dev/null
@@ -0,0 +1,642 @@
+# Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from bzrlib.branch import BranchCheckResult
+from bzrlib.config import config_dir
+from bzrlib.repository import Repository
+from bzrlib.lockable_files import LockableFiles, TransportLock
+from bzrlib.trace import mutter
+from bzrlib.revision import Revision
+from bzrlib.errors import NoSuchRevision, InvalidRevisionId, BzrError
+from bzrlib.progress import ProgressBar
+from bzrlib.inventory import Inventory, InventoryFile, InventoryDirectory, \
+            ROOT_ID
+from libsvn.core import SubversionException
+import svn.core
+import os
+import pickle
+import bzrlib
+import branch
+from cStringIO import StringIO
+from bzrlib.graph import Graph
+
+cache_dir = os.path.join(config_dir(), 'svn-cache')
+
+class SvnLogCache:
+    def __init__(self, ra, uuid, to_revnum):
+        cache_file = os.path.join(cache_dir, uuid)
+
+        # Try to load cache from file
+        try:
+            self.revisions = pickle.load(open(cache_file))
+            from_revnum = len(self.revisions)-1
+        except:
+            self.revisions = {}
+            from_revnum = 0
+
+        def rcvr(orig_paths, rev, author, date, message, pool):
+            self.pb.update('fetching svn revision info', rev, to_revnum)
+            paths = {}
+            if orig_paths is None:
+                orig_paths = {}
+            for p in orig_paths:
+                paths[p] = (orig_paths[p].action,
+                            orig_paths[p].copyfrom_path,
+                            orig_paths[p].copyfrom_rev)
+
+            self.revisions[rev] = {
+                    'paths': paths,
+                    'author': author,
+                    'date': date,
+                    'message': message
+                    }
+        if from_revnum != to_revnum:
+            mutter('log -r %r:%r /' % (from_revnum, to_revnum))
+            self.pb = ProgressBar()
+
+            svn.ra.get_log(ra, ["/"], from_revnum, to_revnum, 0, True, True, rcvr)
+            self.pb.clear()
+            try:
+                os.mkdir(cache_dir)
+            except OSError:
+                pass
+            pickle.dump(self.revisions, open(cache_file, 'w'))
+
+    def get_log(self, paths, from_revno, to_revno, limit, 
+                strict_node_history, rcvr):
+        num = 0
+        for i in range(0,abs(from_revno-to_revno)+1):
+            if to_revno < from_revno:
+                i = from_revno - i
+            else:
+                i = from_revno + i
+            if i == 0:
+                continue
+            rev = self.revisions[i]
+            changed_paths = {}
+            for p in rev['paths']:
+                for q in paths:
+                    if p.startswith(q) or p[1:].startswith(q):
+                        changed_paths[p] = rev['paths'][p]
+
+            if len(changed_paths) > 0:
+                num = num + 1
+                rcvr(changed_paths, i, rev['author'], rev['date'], 
+                     rev['message'], None)
+                if limit and num == limit:
+                    return 
+
+    def get_branch_log(self, branch_path, from_revno, to_revno, limit, \
+            strict_node_history, rcvr):
+        self.get_log([branch_path], from_revno, to_revno, limit, 
+                     strict_node_history, rcvr)
+
+class SvnInventoryFile(InventoryFile):
+    """Inventory entry that can either be a plain file or a 
+    symbolic link. Avoids fetching data until necessary. """
+    def __init__(self, file_id, name, parent_id, repository, path, revnum, 
+                 has_props):
+        self.repository = repository
+        self.path = path
+        self.has_props = has_props
+        self.revnum = revnum
+        InventoryFile.__init__(self, file_id, name, parent_id)
+
+    def _get_sha1(self):
+        text = self.repository._get_file(self.path, self.revnum).read()
+        return bzrlib.osutils.sha_string(text)
+
+    def _get_executable(self):
+        if not self.has_props:
+            return False
+
+        value = self.repository._get_file_prop(self.path, self.revnum, 
+                    svn.core.SVN_PROP_EXECUTABLE)
+        if value and value == svn.core.SVN_PROP_EXECUTABLE_VALUE:
+            return True
+        return False 
+
+    def _is_special(self):
+        if not self.has_props:
+            return False
+
+        value = self.repository._get_file_prop(self.path, self.revnum, 
+                    svn.core.SVN_PROP_SPECIAL)
+        if value and value == svn.core.SVN_PROP_SPECIAL_VALUE:
+            return True
+        return False 
+
+    def _get_symlink_target(self):
+        if not self._is_special():
+            return None
+        data = self.repository._get_file(self.path, self.revnum).read()
+        if not data.startswith("link "):
+            raise BzrError("Improperly formatted symlink file")
+        return data[len("link "):]
+
+    def _get_kind(self):
+        if self._is_special():
+            return 'symlink'
+        return 'file'
+
+    # FIXME: we need a set function here because of InventoryEntry.__init__
+    def _phony_set(self, data):
+        pass
+   
+    text_sha1 = property(_get_sha1, _phony_set)
+    executable = property(_get_executable, _phony_set)
+    symlink_target = property(_get_symlink_target, _phony_set)
+    kind = property(_get_kind, _phony_set)
+
+
+class SvnRepository(Repository):
+    """
+    Provides a simplified interface to a Subversion repository 
+    by using the RA (remote access) API from subversion
+    """
+    def __init__(self, bzrdir, url):
+        _revision_store = None
+
+        control_files = LockableFiles(bzrdir.transport, '', TransportLock)
+        Repository.__init__(self, 'Subversion Smart Server', bzrdir, 
+            control_files, None, None, None)
+
+        self.pool = svn.core.svn_pool_create(None)
+
+        self._scheme = bzrdir.transport._scheme
+        self.ra = bzrdir.transport.ra
+
+        self.uuid = svn.ra.get_uuid(self.ra)
+        self.base = self.url = url
+        self.fileid_map = {}
+        self.text_cache = {}
+        self.dir_cache = {}
+
+        assert self.url
+        assert self.uuid
+
+        mutter("Connected to repository at %s, UUID %s" % (
+            bzrdir.transport.svn_root_url, self.uuid))
+
+        self.logcache = SvnLogCache(self.ra, self.uuid, 
+                svn.ra.get_latest_revnum(self.ra))
+
+    def __del__(self):
+        svn.core.svn_pool_destroy(self.pool)
+
+    def _check(self, revision_ids):
+        return BranchCheckResult(self)
+
+    def get_inventory(self, revision_id):
+        (path, revnum) = self.parse_revision_id(revision_id)
+        mutter('getting inventory %r for branch %r' % (revnum, path))
+
+        def read_directory(inv, id, path, revnum):
+
+            (props, dirents) = self._cache_get_dir(path, revnum)
+
+            recurse = {}
+
+            for child_name in dirents:
+                dirent = dirents[child_name]
+
+                if path:
+                    child_path = "%s/%s" % (path, child_name)
+                else:
+                    child_path = child_name
+
+                (child_id, revid) = self.path_to_file_id(dirent.created_rev, 
+                    child_path)
+                if dirent.kind == svn.core.svn_node_dir:
+                    inventry = InventoryDirectory(child_id, child_name, id)
+                    recurse[child_path] = dirent.created_rev
+                elif dirent.kind == svn.core.svn_node_file:
+                    inventry = SvnInventoryFile(child_id, child_name, id, self, 
+                        child_path, dirent.created_rev, dirent.has_props)
+
+                else:
+                    raise BzrError("Unknown entry kind for '%s': %s" % 
+                        (child_path, dirent.kind))
+
+                inventry.revision = revid
+                inv.add(inventry)
+
+            for child_path in recurse:
+                (child_id, _) = self.path_to_file_id(recurse[child_path], 
+                    child_path)
+                read_directory(inv, child_id, child_path, recurse[child_path])
+    
+        inv = Inventory()
+
+        read_directory(inv, ROOT_ID, path, revnum)
+
+        return inv
+
+    def path_from_file_id(self, revision_id, file_id):
+        """Generate a Subversion path from a bzr file id."""
+        
+        return self.fileid_map[revision_id][file_id]
+
+    def path_to_file_id(self, revnum, path):
+        """Generate a bzr file id from a Subversion file name. 
+        Does not use svn.ra """
+
+        (path_branch, filename) = self._scheme.unprefix(path)
+
+        revision_id = self.generate_revision_id(revnum, path_branch)
+
+        if not self.fileid_map.has_key(revision_id):
+            self.fileid_map[revision_id] = {}
+
+        file_id = filename.replace("/", "@")
+        if file_id == "":
+            file_id = ROOT_ID
+
+        self.fileid_map[revision_id][file_id] = (path, revnum)
+        return (file_id, revision_id)
+
+    def all_revision_ids(self):
+        raise NotImplementedError(self.all_revision_ids)
+
+    def get_inventory_weave(self):
+        raise NotImplementedError(self.get_inventory_weave)
+
+    def get_ancestry(self, revision_id):
+        if revision_id is None: # FIXME: Is this correct?
+            return []
+        #FIXME: Find not just direct predecessors 
+        # but also branches from which this branch was copied
+        (path, revnum) = self.parse_revision_id(revision_id)
+
+        self._ancestry = [None]
+
+        # FIXME: use get_file_revs() ?
+        def rcvr(paths, rev, author, date, message, pool):
+            revid = self.generate_revision_id(rev, path)
+            self._ancestry.append(revid)
+
+        try:
+            self._get_branch_log(path.encode('utf8'), 0, revnum - 1, 1, 
+                          False, rcvr)
+        except SubversionException, (_, num):
+            if num != svn.core.SVN_ERR_FS_NOT_FOUND:
+                raise
+
+        return self._ancestry
+
+    def has_revision(self, revision_id):
+        (path, revnum) = self.parse_revision_id(revision_id)
+
+        mutter("svn check_path -r%d %s" % (revnum, path))
+        kind = svn.ra.check_path(self.ra, path.encode('utf8'), revnum)
+
+        return (kind != svn.core.svn_node_none)
+
+    def revision_parents(self, revision_id):
+        (path, revnum) = self.parse_revision_id(revision_id)
+
+        parent_ids = []
+
+        # TODO: Use get_file_revs()
+        def rcvr(paths, rev, *args):
+            revid = self.generate_revision_id(rev, path)
+            parent_ids.append(revid)
+
+
+        try:
+            self._get_branch_log(path.encode('utf8'), revnum - 1, 0, 1, False, rcvr)
+        except SubversionException, (_, num):
+            # If this is the first revision, there are no parents
+            if num != svn.core.SVN_ERR_FS_NOT_FOUND:
+                raise
+
+        return parent_ids
+
+    def get_revision(self, revision_id):
+        if not revision_id or not isinstance(revision_id, basestring):
+            raise InvalidRevisionId(revision_id=revision_id, branch=self)
+
+        mutter("retrieving %s" % revision_id)
+        (path, revnum) = self.parse_revision_id(revision_id)
+        
+        mutter('svn proplist -r %r' % revnum)
+        svn_props = svn.ra.rev_proplist(self.ra, revnum)
+
+        parent_ids = self.revision_parents(revision_id)
+
+        # Commit SVN revision properties to a Revision object
+        bzr_props = {}
+        rev = Revision(revision_id=revision_id,
+                       parent_ids=parent_ids)
+
+        for name in svn_props:
+            bzr_props[name] = svn_props[name].decode('utf8')
+
+        rev.timestamp = 1.0 * svn.core.secs_from_timestr(
+            bzr_props[svn.core.SVN_PROP_REVISION_DATE], self.pool)
+        rev.timezone = None
+
+        rev.committer = bzr_props[svn.core.SVN_PROP_REVISION_AUTHOR]
+        rev.message = bzr_props[svn.core.SVN_PROP_REVISION_LOG]
+
+        rev.properties = bzr_props
+
+        rev.inventory_sha1 = self.get_inventory_sha1(revision_id)
+
+        return rev
+
+    def add_revision(self, rev_id, rev, inv=None, config=None):
+        raise NotImplementedError()
+
+    def fileid_involved_between_revs(self, from_revid, to_revid):
+        raise NotImplementedError()
+
+    def fileid_involved(self, last_revid=None):
+        raise NotImplementedError()
+
+    def fileids_altered_by_revision_ids(self, revision_ids):
+        ranges = {}
+        interested = {}
+
+        # First, figure out for which revisions to fetch 
+        # the logs. Keeps the range as narrow as possible to 
+        # save bandwidth (and thus increase speed)
+        for revid in revision_ids:
+            (path, revnum) = self.parse_revision_id(revid)
+
+            if not ranges.has_key(path):
+                ranges[path] = (revnum, revnum)
+                interested[path] = [revnum]
+            else:
+                (min, max) = ranges[path]
+                
+                if revnum < min: 
+                    min = revnum
+                if revnum > max:
+                    max = revnum
+                
+                interested.append(revnum)
+
+        result = {}
+
+        def rcvr(paths, revnum, *args):
+            if not revnum in interested[self._tmp]:
+                return
+            for path in paths:
+                (file_id, revid) = self.path_to_file_id(revnum, path)
+                if not result.has_key(file_id):
+                    result[file_id] = []
+                result[file_id].append(revid)
+
+        for path in ranges:
+            self._tmp = path
+            (min, max) = ranges[path]
+            self._get_branch_log(path.encode('utf8'), min, max, 0, False, rcvr)
+
+        return result
+
+    def _get_log(self, paths, from_revno, to_revno, limit, strict_node_history,
+                 rcvr):
+        self.logcache.get_log(paths, from_revno, to_revno, limit, 
+                strict_node_history, rcvr)
+
+    def _get_branch_log(self, branch_path, from_revno, to_revno, limit, \
+            strict_node_history, rcvr):
+        self.logcache.get_branch_log(branch_path, from_revno, to_revno, limit, 
+                strict_node_history, rcvr)
+
+
+    def fileid_involved_by_set(self, changes):
+        ids = []
+
+        for revid in changes:
+            pass #FIXME
+
+        return ids
+
+    def generate_revision_id(self, rev, path):
+        """ Generate a unambiguous revision id. Does not use svn.ra """
+        return "svn:%d@%s-%s" % (rev, self.uuid, path)
+
+    def parse_revision_id(self, revid):
+        assert revid
+        assert isinstance(revid, basestring)
+
+        if not revid.startswith("svn:"):
+            raise NoSuchRevision()
+
+        revid = revid[len("svn:"):]
+
+        at = revid.index("@")
+        fash = revid.rindex("-")
+        uuid = revid[at+1:fash]
+
+        if uuid != self.uuid:
+            raise NoSuchRevision()
+
+        return (revid[fash+1:], int(revid[0:at]))
+
+    def get_inventory_xml(self, revision_id):
+        return bzrlib.xml5.serializer_v5.write_inventory_to_string(
+            self.get_inventory(revision_id))
+
+    def get_inventory_sha1(self, revision_id):
+        return bzrlib.osutils.sha_string(self.get_inventory_xml(revision_id))
+
+    def get_revision_xml(self, revision_id):
+        return bzrlib.xml5.serializer_v5.write_revision_to_string(
+            self.get_revision(revision_id))
+
+    def get_revision_sha1(self, revision_id):
+        return bzrlib.osutils.sha_string(self.get_revision_xml(revision_id))
+
+    def has_signature_for_revision_id(self, revision_id):
+        return False # SVN doesn't store GPG signatures. Perhaps 
+                     # store in SVN revision property?
+
+    def get_signature_text(self, revision_id):
+        # SVN doesn't store GPG signatures
+        raise NoSuchRevision(self, revision_id)
+
+    def _cache_get_dir(self, path, revnum):
+        if self.dir_cache.has_key(path) and \
+           self.dir_cache[path].has_key(revnum):
+            return self.dir_cache[path][revnum]
+
+        mutter("svn ls -r %d '%r'" % (revnum, path))
+
+        (dirents, _, props) = svn.ra.get_dir2(
+                self.ra, path.encode('utf8'), 
+                revnum, svn.core.SVN_DIRENT_KIND
+                + svn.core.SVN_DIRENT_CREATED_REV
+                + svn.core.SVN_DIRENT_HAS_PROPS, self.pool)
+
+        if not self.dir_cache.has_key(path):
+            self.dir_cache[path] = {}
+
+        self.dir_cache[path][revnum] = (props, dirents)
+
+        return self.dir_cache[path][revnum]
+
+    def _cache_get_file(self, path, revnum):
+        if self.text_cache.has_key(path) and \
+           self.text_cache[path].has_key(revnum):
+               return self.text_cache[path][revnum]
+
+        stream = StringIO()
+        mutter('svn getfile -r %r %s' % (revnum, path))
+        (realrevnum, props) = svn.ra.get_file(self.ra, path.encode('utf8'), 
+            revnum, stream, self.pool)
+        if not self.text_cache.has_key(path):
+            self.text_cache[path] = {}
+
+        self.text_cache[path][revnum] = (props, stream)
+        return self.text_cache[path][revnum]
+
+    def _get_file_prop(self, path, revnum, name):
+        (props, _) = self._cache_get_file(path, revnum)
+        if props.has_key(name):
+            return props[name]
+        return None
+
+    def _get_file(self, path, revnum):
+        (_, stream) = self._cache_get_file(path, revnum)
+        stream.seek(0)
+        return stream
+
+    def get_revision_graph(self, revision_id):
+        if revision_id is None:
+            raise NotImplementedError()
+
+        (path, revnum) = self.parse_revision_id(revision_id)
+
+        self._previous = revision_id
+        self._ancestry = {}
+        
+        def rcvr(paths, rev, author, date, message, pool):
+            revid = self.generate_revision_id(rev, path)
+            self._ancestry[self._previous] = [revid]
+            self._previous = revid
+
+        try:
+            self._get_branch_log(path.encode('utf8'), revnum - 1, 0, 0, False, rcvr)
+        except SubversionException, (_, num):
+            if num != svn.core.SVN_ERR_FS_NOT_FOUND:
+                raise
+
+        self._ancestry[self._previous] = []
+
+        return self._ancestry
+
+    def is_shared(self):
+        """Return True if this repository is flagged as a shared repository."""
+        return True
+
+    def get_physical_lock_status(self):
+        return False
+
+    def copy_content_into(self, destination, revision_id=None, basis=None):
+        pb = ProgressBar()
+
+        # Loop over all the revnums until revision_id
+        # (or youngest_revnum) and call destination.add_revision() 
+        # or destination.add_inventory() each time
+
+        if revision_id is None:
+            path = ""
+            until_revnum = svn.ra.get_latest_revnum(self.ra)
+        else:
+            (path, until_revnum) = self.parse_revision_id(revision_id)
+        
+        weave_store = destination.weave_store
+
+        current = {}
+
+        transact = destination.get_transaction()
+
+        changed = []
+
+        def rcvr(paths, revnum, author, date, message, pool):
+            changed.append((paths, revnum))
+            pb.update('receiving revision information', revnum, until_revnum)
+
+        self._get_log([path.encode('utf8')], 0, until_revnum, 0, False, rcvr)
+
+        for (paths, revnum) in changed:
+            pb.update('copying revision', revnum, until_revnum)
+            revid = self.generate_revision_id(revnum, path)
+            inv = self.get_inventory(revid)
+            rev = self.get_revision(revid)
+            destination.add_revision(revid, rev, inv)
+
+            #FIXME: use svn.ra.do_update
+            for item in paths:
+                (fileid, revid) = self.path_to_file_id(revnum, item)
+                branch_path = self.parse_revision_id(revid)[0]
+                if branch_path != path:
+                    continue
+
+                if paths[item].action == 'A':
+                    weave = weave_store.get_weave_or_empty(fileid, transact)
+                elif paths[item].action == 'M' or paths[item].action == 'R':
+                    weave = weave_store.get_weave(fileid, transact)
+                elif paths[item].action == 'D':
+                    continue
+                else:
+                    raise BzrError("Unknown SVN action '%s'" % 
+                        paths[item].action)
+
+                parents = []
+                if current.has_key(fileid):
+                    parents = [current[fileid]]
+                
+                try:
+                    stream = self._get_file(item, revnum)
+                except SubversionException, (_, num):
+                    if num != svn.core.SVN_ERR_FS_NOT_FILE:
+                        raise
+                    stream = None
+
+                if stream:
+                    stream.seek(0)
+                    weave.add_lines(revid, parents, stream.readlines())
+        
+        pb.clear()
+
+    def fetch(self, source, revision_id=None, pb=None):
+        raise NotImplementedError(self.fetch)
+
+    def get_commit_builder(self, branch, parents, config, timestamp=None, 
+                           timezone=None, committer=None, revprops=None, 
+                           revision_id=None):
+        if timestamp != None:
+            raise NotImplementedError(self.get_commit_builder, 
+                "timestamp can not be user-specified for Subversion repositories")
+
+        if timezone != None:
+            raise NotImplementedError(self.get_commit_builder, 
+                "timezone can not be user-specified for Subversion repositories")
+
+        if committer != None:
+            raise NotImplementedError(self.get_commit_builder, 
+                "committer can not be user-specified for Subversion repositories")
+
+        if revision_id != None:
+            raise NotImplementedError(self.get_commit_builder, 
+                "revision_id can not be user-specified for Subversion repositories")
+
+        from commit import SvnCommitBuilder
+        return SvnCommitBuilder(self, branch, parents, config, revprops)
diff --git a/scheme.py b/scheme.py
new file mode 100644 (file)
index 0000000..91d1954
--- /dev/null
+++ b/scheme.py
@@ -0,0 +1,62 @@
+# Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from bzrlib.errors import NoSuchFile
+
+class BranchingScheme:
+    """ Divides SVN repository data up into branches. Since there
+    is no proper way to do this, there are several subclasses of this class
+    each of which handles a particular convention that may be in use.
+    """
+    @staticmethod
+    def is_branch(name):
+        raise NotImplementedError
+
+    def unprefix(name):
+        raise NotImplementedError
+
+class DefaultBranchingScheme:
+    @staticmethod
+    def is_branch(name):
+        parts = name.split("/")
+        if len(parts) == 1 and parts[0] == "trunk":
+            return True
+
+        if len(parts) == 2 and (parts[0] == "branches" or parts[0] == "tags"):
+            return True
+
+        return False
+
+    @staticmethod
+    def unprefix(path):
+        parts = path.lstrip("/").split("/")
+        if parts[0] == "trunk" or parts[0] == "hooks":
+            return (parts[0], "/".join(parts[1:]))
+        elif parts[0] == "tags" or parts[0] == "branches":
+            return ("/".join(parts[0:2]), "/".join(parts[2:]))
+        else:
+            raise BzrError("Unable to unprefix path %s" % path)
+
+class NoBranchingScheme:
+    @staticmethod
+    def is_branch(name):
+        parts = name.split("/")
+        return len(parts) == 0
+
+    @staticmethod
+    def unprefix(path):
+        return ("", path)
+
diff --git a/setup.py b/setup.py
new file mode 100755 (executable)
index 0000000..858d23f
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python2.4
+
+from distutils.core import setup
+
+setup(name='bzr-svn',
+      description='Support for Subversion branches in Bazaar-NG',
+      keywords='plugin bzr svn',
+      version='0.1',
+      url='http://bazaar-vcs.org/BzrForeignBranches/Subversion',
+      download_url='http://samba.org/~jelmer/bzr/svn',
+      license='GPL',
+      author='Jelmer Vernooij',
+      author_email='jelmer@samba.org',
+      long_description="""
+      This plugin adds support for branching off Subversion 
+      repositories.
+      """,
+      package_dir={'bzrlib.plugins.svn':'.'},
+      packages=['bzrlib.plugins.svn'])
diff --git a/submit.py b/submit.py
new file mode 100644 (file)
index 0000000..a915e0c
--- /dev/null
+++ b/submit.py
@@ -0,0 +1,50 @@
+# Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
+# cmd_submit() based on cmd_commit() from bzrlib.builtins
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from bzrlib.commands import Command, register_command
+from bzrlib.builtins import tree_files
+from bzrlib.bzrdir import BzrDir
+from bzrlib.branch import Branch
+
+class cmd_submit(Command):
+    """Submit a revision to another (related) branch.
+    
+    This is basically a push to a Subversion repository, 
+    without the guarantee that a pull from that same repository 
+    is a no-op.
+    """
+
+    takes_args = ["location?"]
+    takes_options = ["revision", "verbose"]
+    
+    def run(self, revid=None, verbose=True, location=None):
+        (branch, _) = Branch.open_containing(".")
+
+        if location is None:
+            location = branch.get_parent()
+
+        if location is None:
+            raise BzrError("No location specified and no default location set on branch")
+
+        parent_branch = Branch.open(location)
+
+        if revid is None:
+            revid = branch.last_revision()
+
+        parent_branch.submit(branch, revid)
+
+register_command(cmd_submit)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644 (file)
index 0000000..b9ac7e1
--- /dev/null
@@ -0,0 +1,32 @@
+# Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import svn.repos
+import os
+from bzrlib import osutils
+from bzrlib.bzrdir import BzrDir
+from bzrlib.tests import TestCaseInTempDir
+
+def test_suite():
+    from unittest import TestSuite, TestLoader
+    import tests.test_repos, tests.test_branch
+
+    suite = TestSuite()
+
+    suite.addTest(TestLoader().loadTestsFromModule(tests.test_repos))
+    suite.addTest(TestLoader().loadTestsFromModule(tests.test_branch))
+
+    return suite
similarity index 72%
rename from svntest.py
rename to tests/svntest.py
index 0512b222f234b590ae9be8210a02bdb40faca36a..bd0d01f4a330bd0773a6bfc5e00e82fe9533a679 100644 (file)
@@ -18,44 +18,25 @@ from bzrlib.tests import TestCaseInTempDir
 
 import svn.ra, svn.repos, svn.wc
 
-class TestCaseWithSubversionRepository(TestCaseInTempDir):
+class TestCaseWithSvnRepository(TestCaseInTempDir):
     """A test case that provides the ability to build Subversion 
     repositories."""
 
-    def make_repository(self, relpath):
+    def make_repository(self):
         """Create a repository.
 
         :return: Handle to the repository.
         """
-        abspath = os.path.join(self.test_dir, relpath)
-        repos_url = "file://%s" % abspath
+        pass
 
-        repos = svn.repos.create(abspath, '', '', None, None)
+    def make_fs(self, relpath):
+        """Create repository in relpath.
 
-        return repos_url
-
-    def make_remote_bzrdir(self, relpath):
-        """Create a repository."""
-
-        repos_url = self.make_repository(relpath)
-
-        return BzrDir.open(repos_url)
-
-    def make_local_bzrdir(self, relpath):
-        """Create a repository and checkout."""
-
-        repos_url = self.make_repository(relpath)
-
-        ctx = svn.client.create_context()
-        
-        rev = svn.core.svn_opt_revision_t()
-        rev.kind = svn.core.svn_opt_revision_head
-
-        svn.client.checkout2(repos_url, relpath, 
-                rev, rev, True, False, ctx)
+        :param relpath: Path to create repository in.
+        :return: Subversion fs handle.
+        """
+        raise NotImplementedError(self.make_fs)
 
-        return BzrDir.open(relpath)
-        
     def wc_commit(self, relpaths):
         """Commit current changes in specified working copy.
         
@@ -93,15 +74,14 @@ class TestCaseWithSubversionRepository(TestCaseInTempDir):
         """
         raise NotImplementedError(self.build_tree)
 
-    def make_wc(self, relpath, repos_url=None):
+    def make_wc(self, relpath, reppath=""):
         """Create a repository and a checkout. Return the checkout.
 
         :param relpath: Optional relpath to check out if not the full 
             repository.
         :return: Subversion wc handle.
         """
-        # FIXME
-        raise NotImplementedError(self.make_wc)
+        raise NotImplementedError(self.make_fs_and_wc)
 
     def make_ra(self, relpath):
         """Create a repository and a ra connection to it. 
@@ -109,10 +89,7 @@ class TestCaseWithSubversionRepository(TestCaseInTempDir):
         :param relpath: Path to create repository at.
         :return: The ra connection.
         """
-
-        repos_url = self.make_repository(relpath)
-
-        return svn.ra.open2(repos_url, svn.ra.callbacks2_t(), None)
+        raise NotImplementedError(self.make_ra)
 
     def dumpfile(self, repos):
         """Create a dumpfile for the specified repository.
@@ -121,3 +98,11 @@ class TestCaseWithSubversionRepository(TestCaseInTempDir):
         """
         raise NotImplementedError(self.dumpfile)
 
+    def make_wc(self, relpath=""):
+        """Create a repository and a checkout. Return the checkout.
+
+        :param relpath: Optional relpath to check out if not the full 
+            repository.
+        """
+        raise NotImplementedError(self.make_wc)
+
diff --git a/tests/test_branch.py b/tests/test_branch.py
new file mode 100644 (file)
index 0000000..523254d
--- /dev/null
@@ -0,0 +1,26 @@
+# Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import svn
+import format
+from svntest import TestCaseWithSubversionRepository
+from bzrlib.bzrdir import BzrDir, BzrDirTestProviderAdapter, BzrDirFormat
+
+class WorkingSubversionBranch(TestCaseWithSubversionRepository):
+    def test_num_revnums(self):
+        bzrdir = self.make_local_bzrdir('a', 'ac')
+        branch = bzrdir.open_branch()
+        self.assertEqual(None, branch.last_revision())
diff --git a/tests/test_repos.py b/tests/test_repos.py
new file mode 100644 (file)
index 0000000..f0f6d28
--- /dev/null
@@ -0,0 +1,45 @@
+# Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import svn
+import format
+from svntest import TestCaseWithSubversionRepository
+from bzrlib.bzrdir import BzrDir
+from bzrlib.tests.repository_implementations.test_repository import TestCaseWithRepository
+
+class TestSubversionRepositoryWorks(TestCaseWithSubversionRepository):
+    def test_format(self):
+        """ Test repository format is correct """
+        bzrdir = self.make_local_bzrdir('a', 'ac')
+        self.assertEqual(bzrdir._format.get_format_string(), \
+                "Subversion Local Checkout")
+        
+        self.assertEqual(bzrdir._format.get_format_description(), \
+                "Subversion Local Checkout")
+
+    def test_url(self):
+        """ Test repository URL is kept """
+
+        bzrdir = self.make_local_bzrdir('b', 'bc')
+        self.assertTrue(isinstance(bzrdir, BzrDir))
+
+    def test_uuid(self):
+        """ Test UUID is retrieved correctly """
+        bzrdir = self.make_local_bzrdir('c', 'cc')
+        self.assertTrue(isinstance(bzrdir, BzrDir))
+        repository = bzrdir.open_repository()
+        fs = self.open_fs('c')
+        self.assertEqual(svn.fs.get_uuid(fs), repository.uuid)
diff --git a/transport.py b/transport.py
new file mode 100644 (file)
index 0000000..45b6525
--- /dev/null
@@ -0,0 +1,129 @@
+# Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from bzrlib.transport import Transport
+from cStringIO import StringIO
+import svn.ra
+import os
+from bzrlib.errors import NoSuchFile, NotBranchError
+from scheme import NoBranchingScheme
+
+def _create_auth_baton(pool):
+    """ Create a Subversion authentication baton.
+    
+    :param pool: An APR memory pool
+    """
+    import svn.client
+    # Give the client context baton a suite of authentication
+    # providers.h
+    providers = [
+        svn.client.svn_client_get_simple_provider(pool),
+        svn.client.svn_client_get_ssl_client_cert_file_provider(pool),
+        svn.client.svn_client_get_ssl_client_cert_pw_file_provider(pool),
+        svn.client.svn_client_get_ssl_server_trust_file_provider(pool),
+        svn.client.svn_client_get_username_provider(pool),
+        ]
+    return svn.core.svn_auth_open(providers, pool)
+
+
+# Don't run any tests on SvnTransport as it is not intended to be 
+# a full implementation of Transport
+def get_test_permutations():
+    return []
+
+class SvnRaCallbacks(svn.ra.callbacks2_t):
+    def __init__(self):
+        svn.ra.callbacks2_t.__init__(self)
+        from branch import _global_pool
+        self.auth_baton = _create_auth_baton(_global_pool)
+
+    def open_tmp_file(self):
+        print "foo"
+
+    def progress(self, f, c, pool):
+        print "%s: %d / %d" % (self, f, c)
+
+
+class SvnRaTransport(Transport):
+    """Fake transport for Subversion-related namespaces. This implements 
+    just as much of Transport as is necessary to fool Bazaar-NG. """
+    def __init__(self, url="", ra=None, root_url=None, scheme=None):
+        Transport.__init__(self, url)
+
+        if url.startswith("svn://") or \
+           url.startswith("svn+ssh://"):
+            self.svn_url = url
+        else:
+            self.svn_url = url[4:] # Skip svn+
+
+        # The SVN libraries don't like trailing slashes...
+        self.svn_url = self.svn_url.rstrip('/')
+
+        callbacks = SvnRaCallbacks()
+
+        if not ra:
+            self.ra = svn.ra.open2(self.svn_url.encode('utf8'), callbacks, None, None)
+            self.svn_root_url = svn.ra.get_repos_root(self.ra)
+            if self.svn_root_url != self.svn_url:
+                svn.ra.reparent(self.ra, self.svn_root_url.encode('utf8'))
+        else:
+            self.ra = ra
+            self.svn_root_url = root_url
+
+        self.root_url = self.svn_root_url
+        if not self.root_url.startswith("svn+"):
+            self.root_url = "svn+%s" % self.root_url
+
+        # Browsed above this directory
+        if not self.svn_url.startswith(self.svn_root_url):
+            raise NotBranchError(url)
+
+        self.path = self.svn_url[len(self.svn_root_url)+1:]
+
+        if not scheme:
+            scheme = NoBranchingScheme()
+
+        self._scheme = scheme
+        self.is_branch_root = scheme.is_branch(self.path)
+
+    def has(self, relpath):
+        return False
+
+    def get(self, relpath):
+        raise NoSuchFile(relpath)
+
+    def stat(self, relpath):
+        return os.stat('.') #FIXME
+
+    def listable(self):
+        return False
+
+    def lock_read(self, relpath):
+        class PhonyLock:
+            def unlock(self):
+                pass
+        return PhonyLock()
+
+    def clone(self, path):
+        parts = self.svn_url.split("/")
+        
+        # FIXME: Handle more complicated paths
+        if path == '..':
+            parts.pop()
+        elif path != '.':
+            parts.append(path)
+
+        return SvnRaTransport("/".join(parts),ra=self.ra,root_url=self.svn_root_url)
diff --git a/workingtree.py b/workingtree.py
new file mode 100644 (file)
index 0000000..9032163
--- /dev/null
@@ -0,0 +1,202 @@
+# Copyright (C) 2005-2006 Jelmer Vernooij <jelmer@samba.org>
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import bzrlib
+from bzrlib.bzrdir import BzrDirFormat
+from bzrlib.errors import NotBranchError
+from bzrlib.inventory import Inventory
+from bzrlib.lockable_files import TransportLock
+from bzrlib.progress import DummyProgress
+from bzrlib.workingtree import WorkingTree, WorkingTreeFormat
+
+from branch import _global_pool
+from format import SvnRemoteAccess, SvnFormat
+from repository import SvnRepository
+from transport import SvnRaTransport
+
+import os
+
+import svn.core, svn.wc
+from libsvn.core import SubversionException
+
+class SvnWorkingTree(WorkingTree):
+    """Implementation of WorkingTree that uses a Subversion 
+    Working Copy for storage."""
+    def __init__(self, wc, branch):
+        self._format = SvnWorkingTreeFormat()
+        self.wc = wc
+        self.basedir = svn.wc.adm_access_path(self.wc)
+        self._branch = branch
+
+    def _get_inventory(self):
+        return Inventory()
+        raise NotImplementedError(self._get_inventory)
+
+    inventory = property(_get_inventory,
+                         doc="Inventory of this Tree")
+
+    def lock_write(self):
+        pass
+
+    def lock_read(self):
+        pass
+
+    def unlock(self):
+        pass
+
+    def is_control_filename(self, path):
+        return path == '.svn'
+
+    def get_file_by_path(self, path):
+        raise NotImplementedError(self.get_file_by_path)
+
+    def get_file_lines(self, file_id):
+        raise NotImplementedError(self.get_file_lines)
+
+    def remove(self, files, verbose=False, to_file=None):
+        for file in files:
+            svn.wc.delete2(os.path.join(self.basedir, file), self.wc, None, 
+                           None, None)
+
+    def revert(self, files, old_tree=None, backups=True, pb=DummyProgress()):
+        if old_tree is not None:
+            # TODO: Also make sure old_tree != basis_tree
+            super(SvnWorkingTree, self).revert(files, old_tree, backups, pb)
+            return
+        
+        svn.wc.revert([os.path.join(self.basedir, f) for f in files],
+                      self.wc, False, False, None, None)
+
+    def move(self, from_paths, to_name):
+        revt = svn.core.svn_opt_revision_t()
+        revt.kind = svn.core.svn_opt_revision_unspecified
+        for entry in from_paths:
+            svn.wc.move(entry, revt, to_name, False, self.wc)
+
+    def rename_one(self, from_rel, to_rel):
+        # There is no difference between rename and move in SVN
+        self.move([from_rel], to_rel)
+
+    def read_working_inventory(self):
+        return self.inventory
+
+    def add(self, files, ids=None):
+        for f in files:
+            svn.wc.add2(f, False, self.wc)
+            if ids:
+                id = ids.pop()
+                if id:
+                    svn.wc.prop_set2('bzr:id', id, f, False)
+
+    def pending_merges(self):
+        return []
+
+    def set_pending_merges(self):
+        raise NotImplementedError(self.set_pending_merges)
+
+    def unknowns(self):
+        raise NotImplementedError(self.unknowns)
+
+    def basis_tree(self):
+        raise NotImplementedError(self.basis_tree)
+
+    def pull(self, source, overwrite=False, stop_revision=None):
+        raise NotImplementedError(self.pull)
+
+    def extras(self):
+        raise NotImplementedError(self.extras)
+
+class SvnWorkingTreeFormat(WorkingTreeFormat):
+    def get_format_description(self):
+        return "Subversion Working Copy"
+
+    def initialize(self, a_bzrdir, revision_id=None):
+        # FIXME
+        raise NotImplementedError(self.initialize)
+
+    def open(self, a_bzrdir):
+        # FIXME
+        raise NotImplementedError(self.initialize)
+
+class OptimizedRepository(SvnRepository):
+    def revision_tree(self, revision_id):
+        # TODO: if revision id matches base revno, 
+        # return working_tree.basis_tree() 
+        return super(OptimizedRepository, self).revision_tree(revision_id)
+
+class SvnLocalAccess(SvnRemoteAccess):
+    def __init__(self, transport, format):
+        self.local_path = transport.base.rstrip("/")
+        if self.local_path.startswith("file://"):
+            self.local_path = self.local_path[len("file://"):]
+        
+        self.wc = svn.wc.adm_open3(None, self.local_path, True, 100, None)
+        self.transport = transport
+
+        # Open related remote repository + branch
+        url, self.base_revno = svn.wc.get_ancestry(self.local_path, self.wc)
+        if not url.startswith("svn"):
+            url = "svn+" + url
+
+        remote_transport = SvnRaTransport(url)
+
+        super(SvnLocalAccess, self).__init__(remote_transport, format)
+
+    def __del__(self):
+        svn.wc.adm_close(self.wc)
+
+    def open_repository(self):
+        repos = OptimizedRepository(self, self.transport.root_url)
+        repos._format = self._format
+        return repos
+
+    def clone(self, path):
+        raise NotImplementedError(self.clone)
+
+    # Subversion has all-in-one, so a repository is always present
+    find_repository = open_repository
+
+    # Working trees never exist on Subversion repositories
+    def open_workingtree(self, _unsupported=False):
+        return SvnWorkingTree(self.wc, self.open_branch())
+
+    def create_workingtree(self):
+        raise NotImplementedError(SvnRemoteAccess.create_workingtree)
+
+
+class SvnWorkingTreeDirFormat(BzrDirFormat):
+    _lock_class = TransportLock
+
+    @classmethod
+    def probe_transport(klass, transport):
+        format = klass()
+
+        if transport.has('.svn'):
+            return format
+
+        raise NotBranchError(path=transport.base)
+
+    def _open(self, transport):
+        return SvnLocalAccess(transport, self)
+
+    def get_format_string(self):
+        return 'Subversion Local Checkout'
+
+    def get_format_description(self):
+        return 'Subversion Local Checkout'
+
+    def initialize(self,url):
+        raise NotImplementedError(self.initialize)