You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@subversion.apache.org by pr...@apache.org on 2013/07/09 10:48:47 UTC

svn commit: r1501136 [3/10] - in /subversion/branches/verify-keep-going: ./ build/ build/ac-macros/ build/generator/ subversion/bindings/javahl/native/ subversion/bindings/javahl/src/org/apache/subversion/javahl/ subversion/bindings/javahl/src/org/apac...

Modified: subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/ISVNRemote.java
URL: http://svn.apache.org/viewvc/subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/ISVNRemote.java?rev=1501136&r1=1501135&r2=1501136&view=diff
==============================================================================
--- subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/ISVNRemote.java (original)
+++ subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/ISVNRemote.java Tue Jul  9 08:48:43 2013
@@ -28,6 +28,8 @@ import org.apache.subversion.javahl.call
 
 import java.util.Date;
 import java.util.Map;
+import java.util.Set;
+import java.io.OutputStream;
 
 /**
  * Encapsulates an RA session object and related operations.
@@ -43,7 +45,7 @@ public interface ISVNRemote
     void dispose();
 
     /**
-     * Cancel the active operation.
+     * Cancel the active operation, including any ongoing edits.
      * @throws ClientException
      */
     void cancelOperation() throws ClientException;
@@ -155,10 +157,280 @@ public interface ISVNRemote
             throws ClientException;
 
     /**
-     * Create a commit editor instance, rooted at the current session URL.
+     * Return an editor for committing changes to the session's
+     * repository, setting the revision properties from
+     * <code>revisionProperties</code>. The revisions being committed
+     * against are passed to the editor functions. The root of the commit
+     * is the session's URL.
+     * <p>
+     * <code>revisionProperties</code> is a hash mapping property names to
+     * property values. The commit log message is expected to be in the
+     * {@link Property#REV_LOG} element.  <code>revisionProperties</code>
+     * can not contain either of {@link Property#REV_DATE} or
+     * {@link Property#REV_AUTHOR}.
+     * <p>
+     * Before {@link ISVNEditor#complete()} returns, but after the commit
+     * has succeeded, it will invoke <code>commitCallback</code> (if not
+     * <code>null</code>) with filled-in {@link CommitInfo}.  If
+     * <code>commitCallback</code> returns an error, that error will be
+     * returned from {@link ISVNEditor#complete()}, otherwise
+     * {@link ISVNEditor#complete()} will return successfully (unless it
+     * encountered an error before invoking <code>commitCallback</code>).
+     * The callback will not be called if the commit was a no-op
+     * (i.e., nothing was committed).
+     * <p>
+     * <code>lockTokens</code>, if not <code>null</code>, is a hash
+     * mapping paths (relative to the session's URL) to lock tokens.  The
+     * server checks that the correct token is provided for each
+     * committed, locked path.  <code>lockTokens</code> must live during
+     * the whole commit operation.
+     * <p>
+     * If <cpde>keepLocks</code> is <cpde>true</code>, then do not release
+     * locks on committed objects.  Else, automatically release such
+     * locks.
+     * <p>
+     * The caller may not perform any remote operations using this session
+     * before finishing the edit.
+     * @throws ClientException
+     */
+    ISVNEditor getCommitEditor(Map<String, byte[]> revisionProperties,
+                               CommitCallback commitCallback,
+                               Set<Lock> lockTokens,
+                               boolean keepLocks)
+            throws ClientException;
+
+    /**
+     * Fetch the contents and properties of file <code>path</code> at
+     * <code>revision</code>.  <code>revision</code> may be
+     * {@link org.apache.subversion.javahl.types.Revision#SVN_INVALID_REVNUM}
+     * indicating that the HEAD revision should be
+     * used. <code>path</code> is interpreted relative to the
+     * session's URL.
+     * <p>
+
+     * If <code>revision</code> is
+     * {@link org.apache.subversion.javahl.types.Revision#SVN_INVALID_REVNUM}.
+     * returns the actual revision that was retrieved; otherwise
+     * returns <code>revision</code>.
+     * <p>
+     * If <code>contents</code> is not <code>null</code>, push the
+     * contents of the file into the stream.
+     * <p>
+     * If <code>properties</code> is not <code>null</code>, set
+     * <code>properties</code> to contain the properties of the file. This
+     * means <em>all</em> properties: not just ones controlled by the
+     * user and stored in the repository, but immutable ones generated
+     * by the SCM system itself (e.g. 'wcprops', 'entryprops',
+     * etc.). Any existing contents of the <code>properties</code> map
+     * will be discarded by calling {@link java.util.Map#clear()}, if the
+     * map implementation supports that operation.
+     * <p>
+     * The implementations of <code>contents</code> and
+     * <code>properties</code> may not perform any ISVNRemote
+     * operations using this session.
+     * @return The revision of the file that was retreived.
+     * @throws ClientException
+     */
+    long getFile(long revision, String path,
+                 OutputStream contents,
+                 Map<String, byte[]> properties)
+            throws ClientException;
+
+    /**
+     * Fetch the contents and properties of directory <code>path</code>
+     * at <code>revision</code>.  <code>revision</code> may be
+     * {@link org.apache.subversion.javahl.types.Revision#SVN_INVALID_REVNUM},
+     * indicating that the HEAD revision should be
+     * used. <code>path</code> is interpreted relative to the
+     * session's URL.
+     * <p>
+     * If <code>dirents</code> is not <code>null</code>, it will
+     * contain all the entries of the directory; the keys will be the
+     * entry basenames.  Any existing contente of the
+     * <code>dirents</code> collection will be discarded by calling
+     * {@link java.util.Map#clear()}, if the collection implementation
+     * supports that operation.
+     * <p>
+     * <code>direntFields</code> controls which portions of the DirEntry
+     * objects are filled in. To have them completely filled in, just pass
+     * DirEntry.Fields.all, othewise pass a bitwise OR of any of the
+     * DirEntry.Fields flags you would like to have.
+     * <p>
+     * If <code>properties</code> is not <code>null</code>, set
+     * <code>properties</code> to contain the properties of the directory.
+     * This means <em>all</em> properties: not just ones controlled by the
+     * user and stored in the repository, but immutable ones generated
+     * by the SCM system itself (e.g. 'wcprops', 'entryprops',
+     * etc.). Any existing contents of the <code>properties</code> map
+     * will be discarded by calling {@link java.util.Map#clear()}, if the
+     * map implementation supports that operation.
+     * <p>
+     * The implementations of <code>dirents</code> and
+     * <code>properties</code> may not perform any ISVNRemote
+     * operations using this session.
+     * @return The revision of the directory that was retreived.
+     * @throws ClientException
+     */
+    long getDirectory(long revision, String path,
+                      int direntFields,
+                      Map<String, DirEntry> dirents,
+                      Map<String, byte[]> properties)
+            throws ClientException;
+
+    /**
+     * Retreive the merginfo for <code>paths</code>, whose elements
+     * are relative to the session's URL. The request will fail if any
+     * one of <code>paths</code> does not exist in the given
+     * <code>revision</code>.
+     * <p>
+     * <b>Note:</b> If the server doesn't support retrieval of
+     * mergeinfo (which can happen even for file:// URLs, if the
+     * repository itself hasn't been upgraded), an unsupported feature
+     * exception is thrown in preference to any other error that might
+     * otherwise be returned.
+     *
+     * @param revision The revision to look for <code>paths</code>
+     *                 in. Defaults to the youngest revision when
+     *                 {@link Revision.SVN_INVALID_REVNUM}.
+     * @param inherit Indicates whether explicit, explicit or
+     *                inherited, or only inherited mergeinfo for
+     *                <code>paths</code> is retrieved.
+     * @param includeDescendants When <code>true</code>, additionally
+     *        return the mergeinfo for any descendant of any element
+     *        of <code>paths</code> which has the mergeinfo explicitly
+     *        set on it.  (Note that inheritance is only taken into
+     *        account for the elements in <code>paths</code>;
+     *        descendants of the elements in <code>paths</code> which
+     *        get their mergeinfo via inheritance are not included.)
+     *
+     * @return A dictionary of {@link Mergeinfo} objects for the given
+     *         <code>paths</code>, or <code>null</code> if no
+     *         mergeinfo is available..
      * @throws ClientException
      */
