You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@subversion.apache.org by ju...@apache.org on 2013/03/20 20:33:59 UTC

svn commit: r1459012 - in /subversion/trunk/subversion: libsvn_client/merge.c svn/merge-cmd.c svn/notify.c tests/cmdline/merge_tests.py tests/cmdline/svntest/main.py

Author: julianfoad
Date: Wed Mar 20 19:33:59 2013
New Revision: 1459012

URL: http://svn.apache.org/r1459012
Log:
Fix issue #4316 "Merge errors out after resolving conflicts".

In the libsvn_client merge code, if conflicts are raised while merging one
of the revision ranges, then after that revision range call the conflict
resolution callback for each of those conflicts, and then continue with the
rest of the merge if all of those conflicts were resolved.

Notifications are changed.  Previously, for a text or property conflict,
merge called the conflict resolution callback before notifying, and avoided
notifying a 'conflict' status if the resolver resolved it.  Thus, a merge
with '--accept=mine-full' or a similar option would not notify any text or
prop conflicts.  Now it will notify with a 'C' (conflict) status for each
path on which a conflict is raised, and later on reaching the end of a
revision range it will call the resolver which may resolve them and send a
'Resolved conflicts on ...' notification for each path.

If any conflicts are resolved in this way before the merge completes, the
Summary of Conflicts will be extended to report both the number of remaining
conflicts and the number of resolved conflicts.  An example follows:

  $ svn merge ^/A wc/A2 --accept=mf
  --- Merging r2 through r4 into 'wc/A2':
   C   wc/A2/B
   C   wc/A2/mu
  --- Recording mergeinfo for merge of r2 through r4 into 'wc/A2':
   U   wc/A2
  Resolved conflicted state of 'wc/A2/B'
  Resolved conflicted state of 'wc/A2/mu'
  --- Merging r10 through r11 into 'wc/A2':
   U   wc/A2/B
   U   wc/A2/mu
  --- Recording mergeinfo for merge of r10 through r11 into 'wc/A2':
   G   wc/A2
  Summary of conflicts:
    Property conflicts: 0 remaining (and 2 already resolved)


* subversion/libsvn_client/merge.c
  (merge_file_changed,
   merge_dir_changed,
   merge_dir_added): If there is a conflict, don't call the resolver, just
    record the conflict.
  (single_range_conflict_report_t,
   single_range_conflict_report_create): New conflict reporting structure,
    containing also the remaining range to be merged.
  (conflict_report_t,
   conflict_report_create,
   conflict_report_dup,
   make_merge_conflict_error): Store a merge_source_t instead of just a
    revision range.
  (subrange_source): Add an assertion.
  (do_file_merge,
   do_mergeinfo_unaware_dir_merge,
   do_mergeinfo_aware_dir_merge,
   do_directory_merge): Use the new kind of conflict report structure.
  (resolve_conflicts): New function.
  (do_merge): After merging a revision range, do conflict resolution and
    continue with the rest of the merge until it is all done.

* subversion/svn/merge-cmd.c
  (svn_cl__merge): Always install the requested conflict resolver before
    doing the merge, and never run svn_cl__resolve_postponed_conflicts()
    after the merge.

* subversion/svn/notify.c
  (notify_baton): Add fields to track which paths have been notified as in
    conflict and how many conflicts have been notified as resolved.
  (store_path): New function.
  (svn_cl__notifier_reset_conflict_stats,
   svn_cl__get_notifier): Adjust accordingly.
  (svn_cl__notifier_print_conflict_stats): If any conflicts are logged as
    resolved, extend the relevant summary line to say so.
  (notify): On receiving a 'conflict' notification, keep track of the path.
    On receiving a 'resolved' notification, remove the resolved path from
    the hashes of text-, prop- and/or tree-conflicted paths.

* subversion/tests/cmdline/merge_tests.py
  (expected_merge_output,
   svn_merge,
   expected_merge_output2,
   expected_out_and_err): Add support for the new notification format.
  (merge_two_edits_to_same_prop,
   merge_automatic_conflict_resolution): Tweak expectations.
  (conflicted_split_merge_with_resolve): New test.
  (test_list): Add the new test.
 
* subversion/tests/cmdline/svntest/main.py
  (summary_of_conflicts): Add support for the new notification format.

Modified:
    subversion/trunk/subversion/libsvn_client/merge.c
    subversion/trunk/subversion/svn/merge-cmd.c
    subversion/trunk/subversion/svn/notify.c
    subversion/trunk/subversion/tests/cmdline/merge_tests.py
    subversion/trunk/subversion/tests/cmdline/svntest/main.py

Modified: subversion/trunk/subversion/libsvn_client/merge.c
URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/libsvn_client/merge.c?rev=1459012&r1=1459011&r2=1459012&view=diff
==============================================================================
--- subversion/trunk/subversion/libsvn_client/merge.c (original)
+++ subversion/trunk/subversion/libsvn_client/merge.c Wed Mar 20 19:33:59 2013
@@ -2002,9 +2002,14 @@ merge_file_changed(const char *relpath,
                                   left, right,
                                   left_props, prop_changes,
                                   merge_b->dry_run,
-                                  ctx->conflict_func2, ctx->conflict_baton2,
+                                  NULL, NULL,
                                   ctx->cancel_func, ctx->cancel_baton,
                                   scratch_pool));
+      if (property_state == svn_wc_notify_state_conflicted)
+        {
+          alloc_and_store_path(&merge_b->conflicted_paths, local_abspath,
+                               merge_b->pool);
+        }
     }
 
   /* Easy out: We are only applying mergeinfo differences. */
@@ -2040,7 +2045,7 @@ merge_file_changed(const char *relpath,
                             merge_b->dry_run, merge_b->diff3_cmd,
                             merge_b->merge_options,
                             left_props, prop_changes,
-                            ctx->conflict_func2, ctx->conflict_baton2,
+                            NULL, NULL,
                             ctx->cancel_func,
                             ctx->cancel_baton,
                             scratch_pool));
@@ -2842,7 +2847,7 @@ merge_dir_changed(const char *relpath,
                                   left, right,
                                   left_props, props,
                                   merge_b->dry_run,
-                                  ctx->conflict_func2, ctx->conflict_baton2,
+                                  NULL, NULL,
                                   ctx->cancel_func, ctx->cancel_baton,
                                   scratch_pool));
 
@@ -2994,11 +2999,15 @@ merge_dir_added(const char *relpath,
                                   svn_prop_hash_to_array(new_props,
                                                          scratch_pool),
                                   merge_b->dry_run,
-                                  merge_b->ctx->conflict_func2,
-                                  merge_b->ctx->conflict_baton2,
+                                  NULL, NULL,
                                   merge_b->ctx->cancel_func,
                                   merge_b->ctx->cancel_baton,
                                   scratch_pool));
+      if (prop_state == svn_wc_notify_state_conflicted)
+        {
+          alloc_and_store_path(&merge_b->conflicted_paths, local_abspath,
+                               merge_b->pool);
+        }
     }
 
   return SVN_NO_ERROR;
