Continuing issue #749.  Make svn_wc_adm_access_t optionally
hierarchical.  Lock entire tree before starting a commit.  Revert
r2504 as it's no longer needed.

This got more complicated than I would have liked. I am still
concerned about which directories get locked, and in which order they
get locked.


* subversion/include/svn_error_codes.h (SVN_ERR_WC_INVALID_LOCK): New
  error code.

* subversion/include/svn_wc.h
  (svn_wc_adm_open): Add parent_access and tree_lock parameters.

  (svn_wc_adm_retrieve): New function.

  (svn_wc_adm_close): Document new behavior.

* subversion/libsvn_wc/adm_ops.c
  (svn_wc_process_committed, svn_wc_remove_from_revision_control):
  Replace svn_wc_adm_open with svn_wc_adm_retrieve and delete
  svn_wc_adm_close.

  (svn_wc_delete, svn_wc_revert): Pass new parameters to svn_wc_adm_open.

* subversion/libsvn_wc/log.c
  (log_do_delete_entry): Replace svn_wc_adm_open with svn_wc_adm_retrieve.

  (log_do_committed): Replace svn_wc_adm_open with svn_wc_adm_retrieve
  and delete svn_wc_adm_close.

* subversion/libsvn_wc/props.c (svn_wc_merge_prop_diffs): Pass new
  parameters to svn_wc_adm_open.

* subversion/libsvn_wc/adm_files.c (init_adm): Pass new parameters to
  svn_wc_adm_open.

* subversion/libsvn_client/commit.c
  (svn_client_commit): Revert r2504. Return an error if the hash lookup
  for an access baton fails.

* subversion/libsvn_client/commit_util.c
  (lock_dir): Lookup parent baton. Pass new parameters to svn_wc_adm_open.  

  (harvest_committables): New lock_subdirs parameter. Lock all
  sub-directories when locking a directory.

  (svn_client__harvest_committables, svn_client__get_copy_committables): Pass
  new parameter to harvest_committables.

* subversion/libsvn_wc/update_editor.c
  (delete_entry): Pass new parameters to svn_wc_adm_open to lock
  entire tree.

  (close_directory, svn_wc_install_file): Pass new parameters to
  svn_wc_adm_open.

* subversion/libsvn_client/externals.c (relegate_external,
  handle_external_item_change): Pass new parameters to svn_wc_adm_open.

* subversion/libsvn_wc/wc.h
  (enum svn_wc__adm_access_type): Renamed from enum svn_wc_adm_access_type.

  (svn_wc__adm_access_unlocked, svn_wc__adm_access_read_lock,
  svn_wc__adm_access_write_lock): Renamed enum identifiers.
  (svn_wc__adm_access_closed): New enum identifier.

  (struct svn_wc_adm_access_t): Added parent and children members.

* subversion/libsvn_wc/lock.c
  (svn_wc__adm_access_pool_cleanup, svn_wc__adm_access_pool_cleanup_child,
  svn_wc__adm_access_alloc, svn_wc_adm_retrieve): New functions.

  (svn_wc__adm_steal_write_lock): Use svn_wc__adm_access_alloc.

  (svn_wc_adm_open): Add parent_access and tree_lock parameters and
  processing. Use svn_wc__adm_access_alloc. For read locks check path
  is a directory. Register pool cleanup handler.

  (svn_wc__do_adm_close): Renamed from svn_wc_adm_close and made
  static. New parameter to preserver lock file. Close children. Kill
  pool cleanup handler. Reset state to svn_wc__adm_access_closed.

  (svn_wc_adm_close): Now a simple wrapper that calls svn_wc__do_adm_close.

  (svn_wc_adm_write_check): enum svn_wc__adm_access_type name change.

* subversion/tests/clients/cmdline/copy_tests.py (copy_modify_commit): New
  test.


git-svn-id: https://svn.apache.org/repos/asf/subversion/branches/issue-749-dev@842628 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/subversion/include/svn_error_codes.h b/subversion/include/svn_error_codes.h
index 88e7656..f7f2e91 100644
--- a/subversion/include/svn_error_codes.h
+++ b/subversion/include/svn_error_codes.h
@@ -200,6 +200,9 @@
   SVN_ERRDEF (SVN_ERR_WC_NOT_LOCKED,
               "Working copy not locked")
 
+  SVN_ERRDEF (SVN_ERR_WC_INVALID_LOCK,
+              "Invalid lock")
+
   SVN_ERRDEF (SVN_ERR_WC_NOT_DIRECTORY,
               "Path is not a working copy directory")
 