-    ISVNEditor getCommitEditor() throws ClientException;
+    Map<String, Mergeinfo> getMergeinfo(Iterable<String> paths, long revision,
+                                        Mergeinfo.Inheritance inherit,
+                                        boolean includeDescendants)
+            throws ClientException;
+
+    // TODO: update
+    // TODO: switch
+
+    /**
+     * Ask for a description of the status of a working copy with
+     * respect to <code>revision</code> of the session's repository,
+     * or the HEAD revision if <code>revision</code> is
+     * {@link org.apache.subversion.javahl.types.Revision#SVN_INVALID_REVNUM}.
+     * <p>
+     * The client begins by providing a <code>receiver</code> to
+     * the remote session; this object must contain knowledge of where
+     * the change will begin in the working copy.
+     * <p>
+     * In return, the client receives an {@link ISVNReporter}
+     * instance, which it uses to describe its working copy by making
+     * calls to its methods.
+     * <p>
+     * When finished, the client calls {@link ISVNReporter#finishReport}.
+     * This results in <code>receiver</code> being called once for
+     * every path in the working copy that is different from the
+     * repository. <code>statusTarget</code> is an optional single
+     * path component that restricts the scope of the status report to
+     * an entry in the directory represented by the session's URL, or
+     * empty if the entire directory is meant to be examined.
+     * <p>
+     * Get status as deeply as <code>depth</code> indicates.  If
+     * <code>depth</code> is
+     * {@link org.apache.subversion.javahl.types.Depth#unknown},
+     * get the status down to the ambient depth of the working
+     * copy. If <code>depth</code> is deeper than the working copy,
+     * include changes that would be needed to populate the working
+     * copy to that depth.
+     * <p>
+     * The caller may not perform any operations using this session
+     * before finishing the report, and may not perform any operations
+     * using this session from within the implementation of
+     * <code>receiver</code>.
+     * <p>
+     * <b>Note:</b> The reporter provided by this function does
+     * <em>not</em> supply copy-from information to the editor
+     * methods.
+     * <p>
+     * <b>Note:</b> In order to prevent pre-1.5 servers from doing
+     * more work than needed, and sending too much data back, a
+     * pre-1.5 'recurse' directive may be sent to the server, based on
+     * <code>depth</code>.
+     * @throws ClientException
+     */
+    ISVNReporter status(String statusTarget,
+                        long revision, Depth depth,
+                        RemoteStatus receiver)
+            throws ClientException;
+
+    // TODO: diff
+
+    /**
+     * Invoke <code>callback</code> for each log message from
+     * <code>start</code> to <code>end</code>.  <code>start</code> may be greater or less than <code>end</code>;
+     * this just controls whether the log messages are processed in descending
+     * or ascending revision number order.
+     * <p>
+     * If <code>start</code> or <code>end</code> is
+     * {@link org.apache.subversion.javahl.types.Revision#SVN_INVALID_REVNUM},
+     * the HEAD revision is uses for that argument. If eiter is an
+     * invaild non-existent revision, an error will be returned.
+     * <p>
+     * If <code>paths</code> is not <code>null</code> and has one or
+     * more elements, then only show revisions in which at least one
+     * of <code>paths</code> was changed (i.e., if file, text or props
+     * changed; if dir, props changed or an entry was added or
+     * deleted).
+     * <p>
+     * If <code>limit</code> is non-zero only invoke @a receiver on
+     * the first code>limit</code> logs.
+     * <p>
+     * If <code>discoverPath</code> is set, then each call to
+     * <code>callback</code> the list of changed paths in that
+     * revision.
+     * <p>
+     * If <code>strictNodeHistory</code> is set, copy history will not be
+     * traversed (if any exists) when harvesting the revision logs for
+     * each path.
+     * <p>
+     * If <code>includeMergedRevisions</code> is set, log information
+     * for revisions which have been merged to @a targets will also be
+     * returned.
+     * <p>
+     * If <code>revisionProperties</code> is <code>null</code>,
+     * retrieve all revision properties; otherwise, retrieve only the
+     * revision properties contained in the set (i.e. retrieve none if
+     * the set is empty).
+     * <p>
+     * The implementation of <code>callback</code> may not perform any
+     * operations using this session. If the invocation of
+     * <code>callback</code> throws an exception, the operation will
+     * stop.
+     * <p>
+     * <b>Note:</b> If <code>paths</code> is <code>null</code> or
+     * empty, the result depends on the server.  Pre-1.5 servers will
+     * send nothing; 1.5 servers will effectively perform the log
+     * operation on the root of the repository.  This behavior may be
+     * changed in the future to ensure consistency across all
+     * pedigrees of server.
+     * <p>
+     * <b>Note:</b> Pre-1.5 servers do not support custom revprop
+     * retrieval; <code>revisionProperties</code> is <code>null</code>
+     * or contains a revprop other than svn:author, svn:date, or
+     * svn:log, an not-implemented error is returned.
+     *
+     * @throws ClientException
+     */
+    void getLog(Iterable<String> paths,
+                long startRevision, long endRevision, int limit,
+                boolean strictNodeHistory, boolean discoverPath,
+                boolean includeMergedRevisions,
+                Iterable<String> revisionProperties,
+                LogMessageCallback callback)
+            throws ClientException;
 
     /**
      * Return the kind of the node in path at revision.
@@ -168,6 +440,14 @@ public interface ISVNRemote
     NodeKind checkPath(String path, long revision)
             throws ClientException;
 
+    // TODO: stat
+    // TODO: getLocations
+    // TODO: getLocationSegments
+    // TODO: getFileRevisions
+    // TODO: lock
+    // TODO: unlock
+    // TODO: getLock
+
     /**
      * Return a dictionary containing all locks on or below the given path.
      * @param path A path relative to the sessionn URL
@@ -180,6 +460,11 @@ public interface ISVNRemote
     Map<String, Lock> getLocks(String path, Depth depth)
             throws ClientException;
 
+    // TODO: replayRange
+    // TODO: replay
+    // TODO: getDeletedRevision
+    // TODO: getInheritedProperties
+
     /**
      * Check if the server associated with this session has
      * the given <code>capability</code>.

Modified: subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/SVNClient.java
URL: http://svn.apache.org/viewvc/subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/SVNClient.java?rev=1501136&r1=1501135&r2=1501136&view=diff
==============================================================================
--- subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/SVNClient.java (original)
+++ subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/SVNClient.java Tue Jul  9 08:48:43 2013
@@ -573,6 +573,8 @@ public class SVNClient implements ISVNCl
     public native void setConfigDirectory(String configDir)
             throws ClientException;
 
+    public native void setConfigEventHandler(ConfigEvent configHandler);
+
     public native String getConfigDirectory()
             throws ClientException;
 

Modified: subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/remote/CommitEditor.java
URL: http://svn.apache.org/viewvc/subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/remote/CommitEditor.java?rev=1501136&r1=1501135&r2=1501136&view=diff
==============================================================================
--- subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/remote/CommitEditor.java (original)
+++ subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/remote/CommitEditor.java Tue Jul  9 08:48:43 2013
@@ -31,8 +31,8 @@ import org.apache.subversion.javahl.JNIO
 import org.apache.subversion.javahl.ClientException;
 
 import java.io.InputStream;
-import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Implementation of ISVNEditor that drives commits.
@@ -40,132 +40,124 @@ import java.util.Map;
  */
 public class CommitEditor extends JNIObject implements ISVNEditor
 {
-    public void dispose() {/* TODO */}
-
-    public void addDirectory(String relativePath,
-                             Iterable<String> children,
-                             Map<String, byte[]> properties,
-                             long replacesRevision)
-            throws ClientException
-    {
-        notimplemented("addDirectory");
-    }
-
-    public void addFile(String relativePath,
-                        Checksum checksum,
-                        InputStream contents,
-                        Map<String, byte[]> properties,
-                        long replacesRevision)
-            throws ClientException
-    {
-        notimplemented("addFile");
-    }
-
-    public void addSymlink(String relativePath,
-                           String target,
-                           Map<String, byte[]> properties,
-                           long replacesRevision)
-            throws ClientException
-    {
-        notimplemented("addSymlink");
-    }
-
-    public void addAbsent(String relativePath,
-                          NodeKind kind,
-                          long replacesRevision)
-            throws ClientException
-    {
-        notimplemented("addAbsent");
-    }
-
-    public void alterDirectory(String relativePath,
-                               long revision,
-                               Iterable<String> children,
-                               Map<String, byte[]> properties)
-            throws ClientException
-    {
-        notimplemented("alterDirectory");
-    }
-
-    public void alterFile(String relativePath,
-                          long revision,
-                          Checksum checksum,
-                          InputStream contents,
-                          Map<String, byte[]> properties)
-            throws ClientException
-    {
-        notimplemented("alterFile");
-    }
-
-    public void alterSymlink(String relativePath,
-                             long revision,
-                             String target,
-                             Map<String, byte[]> properties)
-            throws ClientException
+    public void dispose()
     {
-        notimplemented("alterSymlink");
+        session.disposeEditor(this);
+        nativeDispose();
     }
 
-    public void delete(String relativePath,
-                       long revision)
-            throws ClientException
-    {
-        notimplemented("delete");
-    }
+    public native void addDirectory(String relativePath,
+                                    Iterable<String> children,
+                                    Map<String, byte[]> properties,
+                                    long replacesRevision)
+            throws ClientException;
+
+    public native void addFile(String relativePath,
+                               Checksum checksum,
+                               InputStream contents,
+                               Map<String, byte[]> properties,
+                               long replacesRevision)
+            throws ClientException;
 
-    public void copy(String sourceRelativePath,
-                     long sourceRevision,
-                     String destinationRelativePath,
-                     long replacesRevision)
-            throws ClientException
-    {
-        notimplemented("copy");
-    }
+    /**
+     * <b>Note:</b> Not implemented.
+     */
+    public native void addSymlink(String relativePath,
+                                  String target,
+                                  Map<String, byte[]> properties,
+                                  long replacesRevision)
+            throws ClientException;
+
+    public native void addAbsent(String relativePath,
+                                 NodeKind kind,
+                                 long replacesRevision)
+            throws ClientException;
+
+    public native void alterDirectory(String relativePath,
+                                      long revision,
+                                      Iterable<String> children,
+                                      Map<String, byte[]> properties)
+            throws ClientException;
+
+    public native void alterFile(String relativePath,
+                                 long revision,
+                                 Checksum checksum,
+                                 InputStream contents,
+                                 Map<String, byte[]> properties)
+            throws ClientException;
 
-    public void move(String sourceRelativePath,
-                     long sourceRevision,
-                     String destinationRelativePath,
-                     long replacesRevision)
-            throws ClientException
-    {
-        notimplemented("move");
-    }
+    /**
+     * <b>Note:</b> Not implemented.
+     */
+    public native void alterSymlink(String relativePath,
+                                    long revision,
+                                    String target,
+                                    Map<String, byte[]> properties)
+            throws ClientException;
+
+    public native void delete(String relativePath,
+                              long revision)
+            throws ClientException;
+
+    public native void copy(String sourceRelativePath,
+                            long sourceRevision,
+                            String destinationRelativePath,
+                            long replacesRevision)
+            throws ClientException;
+
+    public native void move(String sourceRelativePath,
+                            long sourceRevision,
+                            String destinationRelativePath,
+                            long replacesRevision)
+            throws ClientException;
 
-    public void rotate(List<RotatePair> elements) throws ClientException
-    {
-        notimplemented("rotate");
-    }
+//    public native void rotate(Iterable<RotatePair> elements)
+//            throws ClientException;
 
-    public void complete() throws ClientException
-    {
-        notimplemented("complete");
-    }
+    public native void complete() throws ClientException;
 
-    public void abort() throws ClientException
-    {
-        notimplemented("abort");
-    }
+    public native void abort() throws ClientException;
 
     /**
      * This factory method called from RemoteSession.getCommitEditor.
      */
-    static final CommitEditor createInstance(RemoteSession owner)
+    static final
+        CommitEditor createInstance(RemoteSession session,
+                                    Map<String, byte[]> revisionProperties,
+                                    CommitCallback commitCallback,
+                                    Set<Lock> lockTokens,
+                                    boolean keepLocks)
             throws ClientException
     {
-        // FIXME: temporary implementation
-        return new CommitEditor(0L);
+        long cppAddr = nativeCreateInstance(session, revisionProperties,
+                                            commitCallback, lockTokens, keepLocks);
+        return new CommitEditor(cppAddr, session);
     }
 
     /**
-     * This constructor is called from JNI to get an instance.
+     * This constructor is called from the factory to get an instance.
      */
-    protected CommitEditor(long cppAddr)
+    protected CommitEditor(long cppAddr, RemoteSession session)
     {
         super(cppAddr);
+        this.session = session;
     }
 
-    private void notimplemented(String name)
-    {
-        throw new RuntimeException("Not implemented: " + name);
-    }
+    /** Stores a reference to the session that created this editor. */
+    protected RemoteSession session;
+
+    @Override
+    public native void finalize() throws Throwable;
+
+    /*
+     * Wrapped private native implementation declarations.
+     */
+    private native void nativeDispose();
+    private static final native
+        long nativeCreateInstance(RemoteSession session,
+                                  Map<String, byte[]> revisionProperties,
+                                  CommitCallback commitCallback,
+                                  Set<Lock> lockTokens,
+                                  boolean keepLocks)
+            throws ClientException;
 }

