Author: pburba
Date: Thu Sep  2 18:10:01 2010
New Revision: 992042

URL: http://svn.apache.org/viewvc?rev=992042&view=rev
Log:
Fix issue #2915 'Handle mergeinfo for subtrees missing due to removal by
non-svn command'.

With this change, if you attempt a merge-tracking aware merge to a WC
which is missing subtrees due to an OS-level deletion, then an error is
raised before any editor drives begin.  The error message describes the
root of each missing path.

* subversion/libsvn_client/merge.c

 (get_mergeinfo_walk_baton): Add some new members for tracking sub-
  directories' dirents.

 (record_missing_subtree_roots): New function.

 (get_mergeinfo_walk_cb): Use new function to flag missing subtree
  roots.

 (get_mergeinfo_paths): Raise a SVN_ERR_CLIENT_NOT_READY_TO_MERGE error if
  any unexpectedly missing subtrees are found.

* subversion/tests/cmdline/merge_authz_tests.py

  (skipped paths get overriding mergeinfo): Don't test the file missing via
   OS-delete scenario, this is now covered in a much clearer manner in the
   new merge_tests.py.merge_with_os_deleted_subtrees test.  Also remove not
   about testing a missing directory, that is also covered in the new test.

* subversion/tests/cmdline/merge_tests.py

 (merge_into_missing): Use --ignore-ancestry during this test's merge to
  disregard mergeinfo and preserve the original intent of the test.
 
 (skipped_files_get_correct_mergeinfo): Use a shallow WC to rather than an
  OS-level delete to test issue #3440.  Remove the second part of this test
  which has no relevance now that merge tracking doesn't tolerate subtrees
  missing via OS-deletion.

 (merge_with_os_deleted_subtrees): New test.

 (test_list): Add merge_with_os_deleted_subtrees.

* subversion/tests/cmdline/merge_tree_conflict_tests.py

  (tree_conflicts_merge_edit_onto_missing,
   tree_conflicts_merge_del_onto_missing): Use --ignore-ancestry during these
   tests' merges to disregard mergeinfo and preserve the original intent of
   the test.  Don't bother with the post-merge commit either, since there is
   nothing to commit as there are no mergeinfo changes.

* subversion/tests/cmdline/svntest/actions.py

  (deep_trees_run_tests_scheme_for_merge): Add new args allowing caller to
   use --ignore-ancestry during the merge and to skip the post merge commit
   if desired.

Modified:
    subversion/trunk/subversion/libsvn_client/merge.c
    subversion/trunk/subversion/tests/cmdline/merge_authz_tests.py
    subversion/trunk/subversion/tests/cmdline/merge_tests.py
    subversion/trunk/subversion/tests/cmdline/merge_tree_conflict_tests.py
    subversion/trunk/subversion/tests/cmdline/svntest/actions.py

Modified: subversion/trunk/subversion/libsvn_client/merge.c
URL: 
http://svn.apache.org/viewvc/subversion/trunk/subversion/libsvn_client/merge.c?rev=992042&r1=992041&r2=992042&view=diff
==============================================================================
--- subversion/trunk/subversion/libsvn_client/merge.c (original)
+++ subversion/trunk/subversion/libsvn_client/merge.c Thu Sep  2 18:10:01 2010
@@ -5304,6 +5304,17 @@ struct get_mergeinfo_walk_baton
 {
   /* Array of paths that have explicit mergeinfo and/or are switched. */
   apr_array_header_t *children_with_mergeinfo;
+
+  /* A hash of MERGE_TARGET_ABSPATH's subdirectories' dirents.  Maps
+     const char * absolute working copy paths to dirent hashes as obtained
+     by svn_io_get_dirents3().  Contents are allocated in CB_POOL. */
+  apr_hash_t *subtree_dirents;
+
+  /* A hash to keep track of any subtrees in the merge target which are
+     unexpectedly missing from disk.  Maps const char * absolute working
+     copy paths to the same.  Contents are allocated in CB_POOL. */
+  apr_hash_t *missing_subtrees;
+
   /* Merge source canonical path. */
   const char* merge_src_canon_path;
 
@@ -5324,8 +5335,111 @@ struct get_mergeinfo_walk_baton
 
   /* Pool from which to allocate new elements of CHILDREN_WITH_MERGEINFO. */
   apr_pool_t *pool;
+
+  /* Pool with a lifetime guaranteed over all the get_mergeinfo_walk_cb
+     callbacks. */
+  apr_pool_t *cb_pool;
 };
 
