The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
/*
 * mergeinfo.c :  merge history functions for the libsvn_client library
 *
 * ====================================================================
 *    Licensed to the Apache Software Foundation (ASF) under one
 *    or more contributor license agreements.  See the NOTICE file
 *    distributed with this work for additional information
 *    regarding copyright ownership.  The ASF licenses this file
 *    to you under the Apache License, Version 2.0 (the
 *    "License"); you may not use this file except in compliance
 *    with the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing,
 *    software distributed under the License is distributed on an
 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *    KIND, either express or implied.  See the License for the
 *    specific language governing permissions and limitations
 *    under the License.
 * ====================================================================
 */

#include <apr_pools.h>
#include <apr_strings.h>

#include "svn_pools.h"
#include "svn_dirent_uri.h"
#include "svn_path.h"
#include "svn_string.h"
#include "svn_opt.h"
#include "svn_error.h"
#include "svn_error_codes.h"
#include "svn_props.h"
#include "svn_mergeinfo.h"
#include "svn_sorts.h"
#include "svn_ra.h"
#include "svn_client.h"
#include "svn_hash.h"

#include "private/svn_opt_private.h"
#include "private/svn_mergeinfo_private.h"
#include "private/svn_wc_private.h"
#include "private/svn_ra_private.h"
#include "private/svn_fspath.h"
#include "private/svn_client_private.h"
#include "client.h"
#include "mergeinfo.h"
#include "svn_private_config.h"



svn_client__merge_path_t *
svn_client__merge_path_dup(const svn_client__merge_path_t *old,
                           apr_pool_t *pool)
{
  svn_client__merge_path_t *new = apr_pmemdup(pool, old, sizeof(*old));

  new->abspath = apr_pstrdup(pool, old->abspath);
  if (new->remaining_ranges)
    new->remaining_ranges = svn_rangelist_dup(old->remaining_ranges, pool);
  if (new->pre_merge_mergeinfo)
    new->pre_merge_mergeinfo = svn_mergeinfo_dup(old->pre_merge_mergeinfo,
                                                 pool);
  if (new->implicit_mergeinfo)
    new->implicit_mergeinfo = svn_mergeinfo_dup(old->implicit_mergeinfo,
                                                pool);

  return new;
}

svn_client__merge_path_t *
svn_client__merge_path_create(const char *abspath,
                              apr_pool_t *pool)
{
  svn_client__merge_path_t *result = apr_pcalloc(pool, sizeof(*result));

  result->abspath = apr_pstrdup(pool, abspath);
  return result;
}