Modified: subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/remote/RemoteFactory.java
URL: http://svn.apache.org/viewvc/subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/remote/RemoteFactory.java?rev=1501136&r1=1501135&r2=1501136&view=diff
==============================================================================
--- subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/remote/RemoteFactory.java (original)
+++ subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/remote/RemoteFactory.java Tue Jul  9 08:48:43 2013
@@ -49,11 +49,13 @@ public class RemoteFactory
      * Initializing constructor. Any or all of its arguments may be null.
      */
     public RemoteFactory(String configDirectory,
+                         ConfigEvent configHandler,
                          String username, String password,
                          UserPasswordCallback prompt,
                          ProgressCallback progress)
     {
         setConfigDirectory(configDirectory);
+        setConfigEventHandler(configHandler);
         setUsername(username);
         setPassword(password);
         setPrompt(prompt);
@@ -116,26 +118,45 @@ public class RemoteFactory
         this.configDirectory = configDirectory;
     }
 
+    /**
+     * Set an event handler that will be called every time the
+     * configuration is loaded.
+     */
+    public void setConfigEventHandler(ConfigEvent configHandler)
+    {
+        this.configHandler = configHandler;
+    }
+
 
     /**
      * Open a persistent session to a repository.
+     * <p>
+     * <b>Note:</b> The URL can point to a subtree of the repository.
+     * <p>
+     * <b>Note:</b> The session object inherits the progress callback,
+     * configuration directory and authentication info.
+     *
      * @param url The initial session root URL.
      * @throws RetryOpenSession If the session URL was redirected
      * @throws SubversionException If an URL redirect cycle was detected
      * @throws ClientException
-     * @note The URL can point to a subtree of the repository.
-     * @note The session object inherits the progress callback,
-     *       configuration directory and authentication info.
      */
     public ISVNRemote openRemoteSession(String url)
             throws ClientException, SubversionException
     {
         return open(1, url, null,
-                    configDirectory, username, password, prompt, progress);
+                    configDirectory, configHandler,
+                    username, password, prompt, progress);
     }
 
     /**
      * Open a persistent session to a repository.
+     * <p>
+     * <b>Note:</b> The URL can point to a subtree of the repository.
+     * <p>
+     * <b>Note:</b> The session object inherits the progress callback,
+     * configuration directory and authentication info.
+     *
      * @param url The initial session root URL.
      * @param retryAttempts The number of times to retry the operation
      *        if the given URL is redirected.
@@ -144,9 +165,6 @@ public class RemoteFactory
      * @throws RetryOpenSession If the session URL was redirected
      * @throws SubversionException If an URL redirect cycle was detected
      * @throws ClientException
-     * @note The URL can point to a subtree of the repository.
-     * @note The session object inherits the progress callback,
-     *       configuration directory and authentication info.
      */
     public ISVNRemote openRemoteSession(String url, int retryAttempts)
             throws ClientException, SubversionException
@@ -155,21 +173,27 @@ public class RemoteFactory
             throw new IllegalArgumentException(
                 "retryAttempts must be positive");
         return open(retryAttempts, url, null,
-                    configDirectory, username, password, prompt, progress);
+                    configDirectory, configHandler,
+                    username, password, prompt, progress);
     }
 
     /**
      * Open a persistent session to a repository.
+     * <p>
+     * <b>Note:</b> The URL can point to a subtree of the repository.
+     * <p>
+     * <b>Note:</b> If the UUID does not match the repository,
+     * this function fails.
+     * <p>
+     * <b>Note:</b> The session object inherits the progress callback,
+     * configuration directory and authentication info.
+     *
      * @param url The initial session root URL.
      * @param reposUUID The expected repository UUID; may not be null..
      * @throws IllegalArgumentException If <code>reposUUID</code> is null.
      * @throws RetryOpenSession If the session URL was redirected
      * @throws SubversionException If an URL redirect cycle was detected
      * @throws ClientException
-     * @note The URL can point to a subtree of the repository.
-     * @note If the UUID does not match the repository, this function fails.
-     * @note The session object inherits the progress callback,
-     *       configuration directory and authentication info.
      */
     public ISVNRemote openRemoteSession(String url, String reposUUID)
             throws ClientException, SubversionException
@@ -177,11 +201,21 @@ public class RemoteFactory
         if (reposUUID == null)
             throw new IllegalArgumentException("reposUUID may not be null");
         return open(1, url, reposUUID,
-                    configDirectory, username, password, prompt, progress);
+                    configDirectory, configHandler,
+                    username, password, prompt, progress);
     }
 
     /**
      * Open a persistent session to a repository.
+     * <p>
+     * <b>Note:</b> The URL can point to a subtree of the repository.
+     * <p>
+     * <b>Note:</b> If the UUID does not match the repository,
+     * this function fails.
+     * <p>
+     * <b>Note:</b> The session object inherits the progress callback,
+     * configuration directory and authentication info.
+     *
      * @param url The initial session root URL.
      * @param reposUUID The expected repository UUID; may not be null..
      * @param retryAttempts The number of times to retry the operation
@@ -191,10 +225,6 @@ public class RemoteFactory
      * @throws RetryOpenSession If the session URL was redirected
      * @throws SubversionException If an URL redirect cycle was detected
      * @throws ClientException
-     * @note The URL can point to a subtree of the repository.
-     * @note If the UUID does not match the repository, this function fails.
-     * @note The session object inherits the progress callback,
-     *       configuration directory and authentication info.
      */
     public ISVNRemote openRemoteSession(String url, String reposUUID,
                                         int retryAttempts)
@@ -206,10 +236,12 @@ public class RemoteFactory
             throw new IllegalArgumentException(
                 "retryAttempts must be positive");
         return open(retryAttempts, url, reposUUID,
-                    configDirectory, username, password, prompt, progress);
+                    configDirectory, configHandler,
+                    username, password, prompt, progress);
     }
 
     private String configDirectory;
+    private ConfigEvent configHandler;
     private String username;
     private String password;
     private UserPasswordCallback prompt;
@@ -219,6 +251,7 @@ public class RemoteFactory
     private static native ISVNRemote open(int retryAttempts,
                                           String url, String reposUUID,
                                           String configDirectory,
+                                          ConfigEvent configHandler,
                                           String username, String password,
                                           UserPasswordCallback prompt,
                                           ProgressCallback progress)

Modified: subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/remote/RemoteSession.java
URL: http://svn.apache.org/viewvc/subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/remote/RemoteSession.java?rev=1501136&r1=1501135&r2=1501136&view=diff
==============================================================================
--- subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/remote/RemoteSession.java (original)
+++ subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/remote/RemoteSession.java Tue Jul  9 08:48:43 2013
@@ -28,14 +28,16 @@ import org.apache.subversion.javahl.call
 
 import org.apache.subversion.javahl.ISVNRemote;
 import org.apache.subversion.javahl.ISVNEditor;
+import org.apache.subversion.javahl.ISVNReporter;
 import org.apache.subversion.javahl.JNIObject;
 import org.apache.subversion.javahl.OperationContext;
 import org.apache.subversion.javahl.ClientException;
 
 import java.lang.ref.WeakReference;
-import java.util.HashSet;
 import java.util.Date;
 import java.util.Map;