@@ -5319,6 +5328,38 @@ record_skips_in_mergeinfo(const char *me
 }
 
 /* Data for reporting when a merge aborted because of raising conflicts.
+ */
+typedef struct single_range_conflict_report_t
+{
+  /* What sub-range of the requested source raised conflicts?
+   * The 'inheritable' flag is ignored. */
+  merge_source_t *conflicted_range;
+  /* What sub-range of the requested source remains to be merged?
+   * NULL if no more.  The 'inheritable' flag is ignored. */
+  merge_source_t *remaining_source;
+
+} single_range_conflict_report_t;
+
+/* Create a single_range_conflict_report_t, containing deep copies of
+ * CONFLICTED_RANGE and REMAINING_SOURCE, allocated in RESULT_POOL. */
+static single_range_conflict_report_t *
+single_range_conflict_report_create(const merge_source_t *conflicted_range,
+                                    const merge_source_t *remaining_source,
+                                    apr_pool_t *result_pool)
+{
+  single_range_conflict_report_t *report
+    = apr_palloc(result_pool, sizeof(*report));
+
+  assert(conflicted_range != NULL);
+
+  report->conflicted_range = merge_source_dup(conflicted_range, result_pool);
+  report->remaining_source
+    = remaining_source ? merge_source_dup(remaining_source, result_pool)
+                       : NULL;
+  return report;
+}
+
+/* Data for reporting when a merge aborted because of raising conflicts.
  *
  * ### TODO: More info, including the ranges (or other parameters) the user
  *     needs to complete the merge.
@@ -5327,7 +5368,8 @@ typedef struct conflict_report_t
 {
   const char *target_abspath;
   /* The revision range during which conflicts were raised */
-  svn_merge_range_t *r;
+  const merge_source_t *conflicted_range;
+  /* Was the conflicted range the last range in the whole requested merge? */
   svn_boolean_t was_last_range;
 } conflict_report_t;
 
@@ -5335,14 +5377,14 @@ typedef struct conflict_report_t
  * allocated in RESULT_POOL. */
 static conflict_report_t *
 conflict_report_create(const char *target_abspath,
-                       const svn_merge_range_t *conflicted_range,
+                       const merge_source_t *conflicted_range,
                        svn_boolean_t was_last_range,
                        apr_pool_t *result_pool)
 {
   conflict_report_t *report = apr_palloc(result_pool, sizeof(*report));
 
   report->target_abspath = apr_pstrdup(result_pool, target_abspath);
-  report->r = svn_merge_range_dup(conflicted_range, result_pool);
+  report->conflicted_range = merge_source_dup(conflicted_range, result_pool);
   report->was_last_range = was_last_range;
   return report;
 }
@@ -5355,7 +5397,8 @@ conflict_report_dup(const conflict_repor
   conflict_report_t *new = apr_pmemdup(result_pool, report, sizeof(*new));
 
   new->target_abspath = apr_pstrdup(result_pool, report->target_abspath);
-  new->r = svn_merge_range_dup(report->r, result_pool);
+  new->conflicted_range = merge_source_dup(report->conflicted_range,
+                                           result_pool);
   return new;
 }
 
