<commit_before>/*
 * diff.c: comparing and merging
 *
 * ====================================================================
 * Copyright (c) 2000-2004 CollabNet.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://subversion.tigris.org/license-1.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 *
 * This software consists of voluntary contributions made by many
 * individuals.  For exact contribution history, see the revision
 * history and logs, available at http://subversion.tigris.org/.
 * ====================================================================
 */

/* ==================================================================== */




/*** Includes. ***/

#include <apr_strings.h>
#include <apr_pools.h>
#include <apr_hash.h>
#include "svn_wc.h"
#include "svn_delta.h"
#include "svn_diff.h"
#include "svn_client.h"
#include "svn_string.h"
#include "svn_error.h"
#include "svn_path.h"
#include "svn_io.h"
#include "svn_utf.h"
#include "svn_pools.h"
#include "svn_config.h"
#include "svn_props.h"
#include "client.h"
#include <assert.h>

#include "svn_private_config.h"

/*
 * Constant separator strings
 */
static const char equal_string[] = 
  "===================================================================";
static const char under_string[] =
  "___________________________________________________________________";


/*-----------------------------------------------------------------*/


/* Utilities */

/* Wrapper for @c apr_file_printf(), which see.  FORMAT is a utf8-encoded
   string after it is formatted, so this function can convert it to
   native encoding before printing. */
static svn_error_t *
file_printf_from_utf8 (apr_file_t *fptr, const char *format, ...)
{
  va_list ap;
  const char *buf, *buf_apr;

  va_start (ap, format);
  buf = apr_pvsprintf (apr_file_pool_get (fptr), format, ap); 
  va_end(ap);

  SVN_ERR (svn_utf_cstring_from_utf8 (&buf_apr, buf,
                                       apr_file_pool_get (fptr)));

  return svn_io_file_write_full (fptr, buf_apr, strlen (buf_apr), 
                                 NULL, apr_file_pool_get (fptr));
}


/* A helper func that writes out verbal descriptions of property diffs
   to FILE.   Of course, the apr_file_t will probably be the 'outfile'
   passed to svn_client_diff2, which is probably stdout. */
static svn_error_t *
display_prop_diffs (const apr_array_header_t *propchanges,
                    apr_hash_t *original_props,
                    const char *path,
                    apr_file_t *file,
                    apr_pool_t *pool)
{
  int i;

  SVN_ERR (file_printf_from_utf8 (file,
                                  _("%sProperty changes on: %s%s"),
                                  APR_EOL_STR,
                                  svn_path_local_style (path, pool),
                                  APR_EOL_STR));

  /* ### todo [issue #1533]: Use file_printf_from_utf8() to convert this
     line of dashes to native encoding, at least conditionally?  Or is
     it better to have under_string always output the same, so
     programs can find it?  Also, what about checking for error? */
  apr_file_printf (file, "%s" APR_EOL_STR, under_string);

  for (i = 0; i < propchanges->nelts; i++)
    {
      const svn_prop_t *propchange
        = &APR_ARRAY_IDX(propchanges, i, svn_prop_t);

      const svn_string_t *original_value;

      if (original_props)
        original_value = apr_hash_get (original_props, 
                                       propchange->name, APR_HASH_KEY_STRING);
      else
        original_value = NULL;
      
      SVN_ERR (file_printf_from_utf8 (file, _("Name: %s%s"),
                                      propchange->name, APR_EOL_STR));

      /* For now, we have a rather simple heuristic: if this is an
         "svn:" property, then assume the value is UTF-8 and must
         therefore be converted before printing.  Otherwise, just
         print whatever's there and hope for the best. */
      {
        svn_boolean_t val_is_utf8 = svn_prop_is_svn_prop (propchange->name);
        
        if (original_value != NULL)
          {
            if (val_is_utf8)
              {
                SVN_ERR (file_printf_from_utf8
                         (file, "   - %s" APR_EOL_STR, original_value->data));
              }
            else
              {
                /* ### todo: check for error? */
                apr_file_printf
                  (file, "   - %s" APR_EOL_STR, original_value->data);
              }
          }
        
        if (propchange->value != NULL)
          {
            if (val_is_utf8)
              {
                SVN_ERR (file_printf_from_utf8
                         (file, "   + %s" APR_EOL_STR,
                          propchange->value->data));
              }
            else
              {
                /* ### todo: check for error? */
                apr_file_printf (file, "   + %s" APR_EOL_STR,
                                 propchange->value->data);
              }
          }
      }
    }

  /* ### todo [issue #1533]: Use file_printf_from_utf8() to convert this
     to native encoding, at least conditionally?  Or is it better to
     have under_string always output the same eol, so programs can
     find it consistently?  Also, what about checking for error? */
  apr_file_printf (file, APR_EOL_STR);

  return SVN_NO_ERROR;
}


/* Return SVN_ERR_UNSUPPORTED_FEATURE if URL's schema does not
   match the schema of the url for ADM_ACCESS's path; return
   SVN_ERR_BAD_URL if no schema can be found for one or both urls;
   otherwise return SVN_NO_ERROR.  Use @a ADM_ACCESS's pool for
   temporary allocation. */
static svn_error_t *
check_schema_match (svn_wc_adm_access_t *adm_access, const char *url)
{
  const char *path = svn_wc_adm_access_path (adm_access);
  apr_pool_t *pool = svn_wc_adm_access_pool (adm_access);
  const svn_wc_entry_t *ent;
  const char *idx1, *idx2;
  
  SVN_ERR (svn_wc_entry (&ent, path, adm_access, TRUE, pool));
  
  idx1 = strchr (url, ':');
  idx2 = strchr (ent->url, ':');

  if ((idx1 == NULL) && (idx2 == NULL))
    {
      return svn_error_createf
        (SVN_ERR_BAD_URL, NULL,
         _("URLs have no schema ('%s' and '%s')"), url, ent->url);
    }
  else if (idx1 == NULL)
    {
      return svn_error_createf
        (SVN_ERR_BAD_URL, NULL,
         _("URL has no schema: '%s'"), url);
    }
  else if (idx2 == NULL)
    {
      return svn_error_createf
        (SVN_ERR_BAD_URL, NULL,
         _("URL has no schema: '%s'"), ent->url);
    }
  else if (((idx1 - url) != (idx2 - ent->url))
           || (strncmp (url, ent->url, idx1 - url) != 0))
    {
      return svn_error_createf
        (SVN_ERR_UNSUPPORTED_FEATURE, NULL,
         _("Access schema mixtures not yet supported ('%s' and '%s')"),
         url, ent->url);
    }

  /* else */

  return SVN_NO_ERROR;
}


/*-----------------------------------------------------------------*/


/*** Callbacks for 'svn diff', invoked by the repos-diff editor. ***/


struct diff_cmd_baton {
  const apr_array_header_t *options;
  apr_pool_t *pool;
  apr_file_t *outfile;
  apr_file_t *errfile;

  /* The original targets passed to the diff command.  We may need
     these to construct distinctive diff labels when comparing the
     same relative path in the same revision, under different anchors
     (for example, when comparing a trunk against a branch). */
  const char *orig_path_1;
  const char *orig_path_2;

  /* These are the numeric representations of the revisions passed to
     svn_client_diff2, either may be SVN_INVALID_REVNUM.  We need these
     because some of the svn_wc_diff_callbacks2_t don't get revision
     arguments.

     ### Perhaps we should change the callback signatures and eliminate
     ### these?
  */
  svn_revnum_t revnum1;
  svn_revnum_t revnum2;

  /* Client config hash (may be NULL). */
  apr_hash_t *config;

  /* Set this if you want diff output even for binary files. */
  svn_boolean_t force_binary;

  /* Set this flag if you want diff_file_changed to output diffs
     unconditionally, even if the diffs are empty. */
  svn_boolean_t force_empty;

};


/* Generate a label for the diff output for file PATH at revision REVNUM.
   If REVNUM is invalid then it is assumed to be the current working
   copy.  Assumes the paths are already in the desired style (local
   vs internal).  Allocate the label in POOL. */
static const char *
diff_label (const char *path,
            svn_revnum_t revnum,
            apr_pool_t *pool)
{
  const char *label;
  if (revnum != SVN_INVALID_REVNUM)
    label = apr_psprintf (pool, _("%s\t(revision %ld)"), path, revnum);
  else
    label = apr_psprintf (pool, _("%s\t(working copy)"), path);

  return label;
}

/* A svn_wc_diff_callbacks2_t function.  Used for both file and directory
   property diffs. */
static svn_error_t *
diff_props_changed (svn_wc_adm_access_t *adm_access,
                    svn_wc_notify_state_t *state,
                    const char *path,
                    const apr_array_header_t *propchanges,
                    apr_hash_t *original_props,
                    void *diff_baton)
{
  struct diff_cmd_baton *diff_cmd_baton = diff_baton;
  apr_array_header_t *props;
  apr_pool_t *subpool = svn_pool_create (diff_cmd_baton->pool);

  SVN_ERR (svn_categorize_props (propchanges, NULL, NULL, &props, subpool));

  if (props->nelts > 0)
    SVN_ERR (display_prop_diffs (props, original_props, path,
                                 diff_cmd_baton->outfile, subpool));

  if (state)
    *state = svn_wc_notify_state_unknown;

  svn_pool_destroy (subpool);
  return SVN_NO_ERROR;
}

/* Show differences between TMPFILE1 and TMPFILE2. PATH, REV1, and REV2 are
   used in the headers to indicate the file and revisions.  If either
   MIMETYPE1 or MIMETYPE2 indicate binary content, don't show a diff,
   but instread print a warning message. */
