Main


Editing Operation: receiving changes from a repository

Another field of using ISVNEditor is update operations (checkout, update, switch, export). But in update operations roles are just opposite to ones in commit operations: that is, while in a commit you obtain and use the library's editor to edit a repository, in an update you provide your own ISVNEditor implementation to the library, which will be called by a repository server to transmit changes to a client side. If a remote access to a repository is used, it means that according to a particular access protocol a server sends commands which are translated by SVNKit into calls to a client's editor.

So, a low-level update operation takes two main stages:

In other words a client describes local revisions of the items to update, and a server traverses this tree to apply changes between what is in a repository and on the client's computer. This mechanism gives a certain freedom in choosing the format of storing local versioned tree, what means that you are not restricted by the bounds of a general Working Copy.

Reporting a local versioned tree state to a server-side

Descriptions of a local versioned tree state are made via the ISVNReporter interface. We will study how to report local revisions by the example of using the low-level layer in the high-level one, i.e. we will describe how SVNKit reports revisions of Working Copy items using ISVNReporter.

Let's imagine we have got the following Working Copy tree:

Update_Tree1.png

A user performs a recursive update to the latest (HEAD) revision on the root of the Working Copy. How does SVNKit make a report of local revisions in this case? First of all, reports are provided to an SVNRepository driver through an ISVNReporterBaton implementation. When you pass it to an update method of SVNRepository the latter one calls the baton's report() method passing it an ISVNReporter object.

Updating to the HEAD revision:

   1     ...
   2     FSRepositoryFactory.setup( );
   3     String url = "file://localhost/rep/nodeA/";
   4 
   5     SVNRepository repository = SVNRepositoryFactory.create( SVNURL.parseURIDecoded( url ) );
   6     ReporterBaton reporterBaton = ...;
   7     ISVNEditor editor = ...;
   8     
   9     repository.update( -1 /*forces to use the latest revision*/ , null /*target*/ , true /*recursive*/ , reporterBaton , editor );
  10     ...

The repository driver passes an ISVNReporter object to our reporter baton's report() method. We are interested in what calls to an ISVNReporter object our reporter baton should make to report local revisions in our example on condition that a user calls a recursive update on the root of our Working Copy:

   1 ...
   2 public class ReporterBaton implements ISVNReporterBaton {
   3 
   4     ...
   5     
   6     public void report( ISVNReporter reporter ) throws SVNException {
   7         
   8         //for the WC root
   9         reporter.setPath( "" /*path*/ , null /*lockToken*/ , 4 /*revision*/ , false /*startEmpty*/ );
  10 
  11         ...

Here we should stop for some important notes. The first call to ISVNReporter is always setPath("" , ...), i.e. we always start reporting with the item upon which an update is called. If target parameter of an update method is null (like in our case) the setPath("", ...) call describes the node corresponding to the location to which the driver is bound to (/nodeA). Otherwise if target is not null, it's a target item to update, and in this case setPath("", ...) describes this target item. Note that target is never a path but always a name. For example, if we wanted to update only itemA1 we would call:

   1     repository.update( -1 /*revision*/ , "itemA1" /*target*/ , false /*recursive*/ , reporterBaton , editor );

Further we traverse our tree:

   1         ...
   2 
   3         reporter.setPath( "nodeB" , null , 5 , false );
   4 
   5         ...

There's no need to report /nodeA/nodeB/itemB since its current revision coincides with the revision of its parent directory and it's not locally locked. By the same reason we don't report /nodeA/itemA1, but we must report /nodeA/itemA2 - although its revision is the same as the parent's one, but the item's locally locked, so maybe in a repository the lock is broken:

   1         ...
   2 
   3         //provide the item's lock token
   4         String lockToken = ...;
   5         reporter.setPath( "itemA2" , lockToken , 4 , false );
   6 
   7         ...

Continuing our report:

   1         ...
   2 
   3         SVNURL url = SVNURL.parseURIDecoded( "file://localhost/rep/node2A/nodeC/nodeD/" );
   4         //switched paths are described in this way:
   5         reporter.linkPath( url , "nodeC/nodeD" , null /*lockToken*/ , 4 /*revision*/ , false /*startEmpty*/ );
   6 
   7         ...

Although the local revision number of /nodeC/nodeD/ coincides with the revision number of the parent we need to report it as it's switched to another location of the repository.

   1         ...
   2         reporter.deletePath( "nodeF" );
   3         reporter.deletePath( "nodeG" );
   4         reporter.deletePath( "nodeL" );
   5         ...

nodeF is absent from our Working Copy. This means that we haven't got it previously because we don't have sufficient permissions on this directory. Furher we'll discuss how we get aware of absent directories and files when talking about editor invocations.

nodeG was locally deleted and commited in the 6-th revision, but the WC root has not been updated since then. Now imagine that someone returned that directory in revision 7 in the state it was in revision 4 . If we considered that we don't have nodeG, so we don't need to report it, we wouldn't get it back in an update to revision 7. This is because we report the WC root as in revision 4, but in revision 4 nodeG exists in the same state as in revision 7! The case of absent nodes is similar: imagine that in revision 7 permission restrictions on nodeF were broken for you, but you won't get the directory if you don't say you don't have it.

nodeL is missing from our Working Copy, i.e. it's still under version control but has been erased in the file system by a mistake. The idea is the same - we want to get the entire directory back into the Working Copy. However for a missing file (itemA3) there's no need to get the entire file since a missing file can be restored from the base (clear or unchanged) revision residing in an administrative folder (.svn) of the parent directory.

This is why it's important to report deleted and absent nodes when their local revisions are different from the parent's one as well as report missing directories.

   1         ...
   2 
   3         reporter.setPath( "nodeH" , null , 4 , true /*startEmpty*/ );
   4         reporter.setPath( "nodeH/nodeH2" , null , 4 , false /*startEmpty*/ );
   5         reporter.setPath( "nodeH/itemH" , null , 5 , false /*startEmpty*/ );
   6 
   7         ...

nodeH is incomplete what means that it was not updated entirely previous time (for example, an update operation was interrupted due to network connection problems or a server breakdown). So, we report incomplete directories as empty. And also we must report all children entries in an incomplete directory, doesn't matter whether their revisions are different from the parent's revision or not.

Another case of reporting a node as being empty is a checkout operation when initially you have no entries. In this case you make a single call to a reporter:

   1         long rev = ...;
   2         reporter.setPath( "" , null , rev , true );

Well, that's all for our example Working Copy tree. Items scheduled for either addition or deletion are not reported. We are finished:

   1         ...
   2         
   3         //called at the end of a report
   4         reporter.finishReport( );
   5         
   6         ...
   7     }
   8 }