+/* Helper for the svn_wc__node_found_func_t callback get_mergeinfo_walk_cb().
+
+   Checks for issue #2915 subtrees, i.e. those that the WC thinks are on disk
+   but have been removed due to an OS-level deletion.
+
+   If the supposed working path LOCAL_ABSPATH, of kind KIND, is the root
+   of a missing subtree, then add a (const char *) WC absolute path to
+   (const char *) WC absolute path mapping to MISSING_SUBTREES, where the
+   paths are both a copy of LOCAL_ABSPATH, allocated in RESULT_POOL.
+
+   If LOCAL_ABSPATH is a directory and is not missing from disk, then add
+   a (const char *) WC absolute path to (svn_io_dirent2_t *) dirent mapping
+   to SUBTREE_DIRENTS, again allocated in RESULT_POOL (see
+   svn_io_get_dirents3).
+
+   SCRATCH_POOL is used for temporary allocations.
+
+   Note: Since this is effetively a svn_wc__node_found_func_t callback, it
+   must be called in depth-first order. */
+static svn_error_t *
+record_missing_subtree_roots(const char *local_abspath,
+                             svn_node_kind_t kind,
+                             apr_hash_t *subtree_dirents,
+                             apr_hash_t *missing_subtrees,
+                             apr_pool_t *result_pool,
+                             apr_pool_t *scratch_pool)
+{
+  svn_boolean_t missing_subtree_root = FALSE;
+
+  /* Store the dirents for each directory in SUBTREE_DIRENTS. */
+  if (kind == svn_node_dir)
+    {
+      /* If SUBTREE_DIRENTS is empty LOCAL_ABSPATH is merge target. */
+      if (apr_hash_count(subtree_dirents) == 0 
+          || apr_hash_get(subtree_dirents,
+                          svn_dirent_dirname(local_abspath,
+                                             scratch_pool),
+                                             APR_HASH_KEY_STRING))
+        {
+          apr_hash_t *dirents;
+          svn_error_t *err = svn_io_get_dirents3(&dirents, local_abspath,
+                                                 TRUE, result_pool,
+                                                 scratch_pool);
+          if (err)
+            {
+              if (APR_STATUS_IS_ENOENT(err->apr_err)
+                  || SVN__APR_STATUS_IS_ENOTDIR(err->apr_err))
+                {
+                  /* We can't get this directory's dirents, it's missing
+                     from disk. */
+                  svn_error_clear(err);
+                  missing_subtree_root = TRUE;
+                }
+              else
+                {
+                  return err;
+                }
+            }
+          else
+            {
+              apr_hash_set(subtree_dirents,
+                           apr_pstrdup(result_pool, local_abspath),
+                           APR_HASH_KEY_STRING, dirents);
+            }      
+        }
+    }
+  else /* kind != svn_node_dir */
+    {
+      /* Is this non-directory missing from disk?  Check LOCAL_ABSPATH's
+         parent's dirents. */
+      apr_hash_t *parent_dirents = apr_hash_get(subtree_dirents,
+                                                
svn_dirent_dirname(local_abspath,
+                                                                   
scratch_pool),
+                                                APR_HASH_KEY_STRING);
+
+      /* If the parent_dirents is NULL, then LOCAL_ABSPATH is the
+         subtree of a missing subtree.  Since we only report the roots
+         of missing subtrees there is nothing more to do in that case. */
+      if (parent_dirents)
+        {
+          svn_io_dirent2_t *dirent =
+            apr_hash_get(parent_dirents,
+                         svn_dirent_basename(local_abspath, scratch_pool),
+                         APR_HASH_KEY_STRING);
+          if (!dirent)
+            missing_subtree_root = TRUE;
+        }
+    }
+
+  if (missing_subtree_root)
+    {
+      const char *path = apr_pstrdup(result_pool, local_abspath);
+
+      apr_hash_set(missing_subtrees, path,
+                   APR_HASH_KEY_STRING, path);
+    }
+
+  return SVN_NO_ERROR;
+}
 
 /* svn_wc__node_found_func_t callback for get_mergeinfo_paths().
 
@@ -5410,6 +5524,17 @@ get_mergeinfo_walk_cb(const char *local_
                          &&(kind == svn_node_dir)
                          && (strcmp(abs_parent_path,
                                     wb->merge_target_abspath) == 0));
+  /* Make sure what the WC thinks is present on disk really is. */
+#ifndef SVN_WC__SINGLE_DB
+   if (!absent && !deleted && !obstructed)
+#else
+   if (!absent && !deleted)
+#endif
+    SVN_ERR(record_missing_subtree_roots(local_abspath, kind,
+                                         wb->subtree_dirents,
+                                         wb->missing_subtrees,
+                                         wb->cb_pool,
+                                         scratch_pool));
 
   /* Store PATHs with explict mergeinfo, which are switched, are missing
      children due to a sparse checkout, are scheduled for deletion are absent
@@ -5628,7 +5753,7 @@ insert_parent_and_sibs_of_sw_absent_del_
 /* Helper for do_directory_merge()
 
    If HONOR_MERGEINFO is TRUE, then perform a depth first walk of the working
-   copy tree rooted at MERGE_CMD_BATON->TARGET_ABSPATH.
+   copy tree rooted at MERGE_CMD_BATON->TARGET_ABSPATH to depth DEPTH.
    Create an svn_client__merge_path_t * for any path which meets one or more
    of the following criteria:
 
@@ -5656,6 +5781,9 @@ insert_parent_and_sibs_of_sw_absent_del_
    If HONOR_MERGEINFO is FALSE, then create an svn_client__merge_path_t * only
    for MERGE_CMD_BATON->TARGET_ABSPATH (i.e. only criteria 7 is applied).
 
+   If subtrees within the requested DEPTH are unexpectedly missing disk,
+   then raise SVN_ERR_CLIENT_NOT_READY_TO_MERGE.
+
    Store the svn_client__merge_path_t *'s in *CHILDREN_WITH_MERGEINFO in
    depth-first order based on the svn_client__merge_path_t *s path member as
    sorted by svn_path_compare_paths().  Set the remaining_ranges field of each
@@ -5695,6 +5823,9 @@ get_mergeinfo_paths(apr_array_header_t *
   struct get_mergeinfo_walk_baton wb = { 0 };
 
   wb.children_with_mergeinfo = children_with_mergeinfo;
+  wb.cb_pool = svn_pool_create(scratch_pool);
+  wb.subtree_dirents = apr_hash_make(wb.cb_pool);
+  wb.missing_subtrees = apr_hash_make(wb.cb_pool);
   wb.merge_src_canon_path = merge_src_canon_path;
   wb.merge_target_abspath = merge_cmd_baton->target_abspath;
   wb.source_root_url = source_root_url;
@@ -5718,6 +5849,37 @@ get_mergeinfo_paths(apr_array_header_t *
                                      merge_cmd_baton->ctx->cancel_baton,
                                      scratch_pool));
 
+  if (apr_hash_count(wb.missing_subtrees))
+    {
+      apr_hash_index_t *hi;
+      apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+      const char *missing_subtree_err_str = NULL;
+      
+      for (hi = apr_hash_first(iterpool, wb.missing_subtrees);
+           hi;
+           hi = apr_hash_next(hi))
+        {
+          const char *missing_abspath = svn__apr_hash_index_key(hi);
+
+          missing_subtree_err_str = apr_psprintf(
+            scratch_pool, "%s%s\n",
+            missing_subtree_err_str ? missing_subtree_err_str : "",
+            svn_dirent_local_style(missing_abspath, scratch_pool));
+        }
+
+      if (missing_subtree_err_str)
+        return svn_error_createf(SVN_ERR_CLIENT_NOT_READY_TO_MERGE,
+                                 NULL,
+                                 _("Merge tracking not allowed with missing "
+                                   "subtrees; try restoring these items "
+                                   "first:\n%s"),
+                                 missing_subtree_err_str);
+  }
+
+  /* This pool is only needed across all the callbacks to detect
+     missing subtrees. */
+  svn_pool_destroy(wb.cb_pool);
+
   /* CHILDREN_WITH_MERGEINFO must be in depth first order, but the node
      walk code returns nodes in a non particular order.  Also, we may need
      to add elements to the array to cover case 3) through 5) from the

Modified: subversion/trunk/subversion/tests/cmdline/merge_authz_tests.py
URL: 
http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/merge_authz_tests.py?rev=992042&r1=992041&r2=992042&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/merge_authz_tests.py (original)
+++ subversion/trunk/subversion/tests/cmdline/merge_authz_tests.py Thu Sep  2 
18:10:01 2010
@@ -82,13 +82,6 @@ def mergeinfo_and_skipped_paths(sbox):
   #   2) Destination of merge is inaccessible due to authz restrictions.
   #   3) Source *and* destination of merge is inaccessible due to authz
   #      restrictions.
-  #   4) File path is versioned but is missing from disk due to OS deletion.
-  #      This isn't technically part of issue #2893 but we handle this case
-  #      and it didn't warrant its own test).
-  #
-  # Eventually we should also test(?):
-  #
-  #   5) Dir path is versioned but is missing from disk due to an OS deletion.
 
   sbox.build()
   wc_dir = sbox.wc_dir
@@ -122,27 +115,20 @@ def mergeinfo_and_skipped_paths(sbox):
   omega_path = os.path.join(wc_restricted, "A_COPY", "D", "H", "omega")
   zeta_path = os.path.join(wc_dir, "A", "D", "H", "zeta")
 
-  # Restrict access to some more of the merge destination the
-  # old fashioned way, delete it via the OS.
-  ### TODO: Delete a versioned directory?
-  os.remove(omega_path)
-
   # Merge r4:8 into the restricted WC's A_COPY.
   #
   # We expect A_COPY/B/E to be skipped because we can't access the source
   # and A_COPY/D/H/omega because it is missing.  Since we have A_COPY/B/E
   # we should override it's inherited mergeinfo, giving it just what it
-  # inherited from A_COPY before the merge.  omega is missing, but since
-  # it is a file we can record the fact that it is missing in its parent
-  # directory A_COPY/D/H.
+  # inherited from A_COPY before the merge.
   expected_output = wc.State(A_COPY_path, {
     'D/G/rho'   : Item(status='U '),
     'D/H/psi'   : Item(status='U '),
+    'D/H/omega' : Item(status='U '),
     })
   expected_mergeinfo_output = wc.State(A_COPY_path, {
     ''          : Item(status=' U'),
     'B/E'       : Item(status=' U'),
-    'D/H/omega' : Item(status=' U'),
     })
   expected_elision_output = wc.State(A_COPY_path, {
     })
@@ -150,7 +136,7 @@ def mergeinfo_and_skipped_paths(sbox):
     ''          : Item(status=' M', wc_rev=8),
     'D/H/chi'   : Item(status='  ', wc_rev=8),
     'D/H/psi'   : Item(status='M ', wc_rev=8),
-    'D/H/omega' : Item(status='!M', wc_rev=8),
+    'D/H/omega' : Item(status='M ', wc_rev=8),
     'D/H'       : Item(status='  ', wc_rev=8),
     'D/G/pi'    : Item(status='  ', wc_rev=8),
     'D/G/rho'   : Item(status='M ', wc_rev=8),
@@ -171,9 +157,7 @@ def mergeinfo_and_skipped_paths(sbox):
     ''          : Item(props={SVN_PROP_MERGEINFO : '/A:5-8'}),
     'D/H/psi'   : Item("New content"),
     'D/H/chi'   : Item("This is the file 'chi'.\n"),
-     # 'D/H/omega' : run_and_verify_merge() doesn't support checking
-     #               the props on a missing path, so we do that
-     #               manually (see below).
+    'D/H/omega' : Item("New content"),
     'D/H'       : Item(),
     'D/G/pi'    : Item("This is the file 'pi'.\n"),
     'D/G/rho'   : Item("New content"),
@@ -192,7 +176,6 @@ def mergeinfo_and_skipped_paths(sbox):
     })
   expected_skip = wc.State(A_COPY_path, {
     'B/E'       : Item(),
-    'D/H/omega' : Item(),
     })
   svntest.actions.run_and_verify_merge(A_COPY_path, '4', '8',
                                        sbox.repo_url + '/A', None,
@@ -205,10 +188,6 @@ def mergeinfo_and_skipped_paths(sbox):
                                        None, None, None, None,
                                        None, 1)
 
-  # Manually check the props on A_COPY/D/H/omega.
-  svntest.actions.run_and_verify_svn(None, ['\n'], [],
-                                    'pg', SVN_PROP_MERGEINFO, omega_path)
-
   # Merge r4:8 into the restricted WC's A_COPY_2.
   #
   # As before we expect A_COPY_2/B/E to be skipped because we can't access the

Modified: subversion/trunk/subversion/tests/cmdline/merge_tests.py
URL: 
http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/merge_tests.py?rev=992042&r1=992041&r2=992042&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/merge_tests.py (original)
+++ subversion/trunk/subversion/tests/cmdline/merge_tests.py Thu Sep  2 
18:10:01 2010
@@ -1733,6 +1733,10 @@ def merge_into_missing(sbox):
       'Q/baz'    : Item(status='! ', wc_rev='3'),
     })    
 
+  # Use --ignore-ancestry because merge tracking aware merges raise an
+  # error when the merge target is missing subtrees due to OS-level
+  # deletes.
+
   ### Need to real and dry-run separately since real merge notifies Q
   ### twice!
   svntest.actions.run_and_verify_merge(F_path, '1', '2', F_url, None,
@@ -1743,23 +1747,20 @@ def merge_into_missing(sbox):
                                        expected_status,
                                        expected_skip,
                                        None, None, None, None, None,
-                                       0, 0, '--dry-run', F_path)
+                                       0, 0, '--dry-run',
+                                       '--ignore-ancestry', F_path)
 
   expected_status = wc.State(F_path, {
-    ''      : Item(status=' M', wc_rev=1),
-    'foo'   : Item(status='!M', wc_rev=2),
+    ''      : Item(status='  ', wc_rev=1),
+    'foo'   : Item(status='! ', wc_rev=2),
     'Q'     : Item(status='! ', wc_rev='?'),
     })
   expected_mergeinfo_output = wc.State(F_path, {
-    ''    : Item(status=' U'),
-    'foo' : Item(status=' U'), # Mergeinfo is set on missing/obstructed files.
     })
     
   if single_db:
     # Revision is known and we can record mergeinfo
     expected_status.tweak('Q', wc_rev='2', entry_rev='?')
-    expected_mergeinfo_output.add({'Q' : Item(status=' U')})
-    # Missing data still available
     expected_status.add({
       'Q/R'      : Item(status='! ', wc_rev='3'),
       'Q/R/bar'  : Item(status='! ', wc_rev='3'),
@@ -1774,7 +1775,8 @@ def merge_into_missing(sbox):
                                        expected_status,
                                        expected_skip,
                                        None, None, None, None, None,
-                                       0, 0)
+                                       0, 0,
+                                       '--ignore-ancestry', F_path)
 
   # This merge fails when it attempts to descend into the missing
   # directory.  That's OK, there is no real need to support merge into
@@ -1789,8 +1791,8 @@ def merge_into_missing(sbox):
   # Check working copy is not locked.
   expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
   expected_status.add({
-    'A/B/F'     : Item(status=' M', wc_rev=1),
-    'A/B/F/foo' : Item(status='!M', wc_rev=2),
+    'A/B/F'     : Item(status='  ', wc_rev=1),
+    'A/B/F/foo' : Item(status='! ', wc_rev=2),
     'A/B/F/Q'   : Item(status='! ', wc_rev='?'),
     })
   if single_db:
@@ -14995,6 +14997,7 @@ def skipped_files_get_correct_mergeinfo(
 
   # Some paths we'll care about
   A_COPY_path   = os.path.join(wc_dir, "A_COPY")
+  H_COPY_path   = os.path.join(wc_dir, "A_COPY", "D", "H")
   psi_COPY_path = os.path.join(wc_dir, "A_COPY", "D", "H", "psi")
   psi_path      = os.path.join(wc_dir, "A", "D", "H", "psi")
 
@@ -15022,15 +15025,21 @@ def skipped_files_get_correct_mergeinfo(
     [], 'merge', '-c3', sbox.repo_url + '/A', A_COPY_path)
   svntest.main.run_svn(None, 'commit', '-m', 'initial merge', wc_dir)
 
-  # Update WC to uniform revision and then delete, via the OS, A_COPY/D/H/psi
-  # and then merge all available revisions from A to A_COPY.  A_COPY/D/H/psi
-  # should be reported as skipped and get explicit mergeinfo set on it
-  # reflecting what it previously inherited from A_COPY after the first
-  # merge, i.e. '/A/D/H/psi:3'.  Issue #3440 occurred when empty mergeinfo
-  # was set on A_COPY/D/H/psi, making it appear that r3 was never merged.
+  # Update WC to uniform revision and then set the depth on A_COPY/D/H to
+  # empty.  Then merge all available revisions from A to A_COPY.
+  # A_COPY/D/H/psi and A_COPY/D/H/omega are not present due to their
+  # parent's depth and should be reported as skipped.  A_COPY/D/H should
+  # get explicit mergeinfo set on it reflecting what it previously inherited
+  # from A_COPY after the first merge, i.e. '/A/D/H:3', plus non-inheritable
+  # mergeinfo describing what was done during this merge,
+  # i.e. '/A/D/H:2*,4-8*'.
+  #
+  # Issue #3440 occurred when empty mergeinfo was set on A_COPY/D/H, making
+  # it appear that r3 was never merged.
   svntest.actions.run_and_verify_svn(None, ["At revision 8.\n"], [],
                                      'up', wc_dir)
-  os.remove(psi_COPY_path)
+  svntest.actions.run_and_verify_svn(None, None, [],
+                                     'up', '--set-depth=empty', H_COPY_path)
   expected_status = wc.State(A_COPY_path, {
     ''          : Item(status=' M'),
     'B'         : Item(status='  '),
@@ -15047,10 +15056,7 @@ def skipped_files_get_correct_mergeinfo(
     'D/G/rho'   : Item(status='M '),
     'D/G/tau'   : Item(status='  '),
     'D/gamma'   : Item(status='  '),
-    'D/H'       : Item(status='  '),
-    'D/H/chi'   : Item(status='  '),
-    'D/H/psi'   : Item(status='!M'),
-    'D/H/omega' : Item(status='M '),
+    'D/H'       : Item(status=' M'),
     })
   expected_status.tweak(wc_rev=8)
   expected_disk = wc.State('', {
@@ -15069,71 +15075,18 @@ def skipped_files_get_correct_mergeinfo(
     'D/G/rho'   : Item("New content"),
     'D/G/tau'   : Item("This is the file 'tau'.\n"),
     'D/gamma'   : Item("This is the file 'gamma'.\n"),
-    'D/H'       : Item(),
-    'D/H/chi'   : Item("This is the file 'chi'.\n"),
-    #'D/H/psi'  : Nothing here, this file was deleted via the OS.
-    'D/H/omega' : Item("New content"),
+    'D/H'       : Item(props={SVN_PROP_MERGEINFO : '/A/D/H:2*,3,4-8*'}),
     })
   expected_skip = wc.State(A_COPY_path,
-                           {'D/H/psi' : Item()})
+                           {'D/H/psi'   : Item(),
+                            'D/H/omega' : Item()})
   expected_output = wc.State(A_COPY_path,
                              {'B/E/beta'  : Item(status='U '),
-                              'D/G/rho'   : Item(status='U '),
-                              'D/H/omega' : Item(status='U '),})
-  expected_mergeinfo_output = wc.State(A_COPY_path, {
-    ''        : Item(status=' U'),
-    'D/H/psi' : Item(status=' U'),
-    })
-  expected_elision_output = wc.State(A_COPY_path, {
-    })
-  svntest.actions.run_and_verify_merge(A_COPY_path, None, None,
-                                       sbox.repo_url + '/A', None,
-                                       expected_output,
-                                       expected_mergeinfo_output,
-                                       expected_elision_output,
-                                       expected_disk,
-                                       expected_status,
-                                       expected_skip,
-                                       None, None, None, None, None,
-                                       1, 1)
-  # run_and_verify_merge cannot check the properties on A_COPY/D/H/psi
-  # since that file is not on disk, so we'll check the file's mergeinfo
-  # directly with svn propget.
-  svntest.actions.run_and_verify_svn(
-    'Incorrect override mergeinfo set on skipped path',
-    ["/A/D/H/psi:3\n"], [], 'pg', 'svn:mergeinfo', psi_COPY_path)
-
-  # Now test another aspect of issue #3440, that a skipped path with
-  # explicit mergeinfo doesn't get it's mergeinfo updated.
-  #
-  # First revert all changes to the WC and then merge -r2:6 from A/D/H/psi
-  # to A_COPY/D/H/psi, creating explicit mergeinfo of '/A/D/H/psi:3-6' on
-  # the latter.  Commit this merge as r9 and then update the WC.
-  svntest.actions.run_and_verify_svn(None, None, [],
-                                     'revert', '-R', wc_dir)
-  svntest.actions.run_and_verify_svn(
-    None,
-    expected_merge_output([[3,6]],
-                          [' U   ' + psi_COPY_path + '\n',
-                           ' G   ' + psi_COPY_path + '\n']),
-    [], 'merge', '-r2:6', sbox.repo_url + '/A/D/H/psi', psi_COPY_path)
-  svntest.main.run_svn(None, 'commit', '-m',
-                       'subtree merge to create explicit mergeinfo',
-                       wc_dir)
-  svntest.actions.run_and_verify_svn(None, ["At revision 9.\n"], [],
-                                     'up', wc_dir)
-
-  # Remove A_COPY/D/H/psi again and then merge all available revisions
-  # from A to A_COPY.  The results should be mostly similar to the
-  # previous merge we did above, execept that A_COPY/D/H/psi should not
-  # have it's mergeinfo changed.
-  os.remove(psi_COPY_path)
-  expected_status.tweak(wc_rev=9)
-  expected_status.tweak('D/H/psi', status='! ')
-  expected_disk.tweak('', props={SVN_PROP_MERGEINFO : '/A:2-9'})
+                              'D/G/rho'   : Item(status='U ')})
   expected_mergeinfo_output = wc.State(A_COPY_path, {
-    ''        : Item(status=' U'),
-    'D/H/psi' : Item(status=' U'),
+    ''    : Item(status=' U'),
+    'D/H' : Item(status=' G'), # ' G' because override mergeinfo gets set
+                               # on this, the root of a 'missing' subtree.
     })
   expected_elision_output = wc.State(A_COPY_path, {
     })
@@ -15148,16 +15101,6 @@ def skipped_files_get_correct_mergeinfo(
                                        None, None, None, None, None,
                                        1, 1)
 
-  # run_and_verify_merge cannot check the properties on A_COPY/D/H/psi
-  # since that file is not on disk, so we'll check the file's mergeinfo
-  # directly with svn propget.  Issue #3440 also occurred here, when an
-  # the missing file's mergeinfo was updated, making it appear that r2
-  # and r7-9 were also merged into A_COPY/D/H/psi, which is clearly not
-  # the case since psi isn't present.
-  svntest.actions.run_and_verify_svn(
-    'Mergeinfo on skipped path altered',
-    ["/A/D/H/psi:3-6\n"], [], 'pg', 'svn:mergeinfo', psi_COPY_path)
-
 #----------------------------------------------------------------------
 # Test for issue #3115 'Case only renames resulting from merges don't
 # work or break the WC on case-insensitive file systems'.
@@ -15899,6 +15842,87 @@ def merge_into_locally_added_directory(s
                                        True, True, new_dir_path)
   sbox.simple_commit()
 
+#----------------------------------------------------------------------
+# Test for issue #2915 'Handle mergeinfo for subtrees missing due to removal
+# by non-svn command'
+def merge_with_os_deleted_subtrees(sbox):
+  "merge tracking fails if target missing subtrees"
+
+  # r1: Create a greek tree.
+  sbox.build()
+  wc_dir = sbox.wc_dir
+
+  # r2 - r6: Copy A to A_COPY and then make some text changes under A.
+  set_up_branch(sbox)
+
+  # Some paths we'll care about
+  A_COPY_path   = os.path.join(wc_dir, "A_COPY")
+  C_COPY_path   = os.path.join(wc_dir, "A_COPY", "C")
+  psi_COPY_path = os.path.join(wc_dir, "A_COPY", "D", "H", "psi")
+  mu_COPY_path  = os.path.join(wc_dir, "A_COPY", "mu")
+  G_COPY_path   = os.path.join(wc_dir, "A_COPY", "D", "G")
+
+  # Remove several subtrees from disk.
+  svntest.main.safe_rmtree(C_COPY_path)
+  svntest.main.safe_rmtree(G_COPY_path)
+  os.remove(psi_COPY_path)
+  os.remove(mu_COPY_path)
+
+  # Be sure the regex paths are properly escaped on Windows, see the
+  # note about "The Backslash Plague" in expected_merge_output().
+  if sys.platform == 'win32':
+    re_sep = '\\\\'
+  else:
+    re_sep = os.pathsep
+
+  # Common part of the expected error message for all cases we will test.
+  err_re = "svn: Merge tracking not allowed with missing subtrees; " + \
+           "try restoring these items first:"                        + \
+           "|(\n)"                                                   + \
+           "|(.*apr_err.*\n)" # In case of debug build
+
+  # Case 1: Infinite depth merge into infinite depth WC target.
+  # Every missing subtree under the target should be reported as missing.
+  missing = "|(.*A_COPY" + re_sep + "mu\n)"                                + \
+            "|(.*A_COPY" + re_sep + "D" + re_sep + "G\n)"                  + \
+            "|(.*A_COPY" + re_sep + "C\n)"                                 + \
+            "|(.*A_COPY" + re_sep + "D" + re_sep + "H" + re_sep + "psi\n)"
+  exit_code, out, err = svntest.actions.run_and_verify_svn(
+    "Missing subtrees should raise error", [], svntest.verify.AnyOutput,
+    'merge', sbox.repo_url + '/A', A_COPY_path)
+  svntest.verify.verify_outputs("Merge failed but not in the way expected",
+                                err, None, err_re + missing, None,
+                                True) # Match *all* lines of stderr
+
+  # Case 2: Immediates depth merge into infinite depth WC target.
+  # Only the two immediate children of the merge target should be reported
+  # as missing.
+  missing = "|(.*A_COPY" + re_sep + "mu\n)" + \
+            "|(.*A_COPY" + re_sep + "C\n)"
+  exit_code, out, err = svntest.actions.run_and_verify_svn(
+    "Missing subtrees should raise error", [], svntest.verify.AnyOutput,
+    'merge', sbox.repo_url + '/A', A_COPY_path, '--depth=immediates')
+  svntest.verify.verify_outputs("Merge failed but not in the way expected",
+                                err, None, err_re + missing, None, True)
+
+  # Case 3: Files depth merge into infinite depth WC target.
+  # Only the single file child of the merge target should be reported
+  # as missing.
+  missing = "|(.*A_COPY" + re_sep + "mu\n)"
+  exit_code, out, err = svntest.actions.run_and_verify_svn(
+    "Missing subtrees should raise error", [], svntest.verify.AnyOutput,
+    'merge', sbox.repo_url + '/A', A_COPY_path, '--depth=files')
+  svntest.verify.verify_outputs("Merge failed but not in the way expected",
+                                err, None, err_re + missing, None, True)
+
+  # Case 4: Empty depth merge into infinite depth WC target.
+  # Only the...oh, wait, the target is present and that is as deep
+  # as the merge goes, so this merge should succeed!
+  svntest.actions.run_and_verify_svn(
+    "Depth empty merge should succeed as long at the target is present",
+    svntest.verify.AnyOutput, [], 'merge', sbox.repo_url + '/A',
+    A_COPY_path, '--depth=empty')
+
 ########################################################################
 # Run the tests
 
@@ -16087,6 +16111,7 @@ test_list = [ None,
               copy_causes_phantom_eol_conflict,
               merge_into_locally_added_file,
               merge_into_locally_added_directory,
+              merge_with_os_deleted_subtrees,
              ]
 
 if __name__ == '__main__':

Modified: subversion/trunk/subversion/tests/cmdline/merge_tree_conflict_tests.py
URL: 
http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/merge_tree_conflict_tests.py?rev=992042&r1=992041&r2=992042&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/merge_tree_conflict_tests.py 
(original)
+++ subversion/trunk/subversion/tests/cmdline/merge_tree_conflict_tests.py Thu 
Sep  2 18:10:01 2010
@@ -1272,16 +1272,28 @@ def tree_conflicts_merge_edit_onto_missi
 
   # local tree missing (via shell delete), incoming leaf edit
 
+  # Note: In 1.7 merge tracking aware merges raise an error if the
+  # merge target has subtrees missing due to a shell delete.  To
+  # preserve the original intent of this test we'll run the merge
+  # with the --ignore-ancestry option, which neither considers nor
+  # records mergeinfo.  With this option the merge should "succeed"
+  # while skipping the missing paths.  Of course with no mergeinfo
+  # recorded and everything skipped, there is nothing to commit, so
+  # unlike most of the tree conflict tests we don't bother with the
+  # final commit step.
+
   sbox.build()
   expected_output = wc.State('', {
   })
 
   expected_disk = disk_after_tree_del
 
+  # Don't expect any mergeinfo property changes because we run
+  # the merge with the --ignore-ancestry option.
   expected_status = svntest.wc.State('', {
-    ''                  : Item(status=' M', wc_rev=3),
+    ''                  : Item(status='  ', wc_rev=3),
     'F'                 : Item(status='  ', wc_rev=3),
-    'F/alpha'           : Item(status='!M', wc_rev=3),
+    'F/alpha'           : Item(status='! ', wc_rev=3),
     'D'                 : Item(status='  ', wc_rev=3),
     'D/D1'              : Item(status='! ', wc_rev='?'),
     'DF'                : Item(status='  ', wc_rev=3),
@@ -1327,18 +1339,7 @@ def tree_conflicts_merge_edit_onto_missi
                expected_disk,
                expected_status,
                expected_skip,
-
-               ### This should not be happening!
-               ### The commit succeeds (it only commits mergeinfo).
-               ### But then the work queue freaks out while trying to install
-               ### F/alpha into the WC, because F/alpha is missing from disk.
-               ### We end up with a working copy that cannot be cleaned up.
-               ### To make this test pass for now we'll expect this error.
-               ### When the problem is fixed this test will start to fail
-               ### and should be adjusted.
-               commit_block_string=".*Error bumping revisions post-commit",
-
-             ) ], False)
+             ) ], False, do_commit_conflicts=False, ignore_ancestry=True)
 
 #----------------------------------------------------------------------
 def tree_conflicts_merge_del_onto_missing(sbox):
@@ -1346,16 +1347,28 @@ def tree_conflicts_merge_del_onto_missin
 
   # local tree missing (via shell delete), incoming leaf edit
 
+  # Note: In 1.7 merge tracking aware merges raise an error if the
+  # merge target has subtrees missing due to a shell delete.  To
+  # preserve the original intent of this test we'll run the merge
+  # with the --ignore-ancestry option, which neither considers nor
+  # records mergeinfo.  With this option the merge should "succeed"
+  # while skipping the missing paths.  Of course with no mergeinfo
+  # recorded and everything skipped, there is nothing to commit, so
+  # unlike most of the tree conflict tests we don't bother with the
+  # final commit step.
+
   sbox.build()
   expected_output = wc.State('', {
   })
 
   expected_disk = disk_after_tree_del
 
+  # Don't expect any mergeinfo property changes because we run
+  # the merge with the --ignore-ancestry option.
   expected_status = svntest.wc.State('', {
-    ''                  : Item(status=' M', wc_rev=3),
+    ''                  : Item(status='  ', wc_rev=3),
     'F'                 : Item(status='  ', wc_rev=3),
-    'F/alpha'           : Item(status='!M', wc_rev=3),
+    'F/alpha'           : Item(status='! ', wc_rev=3),
     'D'                 : Item(status='  ', wc_rev=3),
     'D/D1'              : Item(status='! ', wc_rev='?'),
     'DF'                : Item(status='  ', wc_rev=3),
@@ -1401,18 +1414,7 @@ def tree_conflicts_merge_del_onto_missin
                expected_disk,
                expected_status,
                expected_skip,
-
-               ### This should not be happening!
-               ### The commit succeeds (it only commits mergeinfo).
-               ### But then the work queue freaks out while trying to install
-               ### F/alpha into the WC, because F/alpha is missing from disk.
-               ### We end up with a working copy that cannot be cleaned up.
-               ### To make this test pass for now we'll expect this error.
-               ### When the problem is fixed this test will start to fail
-               ### and should be adjusted.
-               commit_block_string=".*Error bumping revisions post-commit",
-
-             ) ], False)
+             ) ], False, do_commit_conflicts=False, ignore_ancestry=True)
 
 #----------------------------------------------------------------------
 def merge_replace_setup(sbox):

Modified: subversion/trunk/subversion/tests/cmdline/svntest/actions.py
URL: 
http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/svntest/actions.py?rev=992042&r1=992041&r2=992042&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/svntest/actions.py (original)
+++ subversion/trunk/subversion/tests/cmdline/svntest/actions.py Thu Sep  2 
18:10:01 2010
@@ -2680,7 +2680,9 @@ def deep_trees_run_tests_scheme_for_swit
 
 
 def deep_trees_run_tests_scheme_for_merge(sbox, greater_scheme,
-                                          do_commit_local_changes):
+                                          do_commit_local_changes,
+                                          do_commit_conflicts=True,
+                                          ignore_ancestry=False):
   """
   Runs a given list of tests for conflicts occuring at a merge operation.
 
@@ -2714,12 +2716,14 @@ def deep_trees_run_tests_scheme_for_merg
       Then, in effect, the local changes are committed as well.
 
    8) In each test case subdir, the "incoming" subdir is merged into the
-      "local" subdir.
-      This causes conflicts between the "local" state in the working
-      copy and the "incoming" state from the incoming subdir.
-
-   9) A commit is performed in each separate container, to verify
-      that each tree-conflict indeed blocks a commit.
+      "local" subdir.  If ignore_ancestry is True, then the merge is done
+      with the --ignore-ancestry option, so mergeinfo is neither considered
+      nor recorded.  This causes conflicts between the "local" state in the
+      working copy and the "incoming" state from the incoming subdir.
+
+   9) If do_commit_conflicts is True, then a commit is performed in each
+      separate container, to verify that each tree-conflict indeed blocks
+      a commit.
 
   The sbox parameter is just the sbox passed to a test function. No need
   to call sbox.build(), since it is called (once) within this function.
@@ -2851,10 +2855,15 @@ def deep_trees_run_tests_scheme_for_merg
         x_skip.copy()
         x_skip.wc_dir = local
 
+      varargs = (local,)
+      if ignore_ancestry:
+        varargs = varargs + ('--ignore-ancestry',)
+
       run_and_verify_merge(local, None, None, incoming, None,
                            x_out, None, None, x_disk, None, x_skip,
-                           error_re_string = test_case.error_re_string,
-                           dry_run = False)
+                           test_case.error_re_string,
+                           None, None, None, None,
+                           False, False, *varargs)
       run_and_verify_unquiet_status(local, x_status)
     except:
       print("ERROR IN: Tests scheme for merge: "
@@ -2864,21 +2873,22 @@ def deep_trees_run_tests_scheme_for_merg
 
   # 9) Verify that commit fails.
 
-  for test_case in greater_scheme:
-    try:
-      local = j(wc_dir, test_case.name, 'local')
-
-      x_status = test_case.expected_status
-      if x_status != None:
-        x_status.copy()
-        x_status.wc_dir = local
-
-      run_and_verify_commit(local, None, x_status,
-                            test_case.commit_block_string,
-                            local)
-    except:
-      print("ERROR IN: Tests scheme for merge: "
-          + "while checking commit-blocking in '%s'" % test_case.name)
-      raise
+  if do_commit_conflicts:
+    for test_case in greater_scheme:
+      try:
+        local = j(wc_dir, test_case.name, 'local')
+
+        x_status = test_case.expected_status
+        if x_status != None:
+          x_status.copy()
+          x_status.wc_dir = local
+
+        run_and_verify_commit(local, None, x_status,
+                              test_case.commit_block_string,
+                              local)
+      except:
+        print("ERROR IN: Tests scheme for merge: "
+            + "while checking commit-blocking in '%s'" % test_case.name)
+        raise
 
 


Reply via email to