svn_error_t *
svn_client__parse_mergeinfo(svn_mergeinfo_t *mergeinfo,
                            svn_wc_context_t *wc_ctx,
                            const char *local_abspath,
                            apr_pool_t *result_pool,
                            apr_pool_t *scratch_pool)
{
  const svn_string_t *propval;

  *mergeinfo = NULL;

  /* ### Use svn_wc_prop_get() would actually be sufficient for now.
     ### DannyB thinks that later we'll need behavior more like
     ### svn_client__get_prop_from_wc(). */
  SVN_ERR(svn_wc_prop_get2(&propval, wc_ctx, local_abspath, SVN_PROP_MERGEINFO,
                           scratch_pool, scratch_pool));
  if (propval)
    SVN_ERR(svn_mergeinfo_parse(mergeinfo, propval->data, result_pool));

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client__record_wc_mergeinfo(const char *local_abspath,
                                svn_mergeinfo_t mergeinfo,
                                svn_boolean_t do_notification,
                                svn_client_ctx_t *ctx,
                                apr_pool_t *scratch_pool)
{
  svn_string_t *mergeinfo_str = NULL;
  svn_boolean_t mergeinfo_changes = FALSE;

  SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));

  /* Convert MERGEINFO (if any) into text for storage as a property value. */
  if (mergeinfo)
    SVN_ERR(svn_mergeinfo_to_string(&mergeinfo_str, mergeinfo, scratch_pool));

  if (do_notification && ctx->notify_func2)
    SVN_ERR(svn_client__mergeinfo_status(&mergeinfo_changes, ctx->wc_ctx,
                                         local_abspath, scratch_pool));

  /* Record the new mergeinfo in the WC. */
  /* ### Later, we'll want behavior more analogous to
     ### svn_client__get_prop_from_wc(). */
  SVN_ERR(svn_wc_prop_set4(ctx->wc_ctx, local_abspath, SVN_PROP_MERGEINFO,
                           mergeinfo_str, svn_depth_empty,
                           TRUE /* skip checks */, NULL,
                           NULL, NULL /* cancellation */,
                           NULL, NULL /* notification */,
                           scratch_pool));

  if (do_notification && ctx->notify_func2)
    {
      svn_wc_notify_t *notify =
        svn_wc_create_notify(local_abspath,
                             svn_wc_notify_merge_record_info,
                             scratch_pool);
      if (mergeinfo_changes)
        notify->prop_state = svn_wc_notify_state_merged;
      else
        notify->prop_state = svn_wc_notify_state_changed;

      ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client__record_wc_mergeinfo_catalog(apr_hash_t *result_catalog,
                                        svn_client_ctx_t *ctx,
                                        apr_pool_t *scratch_pool)
{
  apr_pool_t *iterpool = svn_pool_create(scratch_pool);

  if (apr_hash_count(result_catalog))
    {
      int i;
      apr_array_header_t *sorted_cat =
        svn_sort__hash(result_catalog, svn_sort_compare_items_as_paths,
                       scratch_pool);

      /* Write the mergeinfo out in sorted order of the paths (presumably just
       * so that the notifications are in a predictable, convenient order). */
      for (i = 0; i < sorted_cat->nelts; i++)
        {
          svn_sort__item_t elt = APR_ARRAY_IDX(sorted_cat, i,
                                               svn_sort__item_t);
          svn_error_t *err;

          svn_pool_clear(iterpool);
          err = svn_client__record_wc_mergeinfo(elt.key, elt.value, TRUE,
                                                ctx, iterpool);

          if (err && err->apr_err == SVN_ERR_ENTRY_NOT_FOUND)
            {
              /* PATH isn't just missing, it's not even versioned as far
                 as this working copy knows.  But it was included in
                 MERGES, which means that the server knows about it.
                 Likely we don't have access to the source due to authz
                 restrictions.  For now just clear the error and
                 continue... */
              svn_error_clear(err);
            }
          else
            {
              SVN_ERR(err);
            }
        }
    }
  svn_pool_destroy(iterpool);
  return SVN_NO_ERROR;
}

/*-----------------------------------------------------------------------*/

/*** Retrieving mergeinfo. ***/

svn_error_t *
svn_client__get_wc_mergeinfo(svn_mergeinfo_t *mergeinfo,
                             svn_boolean_t *inherited_p,
                             svn_mergeinfo_inheritance_t inherit,
                             const char *local_abspath,
                             const char *limit_abspath,
                             const char **walked_path,
                             svn_boolean_t ignore_invalid_mergeinfo,
                             svn_client_ctx_t *ctx,
                             apr_pool_t *result_pool,
                             apr_pool_t *scratch_pool)
{
  const char *walk_relpath = "";
  svn_mergeinfo_t wc_mergeinfo;
  svn_revnum_t base_revision;
  apr_pool_t *iterpool;
  svn_boolean_t inherited;

  SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
  if (limit_abspath)
    SVN_ERR_ASSERT(svn_dirent_is_absolute(limit_abspath));

  SVN_ERR(svn_wc__node_get_base(NULL, &base_revision, NULL, NULL, NULL, NULL,
                                ctx->wc_ctx, local_abspath,
                                TRUE /* ignore_enoent */,
                                FALSE /* show_hidden */,
                                scratch_pool, scratch_pool));

  iterpool = svn_pool_create(scratch_pool);
  while (TRUE)
    {
      svn_pool_clear(iterpool);

      /* Don't look for explicit mergeinfo on LOCAL_ABSPATH if we are only
         interested in inherited mergeinfo. */
      if (inherit == svn_mergeinfo_nearest_ancestor)
        {
          wc_mergeinfo = NULL;
          inherit = svn_mergeinfo_inherited;
        }
      else
        {
          /* Look for mergeinfo on LOCAL_ABSPATH.  If there isn't any and we
             want inherited mergeinfo, walk towards the root of the WC until
             we encounter either (a) an unversioned directory, or
             (b) mergeinfo.  If we encounter (b), use that inherited
             mergeinfo as our baseline. */
          svn_error_t *err = svn_client__parse_mergeinfo(&wc_mergeinfo,
                                                         ctx->wc_ctx,
                                                         local_abspath,
                                                         result_pool,
                                                         iterpool);
          if ((ignore_invalid_mergeinfo || walk_relpath [0] != '\0')
              && err
              && err->apr_err == SVN_ERR_MERGEINFO_PARSE_ERROR)
            {
              svn_error_clear(err);
              wc_mergeinfo = apr_hash_make(result_pool);
              break;
            }
          else
            {
              SVN_ERR(err);
            }
        }

      if (wc_mergeinfo == NULL &&
          inherit != svn_mergeinfo_explicit &&
          !svn_dirent_is_root(local_abspath, strlen(local_abspath)))
        {
          svn_boolean_t is_wc_root;
          svn_boolean_t is_switched;
          svn_revnum_t parent_base_rev;
          svn_revnum_t parent_changed_rev;

          /* Don't look any higher than the limit path. */
          if (limit_abspath && strcmp(limit_abspath, local_abspath) == 0)
            break;

          /* If we've reached the root of the working copy don't look any
             higher. */
          SVN_ERR(svn_wc_check_root(&is_wc_root, &is_switched, NULL,
                                    ctx->wc_ctx, local_abspath, iterpool));
          if (is_wc_root || is_switched)
            break;

          /* No explicit mergeinfo on this path.  Look higher up the
             directory tree while keeping track of what we've walked. */
          walk_relpath = svn_relpath_join(svn_dirent_basename(local_abspath,
                                                              iterpool),
                                          walk_relpath, result_pool);
          local_abspath = svn_dirent_dirname(local_abspath, scratch_pool);

          SVN_ERR(svn_wc__node_get_base(NULL, &parent_base_rev, NULL, NULL,
                                        NULL, NULL,
                                        ctx->wc_ctx, local_abspath,
                                        TRUE, FALSE,
                                        scratch_pool, scratch_pool));

          /* ### This checks the WORKING changed_rev, so invalid on replacement
             ### not even reliable in case an ancestor was copied from a
             ### different location */
          SVN_ERR(svn_wc__node_get_changed_info(&parent_changed_rev,
                                                NULL, NULL,
                                                ctx->wc_ctx, local_abspath,
                                                scratch_pool,
                                                scratch_pool));

          /* Look in LOCAL_ABSPATH's parent for inherited mergeinfo if
             LOCAL_ABSPATH has no base revision because it is an uncommitted
             addition, or if its base revision falls within the inclusive
             range of its parent's last changed revision to the parent's base
             revision; otherwise stop looking for inherited mergeinfo. */
          if (SVN_IS_VALID_REVNUM(base_revision)
              && (base_revision < parent_changed_rev
                  || parent_base_rev < base_revision))
            break;

          /* We haven't yet risen above the root of the WC. */
          continue;
        }
      break;
    }

  svn_pool_destroy(iterpool);

  if (svn_path_is_empty(walk_relpath))
    {
      /* Mergeinfo is explicit. */
      inherited = FALSE;
      *mergeinfo = wc_mergeinfo;
    }
  else
    {
      /* Mergeinfo may be inherited. */
      if (wc_mergeinfo)
        {
          inherited = TRUE;
          SVN_ERR(svn_mergeinfo__add_suffix_to_mergeinfo(mergeinfo,
                                                         wc_mergeinfo,
                                                         walk_relpath,
                                                         result_pool,
                                                         scratch_pool));
        }
      else
        {
          inherited = FALSE;
          *mergeinfo = NULL;
        }
    }

  if (walked_path)
    *walked_path = walk_relpath;

  /* Remove non-inheritable mergeinfo and paths mapped to empty ranges
     which may occur if WCPATH's mergeinfo is not explicit. */
  if (inherited
      && apr_hash_count(*mergeinfo)) /* Nothing to do for empty mergeinfo. */
    {
      SVN_ERR(svn_mergeinfo_inheritable2(mergeinfo, *mergeinfo, NULL,
                                         SVN_INVALID_REVNUM, SVN_INVALID_REVNUM,
                                         TRUE, result_pool, scratch_pool));
      svn_mergeinfo__remove_empty_rangelists(*mergeinfo, result_pool);
    }

  if (inherited_p)
    *inherited_p = inherited;

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client__get_wc_mergeinfo_catalog(svn_mergeinfo_catalog_t *mergeinfo_cat,
                                     svn_boolean_t *inherited,
                                     svn_boolean_t include_descendants,
                                     svn_mergeinfo_inheritance_t inherit,
                                     const char *local_abspath,
                                     const char *limit_path,
                                     const char **walked_path,
                                     svn_boolean_t ignore_invalid_mergeinfo,
                                     svn_client_ctx_t *ctx,
                                     apr_pool_t *result_pool,
                                     apr_pool_t *scratch_pool)
{
  const char *target_repos_relpath;
  svn_mergeinfo_t mergeinfo;
  const char *repos_root;

  SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
  *mergeinfo_cat = NULL;
  SVN_ERR(svn_wc__node_get_repos_info(NULL, &target_repos_relpath,
                                      &repos_root, NULL,
                                      ctx->wc_ctx, local_abspath,
                                      scratch_pool, scratch_pool));

  /* Get the mergeinfo for the LOCAL_ABSPATH target and set *INHERITED and
     *WALKED_PATH. */
  SVN_ERR(svn_client__get_wc_mergeinfo(&mergeinfo, inherited, inherit,
                                       local_abspath, limit_path,
                                       walked_path, ignore_invalid_mergeinfo,
                                       ctx, result_pool, scratch_pool));

  /* Add any explicit/inherited mergeinfo for LOCAL_ABSPATH to
     *MERGEINFO_CAT. */
  if (mergeinfo)
    {
      *mergeinfo_cat = apr_hash_make(result_pool);
      svn_hash_sets(*mergeinfo_cat,
                    apr_pstrdup(result_pool, target_repos_relpath), mergeinfo);
    }

  /* If LOCAL_ABSPATH is a directory and we want the subtree mergeinfo too,
     then get it.

     With WC-NG it is cheaper to do a single db transaction, than first
     looking if we really have a directory. */
  if (include_descendants)
    {
      apr_hash_t *mergeinfo_props;
      apr_hash_index_t *hi;

      SVN_ERR(svn_wc__prop_retrieve_recursive(&mergeinfo_props,
                                              ctx->wc_ctx, local_abspath,
                                              SVN_PROP_MERGEINFO,
                                              scratch_pool, scratch_pool));

      /* Convert *mergeinfo_props into a proper svn_mergeinfo_catalog_t */
      for (hi = apr_hash_first(scratch_pool, mergeinfo_props);
           hi;
           hi = apr_hash_next(hi))
        {
          const char *node_abspath = svn__apr_hash_index_key(hi);
          svn_string_t *propval = svn__apr_hash_index_val(hi);
          svn_mergeinfo_t subtree_mergeinfo;
          const char *repos_relpath;

          if (strcmp(node_abspath, local_abspath) == 0)
            continue; /* Already parsed in svn_client__get_wc_mergeinfo */

          SVN_ERR(svn_wc__node_get_repos_info(NULL, &repos_relpath, NULL, NULL,
                                              ctx->wc_ctx, node_abspath,
                                              result_pool, scratch_pool));

          SVN_ERR(svn_mergeinfo_parse(&subtree_mergeinfo, propval->data,
                                      result_pool));

          /* If the target had no explicit/inherited mergeinfo and this is the
             first subtree with mergeinfo found, then the catalog will still
             be NULL. */
          if (*mergeinfo_cat == NULL)
            *mergeinfo_cat = apr_hash_make(result_pool);

          svn_hash_sets(*mergeinfo_cat, repos_relpath, subtree_mergeinfo);
        }
    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client__get_repos_mergeinfo(svn_mergeinfo_t *target_mergeinfo,
                                svn_ra_session_t *ra_session,
                                const char *url,
                                svn_revnum_t rev,
                                svn_mergeinfo_inheritance_t inherit,
                                svn_boolean_t squelch_incapable,
                                apr_pool_t *pool)
{
  svn_mergeinfo_catalog_t tgt_mergeinfo_cat;

  *target_mergeinfo = NULL;

  SVN_ERR(svn_client__get_repos_mergeinfo_catalog(&tgt_mergeinfo_cat,
                                                  ra_session,
                                                  url, rev, inherit,
                                                  squelch_incapable, FALSE,
                                                  pool, pool));

  if (tgt_mergeinfo_cat && apr_hash_count(tgt_mergeinfo_cat))
    {
      /* We asked only for the REL_PATH's mergeinfo, not any of its
         descendants.  So if there is anything in the catalog it is the
         mergeinfo for REL_PATH. */
      *target_mergeinfo =
        svn__apr_hash_index_val(apr_hash_first(pool, tgt_mergeinfo_cat));

    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client__get_repos_mergeinfo_catalog(svn_mergeinfo_catalog_t *mergeinfo_cat,
                                        svn_ra_session_t *ra_session,
                                        const char *url,
                                        svn_revnum_t rev,
                                        svn_mergeinfo_inheritance_t inherit,
                                        svn_boolean_t squelch_incapable,
                                        svn_boolean_t include_descendants,
                                        apr_pool_t *result_pool,
                                        apr_pool_t *scratch_pool)
{
  svn_error_t *err;
  svn_mergeinfo_catalog_t repos_mergeinfo_cat;
  apr_array_header_t *rel_paths = apr_array_make(scratch_pool, 1,
                                                 sizeof(const char *));
  const char *old_session_url;

  APR_ARRAY_PUSH(rel_paths, const char *) = "";

  /* Fetch the mergeinfo. */
  SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url,
                                            ra_session, url, scratch_pool));
  err = svn_ra_get_mergeinfo(ra_session, &repos_mergeinfo_cat, rel_paths,
                             rev, inherit, include_descendants, result_pool);
  err = svn_error_compose_create(
          err, svn_ra_reparent(ra_session, old_session_url, scratch_pool));
  if (err)
    {
      if (squelch_incapable && err->apr_err == SVN_ERR_UNSUPPORTED_FEATURE)
        {
          svn_error_clear(err);
          *mergeinfo_cat = NULL;
          return SVN_NO_ERROR;
        }
      else
        return svn_error_trace(err);
    }

  if (repos_mergeinfo_cat == NULL)
    {
      *mergeinfo_cat = NULL;
    }
  else
    {
      const char *session_relpath;

      SVN_ERR(svn_ra_get_path_relative_to_root(ra_session, &session_relpath,
                                               url, scratch_pool));

      if (session_relpath[0] == '\0')
        *mergeinfo_cat = repos_mergeinfo_cat;
      else
        SVN_ERR(svn_mergeinfo__add_prefix_to_catalog(mergeinfo_cat,
                                                     repos_mergeinfo_cat,
                                                     session_relpath,
                                                     result_pool,
                                                     scratch_pool));
    }
  return SVN_NO_ERROR;
}


svn_error_t *
svn_client__get_wc_or_repos_mergeinfo(svn_mergeinfo_t *target_mergeinfo,
                                      svn_boolean_t *inherited,
                                      svn_boolean_t *from_repos,
                                      svn_boolean_t repos_only,
                                      svn_mergeinfo_inheritance_t inherit,
                                      svn_ra_session_t *ra_session,
                                      const char *target_wcpath,
                                      svn_client_ctx_t *ctx,
                                      apr_pool_t *pool)
{
  svn_mergeinfo_catalog_t tgt_mergeinfo_cat;

  *target_mergeinfo = NULL;

  SVN_ERR(svn_client__get_wc_or_repos_mergeinfo_catalog(&tgt_mergeinfo_cat,
                                                        inherited, from_repos,
                                                        FALSE,
                                                        repos_only,
                                                        FALSE, inherit,
                                                        ra_session,
                                                        target_wcpath, ctx,
                                                        pool, pool));
  if (tgt_mergeinfo_cat && apr_hash_count(tgt_mergeinfo_cat))
    {
      /* We asked only for the TARGET_WCPATH's mergeinfo, not any of its
         descendants.  It this mergeinfo is in the catalog, it's keyed
         on TARGET_WCPATH's root-relative path.  We could dig that up
         so we can peek into our catalog, but it ought to be the only
         thing in the catalog, so we'll just fetch the first hash item. */
      *target_mergeinfo =
        svn__apr_hash_index_val(apr_hash_first(pool, tgt_mergeinfo_cat));

    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_client__get_wc_or_repos_mergeinfo_catalog(
  svn_mergeinfo_catalog_t *target_mergeinfo_catalog,
  svn_boolean_t *inherited_p,
  svn_boolean_t *from_repos,
  svn_boolean_t include_descendants,
  svn_boolean_t repos_only,
  svn_boolean_t ignore_invalid_mergeinfo,
  svn_mergeinfo_inheritance_t inherit,
  svn_ra_session_t *ra_session,
  const char *target_wcpath,
  svn_client_ctx_t *ctx,
  apr_pool_t *result_pool,
  apr_pool_t *scratch_pool)
{
  const char *url;
  svn_revnum_t target_rev;
  const char *local_abspath;
  const char *repos_root;
  const char *repos_relpath;
  svn_mergeinfo_catalog_t target_mergeinfo_cat_wc = NULL;
  svn_mergeinfo_catalog_t target_mergeinfo_cat_repos = NULL;

  SVN_ERR(svn_dirent_get_absolute(&local_abspath, target_wcpath,
                                  scratch_pool));

  if (from_repos)
    *from_repos = FALSE;

  /* We may get an entry with abbreviated information from TARGET_WCPATH's
     parent if TARGET_WCPATH is missing.  These limited entries do not have
     a URL and without that we cannot get accurate mergeinfo for
     TARGET_WCPATH. */
  SVN_ERR(svn_wc__node_get_origin(NULL, &target_rev, &repos_relpath,
                                  &repos_root, NULL, NULL,
                                  ctx->wc_ctx, local_abspath, FALSE,
                                  scratch_pool, scratch_pool));

  if (repos_relpath)
    url = svn_path_url_add_component2(repos_root, repos_relpath, scratch_pool);
  else
    url = NULL;

  if (!repos_only)
    {
      svn_boolean_t inherited;
      SVN_ERR(svn_client__get_wc_mergeinfo_catalog(&target_mergeinfo_cat_wc,
                                                   &inherited,
                                                   include_descendants,
                                                   inherit,
                                                   local_abspath,
                                                   NULL, NULL,
                                                   ignore_invalid_mergeinfo,
                                                   ctx,
                                                   result_pool,
                                                   scratch_pool));
      if (inherited_p)
        *inherited_p = inherited;

      /* If we want LOCAL_ABSPATH's inherited mergeinfo, were we able to
         get it from the working copy?  If not, then we must ask the
         repository. */
      if (! (inherited
             || (inherit == svn_mergeinfo_explicit)
             || (repos_relpath
                 && target_mergeinfo_cat_wc
                 && svn_hash_gets(target_mergeinfo_cat_wc, repos_relpath))))
        {
          repos_only = TRUE;
          /* We already have any subtree mergeinfo from the working copy, no
             need to ask the server for it again. */
          include_descendants = FALSE;
        }
    }

  if (repos_only)
    {
      /* No need to check the repos if this is a local addition. */
      if (url != NULL)
        {
          apr_hash_t *original_props;

          /* Check to see if we have local modifications which removed all of
             TARGET_WCPATH's pristine mergeinfo.  If that is the case then
             TARGET_WCPATH effectively has no mergeinfo. */
          SVN_ERR(svn_wc_get_pristine_props(&original_props,
                                            ctx->wc_ctx, local_abspath,
                                            result_pool, scratch_pool));
          if (!svn_hash_gets(original_props, SVN_PROP_MERGEINFO))
            {
              apr_pool_t *sesspool = NULL;

              if (! ra_session)
                {
                  sesspool = svn_pool_create(scratch_pool);
                  SVN_ERR(svn_client_open_ra_session2(&ra_session, url, NULL,
                                                      ctx,
                                                      sesspool, sesspool));
                }

              SVN_ERR(svn_client__get_repos_mergeinfo_catalog(
                        &target_mergeinfo_cat_repos, ra_session,
                        url, target_rev, inherit,
                        TRUE, include_descendants,
                        result_pool, scratch_pool));

              if (target_mergeinfo_cat_repos
                  && svn_hash_gets(target_mergeinfo_cat_repos, repos_relpath))
                {
                  if (inherited_p)
                    *inherited_p = TRUE;
                  if (from_repos)
                    *from_repos = TRUE;
                }

              /* If we created an RA_SESSION above, destroy it.
                 Otherwise, if reparented an existing session, point
                 it back where it was when we were called. */
              if (sesspool)
                {
                  svn_pool_destroy(sesspool);
                }
            }
        }
    }

  /* Combine the mergeinfo from the working copy and repository as needed. */
  if (target_mergeinfo_cat_wc)
    {
      *target_mergeinfo_catalog = target_mergeinfo_cat_wc;
      if (target_mergeinfo_cat_repos)
        SVN_ERR(svn_mergeinfo_catalog_merge(*target_mergeinfo_catalog,
                                            target_mergeinfo_cat_repos,
                                            result_pool, scratch_pool));
    }
  else if (target_mergeinfo_cat_repos)
    {
      *target_mergeinfo_catalog = target_mergeinfo_cat_repos;
    }
  else
    {
      *target_mergeinfo_catalog = NULL;
    }

  return SVN_NO_ERROR;
}


svn_error_t *
svn_client__get_history_as_mergeinfo(svn_mergeinfo_t *mergeinfo_p,
                                      svn_boolean_t *has_rev_zero_history,
                                      const svn_client__pathrev_t *pathrev,
                                      svn_revnum_t range_youngest,
                                      svn_revnum_t range_oldest,
                                      svn_ra_session_t *ra_session,
                                      svn_client_ctx_t *ctx,
                                      apr_pool_t *pool)
{
  apr_array_header_t *segments;

  /* Fetch the location segments for our URL@PEG_REVNUM. */
  if (! SVN_IS_VALID_REVNUM(range_youngest))
    range_youngest = pathrev->rev;
  if (! SVN_IS_VALID_REVNUM(range_oldest))
    range_oldest = 0;

  SVN_ERR(svn_client__repos_location_segments(&segments, ra_session,
                                              pathrev->url, pathrev->rev,
                                              range_youngest, range_oldest,
                                              ctx, pool));

  if (has_rev_zero_history)
    {
      *has_rev_zero_history = FALSE;
        if (segments->nelts)
          {
            svn_location_segment_t *oldest_segment =
              APR_ARRAY_IDX(segments, 0, svn_location_segment_t *);
            if (oldest_segment->range_start == 0)
              *has_rev_zero_history = TRUE;
          }
    }

  SVN_ERR(svn_mergeinfo__mergeinfo_from_segments(mergeinfo_p, segments, pool));

  return SVN_NO_ERROR;
}


/*-----------------------------------------------------------------------*/

/*** Eliding mergeinfo. ***/

/* Given the mergeinfo (CHILD_MERGEINFO) for a path, and the
   mergeinfo of its nearest ancestor with mergeinfo (PARENT_MERGEINFO), compare
   CHILD_MERGEINFO to PARENT_MERGEINFO to see if the former elides to
   the latter, following the elision rules described in
   svn_client__elide_mergeinfo()'s docstring.  Set *ELIDES to whether
   or not CHILD_MERGEINFO is redundant.

   Note: This function assumes that PARENT_MERGEINFO is definitive;
   i.e. if it is NULL then the caller not only walked the entire WC
   looking for inherited mergeinfo, but queried the repository if none
   was found in the WC.  This is rather important since this function
   says empty mergeinfo should be elided if PARENT_MERGEINFO is NULL,
   and we don't want to do that unless we are *certain* that the empty
   mergeinfo on PATH isn't overriding anything.

   If PATH_SUFFIX and PARENT_MERGEINFO are not NULL append PATH_SUFFIX
   to each path in PARENT_MERGEINFO before performing the comparison. */
static svn_error_t *
should_elide_mergeinfo(svn_boolean_t *elides,
                       svn_mergeinfo_t parent_mergeinfo,
                       svn_mergeinfo_t child_mergeinfo,
                       const char *path_suffix,
                       apr_pool_t *scratch_pool)
{
  /* Easy out: No child mergeinfo to elide. */
  if (child_mergeinfo == NULL)
    {
      *elides = FALSE;
    }
  else if (apr_hash_count(child_mergeinfo) == 0)
    {
      /* Empty mergeinfo elides to empty mergeinfo or to "nothing",
         i.e. it isn't overriding any parent. Otherwise it doesn't
         elide. */
      *elides = (!parent_mergeinfo || apr_hash_count(parent_mergeinfo) == 0);
    }
  else if (!parent_mergeinfo || apr_hash_count(parent_mergeinfo) == 0)
    {
      /* Non-empty mergeinfo never elides to empty mergeinfo
         or no mergeinfo. */
      *elides = FALSE;
    }
  else
    {
      /* Both CHILD_MERGEINFO and PARENT_MERGEINFO are non-NULL and
         non-empty. */
      svn_mergeinfo_t path_tweaked_parent_mergeinfo;

      /* If we need to adjust the paths in PARENT_MERGEINFO do it now. */
      if (path_suffix)
        SVN_ERR(svn_mergeinfo__add_suffix_to_mergeinfo(
                  &path_tweaked_parent_mergeinfo, parent_mergeinfo,
                  path_suffix, scratch_pool, scratch_pool));
      else
        path_tweaked_parent_mergeinfo = parent_mergeinfo;

      SVN_ERR(svn_mergeinfo__equals(elides,
                                    path_tweaked_parent_mergeinfo,
                                    child_mergeinfo, TRUE, scratch_pool));
    }

  return SVN_NO_ERROR;
}

/* Helper for svn_client__elide_mergeinfo().

   Given a working copy LOCAL_ABSPATH, its mergeinfo hash CHILD_MERGEINFO, and
   the mergeinfo of LOCAL_ABSPATH's nearest ancestor PARENT_MERGEINFO, use
   should_elide_mergeinfo() to decide whether or not CHILD_MERGEINFO elides to
   PARENT_MERGEINFO; PATH_SUFFIX means the same as in that function.

   If elision does occur, then remove the mergeinfo for LOCAL_ABSPATH.

   If CHILD_MERGEINFO is NULL, do nothing.

   Use SCRATCH_POOL for temporary allocations.
*/
static svn_error_t *
elide_mergeinfo(svn_mergeinfo_t parent_mergeinfo,
                svn_mergeinfo_t child_mergeinfo,
                const char *local_abspath,
                svn_client_ctx_t *ctx,
                apr_pool_t *scratch_pool)
{
  svn_boolean_t elides;

  SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));

  SVN_ERR(should_elide_mergeinfo(&elides,
                                 parent_mergeinfo, child_mergeinfo, NULL,
                                 scratch_pool));

  if (elides)
    {
      SVN_ERR(svn_wc_prop_set4(ctx->wc_ctx, local_abspath, SVN_PROP_MERGEINFO,
                               NULL, svn_depth_empty, TRUE, NULL,
                               NULL, NULL /* cancellation */,
                               NULL, NULL /* notification */,
                               scratch_pool));

      if (ctx->notify_func2)
        {
          svn_wc_notify_t *notify;

          notify = svn_wc_create_notify(local_abspath,
                                        svn_wc_notify_merge_elide_info,
                                        scratch_pool);
          ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);

          notify = svn_wc_create_notify(local_abspath,
                                        svn_wc_notify_update_update,
                                        scratch_pool);
          notify->prop_state = svn_wc_notify_state_changed;

          ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
        }
    }

  return SVN_NO_ERROR;
}


svn_error_t *
svn_client__elide_mergeinfo(const char *target_abspath,
                            const char *wc_elision_limit_abspath,
                            svn_client_ctx_t *ctx,
                            apr_pool_t *pool)
{
  const char *limit_abspath = wc_elision_limit_abspath;

  SVN_ERR_ASSERT(svn_dirent_is_absolute(target_abspath));
  SVN_ERR_ASSERT(!wc_elision_limit_abspath || svn_dirent_is_absolute(wc_elision_limit_abspath));

  /* Check for first easy out: We are already at the limit path. */
  if (!limit_abspath
      || strcmp(target_abspath, limit_abspath) != 0)
    {
      svn_mergeinfo_t target_mergeinfo;
      svn_mergeinfo_t mergeinfo = NULL;
      svn_boolean_t inherited;
      const char *walk_path;
      svn_error_t *err;

      /* Get the TARGET_WCPATH's explicit mergeinfo. */
      err = svn_client__get_wc_mergeinfo(&target_mergeinfo, &inherited,
                                         svn_mergeinfo_inherited,
                                         target_abspath,
                                         limit_abspath,
                                         &walk_path, FALSE,
                                         ctx, pool, pool);
      if (err)
        {
          if (err->apr_err == SVN_ERR_MERGEINFO_PARSE_ERROR)
            {
              /* Issue #3896: If we attempt elision because invalid
                 mergeinfo is present on TARGET_WCPATH, then don't let
                 the merge fail, just skip the elision attempt. */
              svn_error_clear(err);
              return SVN_NO_ERROR;
            }
          else
            {
              return svn_error_trace(err);
            }
        }

     /* If TARGET_WCPATH has no explicit mergeinfo, there's nothing to
         elide, we're done. */
      if (inherited || target_mergeinfo == NULL)
        return SVN_NO_ERROR;

      /* Get TARGET_WCPATH's inherited mergeinfo from the WC. */
      err = svn_client__get_wc_mergeinfo(&mergeinfo, NULL,
                                         svn_mergeinfo_nearest_ancestor,
                                         target_abspath,
                                         limit_abspath,
                                         &walk_path, FALSE, ctx, pool, pool);
      if (err)
        {
          if (err->apr_err == SVN_ERR_MERGEINFO_PARSE_ERROR)
            {
              /* Issue #3896 again, but invalid mergeinfo is inherited. */
              svn_error_clear(err);
              return SVN_NO_ERROR;
            }
          else
            {
              return svn_error_trace(err);
            }
        }

      /* If TARGET_WCPATH inherited no mergeinfo from the WC and we are
         not limiting our search to the working copy then check if it
         inherits any from the repos. */
      if (!mergeinfo && !wc_elision_limit_abspath)
        {
          err = svn_client__get_wc_or_repos_mergeinfo(
            &mergeinfo, NULL, NULL, TRUE,
            svn_mergeinfo_nearest_ancestor,
            NULL, target_abspath, ctx, pool);
          if (err)
            {
              if (err->apr_err == SVN_ERR_MERGEINFO_PARSE_ERROR)
                {
                  /* Issue #3896 again, but invalid mergeinfo is inherited
                     from the repository. */
                  svn_error_clear(err);
                  return SVN_NO_ERROR;
                }
              else
                {
                  return svn_error_trace(err);
                }
            }
        }

      /* If there is nowhere to elide TARGET_WCPATH's mergeinfo to and
         the elision is limited, then we are done.*/
      if (!mergeinfo && wc_elision_limit_abspath)
        return SVN_NO_ERROR;

      SVN_ERR(elide_mergeinfo(mergeinfo, target_mergeinfo, target_abspath,
                              ctx, pool));
    }
  return SVN_NO_ERROR;
}


/* Set *MERGEINFO_CATALOG to the explicit or inherited mergeinfo for
   PATH_OR_URL@PEG_REVISION.  If INCLUDE_DESCENDANTS is true, also
   store in *MERGEINFO_CATALOG the explicit mergeinfo on any subtrees
   under PATH_OR_URL.  Key all mergeinfo in *MERGEINFO_CATALOG on
   repository relpaths.

   If no mergeinfo is found then set *MERGEINFO_CATALOG to NULL.

   Set *REPOS_ROOT to the root URL of the repository associated with
   PATH_OR_URL.

   If RA_SESSION is NOT NULL and PATH_OR_URL refers to a URL, RA_SESSION
   (which must be of the repository containing PATH_OR_URL) will be used
   instead of a temporary RA session. Caller is responsible for reparenting
   the session if it wants to use it after the call.

   Allocate *MERGEINFO_CATALOG and all its contents in RESULT_POOL.  Use
   SCRATCH_POOL for all temporary allocations.

   Return SVN_ERR_UNSUPPORTED_FEATURE if the server does not support
   Merge Tracking.  */
static svn_error_t *
get_mergeinfo(svn_mergeinfo_catalog_t *mergeinfo_catalog,
              const char **repos_root,
              const char *path_or_url,
              const svn_opt_revision_t *peg_revision,
              svn_boolean_t include_descendants,
              svn_boolean_t ignore_invalid_mergeinfo,
              svn_client_ctx_t *ctx,
              svn_ra_session_t *ra_session,
              apr_pool_t *result_pool,
              apr_pool_t *scratch_pool)
{
  const char *local_abspath;
  svn_boolean_t use_url = svn_path_is_url(path_or_url);
  svn_client__pathrev_t *peg_loc;

  if (ra_session && svn_path_is_url(path_or_url))
    {
      SVN_ERR(svn_ra_reparent(ra_session, path_or_url, scratch_pool));
      SVN_ERR(svn_client__resolve_rev_and_url(&peg_loc, ra_session,
                                              path_or_url,
                                              peg_revision,
                                              peg_revision,
                                              ctx, scratch_pool));
    }
  else
    {
      SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &peg_loc,
                                                path_or_url, NULL,
                                                peg_revision,
                                                peg_revision, ctx, scratch_pool));
    }

  /* If PATH_OR_URL is as working copy path determine if we will need to
     contact the repository for the requested PEG_REVISION. */
  if (!use_url)
    {
      svn_client__pathrev_t *origin;
      SVN_ERR(svn_dirent_get_absolute(&local_abspath, path_or_url,
                                      scratch_pool));

      SVN_ERR(svn_client__wc_node_get_origin(&origin, local_abspath, ctx,
                                             scratch_pool, scratch_pool));
      if (!origin
          || strcmp(origin->url, peg_loc->url) != 0
          || peg_loc->rev != origin->rev)
      {
        use_url = TRUE; /* Don't rely on local mergeinfo */
      }
    }

  SVN_ERR(svn_ra_get_repos_root2(ra_session, repos_root, result_pool));

  if (use_url)
    {
      SVN_ERR(svn_client__get_repos_mergeinfo_catalog(
        mergeinfo_catalog, ra_session, peg_loc->url, peg_loc->rev,
        svn_mergeinfo_inherited, FALSE, include_descendants,
        result_pool, scratch_pool));
    }
  else /* ! svn_path_is_url() */
    {
      SVN_ERR(svn_client__get_wc_or_repos_mergeinfo_catalog(
        mergeinfo_catalog, NULL, NULL, include_descendants, FALSE,
        ignore_invalid_mergeinfo, svn_mergeinfo_inherited,
        ra_session, path_or_url, ctx,
        result_pool, scratch_pool));
    }

  return SVN_NO_ERROR;
}

/*** In-memory mergeinfo elision ***/
svn_error_t *
svn_client__elide_mergeinfo_catalog(svn_mergeinfo_catalog_t mergeinfo_catalog,
                                    apr_pool_t *scratch_pool)
{
  apr_array_header_t *sorted_hash;
  apr_array_header_t *elidable_paths = apr_array_make(scratch_pool, 1,
                                                      sizeof(const char *));
  apr_array_header_t *dir_stack = apr_array_make(scratch_pool, 1,
                                                 sizeof(const char *));
  apr_pool_t *iterpool;
  int i;

  /* Here's the general algorithm:
     Walk through the paths sorted in tree order.  For each path, pop
     the dir_stack until it is either empty or the top item contains a parent
     of the current path. Check to see if that mergeinfo is then elidable,
     and build the list of elidable mergeinfo based upon that determination.
     Finally, push the path of interest onto the stack, and continue. */
  sorted_hash = svn_sort__hash(mergeinfo_catalog,
                               svn_sort_compare_items_as_paths,
                               scratch_pool);
  iterpool = svn_pool_create(scratch_pool);
  for (i = 0; i < sorted_hash->nelts; i++)
    {
      svn_sort__item_t *item = &APR_ARRAY_IDX(sorted_hash, i,
                                              svn_sort__item_t);
      const char *path = item->key;

      if (dir_stack->nelts > 0)
        {
          const char *top;
          const char *path_suffix;
          svn_boolean_t elides = FALSE;

          svn_pool_clear(iterpool);

          /* Pop off any paths which are not ancestors of PATH. */
          do
            {
              top = APR_ARRAY_IDX(dir_stack, dir_stack->nelts - 1,
                                          const char *);
              path_suffix = svn_dirent_is_child(top, path, NULL);

              if (!path_suffix)
                apr_array_pop(dir_stack);
            }
          while (dir_stack->nelts > 0 && !path_suffix);

          /* If we have a path suffix, it means we haven't popped the stack
             clean. */
          if (path_suffix)
            {
              SVN_ERR(should_elide_mergeinfo(&elides,
                                         svn_hash_gets(mergeinfo_catalog, top),
                                         svn_hash_gets(mergeinfo_catalog, path),
                                         path_suffix,
                                         iterpool));

              if (elides)
                APR_ARRAY_PUSH(elidable_paths, const char *) = path;
            }
        }

      APR_ARRAY_PUSH(dir_stack, const char *) = path;
    }
  svn_pool_destroy(iterpool);

  /* Now remove the elidable paths from the catalog. */
  for (i = 0; i < elidable_paths->nelts; i++)
    {
      const char *path = APR_ARRAY_IDX(elidable_paths, i, const char *);
      svn_hash_sets(mergeinfo_catalog, path, NULL);
    }

  return SVN_NO_ERROR;
}


/* Helper for filter_log_entry_with_rangelist().

   DEPTH_FIRST_CATALOG_INDEX is an array of svn_sort__item_t's.  The keys are
   repository-absolute const char *paths, the values are svn_mergeinfo_t for
   each path.

   Return a pointer to the mergeinfo value of the nearest path-wise ancestor
   of FSPATH in DEPTH_FIRST_CATALOG_INDEX.  A path is considered its
   own ancestor, so if a key exactly matches FSPATH, return that
   key's mergeinfo and set *ANCESTOR_IS_SELF to true (set it to false in all
   other cases).

   If DEPTH_FIRST_CATALOG_INDEX is NULL, empty, or no ancestor is found, then
   return NULL. */
static svn_mergeinfo_t
find_nearest_ancestor(const apr_array_header_t *depth_first_catalog_index,
                      svn_boolean_t *ancestor_is_self,
                      const char *fspath)
{
  int ancestor_index = -1;

  *ancestor_is_self = FALSE;

  if (depth_first_catalog_index)
    {
      int i;

      for (i = 0; i < depth_first_catalog_index->nelts; i++)
        {
          svn_sort__item_t item = APR_ARRAY_IDX(depth_first_catalog_index, i,
                                                svn_sort__item_t);
          if (svn_fspath__skip_ancestor(item.key, fspath))
            {
              ancestor_index = i;

              /* There's no nearer ancestor than FSPATH itself. */
              if (strcmp(item.key, fspath) == 0)
                {
                  *ancestor_is_self = TRUE;
                  break;
                }
            }

        }
    }

  if (ancestor_index == -1)
    return NULL;
  else
    return (APR_ARRAY_IDX(depth_first_catalog_index,
                          ancestor_index,
                          svn_sort__item_t)).value;
}

/* Baton for use with the filter_log_entry_with_rangelist()
   svn_log_entry_receiver_t callback. */
struct filter_log_entry_baton_t
{
  /* Is TRUE if RANGELIST describes potentially merged revisions, is FALSE
     if RANGELIST describes potentially eligible revisions. */
  svn_boolean_t filtering_merged;

  /* Unsorted array of repository relative paths representing the merge
     sources.  There will be more than one source  */
  const apr_array_header_t *merge_source_fspaths;

  /* The repository-absolute path we are calling svn_client_log5() on. */
  const char *target_fspath;

  /* Mergeinfo catalog for the tree rooted at TARGET_FSPATH.
     The path keys must be repository-absolute. */
  svn_mergeinfo_catalog_t target_mergeinfo_catalog;

  /* Depth first sorted array of svn_sort__item_t's for
     TARGET_MERGEINFO_CATALOG. */
  apr_array_header_t *depth_first_catalog_index;

  /* A rangelist describing all the revisions potentially merged or
     potentially eligible for merging (see FILTERING_MERGED) based on
     the target's explicit or inherited mergeinfo. */
  const svn_rangelist_t *rangelist;

  /* The wrapped svn_log_entry_receiver_t callback and baton which
     filter_log_entry_with_rangelist() is acting as a filter for. */
  svn_log_entry_receiver_t log_receiver;
  void *log_receiver_baton;

  svn_client_ctx_t *ctx;
};

/* Implements the svn_log_entry_receiver_t interface.  BATON is a
   `struct filter_log_entry_baton_t *'.

   Call the wrapped log receiver BATON->log_receiver (with
   BATON->log_receiver_baton) if:

   BATON->FILTERING_MERGED is FALSE and the changes represented by LOG_ENTRY
   have been fully merged from BATON->merge_source_fspaths to the WC target
   based on the mergeinfo for the WC contained in BATON->TARGET_MERGEINFO_CATALOG.

   Or

   BATON->FILTERING_MERGED is TRUE and the changes represented by LOG_ENTRY
   have not been merged, or only partially merged, from
   BATON->merge_source_fspaths to the WC target based on the mergeinfo for the
   WC contained in BATON->TARGET_MERGEINFO_CATALOG. */
static svn_error_t *
filter_log_entry_with_rangelist(void *baton,
                                svn_log_entry_t *log_entry,
                                apr_pool_t *pool)
{
  struct filter_log_entry_baton_t *fleb = baton;
  svn_rangelist_t *intersection, *this_rangelist;

  if (fleb->ctx->cancel_func)
    SVN_ERR(fleb->ctx->cancel_func(fleb->ctx->cancel_baton));

  /* Ignore r0 because there can be no "change 0" in a merge range. */
  if (log_entry->revision == 0)
    return SVN_NO_ERROR;

  this_rangelist = svn_rangelist__initialize(log_entry->revision - 1,
                                             log_entry->revision,
                                             TRUE, pool);

  /* Don't consider inheritance yet, see if LOG_ENTRY->REVISION is
     fully or partially represented in BATON->RANGELIST. */
  SVN_ERR(svn_rangelist_intersect(&intersection, fleb->rangelist,
                                  this_rangelist, FALSE, pool));
  if (! (intersection && intersection->nelts))
    return SVN_NO_ERROR;

  SVN_ERR_ASSERT(intersection->nelts == 1);

  /* Ok, we know LOG_ENTRY->REVISION is represented in BATON->RANGELIST,
     but is it only partially represented, i.e. is the corresponding range in
     BATON->RANGELIST non-inheritable?  Ask for the same intersection as
     above but consider inheritance this time, if the intersection is empty
     we know the range in BATON->RANGELIST is non-inheritable. */
  SVN_ERR(svn_rangelist_intersect(&intersection, fleb->rangelist,
                                  this_rangelist, TRUE, pool));
  log_entry->non_inheritable = !intersection->nelts;

  /* If the paths changed by LOG_ENTRY->REVISION are provided we can determine
     if LOG_ENTRY->REVISION, while only partially represented in
     BATON->RANGELIST, is in fact completely applied to all affected paths.
     ### And ... what if it is, or if it isn't? What do we do with the answer?
         And how do we cope if the changed paths are not provided? */
  if ((log_entry->non_inheritable || !fleb->filtering_merged)
      && log_entry->changed_paths2)
    {
      apr_hash_index_t *hi;
      svn_boolean_t all_subtrees_have_this_rev = TRUE;
      svn_rangelist_t *this_rev_rangelist =
        svn_rangelist__initialize(log_entry->revision - 1,
                                  log_entry->revision, TRUE, pool);
      apr_pool_t *iterpool = svn_pool_create(pool);

      for (hi = apr_hash_first(pool, log_entry->changed_paths2);
           hi;
           hi = apr_hash_next(hi))
        {
          int i;
          const char *path = svn__apr_hash_index_key(hi);
          svn_log_changed_path2_t *change = svn__apr_hash_index_val(hi);
          const char *target_fspath_affected;
          svn_mergeinfo_t nearest_ancestor_mergeinfo;
          svn_boolean_t found_this_revision = FALSE;
          const char *merge_source_rel_target;
          const char *merge_source_fspath;
          svn_boolean_t ancestor_is_self;

          svn_pool_clear(iterpool);

          /* Check that PATH is a subtree of at least one of the
             merge sources.  If not then ignore this path.  */
          for (i = 0; i < fleb->merge_source_fspaths->nelts; i++)
            {
              merge_source_fspath = APR_ARRAY_IDX(fleb->merge_source_fspaths,
                                                  i, const char *);

              merge_source_rel_target
                = svn_fspath__skip_ancestor(merge_source_fspath, path);
              if (merge_source_rel_target)
                {
                  /* If MERGE_SOURCE was itself deleted, replaced, or added
                     in LOG_ENTRY->REVISION then ignore this PATH since you
                     can't merge a addition or deletion of yourself. */
                  if (merge_source_rel_target[0] == '\0'
                      && (change->action != 'M'))
                    i = fleb->merge_source_fspaths->nelts;
                  break;
                }
            }
          /* If we examined every merge source path and PATH is a child of
             none of them then we can ignore this PATH. */
          if (i == fleb->merge_source_fspaths->nelts)
            continue;

          /* Calculate the target path which PATH would affect if merged. */
          target_fspath_affected = svn_fspath__join(fleb->target_fspath,
                                                    merge_source_rel_target,
                                                    iterpool);

          nearest_ancestor_mergeinfo =
            find_nearest_ancestor(fleb->depth_first_catalog_index,
                                  &ancestor_is_self,
                                  target_fspath_affected);

          /* Issue #3791: A path should never have explicit mergeinfo
             describing its own addition (that's self-referential).  Nor will
             it have explicit mergeinfo describing its own deletion (we
             obviously can't add new mergeinfo to a path we are deleting).

             This lack of explicit mergeinfo should not cause such revisions
             to show up as eligible however.  If PATH was deleted, replaced,
             or added in LOG_ENTRY->REVISION, but the corresponding
             TARGET_PATH_AFFECTED already exists and has explicit mergeinfo
             describing merges from PATH *after* LOG_ENTRY->REVISION, then
             ignore this PATH.  If it was deleted in LOG_ENTRY->REVISION it's
             obviously back.  If it was added or replaced it's still around
             possibly it was replaced one or more times, but it's back now.
             Regardless, LOG_ENTRY->REVISION is *not* an eligible revision! */
          if (nearest_ancestor_mergeinfo &&
              ancestor_is_self /* Explicit mergeinfo on TARGET_PATH_AFFECTED */
              && (change->action != 'M'))
            {
              svn_rangelist_t *rangelist =
                  svn_hash_gets(nearest_ancestor_mergeinfo, path);
              if (rangelist)
                {
                  svn_merge_range_t *youngest_range = APR_ARRAY_IDX(
                    rangelist, rangelist->nelts - 1, svn_merge_range_t *);

                  if (youngest_range
                      && (youngest_range->end > log_entry->revision))
                    continue;
                }
            }

          if (nearest_ancestor_mergeinfo)
            {
              apr_hash_index_t *hi2;

              for (hi2 = apr_hash_first(iterpool, nearest_ancestor_mergeinfo);
                   hi2;
                   hi2 = apr_hash_next(hi2))
                {
                  const char *mergeinfo_path = svn__apr_hash_index_key(hi2);
                  svn_rangelist_t *rangelist = svn__apr_hash_index_val(hi2);

                  /* Does the mergeinfo for PATH reflect if
                     LOG_ENTRY->REVISION was previously merged
                     from MERGE_SOURCE_FSPATH? */
                  if (svn_fspath__skip_ancestor(merge_source_fspath,
                                                mergeinfo_path))
                    {
                      /* Something was merged from MERGE_SOURCE_FSPATH, does
                         it include LOG_ENTRY->REVISION? */
                      SVN_ERR(svn_rangelist_intersect(&intersection,
                                                      rangelist,
                                                      this_rev_rangelist,
                                                      FALSE,
                                                      iterpool));
                      if (intersection->nelts)
                        {
                          if (ancestor_is_self)
                            {
                              /* TARGET_PATH_AFFECTED has explicit mergeinfo,
                                 so we don't need to worry if that mergeinfo
                                 is inheritable or not. */
                              found_this_revision = TRUE;
                              break;
                            }
                          else
                            {
                              /* TARGET_PATH_AFFECTED inherited its mergeinfo,
                                 so we have to ignore non-inheritable
                                 ranges. */
                              SVN_ERR(svn_rangelist_intersect(
                                &intersection,
                                rangelist,
                                this_rev_rangelist,
                                TRUE, iterpool));
                              if (intersection->nelts)
                                {
                                  found_this_revision = TRUE;
                                  break;
                                }
                            }
                        }
                    }
                }
            }

          if (!found_this_revision)
            {
              /* As soon as any PATH is found that is not fully merged for
                 LOG_ENTRY->REVISION then we can stop. */
              all_subtrees_have_this_rev = FALSE;
              break;
            }
        }

      svn_pool_destroy(iterpool);

      if (all_subtrees_have_this_rev)
        {
          if (fleb->filtering_merged)
            log_entry->non_inheritable = FALSE;
          else
            return SVN_NO_ERROR;
        }
    }

  /* Call the wrapped log receiver which this function is filtering for. */
  return fleb->log_receiver(fleb->log_receiver_baton, log_entry, pool);
}

static svn_error_t *
logs_for_mergeinfo_rangelist(const char *source_url,
                             const apr_array_header_t *merge_source_fspaths,
                             svn_boolean_t filtering_merged,
                             const svn_rangelist_t *rangelist,
                             svn_boolean_t oldest_revs_first,
                             svn_mergeinfo_catalog_t target_mergeinfo_catalog,
                             const char *target_fspath,
                             svn_boolean_t discover_changed_paths,
                             const apr_array_header_t *revprops,
                             svn_log_entry_receiver_t log_receiver,
                             void *log_receiver_baton,
                             svn_client_ctx_t *ctx,
                             svn_ra_session_t *ra_session,
                             apr_pool_t *scratch_pool)
{
  svn_merge_range_t *oldest_range, *youngest_range;
  svn_revnum_t oldest_rev, youngest_rev;
  struct filter_log_entry_baton_t fleb;

  if (! rangelist->nelts)
    return SVN_NO_ERROR;

  /* Calculate and construct the bounds of our log request. */
  youngest_range = APR_ARRAY_IDX(rangelist, rangelist->nelts - 1,
                                 svn_merge_range_t *);
  youngest_rev = youngest_range->end;
  oldest_range = APR_ARRAY_IDX(rangelist, 0, svn_merge_range_t *);
  oldest_rev = oldest_range->start;

  if (! target_mergeinfo_catalog)
    target_mergeinfo_catalog = apr_hash_make(scratch_pool);

  /* FILTER_LOG_ENTRY_BATON_T->TARGET_MERGEINFO_CATALOG's keys are required
     to be repository-absolute. */
  SVN_ERR(svn_mergeinfo__add_prefix_to_catalog(&target_mergeinfo_catalog,
                                               target_mergeinfo_catalog, "/",
                                               scratch_pool, scratch_pool));

  /* Build the log filtering callback baton. */
  fleb.filtering_merged = filtering_merged;
  fleb.merge_source_fspaths = merge_source_fspaths;
  fleb.target_mergeinfo_catalog = target_mergeinfo_catalog;
  fleb.depth_first_catalog_index =
    svn_sort__hash(target_mergeinfo_catalog,
                   svn_sort_compare_items_as_paths,
                   scratch_pool);
  fleb.target_fspath = target_fspath;
  fleb.rangelist = rangelist;
  fleb.log_receiver = log_receiver;
  fleb.log_receiver_baton = log_receiver_baton;
  fleb.ctx = ctx;

  if (!ra_session)
    SVN_ERR(svn_client__open_ra_session_internal(&ra_session, NULL, source_url,
                                                 NULL, NULL, FALSE, FALSE, ctx,
                                                 scratch_pool, scratch_pool));
  else
    SVN_ERR(svn_ra_reparent(ra_session, source_url, scratch_pool));

  {
    apr_array_header_t *target;
    target = apr_array_make(scratch_pool, 1, sizeof(const char *));
    APR_ARRAY_PUSH(target, const char *) = "";

    SVN_ERR(svn_ra_get_log2(ra_session, target,
                            oldest_revs_first ? oldest_rev : youngest_rev,
                            oldest_revs_first ? youngest_rev : oldest_rev,
                            0 /* limit */,
                            discover_changed_paths,
                            FALSE /* strict_node_history */,
                            FALSE /* include_merged_revisions */,
                            revprops,
                            filter_log_entry_with_rangelist, &fleb,
                            scratch_pool));
  }

  /* Check for cancellation. */
  if (ctx->cancel_func)
    SVN_ERR(ctx->cancel_func(ctx->cancel_baton));

  return SVN_NO_ERROR;
}

/* Set *OUT_MERGEINFO to a shallow copy of MERGEINFO with each source path
   converted to a (URI-encoded) URL based on REPOS_ROOT_URL. *OUT_MERGEINFO
   is declared as 'apr_hash_t *' because its key do not obey the rules of
   'svn_mergeinfo_t'.

   Allocate *OUT_MERGEINFO and the new keys in RESULT_POOL.  Use
   SCRATCH_POOL for any temporary allocations. */
static svn_error_t *
mergeinfo_relpaths_to_urls(apr_hash_t **out_mergeinfo,
                           svn_mergeinfo_t mergeinfo,
                           const char *repos_root_url,
                           apr_pool_t *result_pool,
                           apr_pool_t *scratch_pool)
{
  *out_mergeinfo = NULL;
  if (mergeinfo)
    {
      apr_hash_index_t *hi;
      apr_hash_t *full_path_mergeinfo = apr_hash_make(result_pool);

      for (hi = apr_hash_first(scratch_pool, mergeinfo);
           hi; hi = apr_hash_next(hi))
        {
          const char *key = svn__apr_hash_index_key(hi);
          void *val = svn__apr_hash_index_val(hi);

          svn_hash_sets(full_path_mergeinfo,
                        svn_path_url_add_component2(repos_root_url, key + 1,
                                                    result_pool),
                        val);
        }
      *out_mergeinfo = full_path_mergeinfo;
    }

  return SVN_NO_ERROR;
}


/*** Public APIs ***/

svn_error_t *
svn_client_mergeinfo_get_merged(apr_hash_t **mergeinfo_p,
                                const char *path_or_url,
                                const svn_opt_revision_t *peg_revision,
                                svn_client_ctx_t *ctx,
                                apr_pool_t *pool)
{
  const char *repos_root;
  svn_mergeinfo_catalog_t mergeinfo_cat;
  svn_mergeinfo_t mergeinfo;

  SVN_ERR(get_mergeinfo(&mergeinfo_cat, &repos_root, path_or_url,
                        peg_revision, FALSE, FALSE, ctx, NULL, pool, pool));
  if (mergeinfo_cat)
    {
      const char *repos_relpath;

      if (! svn_path_is_url(path_or_url))
        {
          SVN_ERR(svn_dirent_get_absolute(&path_or_url, path_or_url, pool));
          SVN_ERR(svn_wc__node_get_repos_info(NULL, &repos_relpath, NULL, NULL,
                                              ctx->wc_ctx, path_or_url,
                                              pool, pool));
        }
      else
        {
          repos_relpath = svn_uri_skip_ancestor(repos_root, path_or_url, pool);

          SVN_ERR_ASSERT(repos_relpath != NULL); /* Or get_mergeinfo failed */
        }

      mergeinfo = svn_hash_gets(mergeinfo_cat, repos_relpath);
    }
  else
    {
      mergeinfo = NULL;
    }

  SVN_ERR(mergeinfo_relpaths_to_urls(mergeinfo_p, mergeinfo,
                                     repos_root, pool, pool));
  return SVN_NO_ERROR;
}

svn_error_t *
svn_client__mergeinfo_log(svn_boolean_t finding_merged,
                          const char *target_path_or_url,
                          const svn_opt_revision_t *target_peg_revision,
                          svn_mergeinfo_catalog_t *target_mergeinfo_catalog,
                          const char *source_path_or_url,
                          const svn_opt_revision_t *source_peg_revision,
                          const svn_opt_revision_t *source_start_revision,
                          const svn_opt_revision_t *source_end_revision,
                          svn_log_entry_receiver_t log_receiver,
                          void *log_receiver_baton,
                          svn_boolean_t discover_changed_paths,
                          svn_depth_t depth,
                          const apr_array_header_t *revprops,
                          svn_client_ctx_t *ctx,
                          svn_ra_session_t *ra_session,
                          apr_pool_t *result_pool,
                          apr_pool_t *scratch_pool)
{
  const char *log_target = NULL;
  const char *repos_root;
  const char *target_repos_relpath;
  svn_mergeinfo_catalog_t target_mergeinfo_cat;
  svn_ra_session_t *target_session = NULL;
  svn_client__pathrev_t *pathrev;

  /* A hash of paths, at or under TARGET_PATH_OR_URL, mapped to
     rangelists.  Not technically mergeinfo, so not using the
     svn_mergeinfo_t type. */
  apr_hash_t *inheritable_subtree_merges;

  svn_mergeinfo_t source_history;
  svn_mergeinfo_t target_history;
  svn_rangelist_t *master_noninheritable_rangelist;
  svn_rangelist_t *master_inheritable_rangelist;
  apr_array_header_t *merge_source_fspaths =
    apr_array_make(scratch_pool, 1, sizeof(const char *));
  apr_hash_index_t *hi_catalog;
  apr_hash_index_t *hi;
  apr_pool_t *iterpool;
  svn_boolean_t oldest_revs_first = TRUE;
  apr_pool_t *subpool;

  /* We currently only support depth = empty | infinity. */
  if (depth != svn_depth_infinity && depth != svn_depth_empty)
    return svn_error_create(
      SVN_ERR_UNSUPPORTED_FEATURE, NULL,
      _("Only depths 'infinity' and 'empty' are currently supported"));

  /* Validate and sanitize the incoming source operative revision range. */
  if (!((source_start_revision->kind == svn_opt_revision_unspecified) ||
        (source_start_revision->kind == svn_opt_revision_number) ||
        (source_start_revision->kind == svn_opt_revision_date) ||
        (source_start_revision->kind == svn_opt_revision_head)))
    return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL, NULL);
  if (!((source_end_revision->kind == svn_opt_revision_unspecified) ||
        (source_end_revision->kind == svn_opt_revision_number) ||
        (source_end_revision->kind == svn_opt_revision_date) ||
        (source_end_revision->kind == svn_opt_revision_head)))
    return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL, NULL);
  if ((source_end_revision->kind != svn_opt_revision_unspecified)
      && (source_start_revision->kind == svn_opt_revision_unspecified))
    return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL, NULL);
  if ((source_end_revision->kind == svn_opt_revision_unspecified)
      && (source_start_revision->kind != svn_opt_revision_unspecified))
    return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL, NULL);

  subpool = svn_pool_create(scratch_pool);

  if (ra_session)
    target_session = ra_session;

  /* We need the union of TARGET_PATH_OR_URL@TARGET_PEG_REVISION's mergeinfo
     and MERGE_SOURCE_URL's history.  It's not enough to do path
     matching, because renames in the history of MERGE_SOURCE_URL
     throw that all in a tizzy.  Of course, if there's no mergeinfo on
     the target, that vastly simplifies matters (we'll have nothing to
     do). */
  /* This get_mergeinfo() call doubles as a mergeinfo capabilities check. */
  if (target_mergeinfo_catalog)
    {
      if (*target_mergeinfo_catalog)
        {
          /* The caller provided the mergeinfo catalog for
             TARGET_PATH_OR_URL, so we don't need to accquire
             it ourselves.  We do need to get the repos_root
             though, because get_mergeinfo() won't do it for us. */
          target_mergeinfo_cat = *target_mergeinfo_catalog;

          if (ra_session && svn_path_is_url(target_path_or_url))
            {
              SVN_ERR(svn_ra_reparent(ra_session, target_path_or_url, subpool));
              SVN_ERR(svn_client__resolve_rev_and_url(&pathrev, ra_session,
                                                      target_path_or_url,
                                                      target_peg_revision,
                                                      target_peg_revision,
                                                      ctx, subpool));
              target_session = ra_session;
            }
          else
            {
              SVN_ERR(svn_client__ra_session_from_path2(&target_session,
                                                        &pathrev,
                                                        target_path_or_url,
                                                        NULL,
                                                        target_peg_revision,
                                                        target_peg_revision,
                                                        ctx, subpool));
            }
          SVN_ERR(svn_ra_get_repos_root2(target_session, &repos_root,
                                         scratch_pool));
        }
      else
        {
          /* The caller didn't provide the mergeinfo catalog for
             TARGET_PATH_OR_URL, but wants us to pass a copy back
             when we get it, so use RESULT_POOL. */
          SVN_ERR(get_mergeinfo(target_mergeinfo_catalog, &repos_root,
                                target_path_or_url, target_peg_revision,
                                depth == svn_depth_infinity, TRUE,
                                ctx, ra_session, result_pool, scratch_pool));
          target_mergeinfo_cat = *target_mergeinfo_catalog;
        }
    }
  else
    {
      /* The caller didn't provide the mergeinfo catalog for
         TARGET_PATH_OR_URL, nor does it want a copy, so we can use
         nothing but SCRATCH_POOL. */
      SVN_ERR(get_mergeinfo(&target_mergeinfo_cat, &repos_root,
                            target_path_or_url, target_peg_revision,
                            depth == svn_depth_infinity, TRUE,
                            ctx, ra_session, scratch_pool, scratch_pool));
    }

  if (!svn_path_is_url(target_path_or_url))
    {
      SVN_ERR(svn_dirent_get_absolute(&target_path_or_url,
                                      target_path_or_url, scratch_pool));
      SVN_ERR(svn_wc__node_get_repos_info(NULL, &target_repos_relpath,
                                          NULL, NULL,
                                          ctx->wc_ctx, target_path_or_url,
                                          scratch_pool, scratch_pool));
    }
  else
    {
      target_repos_relpath = svn_uri_skip_ancestor(repos_root,
                                                   target_path_or_url,
                                                   scratch_pool);

      /* TARGET_REPOS_REL should be non-NULL, else get_mergeinfo
         should have failed.  */
      SVN_ERR_ASSERT(target_repos_relpath != NULL);
    }

  if (!target_mergeinfo_cat)
    {
      /* If we are looking for what has been merged and there is no
         mergeinfo then we already know the answer.  If we are looking
         for eligible revisions then create a catalog with empty mergeinfo
         on the target.  This is semantically equivalent to no mergeinfo
         and gives us something to combine with MERGE_SOURCE_URL's
         history. */
      if (finding_merged)
        {
          svn_pool_destroy(subpool);
          return SVN_NO_ERROR;
        }
      else
        {
          target_mergeinfo_cat = apr_hash_make(scratch_pool);
          svn_hash_sets(target_mergeinfo_cat, target_repos_relpath,
                        apr_hash_make(scratch_pool));
        }
    }

  /* Fetch the location history as mergeinfo, for the source branch
   * (between the given start and end revisions), and, if we're finding
   * merged revisions, then also for the entire target branch.
   *
   * ### TODO: As the source and target must be in the same repository, we
   * should share a single session, tracking the two URLs separately. */
  {
    svn_ra_session_t *source_session;
    svn_revnum_t start_rev, end_rev, youngest_rev = SVN_INVALID_REVNUM;

    if (! finding_merged)
      {
        if (!target_session)
          SVN_ERR(svn_client__ra_session_from_path2(&target_session, &pathrev,
                                                    target_path_or_url, NULL,
                                                    target_peg_revision,
                                                    target_peg_revision,
                                                    ctx, subpool));
        SVN_ERR(svn_client__get_history_as_mergeinfo(&target_history, NULL,
                                                     pathrev,
                                                     SVN_INVALID_REVNUM,
                                                     SVN_INVALID_REVNUM,
                                                     target_session, ctx,
                                                     scratch_pool));
      }

    if (target_session
        && svn_path_is_url(source_path_or_url)
        && repos_root
        && svn_uri_skip_ancestor(repos_root, source_path_or_url, subpool))
      {
        /* We can re-use the existing session */
        source_session = target_session;
        SVN_ERR(svn_ra_reparent(source_session, source_path_or_url, subpool));
        SVN_ERR(svn_client__resolve_rev_and_url(&pathrev, source_session,
                                                source_path_or_url,
                                                source_peg_revision,
                                                source_peg_revision,
                                                ctx, subpool));
      }
    else
      {
        SVN_ERR(svn_client__ra_session_from_path2(&source_session, &pathrev,
                                                  source_path_or_url, NULL,
                                                  source_peg_revision,
                                                  source_peg_revision,
                                                  ctx, subpool));
      }
    SVN_ERR(svn_client__get_revision_number(&start_rev, &youngest_rev,
                                            ctx->wc_ctx, source_path_or_url,
                                            source_session,
                                            source_start_revision,
                                            subpool));
    SVN_ERR(svn_client__get_revision_number(&end_rev, &youngest_rev,
                                            ctx->wc_ctx, source_path_or_url,
                                            source_session,
                                            source_end_revision,
                                            subpool));
    SVN_ERR(svn_client__get_history_as_mergeinfo(&source_history, NULL,
                                                 pathrev,
                                                 MAX(end_rev, start_rev),
                                                 MIN(end_rev, start_rev),
                                                 source_session, ctx,
                                                 scratch_pool));
    if (start_rev > end_rev)
      oldest_revs_first = FALSE;
  }

  /* Separate the explicit or inherited mergeinfo on TARGET_PATH_OR_URL,
     and possibly its explicit subtree mergeinfo, into their
     inheritable and non-inheritable parts. */
  master_noninheritable_rangelist = apr_array_make(scratch_pool, 64,
                                                   sizeof(svn_merge_range_t *));
  master_inheritable_rangelist = apr_array_make(scratch_pool, 64,
                                                sizeof(svn_merge_range_t *));
  inheritable_subtree_merges = apr_hash_make(scratch_pool);

  iterpool = svn_pool_create(scratch_pool);

  for (hi_catalog = apr_hash_first(scratch_pool, target_mergeinfo_cat);
       hi_catalog;
       hi_catalog = apr_hash_next(hi_catalog))
    {
      svn_mergeinfo_t subtree_mergeinfo = svn__apr_hash_index_val(hi_catalog);
      svn_mergeinfo_t subtree_history;
      svn_mergeinfo_t subtree_source_history;
      svn_mergeinfo_t subtree_inheritable_mergeinfo;
      svn_mergeinfo_t subtree_noninheritable_mergeinfo;
      svn_mergeinfo_t merged_noninheritable;
      svn_mergeinfo_t merged;
      const char *subtree_path = svn__apr_hash_index_key(hi_catalog);
      svn_boolean_t is_subtree = strcmp(subtree_path,
                                        target_repos_relpath) != 0;
      svn_pool_clear(iterpool);

      if (is_subtree)
        {
          /* If SUBTREE_PATH is a proper subtree of TARGET_PATH_OR_URL
             then make a copy of SOURCE_HISTORY that is path adjusted
             for the subtree.  */
          const char *subtree_rel_path =
            subtree_path + strlen(target_repos_relpath) + 1;

          SVN_ERR(svn_mergeinfo__add_suffix_to_mergeinfo(
            &subtree_source_history, source_history,
            subtree_rel_path, scratch_pool, scratch_pool));

          if (!finding_merged)
            SVN_ERR(svn_mergeinfo__add_suffix_to_mergeinfo(
                    &subtree_history, target_history,
                    subtree_rel_path, scratch_pool, scratch_pool));
        }
      else
        {
          subtree_source_history = source_history;
          if (!finding_merged)
            subtree_history = target_history;
        }

      if (!finding_merged)
        {
          svn_mergeinfo_t merged_via_history;
          SVN_ERR(svn_mergeinfo_intersect2(&merged_via_history,
                                           subtree_history,
                                           subtree_source_history, TRUE,
                                           scratch_pool, iterpool));
          SVN_ERR(svn_mergeinfo_merge2(subtree_mergeinfo,
                                       merged_via_history,
                                       scratch_pool, scratch_pool));
        }

      SVN_ERR(svn_mergeinfo_inheritable2(&subtree_inheritable_mergeinfo,
                                         subtree_mergeinfo, NULL,
                                         SVN_INVALID_REVNUM,
                                         SVN_INVALID_REVNUM,
                                         TRUE, scratch_pool, iterpool));
      SVN_ERR(svn_mergeinfo_inheritable2(&subtree_noninheritable_mergeinfo,
                                         subtree_mergeinfo, NULL,
                                         SVN_INVALID_REVNUM,
                                         SVN_INVALID_REVNUM,
                                         FALSE, scratch_pool, iterpool));

      /* Find the intersection of the non-inheritable part of
         SUBTREE_MERGEINFO and SOURCE_HISTORY.  svn_mergeinfo_intersect2()
         won't consider non-inheritable and inheritable ranges
         intersecting unless we ignore inheritance, but in doing so the
         resulting intersections have all inheritable ranges.  To get
         around this we set the inheritance on the result to all
         non-inheritable. */
      SVN_ERR(svn_mergeinfo_intersect2(&merged_noninheritable,
                                       subtree_noninheritable_mergeinfo,
                                       subtree_source_history, FALSE,
                                       scratch_pool, iterpool));
      svn_mergeinfo__set_inheritance(merged_noninheritable, FALSE,
                                     scratch_pool);

      /* Keep track of all ranges partially merged to any and all
         subtrees. */
      SVN_ERR(svn_rangelist__merge_many(master_noninheritable_rangelist,
                                        merged_noninheritable,
                                        scratch_pool, iterpool));

      /* Find the intersection of the inheritable part of TGT_MERGEINFO
         and SOURCE_HISTORY. */
      SVN_ERR(svn_mergeinfo_intersect2(&merged,
                                       subtree_inheritable_mergeinfo,
                                       subtree_source_history, FALSE,
                                       scratch_pool, iterpool));

      /* Keep track of all ranges fully merged to any and all
         subtrees. */
      if (apr_hash_count(merged))
        {
          /* The inheritable rangelist merged from SUBTREE_SOURCE_HISTORY
             to SUBTREE_PATH. */
          svn_rangelist_t *subtree_merged_rangelist =
            apr_array_make(scratch_pool, 1, sizeof(svn_merge_range_t *));

          SVN_ERR(svn_rangelist__merge_many(master_inheritable_rangelist,
                                            merged, scratch_pool, iterpool));
          SVN_ERR(svn_rangelist__merge_many(subtree_merged_rangelist,
                                            merged, scratch_pool, iterpool));

          svn_hash_sets(inheritable_subtree_merges, subtree_path,
                        subtree_merged_rangelist);
        }
      else
        {
          /* Map SUBTREE_PATH to an empty rangelist if there was nothing
             fully merged. e.g. Only empty or non-inheritable mergeinfo
             on the subtree or mergeinfo unrelated to the source. */
          svn_hash_sets(inheritable_subtree_merges, subtree_path,
                        apr_array_make(scratch_pool, 0,
                                       sizeof(svn_merge_range_t *)));
        }
    }

  /* Make sure every range in MASTER_INHERITABLE_RANGELIST is fully merged to
     each subtree (including the target itself).  Any revisions which don't
     exist in *every* subtree are *potentially* only partially merged to the
     tree rooted at TARGET_PATH_OR_URL, so move those revisions to
     MASTER_NONINHERITABLE_RANGELIST.  It may turn out that that a revision
     was merged to the only subtree it affects, but we need to examine the
     logs to make this determination (which will be done by
     logs_for_mergeinfo_rangelist). */
  if (master_inheritable_rangelist->nelts)
    {
      for (hi = apr_hash_first(scratch_pool, inheritable_subtree_merges);
           hi;
           hi = apr_hash_next(hi))
        {
          svn_rangelist_t *deleted_rangelist;
          svn_rangelist_t *added_rangelist;
          svn_rangelist_t *subtree_merged_rangelist =
            svn__apr_hash_index_val(hi);

          svn_pool_clear(iterpool);

          SVN_ERR(svn_rangelist_diff(&deleted_rangelist, &added_rangelist,
                                     master_inheritable_rangelist,
                                     subtree_merged_rangelist, TRUE,
                                     iterpool));

          if (deleted_rangelist->nelts)
            {
              svn_rangelist__set_inheritance(deleted_rangelist, FALSE);
              SVN_ERR(svn_rangelist_merge2(master_noninheritable_rangelist,
                                           deleted_rangelist,
                                           scratch_pool, iterpool));
              SVN_ERR(svn_rangelist_remove(&master_inheritable_rangelist,
                                           deleted_rangelist,
                                           master_inheritable_rangelist,
                                           FALSE,
                                           scratch_pool));
            }
        }
    }

  if (finding_merged)
    {
      /* Roll all the merged revisions into one rangelist. */
      SVN_ERR(svn_rangelist_merge2(master_inheritable_rangelist,
                                   master_noninheritable_rangelist,
                                   scratch_pool, scratch_pool));

    }
  else
    {
      /* Create the starting rangelist for what might be eligible. */
      svn_rangelist_t *source_master_rangelist =
        apr_array_make(scratch_pool, 1, sizeof(svn_merge_range_t *));

      SVN_ERR(svn_rangelist__merge_many(source_master_rangelist,
                                        source_history,
                                        scratch_pool, scratch_pool));

      /* From what might be eligible subtract what we know is
         partially merged and then merge that back. */
      SVN_ERR(svn_rangelist_remove(&source_master_rangelist,
                                   master_noninheritable_rangelist,
                                   source_master_rangelist,
                                   FALSE, scratch_pool));
      SVN_ERR(svn_rangelist_merge2(source_master_rangelist,
                                   master_noninheritable_rangelist,
                                   scratch_pool, scratch_pool));
      SVN_ERR(svn_rangelist_remove(&master_inheritable_rangelist,
                                   master_inheritable_rangelist,
                                   source_master_rangelist,
                                   TRUE, scratch_pool));
    }

  /* Nothing merged?  Not even when considering shared history if
     looking for eligible revisions (i.e. !FINDING_MERGED)?  Then there
     is nothing more to do. */
  if (! master_inheritable_rangelist->nelts)
    {
      svn_pool_destroy(iterpool);
      return SVN_NO_ERROR;
    }
  else
    {
      /* Determine the correct (youngest) target for 'svn log'. */
      svn_merge_range_t *youngest_range
        = APR_ARRAY_IDX(master_inheritable_rangelist,
                        master_inheritable_rangelist->nelts - 1,
                        svn_merge_range_t *);
      svn_rangelist_t *youngest_rangelist =
        svn_rangelist__initialize(youngest_range->end - 1,
                                  youngest_range->end,
                                  youngest_range->inheritable,
                                  scratch_pool);;

      for (hi = apr_hash_first(scratch_pool, source_history);
           hi;
           hi = apr_hash_next(hi))
        {
          const char *key = svn__apr_hash_index_key(hi);
          svn_rangelist_t *subtree_merged_rangelist =
            svn__apr_hash_index_val(hi);
          svn_rangelist_t *intersecting_rangelist;

          svn_pool_clear(iterpool);
          SVN_ERR(svn_rangelist_intersect(&intersecting_rangelist,
                                          youngest_rangelist,
                                          subtree_merged_rangelist,
                                          FALSE, iterpool));

          APR_ARRAY_PUSH(merge_source_fspaths, const char *) = key;

          if (intersecting_rangelist->nelts)
            log_target = key;
        }
    }

  svn_pool_destroy(iterpool);

  /* Step 4: Finally, we run 'svn log' to drive our log receiver, but
     using a receiver filter to only allow revisions to pass through
     that are in our rangelist. */
  log_target = svn_path_url_add_component2(repos_root, log_target + 1,
                                           scratch_pool);

  {
    svn_error_t *err;

    err = logs_for_mergeinfo_rangelist(log_target, merge_source_fspaths,
                                       finding_merged,
                                       master_inheritable_rangelist,
                                       oldest_revs_first,
                                       target_mergeinfo_cat,
                                       svn_fspath__join("/",
                                                        target_repos_relpath,
                                                        scratch_pool),
                                       discover_changed_paths,
                                       revprops,
                                       log_receiver, log_receiver_baton,
                                       ctx, target_session, scratch_pool);

    /* Close the source and target sessions. */
    svn_pool_destroy(subpool); /* For SVN_ERR_CEASE_INVOCATION */

    return svn_error_trace(err);
  }
}