static svn_error_t *
diff_content_changed (const char *path,
                      const char *tmpfile1,
                      const char *tmpfile2,
                      svn_revnum_t rev1,
                      svn_revnum_t rev2,
                      const char *mimetype1,
                      const char *mimetype2,
                      void *diff_baton)
{
  struct diff_cmd_baton *diff_cmd_baton = diff_baton;
  const char *diff_cmd = NULL;
  const char **args = NULL;
  int nargs, exitcode;
  apr_pool_t *subpool = svn_pool_create (diff_cmd_baton->pool);
  svn_stream_t *os;
  apr_file_t *errfile = diff_cmd_baton->errfile;
  const char *label1, *label2;
  svn_boolean_t mt1_binary = FALSE, mt2_binary = FALSE;
  const char *path1, *path2;
  int i;

  /* Get a stream from our output file. */
  os = svn_stream_from_aprfile(diff_cmd_baton->outfile, subpool); 

  /* Assemble any option args. */
  nargs = diff_cmd_baton->options->nelts;
  if (nargs)
    {
      args = apr_palloc (subpool, nargs * sizeof (char *));
      for (i = 0; i < diff_cmd_baton->options->nelts; i++)
        {
          args[i] = 
            ((const char **)(diff_cmd_baton->options->elts))[i];
        }
      assert (i == nargs);
    }

  /* Generate the diff headers. */

  /* ### Holy cow.  Due to anchor/target weirdness, we can't
     simply join diff_cmd_baton->orig_path_1 with path, ditto for
     orig_path_2.  That will work when they're directory URLs, but
     not for file URLs.  Nor can we just use anchor1 and anchor2
     from do_diff(), at least not without some more logic here.
     What a nightmare.
     
     For now, to distinguish the two paths, we'll just put the
     unique portions of the original targets in parentheses before
     the received path, with ellipses for handwaving.  This makes
     the labels a bit clumsy, but at least distinctive.  Better
     solutions are possible, they'll just take more thought. */

  path1 = diff_cmd_baton->orig_path_1;
  path2 = diff_cmd_baton->orig_path_2;
  
  for (i = 0; path1[i] && path2[i] && (path1[i] == path2[i]); i++)
    ;
  
  /* Make sure the prefix is made of whole components. (Issue #1771) */
  if (path1[i] || path2[i])
    {
      for ( ; (i > 0) && (path1[i] != '/'); i--)
        ;
    }
  
  path1 = path1 + i;
  path2 = path2 + i;
  
  /* ### Should diff labels print paths in local style?  Is there
     already a standard for this?  In any case, this code depends on
     a particular style, so not calling svn_path_local_style() on the
     paths below.*/
  if (path1[0] == '\0')
    path1 = apr_psprintf (subpool, "%s", path);
  else if (path1[0] == '/')
    path1 = apr_psprintf (subpool, "%s\t(...%s)", path, path1);
  else
    path1 = apr_psprintf (subpool, "%s\t(.../%s)", path, path1);
  
  if (path2[0] == '\0')
    path2 = apr_psprintf (subpool, "%s", path);
  else if (path2[0] == '/')
    path2 = apr_psprintf (subpool, "%s\t(...%s)", path, path2);
  else
    path2 = apr_psprintf (subpool, "%s\t(.../%s)", path, path2);
  
  label1 = diff_label (path1, rev1, subpool);
  label2 = diff_label (path2, rev2, subpool);

  /* Possible easy-out: if either mime-type is binary and force was not
     specified, don't attempt to generate a viewable diff at all.
     Print a warning and exit. */
  if (mimetype1)
    mt1_binary = svn_mime_type_is_binary (mimetype1);
  if (mimetype2)
    mt2_binary = svn_mime_type_is_binary (mimetype2);

  if (! diff_cmd_baton->force_binary && (mt1_binary || mt2_binary))
    {
      /* Print out the diff header. */
      SVN_ERR (svn_stream_printf (os, subpool,
                                  "Index: %s" APR_EOL_STR
                                  "%s" APR_EOL_STR, path, equal_string));

      SVN_ERR (svn_stream_printf
               (os, subpool,
                _("Cannot display: file marked as a binary type.%s"),
                APR_EOL_STR));
      
      if (mt1_binary && !mt2_binary)
        SVN_ERR (svn_stream_printf (os, subpool,
                                    "svn:mime-type = %s" APR_EOL_STR,
                                    mimetype1));
      else if (mt2_binary && !mt1_binary)
        SVN_ERR (svn_stream_printf (os, subpool,
                                    "svn:mime-type = %s" APR_EOL_STR,
                                    mimetype2));
      else if (mt1_binary && mt2_binary)
        {
          if (strcmp (mimetype1, mimetype2) == 0)
            SVN_ERR (svn_stream_printf
                     (os, subpool,
                      "svn:mime-type = %s" APR_EOL_STR,
                      mimetype1));
          else
            SVN_ERR (svn_stream_printf
                     (os, subpool,
                      "svn:mime-type = (%s, %s)" APR_EOL_STR,
                      mimetype1, mimetype2));
        }

      /* Exit early. */
      svn_pool_destroy (subpool);
      return SVN_NO_ERROR;
    }


  /* Find out if we need to run an external diff */
  if (diff_cmd_baton->config)
    {
      svn_config_t *cfg = apr_hash_get (diff_cmd_baton->config,
                                        SVN_CONFIG_CATEGORY_CONFIG,
                                        APR_HASH_KEY_STRING);
      svn_config_get (cfg, &diff_cmd, SVN_CONFIG_SECTION_HELPERS,
                      SVN_CONFIG_OPTION_DIFF_CMD, NULL);
    }

  if (diff_cmd)
    {
      /* Print out the diff header. */
      SVN_ERR (svn_stream_printf (os, subpool,
                                  "Index: %s" APR_EOL_STR
                                  "%s" APR_EOL_STR, path, equal_string));
      /* Close the stream (flush) */
      SVN_ERR (svn_stream_close (os));

      SVN_ERR (svn_io_run_diff (".", args, nargs, label1, label2,
                                tmpfile1, tmpfile2, 
                                &exitcode, diff_cmd_baton->outfile, errfile,
                                diff_cmd, subpool));
    }
  else   /* use libsvn_diff to generate the diff  */
    {
      svn_diff_t *diff;

      /* We don't currently support any options (well, other than -u, since we 
         default to unified diff output anyway), so if we received anything 
         other than that it's an error. */
      if (diff_cmd_baton->options)
        {
          for (i = 0; i < diff_cmd_baton->options->nelts; ++i)
            {
              const char *arg
                = ((const char **)(diff_cmd_baton->options->elts))[i];

              if (strcmp(arg, "-u") == 0)
                continue;
              else
                return svn_error_createf(SVN_ERR_INVALID_DIFF_OPTION, NULL,
                                         _("'%s' is not supported"), arg);
            }
        }

      SVN_ERR (svn_diff_file_diff (&diff, tmpfile1, tmpfile2, subpool));

      if (svn_diff_contains_diffs (diff) || diff_cmd_baton->force_empty)
        {
          /* Print out the diff header. */
          SVN_ERR (svn_stream_printf (os, subpool,
                                      "Index: %s" APR_EOL_STR
                                      "%s" APR_EOL_STR, path, equal_string));

          /* Output the actual diff */
          SVN_ERR (svn_diff_file_output_unified (os, diff,
                                                 tmpfile1, tmpfile2,
                                                 label1, label2,
                                                 subpool));
        }
    }

  /* ### todo: someday we'll need to worry about whether we're going
     to need to write a diff plug-in mechanism that makes use of the
     two paths, instead of just blindly running SVN_CLIENT_DIFF.  */

  /* Destroy the subpool. */
  svn_pool_destroy (subpool);

  return SVN_NO_ERROR;
}

/* A svn_wc_diff_callbacks2_t function. */
static svn_error_t *
diff_file_changed (svn_wc_adm_access_t *adm_access,
                   svn_wc_notify_state_t *content_state,
                   svn_wc_notify_state_t *prop_state,
                   const char *path,
                   const char *tmpfile1,
                   const char *tmpfile2,
                   svn_revnum_t rev1,
                   svn_revnum_t rev2,
                   const char *mimetype1,
                   const char *mimetype2,
                   const apr_array_header_t *prop_changes,
                   apr_hash_t *original_props,
                   void *diff_baton)
{
  if (tmpfile1)
    SVN_ERR (diff_content_changed (path,
                                   tmpfile1, tmpfile2, rev1, rev2,
                                   mimetype1, mimetype2, diff_baton));
  if (prop_changes->nelts > 0)
    SVN_ERR (diff_props_changed (adm_access, prop_state, path, prop_changes,
                                 original_props, diff_baton));
  if (content_state)
    *content_state = svn_wc_notify_state_unknown;
  if (prop_state)
    *prop_state = svn_wc_notify_state_unknown;
  return SVN_NO_ERROR;
}

/* Because the repos-diff editor passes at least one empty file to
   each of these next two functions, they can be dumb wrappers around
   the main workhorse routine. */

/* A svn_wc_diff_callbacks2_t function. */
static svn_error_t *
diff_file_added (svn_wc_adm_access_t *adm_access,
                 svn_wc_notify_state_t *content_state,
                 svn_wc_notify_state_t *prop_state,
                 const char *path,
                 const char *tmpfile1,
                 const char *tmpfile2,
                 svn_revnum_t rev1,
                 svn_revnum_t rev2,
                 const char *mimetype1,
                 const char *mimetype2,
                 const apr_array_header_t *prop_changes,
                 apr_hash_t *original_props,
                 void *diff_baton)
{
  struct diff_cmd_baton *diff_cmd_baton = diff_baton;

  /* We want diff_file_changed to unconditionally show diffs, even if
     the diff is empty (as would be the case if an empty file were
     added.)  It's important, because 'patch' would still see an empty
     diff and create an empty file.  It's also important to let the
     user see that *something* happened. */
  diff_cmd_baton->force_empty = TRUE;

  SVN_ERR (diff_file_changed (adm_access, content_state, prop_state, path,
                              tmpfile1, tmpfile2, 
                              rev1, rev2,
                              mimetype1, mimetype2,
                              prop_changes, original_props, diff_baton));
  
  diff_cmd_baton->force_empty = FALSE;

  return SVN_NO_ERROR;
}

/* A svn_wc_diff_callbacks2_t function. */
static svn_error_t *
diff_file_deleted_with_diff (svn_wc_adm_access_t *adm_access,
                             svn_wc_notify_state_t *state,
                             const char *path,
                             const char *tmpfile1,
                             const char *tmpfile2,
                             const char *mimetype1,
                             const char *mimetype2,
                             apr_hash_t *original_props,
                             void *diff_baton)
{
  struct diff_cmd_baton *diff_cmd_baton = diff_baton;

  /* We don't list all the deleted properties. */
  return diff_file_changed (adm_access, state, NULL, path,
                            tmpfile1, tmpfile2, 
                            diff_cmd_baton->revnum1, diff_cmd_baton->revnum2,
                            mimetype1, mimetype2,
                            apr_array_make (diff_cmd_baton->pool, 1,
                                            sizeof(svn_prop_t)),
                            apr_hash_make (diff_cmd_baton->pool), diff_baton);
}