diff --git a/subversion/include/svn_wc.h b/subversion/include/svn_wc.h
index ca037db..b58df44 100644
--- a/subversion/include/svn_wc.h
+++ b/subversion/include/svn_wc.h
@@ -59,17 +59,57 @@
 /* Return an access baton in ADM_ACCESS for the working copy administrative
    area associated with the directory PATH.  If WRITE_LOCK is set the baton
    will include a write lock, otherwise the baton can only be used for read
-   access. POOL will be used to allocate the baton and any subsequently
-   cached items. */
+   access.
+
+   If PARENT_ACCESS is an open access baton PATH must refer to a
+   subdirectory of the PARENT_ACCESS directory, and then ADM_ACCESS will be
+   a child of PARENT_ACCESS.  svn_wc_adm_close can be used to close
+   ADM_ACCESS. If ADM_ACCESS has not been closed when PARENT_ACCESS is
+   closed, it wil be closed automatically at that stage.
+
+   If PARENT_ACCESS is an open access baton, and PATH refers to a
+   subdirectory that has already been opened as a child of PARENT_ACCESS
+   then the error SVN_ERR_WC_LOCKED will be returned.
+
+   PARENT_ACCESS can be NULL, in which case ADM_ACCESS is not a child of
+   any other baton and must be explicitly closed.
+
+   If TREE_LOCK is TRUE then the working copy directory hierarchy under
+   PATH will be locked. At each level batons for the subdirectories will be
+   children of the parent directory's baton.  This is an all-or-nothing
+   option, if it is not possible to lock the entire tree then an error will
+   be returned, there will be no locks and ADM_ACCESS will not be valid.
+
+   POOL will be used to allocate memory for the baton and any subsequently
+   cached items.  If ADM_ACCESS has not been closed when the pool is
+   cleared, it will be closed automatically at the point but any physical
+   lock will remain in the working copy.  */
 svn_error_t *svn_wc_adm_open (svn_wc_adm_access_t **adm_access,
+                              svn_wc_adm_access_t *parent_access,
                               const char *path,
                               svn_boolean_t write_lock,
+                              svn_boolean_t tree_lock,
                               apr_pool_t *pool);
 
-/* Give up the access baton ADM_ACCESS, and its lock if any */
+/* Return an access baton in ADM_ACCESS associated with PATH. PATH must be
+   a subdirectory of the PARENT_ACCESS directory, and must have been
+   previously opened with PARENT_ACCESS as a parent.
+
+   POOL is used only for local processing it is not used for the batons.
+
+   ### I can't think of a good name for this function. */
+svn_error_t *svn_wc_adm_retrieve (svn_wc_adm_access_t **adm_access,
+                                  svn_wc_adm_access_t *parent_access,
+                                  const char *path,
+                                  apr_pool_t *pool);
+
+/* Give up the access baton ADM_ACCESS, and its lock if any. It will also
+   close any child batons, for which this is a parent baton, that have not
+   yet been explicitly closed.  Any physical locks will be removed from the
+   working copy. */
 svn_error_t *svn_wc_adm_close (svn_wc_adm_access_t *adm_access);
 
-/* Ensure ADM_ACCESS has a write lock, and that it still exists. Returns
+/* Ensure ADM_ACCESS has a write lock, and that it is still valid. Returns
    SVN_ERR_WC_NOT_LOCKED if this is not the case. */
 svn_error_t *svn_wc_adm_write_check (svn_wc_adm_access_t *adm_access);
 
diff --git a/subversion/libsvn_client/commit.c b/subversion/libsvn_client/commit.c
index ca4a739..87a7bc8 100644
--- a/subversion/libsvn_client/commit.c
+++ b/subversion/libsvn_client/commit.c
@@ -919,15 +919,12 @@
             svn_path_split_nts (item->path, &adm_access_path, NULL, pool);
           adm_access = apr_hash_get (locked_dirs, adm_access_path,
                                      APR_HASH_KEY_STRING);
-         
-          if ( ! adm_access )
-            {
-              SVN_ERR (svn_wc_adm_open (&adm_access, adm_access_path, TRUE,
-                                        pool));
-              apr_hash_set (locked_dirs, apr_pstrdup(pool, adm_access_path),
-                            APR_HASH_KEY_STRING, adm_access);
-            }
-
+          if (! adm_access)
+            /* This should *never* happen */
+            return svn_error_createf(SVN_ERR_GENERAL, 0, NULL, pool,
+                                     "BUG: no lock for %s",
+                                     item->path);
+          
           if ((item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD) 
               && (item->kind == svn_node_dir)
               && (item->copyfrom_url))
diff --git a/subversion/libsvn_client/commit_util.c b/subversion/libsvn_client/commit_util.c
index 7208e51..05fbb73 100644
--- a/subversion/libsvn_client/commit_util.c
+++ b/subversion/libsvn_client/commit_util.c
@@ -60,8 +60,15 @@
 
   if (! apr_hash_get (locked_dirs, dir, APR_HASH_KEY_STRING))
     {
-      svn_wc_adm_access_t *adm_access;
-      SVN_ERR (svn_wc_adm_open (&adm_access, dir, TRUE, hash_pool));
+      /* Short lived, don't use hash pool */
+      const char *parent_dir = svn_path_remove_component_nts (dir, pool);
+      svn_wc_adm_access_t *adm_access, *parent_access;
+
+      /* Get parent, NULL is OK */
+      parent_access = apr_hash_get (locked_dirs, parent_dir,
+                                    APR_HASH_KEY_STRING);
+      SVN_ERR (svn_wc_adm_open (&adm_access, parent_access, dir, TRUE, FALSE,
+                                hash_pool));
       apr_hash_set (locked_dirs, apr_pstrdup (hash_pool, dir), 
                     APR_HASH_KEY_STRING, adm_access);
     }
