SVNKit Low Level API 使用(2)

 编辑操作从一个资源库接收修改  

       ISVNEditor的另一个使用领域,更新操作(checkout, update, switch, export)。更新操作正好与提交操作相反。也就是说,就像提交操作一样,你将使用编辑器来操作存储库。在一个更新操作中,您需要提供自己的ISVNEditor实现,它将被存储库服务器调用,传输更改到客户端。如果使用远程访问存储库,这意味着,根据一个特定的访问协议向服务器发送的命令被SVNKit翻译成调用客户端的编辑器。

 

因此,一个低级别的更新操作需要两个主要步骤:

  • 客户端应用程序向资源库描述本地本地条目的版本状态;
  • 服务器端根据客户端提供的本地条目版本状态,来决定哪些条目需要更新,这种策略由客户端的editor提供实现。

这种机制提供一定的自由选择的格式存储本地的版本树,意味着什么,你不会受到一般工作副本的边界限制。

 

报告本地的版本树状态到服务器端

  本地的版本树的状态通过ISVNReporterinterface来描述,我们将研究如何报告当地的版本,我们将介绍SVNKit是如何如何使用ISVNReporter报告条目的状态。

让我们想象一下,我们得到了以下的工作副本树:

 

用户执行递归更新,将工作副本的根更新到最新(HEAD)版本。这种情况下,SVNKit如何报告本地版本?首先,通过ISVNReporterBaton的实现,将报告提供给SVNRepository驱动器。当你把它传递SVNRepository的一个更新操作,后者传递一个ISVNReporter对象调用baton的doRepot()方法。

更新到HEAD版本

    ...
    FSRepositoryFactory.setup( );
    String url = "file://localhost/rep/nodeA/";

    SVNRepository repository = SVNRepositoryFactory.create( SVNURL.parseURIDecoded( url ) );
    ReporterBaton reporterBaton = ...;
    ISVNEditor editor = ...;
    
    repository.update( -1 /*forces to use the latest revision*/ , null /*target*/ , true /*recursive*/ , reporterBaton , editor );
    ...

 

库驱动传递一个ISVNReporter对象到reporter baton的doReport()方法:

...
public class ReporterBaton implements ISVNReporterBaton {

    ...
    