If any method of ISVNReporter throws an exception you should abort the reorter in the following way:

   1         ...
   2         
   3         try {
   4             ...        
   5             reporter.setPath( ... );
   6             ...
   7         } catch( SVNException svne ) {
   8             reporter.abortReport( );
   9             ...
  10         }
  11         
  12         ...

Receiving changes from a server-side

Now we know that when you call an update method of an SVNRepository driver, the driver first invokes your reporter baton to get a client's report of a local tree state:

   1     ...
   2 
   3     ISVNReporter reporter = ...;
   4     reporterBaton.report( reporter );
   5 
   6     ...

If our local tree (Working Copy, for instance) is successfully reported the diver calculates changes between our tree contents and what is in the repository. The driver passes these changes to the caller's editor as it traverses the tree. In other words, the driver edits our tree in a hierarchical way.

Let's proceed with our example. We have discussed how we should report our example Working Copy tree. Now we'll speak of how a server-side invokes our editor. First of all, it sets the actual revision which a local tree will be updated to, then opens the root node (the root of the Working Copyin in our case) and traverses the tree approximately like this:

   1     ...
   2     //let HEAD revision be 7
   3     editor.targetRevision( 7 );
   4 
   5     //gives the source revision we provided in our report
   6     editor.openRoot( 4 );
   7 
   8     //in revision 7 properties were added for nodeB
   9     editor.openDir( "nodeB" , 5 );
  10     editor.changeDirProperty( "prop1" , "val1" );
  11     editor.changeDirProperty( "prop2" , "val2" );
  12     editor.closeDir( );
  13 
  14     ...
  15     
  16     //receiving changes for a switched node - nodeD
  17     editor.openDir( "nodeC" , 4 );
  18     editor.openDir( "nodeC/nodeD" , 4 );
  19     //itemD2 was added under /node2A/nodeC/nodeD/ in the repository
  20     editor.addFile( "nodeC/nodeD/itemD2" , null , -1 );
  21     editor.applyTextDelta( "nodeC/nodeD/itemD2" , null );
  22     editor.textDeltaChunk( "nodeC/nodeD/itemD2" , window1 );
  23     ...
  24     editor.textDeltaEnd( "nodeC/nodeD/itemD2" );
  25     //text checksum
  26     editor.closeFile( "nodeC/nodeD/itemD2" , checksum );
  27     //closing nodeC/nodeD
  28     editor.closeDir( );
  29     //closing nodeC
  30     editor.closeDir( );
  31 
  32     ...
  33     
  34     //we are still not permitted to read /nodeA/nodeF,
  35     //this is how a server lets us know about this
  36     editor.absentDir( "nodeF" );
  37     
  38     ...