@@ -139,6 +146,7 @@
                       svn_boolean_t adds_only,
                       svn_boolean_t copy_mode,
                       svn_boolean_t nonrecursive,
+                      svn_boolean_t lock_subdirs,
                       apr_pool_t *pool)
 {
   apr_pool_t *subpool = svn_pool_create (pool);  /* ### why? */
@@ -293,6 +301,8 @@
     {
       /* If the commit item is a directory, lock it, else lock its parent. */
       if (entry->kind == svn_node_dir)
+        /* ### Need to lock parent here as well? We will need to modify the
+           ### parents entry file if this is not the root */
         SVN_ERR (lock_dir (locked_dirs, path, pool));
       else
         SVN_ERR (lock_dir (locked_dirs, p_path, pool));
@@ -301,46 +311,50 @@
       add_committable (committables, path, entry->kind, url,
                        cf_url ? entry->copyfrom_rev : entry->revision, 
                        cf_url, state_flags);
+
+      /* We need to lock all directories to enable post-commit
+         processing of the working copy */
+      lock_subdirs = TRUE;
     }
 
-  /* For directories, recursively handle each of their entries (except
-     when the directory is being deleted, unless the deletion is part
-     of a replacement ... how confusing). */
-  if ((entries) 
-      && ((! (state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE))
-          || (state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)))
+  if (entries) 
     {
       apr_hash_index_t *hi;
       svn_wc_entry_t *this_entry;
       apr_pool_t *loop_pool = svn_pool_create (subpool);
 
       /* Loop over all other entries in this directory, skipping the
-         "this dir" entry. */
+         "this dir" entry.
+
+         ### May need to do this twice, if we need to do sub-directories,
+         ### before files. Subdirectories may require us to lock the
+         ### parent, files only lock this dir, and doing them in the wrong
+         ### order may break the parent-child lock relationship. */
       for (hi = apr_hash_first (subpool, entries);
            hi;
            hi = apr_hash_next (hi))
         {
           const void *key;
-          apr_ssize_t klen;
           void *val;
           const char *name;
           const char *used_url = NULL;
 
+          /* ### Why do we need to alloc these? */
           const char *full_path = apr_pstrdup (loop_pool, path);
           const char *this_url = apr_pstrdup (loop_pool, url);
           const char *this_cf_url
             = cf_url ? apr_pstrdup (loop_pool, cf_url) : NULL;
 
           /* Get the next entry */
-          apr_hash_this (hi, &key, &klen, &val);
-          name = (const char *) key;
+          apr_hash_this (hi, &key, NULL, &val);
+          name = key;
           
           /* Skip "this dir" */
           if (! strcmp (name, SVN_WC_ENTRY_THIS_DIR))
             continue;
 
           /* Name is an entry name; value is an entry structure. */
-          this_entry = (svn_wc_entry_t *) val;
+          this_entry = val;
 
           /* Skip subdirectory entries when we're not recursing.
 
@@ -358,6 +372,16 @@
           if (this_cf_url)
             this_cf_url = svn_path_join (this_cf_url, name, loop_pool);
 
+          if (lock_subdirs && this_entry->kind == svn_node_dir)
+            SVN_ERR (lock_dir (locked_dirs, full_path, pool));
+
+          /* For directories, recursively handle each of their entries
+             (except when the directory is being deleted, unless the
+             deletion is part of a replacement ... how confusing). */
+          if ((state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
+              && ! (state_flags & SVN_CLIENT_COMMIT_ITEM_ADD))
+            continue;
+
           /* We'll use the entry's URL if it has one and if we aren't
              in copy_mode, else, we'll just extend the parent's URL
              with the entry's basename.  */
@@ -377,6 +401,7 @@
                     adds_only,
                     copy_mode,
                     FALSE,
+                    lock_subdirs,
                     subpool));
 
           svn_pool_clear (loop_pool);
@@ -493,7 +518,7 @@
       /* Handle our TARGET. */
       SVN_ERR (harvest_committables (*committables, *locked_dirs, target, 
                                      url, NULL, entry, NULL, FALSE, FALSE, 
-                                     nonrecursive, pool));
+                                     nonrecursive, FALSE, pool));
 
       i++;
     }
@@ -527,7 +552,7 @@
   /* Handle our TARGET. */
   SVN_ERR (harvest_committables (*committables, *locked_dirs, target, 
                                  new_url, entry->url, entry, NULL,
-                                 FALSE, TRUE, FALSE, pool));
+                                 FALSE, TRUE, FALSE, FALSE, pool));
 
   return SVN_NO_ERROR;
 }