@@ -5368,13 +5411,17 @@ make_merge_conflict_error(conflict_repor
   assert(!report || svn_dirent_is_absolute(report->target_abspath));
 
   if (report && ! report->was_last_range)
-    return svn_error_createf(SVN_ERR_WC_FOUND_CONFLICT, NULL,
+    {
+      svn_error_t *err = svn_error_createf(SVN_ERR_WC_FOUND_CONFLICT, NULL,
        _("One or more conflicts were produced while merging r%ld:%ld into\n"
          "'%s' --\n"
          "resolve all conflicts and rerun the merge to apply the remaining\n"
          "unmerged revisions"),
-       report->r->start, report->r->end,
+       report->conflicted_range->loc1->rev, report->conflicted_range->loc2->rev,
        svn_dirent_local_style(report->target_abspath, scratch_pool));
+      assert(report->conflicted_range->loc1->rev != report->conflicted_range->loc2->rev); /* ### is a valid case in a 2-URL merge */
+      return err;
+    }
   return SVN_NO_ERROR;
 }
 
@@ -7237,6 +7284,7 @@ subrange_source(const merge_source_t *so
 
   /* For this function we require that the input source is 'ancestral'. */
   SVN_ERR_ASSERT_NO_RETURN(source->ancestral);
+  SVN_ERR_ASSERT_NO_RETURN(start_rev != end_rev);
 
   loc1.rev = start_rev;
   loc2.rev = end_rev;
@@ -7277,7 +7325,7 @@ subrange_source(const merge_source_t *so
 */
 static svn_error_t *
 do_file_merge(svn_mergeinfo_catalog_t result_catalog,
-              svn_merge_range_t **conflicted_range,
+              single_range_conflict_report_t **conflict_report,
               const merge_source_t *source,
               const char *target_abspath,
               const svn_diff_tree_processor_t *processor,
@@ -7301,7 +7349,7 @@ do_file_merge(svn_mergeinfo_catalog_t re
 
   SVN_ERR_ASSERT(svn_dirent_is_absolute(target_abspath));
 
-  *conflicted_range = NULL;
+  *conflict_report = NULL;
 
   /* Note that this is a single-file merge. */
   range.start = source->loc1->rev;
@@ -7545,7 +7593,16 @@ do_file_merge(svn_mergeinfo_catalog_t re
 
           if (is_path_conflicted_by_merge(merge_b))
             {
-              *conflicted_range = svn_merge_range_dup(r, result_pool);
+              merge_source_t *remaining_range = NULL;
+
+              if (real_source->loc2->rev != source->loc2->rev)
+                remaining_range = subrange_source(source,
+                                                  real_source->loc2->rev,
+                                                  source->loc2->rev,
+                                                  scratch_pool);
+              *conflict_report = single_range_conflict_report_create(
+                                   real_source, remaining_range, result_pool);
+
               /* Only record partial mergeinfo if only a partial merge was
                  performed before a conflict was encountered. */
               range.end = r->end;
@@ -7802,14 +7859,14 @@ subtree_touched_by_merge(const char *loc
    SOURCE, DEPTH, NOTIFY_B, and MERGE_B
    are all cascaded from do_directory_merge's arguments of the same names.
 
-   CONFLICTED_RANGE is as documented for do_directory_merge().
+   CONFLICT_REPORT is as documented for do_directory_merge().
 
    NOTE: This is a very thin wrapper around drive_merge_report_editor() and
    exists only to populate CHILDREN_WITH_MERGEINFO with the single element
    expected during mergeinfo unaware merges.
 */
 static svn_error_t *
-do_mergeinfo_unaware_dir_merge(svn_merge_range_t **conflicted_range,
+do_mergeinfo_unaware_dir_merge(single_range_conflict_report_t **conflict_report,
                                const merge_source_t *source,
                                const char *target_dir_wcpath,
                                apr_array_header_t *children_with_mergeinfo,
@@ -7825,7 +7882,7 @@ do_mergeinfo_unaware_dir_merge(svn_merge
   svn_client__merge_path_t *item
     = svn_client__merge_path_create(target_dir_wcpath, scratch_pool);
 
-  *conflicted_range = NULL;
+  *conflict_report = NULL;
   item->remaining_ranges = svn_rangelist__initialize(source->loc1->rev,
                                                      source->loc2->rev,
                                                      TRUE, scratch_pool);
@@ -7837,11 +7894,8 @@ do_mergeinfo_unaware_dir_merge(svn_merge
                                     merge_b, scratch_pool));
   if (is_path_conflicted_by_merge(merge_b))
     {
-      svn_merge_range_t *r = apr_palloc(result_pool, sizeof(*r));
-      r->start = source->loc1->rev;
-      r->end = source->loc2->rev;
-      r->inheritable = TRUE;
-      *conflicted_range = r;
+      *conflict_report = single_range_conflict_report_create(
+                           source, NULL, result_pool);
     }
   return SVN_NO_ERROR;
 }
@@ -9088,7 +9142,7 @@ remove_noop_subtree_ranges(const merge_s
 
    Handle DEPTH as documented for svn_client_merge5().
 
-   CONFLICTED_RANGE is as documented for do_directory_merge().
+   CONFLICT_REPORT is as documented for do_directory_merge().
 
    Perform any temporary allocations in SCRATCH_POOL.
 
@@ -9099,7 +9153,7 @@ remove_noop_subtree_ranges(const merge_s
 */
 static svn_error_t *
 do_mergeinfo_aware_dir_merge(svn_mergeinfo_catalog_t result_catalog,
-                             svn_merge_range_t **conflicted_range,
+                             single_range_conflict_report_t **conflict_report,
                              const merge_source_t *source,
                              const char *target_abspath,
                              apr_array_header_t *children_with_mergeinfo,
@@ -9131,7 +9185,7 @@ do_mergeinfo_aware_dir_merge(svn_mergein
        same repository as the target -- merge tracking might be
        happenin'! ***/
 
-  *conflicted_range = NULL;
+  *conflict_report = NULL;
 
   /* Point our RA_SESSION to the URL of our youngest merge source side. */
   ra_session = is_rollback ? merge_b->ra_session1 : merge_b->ra_session2;
@@ -9320,11 +9374,17 @@ do_mergeinfo_aware_dir_merge(svn_mergein
                  we have merged. */
               if (is_path_conflicted_by_merge(merge_b))
                 {
-                  svn_merge_range_t *r = apr_palloc(result_pool, sizeof(*r));
-                  r->start = start_rev;
-                  r->end = end_rev;
-                  r->inheritable = TRUE;
-                  *conflicted_range = r;
+                  merge_source_t *remaining_range = NULL;
+
+                  if (real_source->loc2->rev != source->loc2->rev)
+                    remaining_range = subrange_source(source,
+                                                      real_source->loc2->rev,
+                                                      source->loc2->rev,
+                                                      scratch_pool);
+                  *conflict_report = single_range_conflict_report_create(
+                                       real_source, remaining_range,
+                                       result_pool);
+
                   range.end = end_rev;
                   break;
                 }
@@ -9412,7 +9472,7 @@ do_mergeinfo_aware_dir_merge(svn_mergein
  */
 static svn_error_t *
 do_directory_merge(svn_mergeinfo_catalog_t result_catalog,
-                   svn_merge_range_t **conflicted_range,
+                   single_range_conflict_report_t **conflict_report,
                    const merge_source_t *source,
                    const char *target_abspath,
                    const svn_diff_tree_processor_t *processor,
@@ -9435,14 +9495,14 @@ do_directory_merge(svn_mergeinfo_catalog
   /* If we are not honoring mergeinfo we can skip right to the
      business of merging changes! */
   if (HONOR_MERGEINFO(merge_b))
-    SVN_ERR(do_mergeinfo_aware_dir_merge(result_catalog, conflicted_range,
+    SVN_ERR(do_mergeinfo_aware_dir_merge(result_catalog, conflict_report,
                                          source, target_abspath,
                                          children_with_mergeinfo,
                                          processor, depth,
                                          squelch_mergeinfo_notifications,
                                          merge_b, result_pool, scratch_pool));
   else
-    SVN_ERR(do_mergeinfo_unaware_dir_merge(conflicted_range,
+    SVN_ERR(do_mergeinfo_unaware_dir_merge(conflict_report,
                                            source, target_abspath,
                                            children_with_mergeinfo,
                                            processor, depth,
@@ -9487,6 +9547,52 @@ ensure_ra_session_url(svn_ra_session_t *
   return SVN_NO_ERROR;
 }
 
+/* Call the conflict resolver callback in CTX for each conflict recorded
+ * in MERGE_B.  Set *RESOLVED to true if all of the conflicts were
+ * resolved, else to false, */
+static svn_error_t *
+resolve_conflicts(svn_boolean_t *resolved,
+                  merge_cmd_baton_t *merge_b,
+                  svn_client_ctx_t *ctx,
+                  apr_pool_t *scratch_pool)
+{
+  apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+  apr_hash_index_t *hi;
+
+  SVN_DBG(("resolving any conflicts: %lx", (long)merge_b->conflicted_paths));
+  *resolved = TRUE;
+  for (hi = (merge_b->conflicted_paths
+             ? apr_hash_first(scratch_pool, merge_b->conflicted_paths) : NULL);
+       hi; hi = apr_hash_next(hi))
+    {
+      const char *local_abspath = svn__apr_hash_index_key(hi);
+      svn_boolean_t text_c, prop_c, tree_c;
+
+      SVN_DBG(("resolving conflicts on path '%s'", local_abspath));
+      svn_pool_clear(iterpool);
+      SVN_ERR(svn_wc__resolve_conflicts(ctx->wc_ctx, local_abspath,
+                                        svn_depth_empty,
+                                        TRUE /* resolve_text */,
+                                        "" /* resolve_prop (ALL props) */,
+                                        TRUE /* resolve_tree */,
+                                        svn_wc_conflict_choose_unspecified,
+                                        ctx->conflict_func2,
+                                        ctx->conflict_baton2,
+                                        ctx->cancel_func, ctx->cancel_baton,
+                                        ctx->notify_func2, ctx->notify_baton2,
+                                        iterpool));
+
+      SVN_ERR(svn_wc_conflicted_p3(&text_c, &prop_c, &tree_c,
+                                   ctx->wc_ctx, local_abspath,
+                                   iterpool));
+      if (text_c || prop_c || tree_c)
+        *resolved = FALSE;
+    }
+  svn_pool_destroy(iterpool);
+
+  return SVN_NO_ERROR;
+}
+
 /* Drive a merge of MERGE_SOURCES into working copy node TARGET
    and possibly record mergeinfo describing the merge -- see
    RECORD_MERGEINFO().
@@ -9708,7 +9814,7 @@ do_merge(apr_hash_t **modified_subtrees,
       svn_node_kind_t src1_kind;
       merge_source_t *source =
         APR_ARRAY_IDX(merge_sources, i, merge_source_t *);
-      svn_merge_range_t *conflicted_range;
+      single_range_conflict_report_t *conflicted_range_report;
 
       svn_pool_clear(iterpool);
 
@@ -9750,24 +9856,53 @@ do_merge(apr_hash_t **modified_subtrees,
       SVN_ERR(svn_ra_check_path(ra_session1, "", source->loc1->rev,
                                 &src1_kind, iterpool));
 
-      /* Call our merge helpers based on SRC1's kind. */
-      if (src1_kind != svn_node_dir)
-        {
-          SVN_ERR(do_file_merge(result_catalog, &conflicted_range,
-                                source, target->abspath,
-                                processor,
-                                sources_related,
-                                squelch_mergeinfo_notifications,
-                                &merge_cmd_baton, iterpool, iterpool));
-        }
-      else /* Directory */
-        {
-          SVN_ERR(do_directory_merge(result_catalog, &conflicted_range,
-                                     source, target->abspath,
-                                     processor,
-                                     depth, squelch_mergeinfo_notifications,
-                                     &merge_cmd_baton, iterpool, iterpool));
+      /* Run the merge; if there are conflicts, allow the callback to
+       * resolve them, and if it resolves all of them, then run the
+       * merge again with the remaining revision range, until it is all
+       * done. */
+      do
+        {
+          /* Merge as far as possible without resolving any conflicts */
+          if (src1_kind != svn_node_dir)
+            {
+              SVN_ERR(do_file_merge(result_catalog, &conflicted_range_report,
+                                    source, target->abspath,
+                                    processor,
+                                    sources_related,
+                                    squelch_mergeinfo_notifications,
+                                    &merge_cmd_baton, iterpool, iterpool));
+            }
+          else /* Directory */
+            {
+              SVN_ERR(do_directory_merge(result_catalog, &conflicted_range_report,
+                                         source, target->abspath,
+                                         processor,
+                                         depth, squelch_mergeinfo_notifications,
+                                         &merge_cmd_baton, iterpool, iterpool));
+            }
+
+          /* Give the conflict resolver callback the opportunity to
+           * resolve any conflicts that were raised.  If it resolves all
+           * of them, go around again to merge the next sub-range (if any). */
+          if (conflicted_range_report && ctx->conflict_func2 && ! dry_run)
+            {
+              svn_boolean_t resolved_all;
+
+              SVN_ERR(resolve_conflicts(&resolved_all,
+                                        &merge_cmd_baton, ctx, iterpool));
+              if (! resolved_all)
+                break;
+
+              merge_cmd_baton.conflicted_paths = NULL;
+              /* Caution: this source is in iterpool */
+              source = conflicted_range_report->remaining_source;
+              conflicted_range_report = NULL;
+            }
+          else
+            break;
         }
+      while (source);
+
       /* The final mergeinfo on TARGET_WCPATH may itself elide. */
       if (! dry_run)
         SVN_ERR(svn_client__elide_mergeinfo(target->abspath, NULL,
@@ -9777,12 +9912,12 @@ do_merge(apr_hash_t **modified_subtrees,
        * range of a multi-pass merge, we raise an error that aborts
        * the merge. The user will be asked to resolve conflicts
        * before merging subsequent revision ranges. */
-      if (conflicted_range)
+      if (conflicted_range_report)
         {
           *conflict_report = conflict_report_create(
-                               target->abspath, conflicted_range,
+                               target->abspath, conflicted_range_report->conflicted_range,
                                (i == merge_sources->nelts - 1
-                                && conflicted_range->end == source->loc2->rev),
+                                && ! conflicted_range_report->remaining_source),
                                result_pool);
           break;
         }

Modified: subversion/trunk/subversion/svn/merge-cmd.c
URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/svn/merge-cmd.c?rev=1459012&r1=1459011&r2=1459012&view=diff
==============================================================================
--- subversion/trunk/subversion/svn/merge-cmd.c (original)
+++ subversion/trunk/subversion/svn/merge-cmd.c Wed Mar 20 19:33:59 2013
@@ -542,29 +542,18 @@ svn_cl__merge(apr_getopt_t *os,
                                   "with --reintegrate"));
     }
 
-  /* Decide how to handle conflicts.  If the user wants interactive
-   * conflict resolution, postpone conflict resolution during the merge
-   * and if any conflicts occur we'll run the conflict resolver later.
-   * Otherwise install the appropriate resolver now. */
-  if (opt_state->accept_which == svn_cl__accept_unspecified
-      || opt_state->accept_which == svn_cl__accept_postpone
-      || opt_state->accept_which == svn_cl__accept_edit
-      || opt_state->accept_which == svn_cl__accept_launch)
-    {
-      /* 'svn.c' has already installed the 'postpone' handler for us. */
-    }
-  else
-    {
-      svn_cl__interactive_conflict_baton_t *b;
-
-      ctx->conflict_func2 = svn_cl__conflict_func_interactive;
-      SVN_ERR(svn_cl__get_conflict_func_interactive_baton(
-                &b,
-                opt_state->accept_which,
-                ctx->config, opt_state->editor_cmd,
-                ctx->cancel_func, ctx->cancel_baton, pool));
-      ctx->conflict_baton2 = b;
-    }
+  /* Install the conflict resolver. */
+  {
+    svn_cl__interactive_conflict_baton_t *b;
+
+    ctx->conflict_func2 = svn_cl__conflict_func_interactive;
+    SVN_ERR(svn_cl__get_conflict_func_interactive_baton(
+              &b,
+              opt_state->accept_which,
+              ctx->config, opt_state->editor_cmd,
+              ctx->cancel_func, ctx->cancel_baton, pool));
+    ctx->conflict_baton2 = b;
+  }
 
   merge_err = run_merge(two_sources_specified,
                         sourcepath1, peg_revision1,
@@ -580,22 +569,12 @@ svn_cl__merge(apr_getopt_t *os,
                _("Merge tracking not possible, use --ignore-ancestry or\n"
                  "fix invalid mergeinfo in target with 'svn propset'"));
     }
-  if (! merge_err || merge_err->apr_err == SVN_ERR_WC_FOUND_CONFLICT)
+
+  if (!opt_state->quiet)
     {
-      svn_error_t *err = SVN_NO_ERROR;
+      svn_error_t *err = svn_cl__notifier_print_conflict_stats(
+                           ctx->notify_baton2, pool);
 
-      if (! opt_state->quiet)
-        err = svn_cl__notifier_print_conflict_stats(ctx->notify_baton2,
-                                                        pool);
-
-      /* Resolve any postponed conflicts.  (Only if we've been using the
-       * default 'postpone' resolver which remembers what was postponed.) */
-      if (!err && ctx->conflict_func2 == svn_cl__conflict_func_postpone)
-        err = svn_cl__resolve_postponed_conflicts(NULL,
-                                                  ctx->conflict_baton2,
-                                                  opt_state->accept_which,
-                                                  opt_state->editor_cmd,
-                                                  ctx, pool);
       merge_err = svn_error_compose_create(merge_err, err);
     }
 

Modified: subversion/trunk/subversion/svn/notify.c
URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/svn/notify.c?rev=1459012&r1=1459011&r2=1459012&view=diff
==============================================================================
--- subversion/trunk/subversion/svn/notify.c (original)
+++ subversion/trunk/subversion/svn/notify.c Wed Mar 20 19:33:59 2013
@@ -36,6 +36,7 @@
 #include "svn_dirent_uri.h"
 #include "svn_path.h"
 #include "svn_sorts.h"
+#include "svn_hash.h"
 #include "cl.h"
 
 #include "svn_private_config.h"
@@ -54,9 +55,9 @@ struct notify_baton
                                     when we've already had one print error. */
 
   /* Conflict stats for update and merge. */
-  int text_conflicts;
-  int prop_conflicts;
-  int tree_conflicts;
+  apr_pool_t *stats_pool;
+  apr_hash_t *text_conflicts, *prop_conflicts, *tree_conflicts;
+  int text_conflicts_resolved, prop_conflicts_resolved, tree_conflicts_resolved;
   int skipped_paths;
 
   /* The cwd, for use in decomposing absolute paths. */
@@ -64,14 +65,24 @@ struct notify_baton
 };
 
 
+/* Add the PATH (as a key, with a meaningless value) into the HASH in NB. */
+static void
+store_path(struct notify_baton *nb, apr_hash_t *hash, const char *path)
+{
+  svn_hash_sets(hash, apr_pstrdup(nb->stats_pool, path), "");
+}
+
 svn_error_t *
 svn_cl__notifier_reset_conflict_stats(void *baton)
 {
   struct notify_baton *nb = baton;
 
-  nb->text_conflicts = 0;
-  nb->prop_conflicts = 0;
-  nb->tree_conflicts = 0;
+  apr_hash_clear(nb->text_conflicts);
+  apr_hash_clear(nb->prop_conflicts);
+  apr_hash_clear(nb->tree_conflicts);
+  nb->text_conflicts_resolved = 0;
+  nb->prop_conflicts_resolved = 0;
+  nb->tree_conflicts_resolved = 0;
   nb->skipped_paths = 0;
   return SVN_NO_ERROR;
 }
@@ -80,24 +91,53 @@ svn_error_t *
 svn_cl__notifier_print_conflict_stats(void *baton, apr_pool_t *scratch_pool)
 {
   struct notify_baton *nb = baton;
-
-  if (nb->text_conflicts > 0 || nb->prop_conflicts > 0
-      || nb->tree_conflicts > 0 || nb->skipped_paths > 0)
+  int n_text = apr_hash_count(nb->text_conflicts);
+  int n_prop = apr_hash_count(nb->prop_conflicts);
+  int n_tree = apr_hash_count(nb->tree_conflicts);
+  int n_text_r = nb->text_conflicts_resolved;
+  int n_prop_r = nb->prop_conflicts_resolved;
+  int n_tree_r = nb->tree_conflicts_resolved;
+
+  if (n_text > 0 || n_text_r > 0
+      || n_prop > 0 || n_prop_r > 0
+      || n_tree > 0 || n_tree_r > 0
+      || nb->skipped_paths > 0)
     SVN_ERR(svn_cmdline_printf(scratch_pool,
                                _("Summary of conflicts:\n")));
 
-  if (nb->text_conflicts > 0)
-    SVN_ERR(svn_cmdline_printf(scratch_pool,
-                               _("  Text conflicts: %d\n"),
-                               nb->text_conflicts));
-  if (nb->prop_conflicts > 0)
-    SVN_ERR(svn_cmdline_printf(scratch_pool,
-                               _("  Property conflicts: %d\n"),
-                               nb->prop_conflicts));
-  if (nb->tree_conflicts > 0)
-    SVN_ERR(svn_cmdline_printf(scratch_pool,
-                               _("  Tree conflicts: %d\n"),
-                               nb->tree_conflicts));
+  if (n_text > 0 || n_text_r > 0)
+    {
+      if (n_text_r == 0)
+        SVN_ERR(svn_cmdline_printf(scratch_pool,
+          _("  Text conflicts: %d\n"),
+          n_text));
+      else
+        SVN_ERR(svn_cmdline_printf(scratch_pool,
+          _("  Text conflicts: %d remaining (and %d already resolved)\n"),
+          n_text, n_text_r));
+    }
+  if (n_prop > 0 || n_prop_r > 0)
+    {
+      if (n_prop_r == 0)
+        SVN_ERR(svn_cmdline_printf(scratch_pool,
+          _("  Property conflicts: %d\n"),
+          n_prop));
+      else
+        SVN_ERR(svn_cmdline_printf(scratch_pool,
+          _("  Property conflicts: %d remaining (and %d already resolved)\n"),
+          n_prop, n_prop_r));
+    }
+  if (n_tree > 0 || n_tree_r > 0)
+    {
+      if (n_tree_r == 0)
+        SVN_ERR(svn_cmdline_printf(scratch_pool,
+          _("  Tree conflicts: %d\n"),
+          n_tree));
+      else
+        SVN_ERR(svn_cmdline_printf(scratch_pool,
+          _("  Tree conflicts: %d remaining (and %d already resolved)\n"),
+          n_tree, n_tree_r));
+    }
   if (nb->skipped_paths > 0)
     SVN_ERR(svn_cmdline_printf(scratch_pool,
                                _("  Skipped paths: %d\n"),
@@ -224,7 +264,7 @@ notify(void *baton, const svn_wc_notify_
       nb->received_some_change = TRUE;
       if (n->content_state == svn_wc_notify_state_conflicted)
         {
-          nb->text_conflicts++;
+          store_path(nb, nb->text_conflicts, path_local);
           if ((err = svn_cmdline_printf(pool, "C    %s\n", path_local)))
             goto print_error;
         }
@@ -239,7 +279,7 @@ notify(void *baton, const svn_wc_notify_
       nb->received_some_change = TRUE;
       if (n->content_state == svn_wc_notify_state_conflicted)
         {
-          nb->text_conflicts++;
+          store_path(nb, nb->text_conflicts, path_local);
           statchar_buf[0] = 'C';
         }
       else
@@ -247,7 +287,7 @@ notify(void *baton, const svn_wc_notify_
 
       if (n->prop_state == svn_wc_notify_state_conflicted)
         {
-          nb->prop_conflicts++;
+          store_path(nb, nb->prop_conflicts, path_local);
           statchar_buf[1] = 'C';
         }
       else if (n->prop_state == svn_wc_notify_state_merged)
@@ -277,6 +317,23 @@ notify(void *baton, const svn_wc_notify_
       break;
 
     case svn_wc_notify_resolved:
+      /* All conflicts on this path are resolved, so remove path from
+         each of the text-, prop- and tree-conflict lists. */
+      if (svn_hash_gets(nb->text_conflicts, path_local))
+        {
+          svn_hash_sets(nb->text_conflicts, path_local, NULL);
+          nb->text_conflicts_resolved++;
+        }
+      if (svn_hash_gets(nb->prop_conflicts, path_local))
+        {
+          svn_hash_sets(nb->prop_conflicts, path_local, NULL);
+          nb->prop_conflicts_resolved++;
+        }
+      if (svn_hash_gets(nb->tree_conflicts, path_local))
+        {
+          svn_hash_sets(nb->tree_conflicts, path_local, NULL);
+          nb->tree_conflicts_resolved++;
+        }
       if ((err = svn_cmdline_printf(pool,
                                     _("Resolved conflicted state of '%s'\n"),
                                     path_local)))
@@ -313,7 +370,7 @@ notify(void *baton, const svn_wc_notify_
         nb->received_some_change = TRUE;
         if (n->content_state == svn_wc_notify_state_conflicted)
           {
-            nb->text_conflicts++;
+            store_path(nb, nb->text_conflicts, path_local);
             statchar_buf[0] = 'C';
           }
         else if (n->kind == svn_node_file)
@@ -326,7 +383,7 @@ notify(void *baton, const svn_wc_notify_
 
         if (n->prop_state == svn_wc_notify_state_conflicted)
           {
-            nb->prop_conflicts++;
+            store_path(nb, nb->prop_conflicts, path_local);
             statchar_buf[1] = 'C';
           }
         else if (n->prop_state == svn_wc_notify_state_changed)
@@ -525,7 +582,7 @@ notify(void *baton, const svn_wc_notify_
       {
         if (n->content_state == svn_wc_notify_state_conflicted)
           {
-            nb->text_conflicts++;
+            store_path(nb, nb->text_conflicts, path_local);
             statchar_buf[0] = 'C';
           }
         else if (n->kind == svn_node_file)
@@ -538,7 +595,7 @@ notify(void *baton, const svn_wc_notify_
 
         if (n->prop_state == svn_wc_notify_state_conflicted)
           {
-            nb->prop_conflicts++;
+            store_path(nb, nb->prop_conflicts, path_local);
             statchar_buf[1] = 'C';
           }
         else if (n->prop_state == svn_wc_notify_state_merged)
@@ -915,7 +972,7 @@ notify(void *baton, const svn_wc_notify_
       break;
 
     case svn_wc_notify_tree_conflict:
-      nb->tree_conflicts++;
+      store_path(nb, nb->tree_conflicts, path_local);
       if ((err = svn_cmdline_printf(pool, "   C %s\n", path_local)))
         goto print_error;
       break;
@@ -1070,9 +1127,13 @@ svn_cl__get_notifier(svn_wc_notify_func2
   nb->is_wc_to_repos_copy = FALSE;
   nb->in_external = FALSE;
   nb->had_print_error = FALSE;
-  nb->text_conflicts = 0;
-  nb->prop_conflicts = 0;
-  nb->tree_conflicts = 0;
+  nb->stats_pool = pool;
+  nb->text_conflicts = apr_hash_make(pool);
+  nb->prop_conflicts = apr_hash_make(pool);
+  nb->tree_conflicts = apr_hash_make(pool);
+  nb->text_conflicts_resolved = 0;
+  nb->prop_conflicts_resolved = 0;
+  nb->tree_conflicts_resolved = 0;
   nb->skipped_paths = 0;
   SVN_ERR(svn_dirent_get_absolute(&nb->path_prefix, "", pool));
 

Modified: subversion/trunk/subversion/tests/cmdline/merge_tests.py
URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/merge_tests.py?rev=1459012&r1=1459011&r2=1459012&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/merge_tests.py (original)
+++ subversion/trunk/subversion/tests/cmdline/merge_tests.py Wed Mar 20 19:33:59 2013
@@ -54,6 +54,7 @@ from svntest.verify import RegexListOutp
 def expected_merge_output(rev_ranges, additional_lines=[], foreign=False,
                           elides=False, two_url=False, target=None,
                           text_conflicts=0, prop_conflicts=0, tree_conflicts=0,
+                          text_resolved=0, prop_resolved=0, tree_resolved=0,
                           skipped_paths=0):
   """Generate an (inefficient) regex representing the expected merge
   output and mergeinfo notifications from REV_RANGES and ADDITIONAL_LINES.
@@ -113,8 +114,11 @@ def expected_merge_output(rev_ranges, ad
     additional_lines = [line.replace("\\", "\\\\") for line in additional_lines]
   lines += additional_lines
 
-  lines += svntest.main.summary_of_conflicts(text_conflicts, prop_conflicts,
-                                             tree_conflicts, skipped_paths)
+  lines += svntest.main.summary_of_conflicts(
+             text_conflicts, prop_conflicts, tree_conflicts,
+             text_resolved, prop_resolved, tree_resolved,
+             skipped_paths,
+             as_regex=True)
 
   return "|".join(lines)
 
@@ -12542,7 +12546,9 @@ def svn_copy(s_rev, path1, path2):
                                      '-r', s_rev, path1, path2)
 
 def svn_merge(rev_range, source, target, lines=None, elides=[],
-              text_conflicts=0, prop_conflicts=0, tree_conflicts=0, args=[]):
+              text_conflicts=0, prop_conflicts=0, tree_conflicts=0,
+              text_resolved=0, prop_resolved=0, tree_resolved=0,
+              args=[]):
   """Merge a single change from path SOURCE to path TARGET and verify the
   output and that there is no error.  (The changes made are not verified.)
 
@@ -12579,7 +12585,10 @@ def svn_merge(rev_range, source, target,
                                   elides=elides,
                                   text_conflicts=text_conflicts,
                                   prop_conflicts=prop_conflicts,
-                                  tree_conflicts=tree_conflicts)
+                                  tree_conflicts=tree_conflicts,
+                                  text_resolved=text_resolved,
+                                  prop_resolved=prop_resolved,
+                                  tree_resolved=tree_resolved)
   svntest.actions.run_and_verify_svn(None, exp_out, [],
                                      'merge', rev_arg, source, target, *args)
 
@@ -13131,8 +13140,9 @@ def merge_two_edits_to_same_prop(sbox):
 
   # Merge the first change, then the second, to trunk.
   svn_merge(rev3, A_COPY_path, A_path, [
-      " G   %s\n" % mu_path,
-      ],
+      " C   %s\n" % mu_path,
+      "Resolved .* '%s'\n" % mu_path,
+      ], prop_resolved=1,
       args=['--allow-mixed-revisions',
             '--accept=working'])
   svn_merge(rev4, A_COPY_path, A_path, [
@@ -15243,7 +15253,8 @@ def merge_automatic_conflict_resolution(
                                      'revert', '--recursive', wc_dir)
 
   # Test --accept mine-conflict and mine-full
-  expected_output = wc.State(A_COPY_path, {'D/H/psi' : Item(status='U ')})
+  ### TODO: Also test that the output has a 'Resolved' line for this path.
+  expected_output = wc.State(A_COPY_path, {'D/H/psi' : Item(status='C ')})
   expected_disk.tweak('D/H/psi', contents="BASE.\n")
   expected_status.tweak('D/H/psi', status='  ')
   svntest.actions.run_and_verify_merge(A_COPY_path, '2', '3',
@@ -15278,7 +15289,8 @@ def merge_automatic_conflict_resolution(
                                      'revert', '--recursive', wc_dir)
 
   # Test --accept theirs-conflict and theirs-full
-  expected_output = wc.State(A_COPY_path, {'D/H/psi' : Item(status='U ')})
+  ### TODO: Also test that the output has a 'Resolved' line for this path.
+  expected_output = wc.State(A_COPY_path, {'D/H/psi' : Item(status='C ')})
   expected_disk.tweak('D/H/psi', contents="New content")
   expected_status.tweak('D/H/psi', status='M ')
   svntest.actions.run_and_verify_merge(A_COPY_path, '2', '3',
@@ -15312,7 +15324,8 @@ def merge_automatic_conflict_resolution(
   svntest.actions.run_and_verify_svn(None, None, [],
                                      'revert', '--recursive', wc_dir)
   # Test --accept base
-  expected_output = wc.State(A_COPY_path, {'D/H/psi' : Item(status='U ')})
+  ### TODO: Also test that the output has a 'Resolved' line for this path.
+  expected_output = wc.State(A_COPY_path, {'D/H/psi' : Item(status='C ')})
   expected_elision_output = wc.State(A_COPY_path, {
     })
   expected_disk.tweak('D/H/psi', contents="This is the file 'psi'.\n")
@@ -18284,7 +18297,8 @@ class RangeList(list):
 def expected_merge_output2(tgt_ospath,
                            recorded_ranges,
                            merged_ranges=None,
-                           prop_conflicts=0):
+                           prop_conflicts=0,
+                           prop_resolved=0):
   """Return an ExpectedOutput instance corresponding to the expected
      output of a merge into TGT_OSPATH, with one 'recording
      mergeinfo...' notification per specified revision range in
@@ -18306,7 +18320,7 @@ def expected_merge_output2(tgt_ospath,
     # List of mergeinfo-strings => list of rangelists
     merged_ranges = [RangeList(r) for r in merged_ranges]
 
-  status_letters_re = prop_conflicts and ' [UC]' or ' U'
+  status_letters_re = (prop_conflicts or prop_resolved) and ' [UC]' or ' U'
   status_letters_mi = ' [UG]'
   lines = []
   for i, rr in enumerate(recorded_ranges):
@@ -18325,14 +18339,19 @@ def expected_merge_output2(tgt_ospath,
     lines += [status_letters_mi + '   ' + re.escape(tgt_ospath) + '\n']
 
   # Summary of conflicts
-  lines += svntest.main.summary_of_conflicts(prop_conflicts=prop_conflicts)
-
-  return RegexListOutput(lines)
+  lines += svntest.main.summary_of_conflicts(prop_conflicts=prop_conflicts,
+                                             prop_resolved=prop_resolved,
+                                             as_regex=True)
+
+  # The 'match_all=False' is because we also expect some
+  # 'Resolved conflicted state of ...' lines.
+  return RegexListOutput(lines, match_all=False)
 
 def expected_out_and_err(tgt_ospath,
                            recorded_ranges,
                            merged_ranges=None,
                            prop_conflicts=0,
+                           prop_resolved=0,
                            expect_error=True):
   """Return a tuple (expected_out, expected_err) giving the expected
      output and expected error output for a merge into TGT_OSPATH. See
@@ -18342,7 +18361,8 @@ def expected_out_and_err(tgt_ospath,
      raised.
   """
   expected_out = expected_merge_output2(tgt_ospath, recorded_ranges,
-                                        merged_ranges, prop_conflicts)
+                                        merged_ranges,
+                                        prop_conflicts, prop_resolved)
   if expect_error:
     expected_err = RegexListOutput([
                      '^svn: E155015: .* conflicts were produced .* into$',
@@ -18747,6 +18767,204 @@ def single_editor_drive_merge_notificati
                                      '-r9:2', sbox.repo_url + '/A',
                                      A_copy_path)
 
+@SkipUnless(server_has_mergeinfo)
+@Issue(4316)  # 'Merge errors out after resolving conflicts'
+# Very similar to conflict_aborted_mergeinfo_described_partial_merge()
+# (test number 135), except here we tell the merge to resolve the
+# conflicts that are generated part way through a multi-revision-range
+# merge, and we expect it to continue with the rest of the merge.
+def conflicted_split_merge_with_resolve(sbox):
+  "conflicted split merge with resolve"
+
+  sbox.build()
+
+  trunk = 'A'
+  branch = 'A2'
+  file = 'mu'
+  dir = 'B'
+  trunk_file = 'A/mu'
+  trunk_dir = 'A/B'
+
+  # r2: initial state
+  for rev in range(4, 11):
+    sbox.simple_propset('prop-' + str(rev), 'Old pval ' + str(rev),
+                        trunk_file, trunk_dir)
+  sbox.simple_commit()
+
+  # r3: branch
+  sbox.simple_copy(trunk, branch)
+  sbox.simple_commit()
+
+  zero_rev = 3
+
+  def edit_file_or_dir(path, rev, val):
+    """Make a local edit to the file at PATH."""
+    sbox.simple_propset('prop-' + str(rev), val + ' pval ' + str(rev), path)
+
+  # r4 through r10: simple edits
+  for rev in range(4, 11):
+    edit_file_or_dir(trunk_file, rev, 'Edited')
+    edit_file_or_dir(trunk_dir, rev, 'Edited')
+    sbox.simple_commit()
+
+  # r14: merge some changes to the branch so that later merges will be split
+  svntest.actions.run_and_verify_svn(None, None, [], 'merge', '-c5,9',
+                                     '^/' + trunk, sbox.ospath(branch),
+                                     '--accept', 'theirs-conflict')
+  sbox.simple_commit()
+  sbox.simple_update()
+
+  def revert_branch():
+    svntest.actions.run_and_verify_svn(None, None, [], 'revert', '-R',
+                                       sbox.ospath(branch))
+
+  def try_merge(relpath, conflict_rev, rev_args,
+                expected_out_err, expected_mi):
+    """Revert RELPATH in the branch; make a change that will conflict
+       with CONFLICT_REV if not None; merge RELPATH in the trunk
+       to RELPATH in the branch using revision arguments REV_ARGS (list of
+       '-r...' or '-c...' strings).
+
+       EXPECTED_OUT_ERR_MI is a tuple: (expected_out, expected_err,
+       expected_mi).  EXPECTED_OUT and EXPECTED_ERR are instances of
+       ExpectedOutput.
+
+       Expect to find mergeinfo EXPECTED_MI if not None.  EXPECTED_MI is
+       a single mergeinfo-string.
+    """
+    src_path = trunk + '/' + relpath
+    tgt_path = branch + '/' + relpath
+    tgt_ospath = sbox.ospath(tgt_path)
+
+    expected_out, expected_err = expected_out_err
+
+    revert_branch()
+
+    # Arrange for the merge to conflict at CONFLICT_REV.
+    if conflict_rev:
+      edit_file_or_dir(tgt_path, conflict_rev, 'Conflict')
+
+    src_url = '^/' + src_path + '@11'
+    svntest.actions.run_and_verify_svn(
+                      None, expected_out, expected_err,
+                      'merge', src_url, tgt_ospath, '--accept', 'mine-full',
+                      *rev_args)
+
+    if expected_mi is not None:
+      expected_mergeinfo = ['/' + src_path + ':' + expected_mi + '\n']
+      check_mergeinfo(expected_mergeinfo, tgt_ospath)
+
+  # In a mergeinfo-aware merge, each specified revision range is split
+  # internally into sub-ranges, to avoid any already-merged revisions.
+  #
+  # From white-box inspection, we see there are code paths that treat
+  # the last specified range and the last sub-range specially.  The
+  # first specified range or sub-range is not treated specially in terms
+  # of the code paths, although it might be in terms of data flow.
+  #
+  # We test merges that raise a conflict in the first and last sub-range
+  # of the first and last specified range.
+
+  for target in [file, dir]:
+
+    tgt_ospath = sbox.ospath(branch + '/' + target)
+
+    # First test: Merge "everything" to the branch.
+    #
+    # This merge is split into three sub-ranges: r3-4, r6-8, r10-head.
+    # We have arranged that the merge will raise a conflict in the first
+    # sub-range.  Since we are postponing conflict resolution, the merge
+    # should stop after the first sub-range, allowing us to resolve and
+    # repeat the merge at which point the next sub-range(s) can be merged.
+    # The mergeinfo on the target then should only reflect that the first
+    # sub-range (r3-4) has been merged.
+
+    # ### Bug? The mergeinfo ranges recorded by the merge differ
+    #     slightly for a single-file merge vs. a dir merge.  That's
+    #     incidental to the purpose of this test, so we work around it
+    #     with "target == file and 'X' or 'Y'".
+    expect = expected_out_and_err(tgt_ospath,
+                                  target == file and '3-4,5-11' or '3-4,6-11',
+                                  ['3-4', '6-8,10-11'],
+                                  prop_resolved=1, expect_error=False)
+    try_merge(target, 4, [], expect, '3-11')
+
+    # Try a multiple-range merge that raises a conflict in the
+    # first sub-range in the first specified range;
+    expect = expected_out_and_err(tgt_ospath,
+                                  target == file and '4,5-6,8-10' or '4,6,8-10',
+                                  ['4', '6', '8,10'],
+                                  prop_resolved=1, expect_error=False)
+    try_merge(target, 4, ['-c4-6,8-10'], expect, '4-6,8-10')
+    # last sub-range in the first specified range;
+    expect = expected_out_and_err(tgt_ospath,
+                                  '4-6,8-10', ['4,6', '8,10'],
+                                  prop_resolved=1, expect_error=False)
+    try_merge(target, 6, ['-c4-6,8-10'], expect, '4-6,8-10')
+    # first sub-range in the last specified range;
+    expect = expected_out_and_err(tgt_ospath,
+                                  target == file and '4-6,8,9-10' or '4-6,8,10',
+                                  ['4,6', '8', '10'],
+                                  prop_resolved=1, expect_error=False)
+    try_merge(target, 8, ['-c4-6,8-10'], expect, '4-6,8-10')
+    # last sub-range in the last specified range.
+    # (Expect no error, because 'svn merge' does not throw an error if
+    # there is no more merging to do when a conflict occurs.)
+    expect = expected_out_and_err(tgt_ospath,
+                                  '4-6,8-10', ['4,6', '8,10'],
+                                  prop_resolved=1, expect_error=False)
+    try_merge(target, 10, ['-c4-6,8-10'], expect, '4-6,8-10')
+
+    # Try similar merges but involving ranges in reverse order.
+    expect = expected_out_and_err(tgt_ospath,
+                                  '8', ['8'],
+                                  prop_resolved=1, expect_error=False)
+    try_merge(target, 8,  ['-c8-10,4-6'], expect, '4-6,8-10')
+    expect = expected_out_and_err(tgt_ospath,
+                                  '8-10', ['8,10'],
+                                  prop_resolved=1, expect_error=False)
+    try_merge(target, 10, ['-c8-10,4-6'], expect, '4-6,8-10')
+    expect = expected_out_and_err(tgt_ospath,
+                                  '8-10,4', ['8,10', '4'],
+                                  prop_resolved=1, expect_error=False)
+    try_merge(target, 4,  ['-c8-10,4-6'], expect, '4-6,8-10')
+    expect = expected_out_and_err(tgt_ospath,
+                                  '8-10,4-6', ['8,10', '4,6'],
+                                  prop_resolved=1, expect_error=False)
+    try_merge(target, 6,  ['-c8-10,4-6'], expect, '4-6,8-10')
+
+    # Try some reverse merges, with ranges in forward and reverse order.
+    #
+    # Reverse merges start with all source changes merged except 5 and 9.
+    revert_branch()
+    simple_merge(trunk + '/' + target, sbox.ospath(branch + '/' + target),
+                 ['-c-5,-9,4,6-8,10'])
+    sbox.simple_commit()
+    sbox.simple_update()
+
+    expect = expected_out_and_err(tgt_ospath,
+                                  '6-4,10-8', ['-6,-4', '-10,-8'],
+                                  expect_error=False)
+    try_merge(target, None, ['-r6:3', '-r10:7'], expect, '7')
+    expect = expected_out_and_err(tgt_ospath,
+                                  target == file and '-6,5-4,10-8' or '-6,-4,10-8',
+                                  ['-6', '-4', '-10,-8'],
+                                  prop_resolved=1, expect_error=False)
+    try_merge(target, 6,  ['-r6:3', '-r10:7'], expect, '7')
+    expect = expected_out_and_err(tgt_ospath,
+                                  '6-4,10-8', ['-6,-4', '-10,-8'],
+                                  prop_resolved=1, expect_error=False)
+    try_merge(target, 4,  ['-r6:3', '-r10:7'], expect, '7')
+    expect = expected_out_and_err(tgt_ospath,
+                                  target == file and '6-4,-10,9-8' or '6-4,-10,-8',
+                                  ['-6,-4', '-10', '-8'],
+                                  prop_resolved=1, expect_error=False)
+    try_merge(target, 10, ['-r6:3', '-r10:7'], expect, '7')
+    expect = expected_out_and_err(tgt_ospath,
+                                  '6-4,10-8', ['-6,-4', '-10,-8'],
+                                  prop_resolved=1, expect_error=False)
+    try_merge(target, 8,  ['-r6:3', '-r10:7'], expect, '7')
+
 ########################################################################
 # Run the tests
 
@@ -18890,6 +19108,7 @@ test_list = [ None,
               conflict_aborted_mergeinfo_described_partial_merge,
               multiple_editor_drive_merge_notifications,
               single_editor_drive_merge_notifications,
+              conflicted_split_merge_with_resolve,
              ]
 
 if __name__ == '__main__':

Modified: subversion/trunk/subversion/tests/cmdline/svntest/main.py
URL: http://svn.apache.org/viewvc/subversion/trunk/subversion/tests/cmdline/svntest/main.py?rev=1459012&r1=1459011&r2=1459012&view=diff
==============================================================================
--- subversion/trunk/subversion/tests/cmdline/svntest/main.py (original)
+++ subversion/trunk/subversion/tests/cmdline/svntest/main.py Wed Mar 20 19:33:59 2013
@@ -1186,24 +1186,46 @@ def merge_notify_line(revstart=None, rev
       return "--- Merging %sr%ld through r%ld into '%s':\n" \
              % (from_foreign_phrase, revstart, revend, target_re)
 
-def summary_of_conflicts(text_conflicts=0, prop_conflicts=0,
-                         tree_conflicts=0, skipped_paths=0):
+def summary_of_conflicts(text_conflicts=0,
+                         prop_conflicts=0,
+                         tree_conflicts=0,
+                         text_resolved=0,
+                         prop_resolved=0,
+                         tree_resolved=0,
+                         skipped_paths=0,
+                         as_regex=False):
   """Return a list of lines corresponding to the summary of conflicts and
      skipped paths that is printed by merge and update and switch.  If all
      parameters are zero, return an empty list.
   """
   lines = []
-  if text_conflicts or prop_conflicts or tree_conflicts or skipped_paths:
+  if (text_conflicts or prop_conflicts or tree_conflicts
+      or text_resolved or prop_resolved or tree_resolved
+      or skipped_paths):
     lines.append("Summary of conflicts:\n")
-    if text_conflicts:
-      lines.append("  Text conflicts: %d\n" % text_conflicts)
-    if prop_conflicts:
-      lines.append("  Property conflicts: %d\n" % prop_conflicts)
-    if tree_conflicts:
-      lines.append("  Tree conflicts: %d\n" % tree_conflicts)
+    if text_conflicts or text_resolved:
+      if text_resolved == 0:
+        lines.append("  Text conflicts: %d\n" % text_conflicts)
+      else:
+        lines.append("  Text conflicts: %d remaining (and %d already resolved)\n"
+                     % (text_conflicts, text_resolved))
+    if prop_conflicts or prop_resolved:
+      if prop_resolved == 0:
+        lines.append("  Property conflicts: %d\n" % prop_conflicts)
+      else:
+        lines.append("  Property conflicts: %d remaining (and %d already resolved)\n"
+                     % (prop_conflicts, prop_resolved))
+    if tree_conflicts or tree_resolved:
+      if tree_resolved == 0:
+        lines.append("  Tree conflicts: %d\n" % tree_conflicts)
+      else:
+        lines.append("  Tree conflicts: %d remaining (and %d already resolved)\n"
+                     % (tree_conflicts, tree_resolved))
     if skipped_paths:
       lines.append("  Skipped paths: %d\n" % skipped_paths)
 
+  if as_regex:
+    lines = map(re.escape, lines)
   return lines