+import java.util.Set;
+import java.io.OutputStream;
 
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
@@ -44,17 +46,27 @@ public class RemoteSession extends JNIOb
 {
     public void dispose()
     {
-        if (editors != null)
+        if (editorReference != null)
         {
-            // Deactivate all open editors
-            for (WeakReference<ISVNEditor> ref : editors)
+            // Deactivate the open editor
+            ISVNEditor ed = editorReference.get();
+            if (ed != null)
             {
-                ISVNEditor ed = ref.get();
-                if (ed == null)
-                    continue;
                 ed.dispose();
-                ref.clear();
+                editorReference.clear();
             }
+            editorReference = null;
+        }
+        if (reporterReference != null)
+        {
+            // Deactivate the open reporter
+            ISVNReporter rp = reporterReference.get();
+            if (rp != null)
+            {
+                rp.dispose();
+                reporterReference.clear();
+            }
+            reporterReference = null;
         }
         nativeDispose();
     }
@@ -107,21 +119,100 @@ public class RemoteSession extends JNIOb
     public native byte[] getRevisionProperty(long revision, String propertyName)
             throws ClientException;
 
-    public ISVNEditor getCommitEditor() throws ClientException
+    public ISVNEditor getCommitEditor(Map<String, byte[]> revisionProperties,
+                                      CommitCallback commitCallback,
+                                      Set<Lock> lockTokens,
+                                      boolean keepLocks)
+            throws ClientException
     {
-        ISVNEditor ed = CommitEditor.createInstance(this);
-        if (editors == null)
-            editors = new HashSet<WeakReference<ISVNEditor>>();
-        editors.add(new WeakReference<ISVNEditor>(ed));
+        check_inactive(editorReference, reporterReference);
+        ISVNEditor ed =
+            CommitEditor.createInstance(this, revisionProperties,
+                                        commitCallback, lockTokens, keepLocks);
+        if (editorReference != null)
+            editorReference.clear();
+        editorReference = new WeakReference<ISVNEditor>(ed);
         return ed;
     }
 
+    public long getFile(long revision, String path,
+                        OutputStream contents,
+                        Map<String, byte[]> properties)
+            throws ClientException
+    {
+        maybe_clear(properties);
+        return nativeGetFile(revision, path, contents, properties);
+    }
+
+    public long getDirectory(long revision, String path,
+                             int direntFields,
+                             Map<String, DirEntry> dirents,
+                             Map<String, byte[]> properties)
+            throws ClientException
+    {
+        maybe_clear(dirents);
+        maybe_clear(properties);
+        return nativeGetDirectory(revision, path,
+                                  direntFields, dirents, properties);
+    }
+
+    public native Map<String, Mergeinfo>
+        getMergeinfo(Iterable<String> paths, long revision,
+                     Mergeinfo.Inheritance inherit,
+                     boolean includeDescendants)
+            throws ClientException;
+
+    // TODO: update
+    // TODO: switch
+
+    public ISVNReporter status(String statusTarget,
+                               long revision, Depth depth,
+                               RemoteStatus receiver)
+            throws ClientException
+    {
+        check_inactive(editorReference, reporterReference);
+        StateReporter rp = StateReporter.createInstance(this);
+
+        // At this point, the reporter is not active/valid.
+        StatusEditor editor = new StatusEditor(receiver);
+        nativeStatus(statusTarget, revision, depth, editor, rp);
+        // Now it should be valid.
+
+        if (reporterReference != null)
+            reporterReference.clear();
+        reporterReference = new WeakReference<ISVNReporter>(rp);
+        return rp;
+    }
+
+    // TODO: diff
+
+    public native void getLog(Iterable<String> paths,
+                              long startRevision, long endRevision, int limit,
+                              boolean strictNodeHistory, boolean discoverPath,
+                              boolean includeMergedRevisions,
+                              Iterable<String> revisionProperties,
+                              LogMessageCallback callback)
+            throws ClientException;
+
     public native NodeKind checkPath(String path, long revision)
             throws ClientException;
 
+    // TODO: stat
+    // TODO: getLocations
+    // TODO: getLocationSegments
+    // TODO: getFileRevisions
+    // TODO: lock
+    // TODO: unlock
+    // TODO: getLock
+
     public native Map<String, Lock> getLocks(String path, Depth depth)
             throws ClientException;
 
+    // TODO: replayRange
+    // TODO: replay
+    // TODO: getDeletedRevision
+    // TODO: getInheritedProperties
+
     public boolean hasCapability(Capability capability)
             throws ClientException
     {
@@ -148,6 +239,20 @@ public class RemoteSession extends JNIOb
                                                      byte[] oldValue,
                                                      byte[] newValue)
             throws ClientException;
+    private native long nativeGetFile(long revision, String path,
+                                      OutputStream contents,
+                                      Map<String, byte[]> properties)
+            throws ClientException;
+    private native long nativeGetDirectory(long revision, String path,
+                                           int direntFields,
+                                           Map<String, DirEntry> dirents,
+                                           Map<String, byte[]> properties)
+            throws ClientException;
+    private native void nativeStatus(String statusTarget,
+                                     long revision, Depth depth,
+                                     ISVNEditor statusEditor,
+                                     ISVNReporter reporter)
+            throws ClientException;
     private native boolean nativeHasCapability(String capability)
             throws ClientException;
 
@@ -158,8 +263,75 @@ public class RemoteSession extends JNIOb
     private class RemoteSessionContext extends OperationContext {}
 
     /*
-     * The set of open editors. We need this in order to dispose/abort
-     * the editors when the session is disposed.
+     * A reference to the current active editor. We need this in order
+     * to dispose/abort the editor when the session is disposed. And
+     * furthermore, there can be only one editor or reporter active at
+     * any time.
      */
-    private HashSet<WeakReference<ISVNEditor>> editors;
+    private WeakReference<ISVNEditor> editorReference;
+
+    /*
+     * The commit editor calls this when disposed to clear the
+     * reference. Note that this function will be called during our
+     * dispose, so make sure they don't step on each others' toes.
+     */
+    void disposeEditor(ISVNEditor editor)
+    {
+        if (editorReference == null)
+            return;
+        ISVNEditor ed = editorReference.get();
+        if (ed == null)
+            return;
+        if (ed != editor)
+            throw new IllegalStateException("Disposing unknown editor");
+        editorReference.clear();
+    }
+
+    /*
+     * A reference to the current active reporter. We need this in
+     * order to dispose/abort the report when the session is
+     * disposed. And furthermore, there can be only one reporter or
+     * editor active at any time.
+     */
+    private WeakReference<ISVNReporter> reporterReference;
+
+    /*
+     * The update reporter calls this when disposed to clear the
+     * reference. Note that this function will be called during our
+     * dispose, so make sure they don't step on each others' toes.
+     */
+    void disposeReporter(ISVNReporter reporter)
+    {
+        if (reporterReference == null)
+            return;
+        ISVNReporter rp = reporterReference.get();
+        if (rp == null)
+            return;
+        if (rp != reporter)
+            throw new IllegalStateException("Disposing unknown reporter");
+        reporterReference.clear();
+    }
+
+    /*
+     * Private helper methods.
+     */
+    private final static void maybe_clear(Map clearable)
+    {
+        if (clearable != null && !clearable.isEmpty())
+            try {
+                clearable.clear();
+            } catch (UnsupportedOperationException ex) {
+                // ignored
+            }
+    }
+
+    private final static
+        void check_inactive(WeakReference<ISVNEditor> editorReference,
+                            WeakReference<ISVNReporter> reporterReference)
+    {
+        if (editorReference != null && editorReference.get() != null)
+            throw new IllegalStateException("An editor is already active");
+        if (reporterReference != null && reporterReference.get() != null)
+            throw new IllegalStateException("A reporter is already active");
+    }
 }

Modified: subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/types/Mergeinfo.java
URL: http://svn.apache.org/viewvc/subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/types/Mergeinfo.java?rev=1501136&r1=1501135&r2=1501136&view=diff
==============================================================================
--- subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/types/Mergeinfo.java (original)
+++ subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/types/Mergeinfo.java Tue Jul  9 08:48:43 2013
@@ -69,6 +69,31 @@ public class Mergeinfo implements java.i
     }
 
     /**
+     * The three ways to request mergeinfo affecting a given path
+     * in {@link org.apache.subversion.javahl.ISVNRemote#getMergeinfo}.
+     * @since 1.9
+     */
+    public static enum Inheritance
+    {
+        /** Explicit mergeinfo only. */
+        explicit,
+
+        /**
+         * Explicit mergeinfo, or if that doesn't exist, the inherited
+         * mergeinfo from a target's nearest (path-wise, not history-wise)
+         * ancestor.
+         */
+        inherited,
+
+        /**
+         * Mergeinfo inherited from a target's nearest (path-wise,
+         * not history-wise) ancestor, regardless of whether target
+         * has explicit mergeinfo.
+         */
+        nearest_ancestor;
+    }
+
+    /**
      * Add one or more RevisionRange objects to merge info. If the
      * merge source is already stored, the list of revisions is
      * replaced.

Modified: subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/types/RevisionRange.java
URL: http://svn.apache.org/viewvc/subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/types/RevisionRange.java?rev=1501136&r1=1501135&r2=1501136&view=diff
==============================================================================
--- subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/types/RevisionRange.java (original)
+++ subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/types/RevisionRange.java Tue Jul  9 08:48:43 2013
@@ -36,25 +36,35 @@ public class RevisionRange implements Co
     // http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf
     // http://java.sun.com/j2se/1.5.0/docs/guide/serialization/spec/version.html#6678
     // http://java.sun.com/javase/6/docs/platform/serialization/spec/version.html#6678
-    private static final long serialVersionUID = 1L;
+    private static final long serialVersionUID = 2L;
 
     private Revision from;
     private Revision to;
+    private boolean inheritable;
 
     /**
      * Creates a new instance.  Called by native library.
      */