All items which are located under an incomplete directory and have got the same revision as the incomplete parent's one are ADDED once again. But those items that have got revisions different from the incomplete parent's one will rather receive differences.

   1     editor.openDir( "nodeH" , 4 );
   2     editor.addDir( "nodeH/nodeH2" , null , -1 );
   3     //closing nodeH/nodeH2
   4     editor.closeDir( );
   5 
   6     editor.addDir( "nodeH/nodeH3" , null , -1 );
   7     //closing nodeH/nodeH3
   8     editor.closeDir( );
   9 
  10     editor.addFile( "nodeH/itemH2" , null , -1 );
  11     editor.applyTextDelta( "nodeH/itemH2" , null );
  12     //sending delta windows
  13     ...
  14     editor.textDeltaEnd( "nodeH/itemH2" );
  15     editor.closeFile( "nodeH/itemH2" , checksum );
  16 
  17     //receiving changes for nodeH/itemH
  18     editor.openFile( "nodeH/itemH" , 5 );
  19     editor.applyTextDelta( "nodeH/itemH" , baseChecksum );
  20     //sending delta windows
  21     ...
  22     editor.textDeltaEnd( "nodeH/itemH" );
  23     editor.closeFile( "nodeH/itemH" , checksum );
  24     //closing nodeH
  25     editor.closeDir( );
  26     ...
  27     
  28     //the lock on itemA2 was broken in the repository
  29     editor.changeFileProperty( "itemA2" , SVNProperty.LOCK_TOKEN , null );
  30     
  31     ...
  32     
  33     //receiving a missing node - /nodeA/nodeL
  34     editor.addDir( "nodeL" , null , -1 );
  35     ...
  36     editor.closeDir( );
  37     
  38     ...

And so forth.

   1     ...
   2     
   3     //closes the WC root
   4     editor.closeDir( );
   5     //finishes editing
   6     ediotr.closeEdit( );
   7     
   8     ...

The update is finished. Now our Working Copy looks like this:

Update_Tree2.png

This is how a local tree is traversed and applied changes coming from a repository. To a certain extent, this is only an example, a scheme. Besides versioned properties files as well as directories receive some metadata - unmanaged (by a user) properties used for version control. We don't show them in our demonstration code. Nevertheless the main idea is correct.

With a reporter (ISVNReporter) and an editor (ISVNEditor) you are not restricted by a Working Copy format. The SVNKit high-level engine implements an editor that stores a local data tree as directories and files within a Working Copy, but you can choose a different format of saving received versioned data for your editor.

Example: exporting a repository directory

In Subversion export is like checkout except that exported directories are clean, not versioned since they don't have administrative directories. This example demonstrates usage of ISVNReporter and ISVNEditor for exporting a directory from a repository.

We implement the following reporter that reports our local tree as being empty:

   1 public class ExportReporterBaton implements ISVNReporterBaton {
   2 
   3     private long exportRevision;
   4         
   5     public ExportReporterBaton( long revision ){
   6         exportRevision = revision;
   7     }
   8         
   9     public void report( ISVNReporter reporter ) throws SVNException {
  10         try {
  11             reporter.setPath( "" , null , exportRevision , true );
  12             reporter.finishReport( );
  13         } catch( SVNException svne ) {
  14             reporter.abortReport( );
  15             System.out.println( "Report failed" );
  16         }
  17     }
  18 }