    public void report( ISVNReporter reporter ) throws SVNException {
        
        //for the WC root
        reporter.setPath( "" /*path*/ , null /*lockToken*/ , 4 /*revision*/ , false /*startEmpty*/ );

        ...

 

这里我们需要停下来做一些重要的解释,ISVNReporte的第一个方法调用总是setPath("" , ...),如果更新方法的目标参数是空(在我们的例子一样),驱动程序被绑定到/nodeA上的位置对应的节点。否则,如果目标参数不为空,这是一个目标更新条目,在这种情况下,setpath("",...)描述了这一目标条目。注意目标但始终是一个名字,而不是路径。例如,如果我们想只更新itemA1,我们会调用:

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

 

此外,我们遍历树:

...

reporter.setPath( "nodeB" , null , 5 , false );

...

 

 我们没有必要对/nodeA/nodeB/itemB 进行report,因为它当前的版本和父节点的版本是一样的并且没有本地锁定。同样的原因,我们对/nodeA/itemA1也不report。 但是我们必须 report /nodeA/itemA2,虽然它的版本和父节点的版本一致,但是它是本地锁定的。所以也许在资源库中​​锁定被打破。

       ...

        //provide the item's lock token
        String lockToken = ...;
        reporter.setPath( "itemA2" , lockToken , 4 , false );

        ...

 

继续我们的report:

        ...

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

        ...

 

虽然 /nodeC/nodeD/的本地版本号和我们需要report的父节点的版本号一致,我们还是需要report它,因为它切换到了资源库的另外一个位置。

        ...
        reporter.deletePath( "nodeF" );
        reporter.deletePath( "nodeG" );
        reporter.deletePath( "nodeL" );
        ...

 

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.

 

        ...

        reporter.setPath( "nodeH" , null , 4 , true /*startEmpty*/ );
        reporter.setPath( "nodeH/nodeH2" , null , 4 , false /*startEmpty*/ );
        reporter.setPath( "nodeH/itemH" , null , 5 , false /*startEmpty*/ );

        ...

 

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:

        long rev = ...;
        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:

        ...
        
        //called at the end of a report
        reporter.finishReport( );
        
        ...
    }
}

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

        ...
        
        try {
            ...        
            reporter.setPath( ... );
            ...
        } catch( SVNException svne ) {
            reporter.abortReport( );
            ...
        }
        
        ...

 

 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:

    ...

    ISVNReporter reporter = ...;
    reporterBaton.report( reporter );

    ...

 

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: 

 

    ...
    //let HEAD revision be 7
    editor.targetRevision( 7 );

    //gives the source revision we provided in our report
    editor.openRoot( 4 );

    //in revision 7 properties were added for nodeB
    editor.openDir( "nodeB" , 5 );
    editor.changeDirProperty( "prop1" , "val1" );
    editor.changeDirProperty( "prop2" , "val2" );
    editor.closeDir( );

    ...
    
    //receiving changes for a switched node - nodeD
    editor.openDir( "nodeC" , 4 );
    editor.openDir( "nodeC/nodeD" , 4 );
    //itemD2 was added under /node2A/nodeC/nodeD/ in the repository
    editor.addFile( "nodeC/nodeD/itemD2" , null , -1 );
    editor.applyTextDelta( "nodeC/nodeD/itemD2" , null );
    editor.textDeltaChunk( "nodeC/nodeD/itemD2" , window1 );
    ...
    editor.textDeltaEnd( "nodeC/nodeD/itemD2" );
    //text checksum
    editor.closeFile( "nodeC/nodeD/itemD2" , checksum );
    //closing nodeC/nodeD
    editor.closeDir( );
    //closing nodeC
    editor.closeDir( );

    ...
    
    //we are still not permitted to read /nodeA/nodeF,
    //this is how a server lets us know about this
    editor.absentDir( "nodeF" );
    
    ...

 

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. 

 

    editor.openDir( "nodeH" , 4 );
    editor.addDir( "nodeH/nodeH2" , null , -1 );
    //closing nodeH/nodeH2
    editor.closeDir( );

    editor.addDir( "nodeH/nodeH3" , null , -1 );
    //closing nodeH/nodeH3
    editor.closeDir( );

    editor.addFile( "nodeH/itemH2" , null , -1 );
    editor.applyTextDelta( "nodeH/itemH2" , null );
    //sending delta windows
    ...
    editor.textDeltaEnd( "nodeH/itemH2" );
    editor.closeFile( "nodeH/itemH2" , checksum );

    //receiving changes for nodeH/itemH
    editor.openFile( "nodeH/itemH" , 5 );
    editor.applyTextDelta( "nodeH/itemH" , baseChecksum );
    //sending delta windows
    ...
    editor.textDeltaEnd( "nodeH/itemH" );
    editor.closeFile( "nodeH/itemH" , checksum );
    //closing nodeH
    editor.closeDir( );
    ...
    
    //the lock on itemA2 was broken in the repository
    editor.changeFileProperty( "itemA2" , SVNProperty.LOCK_TOKEN , null );
    
    ...
    
    //receiving a missing node - /nodeA/nodeL
    editor.addDir( "nodeL" , null , -1 );
    ...
    editor.closeDir( );
    
    ...

 

And so forth. 

 

    ...
    
    //closes the WC root
    editor.closeDir( );
    //finishes editing
    ediotr.closeEdit( );
    
    ...

 

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

 

 

 

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:

public class ExportReporterBaton implements ISVNReporterBaton {

    private long exportRevision;
        
    public ExportReporterBaton( long revision ){
        exportRevision = revision;
    }
        
    public void report( ISVNReporter reporter ) throws SVNException {
        try {
            reporter.setPath( "" , null , exportRevision , true );
            reporter.finishReport( );
        } catch( SVNException svne ) {
            reporter.abortReport( );
            System.out.println( "Report failed" );
        }
    }
}

 

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

public class ExportEditor implements ISVNEditor {
        
    private File myRootDirectory;
    private SVNDeltaProcessor myDeltaProcessor;
        
    public ExportEditor( File root ) {
        myRootDirectory = root;

        /*
         * Utility class that will help us to transform 'deltas' sent by the 
         * server to the new file contents.  
         */
        myDeltaProcessor = new SVNDeltaProcessor( );
    }

    public void targetRevision( long revision ) throws SVNException {
    }

    public void openRoot( long revision ) throws SVNException {
    }
        
    public void addDir( String path , String copyFromPath , long copyFromRevision ) throws SVNException {
        File newDir = new File( myRootDirectory , path );
        if ( !newDir.exists( ) ) {
            if ( !newDir.mkdirs( ) ) {
                SVNErrorMessage err = SVNErrorMessage.create( SVNErrorCode.IO_ERROR , "error: failed to add the directory ''{0}''." , newDir );
                throw new SVNException( err );
            }
        }
        System.out.println( "dir added: " + path );
    }
        