-    @SuppressWarnings("unused")
-    private RevisionRange(long from, long to)
+    protected RevisionRange(long from, long to, boolean inheritable)
     {
         this.from = Revision.getInstance(from);
         this.to = Revision.getInstance(to);
+        this.inheritable = inheritable;
+    }
+
+    /** @since 1.9 */
+    public RevisionRange(Revision from, Revision to, boolean inheritable)
+    {
+        this.from = from;
+        this.to = to;
+        this.inheritable = inheritable;
     }
 
     public RevisionRange(Revision from, Revision to)
     {
         this.from = from;
         this.to = to;
+        this.inheritable = true;
     }
 
     /**
@@ -70,6 +80,11 @@ public class RevisionRange implements Co
             return;
         }
 
+        this.inheritable = !revisionElement.endsWith("*");
+        if (!this.inheritable)
+            revisionElement =
+                revisionElement.substring(0, revisionElement.length() - 1);
+
         int hyphen = revisionElement.indexOf('-');
         if (hyphen > 0)
         {
@@ -113,14 +128,20 @@ public class RevisionRange implements Co
         return to;
     }
 
+    public boolean isInheritable()
+    {
+        return inheritable;
+    }
+
     public String toString()
     {
         if (from != null && to != null)
         {
-            if (from.equals(to))
-                return from.toString();
-            else
-                return from.toString() + '-' + to.toString();
+            String rep = (from.equals(to) ? from.toString()
+                          : from.toString() + '-' + to.toString());
+            if (!inheritable)
+                return rep + '*';
+            return rep;
         }
         return super.toString();
     }
@@ -138,7 +159,7 @@ public class RevisionRange implements Co
     public int hashCode()
     {
         final int prime = 31;
-        int result = 1;
+        int result = (inheritable ? 1 : 2);
         result = prime * result + ((from == null) ? 0 : from.hashCode());
         result = prime * result + ((to == null) ? 0 : to.hashCode());
         return result;
@@ -178,10 +199,12 @@ public class RevisionRange implements Co
             return false;
         }
 
-        return true;
+        return (inheritable == other.inheritable);
     }
 
     /**
+     * <b>Note:</b> Explicitly ignores inheritable state.
+     *
      * @param range The RevisionRange to compare this object to.
      */
     public int compareTo(RevisionRange range)

Modified: subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/types/VersionExtended.java
URL: http://svn.apache.org/viewvc/subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/types/VersionExtended.java?rev=1501136&r1=1501135&r2=1501136&view=diff
==============================================================================
--- subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/types/VersionExtended.java (original)
+++ subversion/branches/verify-keep-going/subversion/bindings/javahl/src/org/apache/subversion/javahl/types/VersionExtended.java Tue Jul  9 08:48:43 2013
@@ -45,9 +45,9 @@ public class VersionExtended
     /**
      * @return The canonical host triplet (arch-vendor-osname) of the
      * system where libsvn_subr was compiled.
-     *
-     * @note On Unix-like systems (includng Mac OS X), this string is
-     * the same as the output of the config.guess script for the
+     * <p>
+     * <b>Note:</b> On Unix-like systems (includng Mac OS X), this string
+     * is the same as the output of the config.guess script for the
      * underlying Subversion libraries.
      */
     public native String getBuildHost();
@@ -60,8 +60,8 @@ public class VersionExtended
     /**
      * @return The canonical host triplet (arch-vendor-osname) of the
      * system where the current process is running.
-     *
-     * @note This string may not be the same as the output of
+     * <p>
+     * <b>Note:</b> This string may not be the same as the output of
      * config.guess on the same system.
      */
     public native String getRuntimeHost();
@@ -139,8 +139,8 @@ public class VersionExtended
     /**
      * @return Iterator for an immutable internal list of #LoadedLib
      * describing loaded shared libraries.  The the list may be empty.
-     *
-     * @note On Mac OS X, the loaded frameworks, private frameworks
+     * <p>
+     * <b>Note:</b> On Mac OS X, the loaded frameworks, private frameworks
      * and system libraries will not be listed.
      */
     public java.util.Iterator<LoadedLib> getLoadedLibs()