diff --git a/subversion/libsvn_client/externals.c b/subversion/libsvn_client/externals.c
index 6ddf4e1..29d30a1 100644
--- a/subversion/libsvn_client/externals.c
+++ b/subversion/libsvn_client/externals.c
@@ -232,7 +232,7 @@
   svn_error_t *err;
   svn_wc_adm_access_t *adm_access;
 
-  SVN_ERR (svn_wc_adm_open (&adm_access, path, TRUE, pool));
+  SVN_ERR (svn_wc_adm_open (&adm_access, NULL, path, TRUE, FALSE, pool));
   err = svn_wc_remove_from_revision_control (adm_access,
                                              SVN_WC_ENTRY_THIS_DIR,
                                              TRUE,
@@ -378,7 +378,8 @@
       svn_error_t *err;
       svn_wc_adm_access_t *adm_access;
 
-      SVN_ERR (svn_wc_adm_open (&adm_access, path, TRUE, ib->pool));
+      SVN_ERR (svn_wc_adm_open (&adm_access, NULL, path, TRUE, FALSE,
+                                ib->pool));
 
       /* We don't use relegate_external() here, because we know that
          nothing else in this externals description (at least) is
diff --git a/subversion/libsvn_wc/adm_files.c b/subversion/libsvn_wc/adm_files.c
index 0de4bae..8549b86 100644
--- a/subversion/libsvn_wc/adm_files.c
+++ b/subversion/libsvn_wc/adm_files.c
@@ -1107,7 +1107,7 @@
   /* Lock it immediately.  Theoretically, no compliant wc library
      would ever consider this an adm area until a README file were
      present... but locking it is still appropriately paranoid. */
-  SVN_ERR (svn_wc_adm_open (&adm_access, path, TRUE, pool));
+  SVN_ERR (svn_wc_adm_open (&adm_access, NULL, path, TRUE, FALSE, pool));
 
 
   /** Make subdirectories. ***/
diff --git a/subversion/libsvn_wc/adm_ops.c b/subversion/libsvn_wc/adm_ops.c
index ff79b88..64a3a70 100644
--- a/subversion/libsvn_wc/adm_ops.c
+++ b/subversion/libsvn_wc/adm_ops.c
@@ -354,8 +354,8 @@
           this_path = svn_path_join (path, name, subpool);
 
           if (current_entry->kind == svn_node_dir)
-             SVN_ERR (svn_wc_adm_open (&child_access, this_path, TRUE,
-                                       subpool));
+             SVN_ERR (svn_wc_adm_retrieve (&child_access, adm_access, this_path,
+                                           subpool));
           else
              child_access = adm_access;
           
@@ -366,9 +366,6 @@
                     (current_entry->kind == svn_node_dir) ? TRUE : FALSE,
                     new_revnum, rev_date, rev_author, subpool));
 
-          if (current_entry->kind == svn_node_dir)
-             SVN_ERR (svn_wc_adm_close (child_access));
-
           svn_pool_clear (subpool);
         }
 
@@ -630,7 +627,8 @@
              ### function, at which stage this open will fail and can be
              ### removed. */
           svn_wc_adm_access_t *adm_access;
-          SVN_ERR (svn_wc_adm_open (&adm_access, path, TRUE, pool));
+          SVN_ERR (svn_wc_adm_open (&adm_access, NULL, path, TRUE, FALSE,
+                                    pool));
 
           SVN_ERR (svn_wc_remove_from_revision_control
                    (adm_access, SVN_WC_ENTRY_THIS_DIR, FALSE, pool));
@@ -1201,7 +1199,8 @@
       if (entry->kind == svn_node_file)
         {
           was_deleted = entry->deleted;
-          SVN_ERR (svn_wc_adm_open (&adm_access, parent, TRUE, pool));
+          SVN_ERR (svn_wc_adm_open (&adm_access, NULL, parent, TRUE, FALSE,
+                                    pool));
         }
       else if (entry->kind == svn_node_dir)
         {
@@ -1211,7 +1210,8 @@
           parents_entry = apr_hash_get (entries, basey, APR_HASH_KEY_STRING);
           if (parents_entry)
             was_deleted = parents_entry->deleted;
-          SVN_ERR (svn_wc_adm_open (&adm_access, path, TRUE, pool));
+          SVN_ERR (svn_wc_adm_open (&adm_access, NULL, path, TRUE, TRUE, 
+                                    pool));
         }
 
       /* Remove the item from revision control. */
@@ -1507,8 +1507,8 @@
               const char *entrypath = svn_path_join (adm_access->path,
                                                      current_entry_name,
                                                      subpool);
-              SVN_ERR (svn_wc_adm_open (&entry_access, entrypath, TRUE,
-                                        subpool));
+              SVN_ERR (svn_wc_adm_retrieve (&entry_access, adm_access,
+                                            entrypath, pool));
               err = svn_wc_remove_from_revision_control (entry_access,
                                                          SVN_WC_ENTRY_THIS_DIR,
                                                          destroy_wf, subpool);