svn_error_t *
svn_client_mergeinfo_log2(svn_boolean_t finding_merged,
                          const char *target_path_or_url,
                          const svn_opt_revision_t *target_peg_revision,
                          const char *source_path_or_url,
                          const svn_opt_revision_t *source_peg_revision,
                          const svn_opt_revision_t *source_start_revision,
                          const svn_opt_revision_t *source_end_revision,
                          svn_log_entry_receiver_t log_receiver,
                          void *log_receiver_baton,
                          svn_boolean_t discover_changed_paths,
                          svn_depth_t depth,
                          const apr_array_header_t *revprops,
                          svn_client_ctx_t *ctx,
                          apr_pool_t *scratch_pool)
{
  return svn_error_trace(
         svn_client__mergeinfo_log(finding_merged, target_path_or_url,
                                   target_peg_revision, NULL,
                                   source_path_or_url, source_peg_revision,
                                   source_start_revision, source_end_revision,
                                   log_receiver, log_receiver_baton,
                                   discover_changed_paths, depth, revprops,
                                   ctx, NULL,
                                   scratch_pool, scratch_pool));
}

svn_error_t *
svn_client_suggest_merge_sources(apr_array_header_t **suggestions,
                                 const char *path_or_url,
                                 const svn_opt_revision_t *peg_revision,
                                 svn_client_ctx_t *ctx,
                                 apr_pool_t *pool)
{
  const char *repos_root;
  const char *copyfrom_path;
  apr_array_header_t *list;
  svn_revnum_t copyfrom_rev;
  svn_mergeinfo_catalog_t mergeinfo_cat;
  svn_mergeinfo_t mergeinfo;
  apr_hash_index_t *hi;

  list = apr_array_make(pool, 1, sizeof(const char *));

  /* In our ideal algorithm, the list of recommendations should be
     ordered by:

        1. The most recent existing merge source.
        2. The copyfrom source (which will also be listed as a merge
           source if the copy was made with a 1.5+ client and server).
        3. All other merge sources, most recent to least recent.

     However, determining the order of application of merge sources
     requires a new RA API.  Until such an API is available, our
     algorithm will be:

        1. The copyfrom source.
        2. All remaining merge sources (unordered).
  */

  /* ### TODO: Share ra_session batons to improve efficiency? */
  SVN_ERR(get_mergeinfo(&mergeinfo_cat, &repos_root, path_or_url,
                        peg_revision, FALSE, FALSE, ctx, NULL, pool, pool));

  if (mergeinfo_cat && apr_hash_count(mergeinfo_cat))
    {
      /* We asked only for the PATH_OR_URL's mergeinfo, not any of its
         descendants.  So if there is anything in the catalog it is the
         mergeinfo for PATH_OR_URL. */
      mergeinfo = svn__apr_hash_index_val(apr_hash_first(pool, mergeinfo_cat));
    }
  else
    {
      mergeinfo = NULL;
    }

  SVN_ERR(svn_client__get_copy_source(&copyfrom_path, &copyfrom_rev,
                                      path_or_url, peg_revision, ctx,
                                      pool, pool));
  if (copyfrom_path)
    {
      APR_ARRAY_PUSH(list, const char *) =
        svn_path_url_add_component2(repos_root, copyfrom_path, pool);
    }

  if (mergeinfo)
    {
      for (hi = apr_hash_first(pool, mergeinfo); hi; hi = apr_hash_next(hi))
        {
          const char *rel_path = svn__apr_hash_index_key(hi);

          if (copyfrom_path == NULL || strcmp(rel_path, copyfrom_path) != 0)
            APR_ARRAY_PUSH(list, const char *) = \
              svn_path_url_add_component2(repos_root, rel_path + 1, pool);
        }
    }

  *suggestions = list;
  return SVN_NO_ERROR;
}

svn_error_t *
svn_client__mergeinfo_status(svn_boolean_t *mergeinfo_changes,
                             svn_wc_context_t *wc_ctx,
                             const char *local_abspath,
                             apr_pool_t *scratch_pool)
{
  apr_array_header_t *propchanges;
  int i;

  *mergeinfo_changes = FALSE;

  SVN_ERR(svn_wc_get_prop_diffs2(&propchanges, NULL, wc_ctx,
                                 local_abspath, scratch_pool, scratch_pool));

  for (i = 0; i < propchanges->nelts; i++)
    {
      svn_prop_t prop = APR_ARRAY_IDX(propchanges, i, svn_prop_t);
      if (strcmp(prop.name, SVN_PROP_MERGEINFO) == 0)
        {
          *mergeinfo_changes = TRUE;
          break;
        }
    }

  return SVN_NO_ERROR;
}