And the editor which performs minimal work to save a coming versioned tree as files and directories:

   1 public class ExportEditor implements ISVNEditor {
   2         
   3     private File myRootDirectory;
   4     private SVNDeltaProcessor myDeltaProcessor;
   5         
   6     public ExportEditor( File root ) {
   7         myRootDirectory = root;
   8 
   9         /*
  10          * Utility class that will help us to transform 'deltas' sent by the 
  11          * server to the new file contents.  
  12          */
  13         myDeltaProcessor = new SVNDeltaProcessor( );
  14     }
  15 
  16     public void targetRevision( long revision ) throws SVNException {
  17     }
  18 
  19     public void openRoot( long revision ) throws SVNException {
  20     }
  21         
  22     public void addDir( String path , String copyFromPath , long copyFromRevision ) throws SVNException {
  23         File newDir = new File( myRootDirectory , path );
  24         if ( !newDir.exists( ) ) {
  25             if ( !newDir.mkdirs( ) ) {
  26                 SVNErrorMessage err = SVNErrorMessage.create( SVNErrorCode.IO_ERROR , "error: failed to add the directory ''{0}''." , newDir );
  27                 throw new SVNException( err );
  28             }
  29         }
  30         System.out.println( "dir added: " + path );
  31     }
  32         
  33     public void openDir( String path , long revision ) throws SVNException {
  34     }
  35 
  36     public void changeDirProperty( String name , String value ) throws SVNException {
  37     }
  38 
  39     public void addFile( String path , String copyFromPath , long copyFromRevision ) throws SVNException {
  40         File file = new File( myRootDirectory , path );
  41         if ( file.exists( ) ) {
  42             SVNErrorMessage err = SVNErrorMessage.create( SVNErrorCode.IO_ERROR , "error: exported file ''{0}'' already exists!" , file );
  43             throw new SVNException( err );
  44         }
  45 
  46         try {
  47             file.createNewFile( );
  48         } catch ( IOException e ) {
  49             SVNErrorMessage err = SVNErrorMessage.create( SVNErrorCode.IO_ERROR , "error: cannot create new  file ''{0}''" , file );
  50             throw new SVNException( err );
  51         }
  52     }
  53         
  54     public void openFile( String path , long revision ) throws SVNException {
  55     }
  56 
  57     public void changeFileProperty( String path , String name , String value ) throws SVNException {
  58     }        
  59 
  60     public void applyTextDelta( String path , String baseChecksum ) throws SVNException {
  61         myDeltaProcessor.applyTextDelta( null , new File( myRootDirectory , path ) , false );
  62     }
  63 
  64     public OutputStream textDeltaChunk( String path , SVNDiffWindow diffWindow )   throws SVNException {
  65         return myDeltaProcessor.textDeltaChunk( diffWindow );
  66     }
  67         
  68     public void textDeltaEnd(String path) throws SVNException {
  69         myDeltaProcessor.textDeltaEnd( );
  70     }
  71         
  72     public void closeFile( String path , String textChecksum ) throws SVNException {
  73         System.out.println( "file added: " + path );
  74     }
  75 
  76     public void closeDir( ) throws SVNException {
  77     }
  78 
  79     public void deleteEntry( String path , long revision ) throws SVNException {
  80     }
  81         
  82     public void absentDir( String path ) throws SVNException {
  83     }
  84 
  85     public void absentFile( String path ) throws SVNException {
  86     }        
  87         
  88     public SVNCommitInfo closeEdit( ) throws SVNException {
  89         return null;
  90     }
  91         
  92     public void abortEdit( ) throws SVNException {
  93     }
  94 }

Having got these two implementations we export a directory from a world-readable repository:

   1 public class Export {
   2     
   3     public static void main( String[] args ) { 
   4         DAVRepositoryFactory.setup( );
   5         SVNURL url = SVNURL.parseURIEncoded( "http://svn.svnkit.com/repos/svnkit/tags/1.3.5/doc" );
   6         String userName = "foo";
   7         String userPassword = "bar";
   8         
   9         //Prepare filesystem directory (export destination).
  10         File exportDir = new File( "export" );
  11         if ( exportDir.exists( ) ) {
  12             SVNErrorMessage err = SVNErrorMessage.create( SVNErrorCode.IO_ERROR , "Path ''{0}'' already exists" , exportDir );
  13             throw new SVNException( err );
  14         }
  15         exportDir.mkdirs( );
  16 
  17         SVNRepository repository = SVNRepositoryFactory.create( url );
  18 
  19         ISVNAuthenticationManager authManager = SVNWCUtil.createDefaultAuthenticationManager( userName, userPassword );
  20         repository.setAuthenticationManager( authManager );
  21 
  22         SVNNodeKind nodeKind = repository.checkPath( "" , -1 );
  23         if ( nodeKind == SVNNodeKind.NONE ) {
  24             SVNErrorMessage err = SVNErrorMessage.create( SVNErrorCode.UNKNOWN , "No entry at URL ''{0}''" , url );
  25             throw new SVNException( err );
  26         } else if ( nodeKind == SVNNodeKind.FILE ) {
  27             SVNErrorMessage err = SVNErrorMessage.create( SVNErrorCode.UNKNOWN , "Entry at URL ''{0}'' is a file while directory was expected" , url );
  28             throw new SVNException( err );
  29         }
  30 
  31         //Get latest repository revision. We will export repository contents at this very revision.
  32         long latestRevision = repository.getLatestRevision( );
  33         
  34         ISVNReporterBaton reporterBaton = new ExportReporterBaton( latestRevision );
  35         
  36         ISVNEditor exportEditor = new ExportEditor( exportDir );
  37         
  38         /*
  39          * Now ask SVNKit to perform generic 'update' operation using our reporter and editor.
  40          * 
  41          * We are passing:
  42          * 
  43          * - revision from which we would like to export
  44          * - null as "target" name, to perform export from the URL SVNRepository was created for, 
  45          *   not from some child directory.
  46          * - reporterBaton
  47          * - exportEditor.  
  48          */
  49         repository.update( latestRevision , null , true , reporterBaton , exportEditor );
  50         
  51         System.out.println( "Exported revision: " + latestRevision );
  52     
  53     }
  54 }


Download the example program source code.

Updating_From_A_Repository (last edited 2012-01-03 18:05:02 by ip-109-80-120-205)