@@ -1519,7 +1519,6 @@
                 }
               else if (err)
                 return err;
-              SVN_ERR (svn_wc_adm_close (entry_access));
             }
         }
 
diff --git a/subversion/libsvn_wc/lock.c b/subversion/libsvn_wc/lock.c
index 651ba99..668e389 100644
--- a/subversion/libsvn_wc/lock.c
+++ b/subversion/libsvn_wc/lock.c
@@ -23,9 +23,14 @@
 
 #include "wc.h"
 #include "adm_files.h"
+#include "svn_pools.h"
 
 
 static svn_error_t *
+svn_wc__do_adm_close (svn_wc_adm_access_t *adm_access,
+                      svn_boolean_t abort);
+
+static svn_error_t *
 svn_wc__lock (svn_wc_adm_access_t *adm_access, int wait_for, apr_pool_t *pool)
 {
   svn_error_t *err;
@@ -59,17 +64,64 @@
   return svn_wc__remove_adm_file (path, pool, SVN_WC__ADM_LOCK, NULL);
 }
 
+static apr_status_t
+svn_wc__adm_access_pool_cleanup (void *p)
+{
+  svn_wc_adm_access_t *lock = p;
+  svn_error_t *err;
+
+  err = svn_wc__do_adm_close (lock, TRUE);
+
+  /* ### Is this the correct way to handle the error? */
+  if (err)
+    {
+      apr_status_t apr_err = err->apr_err;
+      svn_error_clear_all (err);
+      return apr_err;
+    }
+  else
+    return APR_SUCCESS;
+}
+
+static apr_status_t
+svn_wc__adm_access_pool_cleanup_child (void *p)
+{
+  svn_wc_adm_access_t *lock = p;
+  apr_pool_cleanup_kill (lock->pool, lock, svn_wc__adm_access_pool_cleanup);
+  return APR_SUCCESS;
+}
+
+static svn_wc_adm_access_t *
+svn_wc__adm_access_alloc (enum svn_wc__adm_access_type type,
+                          svn_wc_adm_access_t *parent,
+                          const char *path,
+                          apr_pool_t *pool)
+{
+  svn_wc_adm_access_t *lock = apr_palloc (pool, sizeof (*lock));
+  lock->type = type;
+  lock->parent = parent;
+  lock->children = NULL;
+  lock->lock_exists = FALSE;
+  /* ### Some places lock with a path that is not canonical, we need
+     ### cannonical paths for reliable parent-child determination */
+  lock->path = svn_path_canonicalize_nts (apr_pstrdup (pool, path), pool);
+  lock->pool = pool;
+
+  apr_pool_cleanup_register (lock->pool, lock,
+                             svn_wc__adm_access_pool_cleanup,
+                             svn_wc__adm_access_pool_cleanup_child);
+  return lock;
+}
+
 svn_error_t *
 svn_wc__adm_steal_write_lock (svn_wc_adm_access_t **adm_access,
                               const char *path,
                               apr_pool_t *pool)
 {
   svn_error_t *err;
-  svn_wc_adm_access_t *lock = apr_palloc (pool, sizeof (**adm_access));
-  lock->type = svn_wc_adm_access_write_lock;
-  lock->lock_exists = FALSE;
-  lock->path = apr_pstrdup (pool, path);
-  lock->pool = pool;
+  svn_wc_adm_access_t *lock
+    = svn_wc__adm_access_alloc (svn_wc__adm_access_write_lock, NULL, path,
+                                pool);
 
   err = svn_wc__lock (lock, 0, pool);
   if (err)
@@ -85,27 +137,126 @@
   return SVN_NO_ERROR;
 }
 