@@ -178,7 +178,8 @@ public class VersionExtended
 
         /**
          * Implementation of java.util.Iterator#remove().
-         * @note Not implemented, all sequences are immutable.
+         * <p>
+         * <b>Note:</b> Not implemented, all sequences are immutable.
          */
         public void remove()
         {
@@ -219,7 +220,8 @@ public class VersionExtended
 
         /**
          * Implementation of java.util.Iterator#remove().
-         * @note Not implemented, all sequences are immutable.
+         * <p>
+         * <b>Note:</b> Not implemented, all sequences are immutable.
          */
         public void remove()
         {

Modified: subversion/branches/verify-keep-going/subversion/bindings/javahl/tests/org/apache/subversion/javahl/BasicTests.java
URL: http://svn.apache.org/viewvc/subversion/branches/verify-keep-going/subversion/bindings/javahl/tests/org/apache/subversion/javahl/BasicTests.java?rev=1501136&r1=1501135&r2=1501136&view=diff
==============================================================================
--- subversion/branches/verify-keep-going/subversion/bindings/javahl/tests/org/apache/subversion/javahl/BasicTests.java (original)
+++ subversion/branches/verify-keep-going/subversion/bindings/javahl/tests/org/apache/subversion/javahl/BasicTests.java Tue Jul  9 08:48:43 2013
@@ -251,16 +251,17 @@ public class BasicTests extends SVNTests
     public void testMergeinfoParser() throws Throwable
     {
         String mergeInfoPropertyValue =
-            "/trunk:1-300,305,307,400-405\n/branches/branch:308-400";
+            "/trunk:1-300,305*,307,400-405*\n" +
+            "/branches/branch:308-400";
         Mergeinfo info = new Mergeinfo(mergeInfoPropertyValue);
         Set<String> paths = info.getPaths();
         assertEquals(2, paths.size());
         List<RevisionRange> trunkRange = info.getRevisionRange("/trunk");
         assertEquals(4, trunkRange.size());
         assertEquals("1-300", trunkRange.get(0).toString());
-        assertEquals("305", trunkRange.get(1).toString());
+        assertEquals("305*", trunkRange.get(1).toString());
         assertEquals("307", trunkRange.get(2).toString());
-        assertEquals("400-405", trunkRange.get(3).toString());
+        assertEquals("400-405*", trunkRange.get(3).toString());
         List<RevisionRange> branchRange =
             info.getRevisionRange("/branches/branch");
         assertEquals(1, branchRange.size());
@@ -3299,6 +3300,16 @@ public class BasicTests extends SVNTests
         }
     }
 
+    private static class CountingProgressListener implements ProgressCallback
+    {
+        public void onProgress(ProgressEvent event)
+        {
+            // TODO: Examine the byte counts from "event".
+            gotProgress = true;
+        }
+        public boolean gotProgress = false;
+    }
+
     public void testDataTransferProgressReport() throws Throwable
     {
         // ### FIXME: This isn't working over ra_local, because
@@ -3308,25 +3319,13 @@ public class BasicTests extends SVNTests
 
         // build the test setup
         OneTest thisTest = new OneTest();
-        ProgressCallback listener = new ProgressCallback()
-        {
-            public void onProgress(ProgressEvent event)
-            {
-                // TODO: Examine the byte counts from "event".
-                throw new RuntimeException("Progress reported as expected");
-            }
-        };
+        CountingProgressListener listener = new CountingProgressListener();
         client.setProgressCallback(listener);
 
         // Perform an update to exercise the progress notification.
-        try
-        {
-            update(thisTest);
+        update(thisTest);
+        if (!listener.gotProgress)
             fail("No progress reported");
-        }
-        catch (RuntimeException progressReported)
-        {
-        }
     }
 
     /**

Modified: subversion/branches/verify-keep-going/subversion/bindings/javahl/tests/org/apache/subversion/javahl/SVNRemoteTests.java
URL: http://svn.apache.org/viewvc/subversion/branches/verify-keep-going/subversion/bindings/javahl/tests/org/apache/subversion/javahl/SVNRemoteTests.java?rev=1501136&r1=1501135&r2=1501136&view=diff
==============================================================================
--- subversion/branches/verify-keep-going/subversion/bindings/javahl/tests/org/apache/subversion/javahl/SVNRemoteTests.java (original)
+++ subversion/branches/verify-keep-going/subversion/bindings/javahl/tests/org/apache/subversion/javahl/SVNRemoteTests.java Tue Jul  9 08:48:43 2013
@@ -24,15 +24,24 @@ package org.apache.subversion.javahl;
 
 import org.apache.subversion.javahl.*;
 import org.apache.subversion.javahl.remote.*;
+import org.apache.subversion.javahl.callback.*;
 import org.apache.subversion.javahl.types.*;
 
 import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
+import java.util.List;
 import java.util.Set;
 import java.util.Map;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 
 /**
  * This class is used for testing the SVNReposAccess class
@@ -59,12 +68,14 @@ public class SVNRemoteTests extends SVNT
         thisTest = new OneTest();
     }
 
-    public static ISVNRemote getSession(String url, String configDirectory)
+    public static ISVNRemote getSession(String url, String configDirectory,
+                                        ConfigEvent configHandler)
     {
         try
         {
             RemoteFactory factory = new RemoteFactory();
             factory.setConfigDirectory(configDirectory);
+            factory.setConfigEventHandler(configHandler);
             factory.setUsername(USERNAME);
             factory.setPassword(PASSWORD);
             factory.setPrompt(new DefaultPromptUserPassword());
@@ -81,7 +92,7 @@ public class SVNRemoteTests extends SVNT
 
     private ISVNRemote getSession()
     {
-        return getSession(getTestRepoUrl(), super.conf.getAbsolutePath());
+        return getSession(getTestRepoUrl(), super.conf.getAbsolutePath(), null);
     }
 
     /**
@@ -100,7 +111,7 @@ public class SVNRemoteTests extends SVNT
         try
         {
             session = new RemoteFactory(
-                super.conf.getAbsolutePath(),
+                super.conf.getAbsolutePath(), null,
                 USERNAME, PASSWORD,
                 new DefaultPromptUserPassword(), null)
                 .openRemoteSession(getTestRepoUrl());
@@ -235,13 +246,13 @@ public class SVNRemoteTests extends SVNT
     public void testGetCommitEditor() throws Exception
     {
         ISVNRemote session = getSession();
-        session.getCommitEditor();
+        session.getCommitEditor(null, null, null, false);
     }
 
     public void testDisposeCommitEditor() throws Exception
     {
         ISVNRemote session = getSession();
-        session.getCommitEditor();
+        session.getCommitEditor(null, null, null, false);
         session.dispose();
     }
 
@@ -288,7 +299,8 @@ public class SVNRemoteTests extends SVNT
         if (!atomic)
             return;
 
-        byte[] oldValue = USERNAME.getBytes(UTF8);
+        byte[] oldValue = client.revProperty(getTestRepoUrl(), "svn:author",
+                                             Revision.getInstance(1));
         byte[] newValue = "rayjandom".getBytes(UTF8);
         try
         {
@@ -297,16 +309,26 @@ public class SVNRemoteTests extends SVNT
         }
         catch (ClientException ex)
         {
-            String msg = ex.getMessage();
-            int index = msg.indexOf('\n');
-            assertEquals(msg.substring(0, index),
-                         "Disabled repository feature");
+            ClientException.ErrorMessage error = null;
+            for (ClientException.ErrorMessage m : ex.getAllMessages())
+                if (!m.isGeneric()) {
+                    error = m;
+                    break;
+                }
+
+            if (error == null)
+                fail("Failed with no error message");
+
+            if (error.getCode() != 175002 && // SVN_ERR_RA_DAV_REQUEST_FAILED
+                error.getCode() != 165006)   // SVN_ERR_REPOS_DISABLED_FEATURE
+                fail(error.getMessage());
+
             return;
         }
 
         byte[] check = client.revProperty(getTestRepoUrl(), "svn:author",
                                           Revision.getInstance(1));
-        assertTrue(Arrays.equals(newValue, check));
+        assertTrue(Arrays.equals(check, newValue));
     }
 
     public void testGetRevpropList() throws Exception
@@ -327,4 +349,738 @@ public class SVNRemoteTests extends SVNT
         byte[] propval = session.getRevisionProperty(1, "svn:author");
         assertTrue(Arrays.equals(propval, USERNAME.getBytes(UTF8)));
     }
+
+    public void testGetFile() throws Exception
+    {
+        Charset UTF8 = Charset.forName("UTF-8");
+        ISVNRemote session = getSession();
+
+        ByteArrayOutputStream contents = new ByteArrayOutputStream();
+        HashMap<String, byte[]> properties = new HashMap<String, byte[]>();
+        properties.put("fakename", "fakecontents".getBytes(UTF8));
+        long fetched_rev =
+            session.getFile(Revision.SVN_INVALID_REVNUM, "A/B/lambda",
+                            contents, properties);
+        assertEquals(fetched_rev, 1);
+        assertEquals(contents.toString("UTF-8"),
+                     "This is the file 'lambda'.");
+        for (Map.Entry<String, byte[]> e : properties.entrySet()) {
+            final String key = e.getKey();
+            assertTrue(key.startsWith("svn:entry:")
+                       || key.startsWith("svn:wc:"));
+        }
+    }
+
+    public void testGetDirectory() throws Exception
+    {
+        Charset UTF8 = Charset.forName("UTF-8");
+        ISVNRemote session = getSession();
+
+        HashMap<String, DirEntry> dirents = new HashMap<String, DirEntry>();
+        dirents.put("E", null);
+        dirents.put("F", null);
+        dirents.put("lambda", null);
+        HashMap<String, byte[]> properties = new HashMap<String, byte[]>();
+        properties.put("fakename", "fakecontents".getBytes(UTF8));
+        long fetched_rev =
+            session.getDirectory(Revision.SVN_INVALID_REVNUM, "A/B",
+                                 DirEntry.Fields.all, dirents, properties);
+        assertEquals(fetched_rev, 1);
+        assertEquals(dirents.get("E").getPath(), "E");
+        assertEquals(dirents.get("F").getPath(), "F");
+        assertEquals(dirents.get("lambda").getPath(), "lambda");
+        for (Map.Entry<String, byte[]> e : properties.entrySet()) {
+            final String key = e.getKey();
+            assertTrue(key.startsWith("svn:entry:")
+                       || key.startsWith("svn:wc:"));
+        }
+    }
+
+    private final class CommitContext implements CommitCallback
+    {
+        public final ISVNEditor editor;
+        public CommitContext(ISVNRemote session, String logstr)
+            throws ClientException
+        {
+            Charset UTF8 = Charset.forName("UTF-8");
+            byte[] log = (logstr == null
+                          ? new byte[0]
+                          : logstr.getBytes(UTF8));
+            HashMap<String, byte[]> revprops = new HashMap<String, byte[]>();
+            revprops.put("svn:log", log);
+            editor = session.getCommitEditor(revprops, this, null, false);
+        }
+
+        public void commitInfo(CommitInfo info) { this.info = info; }
+        public long getRevision() { return info.getRevision(); }
+
+        private CommitInfo info;
+    }
+
+    public void testEditorCopy() throws Exception
+    {
+        ISVNRemote session = getSession();
+        CommitContext cc =
+            new CommitContext(session, "Copy A/B/lambda -> A/B/omega");
+
+        try {
+            // FIXME: alter dir A/B first
+            cc.editor.copy("A/B/lambda", 1, "A/B/omega",
+                           Revision.SVN_INVALID_REVNUM);
+            cc.editor.complete();
+        } finally {
+            cc.editor.dispose();
+        }
+
+        assertEquals(2, cc.getRevision());
+        assertEquals(2, session.getLatestRevision());
+        assertEquals(NodeKind.file,
+                     session.checkPath("A/B/lambda",
+                                       Revision.SVN_INVALID_REVNUM));
+        assertEquals(NodeKind.file,
+                     session.checkPath("A/B/omega",
+                                       Revision.SVN_INVALID_REVNUM));
+    }
+
+    public void testEditorMove() throws Exception
+    {
+        ISVNRemote session = getSession();
+        CommitContext cc =
+            new CommitContext(session, "Move A/B/lambda -> A/B/omega");
+
+        try {
+            // FIXME: alter dir A/B first
+            cc.editor.move("A/B/lambda", 1, "A/B/omega",
+                           Revision.SVN_INVALID_REVNUM);
+            cc.editor.complete();
+        } finally {
+            cc.editor.dispose();
+        }
+
+        assertEquals(2, cc.getRevision());
+        assertEquals(2, session.getLatestRevision());
+        assertEquals(NodeKind.none,
+                     session.checkPath("A/B/lambda",
+                                       Revision.SVN_INVALID_REVNUM));
+        assertEquals(NodeKind.file,
+                     session.checkPath("A/B/omega",
+                                       Revision.SVN_INVALID_REVNUM));
+    }
+
+    public void testEditorDelete() throws Exception
+    {
+        ISVNRemote session = getSession();
+        CommitContext cc =
+            new CommitContext(session, "Delete all greek files");
+
+        String[] filePaths = { "iota",
+                               "A/mu",
+                               "A/B/lambda",
+                               "A/B/E/alpha",
+                               "A/B/E/beta",
+                               "A/D/gamma",
+                               "A/D/G/pi",
+                               "A/D/G/rho",
+                               "A/D/G/tau",
+                               "A/D/H/chi",
+                               "A/D/H/omega",
+                               "A/D/H/psi" };
+
+        try {
+            // FIXME: alter a bunch of dirs first
+            for (String path : filePaths)
+                cc.editor.delete(path, 1);
+            cc.editor.complete();
+        } finally {
+            cc.editor.dispose();
+        }
+
+        assertEquals(2, cc.getRevision());
+        assertEquals(2, session.getLatestRevision());
+        for (String path : filePaths)
+            assertEquals(NodeKind.none,
+                         session.checkPath(path, Revision.SVN_INVALID_REVNUM));
+    }
+
+    public void testEditorMkdir() throws Exception
+    {
+        ISVNRemote session = getSession();
+        CommitContext cc = new CommitContext(session, "Make hebrew dir");
+
+        try {
+            // FIXME: alter dir . first
+            cc.editor.addDirectory("ALEPH",
+                                   new ArrayList<String>(),
+                                   new HashMap<String, byte[]>(),
+                                   Revision.SVN_INVALID_REVNUM);
+            cc.editor.complete();
+        } finally {
+            cc.editor.dispose();
+        }
+
+        assertEquals(2, cc.getRevision());
+        assertEquals(2, session.getLatestRevision());
+        assertEquals(NodeKind.dir,
+                     session.checkPath("ALEPH",
+                                       Revision.SVN_INVALID_REVNUM));
+    }
+
+    public void testEditorSetDirProps() throws Exception
+    {
+        Charset UTF8 = Charset.forName("UTF-8");
+        ISVNRemote session = getSession();
+
+        byte[] ignoreval = "*.pyc\n.gitignore\n".getBytes(UTF8);
+        HashMap<String, byte[]> props = new HashMap<String, byte[]>();
+        props.put("svn:ignore", ignoreval);
+
+        CommitContext cc = new CommitContext(session, "Add svn:ignore");
+        try {
+            cc.editor.alterDirectory("", 1, null, props);
+            cc.editor.complete();
+        } finally {
+            cc.editor.dispose();
+        }
+
+        assertEquals(2, cc.getRevision());
+        assertEquals(2, session.getLatestRevision());
+        assertTrue(Arrays.equals(ignoreval,
+                                 client.propertyGet(session.getSessionUrl(),
+                                                    "svn:ignore",
+                                                    Revision.HEAD,
+                                                    Revision.HEAD)));
+    }
+
+    private static byte[] SHA1(byte[] text) throws NoSuchAlgorithmException
+    {
+        MessageDigest md = MessageDigest.getInstance("SHA-1");
+        return md.digest(text);
+    }
+
+    public void testEditorAddFile() throws Exception
+    {
+        Charset UTF8 = Charset.forName("UTF-8");
+        ISVNRemote session = getSession();
+
+        byte[] eolstyle = "native".getBytes(UTF8);
+        HashMap<String, byte[]> props = new HashMap<String, byte[]>();
+        props.put("svn:eol-style", eolstyle);
+
+        byte[] contents = "This is file 'xi'.".getBytes(UTF8);
+        Checksum hash = new Checksum(SHA1(contents), Checksum.Kind.SHA1);
+        ByteArrayInputStream stream = new ByteArrayInputStream(contents);
+
+        CommitContext cc = new CommitContext(session, "Add A/xi");
+        try {
+            // FIXME: alter dir A first
+            cc.editor.addFile("A/xi", hash, stream, props,
+                              Revision.SVN_INVALID_REVNUM);
+            cc.editor.complete();
+        } finally {
+            cc.editor.dispose();
+        }
+
+        assertEquals(2, cc.getRevision());
+        assertEquals(2, session.getLatestRevision());
+        assertEquals(NodeKind.file,
+                     session.checkPath("A/xi",
+                                       Revision.SVN_INVALID_REVNUM));
+
+        byte[] propval = client.propertyGet(session.getSessionUrl() + "/A/xi",
+                                            "svn:eol-style",
+                                            Revision.HEAD,
+                                            Revision.HEAD);
+        assertTrue(Arrays.equals(eolstyle, propval));
+    }
+
+    public void testEditorSetFileContents() throws Exception
+    {
+        Charset UTF8 = Charset.forName("UTF-8");
+        ISVNRemote session = getSession();
+
+        byte[] contents = "This is modified file 'alpha'.".getBytes(UTF8);
+        Checksum hash = new Checksum(SHA1(contents), Checksum.Kind.SHA1);
+        ByteArrayInputStream stream = new ByteArrayInputStream(contents);
+
+        CommitContext cc =
+            new CommitContext(session, "Change contents of A/B/E/alpha");
+        try {
+            cc.editor.alterFile("A/B/E/alpha", 1, hash, stream, null);
+            cc.editor.complete();
+        } finally {
+            cc.editor.dispose();
+        }
+
+        assertEquals(2, cc.getRevision());
+        assertEquals(2, session.getLatestRevision());
+        ByteArrayOutputStream checkcontents = new ByteArrayOutputStream();
+        client.streamFileContent(session.getSessionUrl() + "/A/B/E/alpha",
+                                 Revision.HEAD, Revision.HEAD, checkcontents);
+        assertTrue(Arrays.equals(contents, checkcontents.toByteArray()));
+    }
+
+    // public void testEditorRotate() throws Exception
+    // {
+    //     ISVNRemote session = getSession();
+    //
+    //     ArrayList<ISVNEditor.RotatePair> rotation =
+    //         new ArrayList<ISVNEditor.RotatePair>(3);
+    //     rotation.add(new ISVNEditor.RotatePair("A/B", 1));
+    //     rotation.add(new ISVNEditor.RotatePair("A/C", 1));
+    //     rotation.add(new ISVNEditor.RotatePair("A/D", 1));
+    //
+    //     CommitContext cc =
+    //         new CommitContext(session, "Rotate A/B -> A/C -> A/D");
+    //     try {
+    //         // No alter-dir of A is needed, children remain the same.
+    //         cc.editor.rotate(rotation);
+    //         cc.editor.complete();
+    //     } finally {
+    //         cc.editor.dispose();
+    //     }
+    //
+    //     assertEquals(2, cc.getRevision());
+    //     assertEquals(2, session.getLatestRevision());
+    //
+    //     HashMap<String, DirEntry> dirents = new HashMap<String, DirEntry>();
+    //     HashMap<String, byte[]> properties = new HashMap<String, byte[]>();
+    //
+    //     // A/B is now what used to be A/D, so A/B/H must exist
+    //     session.getDirectory(Revision.SVN_INVALID_REVNUM, "A/B",
+    //                          DirEntry.Fields.all, dirents, properties);
+    //     assertEquals(dirents.get("H").getPath(), "H");
+    //
+    //     // A/C is now what used to be A/B, so A/C/F must exist
+    //     session.getDirectory(Revision.SVN_INVALID_REVNUM, "A/C",
+    //                          DirEntry.Fields.all, dirents, properties);
+    //     assertEquals(dirents.get("F").getPath(), "F");
+    //
+    //     // A/D is now what used to be A/C and must be empty
+    //     session.getDirectory(Revision.SVN_INVALID_REVNUM, "A/D",
+    //                          DirEntry.Fields.all, dirents, properties);
+    //     assertTrue(dirents.isEmpty());
+    // }
+
+    // Sanity check so that we don't forget about unimplemented methods.
+    public void testEditorNotImplemented() throws Exception
+    {
+        ISVNRemote session = getSession();
+
+        HashMap<String, byte[]> props = new HashMap<String, byte[]>();
+        // ArrayList<ISVNEditor.RotatePair> rotation =
+        //     new ArrayList<ISVNEditor.RotatePair>();
+
+        CommitContext cc = new CommitContext(session, "not implemented");
+        try {
+            String exmsg;
+
+            try {
+                exmsg = "";
+                cc.editor.addSymlink("", "", props, 1);
+            } catch (RuntimeException ex) {
+                exmsg = ex.getMessage();
+            }
+            assertEquals("Not implemented: CommitEditor.addSymlink", exmsg);
+
+            try {
+                exmsg = "";
+                cc.editor.alterSymlink("", 1, "", null);
+            } catch (RuntimeException ex) {
+                exmsg = ex.getMessage();
+            }
+            assertEquals("Not implemented: CommitEditor.alterSymlink", exmsg);
+
+            // try {
+            //     exmsg = "";
+            //     cc.editor.rotate(rotation);
+            // } catch (RuntimeException ex) {
+            //     exmsg = ex.getMessage();
+            // }
+            // assertEquals("Not implemented: CommitEditor.rotate", exmsg);
+        } finally {
+            cc.editor.dispose();
+        }
+    }
+
+    private static final class LogMsg
+    {
+        public Set<ChangePath> changedPaths;
+        public long revision;
+        public Map<String, byte[]> revprops;
+        public boolean hasChildren;
+    }
+
+    private static final class LogReceiver implements LogMessageCallback
+    {
+        public final ArrayList<LogMsg> logs = new ArrayList<LogMsg>();
+
+        public void singleMessage(Set<ChangePath> changedPaths,
+                                  long revision,
+                                  Map<String, byte[]> revprops,
+                                  boolean hasChildren)
+        {
+            LogMsg msg = new LogMsg();
+            msg.changedPaths = changedPaths;
+            msg.revision = revision;
+            msg.revprops = revprops;
+            msg.hasChildren = hasChildren;
+            logs.add(msg);
+        }
+    }
+
+    public void testGetLog() throws Exception
+    {
+        ISVNRemote session = getSession();
+        LogReceiver receiver = new LogReceiver();
+
+        session.getLog(null,
+                       Revision.SVN_INVALID_REVNUM,
+                       Revision.SVN_INVALID_REVNUM,
+                       0, false, false, false, null,
+                       receiver);
+
+        assertEquals(1, receiver.logs.size());
+    }
+
+    public void testGetLogMissing() throws Exception
+    {
+        ISVNRemote session = getSession();
+        LogReceiver receiver = new LogReceiver();
+
+        ArrayList<String> paths = new ArrayList<String>(1);
+        paths.add("X");
+
+        boolean exception = false;
+        try {
+            session.getLog(paths,
+                           Revision.SVN_INVALID_REVNUM,
+                           Revision.SVN_INVALID_REVNUM,
+                           0, false, false, false, null,
+                           receiver);
+        } catch (ClientException ex) {
+            assertEquals("Filesystem has no item",
+                         ex.getAllMessages().get(0).getMessage());
+            exception = true;
+        }
+
+        assertEquals(0, receiver.logs.size());
+        assertTrue(exception);
+    }
+
+    public void testConfigHandler() throws Exception
+    {
+        ConfigEvent handler = new ConfigEvent()
+            {
+                public void onLoad(ISVNConfig cfg)
+                {
+                    //System.out.println("config:");
+                    onecat(cfg.config());
+                    //System.out.println("servers:");
+                    onecat(cfg.servers());
+                }
+
+                private void onecat(ISVNConfig.Category cat)
+                {
+                    for (String sec : cat.sections()) {
+                        //System.out.println("  [" + sec + "]");
+                        ISVNConfig.Enumerator en = new ISVNConfig.Enumerator()
+                            {
+                                public void option(String name, String value)
+                                {
+                                    //System.out.println("    " + name
+                                    //                   + " = " + value);
+                                }
+                            };
+                        cat.enumerate(sec, en);
+                    }
+                }
+            };
+
+        ISVNRemote session = getSession(getTestRepoUrl(),
+                                        super.conf.getAbsolutePath(),
+                                        handler);
+        session.getLatestRevision(); // Make sure the configuration gets loaded
+    }
+
+    private static class RemoteStatusReceiver implements RemoteStatus
+    {
+        static class StatInfo implements Comparable<StatInfo>
+        {
+            public String relpath = null;
+            public char kind = ' '; // F, D, L
+            public boolean textChanged = false;
+            public boolean propsChanged = false;
+            public boolean deleted = false;
+            public Entry info = null;
+
+            StatInfo(String relpath, char kind, boolean added)
+            {
+                this.relpath = relpath;
+                this.kind = kind;
+                this.deleted = !added;
+            }
+
+            StatInfo(String relpath, char kind,
+                     boolean textChanged, boolean propsChanged,
+                     Entry info)
+            {
+                this.relpath = relpath;
+                this.kind = kind;
+                this.textChanged = textChanged;
+                this.propsChanged = propsChanged;
+                this.info = info;
+            }
+
+            @Override
+            public boolean equals(Object statinfo)
+            {
+                final StatInfo that = (StatInfo)statinfo;
+                return this.relpath.equals(that.relpath);
+            }
+
+            public int compareTo(StatInfo that)
+            {
+                return this.relpath.compareTo(that.relpath);
+            }
+        }
+
+        private boolean debug;
+
+        public RemoteStatusReceiver()
+        {
+            this.debug = false;
+        }
+
+        public RemoteStatusReceiver(boolean debug)
+        {
+            this.debug = debug;
+        }
+
+        public ArrayList<StatInfo> status = new ArrayList<StatInfo>();
+
+        public void addedDirectory(String relativePath)
+        {
+            if (debug)
+                System.err.println("RemoteStatus:  A   (dir)  " +
+                                   relativePath);
+            status.add(new StatInfo(relativePath, 'D', true));
+        }
+
+        public void addedFile(String relativePath)
+        {
+            if (debug)
+                System.err.println("RemoteStatus:  A   (file) "
+                                   + relativePath);
+            status.add(new StatInfo(relativePath, 'F', true));
+        }
+
+        public void addedSymlink(String relativePath)
+        {
+            if (debug)
+                System.err.println("RemoteStatus:  A   (link) "
+                                   + relativePath);
+            status.add(new StatInfo(relativePath, 'L', true));
+        }
+
+        public void modifiedDirectory(String relativePath,
+                                      boolean childrenModified,
+                                      boolean propsModified,
+                                      Entry nodeInfo)
+        {
+            if (debug)
+                System.err.println("RemoteStatus:  " +
+                                   (childrenModified ? 'M' : '_') +
+                                   (propsModified ? 'M' : '_') +
+                                   "  (dir)  " + relativePath);
+            status.add(new StatInfo(relativePath, 'D',
+                                    childrenModified, propsModified,
+                                    nodeInfo));
+        }
+
+        public void modifiedFile(String relativePath,
+                                 boolean textModified,
+                                 boolean propsModified,
+                                 Entry nodeInfo)
+        {
+            if (debug)
+                System.err.println("RemoteStatus:  " +
+                                   (textModified ? 'M' : '_') +
+                                   (propsModified ? 'M' : '_') +
+                                   "  (file) " + relativePath);
+            status.add(new StatInfo(relativePath, 'F',
+                                    textModified, propsModified,
+                                    nodeInfo));
+        }
+
+        public void modifiedSymlink(String relativePath,
+                                    boolean targetModified,
+                                    boolean propsModified,
+                                    Entry nodeInfo)
+        {
+            if (debug)
+                System.err.println("RemoteStatus:  " +
+                                   (targetModified ? 'M' : '_') +
+                                   (propsModified ? 'M' : '_') +
+                                   "  (link) " + relativePath);
+            status.add(new StatInfo(relativePath, 'L',
+                                    targetModified, propsModified,
+                                    nodeInfo));
+
+        }
+
+        public void deleted(String relativePath)
+        {
+            if (debug)
+                System.err.println("RemoteStatus:  D          "
+                                   + relativePath);
+            status.add(new StatInfo(relativePath, ' ', false));
+        }
+    }
+
+    public void testSimpleStatus() throws Exception
+    {
+        ISVNRemote session = getSession();
+
+        RemoteStatusReceiver receiver = new RemoteStatusReceiver();
+        ISVNReporter rp = session.status(null, Revision.SVN_INVALID_REVNUM,
+                                         Depth.infinity, receiver);
+        try {
+            rp.setPath("", 0, Depth.infinity, true, null);
+            assertEquals(1, rp.finishReport());
+        } finally {
+            rp.dispose();
+        }
+        assertEquals(21, receiver.status.size());
+    }
+
+    public void testPropchangeStatus() throws Exception
+    {
+        ISVNRemote session = getSession();
+
+        CommitMessageCallback cmcb = new CommitMessageCallback() {
+                public String getLogMessage(Set<CommitItem> x) {
+                    return "Property change on A/D/gamma";
+                }
+            };
+        client.propertySetRemote(getTestRepoUrl() + "/A/D/gamma",
+                                 1L, "foo", "bar".getBytes(), cmcb,
+                                 false, null, null);
+
+        RemoteStatusReceiver receiver = new RemoteStatusReceiver();
+        ISVNReporter rp = session.status(null, Revision.SVN_INVALID_REVNUM,
+                                         Depth.infinity, receiver);
+        try {
+            rp.setPath("", 1, Depth.infinity, false, null);
+            assertEquals(2, rp.finishReport());
+        } finally {
+            rp.dispose();
+        }
+
+        assertEquals(4, receiver.status.size());
+
+        // ra_serf returns the entries in inverted order compared to ra_local.
+        Collections.sort(receiver.status);
+        RemoteStatusReceiver.StatInfo mod = receiver.status.get(3);
+        assertEquals("A/D/gamma", mod.relpath);
+        assertEquals('F', mod.kind);
+        assertEquals(false, mod.textChanged);
+        assertEquals(true, mod.propsChanged);
+        assertEquals(false, mod.deleted);
+        assertEquals(2, mod.info.getCommittedRevision());
+    }
+
+    public void testDeletedStatus() throws Exception
+    {
+        ISVNRemote session = getSession();
+
+        CommitMessageCallback cmcb = new CommitMessageCallback() {
+                public String getLogMessage(Set<CommitItem> x) {
+                    return "Delete A/mu";
+                }
+            };
+        HashSet<String> paths = new HashSet<String>(1);
+        paths.add(getTestRepoUrl() + "/A/mu");
+        client.remove(paths, false, false, null, cmcb, null);
+
+        RemoteStatusReceiver receiver = new RemoteStatusReceiver();
+        ISVNReporter rp = session.status(null, Revision.SVN_INVALID_REVNUM,
+                                         Depth.infinity, receiver);
+        try {
+            rp.setPath("", 1, Depth.infinity, false, null);
+            assertEquals(2, rp.finishReport());
+        } finally {
+            rp.dispose();
+        }
+        assertEquals(3, receiver.status.size());
+
+        // ra_serf returns the entries in inverted order compared to ra_local.
+        Collections.sort(receiver.status);
+        RemoteStatusReceiver.StatInfo mod = receiver.status.get(2);
+        assertEquals("A/mu", mod.relpath);
+        assertEquals(' ', mod.kind);
+        assertEquals(false, mod.textChanged);
+        assertEquals(false, mod.propsChanged);
+        assertEquals(true, mod.deleted);
+    }
+
+    public void testTrivialMergeinfo() throws Exception
+    {
+        ISVNRemote session = getSession();
+        ArrayList<String> paths = new ArrayList<String>(1);
+        paths.add("");
+
+        Map<String, Mergeinfo> catalog =
+            session.getMergeinfo(paths, 1L, Mergeinfo.Inheritance.explicit,
+                                 false);
+        assertEquals(null, catalog);
+    }
+
+    public void testBranchMergeinfo() throws Exception
+    {
+        CommitMessageCallback cmcb = new CommitMessageCallback() {
+                public String getLogMessage(Set<CommitItem> x) {
+                    return "testBranchMergeinfo";
+                }
+            };
+
+        ISVNRemote session = getSession();
+
+        // Create a branch
+        ArrayList<CopySource> dirA = new ArrayList<CopySource>(1);
+        dirA.add(new CopySource(getTestRepoUrl() + "/A",
+                                Revision.HEAD, Revision.HEAD));
+        client.copy(dirA, getTestRepoUrl() + "/Abranch",
+                    false, false, true, null, cmcb, null);
+
+        // Check mergeinfo on new branch
+        ArrayList<String> paths = new ArrayList<String>(1);
+        paths.add("Abranch");
+        Map<String, Mergeinfo> catalog =
+            session.getMergeinfo(paths, 2L, Mergeinfo.Inheritance.explicit,
+                                 false);
+        assertEquals(null, catalog);
+
+        // Modify source and merge to branch
+        client.propertySetRemote(getTestRepoUrl() + "/A/D/gamma",
+                                 2L, "foo", "bar".getBytes(), cmcb,
+                                 false, null, null);
+        client.update(thisTest.getWCPathSet(), Revision.HEAD, Depth.infinity,
+                      false, false, true, false);
+        client.merge(getTestRepoUrl() + "/A", Revision.HEAD, null,
+                     thisTest.getWCPath() + "/Abranch", false, Depth.infinity,
+                     false, false, false, false);
+        client.commit(thisTest.getWCPathSet(), Depth.infinity, false, false,
+                      null, null, cmcb, null);
+
+        // Check inherited mergeinfo on updated branch
+        paths.set(0, "Abranch/mu");
+        catalog = session.getMergeinfo(paths, 4L,
+                                       Mergeinfo.Inheritance.nearest_ancestor,
+                                       false);
+        assertEquals(1, catalog.size());
+        List<RevisionRange> ranges =
+            catalog.get("Abranch/mu").getRevisions("/A/mu");
+        assertEquals(1, ranges.size());
+        assertEquals("1-3", ranges.get(0).toString());
+    }
 }