/* A svn_wc_diff_callbacks2_t function. */
static svn_error_t *
diff_file_deleted_no_diff (svn_wc_adm_access_t *adm_access,
                           svn_wc_notify_state_t *state,
                           const char *path,
                           const char *tmpfile1,
                           const char *tmpfile2,
                           const char *mimetype1,
                           const char *mimetype2,
                           apr_hash_t *original_props,
                           void *diff_baton)
{
  struct diff_cmd_baton *diff_cmd_baton = diff_baton;

  if (state)
    *state = svn_wc_notify_state_unknown;

  SVN_ERR (file_printf_from_utf8
           (diff_cmd_baton->outfile,
            "Index: %s (deleted)" APR_EOL_STR "%s" APR_EOL_STR, 
            path, equal_string));

  return SVN_NO_ERROR;
}

/* A svn_wc_diff_callbacks2_t function.
   For now, let's have 'svn diff' send feedback to the top-level
   application, so that something reasonable about directories and
   propsets gets printed to stdout. */
static svn_error_t *
diff_dir_added (svn_wc_adm_access_t *adm_access,
                svn_wc_notify_state_t *state,
                const char *path,
                svn_revnum_t rev,
                void *diff_baton)
{
  if (state)
    *state = svn_wc_notify_state_unknown;

  /* ### todo:  send feedback to app */
  return SVN_NO_ERROR;
}

/* A svn_wc_diff_callbacks2_t function. */
static svn_error_t *
diff_dir_deleted (svn_wc_adm_access_t *adm_access,
                  svn_wc_notify_state_t *state,
                  const char *path,
                  void *diff_baton)
{
  if (state)
    *state = svn_wc_notify_state_unknown;

  return SVN_NO_ERROR;
}
  

/*-----------------------------------------------------------------*/


/*** Callbacks for 'svn merge', invoked by the repos-diff editor. ***/


struct merge_cmd_baton {
  svn_boolean_t force;
  svn_boolean_t dry_run;
  const char *added_path;             /* Set to the dir path whenever the
                                         dir is added as a child of a
                                         versioned dir (dry-run only) */
  const char *target;                 /* Working copy target of merge */
  const char *url;                    /* The second URL in the merge */
  const char *path;                   /* The wc path of the second target, this
                                         can be NULL if we don't have one. */
  const svn_opt_revision_t *revision; /* Revision of second URL in the merge */
  svn_client_ctx_t *ctx;

  /* The diff3_cmd in ctx->config, if any, else null.  We could just
     extract this as needed, but since more than one caller uses it,
     we just set it up when this baton is created. */
  const char *diff3_cmd;

  apr_pool_t *pool;
};

/* A svn_wc_diff_callbacks2_t function.  Used for both file and directory
   property merges. */
static svn_error_t *
merge_props_changed (svn_wc_adm_access_t *adm_access,
                     svn_wc_notify_state_t *state,
                     const char *path,
                     const apr_array_header_t *propchanges,
                     apr_hash_t *original_props,
                     void *baton)
{
  apr_array_header_t *props;
  struct merge_cmd_baton *merge_b = baton;
  apr_pool_t *subpool = svn_pool_create (merge_b->pool);
  svn_error_t *err;

  SVN_ERR (svn_categorize_props (propchanges, NULL, NULL, &props, subpool));

  /* We only want to merge "regular" version properties:  by
     definition, 'svn merge' shouldn't touch any data within .svn/  */
  if (props->nelts)
    {
      err = svn_wc_merge_prop_diffs (state, path, adm_access, props,
                                     FALSE, merge_b->dry_run, subpool);
      if (err && (err->apr_err == SVN_ERR_ENTRY_NOT_FOUND
                  || err->apr_err == SVN_ERR_UNVERSIONED_RESOURCE))
        {
          /* if the entry doesn't exist in the wc, just 'skip' over
             this part of the tree-delta. */
          if (state)
            *state = svn_wc_notify_state_missing;
          svn_error_clear (err);
          return SVN_NO_ERROR;        
        }
      else if (err)
        return err;
    }

  svn_pool_destroy (subpool);
  return SVN_NO_ERROR;
}

/* A svn_wc_diff_callbacks2_t function. */
static svn_error_t *
merge_file_changed (svn_wc_adm_access_t *adm_access,
                    svn_wc_notify_state_t *content_state,
                    svn_wc_notify_state_t *prop_state,
                    const char *mine,
                    const char *older,
                    const char *yours,
                    svn_revnum_t older_rev,
                    svn_revnum_t yours_rev,
                    const char *mimetype1,
                    const char *mimetype2,
                    const apr_array_header_t *prop_changes,
                    apr_hash_t *original_props,
                    void *baton)
{
  struct merge_cmd_baton *merge_b = baton;
  apr_pool_t *subpool = svn_pool_create (merge_b->pool);
  /* xgettext: the '.working', '.merge-left.r%ld' and '.merge-right.r%ld'
     strings are used to tag onto a filename in case of a merge conflict */
  const char *target_label = _(".working");
  const char *left_label = apr_psprintf (subpool,
                                         _(".merge-left.r%ld"),
                                         older_rev);
  const char *right_label = apr_psprintf (subpool,
                                          _(".merge-right.r%ld"),
                                          yours_rev);
  svn_boolean_t has_local_mods;
  svn_boolean_t merge_required = TRUE;
  enum svn_wc_merge_outcome_t merge_outcome;

  /* Easy out:  no access baton means there ain't no merge target */
  if (adm_access == NULL)
    {
      if (content_state)
        *content_state = svn_wc_notify_state_missing;
      if (prop_state)
        *prop_state = svn_wc_notify_state_missing;
      return SVN_NO_ERROR;
    }
  
  /* Other easy outs:  if the merge target isn't under version
     control, or is just missing from disk, fogettaboutit.  There's no
     way svn_wc_merge() can do the merge. */
  {
    const svn_wc_entry_t *entry;
    svn_node_kind_t kind;

    SVN_ERR (svn_wc_entry (&entry, mine, adm_access, FALSE, subpool));
    SVN_ERR (svn_io_check_path (mine, &kind, subpool));

    /* ### a future thought:  if the file is under version control,
       but the working file is missing, maybe we can 'restore' the
       working file from the text-base, and then allow the merge to run?  */

    if ((! entry) || (kind != svn_node_file))
      {
        if (content_state)
          *content_state = svn_wc_notify_state_missing;
        if (prop_state)
          *prop_state = svn_wc_notify_state_missing;
        return SVN_NO_ERROR;
      }
  }

  /* This callback is essentially no more than a wrapper around
     svn_wc_merge().  Thank goodness that all the
     diff-editor-mechanisms are doing the hard work of getting the
     fulltexts! */

  /* Do property merge before text merge so that keyword expansion takes
     into account the new property values. */
  if (prop_changes->nelts > 0)
    SVN_ERR (merge_props_changed (adm_access, prop_state, mine, prop_changes,
                                  original_props, baton));
  else
    if (prop_state)
      *prop_state = svn_wc_notify_state_unchanged;

  if (older)
    {
      SVN_ERR (svn_wc_text_modified_p (&has_local_mods, mine, FALSE,
                                       adm_access, subpool));

      /* Special case:  if a binary file isn't locally modified, and is
         exactly identical to the 'left' side of the merge, then don't
         allow svn_wc_merge to produce a conflict.  Instead, just
         overwrite the working file with the 'right' side of the merge. */
      if ((! has_local_mods)
          && ((mimetype1 && svn_mime_type_is_binary (mimetype1))
              || (mimetype2 && svn_mime_type_is_binary (mimetype2))))
        {
          svn_boolean_t same_contents;
          SVN_ERR (svn_io_files_contents_same_p (&same_contents,
                                                 older, mine, subpool));
          if (same_contents)
            {
              if (! merge_b->dry_run)
                SVN_ERR (svn_io_file_rename (yours, mine, subpool));          
              merge_outcome = svn_wc_merge_merged;
              merge_required = FALSE;
            }
        }  

      if (merge_required)
        {
          SVN_ERR (svn_wc_merge (older, yours, mine, adm_access,
                                 left_label, right_label, target_label,
                                 merge_b->dry_run, &merge_outcome, 
                                 merge_b->diff3_cmd, subpool));
        }

      /* Philip asks "Why?"  Why does the notification depend on whether the
         file had modifications before the merge?  If the merge didn't change
         the file because the local mods already included the change why does
         that result it "merged" notification?  That's information available
         through the status command, while the fact that the merge didn't
         change the file is lost :-( */

      if (content_state)
        {
          if (merge_outcome == svn_wc_merge_conflict)
            *content_state = svn_wc_notify_state_conflicted;
          else if (has_local_mods)
            *content_state = svn_wc_notify_state_merged;
          else if (merge_outcome == svn_wc_merge_merged)
            *content_state = svn_wc_notify_state_changed;
          else if (merge_outcome == svn_wc_merge_no_merge)
            *content_state = svn_wc_notify_state_missing;
          else /* merge_outcome == svn_wc_merge_unchanged */
            *content_state = svn_wc_notify_state_unchanged;
        }
    }

  svn_pool_destroy (subpool);
  return SVN_NO_ERROR;
}