+static svn_error_t *
+svn_wc__adm_access_child (svn_wc_adm_access_t **adm_access,
+                          svn_wc_adm_access_t *parent_access,
+                          const char *path,
+                          apr_pool_t *pool)
+{
+
+  /* PATH must be a child. We only need the result, not the allocated
+     name. */
+  const char *name = svn_path_is_child (parent_access->path, path, pool);
+  if (!name)
+    return svn_error_createf (SVN_ERR_WC_INVALID_LOCK, 0, NULL, pool,
+                              "lock path is not a child (%s)",
+                              path);
+
+  if (parent_access->children)
+    *adm_access = apr_hash_get (parent_access->children, path,
+                                APR_HASH_KEY_STRING);
+  else
+    *adm_access = NULL;
+
+  return SVN_NO_ERROR;
+}
+
 svn_error_t *
 svn_wc_adm_open (svn_wc_adm_access_t **adm_access,
+                 svn_wc_adm_access_t *parent_access,
                  const char *path,
                  svn_boolean_t write_lock,
+                 svn_boolean_t tree_lock,
                  apr_pool_t *pool)
 {
-  svn_wc_adm_access_t *lock = apr_palloc (pool, sizeof (**adm_access));
-  lock->lock_exists = FALSE;
-  lock->path = apr_pstrdup (pool, path);
-  lock->pool = pool;
+  svn_wc_adm_access_t *lock;
 
+  if (parent_access)
+    {
+      SVN_ERR (svn_wc__adm_access_child (&lock, parent_access, path,
+                                         pool));
+      if (lock)
+        /* Already locked */
+        return svn_error_createf (SVN_ERR_WC_LOCKED, 0, NULL, pool,
+                                  "directory already locked (%s)",
+                                  path);
+    }
+
+  /* Need to create a new lock */
   if (write_lock)
     {
-      lock->type = svn_wc_adm_access_write_lock;
+      lock = svn_wc__adm_access_alloc (svn_wc__adm_access_write_lock,
+                                       parent_access, path, pool);
       SVN_ERR (svn_wc__lock (lock, 0, pool));
       lock->lock_exists = TRUE;
     }
   else
     {
-      /* ### Read-lock not yet implemented */
-      lock->type = svn_wc_adm_access_unlocked;
+      /* ### Read-lock not yet implemented. Since no physical lock gets
+         ### created we must check PATH is not a file. */
+      enum svn_node_kind node_kind;
+      SVN_ERR (svn_io_check_path (path, &node_kind, pool));
+      if (node_kind != svn_node_dir)
+        return svn_error_createf (SVN_ERR_WC_INVALID_LOCK, 0, NULL, pool,
+                                  "lock path is not a directory (%s)",
+                                  path);
+
+      lock = svn_wc__adm_access_alloc (svn_wc__adm_access_unlocked,
+                                       parent_access, path, pool);
+    }
+
+  lock->parent = parent_access;
+  if (lock->parent)
+    {
+      if (! lock->parent->children)
+        lock->parent->children = apr_hash_make (lock->parent->pool);
+
+#if 0
+      /* ### Need to think about this. Creating an empty APR hash allocates
+         ### several hundred bytes. Since the hash key is the complete
+         ### path, children for different parents cannot clash. Can we save
+         ### memory by reusing the parent's hash? */
+      lock->children = lock->parent->children;
+#endif
+
+      apr_hash_set (lock->parent->children, lock->path, APR_HASH_KEY_STRING,
+                    lock);
+    }
+
+  if (tree_lock)
+    {
+      /* ### Could use this code to initialise the cache, in which case the
+         ### subpool should be removed. */
+      apr_hash_t *entries;
+      apr_hash_index_t *hi;
+      apr_pool_t *subpool = svn_pool_create (pool);
+      SVN_ERR (svn_wc_entries_read (&entries, path, FALSE, subpool));
+      for (hi = apr_hash_first (subpool, entries); hi; hi = apr_hash_next (hi))
+        {
+          void *val;
+          svn_wc_entry_t *entry;
+          svn_wc_adm_access_t *entry_access;
+          const char *entry_path;
+          svn_error_t *svn_err;
+
+          apr_hash_this (hi, NULL, NULL, &val);
+          entry = val;
+          if (entry->kind != svn_node_dir
+              || ! strcmp (entry->name, SVN_WC_ENTRY_THIS_DIR))
+            continue;
+          entry_path = svn_path_join (lock->path, entry->name, subpool);
+
+          /* Use the main lock's pool here, it needs to persist */
+          svn_err = svn_wc_adm_open (&entry_access, lock, entry_path,
+                                     write_lock, tree_lock, lock->pool);
+          if (svn_err)
+            {
+              /* Give up any locks we have acquired. */
+              svn_wc_adm_close (lock);
+              return svn_err;
+            }
+        }
+      svn_pool_destroy (subpool);
     }
 
   *adm_access = lock;
@@ -113,26 +264,73 @@
 }
 
 svn_error_t *
