Subversion remote API: committing without working copy

Can you do something similar with Git? I’m sure: no. In my previous post I described Subversion API basics. Now I’d like to give one more example of editor-based remote API usage: commit creation on-the-fly.

Subversion has API bindings for the most popular programming languages. This time let’s use Java.

There’re 2 ways to use Subversion from Java. The first one is to use JavaHL API of Subversion. There’re 2 implementations of this API: native Subversion (compiled Subversion libraries + JNI) and SVNKit (pure Java implementation). The advantages of the native Subversion implementation are performance and stability. But the problem is that if something goes wrong in the native implementation the JVM is crashed, but if something goes wrong in SVNKit — an exception is thrown.

But as in this post I want to show the power of the remote API, JavaHL interface doesn’t suit because it provies only client API (see the first picture of my previous post; and the client API requires the working copy to commit). In opposite SVNKit provides all Subversion APIs for Java (like native Subversion provides all APIs for C language).

The central class of SVNKit remote API is SVNRepository (corresponds to svn_ra_session_t in C interface). It represents a connection with some certain protocol to some certain URL. After working with SVNRepository the connection should be closed with SVNRepository#closeSession (unless you use ISVNRepositoryPool).

Let’s consider an example:

.......

public class CommitWithoutWorkingCopy {

    public static void main(String[] args) {
        FSRepositoryFactory.setup();
        DAVRepositoryFactory.setup();
        SVNRepositoryFactoryImpl.setup();

        SVNRepository svnRepository = null;
        try {
            svnRepository = SVNRepositoryFactory.create(
                    SVNURL.parseURIEncoded("file:///tmp/test"));

            SVNDeltaGenerator deltaGenerator = new SVNDeltaGenerator();

            ISVNEditor commitEditor;
            String checksum;
            long latestRevision;
            SVNCommitInfo commitInfo;

            commitEditor = svnRepository.getCommitEditor(
                    "My first commit message", null);
            commitEditor.targetRevision(-1);
            commitEditor.openRoot(-1);
            commitEditor.addDir("trunk", null, -1);
            commitEditor.changeFileProperty("trunk/file",
                    "directoryPropertyName",
                    SVNPropertyValue.create("directoryPropertyValue"));
            commitEditor.addFile("trunk/file", null, -1);
            commitEditor.changeFileProperty("trunk/file",
                    "filePropertyName",
                    SVNPropertyValue.create("filePropertyValue"));
            commitEditor.applyTextDelta("trunk/file", null);

            final ByteArrayInputStream fileContentsStream =
                    new ByteArrayInputStream("File contents".getBytes());
            try {
                checksum = deltaGenerator.sendDelta("trunk/file",
                        fileContentsStream, commitEditor, true);
            } finally {
                try {
                    fileContentsStream.close();
                } catch (IOException e) {
                    //ignore
                }
            }
            commitEditor.closeFile("trunk/file", checksum);
            commitEditor.closeDir();
            commitEditor.addDir("branches", null, -1);
            commitEditor.closeDir();
            commitEditor.addDir("tags", null, -1);
            commitEditor.closeDir();
            commitInfo = commitEditor.closeEdit();

            latestRevision = commitInfo.getNewRevision();
            System.out.println("Committed revision " + latestRevision);

            commitEditor = svnRepository.getCommitEditor(
                    "My second commit message", null);
            commitEditor.targetRevision(-1);
            commitEditor.openRoot(1);
            commitEditor.openDir("branches", 1);
            commitEditor.addDir("branches/branch", "/trunk", 1);
            commitEditor.closeDir();
            commitEditor.closeDir();
            commitEditor.deleteEntry("tags", 1);
            commitEditor.closeDir();
            commitInfo = commitEditor.closeEdit();

            latestRevision = commitInfo.getNewRevision();
            System.out.println("Committed revision " + latestRevision);

        } catch (SVNException e) {
            e.printStackTrace();
        } finally {
            if (svnRepository != null) {
                svnRepository.closeSession();
            }
        }
    }
}

Do you understand what happens here? Just the opposite to update-like calls. You get an editor and call its methods (in update/status/switch/diff you run some method — SVNRepository#update or SVNRepository#status and provide your own editor to call). This is the beauty of Subversion API.

You just crawl the tree inside the URL, for which you create SVNRepository object, and describe you changes. The new revision is created only at ISVNEditor#closeEdit. At this time the transaction is fixed or rejected. You never know if someone else commits to the same repository at the same time until you call ISVNEditor#closeEdit to fix your revision. As you never know the latest repository state, you send delta against some certain revisions instead of against the latest revision — that’s what revision r1 means in the following code:

commitEditor.openDir("branches", 1);
//the delta is send against r1
commitEditor.closeDir();

If -1 is used instead of the latest revision, the changes are applied to the latest repository state.

As one can see Subversion checks checksums for every file

commitEditor.closeFile("trunk/file", checksum);

If the file was not added but changed, the checksum should be provided to ISVNEditor#applyTextDelta call. So every file checksum is checked twice: before and after applying delta. If any of checksum is wrong, the commit will be rejected.

One more detail, not very evident: all the paths, not starting with “/”, are relative to the URL of the SVNRepository object (“file:///tmp/test” in my example). But paths, starting with “/”, are relative to the repository root that may differ from the URL for which the connection is created. In my example “file:///tmp/test” is the repository root, that one can check by calling SVNRepository#getRepositoryRoot.

The example code produces the following history when running on the empty repository:

------------------------------------------------------------------------
r2 | (no author) | 2012-07-20 03:14:30 +0200 (Fri, 20 Jul 2012) | 1 line
Changed paths:
   A /branches/branch (from /trunk:1)
   D /tags

My second commit message
------------------------------------------------------------------------
r1 | (no author) | 2012-07-20 03:14:30 +0200 (Fri, 20 Jul 2012) | 1 line
Changed paths:
   A /branches
   A /tags
   A /trunk
   A /trunk/file

My first commit message
------------------------------------------------------------------------

Comments are closed.