/* A svn_wc_diff_callbacks2_t function. */
static svn_error_t *
merge_file_added (svn_wc_adm_access_t *adm_access,
                  svn_wc_notify_state_t *content_state,
                  svn_wc_notify_state_t *prop_state,
                  const char *mine,
                  const char *older,
                  const char *yours,
                  svn_revnum_t rev1,
                  svn_revnum_t rev2,
                  const char *mimetype1,
                  const char *mimetype2,
                  const apr_array_header_t *prop_changes,
                  apr_hash_t *original_props,
                  void *baton)
{
  struct merge_cmd_baton *merge_b = baton;
  apr_pool_t *subpool = svn_pool_create (merge_b->pool);
  svn_node_kind_t kind;
  const char *copyfrom_url;
  const char *child;
  int i;
  apr_hash_t *new_props;

  /* In most cases, we just leave prop_state as unknown, and let the
     content_state what happened, so we set prop_state here to avoid that
     below. */
  if (prop_state)
    *prop_state = svn_wc_notify_state_unknown;

  /* Apply the prop changes to a new hash table. */
  new_props = apr_hash_copy (subpool, original_props);
  for (i = 0; i < prop_changes->nelts; ++i)
    {
      const svn_prop_t *prop = &APR_ARRAY_IDX (prop_changes, i, svn_prop_t);
      apr_hash_set (new_props, prop->name, APR_HASH_KEY_STRING, prop->value);
    }

  /* Easy out:  if we have no adm_access for the parent directory,
     then this portion of the tree-delta "patch" must be inapplicable.
     Send a 'missing' state back;  the repos-diff editor should then
     send a 'skip' notification. */
  if (! adm_access)
    {
      if (merge_b->dry_run && merge_b->added_path
          && svn_path_is_child (merge_b->added_path, mine, subpool))
        {
          if (content_state)
            *content_state = svn_wc_notify_state_changed;
          if (prop_state && apr_hash_count (new_props))
            *prop_state = svn_wc_notify_state_changed;
        }
      else
        *content_state = svn_wc_notify_state_missing;
      return SVN_NO_ERROR;
    }

  SVN_ERR (svn_io_check_path (mine, &kind, subpool));
  switch (kind)
    {
    case svn_node_none:
      {
        const svn_wc_entry_t *entry;
        SVN_ERR (svn_wc_entry (&entry, mine, adm_access, FALSE, subpool));
        if (entry && entry->schedule != svn_wc_schedule_delete)
          {
            /* It's versioned but missing. */
            if (content_state)
              *content_state = svn_wc_notify_state_obstructed;
            return SVN_NO_ERROR;
          }
        if (! merge_b->dry_run)
          {
            child = svn_path_is_child(merge_b->target, mine, merge_b->pool);
            assert (child != NULL);
            copyfrom_url = svn_path_url_add_component (merge_b->url, child,
                                                       merge_b->pool);
            SVN_ERR (check_schema_match (adm_access, copyfrom_url));

            /* Since 'mine' doesn't exist, and this is
               'merge_file_added', I hope it's safe to assume that
               'older' is empty, and 'yours' is the full file.  Merely
               copying 'yours' to 'mine', isn't enough; we need to get
               the whole text-base and props installed too, just as if
               we had called 'svn cp wc wc'. */

            SVN_ERR (svn_wc_add_repos_file (mine, adm_access,
                                            yours,
                                            new_props,
                                            copyfrom_url,
                                            rev2,
                                            merge_b->pool));
          }
        if (content_state)
          *content_state = svn_wc_notify_state_changed;
        if (prop_state && apr_hash_count (new_props))
          *prop_state = svn_wc_notify_state_changed;
      }
      break;
    case svn_node_dir:
      {
        /* this will make the repos_editor send a 'skipped' message */
        if (content_state)
          *content_state = svn_wc_notify_state_obstructed;
      }
    case svn_node_file:
      {
        /* file already exists, is it under version control? */
        const svn_wc_entry_t *entry;
        SVN_ERR (svn_wc_entry (&entry, mine, adm_access, FALSE, subpool));

        /* If it's an unversioned file, don't touch it.  If its scheduled
           for deletion, then rm removed it from the working copy and the
           user must have recreated it, don't touch it */
        if (!entry || entry->schedule == svn_wc_schedule_delete)
          {
            /* this will make the repos_editor send a 'skipped' message */
            if (content_state)
              *content_state = svn_wc_notify_state_obstructed;
          }
        else
          {
            SVN_ERR (merge_file_changed (adm_access, content_state, prop_state,
                                         mine, older, yours,
                                         rev1, rev2,
                                         mimetype1, mimetype2,
                                         prop_changes, original_props,
                                         baton));            
          }
        break;      
      }
    default:
      if (content_state)
        *content_state = svn_wc_notify_state_unknown;
      break;
    }

  svn_pool_destroy (subpool);
  return SVN_NO_ERROR;
}

/* A svn_wc_diff_callbacks2_t function. */
static svn_error_t *
merge_file_deleted (svn_wc_adm_access_t *adm_access,
                    svn_wc_notify_state_t *state,
                    const char *mine,
                    const char *older,
                    const char *yours,
                    const char *mimetype1,
                    const char *mimetype2,
                    apr_hash_t *original_props,
                    void *baton)
{
  struct merge_cmd_baton *merge_b = baton;
  apr_pool_t *subpool = svn_pool_create (merge_b->pool);
  svn_node_kind_t kind;
  svn_wc_adm_access_t *parent_access;
  const char *parent_path;
  svn_error_t *err;

  /* Easy out:  if we have no adm_access for the parent directory,
     then this portion of the tree-delta "patch" must be inapplicable.
     Send a 'missing' state back;  the repos-diff editor should then
     send a 'skip' notification. */
  if (! adm_access)
    {
      if (state)
        *state = svn_wc_notify_state_missing;
      return SVN_NO_ERROR;
    }

  SVN_ERR (svn_io_check_path (mine, &kind, subpool));
  switch (kind)
    {
    case svn_node_file:
      svn_path_split (mine, &parent_path, NULL, merge_b->pool);
      SVN_ERR (svn_wc_adm_retrieve (&parent_access, adm_access, parent_path,
                                    merge_b->pool));
      {
        /* Passing NULL for the notify_func and notify_baton because
         * repos_diff.c:delete_item will do it for us. */
        err = svn_client__wc_delete (mine, parent_access, merge_b->force,
                                     merge_b->dry_run, 
                                     NULL, NULL,
                                     merge_b->ctx, subpool);
      }
      if (err && state)
        {
          *state = svn_wc_notify_state_obstructed;
          svn_error_clear (err);
        }
      else if (state)
        {
          *state = svn_wc_notify_state_changed;
        }
      break;
    case svn_node_dir:
      if (state)
        *state = svn_wc_notify_state_obstructed;
      break;
    case svn_node_none:
      /* file is already non-existent, this is a no-op. */
      if (state)
        *state = svn_wc_notify_state_missing;
      break;
    default:
      if (state)
        *state = svn_wc_notify_state_unknown;
      break;
    }
    
  svn_pool_destroy (subpool);
  return SVN_NO_ERROR;
}

/* A svn_wc_diff_callbacks2_t function. */
static svn_error_t *
merge_dir_added (svn_wc_adm_access_t *adm_access,
                 svn_wc_notify_state_t *state,
                 const char *path,
                 svn_revnum_t rev,
                 void *baton)
{
  struct merge_cmd_baton *merge_b = baton;
  apr_pool_t *subpool = svn_pool_create (merge_b->pool);
  svn_node_kind_t kind;
  const svn_wc_entry_t *entry;
  const char *copyfrom_url, *child;

  /* Easy out:  if we have no adm_access for the parent directory,
     then this portion of the tree-delta "patch" must be inapplicable.
     Send a 'missing' state back;  the repos-diff editor should then
     send a 'skip' notification. */
  if (! adm_access)
    {
      if (state)
        {
          if (merge_b->dry_run && merge_b->added_path
              && svn_path_is_child (merge_b->added_path, path, subpool))
            *state = svn_wc_notify_state_changed;
          else
            *state = svn_wc_notify_state_missing;
        }
      return SVN_NO_ERROR;
    }

  child = svn_path_is_child (merge_b->target, path, subpool);
  assert (child != NULL);
  copyfrom_url = svn_path_url_add_component (merge_b->url, child, subpool);
  SVN_ERR (check_schema_match (adm_access, copyfrom_url));

  SVN_ERR (svn_io_check_path (path, &kind, subpool));
  switch (kind)
    {
    case svn_node_none:
      SVN_ERR (svn_wc_entry (&entry, path, adm_access, FALSE, subpool));
      if (entry && entry->schedule != svn_wc_schedule_delete)
        {
          /* Versioned but missing */
          if (state)
            *state = svn_wc_notify_state_obstructed;
          return SVN_NO_ERROR;
        }
      if (! merge_b->dry_run)
        {
          SVN_ERR (svn_io_make_dir_recursively (path, subpool));
          SVN_ERR (svn_wc_add (path, adm_access,
                               copyfrom_url, rev,
                               merge_b->ctx->cancel_func,
                               merge_b->ctx->cancel_baton,
                               NULL, NULL, /* don't pass notification func! */
                               merge_b->pool));

        }
      if (merge_b->dry_run)
        merge_b->added_path = apr_pstrdup (merge_b->pool, path);
      if (state)
        *state = svn_wc_notify_state_changed;
      break;
    case svn_node_dir:
      /* Adding an unversioned directory doesn't destroy data */
      SVN_ERR (svn_wc_entry (&entry, path, adm_access, TRUE, subpool));
      if (! entry || (entry && entry->schedule == svn_wc_schedule_delete))
        {
          if (!merge_b->dry_run)
            SVN_ERR (svn_wc_add (path, adm_access,
                                 copyfrom_url,
                                 merge_b->revision->value.number,
                                 merge_b->ctx->cancel_func,
                                 merge_b->ctx->cancel_baton,
                                 NULL, NULL, /* no notification func! */
                                 merge_b->pool));
          if (merge_b->dry_run)
            merge_b->added_path = apr_pstrdup (merge_b->pool, path);
          if (state)
            *state = svn_wc_notify_state_changed;
        }
      else
        {
          if (state)
            *state = svn_wc_notify_state_obstructed;
        }
      break;
    case svn_node_file:
      if (merge_b->dry_run)
        merge_b->added_path = NULL;
      if (state)
        *state = svn_wc_notify_state_obstructed;
      break;
    default:
      if (merge_b->dry_run)
        merge_b->added_path = NULL;
      if (state)
        *state = svn_wc_notify_state_unknown;
      break;
    }

  svn_pool_destroy (subpool);
  return SVN_NO_ERROR;
}

/* Struct used for as the baton for calling merge_delete_notify_func(). */
typedef struct merge_delete_notify_baton_t
{
  svn_client_ctx_t *ctx;

  /** path to skip */
  const char *path_skip;
} merge_delete_notify_baton_t;

/* Notify callback function that wraps the normal callback
 * function to remove a notification that will be sent twice
 * and set the proper action. */