-svn_wc_adm_close (svn_wc_adm_access_t *adm_access)
+svn_wc_adm_retrieve (svn_wc_adm_access_t **adm_access,
+                     svn_wc_adm_access_t *parent_access,
+                     const char *path,
+                     apr_pool_t *pool)
 {
-  if (adm_access->type == svn_wc_adm_access_write_lock)
+  SVN_ERR (svn_wc__adm_access_child (adm_access, parent_access, path, pool));
+  if (! *adm_access)
+    return svn_error_createf (SVN_ERR_WC_NOT_LOCKED, 0, NULL, pool,
+                              "directory not locked (%s)",
+                              path);
+  return SVN_NO_ERROR;
+}
+
+static svn_error_t *
+svn_wc__do_adm_close (svn_wc_adm_access_t *adm_access,
+                      svn_boolean_t preserve_lock)
+{
+  apr_hash_index_t *hi;
+
+  apr_pool_cleanup_kill (adm_access->pool, adm_access,
+                         svn_wc__adm_access_pool_cleanup);
+
+  /* Close children */
+  if (adm_access->children)
     {
-      if (adm_access->lock_exists)
+      for (hi = apr_hash_first (adm_access->pool, adm_access->children);
+           hi;
+           hi = apr_hash_next (hi))
+        {
+          void *val;
+          svn_wc_adm_access_t *child_access;
+          apr_hash_this (hi, NULL, NULL, &val);
+          child_access = val;
+          SVN_ERR (svn_wc__do_adm_close (child_access, preserve_lock));
+        }
+    }
+
+  /* Physically unlock if required */
+  if (adm_access->type == svn_wc__adm_access_write_lock)
+    {
+      if (adm_access->lock_exists && ! preserve_lock)
         {
           SVN_ERR (svn_wc__unlock (adm_access->path, adm_access->pool));
           adm_access->lock_exists = FALSE;
         }
       /* Reset to prevent further use of the write lock. */
-      adm_access->type = svn_wc_adm_access_unlocked;
+      adm_access->type = svn_wc__adm_access_closed;
     }
 
+  /* Detach from parent */
+  if (adm_access->parent)
+    apr_hash_set (adm_access->parent->children, adm_access->path,
+                  APR_HASH_KEY_STRING, NULL);
+
   return SVN_NO_ERROR;
 }
 
 svn_error_t *
