In automatic (tracked) merges, detect when a revision to be merged is itself
the result of a merge in the opposite direction (often called a reflective
merge).  Don't merge any such revisions, but do record them as merged.

The most important result is that the user can reintegrate a branch and then
carry on using it without needing to do either of the two currently
recommended work-arounds (the record-only merge known as the 'keep-alive
dance', or deleting and recreating the branch).  The next time the user runs
a sync merge on the branch, Subversion will automatically detect the
reintegration commit on the parent branch and will record that revision as
having been merged to the branch just like the manual 'keep-alive dance'.

Future improvements:

  - Detect when a revision contains both reflective merges and other merges,
    and if so then issue a friendly diagnostic message to let the user know
    that they will need to merge the other changes manually because this
    case is beyond the capability of Subversion's automatic merge tracking.

  - Examine subtree mergeinfo as well as merge-root mergeinfo. (At present,
    any revision that has reflective mergeinfo at the merge-root will be
    skipped across all sub-trees, but a revision that only shows up as
    reflective if we look at its subtree mergeinfo will not be skipped.)

  - Work also for a reverse merge. (Just need to generalize the range
    arithmetic w.r.t. exclusive and inclusive end-points.)

  - Work also for a single-file merge. (But, ugh, the code is completely
    different.)

### Test failures:
### blame_tests.py 10: test 'svn blame -g'
### blame_tests.py 11: don't look for merged files out of range
### log_tests.py 16-19: test 'svn log -g' on ...
### merge_tests.py 93: natural history filtering permits valid mergeinfo

* subversion/libsvn_client/merge.c
  (show_mergeinfo): New (for debugging only).
  (mergeinfo_graph_*): New.
  (fetch_repos_mergeinfo, incoming_mergeinfo_diff, is_reflected_merge,
   find_reflected_rev, remove_reflected_revs): New functions.
  (do_directory_merge): Call remove_reflected_revs().

* subversion/tests/cmdline/merge_reintegrate_tests.py
  (simple_reintegrate, simple_sync_merge): New functions.
  (reintegrate_keep_alive1, reintegrate_keep_alive2, reintegrate_keep_alive3):
    New tests.
  (test_list): Add the new tests.
--This line, and those below, will be ignored--

Index: subversion/libsvn_client/merge.c
===================================================================
--- subversion/libsvn_client/merge.c	(revision 1232383)
+++ subversion/libsvn_client/merge.c	(working copy)
@@ -8348,6 +8348,556 @@ remove_noop_subtree_ranges(const merge_s
   return SVN_NO_ERROR;
 }
 
+/* ### for debug prints */
+static const char *
+show_mergeinfo(svn_mergeinfo_t mergeinfo,
+               const char *prefix,
+               apr_pool_t *scratch_pool)
+{
+  svn_string_t *str;
+
+  svn_error_clear(svn_mergeinfo__to_formatted_string(
+                    &str, mergeinfo,
+                    apr_pstrcat(scratch_pool, "DBG: ", prefix, (char *)NULL),
+                    scratch_pool));
+  if (strcmp(str->data, apr_pstrcat(scratch_pool, "DBG: ", prefix,
+                                    "empty mergeinfo\n", (char *)NULL)) == 0)
+    return "";
+  return str->data;
+}
+
+/* Set *MERGEINFO to the explicit or inherited mergeinfo of the repository
+ * node URL@REVNUM.  If the node URL@REVNUM does not exist, or there is no
+ * mergeinfo, the result is empty (not null).
+ * Use RA_SESSION, reparenting it temporarily.
+ */
+static svn_error_t *
+fetch_repos_mergeinfo(svn_mergeinfo_t *mergeinfo,
+                      const char *url,
+                      svn_revnum_t revnum,
+                      svn_ra_session_t *ra_session,
+                      apr_pool_t *result_pool)
+{
+  svn_error_t *err;
+
+  err = svn_client__get_repos_mergeinfo(
+          mergeinfo, ra_session, url, revnum,
+          svn_mergeinfo_inherited, TRUE /* squelch_incapable */,
+          result_pool);
+  if (err && (err->apr_err == SVN_ERR_FS_NOT_FOUND
+              || err->apr_err == SVN_ERR_RA_DAV_REQUEST_FAILED))
+    {
+      svn_error_clear(err);
+      *mergeinfo = apr_hash_make(result_pool);
+    }
+  else
+    SVN_ERR(err);
+
+  if (! *mergeinfo)
+    *mergeinfo = apr_hash_make(result_pool);
+  return SVN_NO_ERROR;
+}
+
+/* Find the changes in (explicit or inherited) mergeinfo between the left
+ * and right sides of SOURCE.  Set *DELETED to the deleted mergeinfo and
+ * *ADDED to the added mergeinfo; neither of these results will be null.
+ *
+ * ### TODO: Optimization: cache the mergeinfo (in SOURCE?), instead of
+ * always fetching from the repo.
+ */
+static svn_error_t *
+incoming_mergeinfo_diff(svn_mergeinfo_t *deleted,
+                        svn_mergeinfo_t *added,
+                        const merge_source_t *source,
+                        svn_ra_session_t *ra_session,
+                        svn_client_ctx_t *ctx,
+                        apr_pool_t *result_pool,
+                        apr_pool_t *scratch_pool)
+{
+  svn_mergeinfo_t mergeinfo_left, mergeinfo_right;
+
+  SVN_ERR(fetch_repos_mergeinfo(&mergeinfo_left,
+                                source->url1, source->rev1,
+                                ra_session, scratch_pool));
+  SVN_ERR(fetch_repos_mergeinfo(&mergeinfo_right,
+                                source->url2, source->rev2,
+                                ra_session, scratch_pool));
+
+  SVN_ERR(svn_mergeinfo_diff2(deleted, added,
+                              mergeinfo_left, mergeinfo_right,
+                              TRUE /* consider_inheritance */,
+                              result_pool, scratch_pool));
+  return SVN_NO_ERROR;
+}
+
+/*
+ * TODO: store the 'inheritable' flag?
+ */
+typedef struct mergeinfo_graph_node_t
+{
+  enum
+    { mergeinfo_graph_merge,
+      mergeinfo_graph_change,
+      mergeinfo_graph_noop
+    } kind;
+  svn_mergeinfo_t added;
+  svn_mergeinfo_t deleted;
+} mergeinfo_graph_node_t;
+
+/* A graph of merges, or more specifically of mergeinfo changes.
+ *
+ * Each graph node represents a revision (of some directory or file) and
+ * contains (in the form of mergeinfo) pointers to all the graph nodes
+ * that represent the revisions merged into this revision at this time.
+ */
+typedef struct mergeinfo_graph_t
+{
+  /* Keys are (const char *) "relpath:rev" strings; values are
+   * mergeinfo_graph_node_t. */
+  apr_hash_t *hash;
+} mergeinfo_graph_t;
+
+/* Return a string representing ONE_REV_CHANGE in a way that is sufficiently
+ * unique to be used as a hash key in mergeinfo_graph_t. */
+static const char *
+mergeinfo_graph_key(const merge_source_t *one_rev_change,
+                    apr_pool_t *pool)
+{
+  return apr_psprintf(pool, "%s:%ld",
+                      one_rev_change->url2, one_rev_change->rev2);
+}
+
+/* Set the node in GRAPH keyed by ONE_REV_CHANGE to a new node indicating
+ * a merge, MERGEINFO_ADDED and MERGEINFO_DELETED.  Allocate the new node
+ * in GRAPH's pool. */
+static void
+mergeinfo_graph_set(const mergeinfo_graph_t *graph,
+                    const merge_source_t *one_rev_change,
+                    svn_mergeinfo_t mergeinfo_added,
+                    svn_mergeinfo_t mergeinfo_deleted,
+                    apr_pool_t *scratch_pool)
+{
+  apr_pool_t *graph_pool = apr_hash_pool_get(graph->hash);
+  const char *key = mergeinfo_graph_key(one_rev_change, scratch_pool);
+  mergeinfo_graph_node_t *node = apr_palloc(graph_pool, sizeof(*node));
+
+  node->kind = mergeinfo_graph_merge;
+  node->added = mergeinfo_added;
+  node->deleted = mergeinfo_deleted;
+  apr_hash_set(graph->hash, key, APR_HASH_KEY_STRING, node);
+}
+
+/* Return a pointer to the node in GRAPH keyed by ONE_REV_CHANGE, or NULL
+ * if there is no such node. */
+static mergeinfo_graph_node_t *
+mergeinfo_graph_get(const mergeinfo_graph_t *graph,
+                    const merge_source_t *one_rev_change,
+                    apr_pool_t *scratch_pool)
+{
+  const char *key = mergeinfo_graph_key(one_rev_change, scratch_pool);
+
+  return apr_hash_get(graph->hash, key, APR_HASH_KEY_STRING);
+}
+
+/* Find the mergeinfo for PATH@REV, and all the mergeinfo it references,
+ * recursively, storing it all in GRAPH.  Assume any entry that is already
+ * in GRAPH is correct and complete.
+ *
+ * TODO: also recurse on mergeinfo_deleted revs.
+ */
+static svn_error_t *
+mergeinfo_graph_populate(mergeinfo_graph_t *graph,
+                         const merge_source_t *one_rev_change,
+                         svn_ra_session_t *ra_session,
+                         svn_client_ctx_t *ctx,
+                         apr_pool_t *scratch_pool)
+{
+  apr_pool_t *graph_pool = apr_hash_pool_get(graph->hash);
+  const char *repos_root_url;
+#ifdef SVN_DEBUG
+  svn_revnum_t rev = one_rev_change->rev2;
+  const char *relpath;
+#endif
+  svn_mergeinfo_t mergeinfo_deleted, mergeinfo_added;
+  apr_hash_index_t *hi;
+
+  SVN_ERR(svn_ra_get_repos_root2(ra_session, &repos_root_url, scratch_pool));
+#ifdef SVN_DEBUG
+  relpath = svn_uri_skip_ancestor(repos_root_url, one_rev_change->url2, scratch_pool);
+#endif
+
+  if (mergeinfo_graph_get(graph, one_rev_change, scratch_pool))
+    {
+      SVN_DBG(("mi_graph_populate(%s:%ld) => already present\n", relpath, rev));
+      return SVN_NO_ERROR;
+    }
+
+  SVN_ERR(incoming_mergeinfo_diff(&mergeinfo_deleted, &mergeinfo_added,
+                                  one_rev_change, ra_session,
+                                  ctx, graph_pool, scratch_pool));
+
+  SVN_DBG(("mi_graph_populate(%s:%ld) => -%d,+%d\n"
+           "%s"
+           "%s",
+           relpath, rev, apr_hash_count(mergeinfo_deleted),
+           apr_hash_count(mergeinfo_added),
+           show_mergeinfo(mergeinfo_deleted, "- ", scratch_pool),
+           show_mergeinfo(mergeinfo_added,   "+ ", scratch_pool)));
+
+  mergeinfo_graph_set(graph, one_rev_change,
+                      mergeinfo_added, mergeinfo_deleted,
+                      scratch_pool);
+
+  for (hi = apr_hash_first(scratch_pool, mergeinfo_added);
+       hi;
+       hi = apr_hash_next(hi))
+    {
+      const char *merged_fspath = svn__apr_hash_index_key(hi);
+      const apr_array_header_t *rangelist = svn__apr_hash_index_val(hi);
+      const char *merged_url
+        = svn_path_url_add_component2(repos_root_url, merged_fspath + 1,
+                                      scratch_pool);
+      int i;
+
+      for (i = 0; i < rangelist->nelts; i++)
+        {
+          const svn_merge_range_t *range
+            = APR_ARRAY_IDX(rangelist, i, svn_merge_range_t *);
+          svn_revnum_t r;
+
+          for (r = range->start + 1; r <= range->end; r++)
+            {
+              merge_source_t merged_rev = { merged_url, r - 1, merged_url, r };
+              SVN_ERR(mergeinfo_graph_populate(graph, &merged_rev,
+                                               ra_session, ctx, scratch_pool));
+            }
+        }
+    }
+
+  for (hi = apr_hash_first(scratch_pool, mergeinfo_deleted);
+       hi;
+       hi = apr_hash_next(hi))
+    {
+      const char *merged_fspath = svn__apr_hash_index_key(hi);
+      SVN_DBG(("%s: (reverse-mrg)\n", merged_fspath));
+    }
+
+  return SVN_NO_ERROR;
+}
+
+/* Given a merge source and target both represented by CHILDREN_WITH_MERGEINFO,
+ * and a mergeinfo graph GRAPH, determine whether the entire set of logical
+ * changes comprising ONE_REV_CHANGE is already on the target.
+ *
+ * If ONE_REV_CHANGE is a no-op:
+ *   return No-op
+ * If ONE_REV_CHANGE is itself recorded on the target:
+ *   return Yes
+ * If ONE_REV_CHANGE is a logical change:
+ *   return No
+ * If ONE_REV_CHANGE was a merge:
+ *   recurse to examine each merged change
+ *   if they are all No-op:
+ *     return No-op
+ *   if they are all Yes or No-op:
+ *     return Yes
+ *   if they are all No or No-op:
+ *     return No
+ *   return Partial
+ */
+static svn_error_t *
+mergeinfo_graph_trace(int *result,
+                      mergeinfo_graph_t *graph,
+                      const merge_source_t *one_rev_change,
+                      const apr_array_header_t *children_with_mergeinfo,
+                      apr_pool_t *scratch_pool)
+{
+  return SVN_NO_ERROR;
+}
+
+/* Look at the overall change in mergeinfo (that is, compare
+ * mergeinfo in SOURCE left with SOURCE right), and see if
+ * there is any addition of mergeinfo about merges *from* TARGET.
+ *
+ * Set *reflected_completely to true if the change in mergeinfo contains
+ * any merges from TARGET.
+ *
+ * TODO: Set *reflected_completely to true if the change in mergeinfo
+ * consists of nothing more that changes that are present in TARGET,
+ * taking into consideration that a merge 'MB' from another branch 'B' might
+ * show up because of having been merged via TARGET.  So, if the merge
+ * directly from TARGET is 'MT', and the corresponding change(s) on TARGET
+ * brought in the change 'MB', then we can ignore 'MB' because we know it
+ * was a transitive merge.  (### What if in fact MB was transitive in the
+ * other direction?)
+ */
+static svn_error_t *
+is_reflected_merge(svn_boolean_t *reflected,
+                   svn_boolean_t *reflected_completely,
+                   apr_array_header_t *children_with_mergeinfo,
+                   const merge_source_t *source,
+                   svn_ra_session_t *ra_session,
+                   svn_client_ctx_t *ctx,
+                   apr_pool_t *scratch_pool)
+{
+  svn_client__merge_path_t *root_child =
+    APR_ARRAY_IDX(children_with_mergeinfo, 0, svn_client__merge_path_t *);
+  svn_mergeinfo_t deleted;
+  svn_mergeinfo_t added;
+
+  SVN_ERR(incoming_mergeinfo_diff(&deleted, &added, source, ra_session,
+                                  ctx, scratch_pool, scratch_pool));
+  /* Find which differences represent merges from TARGET_LOCATIONS. */
+  SVN_ERR(svn_mergeinfo_intersect2(&deleted, deleted,
+                                   root_child->implicit_mergeinfo,
+                                   FALSE /* consider_inheritance */,
+                                   scratch_pool, scratch_pool));
+  SVN_ERR(svn_mergeinfo_intersect2(&added, added,
+                                   root_child->implicit_mergeinfo,
+                                   FALSE /* consider_inheritance */,
+                                   scratch_pool, scratch_pool));
+
+  if (apr_hash_count(deleted) || apr_hash_count(added))
+    SVN_DBG(("r%ld:%ld mi change:\n%s%s",
+             source->rev1, source->rev2,
+             show_mergeinfo(deleted, "- ", scratch_pool),
+             show_mergeinfo(added,   "+ ", scratch_pool)));
+
+  *reflected = (apr_hash_count(deleted) || apr_hash_count(added));
+  *reflected_completely = FALSE;
+  return SVN_NO_ERROR;
+}
+
+
+/* Are we trying to merge into TGT a change that contains a merge
+ * *from* TGT?  If so, that is probably not desired
+ * and will probably result in conflicts; certainly it is logically
+ * wrong.  The exception is if rollback, reverse-merging or other
+ * forms of undoing are involved; but these are not fully supported
+ * by merge tracking.
+ *
+ * At the very least we should detect and warn.  Better, we could
+ * automatically skip such already-present changes.
+ *
+ * If the entire incoming change (or any single revision of it) says
+ * it is (or includes) a merge from TGT, then we know it is not a
+ * safe change to merge, and we can at least warn about it.
+ *
+ * If we can detect that one revision of the incoming change consists
+ * entirely of a change merged from TGT then we should skip it.  That
+ * will enable us to automatically ignore the following changes in SRC:
+ *
+ *   * the result of a reintegrate merge from TGT
+ *   * the result of a cherry-pick merge from TGT
+ *   * the result of any tracked merge from some other branch, that
+ *     was equivalent (in the merge tracking sense) to a merge from TGT
+ */
+
+/* Given an incoming (multi-rev) change SOURCE, and a target described by
+ * (the first item of) CHILDREN_WITH_MERGEINFO, set *REV to the first
+ * reflective merge in SOURCE, or SVN_INVALID_REVNUM if none.
+ *
+ * This implements algorithm 1.
+ *
+ * ### TODO: Support a reverse range. Presently assumes a forward range.
+ *
+ * Algorithm 1:
+ *
+ *  def find(RANGE):
+ *     # Precondition: rev range RANGE = SOURCE may contain a reflected merge.
+ *     # Postcondition: returns revision R which is in RANGE, or NULL.
+ *     if is_reflected_merge(RANGE):
+ *       if RANGE is a single (operative) revision:
+ *         return R = that revision
+ *       else:
+ *         R := find(first half of RANGE)
+ *         if R:
+ *           return R
+ *         R := find(second half of RANGE)
+ *         assert(R)  # because if RANGE not reflective we should
+ *                      have taken the outer 'else' branch
+ *         return R
+ *     else:
+ *       return NULL
+ *
+ * Algorithm 2:
+ *
+ *   Note: more efficient than algorithm 1.
+ *
+ *   def find(RANGE):
+ *     # Precondition: RANGE contains a reflected merge.
+ *     # Postcondition: returns revision R which is in RANGE.
+ *     if RANGE is a single (operative) revision:
+ *       return R = that revision
+ *     else:
+ *       RANGE1 := first half of RANGE
+ *       if is_reflected_merge(RANGE1):
+ *         return find(RANGE1)
+ *       else:
+ *         RANGE2 := second half of RANGE
+ *         return find(RANGE2)
+ *
+ *   Start:
+ *     if is_reflected_merge(SOURCE):
+ *       return find(SOURCE)
+ *     else:
+ *       return NULL
+ */
+static svn_error_t *
+find_reflected_rev(svn_revnum_t *rev,
+                   apr_array_header_t *children_with_mergeinfo,
+                   const merge_source_t *source,
+                   svn_ra_session_t *ra_session,
+                   svn_client_ctx_t *ctx,
+                   apr_pool_t *scratch_pool)
+{
+  svn_boolean_t reflected, reflected_completely;
+
+  SVN_ERR(is_reflected_merge(&reflected, &reflected_completely,
+                             children_with_mergeinfo, source,
+                             ra_session, ctx, scratch_pool));
+  /* SVN_DBG(("r=%ld:%ld reflected=%d\n", source->rev1, source->rev2, reflected)); */
+
+  if (reflected)
+    {
+      if (source->rev1 + 1 == source->rev2)
+        {
+          *rev = source->rev2;
+        }
+      else
+        {
+          svn_revnum_t half_way = (source->rev1 + source->rev2) / 2;
+          merge_source_t *source1 = subrange_source(source, source->rev1,
+                                                    half_way, scratch_pool);
+          SVN_ERR(find_reflected_rev(rev, children_with_mergeinfo, source1,
+                                     ra_session, ctx, scratch_pool));
+          if (! SVN_IS_VALID_REVNUM(*rev))
+            {
+              merge_source_t *source2
+                = subrange_source(source, half_way, source->rev2, scratch_pool);
+              SVN_ERR(find_reflected_rev(rev, children_with_mergeinfo, source2,
+                                         ra_session, ctx, scratch_pool));
+              SVN_ERR_ASSERT(SVN_IS_VALID_REVNUM(*rev));
+            }
+        }
+    }
+  else
+    {
+      *rev = SVN_INVALID_REVNUM;
+    }
+
+  return SVN_NO_ERROR;
+}
+
+/* Remove revision REV from the 'remaining_ranges' of every child (including
+ * the target root) in CHILDREN_WITH_MERGEINFO. */
+static svn_error_t *
+remove_rev_from_children_with_mergeinfo(
+                apr_array_header_t *children_with_mergeinfo,
+                svn_revnum_t rev,
+                apr_pool_t *result_pool,
+                apr_pool_t *scratch_pool)
+{
+  apr_array_header_t *rangelist
+    = svn_rangelist__initialize(rev - 1, rev, TRUE, scratch_pool);
+  int i;
+
+  for (i = 0; i < children_with_mergeinfo->nelts; i++)
+    {
+      svn_client__merge_path_t *child
+        = APR_ARRAY_IDX(children_with_mergeinfo, i,
+                        svn_client__merge_path_t *);
+
+      if (child->remaining_ranges && child->remaining_ranges->nelts)
+        {
+          SVN_ERR(svn_rangelist_remove(&child->remaining_ranges,
+                                       rangelist,
+                                       child->remaining_ranges,
+                                       FALSE, result_pool));
+        }
+    }
+  return SVN_NO_ERROR;
+}
+
+/* Skip any individual change (revision) on the source that was the
+ * result of a merge from the (current) target branch.  This is a
+ * special case of support for 'reflective' merges.  In particular,
+ * we want to skip any source change that was the result of a
+ * reintegrate merge from the (current) target.
+ *
+ * If it's a record-only merge, then we don't care and we must
+ * allow this case because it's the historically recommended
+ * way of keeping a reintegrated branch alive.
+ */
+static svn_error_t *
+remove_reflected_revs(const merge_source_t *source,
+                      svn_ra_session_t *ra_session,
+                      apr_array_header_t *children_with_mergeinfo,
+                      merge_cmd_baton_t *merge_b,
+                      apr_pool_t *result_pool,
+                      apr_pool_t *scratch_pool)
+{
+  svn_client__merge_path_t *root_child =
+    APR_ARRAY_IDX(children_with_mergeinfo, 0, svn_client__merge_path_t *);
+  const merge_source_t *remaining_source = source;
+  apr_pool_t *iterpool = svn_pool_create(scratch_pool);
+
+  if (! root_child->implicit_mergeinfo || merge_b->record_only)
+    return SVN_NO_ERROR;
+
+  while (remaining_source->rev1 < remaining_source->rev2)
+    {
+      svn_revnum_t rev;
+
+      svn_pool_clear(iterpool);
+      SVN_ERR(find_reflected_rev(&rev, children_with_mergeinfo,
+                                 remaining_source,
+                                 merge_b->ra_session1,
+                                 merge_b->ctx, iterpool));
+      if (rev == SVN_INVALID_REVNUM)
+        {
+          break;
+        }
+
+      /* Part of the incoming change is reflective. */
+      SVN_DBG((_("Skipping reflective revision r%ld\n"), rev));
+
+      /* Determine whether it's partly or completely reflective.
+       * ### TODO: Make use of this info in some way: if partial, then
+       *     issue a diagnostic message with details, and/or go ahead and
+       *     merge like we used to, even though there will be conflicts. */
+      {
+        merge_source_t *rev_source
+          = subrange_source(source, rev - 1, rev, iterpool);
+        mergeinfo_graph_t *graph = apr_palloc(scratch_pool, sizeof(*graph));
+        int result;
+
+        graph->hash = apr_hash_make(scratch_pool);
+        SVN_ERR(mergeinfo_graph_populate(graph, rev_source, ra_session,
+                                         merge_b->ctx, scratch_pool));
+        /* What now? Question to ask is: if we trace the graph from REV_SOURCE,
+         * we know some of the mergeinfo comes from the target (CHILDREN_...)
+         * but does any of it NOT come from there? */
+        SVN_ERR(mergeinfo_graph_trace(&result, graph, rev_source,
+                                      children_with_mergeinfo,
+                                      scratch_pool));
+      }
+
+      /* Remove this revision from target and all children. */
+      SVN_ERR(remove_rev_from_children_with_mergeinfo(
+                children_with_mergeinfo, rev, result_pool, scratch_pool));
+
+      remaining_source = subrange_source(remaining_source,
+                                         rev /*exclusive*/,
+                                         source->rev2 /*inclusive*/,
+                                         scratch_pool);
+    }
+
+  svn_pool_destroy(iterpool);
+  return SVN_NO_ERROR;
+}
+
+
 /* Helper for do_merge() when the merge target is a directory.
 
    Perform a merge of changes in SOURCE to the working copy path
@@ -8486,6 +9036,13 @@ do_directory_merge(svn_mergeinfo_catalog
       if (SVN_IS_VALID_REVNUM(new_range_start))
         range.start = new_range_start;
 
+      /* Don't merge any revision that represents a reflected merge (a merge
+         of a change that was earlier merged *from* the current target). */
+      SVN_ERR(remove_reflected_revs(source,
+                                    ra_session,
+                                    notify_b->children_with_mergeinfo,
+                                    merge_b, scratch_pool, iterpool));
+
       /* Remove inoperative ranges from any subtrees' remaining_ranges
          to spare the expense of noop editor drives. */
       if (!is_rollback)
@@ -8822,6 +9379,18 @@ do_merge(apr_hash_t **modified_subtrees,
 
   SVN_ERR_ASSERT(svn_dirent_is_absolute(target->abspath));
 
+#ifdef SVN_DEBUG
+  for (i = 0; i < merge_sources->nelts; i++)
+    {
+      merge_source_t *source =
+        APR_ARRAY_IDX(merge_sources, i, merge_source_t *);
+
+#define abbrev(s) (strstr(s, "/repo") ? strstr(s, "/repo") + 5 : s)
+      SVN_DBG(("do_merge srcs[%d]: (%s@%ld : %s@%ld)\n",
+               i, abbrev(source->url1), source->rev1, abbrev(source->url2), source->rev2));
+    }
+#endif
+
   /* Check from some special conditions when in record-only mode
      (which is a merge-tracking thing). */
   if (record_only)
Index: subversion/tests/cmdline/merge_reintegrate_tests.py
===================================================================
--- subversion/tests/cmdline/merge_reintegrate_tests.py	(revision 1232383)
+++ subversion/tests/cmdline/merge_reintegrate_tests.py	(working copy)
@@ -2541,7 +2541,229 @@ def reintegrate_replaced_source(sbox):
                                        expected_skip,
                                        [], None, None, None, None, True, True,
                                        '--reintegrate', A_path)
-  
+
+#----------------------------------------------------------------------
+
+def simple_reintegrate(sbox, source, target_wcpath):
+  """"""
+  was_cwd = os.getcwd()
+  os.chdir(sbox.wc_dir)
+  svntest.actions.run_and_verify_svn(None, None, [],
+                                     'merge', '--reintegrate',
+                                     source, target_wcpath)
+  os.chdir(was_cwd)
+
+def simple_sync_merge(sbox, source, target_wcpath):
+  """"""
+  was_cwd = os.getcwd()
+  os.chdir(sbox.wc_dir)
+  svntest.actions.run_and_verify_svn(None, None, [],
+                                     'merge',
+                                     source, target_wcpath)
+  os.chdir(was_cwd)
+
+@SkipUnless(server_has_mergeinfo)
+def reintegrate_keep_alive1(sbox):
+  "reintegrate with automatic keep-alive"
+
+  # Test the ability to keep using a branch after reintegrating it.
+  # Make a branch, reintegrate it to trunk, and then sync the branch without
+  # first doing the 'keep-alive dance' (a record-only merge) that was
+  # required in the past.
+
+  # Make A_COPY branch in r2, and do a few more commits to A in r3-6.
+  sbox.build()
+  wc_dir = sbox.wc_dir
+  expected_disk, expected_status = set_up_branch(sbox)
+
+  # Make a change on the branch
+  sbox.simple_mkdir('A_COPY/NewDir')
+  sbox.simple_commit()
+
+  # Sync the branch
+  sbox.simple_update()
+  simple_sync_merge(sbox, '^/A', 'A_COPY')
+  sbox.simple_commit()
+
+  # Reintegrate
+  simple_reintegrate(sbox, '^/A_COPY', 'A')
+  sbox.simple_commit()
+
+  # Try to sync the branch again, without first doing a keep-alive dance.
+  # If the 'keep alive' works automatically then there should be no
+  # conflicts. In fact in this case there should be no change at all
+  # because it is already up to date and the reintegrate commit that
+  # happened on the trunk should be ignored.
+  sbox.simple_update()
+  simple_sync_merge(sbox, '^/A', 'A_COPY')
+
+@SkipUnless(server_has_mergeinfo)
+def reintegrate_keep_alive2(sbox):
+  "reintegrate with automatic keep-alive"
+
+  # Test the ability to keep using a branch after reintegrating it.
+  #
+  # Like reintegrate_keep_alive1(), but with more complex merges involving
+  # branches other than the two principal branches. Specifically, changes on
+  # 'trunk' (A) will include changes made in a temporary side-branch
+  # (SIDE_BRANCHn) off A, and changes on the child branch (A_COPY) will
+  # include changes made in a temporary side-branch (FEATn) off A_COPY.
+
+  # Make a branch A_COPY in r2, and then a few more commits to A, ending at r7.
+  # Also make a branch A_COPY_2 in r3, which we'll use as a short-term feature branch.
+  sbox.build()
+  wc_dir = sbox.wc_dir
+  expected_disk, expected_status = set_up_branch(sbox)
+
+  def change_via_side_branch(branch, side_branch):
+    """Work on the branch BRANCH, through a short-term feature branch named
+       SIDE_BRANCH."""
+    sbox.simple_repo_copy(branch, side_branch)
+    sbox.simple_update()
+    sbox.simple_mkdir(side_branch + '/NewVia' + side_branch)
+    sbox.simple_commit(side_branch)
+    simple_reintegrate(sbox, '^/' + side_branch, branch)
+    sbox.simple_commit()
+
+  # Work on the branch
+  sbox.simple_mkdir('A_COPY/BranchFix1')
+  sbox.simple_commit()
+  change_via_side_branch('A_COPY', 'BRANCH_DEV1')
+
+  # Work on trunk
+  sbox.simple_mkdir('A/TrunkFix1')
+  sbox.simple_commit()
+  change_via_side_branch('A', 'TRUNK_DEV1')
+
+  # Sync the branch
+  sbox.simple_update()
+  simple_sync_merge(sbox, '^/A', 'A_COPY')
+  sbox.simple_commit()
+
+  # Work on the branch
+  sbox.simple_mkdir('A_COPY/BranchFix2')
+  sbox.simple_commit()
+  change_via_side_branch('A_COPY', 'BRANCH_DEV2')
+
+  # Work on trunk (after the latest sync, but still allowed by reintegrate)
+  sbox.simple_mkdir('A/TrunkFix2')
+  sbox.simple_commit()
+  change_via_side_branch('A', 'TRUNK_DEV2')
+
+  # Reintegrate
+  sbox.simple_update()
+  simple_reintegrate(sbox, '^/A_COPY', 'A')
+  sbox.simple_commit()
+
+  # Work on the branch
+  sbox.simple_mkdir('A_COPY/BranchFix3')
+  sbox.simple_commit()
+  change_via_side_branch('A_COPY', 'BRANCH_DEV3')
+
+  # Work on trunk
+  sbox.simple_mkdir('A/TrunkFix3')
+  sbox.simple_commit()
+  change_via_side_branch('A', 'TRUNK_DEV3')
+
+  # Try to sync the branch again, without first doing a keep-alive dance.
+  # Subversion should automatically skip the reintegrate merge revisions and
+  # merge the other changes (TrunkFix2, TRUNK_DEV2, TrunkFix3, TRUNK_DEV3)
+  # with no conflicts.
+  sbox.simple_update()
+  simple_sync_merge(sbox, '^/A', 'A_COPY')
+
+@SkipUnless(server_has_mergeinfo)
+def reintegrate_keep_alive3(sbox):
+  "reintegrate with automatic keep-alive 3"
+
+  sbox.build()
+  wc_dir = sbox.wc_dir
+
+
+  gamma_COPY_path = os.path.join(sbox.wc_dir, "A_COPY", "D", "gamma")
+  chi_COPY_2_path = os.path.join(sbox.wc_dir, "A_COPY_2", "D", "H", "chi")
+  A_COPY_2_path   = os.path.join(sbox.wc_dir, "A_COPY_2")
+  A_path          = os.path.join(sbox.wc_dir, "A")
+  A_COPY_path     = os.path.join(sbox.wc_dir, "A_COPY")
+
+  # A@1---r4---r5---r6---r7----------------r11----------->
+  #  |\                                     ^          |
+  #  | \                                    |          |
+  #  |  \                              reintegrate     |
+  #  |   V                                  |          |
+  #  |   A_COPY_2-----------------r9---r10---          |
+  #  |                            ^                sync merge
+  #  |                           /                     |
+  #  |                  cherry-pick merge of r8        |
+  #  V                         /                       V
+  #  A_COPY-------------------r8------------------------->
+  #
+  #
+  # Make a branch A_COPY in r2, and A_COPY_2 in r3, and then a few more
+  # commits to A in r4 through r7.
+  expected_disk, expected_status = set_up_branch(sbox, nbr_of_branches=2)
+
+  # r8 - Make an edit on the first branch to A_COPY/D/gamma
+  svntest.main.file_write(gamma_COPY_path, "Branch 1 edit.\n")
+  svntest.main.run_svn(None, "ci", "-m", "Branch 1 edit", wc_dir)
+
+  # r9 - Cherry pick r8 from ^/A_COPY to A_COPY_2
+  svntest.actions.run_and_verify_svn(None, None, [], 'merge',
+                                     sbox.repo_url + '/A_COPY',
+                                     A_COPY_2_path, '-c8')
+  svntest.main.run_svn(None, "ci", "-m",
+                       "Cherry pick r8 from A_COPY to A_COPY_2", wc_dir)
+
+  # r10 - Make an edit on the second branch to A_COPY_2/D/H/chi
+  svntest.main.file_write(chi_COPY_2_path, "Branch 2 edit.\n")
+  svntest.main.run_svn(None, "ci", "-m", "Branch 2 edit", wc_dir)
+
+  # r11 - Reintegrate A_COPY_2 to A.
+  svntest.actions.run_and_verify_svn(None, None, [], 'up', wc_dir)
+  svntest.actions.run_and_verify_svn(None, None, [], 'merge',
+                                     sbox.repo_url + '/A_COPY_2',
+                                     A_path, '--reintegrate')
+  svntest.main.run_svn(None, "ci", "-m", "Reintegrate A_COPY_2 back to A",
+                       wc_dir)
+
+  # Now try to sync merge ^/A to A_COPY.
+  #
+  # Revision r11 is skipped so the change from r10 never makes it into A_COPY.
+  #
+  # >svn merge ^^/A A_COPY
+  # DBG: merge.c:8464: r1:11 mi added:
+  # DBG:   /A_COPY:8
+  # DBG: merge.c:8464: r6:11 mi added:
+  # DBG:   /A_COPY:8
+  # DBG: merge.c:8464: r8:11 mi added:
+  # DBG:   /A_COPY:8
+  # DBG: merge.c:8464: r9:11 mi added:
+  # DBG:   /A_COPY:8
+  # DBG: merge.c:8464: r10:11 mi added:
+  # DBG:   /A_COPY:8
+  # DBG: merge.c:8633: Skipping reflective revision r11
+  # --- Merging r2 through r10 into 'A_COPY':
+  # U    A_COPY\B\E\beta
+  # U    A_COPY\D\G\rho
+  # U    A_COPY\D\H\omega
+  # U    A_COPY\D\H\psi
+  # --- Recording mergeinfo for merge of r2 through r11 into 'A_COPY':
+  #  U   A_COPY
+  svntest.actions.run_and_verify_svn(None, None, [], 'up', wc_dir)
+  svntest.actions.run_and_verify_svn(None, None, [], 'merge',
+                                     sbox.repo_url + '/A', A_COPY_path)
+
+  # And what's more worrisome is that if we commit this sync merge and later
+  # reintegrate ^/A_COPY back to A, the change in r10 is removed from A
+  #
+  #
+  ###svntest.main.run_svn(None, "ci", "-m", "Sync A to A_COPY",
+  ###                     wc_dir)
+  ###svntest.actions.run_and_verify_svn(None, None, [], 'merge',
+  ###                                   sbox.repo_url + '/A_COPY',
+  ###                                   A_path, '--reintegrate')
+
+
 ########################################################################
 # Run the tests
 
@@ -2565,6 +2787,9 @@ test_list = [ None,
               reintegrate_creates_bogus_mergeinfo,
               no_source_subtree_mergeinfo,
               reintegrate_replaced_source,
+              reintegrate_keep_alive1,
+              reintegrate_keep_alive2,
+              reintegrate_keep_alive3,
              ]
 
 if __name__ == '__main__':