    public void openDir( String path , long revision ) throws SVNException {
    }

    public void changeDirProperty( String name , String value ) throws SVNException {
    }

    public void addFile( String path , String copyFromPath , long copyFromRevision ) throws SVNException {
        File file = new File( myRootDirectory , path );
        if ( file.exists( ) ) {
            SVNErrorMessage err = SVNErrorMessage.create( SVNErrorCode.IO_ERROR , "error: exported file ''{0}'' already exists!" , file );
            throw new SVNException( err );
        }

        try {
            file.createNewFile( );
        } catch ( IOException e ) {
            SVNErrorMessage err = SVNErrorMessage.create( SVNErrorCode.IO_ERROR , "error: cannot create new  file ''{0}''" , file );
            throw new SVNException( err );
        }
    }
        
    public void openFile( String path , long revision ) throws SVNException {
    }

    public void changeFileProperty( String path , String name , String value ) throws SVNException {
    }        

    public void applyTextDelta( String path , String baseChecksum ) throws SVNException {
        myDeltaProcessor.applyTextDelta( null , new File( myRootDirectory , path ) , false );
    }

    public OutputStream textDeltaChunk( String path , SVNDiffWindow diffWindow )   throws SVNException {
        return myDeltaProcessor.textDeltaChunk( diffWindow );
    }
        
    public void textDeltaEnd(String path) throws SVNException {
        myDeltaProcessor.textDeltaEnd( );
    }
        
    public void closeFile( String path , String textChecksum ) throws SVNException {
        System.out.println( "file added: " + path );
    }

    public void closeDir( ) throws SVNException {
    }

    public void deleteEntry( String path , long revision ) throws SVNException {
    }
        
    public void absentDir( String path ) throws SVNException {
    }

    public void absentFile( String path ) throws SVNException {
    }        
        
    public SVNCommitInfo closeEdit( ) throws SVNException {
        return null;
    }
        
    public void abortEdit( ) throws SVNException {
    }
}


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

 

public class Export {
    
    public static void main( String[] args ) { 
        DAVRepositoryFactory.setup( );
        SVNURL url = SVNURL.parseURIEncoded( "http://svn.svnkit.com/repos/svnkit/tags/1.3.5/doc" );
        String userName = "foo";
        String userPassword = "bar";
        
        //Prepare filesystem directory (export destination).
        File exportDir = new File( "export" );
        if ( exportDir.exists( ) ) {
            SVNErrorMessage err = SVNErrorMessage.create( SVNErrorCode.IO_ERROR , "Path ''{0}'' already exists" , exportDir );
            throw new SVNException( err );
        }
        exportDir.mkdirs( );

        SVNRepository repository = SVNRepositoryFactory.create( url );

        ISVNAuthenticationManager authManager = SVNWCUtil.createDefaultAuthenticationManager( userName, userPassword );
        repository.setAuthenticationManager( authManager );

        SVNNodeKind nodeKind = repository.checkPath( "" , -1 );
        if ( nodeKind == SVNNodeKind.NONE ) {
            SVNErrorMessage err = SVNErrorMessage.create( SVNErrorCode.UNKNOWN , "No entry at URL ''{0}''" , url );
            throw new SVNException( err );
        } else if ( nodeKind == SVNNodeKind.FILE ) {
            SVNErrorMessage err = SVNErrorMessage.create( SVNErrorCode.UNKNOWN , "Entry at URL ''{0}'' is a file while directory was expected" , url );
            throw new SVNException( err );
        }

        //Get latest repository revision. We will export repository contents at this very revision.
        long latestRevision = repository.getLatestRevision( );
        
        ISVNReporterBaton reporterBaton = new ExportReporterBaton( latestRevision );
        
        ISVNEditor exportEditor = new ExportEditor( exportDir );
        
        /*
         * Now ask SVNKit to perform generic 'update' operation using our reporter and editor.
         * 
         * We are passing:
         * 
         * - revision from which we would like to export
         * - null as "target" name, to perform export from the URL SVNRepository was created for, 
         *   not from some child directory.
         * - reporterBaton
         * - exportEditor.  
         */
        repository.update( latestRevision , null , true , reporterBaton , exportEditor );
        
        System.out.println( "Exported revision: " + latestRevision );
    
    }
}

 

 

[原文:http://wiki.svnkit.com/Updating_From_A_Repository]

[实例源代码下载:http://svn.svnkit.com/repos/svnkit/tags/1.3.5/doc/examples/src/org/tmatesoft/svn/examples/repository/Export.java]

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值