static void
merge_delete_notify_func (void *baton,
                          const char *path,
                          svn_wc_notify_action_t action,
                          svn_node_kind_t kind,
                          const char *mime_type,
                          svn_wc_notify_state_t content_state,
                          svn_wc_notify_state_t prop_state,
                          svn_revnum_t revision)
{
  merge_delete_notify_baton_t *mdb = (merge_delete_notify_baton_t *) baton;

  /* Skip the notification for the path we called svn_client__wc_delete() with,
   * because it will be outputed by repos_diff.c:delete_item */  
  if (strcmp (path,mdb->path_skip) != 0)
    return;<commit_msg>Fix an error in the style fix. oops.<commit_after>/*
 * diff.c: comparing and merging
 *
 * ====================================================================
 * Copyright (c) 2000-2004 CollabNet.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://subversion.tigris.org/license-1.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 *
 * This software consists of voluntary contributions made by many
 * individuals.  For exact contribution history, see the revision
 * history and logs, available at http://subversion.tigris.org/.
 * ====================================================================
 */

/* ==================================================================== */




/*** Includes. ***/

#include <apr_strings.h>
#include <apr_pools.h>
#include <apr_hash.h>
#include "svn_wc.h"
#include "svn_delta.h"
#include "svn_diff.h"
#include "svn_client.h"
#include "svn_string.h"
#include "svn_error.h"
#include "svn_path.h"
#include "svn_io.h"
#include "svn_utf.h"
#include "svn_pools.h"
#include "svn_config.h"
#include "svn_props.h"
#include "client.h"
#include <assert.h>

#include "svn_private_config.h"

/*
 * Constant separator strings
 */
static const char equal_string[] = 
  "===================================================================";
static const char under_string[] =
  "___________________________________________________________________";


/*-----------------------------------------------------------------*/


/* Utilities */

/* Wrapper for @c apr_file_printf(), which see.  FORMAT is a utf8-encoded
   string after it is formatted, so this function can convert it to
   native encoding before printing. */
static svn_error_t *
file_printf_from_utf8 (apr_file_t *fptr, const char *format, ...)
{
  va_list ap;
  const char *buf, *buf_apr;

  va_start (ap, format);
  buf = apr_pvsprintf (apr_file_pool_get (fptr), format, ap); 
  va_end(ap);

  SVN_ERR (svn_utf_cstring_from_utf8 (&buf_apr, buf,
                                       apr_file_pool_get (fptr)));

  return svn_io_file_write_full (fptr, buf_apr, strlen (buf_apr), 
                                 NULL, apr_file_pool_get (fptr));
}


/* A helper func that writes out verbal descriptions of property diffs
   to FILE.   Of course, the apr_file_t will probably be the 'outfile'
   passed to svn_client_diff2, which is probably stdout. */
static svn_error_t *
display_prop_diffs (const apr_array_header_t *propchanges,
                    apr_hash_t *original_props,
                    const char *path,
                    apr_file_t *file,
                    apr_pool_t *pool)
{
  int i;

  SVN_ERR (file_printf_from_utf8 (file,
                                  _("%sProperty changes on: %s%s"),
                                  APR_EOL_STR,
                                  svn_path_local_style (path, pool),
                                  APR_EOL_STR));

  /* ### todo [issue #1533]: Use file_printf_from_utf8() to convert this
     line of dashes to native encoding, at least conditionally?  Or is
     it better to have under_string always output the same, so
     programs can find it?  Also, what about checking for error? */
  apr_file_printf (file, "%s" APR_EOL_STR, under_string);

  for (i = 0; i < propchanges->nelts; i++)
    {
      const svn_prop_t *propchange
        = &APR_ARRAY_IDX(propchanges, i, svn_prop_t);

      const svn_string_t *original_value;

      if (original_props)
        original_value = apr_hash_get (original_props, 
                                       propchange->name, APR_HASH_KEY_STRING);
      else
        original_value = NULL;
      
      SVN_ERR (file_printf_from_utf8 (file, _("Name: %s%s"),
                                      propchange->name, APR_EOL_STR));

      /* For now, we have a rather simple heuristic: if this is an
         "svn:" property, then assume the value is UTF-8 and must
         therefore be converted before printing.  Otherwise, just
         print whatever's there and hope for the best. */
      {
        svn_boolean_t val_is_utf8 = svn_prop_is_svn_prop (propchange->name);
        
        if (original_value != NULL)
          {
            if (val_is_utf8)
              {
                SVN_ERR (file_printf_from_utf8
                         (file, "   - %s" APR_EOL_STR, original_value->data));
              }
            else
              {
                /* ### todo: check for error? */
                apr_file_printf
                  (file, "   - %s" APR_EOL_STR, original_value->data);
              }
          }
        
        if (propchange->value != NULL)
          {
            if (val_is_utf8)
              {
                SVN_ERR (file_printf_from_utf8
                         (file, "   + %s" APR_EOL_STR,
                          propchange->value->data));
              }
            else
              {
                /* ### todo: check for error? */
                apr_file_printf (file, "   + %s" APR_EOL_STR,
                                 propchange->value->data);
              }
          }
      }
    }

  /* ### todo [issue #1533]: Use file_printf_from_utf8() to convert this
     to native encoding, at least conditionally?  Or is it better to
     have under_string always output the same eol, so programs can
     find it consistently?  Also, what about checking for error? */
  apr_file_printf (file, APR_EOL_STR);

  return SVN_NO_ERROR;
}


/* Return SVN_ERR_UNSUPPORTED_FEATURE if URL's schema does not
   match the schema of the url for ADM_ACCESS's path; return
   SVN_ERR_BAD_URL if no schema can be found for one or both urls;
   otherwise return SVN_NO_ERROR.  Use @a ADM_ACCESS's pool for
   temporary allocation. */
static svn_error_t *
check_schema_match (svn_wc_adm_access_t *adm_access, const char *url)
{
  const char *path = svn_wc_adm_access_path (adm_access);
  apr_pool_t *pool = svn_wc_adm_access_pool (adm_access);
  const svn_wc_entry_t *ent;
  const char *idx1, *idx2;
  
  SVN_ERR (svn_wc_entry (&ent, path, adm_access, TRUE, pool));
  
  idx1 = strchr (url, ':');
  idx2 = strchr (ent->url, ':');

  if ((idx1 == NULL) && (idx2 == NULL))
    {
      return svn_error_createf
        (SVN_ERR_BAD_URL, NULL,
         _("URLs have no schema ('%s' and '%s')"), url, ent->url);
    }
  else if (idx1 == NULL)
    {
      return svn_error_createf
        (SVN_ERR_BAD_URL, NULL,
         _("URL has no schema: '%s'"), url);
    }
  else if (idx2 == NULL)
    {
      return svn_error_createf
        (SVN_ERR_BAD_URL, NULL,
         _("URL has no schema: '%s'"), ent->url);
    }
  else if (((idx1 - url) != (idx2 - ent->url))
           || (strncmp (url, ent->url, idx1 - url) != 0))
    {
      return svn_error_createf
        (SVN_ERR_UNSUPPORTED_FEATURE, NULL,
         _("Access schema mixtures not yet supported ('%s' and '%s')"),
         url, ent->url);
    }

  /* else */

  return SVN_NO_ERROR;
}


/*-----------------------------------------------------------------*/


/*** Callbacks for 'svn diff', invoked by the repos-diff editor. ***/


struct diff_cmd_baton {
  const apr_array_header_t *options;
  apr_pool_t *pool;
  apr_file_t *outfile;
  apr_file_t *errfile;

  /* The original targets passed to the diff command.  We may need
     these to construct distinctive diff labels when comparing the
     same relative path in the same revision, under different anchors
     (for example, when comparing a trunk against a branch). */
  const char *orig_path_1;
  const char *orig_path_2;

  /* These are the numeric representations of the revisions passed to
     svn_client_diff2, either may be SVN_INVALID_REVNUM.  We need these
     because some of the svn_wc_diff_callbacks2_t don't get revision
     arguments.

     ### Perhaps we should change the callback signatures and eliminate
     ### these?
  */
  svn_revnum_t revnum1;
  svn_revnum_t revnum2;

  /* Client config hash (may be NULL). */
  apr_hash_t *config;

  /* Set this if you want diff output even for binary files. */
  svn_boolean_t force_binary;

  /* Set this flag if you want diff_file_changed to output diffs
     unconditionally, even if the diffs are empty. */
  svn_boolean_t force_empty;

};


/* Generate a label for the diff output for file PATH at revision REVNUM.
   If REVNUM is invalid then it is assumed to be the current working
   copy.  Assumes the paths are already in the desired style (local
   vs internal).  Allocate the label in POOL. */
static const char *
diff_label (const char *path,
            svn_revnum_t revnum,
            apr_pool_t *pool)
{
  const char *label;
  if (revnum != SVN_INVALID_REVNUM)
    label = apr_psprintf (pool, _("%s\t(revision %ld)"), path, revnum);
  else
    label = apr_psprintf (pool, _("%s\t(working copy)"), path);

  return label;
}

/* A svn_wc_diff_callbacks2_t function.  Used for both file and directory
   property diffs. */
static svn_error_t *
diff_props_changed (svn_wc_adm_access_t *adm_access,
                    svn_wc_notify_state_t *state,
                    const char *path,
                    const apr_array_header_t *propchanges,
                    apr_hash_t *original_props,
                    void *diff_baton)
{
  struct diff_cmd_baton *diff_cmd_baton = diff_baton;
  apr_array_header_t *props;
  apr_pool_t *subpool = svn_pool_create (diff_cmd_baton->pool);

  SVN_ERR (svn_categorize_props (propchanges, NULL, NULL, &props, subpool));

  if (props->nelts > 0)
    SVN_ERR (display_prop_diffs (props, original_props, path,
                                 diff_cmd_baton->outfile, subpool));

  if (state)
    *state = svn_wc_notify_state_unknown;

  svn_pool_destroy (subpool);
  return SVN_NO_ERROR;
}

/* Show differences between TMPFILE1 and TMPFILE2. PATH, REV1, and REV2 are
   used in the headers to indicate the file and revisions.  If either
   MIMETYPE1 or MIMETYPE2 indicate binary content, don't show a diff,
   but instread print a warning message. */
static svn_error_t *
diff_content_changed (const char *path,
                      const char *tmpfile1,
                      const char *tmpfile2,
                      svn_revnum_t rev1,
                      svn_revnum_t rev2,
                      const char *mimetype1,
                      const char *mimetype2,
                      void *diff_baton)
{
  struct diff_cmd_baton *diff_cmd_baton = diff_baton;
  const char *diff_cmd = NULL;
  const char **args = NULL;
  int nargs, exitcode;
  apr_pool_t *subpool = svn_pool_create (diff_cmd_baton->pool);
  svn_stream_t *os;
  apr_file_t *errfile = diff_cmd_baton->errfile;
  const char *label1, *label2;
  svn_boolean_t mt1_binary = FALSE, mt2_binary = FALSE;
  const char *path1, *path2;
  int i;

  /* Get a stream from our output file. */
  os = svn_stream_from_aprfile(diff_cmd_baton->outfile, subpool); 

  /* Assemble any option args. */
  nargs = diff_cmd_baton->options->nelts;
  if (nargs)
    {
      args = apr_palloc (subpool, nargs * sizeof (char *));
      for (i = 0; i < diff_cmd_baton->options->nelts; i++)
        {
          args[i] = 
            ((const char **)(diff_cmd_baton->options->elts))[i];
        }
      assert (i == nargs);
    }

  /* Generate the diff headers. */

  /* ### Holy cow.  Due to anchor/target weirdness, we can't
     simply join diff_cmd_baton->orig_path_1 with path, ditto for
     orig_path_2.  That will work when they're directory URLs, but
     not for file URLs.  Nor can we just use anchor1 and anchor2
     from do_diff(), at least not without some more logic here.
     What a nightmare.
     
     For now, to distinguish the two paths, we'll just put the
     unique portions of the original targets in parentheses before
     the received path, with ellipses for handwaving.  This makes
     the labels a bit clumsy, but at least distinctive.  Better
     solutions are possible, they'll just take more thought. */

  path1 = diff_cmd_baton->orig_path_1;
  path2 = diff_cmd_baton->orig_path_2;
  
  for (i = 0; path1[i] && path2[i] && (path1[i] == path2[i]); i++)
    ;
  
  /* Make sure the prefix is made of whole components. (Issue #1771) */
  if (path1[i] || path2[i])
    {
      for ( ; (i > 0) && (path1[i] != '/'); i--)
        ;
    }
  
  path1 = path1 + i;
  path2 = path2 + i;
  
  /* ### Should diff labels print paths in local style?  Is there
     already a standard for this?  In any case, this code depends on
     a particular style, so not calling svn_path_local_style() on the
     paths below.*/
  if (path1[0] == '\0')
    path1 = apr_psprintf (subpool, "%s", path);
  else if (path1[0] == '/')
    path1 = apr_psprintf (subpool, "%s\t(...%s)", path, path1);
  else
    path1 = apr_psprintf (subpool, "%s\t(.../%s)", path, path1);
  
  if (path2[0] == '\0')
    path2 = apr_psprintf (subpool, "%s", path);
  else if (path2[0] == '/')
    path2 = apr_psprintf (subpool, "%s\t(...%s)", path, path2);
  else
    path2 = apr_psprintf (subpool, "%s\t(.../%s)", path, path2);
  
  label1 = diff_label (path1, rev1, subpool);
  label2 = diff_label (path2, rev2, subpool);

  /* Possible easy-out: if either mime-type is binary and force was not
     specified, don't attempt to generate a viewable diff at all.
     Print a warning and exit. */
  if (mimetype1)
    mt1_binary = svn_mime_type_is_binary (mimetype1);
  if (mimetype2)
    mt2_binary = svn_mime_type_is_binary (mimetype2);

  if (! diff_cmd_baton->force_binary && (mt1_binary || mt2_binary))
    {
      /* Print out the diff header. */
      SVN_ERR (svn_stream_printf (os, subpool,
                                  "Index: %s" APR_EOL_STR
                                  "%s" APR_EOL_STR, path, equal_string));

      SVN_ERR (svn_stream_printf
               (os, subpool,
                _("Cannot display: file marked as a binary type.%s"),
                APR_EOL_STR));
      
      if (mt1_binary && !mt2_binary)
        SVN_ERR (svn_stream_printf (os, subpool,
                                    "svn:mime-type = %s" APR_EOL_STR,
                                    mimetype1));
      else if (mt2_binary && !mt1_binary)
        SVN_ERR (svn_stream_printf (os, subpool,
                                    "svn:mime-type = %s" APR_EOL_STR,
                                    mimetype2));
      else if (mt1_binary && mt2_binary)
        {
          if (strcmp (mimetype1, mimetype2) == 0)
            SVN_ERR (svn_stream_printf
                     (os, subpool,
                      "svn:mime-type = %s" APR_EOL_STR,
                      mimetype1));
          else
            SVN_ERR (svn_stream_printf
                     (os, subpool,
                      "svn:mime-type = (%s, %s)" APR_EOL_STR,
                      mimetype1, mimetype2));
        }

      /* Exit early. */
      svn_pool_destroy (subpool);
      return SVN_NO_ERROR;
    }


  /* Find out if we need to run an external diff */
  if (diff_cmd_baton->config)
    {
      svn_config_t *cfg = apr_hash_get (diff_cmd_baton->config,
                                        SVN_CONFIG_CATEGORY_CONFIG,
                                        APR_HASH_KEY_STRING);
      svn_config_get (cfg, &diff_cmd, SVN_CONFIG_SECTION_HELPERS,
                      SVN_CONFIG_OPTION_DIFF_CMD, NULL);
    }

  if (diff_cmd)
    {
      /* Print out the diff header. */
      SVN_ERR (svn_stream_printf (os, subpool,
                                  "Index: %s" APR_EOL_STR
                                  "%s" APR_EOL_STR, path, equal_string));
      /* Close the stream (flush) */
      SVN_ERR (svn_stream_close (os));

      SVN_ERR (svn_io_run_diff (".", args, nargs, label1, label2,
                                tmpfile1, tmpfile2, 
                                &exitcode, diff_cmd_baton->outfile, errfile,
                                diff_cmd, subpool));
    }
  else   /* use libsvn_diff to generate the diff  */
    {
      svn_diff_t *diff;

      /* We don't currently support any options (well, other than -u, since we 
         default to unified diff output anyway), so if we received anything 
         other than that it's an error. */
      if (diff_cmd_baton->options)
        {
          for (i = 0; i < diff_cmd_baton->options->nelts; ++i)
            {
              const char *arg
                = ((const char **)(diff_cmd_baton->options->elts))[i];

              if (strcmp(arg, "-u") == 0)
                continue;
              else
                return svn_error_createf(SVN_ERR_INVALID_DIFF_OPTION, NULL,
                                         _("'%s' is not supported"), arg);
            }
        }

      SVN_ERR (svn_diff_file_diff (&diff, tmpfile1, tmpfile2, subpool));

      if (svn_diff_contains_diffs (diff) || diff_cmd_baton->force_empty)
        {
          /* Print out the diff header. */
          SVN_ERR (svn_stream_printf (os, subpool,
                                      "Index: %s" APR_EOL_STR
                                      "%s" APR_EOL_STR, path, equal_string));

          /* Output the actual diff */
          SVN_ERR (svn_diff_file_output_unified (os, diff,
                                                 tmpfile1, tmpfile2,
                                                 label1, label2,
                                                 subpool));
        }
    }

  /* ### todo: someday we'll need to worry about whether we're going
     to need to write a diff plug-in mechanism that makes use of the
     two paths, instead of just blindly running SVN_CLIENT_DIFF.  */

  /* Destroy the subpool. */
  svn_pool_destroy (subpool);

  return SVN_NO_ERROR;
}

/* A svn_wc_diff_callbacks2_t function. */
static svn_error_t *
diff_file_changed (svn_wc_adm_access_t *adm_access,
                   svn_wc_notify_state_t *content_state,
                   svn_wc_notify_state_t *prop_state,
                   const char *path,
                   const char *tmpfile1,
                   const char *tmpfile2,
                   svn_revnum_t rev1,
                   svn_revnum_t rev2,
                   const char *mimetype1,
                   const char *mimetype2,
                   const apr_array_header_t *prop_changes,
                   apr_hash_t *original_props,
                   void *diff_baton)
{
  if (tmpfile1)
    SVN_ERR (diff_content_changed (path,
                                   tmpfile1, tmpfile2, rev1, rev2,
                                   mimetype1, mimetype2, diff_baton));
  if (prop_changes->nelts > 0)
    SVN_ERR (diff_props_changed (adm_access, prop_state, path, prop_changes,
                                 original_props, diff_baton));
  if (content_state)
    *content_state = svn_wc_notify_state_unknown;
  if (prop_state)
    *prop_state = svn_wc_notify_state_unknown;
  return SVN_NO_ERROR;
}

/* Because the repos-diff editor passes at least one empty file to
   each of these next two functions, they can be dumb wrappers around
   the main workhorse routine. */

/* A svn_wc_diff_callbacks2_t function. */
static svn_error_t *
diff_file_added (svn_wc_adm_access_t *adm_access,
                 svn_wc_notify_state_t *content_state,
                 svn_wc_notify_state_t *prop_state,
                 const char *path,
                 const char *tmpfile1,
                 const char *tmpfile2,
                 svn_revnum_t rev1,
                 svn_revnum_t rev2,
                 const char *mimetype1,
                 const char *mimetype2,
                 const apr_array_header_t *prop_changes,
                 apr_hash_t *original_props,
                 void *diff_baton)
{
  struct diff_cmd_baton *diff_cmd_baton = diff_baton;

  /* We want diff_file_changed to unconditionally show diffs, even if
     the diff is empty (as would be the case if an empty file were
     added.)  It's important, because 'patch' would still see an empty
     diff and create an empty file.  It's also important to let the
     user see that *something* happened. */
  diff_cmd_baton->force_empty = TRUE;

  SVN_ERR (diff_file_changed (adm_access, content_state, prop_state, path,
                              tmpfile1, tmpfile2, 
                              rev1, rev2,
                              mimetype1, mimetype2,
                              prop_changes, original_props, diff_baton));
  
  diff_cmd_baton->force_empty = FALSE;

  return SVN_NO_ERROR;
}

/* A svn_wc_diff_callbacks2_t function. */
static svn_error_t *
diff_file_deleted_with_diff (svn_wc_adm_access_t *adm_access,
                             svn_wc_notify_state_t *state,
                             const char *path,
                             const char *tmpfile1,
                             const char *tmpfile2,
                             const char *mimetype1,
                             const char *mimetype2,
                             apr_hash_t *original_props,
                             void *diff_baton)
{
  struct diff_cmd_baton *diff_cmd_baton = diff_baton;

  /* We don't list all the deleted properties. */
  return diff_file_changed (adm_access, state, NULL, path,
                            tmpfile1, tmpfile2, 
                            diff_cmd_baton->revnum1, diff_cmd_baton->revnum2,
                            mimetype1, mimetype2,
                            apr_array_make (diff_cmd_baton->pool, 1,
                                            sizeof(svn_prop_t)),
                            apr_hash_make (diff_cmd_baton->pool), diff_baton);
}

/* A svn_wc_diff_callbacks2_t function. */
static svn_error_t *
diff_file_deleted_no_diff (svn_wc_adm_access_t *adm_access,
                           svn_wc_notify_state_t *state,
                           const char *path,
                           const char *tmpfile1,
                           const char *tmpfile2,
                           const char *mimetype1,
                           const char *mimetype2,
                           apr_hash_t *original_props,
                           void *diff_baton)
{
  struct diff_cmd_baton *diff_cmd_baton = diff_baton;

  if (state)
    *state = svn_wc_notify_state_unknown;

  SVN_ERR (file_printf_from_utf8
           (diff_cmd_baton->outfile,
            "Index: %s (deleted)" APR_EOL_STR "%s" APR_EOL_STR, 
            path, equal_string));

  return SVN_NO_ERROR;
}

/* A svn_wc_diff_callbacks2_t function.
   For now, let's have 'svn diff' send feedback to the top-level
   application, so that something reasonable about directories and
   propsets gets printed to stdout. */
static svn_error_t *
diff_dir_added (svn_wc_adm_access_t *adm_access,
                svn_wc_notify_state_t *state,
                const char *path,
                svn_revnum_t rev,
                void *diff_baton)
{
  if (state)
    *state = svn_wc_notify_state_unknown;

  /* ### todo:  send feedback to app */
  return SVN_NO_ERROR;
}

/* A svn_wc_diff_callbacks2_t function. */
static svn_error_t *
diff_dir_deleted (svn_wc_adm_access_t *adm_access,
                  svn_wc_notify_state_t *state,
                  const char *path,
                  void *diff_baton)
{
  if (state)
    *state = svn_wc_notify_state_unknown;

  return SVN_NO_ERROR;
}
  

/*-----------------------------------------------------------------*/


/*** Callbacks for 'svn merge', invoked by the repos-diff editor. ***/


struct merge_cmd_baton {
  svn_boolean_t force;
  svn_boolean_t dry_run;
  const char *added_path;             /* Set to the dir path whenever the
                                         dir is added as a child of a
                                         versioned dir (dry-run only) */
  const char *target;                 /* Working copy target of merge */
  const char *url;                    /* The second URL in the merge */
  const char *path;                   /* The wc path of the second target, this
                                         can be NULL if we don't have one. */
  const svn_opt_revision_t *revision; /* Revision of second URL in the merge */
  svn_client_ctx_t *ctx;

  /* The diff3_cmd in ctx->config, if any, else null.  We could just
     extract this as needed, but since more than one caller uses it,
     we just set it up when this baton is created. */
  const char *diff3_cmd;

  apr_pool_t *pool;
};

/* A svn_wc_diff_callbacks2_t function.  Used for both file and directory
   property merges. */
static svn_error_t *
merge_props_changed (svn_wc_adm_access_t *adm_access,
                     svn_wc_notify_state_t *state,
                     const char *path,
                     const apr_array_header_t *propchanges,
                     apr_hash_t *original_props,
                     void *baton)
{
  apr_array_header_t *props;
  struct merge_cmd_baton *merge_b = baton;
  apr_pool_t *subpool = svn_pool_create (merge_b->pool);
  svn_error_t *err;

  SVN_ERR (svn_categorize_props (propchanges, NULL, NULL, &props, subpool));

  /* We only want to merge "regular" version properties:  by
     definition, 'svn merge' shouldn't touch any data within .svn/  */
  if (props->nelts)
    {
      err = svn_wc_merge_prop_diffs (state, path, adm_access, props,
                                     FALSE, merge_b->dry_run, subpool);
      if (err && (err->apr_err == SVN_ERR_ENTRY_NOT_FOUND
                  || err->apr_err == SVN_ERR_UNVERSIONED_RESOURCE))
        {
          /* if the entry doesn't exist in the wc, just 'skip' over
             this part of the tree-delta. */
          if (state)
            *state = svn_wc_notify_state_missing;
          svn_error_clear (err);
          return SVN_NO_ERROR;        
        }
      else if (err)
        return err;
    }

  svn_pool_destroy (subpool);
  return SVN_NO_ERROR;
}

/* A svn_wc_diff_callbacks2_t function. */
static svn_error_t *
merge_file_changed (svn_wc_adm_access_t *adm_access,
                    svn_wc_notify_state_t *content_state,
                    svn_wc_notify_state_t *prop_state,
                    const char *mine,
                    const char *older,
                    const char *yours,
                    svn_revnum_t older_rev,
                    svn_revnum_t yours_rev,
                    const char *mimetype1,
                    const char *mimetype2,
                    const apr_array_header_t *prop_changes,
                    apr_hash_t *original_props,
                    void *baton)
{
  struct merge_cmd_baton *merge_b = baton;
  apr_pool_t *subpool = svn_pool_create (merge_b->pool);
  /* xgettext: the '.working', '.merge-left.r%ld' and '.merge-right.r%ld'
     strings are used to tag onto a filename in case of a merge conflict */
  const char *target_label = _(".working");
  const char *left_label = apr_psprintf (subpool,
                                         _(".merge-left.r%ld"),
                                         older_rev);
  const char *right_label = apr_psprintf (subpool,
                                          _(".merge-right.r%ld"),
                                          yours_rev);
  svn_boolean_t has_local_mods;
  svn_boolean_t merge_required = TRUE;
  enum svn_wc_merge_outcome_t merge_outcome;

  /* Easy out:  no access baton means there ain't no merge target */
  if (adm_access == NULL)
    {
      if (content_state)
        *content_state = svn_wc_notify_state_missing;
      if (prop_state)
        *prop_state = svn_wc_notify_state_missing;
      return SVN_NO_ERROR;
    }
  
  /* Other easy outs:  if the merge target isn't under version
     control, or is just missing from disk, fogettaboutit.  There's no
     way svn_wc_merge() can do the merge. */
  {
    const svn_wc_entry_t *entry;
    svn_node_kind_t kind;

    SVN_ERR (svn_wc_entry (&entry, mine, adm_access, FALSE, subpool));
    SVN_ERR (svn_io_check_path (mine, &kind, subpool));

    /* ### a future thought:  if the file is under version control,
       but the working file is missing, maybe we can 'restore' the
       working file from the text-base, and then allow the merge to run?  */

    if ((! entry) || (kind != svn_node_file))
      {
        if (content_state)
          *content_state = svn_wc_notify_state_missing;
        if (prop_state)
          *prop_state = svn_wc_notify_state_missing;
        return SVN_NO_ERROR;
      }
  }

  /* This callback is essentially no more than a wrapper around
     svn_wc_merge().  Thank goodness that all the
     diff-editor-mechanisms are doing the hard work of getting the
     fulltexts! */

  /* Do property merge before text merge so that keyword expansion takes
     into account the new property values. */
  if (prop_changes->nelts > 0)
    SVN_ERR (merge_props_changed (adm_access, prop_state, mine, prop_changes,
                                  original_props, baton));
  else
    if (prop_state)
      *prop_state = svn_wc_notify_state_unchanged;

  if (older)
    {
      SVN_ERR (svn_wc_text_modified_p (&has_local_mods, mine, FALSE,
                                       adm_access, subpool));

      /* Special case:  if a binary file isn't locally modified, and is
         exactly identical to the 'left' side of the merge, then don't
         allow svn_wc_merge to produce a conflict.  Instead, just
         overwrite the working file with the 'right' side of the merge. */
      if ((! has_local_mods)
          && ((mimetype1 && svn_mime_type_is_binary (mimetype1))
              || (mimetype2 && svn_mime_type_is_binary (mimetype2))))
        {
          svn_boolean_t same_contents;
          SVN_ERR (svn_io_files_contents_same_p (&same_contents,
                                                 older, mine, subpool));
          if (same_contents)
            {
              if (! merge_b->dry_run)
                SVN_ERR (svn_io_file_rename (yours, mine, subpool));          
              merge_outcome = svn_wc_merge_merged;
              merge_required = FALSE;
            }
        }  

      if (merge_required)
        {
          SVN_ERR (svn_wc_merge (older, yours, mine, adm_access,
                                 left_label, right_label, target_label,
                                 merge_b->dry_run, &merge_outcome, 
                                 merge_b->diff3_cmd, subpool));
        }

      /* Philip asks "Why?"  Why does the notification depend on whether the
         file had modifications before the merge?  If the merge didn't change
         the file because the local mods already included the change why does
         that result it "merged" notification?  That's information available
         through the status command, while the fact that the merge didn't
         change the file is lost :-( */

      if (content_state)
        {
          if (merge_outcome == svn_wc_merge_conflict)
            *content_state = svn_wc_notify_state_conflicted;
          else if (has_local_mods)
            *content_state = svn_wc_notify_state_merged;
          else if (merge_outcome == svn_wc_merge_merged)
            *content_state = svn_wc_notify_state_changed;
          else if (merge_outcome == svn_wc_merge_no_merge)
            *content_state = svn_wc_notify_state_missing;
          else /* merge_outcome == svn_wc_merge_unchanged */
            *content_state = svn_wc_notify_state_unchanged;
        }
    }

  svn_pool_destroy (subpool);
  return SVN_NO_ERROR;
}

/* A svn_wc_diff_callbacks2_t function. */
static svn_error_t *
merge_file_added (svn_wc_adm_access_t *adm_access,
                  svn_wc_notify_state_t *content_state,
                  svn_wc_notify_state_t *prop_state,
                  const char *mine,
                  const char *older,
                  const char *yours,
                  svn_revnum_t rev1,
                  svn_revnum_t rev2,
                  const char *mimetype1,
                  const char *mimetype2,
                  const apr_array_header_t *prop_changes,
                  apr_hash_t *original_props,
                  void *baton)
{
  struct merge_cmd_baton *merge_b = baton;
  apr_pool_t *subpool = svn_pool_create (merge_b->pool);
  svn_node_kind_t kind;
  const char *copyfrom_url;
  const char *child;
  int i;
  apr_hash_t *new_props;

  /* In most cases, we just leave prop_state as unknown, and let the
     content_state what happened, so we set prop_state here to avoid that
     below. */
  if (prop_state)
    *prop_state = svn_wc_notify_state_unknown;

  /* Apply the prop changes to a new hash table. */
  new_props = apr_hash_copy (subpool, original_props);
  for (i = 0; i < prop_changes->nelts; ++i)
    {
      const svn_prop_t *prop = &APR_ARRAY_IDX (prop_changes, i, svn_prop_t);
      apr_hash_set (new_props, prop->name, APR_HASH_KEY_STRING, prop->value);
    }

  /* Easy out:  if we have no adm_access for the parent directory,
     then this portion of the tree-delta "patch" must be inapplicable.
     Send a 'missing' state back;  the repos-diff editor should then
     send a 'skip' notification. */
  if (! adm_access)
    {
      if (merge_b->dry_run && merge_b->added_path
          && svn_path_is_child (merge_b->added_path, mine, subpool))
        {
          if (content_state)
            *content_state = svn_wc_notify_state_changed;
          if (prop_state && apr_hash_count (new_props))
            *prop_state = svn_wc_notify_state_changed;
        }
      else
        *content_state = svn_wc_notify_state_missing;
      return SVN_NO_ERROR;
    }

  SVN_ERR (svn_io_check_path (mine, &kind, subpool));
  switch (kind)
    {
    case svn_node_none:
      {
        const svn_wc_entry_t *entry;
        SVN_ERR (svn_wc_entry (&entry, mine, adm_access, FALSE, subpool));
        if (entry && entry->schedule != svn_wc_schedule_delete)
          {
            /* It's versioned but missing. */
            if (content_state)
              *content_state = svn_wc_notify_state_obstructed;
            return SVN_NO_ERROR;
          }
        if (! merge_b->dry_run)
          {
            child = svn_path_is_child(merge_b->target, mine, merge_b->pool);
            assert (child != NULL);
            copyfrom_url = svn_path_url_add_component (merge_b->url, child,
                                                       merge_b->pool);
            SVN_ERR (check_schema_match (adm_access, copyfrom_url));

            /* Since 'mine' doesn't exist, and this is
               'merge_file_added', I hope it's safe to assume that
               'older' is empty, and 'yours' is the full file.  Merely
               copying 'yours' to 'mine', isn't enough; we need to get
               the whole text-base and props installed too, just as if
               we had called 'svn cp wc wc'. */

            SVN_ERR (svn_wc_add_repos_file (mine, adm_access,
                                            yours,
                                            new_props,
                                            copyfrom_url,
                                            rev2,
                                            merge_b->pool));
          }
        if (content_state)
          *content_state = svn_wc_notify_state_changed;
        if (prop_state && apr_hash_count (new_props))
          *prop_state = svn_wc_notify_state_changed;
      }
      break;
    case svn_node_dir:
      {
        /* this will make the repos_editor send a 'skipped' message */
        if (content_state)
          *content_state = svn_wc_notify_state_obstructed;
      }
    case svn_node_file:
      {
        /* file already exists, is it under version control? */
        const svn_wc_entry_t *entry;
        SVN_ERR (svn_wc_entry (&entry, mine, adm_access, FALSE, subpool));

        /* If it's an unversioned file, don't touch it.  If its scheduled
           for deletion, then rm removed it from the working copy and the
           user must have recreated it, don't touch it */
        if (!entry || entry->schedule == svn_wc_schedule_delete)
          {
            /* this will make the repos_editor send a 'skipped' message */
            if (content_state)
              *content_state = svn_wc_notify_state_obstructed;
          }
        else
          {
            SVN_ERR (merge_file_changed (adm_access, content_state, prop_state,
                                         mine, older, yours,
                                         rev1, rev2,
                                         mimetype1, mimetype2,
                                         prop_changes, original_props,
                                         baton));            
          }
        break;      
      }
    default:
      if (content_state)
        *content_state = svn_wc_notify_state_unknown;
      break;
    }

  svn_pool_destroy (subpool);
  return SVN_NO_ERROR;
}

/* A svn_wc_diff_callbacks2_t function. */
static svn_error_t *
merge_file_deleted (svn_wc_adm_access_t *adm_access,
                    svn_wc_notify_state_t *state,
                    const char *mine,
                    const char *older,
                    const char *yours,
                    const char *mimetype1,
                    const char *mimetype2,
                    apr_hash_t *original_props,
                    void *baton)
{
  struct merge_cmd_baton *merge_b = baton;
  apr_pool_t *subpool = svn_pool_create (merge_b->pool);
  svn_node_kind_t kind;
  svn_wc_adm_access_t *parent_access;
  const char *parent_path;
  svn_error_t *err;

  /* Easy out:  if we have no adm_access for the parent directory,
     then this portion of the tree-delta "patch" must be inapplicable.
     Send a 'missing' state back;  the repos-diff editor should then
     send a 'skip' notification. */
  if (! adm_access)
    {
      if (state)
        *state = svn_wc_notify_state_missing;
      return SVN_NO_ERROR;
    }

  SVN_ERR (svn_io_check_path (mine, &kind, subpool));
  switch (kind)
    {
    case svn_node_file:
      svn_path_split (mine, &parent_path, NULL, merge_b->pool);
      SVN_ERR (svn_wc_adm_retrieve (&parent_access, adm_access, parent_path,
                                    merge_b->pool));
      {
        /* Passing NULL for the notify_func and notify_baton because
         * repos_diff.c:delete_item will do it for us. */
        err = svn_client__wc_delete (mine, parent_access, merge_b->force,
                                     merge_b->dry_run, 
                                     NULL, NULL,
                                     merge_b->ctx, subpool);
      }
      if (err && state)
        {
          *state = svn_wc_notify_state_obstructed;
          svn_error_clear (err);
        }
      else if (state)
        {
          *state = svn_wc_notify_state_changed;
        }
      break;
    case svn_node_dir:
      if (state)
        *state = svn_wc_notify_state_obstructed;
      break;
    case svn_node_none:
      /* file is already non-existent, this is a no-op. */
      if (state)
        *state = svn_wc_notify_state_missing;
      break;
    default:
      if (state)
        *state = svn_wc_notify_state_unknown;
      break;
    }
    
  svn_pool_destroy (subpool);
  return SVN_NO_ERROR;
}

/* A svn_wc_diff_callbacks2_t function. */
static svn_error_t *
merge_dir_added (svn_wc_adm_access_t *adm_access,
                 svn_wc_notify_state_t *state,
                 const char *path,
                 svn_revnum_t rev,
                 void *baton)
{
  struct merge_cmd_baton *merge_b = baton;
  apr_pool_t *subpool = svn_pool_create (merge_b->pool);
  svn_node_kind_t kind;
  const svn_wc_entry_t *entry;
  const char *copyfrom_url, *child;

  /* Easy out:  if we have no adm_access for the parent directory,
     then this portion of the tree-delta "patch" must be inapplicable.
     Send a 'missing' state back;  the repos-diff editor should then
     send a 'skip' notification. */
  if (! adm_access)
    {
      if (state)
        {
          if (merge_b->dry_run && merge_b->added_path
              && svn_path_is_child (merge_b->added_path, path, subpool))
            *state = svn_wc_notify_state_changed;
          else
            *state = svn_wc_notify_state_missing;
        }
      return SVN_NO_ERROR;
    }

  child = svn_path_is_child (merge_b->target, path, subpool);
  assert (child != NULL);
  copyfrom_url = svn_path_url_add_component (merge_b->url, child, subpool);
  SVN_ERR (check_schema_match (adm_access, copyfrom_url));

  SVN_ERR (svn_io_check_path (path, &kind, subpool));
  switch (kind)
    {
    case svn_node_none:
      SVN_ERR (svn_wc_entry (&entry, path, adm_access, FALSE, subpool));
      if (entry && entry->schedule != svn_wc_schedule_delete)
        {
          /* Versioned but missing */
          if (state)
            *state = svn_wc_notify_state_obstructed;
          return SVN_NO_ERROR;
        }
      if (! merge_b->dry_run)
        {
          SVN_ERR (svn_io_make_dir_recursively (path, subpool));
          SVN_ERR (svn_wc_add (path, adm_access,
                               copyfrom_url, rev,
                               merge_b->ctx->cancel_func,
                               merge_b->ctx->cancel_baton,
                               NULL, NULL, /* don't pass notification func! */
                               merge_b->pool));

        }
      if (merge_b->dry_run)
        merge_b->added_path = apr_pstrdup (merge_b->pool, path);
      if (state)
        *state = svn_wc_notify_state_changed;
      break;
    case svn_node_dir:
      /* Adding an unversioned directory doesn't destroy data */
      SVN_ERR (svn_wc_entry (&entry, path, adm_access, TRUE, subpool));
      if (! entry || (entry && entry->schedule == svn_wc_schedule_delete))
        {
          if (!merge_b->dry_run)
            SVN_ERR (svn_wc_add (path, adm_access,
                                 copyfrom_url,
                                 merge_b->revision->value.number,
                                 merge_b->ctx->cancel_func,
                                 merge_b->ctx->cancel_baton,
                                 NULL, NULL, /* no notification func! */
                                 merge_b->pool));
          if (merge_b->dry_run)
            merge_b->added_path = apr_pstrdup (merge_b->pool, path);
          if (state)
            *state = svn_wc_notify_state_changed;
        }
      else
        {
          if (state)
            *state = svn_wc_notify_state_obstructed;
        }
      break;
    case svn_node_file:
      if (merge_b->dry_run)
        merge_b->added_path = NULL;
      if (state)
        *state = svn_wc_notify_state_obstructed;
      break;
    default:
      if (merge_b->dry_run)
        merge_b->added_path = NULL;
      if (state)
        *state = svn_wc_notify_state_unknown;
      break;
    }

  svn_pool_destroy (subpool);
  return SVN_NO_ERROR;
}

/* Struct used for as the baton for calling merge_delete_notify_func(). */
typedef struct merge_delete_notify_baton_t
{
  svn_client_ctx_t *ctx;

  /** path to skip */
  const char *path_skip;
} merge_delete_notify_baton_t;

/* Notify callback function that wraps the normal callback
 * function to remove a notification that will be sent twice
 * and set the proper action. */
static void
merge_delete_notify_func (void *baton,
                          const char *path,
                          svn_wc_notify_action_t action,
                          svn_node_kind_t kind,
                          const char *mime_type,
                          svn_wc_notify_state_t content_state,
                          svn_wc_notify_state_t prop_state,
                          svn_revnum_t revision)
{
  merge_delete_notify_baton_t *mdb = (merge_delete_notify_baton_t *) baton;

  /* Skip the notification for the path we called svn_client__wc_delete() with,
   * because it will be outputed by repos_diff.c:delete_item */  
  if (strcmp (path, mdb->path_skip) == 0)
    return;
{'fraction_non_alphanumeric': 0.09411764705882353, 'fraction_numerical': 0.005289557683538532, 'mean_word_length': 1.894222838100894, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 249, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 180, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7198116', 'n_tokens_mistral': 27756, 'n_tokens_neox': 26996, 'n_words': 7195}