+svn_wc_adm_close (svn_wc_adm_access_t *adm_access)
+{
+  return svn_wc__do_adm_close (adm_access, FALSE);
+}
+
+svn_error_t *
 svn_wc_adm_write_check (svn_wc_adm_access_t *adm_access)
 {
-  if (adm_access->type == svn_wc_adm_access_write_lock)
+  if (adm_access->type == svn_wc__adm_access_write_lock)
     {
       if (adm_access->lock_exists)
         {
diff --git a/subversion/libsvn_wc/log.c b/subversion/libsvn_wc/log.c
index eecb831..f5d1ece 100644
--- a/subversion/libsvn_wc/log.c
+++ b/subversion/libsvn_wc/log.c
@@ -601,7 +601,8 @@
   if (entry->kind == svn_node_dir)
     {
       svn_wc_adm_access_t *adm_access;
-      SVN_ERR (svn_wc_adm_open (&adm_access, full_path, TRUE, loggy->pool));
+      SVN_ERR (svn_wc_adm_retrieve (&adm_access, loggy->adm_access, full_path,
+                                    loggy->pool));
       err = svn_wc_remove_from_revision_control (adm_access,
                                                  SVN_WC_ENTRY_THIS_DIR,
                                                  TRUE, loggy->pool);
@@ -795,15 +796,13 @@
             {
               pdir = svn_path_join (loggy->adm_access->path, key, pool);
               base_name = SVN_WC_ENTRY_THIS_DIR;
-              SVN_ERR (svn_wc_adm_open (&entry_access, pdir, TRUE, pool));
+              SVN_ERR (svn_wc_adm_retrieve (&entry_access, loggy->adm_access,
+                                            pdir, pool));
             }
 
           if (base_name)
             SVN_ERR (svn_wc_remove_from_revision_control 
                      (entry_access, base_name, FALSE, pool));
-
-          if (cur_entry->kind == svn_node_dir)
-            SVN_ERR (svn_wc_adm_close (entry_access));
         }
     }
 
diff --git a/subversion/libsvn_wc/props.c b/subversion/libsvn_wc/props.c
index a8b77a8..919c08d 100644
--- a/subversion/libsvn_wc/props.c
+++ b/subversion/libsvn_wc/props.c
@@ -393,7 +393,7 @@
       return SVN_NO_ERROR; /* ### svn_node_none or svn_node_unknown */
     }
 
-  SVN_ERR (svn_wc_adm_open (&adm_access, parent, TRUE, pool));
+  SVN_ERR (svn_wc_adm_open (&adm_access, NULL, parent, TRUE, FALSE, pool));
   SVN_ERR (svn_wc__open_adm_file (&log_fp, parent, SVN_WC__ADM_LOG,
                                   (APR_WRITE | APR_CREATE), /* not excl */
                                   pool));
diff --git a/subversion/libsvn_wc/update_editor.c b/subversion/libsvn_wc/update_editor.c
index fadd4fb..af926d7 100644
--- a/subversion/libsvn_wc/update_editor.c
+++ b/subversion/libsvn_wc/update_editor.c
@@ -492,7 +492,7 @@
   svn_wc_adm_access_t *adm_access;
   svn_stringbuf_t *log_item = svn_stringbuf_create ("", pool);
 
-  SVN_ERR (svn_wc_adm_open (&adm_access, pb->path, TRUE, pool));
+  SVN_ERR (svn_wc_adm_open (&adm_access, NULL, pb->path, TRUE, TRUE, pool));
   SVN_ERR (svn_wc__open_adm_file (&log_fp,
                                   pb->path,
                                   SVN_WC__ADM_LOG,
@@ -758,7 +758,8 @@
       svn_stringbuf_t *entry_accum = svn_stringbuf_create ("", db->pool);
 
       /* Lock down the administrative area */
-      SVN_ERR (svn_wc_adm_open (&adm_access, db->path, TRUE, db->pool));
+      SVN_ERR (svn_wc_adm_open (&adm_access, NULL, db->path, TRUE, FALSE,
+                                db->pool));
       
       /* Open log file */
       SVN_ERR (svn_wc__open_adm_file (&log_fp,
@@ -1192,7 +1193,7 @@
   /* Lock the parent directory while we change things.  If for some
      reason the parent isn't under version control, this function will
      bomb out.  */
-  SVN_ERR (svn_wc_adm_open (&adm_access, parent_dir, TRUE, pool));
+  SVN_ERR (svn_wc_adm_open (&adm_access, NULL, parent_dir, TRUE, FALSE, pool));
 
   /*
      When this function is called on file F, we assume the following
diff --git a/subversion/libsvn_wc/wc.h b/subversion/libsvn_wc/wc.h
index 17e4a1b..c087a74 100644
--- a/subversion/libsvn_wc/wc.h
+++ b/subversion/libsvn_wc/wc.h
@@ -142,11 +142,11 @@
    /* PATH to directory which contains the administrative area */
    const char *path;
 
-   enum svn_wc_adm_access_type {
+   enum svn_wc__adm_access_type {
 
-      /* SVN_WC_ADM_ACCESS_UNLOCKED indicates no lock is held allowing
+      /* SVN_WC__ADM_ACCESS_UNLOCKED indicates no lock is held allowing
          read-only access without cacheing. */
-      svn_wc_adm_access_unlocked,
+      svn_wc__adm_access_unlocked,
 
 #if 0 /* How cacheing might work one day */
 
@@ -158,14 +158,18 @@
          ### area (consider running svn_wc_status on some other user's
          ### working copy). */
 
-      /* SVN_WC_ADM_ACCESS_READ_LOCK indicates that read-only access and
+      /* SVN_WC__ADM_ACCESS_READ_LOCK indicates that read-only access and
          cacheing are allowed. */
-      svn_wc_adm_access_read_lock,
+      svn_wc__adm_access_read_lock,
 #endif
 
-      /* SVN_WC_ADM_ACCESS_WRITE_LOCK indicates that read-write access and
+      /* SVN_WC__ADM_ACCESS_WRITE_LOCK indicates that read-write access and
          cacheing are allowed. */
-      svn_wc_adm_access_write_lock
+      svn_wc__adm_access_write_lock,
+
+      /* SVN_WC__ADM_ACCESS_CLOSED indicates that the baton has been
+         closed. */
+      svn_wc__adm_access_closed
 
    } type;
 
@@ -184,6 +188,13 @@
    apr_hash_t *entries;
 #endif
 
+   /* PARENT access baton, may be NULL. */
+   svn_wc_adm_access_t *parent;
+
+   /* CHILDREN is a hash of svn_wc_adm_access_t* keyed on char*
+      representing the path to sub-directories that are also locked. */
+   apr_hash_t *children;
+
    /* POOL is used to allocate cached items, they need to persist for the
       lifetime of this access baton */
    apr_pool_t *pool;
diff --git a/subversion/tests/clients/cmdline/copy_tests.py b/subversion/tests/clients/cmdline/copy_tests.py
index 0541799..f3569e7 100755
--- a/subversion/tests/clients/cmdline/copy_tests.py
+++ b/subversion/tests/clients/cmdline/copy_tests.py
@@ -470,6 +470,39 @@
   return 0
 
 
+# Takes out working-copy locks for A/B2 and child A/B2/E. At one stage
+# during issue 749 the second lock cause an already-locked error.
+def copy_modify_commit(sbox):
+  "copy a directory hierarchy and modify before commit"
+
+  if sbox.build():
+    return 1
+
+  wc_dir = sbox.wc_dir
+  outlines, errlines = svntest.main.run_svn(None, 'cp',
+                                            wc_dir + '/A/B', wc_dir + '/A/B2',
+                                            '-m', 'fooogle')
+  if errlines:
+    print "Whoa, failed to copy A/B to A/B2"
+    return 1
+  
+  alpha_path = os.path.join(wc_dir, 'A', 'B2', 'E', 'alpha')
+  svntest.main.file_append(alpha_path, "modified alpha")
+
+  expected_output = svntest.wc.State(wc_dir, {
+    'A/B2' : Item(verb='Adding'),
+    'A/B2/E/alpha' : Item(verb='Sending'),
+    })
+
+  if svntest.actions.run_and_verify_commit (wc_dir,
+                                            expected_output,
+                                            None,
+                                            None,
+                                            None, None,
+                                            None, None,
+                                            wc_dir):
+    return 1
+
 ########################################################################
 # Run the tests
 
@@ -481,6 +514,7 @@
               receive_copy_in_update,
               resurrect_deleted_dir,
               no_copy_overwrites,
+              copy_modify_commit,
              ]
 
 if __name__ == '__main__':