/*
 * notify.c:  feedback handlers for cmdline client.
 *
 * ====================================================================
 *    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.
 * ====================================================================
 */

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



/*** Includes. ***/

#define APR_WANT_STDIO
#define APR_WANT_STRFUNC
#include <apr_want.h>

#include "svn_cmdline.h"
#include "svn_pools.h"
#include "svn_dirent_uri.h"
#include "svn_path.h"
#include "svn_sorts.h"
#include "svn_hash.h"
#include "cl.h"
#include "private/svn_subr_private.h"
#include "private/svn_sorts_private.h"
#include "private/svn_dep_compat.h"

#include "svn_private_config.h"


/* Baton for notify and friends. */
struct notify_baton
{
  svn_boolean_t received_some_change;
  svn_boolean_t is_checkout;
  svn_boolean_t is_export;
  svn_boolean_t is_wc_to_repos_copy;
  svn_boolean_t sent_first_txdelta;
  int in_external;
  svn_revnum_t progress_revision;
  svn_boolean_t had_print_error; /* Used to not keep printing error messages
                                    when we've already had one print error. */

  svn_cl__conflict_stats_t *conflict_stats;

  /* The cwd, for use in decomposing absolute paths. */
  const char *path_prefix;
};

/* Conflict stats for operations such as update and merge. */
struct svn_cl__conflict_stats_t
{
  apr_pool_t *stats_pool;
  apr_hash_t *text_conflicts, *prop_conflicts, *tree_conflicts;
  int text_conflicts_resolved, prop_conflicts_resolved, tree_conflicts_resolved;
  int skipped_paths;
};

svn_cl__conflict_stats_t *
svn_cl__conflict_stats_create(apr_pool_t *pool)
{
  svn_cl__conflict_stats_t *conflict_stats
    = apr_palloc(pool, sizeof(*conflict_stats));

  conflict_stats->stats_pool = pool;
  conflict_stats->text_conflicts = apr_hash_make(pool);
  conflict_stats->prop_conflicts = apr_hash_make(pool);
  conflict_stats->tree_conflicts = apr_hash_make(pool);
  conflict_stats->text_conflicts_resolved = 0;
  conflict_stats->prop_conflicts_resolved = 0;
  conflict_stats->tree_conflicts_resolved = 0;
  conflict_stats->skipped_paths = 0;
  return conflict_stats;
}

/* Add the PATH (as a key, with a meaningless value) into the HASH in NB. */
static void
store_path(struct notify_baton *nb, apr_hash_t *hash, const char *path)
{
  svn_hash_sets(hash, apr_pstrdup(nb->conflict_stats->stats_pool, path), "");
}

void
svn_cl__conflict_stats_resolved(svn_cl__conflict_stats_t *conflict_stats,
                                const char *path_local,
                                svn_wc_conflict_kind_t conflict_kind)
{
  switch (conflict_kind)
    {
      case svn_wc_conflict_kind_text:
        if (svn_hash_gets(conflict_stats->text_conflicts, path_local))
          {
            svn_hash_sets(conflict_stats->text_conflicts, path_local, NULL);
            conflict_stats->text_conflicts_resolved++;
          }
        break;
      case svn_wc_conflict_kind_property:
        if (svn_hash_gets(conflict_stats->prop_conflicts, path_local))
          {
            svn_hash_sets(conflict_stats->prop_conflicts, path_local, NULL);
            conflict_stats->prop_conflicts_resolved++;
          }
        break;
      case svn_wc_conflict_kind_tree:
        if (svn_hash_gets(conflict_stats->tree_conflicts, path_local))
          {
            svn_hash_sets(conflict_stats->tree_conflicts, path_local, NULL);
            conflict_stats->tree_conflicts_resolved++;
          }
        break;
    }
}

static const char *
remaining_str(apr_pool_t *pool, int n_remaining)
{
  return apr_psprintf(pool, Q_("%d remaining",
                               "%d remaining",
                               n_remaining),
                      n_remaining);
}

static const char *
resolved_str(apr_pool_t *pool, int n_resolved)
{
  return apr_psprintf(pool, Q_("and %d already resolved",
                               "and %d already resolved",
                               n_resolved),
                      n_resolved);
}

svn_error_t *
svn_cl__conflict_stats_get_paths(apr_array_header_t **conflicted_paths,
                                 svn_cl__conflict_stats_t *conflict_stats,
                                 apr_pool_t *result_pool,
                                 apr_pool_t *scratch_pool)
{

  int n_text = apr_hash_count(conflict_stats->text_conflicts);
  int n_prop = apr_hash_count(conflict_stats->prop_conflicts);
  int n_tree = apr_hash_count(conflict_stats->tree_conflicts);
  apr_hash_t *all_conflicts;

  *conflicted_paths = NULL;
  if (n_text == 0 && n_prop == 0 && n_tree == 0)
      return SVN_NO_ERROR;

  /* Use a hash table to ensure paths with multiple conflicts are
   * returned just once. */
  all_conflicts = apr_hash_make(result_pool);
  if (n_text > 0)
    {
      apr_array_header_t *k_text;
      int i;

      SVN_ERR(svn_hash_keys(&k_text, conflict_stats->text_conflicts,
                            scratch_pool));
      for (i = 0; i < k_text->nelts; i++)
        {
          const char *path = APR_ARRAY_IDX(k_text, i, const char *);

          svn_hash_sets(all_conflicts, path, "");
        }
    }

  if (n_prop > 0)
    {
      apr_array_header_t *k_prop;
      int i;

      SVN_ERR(svn_hash_keys(&k_prop, conflict_stats->prop_conflicts,
                            scratch_pool));
      for (i = 0; i < k_prop->nelts; i++)
        {
          const char *path = APR_ARRAY_IDX(k_prop, i, const char *);

          svn_hash_sets(all_conflicts, path, "");
        }
    }

  if (n_tree > 0)
    {
      apr_array_header_t *k_tree;
      int i;

      SVN_ERR(svn_hash_keys(&k_tree, conflict_stats->tree_conflicts,
                            scratch_pool));
      for (i = 0; i < k_tree->nelts; i++)
        {
          const char *path = APR_ARRAY_IDX(k_tree, i, const char *);

          svn_hash_sets(all_conflicts, path, "");
        }
    }

  SVN_ERR(svn_hash_keys(conflicted_paths, all_conflicts, result_pool));
  svn_sort__array(*conflicted_paths, svn_sort_compare_paths);

  return SVN_NO_ERROR;
}

svn_error_t *
svn_cl__print_conflict_stats(svn_cl__conflict_stats_t *conflict_stats,
                             apr_pool_t *scratch_pool)
{
  int n_text = apr_hash_count(conflict_stats->text_conflicts);
  int n_prop = apr_hash_count(conflict_stats->prop_conflicts);
  int n_tree = apr_hash_count(conflict_stats->tree_conflicts);
  int n_text_r = conflict_stats->text_conflicts_resolved;
  int n_prop_r = conflict_stats->prop_conflicts_resolved;
  int n_tree_r = conflict_stats->tree_conflicts_resolved;

  if (n_text > 0 || n_text_r > 0
      || n_prop > 0 || n_prop_r > 0
      || n_tree > 0 || n_tree_r > 0
      || conflict_stats->skipped_paths > 0)
    SVN_ERR(svn_cmdline_printf(scratch_pool,
                               _("Summary of conflicts:\n")));

  if (n_text_r == 0 && n_prop_r == 0 && n_tree_r == 0)
    {
      if (n_text > 0)
        SVN_ERR(svn_cmdline_printf(scratch_pool,
          _("  Text conflicts: %d\n"),
          n_text));
      if (n_prop > 0)
        SVN_ERR(svn_cmdline_printf(scratch_pool,
          _("  Property conflicts: %d\n"),
          n_prop));
      if (n_tree > 0)
        SVN_ERR(svn_cmdline_printf(scratch_pool,
          _("  Tree conflicts: %d\n"),
          n_tree));
    }
  else
    {
      if (n_text > 0 || n_text_r > 0)
        SVN_ERR(svn_cmdline_printf(scratch_pool,
                                   _("  Text conflicts: %s (%s)\n"),
                                   remaining_str(scratch_pool, n_text),
                                   resolved_str(scratch_pool, n_text_r)));
      if (n_prop > 0 || n_prop_r > 0)
        SVN_ERR(svn_cmdline_printf(scratch_pool,
                                   _("  Property conflicts: %s (%s)\n"),
                                   remaining_str(scratch_pool, n_prop),
                                   resolved_str(scratch_pool, n_prop_r)));
      if (n_tree > 0 || n_tree_r > 0)
        SVN_ERR(svn_cmdline_printf(scratch_pool,
                                   _("  Tree conflicts: %s (%s)\n"),
                                   remaining_str(scratch_pool, n_tree),
                                   resolved_str(scratch_pool, n_tree_r)));
    }
  if (conflict_stats->skipped_paths > 0)
    SVN_ERR(svn_cmdline_printf(scratch_pool,
                               _("  Skipped paths: %d\n"),
                               conflict_stats->skipped_paths));

  return SVN_NO_ERROR;
}

svn_error_t *
svn_cl__notifier_print_conflict_stats(void *baton, apr_pool_t *scratch_pool)
{
  struct notify_baton *nb = baton;

  SVN_ERR(svn_cl__print_conflict_stats(nb->conflict_stats, scratch_pool));
  return SVN_NO_ERROR;
}

/* The body for notify() function with standard error handling semantic.
 * Handling of errors implemented at caller side. */
static svn_error_t *
notify_body(struct notify_baton *nb,
            const svn_wc_notify_t *n,
            apr_pool_t *pool)
{
  char statchar_buf[5] = "    ";
  const char *path_local;

  if (n->url)
    path_local = n->url;
  else
    {
      /* Skip the path prefix in N, if supplied, or else the path prefix
         in NB (which was set to the current working directory). */
      if (n->path_prefix)
        path_local = svn_cl__local_style_skip_ancestor(n->path_prefix, n->path,
                                                       pool);
      else
        path_local = svn_cl__local_style_skip_ancestor(nb->path_prefix, n->path,
                                                       pool);
    }

  switch (n->action)
    {
    case svn_wc_notify_skip:
      nb->conflict_stats->skipped_paths++;
      if (n->content_state == svn_wc_notify_state_missing)
        {
          SVN_ERR(svn_cmdline_printf(pool,
                                     _("Skipped missing target: '%s'\n"),
                                     path_local));
        }
      else if (n->content_state == svn_wc_notify_state_source_missing)
        {
          SVN_ERR(svn_cmdline_printf(
                    pool,
                    _("Skipped target: '%s' -- copy-source is missing\n"),
                    path_local));
        }
      else if (n->content_state == svn_wc_notify_state_obstructed)
        {
          SVN_ERR(svn_cmdline_printf(
                    pool,
                    _("Skipped '%s' -- obstructed by unversioned node\n"),
                    path_local));
        }
      else
        {
          SVN_ERR(svn_cmdline_printf(pool, _("Skipped '%s'\n"), path_local));
        }
      break;
    case svn_wc_notify_update_skip_obstruction:
      nb->conflict_stats->skipped_paths++;
      SVN_ERR(svn_cmdline_printf(
                pool,
                _("Skipped '%s' -- An obstructing working copy was found\n"),
                path_local));
      break;
    case svn_wc_notify_update_skip_working_only:
      nb->conflict_stats->skipped_paths++;
      SVN_ERR(svn_cmdline_printf(
                pool, _("Skipped '%s' -- Has no versioned parent\n"),
                path_local));
      break;
    case svn_wc_notify_update_skip_access_denied:
      nb->conflict_stats->skipped_paths++;
      SVN_ERR(svn_cmdline_printf(
                pool, _("Skipped '%s' -- Access denied\n"),
                path_local));
      break;
    case svn_wc_notify_skip_conflicted:
      nb->conflict_stats->skipped_paths++;
      SVN_ERR(svn_cmdline_printf(
                pool, _("Skipped '%s' -- Node remains in conflict\n"),
                path_local));
      break;
    case svn_wc_notify_update_delete:
    case svn_wc_notify_exclude:
      nb->received_some_change = TRUE;
      SVN_ERR(svn_cmdline_printf(pool, "D    %s\n", path_local));
      break;
    case svn_wc_notify_update_broken_lock:
      SVN_ERR(svn_cmdline_printf(pool, "B    %s\n", path_local));
      break;

    case svn_wc_notify_update_external_removed:
      nb->received_some_change = TRUE;
      if (n->err && n->err->message)
        {
          SVN_ERR(svn_cmdline_printf(pool, _("Removed external '%s': %s\n"),
                                     path_local, n->err->message));
        }
      else
        {
          SVN_ERR(svn_cmdline_printf(pool, _("Removed external '%s'\n"),
                                     path_local));
        }
      break;

    case svn_wc_notify_left_local_modifications:
      SVN_ERR(svn_cmdline_printf(pool, _("Left local modifications as '%s'\n"),
                                 path_local));
      break;

    case svn_wc_notify_update_replace:
      nb->received_some_change = TRUE;
      SVN_ERR(svn_cmdline_printf(pool, "R    %s\n", path_local));
      break;

    case svn_wc_notify_update_add:
      nb->received_some_change = TRUE;
      if (n->content_state == svn_wc_notify_state_conflicted)
        {
          store_path(nb, nb->conflict_stats->text_conflicts, path_local);
          SVN_ERR(svn_cmdline_printf(pool, "C    %s\n", path_local));
        }
      else
        {
          SVN_ERR(svn_cmdline_printf(pool, "A    %s\n", path_local));
        }
      break;

    case svn_wc_notify_exists:
      nb->received_some_change = TRUE;
      if (n->content_state == svn_wc_notify_state_conflicted)
        {
          store_path(nb, nb->conflict_stats->text_conflicts, path_local);
          statchar_buf[0] = 'C';
        }
      else
        statchar_buf[0] = 'E';

      if (n->prop_state == svn_wc_notify_state_conflicted)
        {
          store_path(nb, nb->conflict_stats->prop_conflicts, path_local);
          statchar_buf[1] = 'C';
        }
      else if (n->prop_state == svn_wc_notify_state_merged)
        statchar_buf[1] = 'G';

      SVN_ERR(svn_cmdline_printf(pool, "%s %s\n", statchar_buf, path_local));
      break;

    case svn_wc_notify_restore:
      SVN_ERR(svn_cmdline_printf(pool, _("Restored '%s'\n"),
                                 path_local));
      break;

    case svn_wc_notify_revert:
      SVN_ERR(svn_cmdline_printf(pool, _("Reverted '%s'\n"),
                                 path_local));
      break;

    case svn_wc_notify_failed_revert:
      SVN_ERR(svn_cmdline_printf(pool, _("Failed to revert '%s' -- "
                                         "try updating instead.\n"),
                                 path_local));
      break;

    case svn_wc_notify_resolved:
      SVN_ERR(svn_cmdline_printf(pool,
                                 _("Resolved conflicted state of '%s'\n"),
                                 path_local));
      break;

    case svn_wc_notify_resolved_text:
      SVN_ERR(svn_cmdline_printf(pool,
                                 _("Merge conflicts in '%s' marked as "
                                   "resolved.\n"),
                                 path_local));
      break;

    case svn_wc_notify_resolved_prop:
      SVN_ERR_ASSERT(n->prop_name && strlen(n->prop_name) > 0);
      SVN_ERR(svn_cmdline_printf(pool,
                                 _("Conflict in property '%s' at '%s' marked "
                                   "as resolved.\n"),
                                 n->prop_name, path_local));
      break;

    case svn_wc_notify_resolved_tree:
      SVN_ERR(svn_cmdline_printf(pool,
                                 _("Tree conflict at '%s' marked as "
                                   "resolved.\n"),
                                 path_local));
      break;

    case svn_wc_notify_begin_search_tree_conflict_details:
      SVN_ERR(svn_cmdline_printf(pool,
                                 _("Searching tree conflict details for '%s' "
                                   "in repository:\n"),
                                 path_local));
      nb->progress_revision = 0;
      break;

    case svn_wc_notify_tree_conflict_details_progress:
      /* First printf is to obliterate any previous progress printf,
         assuming no more than 10 digit revisions.  Avoid i18n so the
         text length is known.  We only need to do this if the new
         revision is 4 digits less than the previous revision but that
         requires counting digits.  Dividing by 1000 works well
         enough: it triggers when needed, it sometimes triggers when
         not needed, but in typical cases it doesn't trigger as the
         revisions don't vary much. */
      if (n->revision < nb->progress_revision / 1000)
        SVN_ERR(svn_cmdline_printf(pool, "\rChecking r             "));
      SVN_ERR(svn_cmdline_printf(pool, "\rChecking r%ld...", n->revision));
      nb->progress_revision = n->revision;
      break;

    case svn_wc_notify_end_search_tree_conflict_details:
      SVN_ERR(svn_cmdline_printf(pool, _(" done\n")));
      nb->progress_revision = 0;
      break;

    case svn_wc_notify_add:
      /* We *should* only get the MIME_TYPE if PATH is a file.  If we
         do get it, and the mime-type is not textual, note that this
         is a binary addition. */
      if (n->mime_type && (svn_mime_type_is_binary(n->mime_type)))
        {
          SVN_ERR(svn_cmdline_printf(pool, "A  (bin)  %s\n",
                                     path_local));
        }
      else
        {
          SVN_ERR(svn_cmdline_printf(pool, "A         %s\n",
                                     path_local));
        }
      break;

    case svn_wc_notify_delete:
      nb->received_some_change = TRUE;
      SVN_ERR(svn_cmdline_printf(pool, "D         %s\n",
                                 path_local));
      break;

    case svn_wc_notify_patch:
      {
        nb->received_some_change = TRUE;
        if (n->content_state == svn_wc_notify_state_conflicted)
          {
            store_path(nb, nb->conflict_stats->text_conflicts, path_local);
            statchar_buf[0] = 'C';
          }
        else if (n->kind == svn_node_file)
          {
            if (n->content_state == svn_wc_notify_state_merged)
              statchar_buf[0] = 'G';
            else if (n->content_state == svn_wc_notify_state_changed)
              statchar_buf[0] = 'U';
          }

        if (n->prop_state == svn_wc_notify_state_conflicted)
          {
            store_path(nb, nb->conflict_stats->prop_conflicts, path_local);
            statchar_buf[1] = 'C';
          }
        else if (n->prop_state == svn_wc_notify_state_merged)
          statchar_buf[1] = 'G';
        else if (n->prop_state == svn_wc_notify_state_changed)
          statchar_buf[1] = 'U';

        if (statchar_buf[0] != ' ' || statchar_buf[1] != ' ')
          {
            SVN_ERR(svn_cmdline_printf(pool, "%s      %s\n",
                                       statchar_buf, path_local));
          }
      }
      break;

    case svn_wc_notify_patch_applied_hunk:
      nb->received_some_change = TRUE;
      if (n->hunk_original_start != n->hunk_matched_line)
        {
          apr_uint64_t off;
          const char *s;
          const char *minus;

          if (n->hunk_matched_line > n->hunk_original_start)
            {
              /* If we are patching from the start of an empty file,
                 it is nicer to show offset 0 */
              if (n->hunk_original_start == 0 && n->hunk_matched_line == 1)
                off = 0; /* No offset, just adding */
              else
                off = n->hunk_matched_line - n->hunk_original_start;

              minus = "";
            }
          else
            {
              off = n->hunk_original_start - n->hunk_matched_line;
              minus = "-";
            }

          /* ### We're creating the localized strings without
           * ### APR_INT64_T_FMT since it isn't translator-friendly */
          if (n->hunk_fuzz)
            {

              if (n->prop_name)
                {
                  s = _(">         applied hunk ## -%lu,%lu +%lu,%lu ## "
                        "with offset %s");

                  SVN_ERR(svn_cmdline_printf(pool,
                                             apr_pstrcat(pool, s,
                                                         "%"APR_UINT64_T_FMT
                                                         " and fuzz %lu (%s)\n",
                                                         SVN_VA_NULL),
                                             n->hunk_original_start,
                                             n->hunk_original_length,
                                             n->hunk_modified_start,
                                             n->hunk_modified_length,
                                             minus, off, n->hunk_fuzz,
                                             n->prop_name));
                }
              else
                {
                  s = _(">         applied hunk @@ -%lu,%lu +%lu,%lu @@ "
                        "with offset %s");

                  SVN_ERR(svn_cmdline_printf(pool,
                                             apr_pstrcat(pool, s,
                                                         "%"APR_UINT64_T_FMT
                                                         " and fuzz %lu\n",
                                                         SVN_VA_NULL),
                                             n->hunk_original_start,
                                             n->hunk_original_length,
                                             n->hunk_modified_start,
                                             n->hunk_modified_length,
                                             minus, off, n->hunk_fuzz));
                }
            }
          else
            {

              if (n->prop_name)
                {
                  s = _(">         applied hunk ## -%lu,%lu +%lu,%lu ## "
                        "with offset %s");
                  SVN_ERR(svn_cmdline_printf(pool,
                                              apr_pstrcat(pool, s,
                                                          "%"APR_UINT64_T_FMT" (%s)\n",
                                                          SVN_VA_NULL),
                                              n->hunk_original_start,
                                              n->hunk_original_length,
                                              n->hunk_modified_start,
                                              n->hunk_modified_length,
                                              minus, off, n->prop_name));
                }
              else
                {
                  s = _(">         applied hunk @@ -%lu,%lu +%lu,%lu @@ "
                        "with offset %s");
                  SVN_ERR(svn_cmdline_printf(pool,
                                             apr_pstrcat(pool, s,
                                                         "%"APR_UINT64_T_FMT"\n",
                                                         SVN_VA_NULL),
                                             n->hunk_original_start,
                                             n->hunk_original_length,
                                             n->hunk_modified_start,
                                             n->hunk_modified_length,
                                             minus, off));
                }
            }
        }
      else if (n->hunk_fuzz)
        {
          if (n->prop_name)
            SVN_ERR(svn_cmdline_printf(pool,
                          _(">         applied hunk ## -%lu,%lu +%lu,%lu ## "
                                        "with fuzz %lu (%s)\n"),
                                        n->hunk_original_start,
                                        n->hunk_original_length,
                                        n->hunk_modified_start,
                                        n->hunk_modified_length,
                                        n->hunk_fuzz,
                                        n->prop_name));
          else
            SVN_ERR(svn_cmdline_printf(pool,
                          _(">         applied hunk @@ -%lu,%lu +%lu,%lu @@ "
                                        "with fuzz %lu\n"),
                                        n->hunk_original_start,
                                        n->hunk_original_length,
                                        n->hunk_modified_start,
                                        n->hunk_modified_length,
                                        n->hunk_fuzz));

        }
      break;

    case svn_wc_notify_patch_rejected_hunk:
      nb->received_some_change = TRUE;

      if (n->prop_name)
        SVN_ERR(svn_cmdline_printf(pool,
                                   _(">         rejected hunk "
                                     "## -%lu,%lu +%lu,%lu ## (%s)\n"),
                                   n->hunk_original_start,
                                   n->hunk_original_length,
                                   n->hunk_modified_start,
                                   n->hunk_modified_length,
                                   n->prop_name));
      else
        SVN_ERR(svn_cmdline_printf(pool,
                                   _(">         rejected hunk "
                                     "@@ -%lu,%lu +%lu,%lu @@\n"),
                                   n->hunk_original_start,
                                   n->hunk_original_length,
                                   n->hunk_modified_start,
                                   n->hunk_modified_length));
      break;

    case svn_wc_notify_patch_hunk_already_applied:
      nb->received_some_change = TRUE;
      if (n->prop_name)
        SVN_ERR(svn_cmdline_printf(pool,
                                   _(">         hunk "
                                     "## -%lu,%lu +%lu,%lu ## "
                                     "already applied (%s)\n"),
                                   n->hunk_original_start,
                                   n->hunk_original_length,
                                   n->hunk_modified_start,
                                   n->hunk_modified_length,
                                   n->prop_name));
      else
        SVN_ERR(svn_cmdline_printf(pool,
                                   _(">         hunk "
                                     "@@ -%lu,%lu +%lu,%lu @@ "
                                     "already applied\n"),
                                   n->hunk_original_start,
                                   n->hunk_original_length,
                                   n->hunk_modified_start,
                                   n->hunk_modified_length));
      break;

    case svn_wc_notify_update_update:
    case svn_wc_notify_merge_record_info:
      {
        if (n->content_state == svn_wc_notify_state_conflicted)
          {
            store_path(nb, nb->conflict_stats->text_conflicts, path_local);
            statchar_buf[0] = 'C';
          }
        else if (n->kind == svn_node_file)
          {
            if (n->content_state == svn_wc_notify_state_merged)
              statchar_buf[0] = 'G';
            else if (n->content_state == svn_wc_notify_state_changed)
              statchar_buf[0] = 'U';
          }

        if (n->prop_state == svn_wc_notify_state_conflicted)
          {
            store_path(nb, nb->conflict_stats->prop_conflicts, path_local);
            statchar_buf[1] = 'C';
          }
        else if (n->prop_state == svn_wc_notify_state_merged)
          statchar_buf[1] = 'G';
        else if (n->prop_state == svn_wc_notify_state_changed)
          statchar_buf[1] = 'U';

        if (n->lock_state == svn_wc_notify_lock_state_unlocked)
          statchar_buf[2] = 'B';

        if (statchar_buf[0] != ' ' || statchar_buf[1] != ' ')
          nb->received_some_change = TRUE;

        if (statchar_buf[0] != ' ' || statchar_buf[1] != ' '
            || statchar_buf[2] != ' ')
          {
            SVN_ERR(svn_cmdline_printf(pool, "%s %s\n",
                                       statchar_buf, path_local));
          }
      }
      break;

    case svn_wc_notify_update_external:
      /* Remember that we're now "inside" an externals definition. */
      ++nb->in_external;

      /* Currently this is used for checkouts and switches too.  If we
         want different output, we'll have to add new actions. */
      SVN_ERR(svn_cmdline_printf(pool,
                                 _("\nFetching external item into '%s':\n"),
                                 path_local));
      break;

    case svn_wc_notify_failed_external:
      /* If we are currently inside the handling of an externals
         definition, then we can simply present n->err as a warning
         and feel confident that after this, we aren't handling that
         externals definition any longer. */
      if (nb->in_external)
        {
          svn_handle_warning2(stderr, n->err, "svn: ");
          --nb->in_external;
          SVN_ERR(svn_cmdline_printf(pool, "\n"));
        }
      /* Otherwise, we'll just print two warnings.  Why?  Because
         svn_handle_warning2() only shows the single "best message",
         but we have two pretty important ones: that the external at
         '/some/path' didn't pan out, and then the more specific
         reason why (from n->err). */
      else
        {
          svn_error_t *warn_err =
            svn_error_createf(SVN_ERR_CL_ERROR_PROCESSING_EXTERNALS, NULL,
                              _("Error handling externals definition for '%s':"),
                              path_local);
          svn_handle_warning2(stderr, warn_err, "svn: ");
          svn_error_clear(warn_err);
          svn_handle_warning2(stderr, n->err, "svn: ");
        }
      break;

    case svn_wc_notify_update_started:
      if (! (nb->in_external ||
             nb->is_checkout ||
             nb->is_export))
        {
          SVN_ERR(svn_cmdline_printf(pool, _("Updating '%s':\n"),
                                     path_local));
        }
      break;

    case svn_wc_notify_update_completed:
      {
        if (SVN_IS_VALID_REVNUM(n->revision))
          {
            if (nb->is_export)
              {
                SVN_ERR(svn_cmdline_printf(
                          pool, nb->in_external
                            ? _("Exported external at revision %ld.\n")
                            : _("Exported revision %ld.\n"),
                          n->revision));
              }
            else if (nb->is_checkout)
              {
                SVN_ERR(svn_cmdline_printf(
                          pool, nb->in_external
                            ? _("Checked out external at revision %ld.\n")
                            : _("Checked out revision %ld.\n"),
                          n->revision));
              }
            else
              {
                if (nb->received_some_change)
                  {
                    nb->received_some_change = FALSE;
                    SVN_ERR(svn_cmdline_printf(
                              pool, nb->in_external
                                ? _("Updated external to revision %ld.\n")
                                : _("Updated to revision %ld.\n"),
                              n->revision));
                  }
                else
                  {
                    SVN_ERR(svn_cmdline_printf(
                              pool, nb->in_external
                                ? _("External at revision %ld.\n")
                                : _("At revision %ld.\n"),
                               n->revision));
                  }
              }
          }
        else  /* no revision */
          {
            if (nb->is_export)
              {
                SVN_ERR(svn_cmdline_printf(
                          pool, nb->in_external
                            ? _("External export complete.\n")
                            : _("Export complete.\n")));
              }
            else if (nb->is_checkout)
              {
                SVN_ERR(svn_cmdline_printf(
                          pool, nb->in_external
                            ? _("External checkout complete.\n")
                            : _("Checkout complete.\n")));
              }
            else
              {
                SVN_ERR(svn_cmdline_printf(
                          pool, nb->in_external
                            ? _("External update complete.\n")
                            : _("Update complete.\n")));
              }
          }
      }

      if (nb->in_external)
        {
          --nb->in_external;
          SVN_ERR(svn_cmdline_printf(pool, "\n"));
        }
      break;

    case svn_wc_notify_status_external:
      SVN_ERR(svn_cmdline_printf(
        pool, _("\nPerforming status on external item at '%s':\n"),
        path_local));
      break;

    case svn_wc_notify_info_external:
      SVN_ERR(svn_cmdline_printf(
         pool, _("\nPerforming info on external item at '%s':\n"),
         path_local));
      break;

    case svn_wc_notify_status_completed:
      if (SVN_IS_VALID_REVNUM(n->revision))
        SVN_ERR(svn_cmdline_printf(pool,
                                   _("Status against revision: %6ld\n"),
                                   n->revision));
      break;

    case svn_wc_notify_commit_modified:
      /* xgettext: Align the %s's on this and the following 4 messages */
      SVN_ERR(svn_cmdline_printf(pool,
                                 nb->is_wc_to_repos_copy
                                   ? _("Sending copy of       %s\n")
                                   : _("Sending        %s\n"),
                                 path_local));
      break;

    case svn_wc_notify_commit_added:
    case svn_wc_notify_commit_copied:
      if (n->mime_type && svn_mime_type_is_binary(n->mime_type))
        {
          SVN_ERR(svn_cmdline_printf(pool,
                                     nb->is_wc_to_repos_copy
                                       ? _("Adding copy of (bin)  %s\n")
                                       : _("Adding  (bin)  %s\n"),
                                     path_local));
        }
      else
        {
          SVN_ERR(svn_cmdline_printf(pool,
                                     nb->is_wc_to_repos_copy
                                       ? _("Adding copy of        %s\n")
                                       : _("Adding         %s\n"),
                                     path_local));
        }
      break;

    case svn_wc_notify_commit_deleted:
      SVN_ERR(svn_cmdline_printf(pool,
                                 nb->is_wc_to_repos_copy
                                   ? _("Deleting copy of      %s\n")
                                   : _("Deleting       %s\n"),
                                 path_local));
      break;

    case svn_wc_notify_commit_replaced:
    case svn_wc_notify_commit_copied_replaced:
      SVN_ERR(svn_cmdline_printf(pool,
                                 nb->is_wc_to_repos_copy
                                   ? _("Replacing copy of     %s\n")
                                   : _("Replacing      %s\n"),
                                 path_local));
      break;

    case svn_wc_notify_commit_postfix_txdelta:
      if (! nb->sent_first_txdelta)
        {
          nb->sent_first_txdelta = TRUE;
          SVN_ERR(svn_cmdline_printf(pool,
                                     _("Transmitting file data ")));
        }

      SVN_ERR(svn_cmdline_printf(pool, "."));
      break;

    case svn_wc_notify_locked:
      SVN_ERR(svn_cmdline_printf(pool, _("'%s' locked by user '%s'.\n"),
                                 path_local, n->lock->owner));
      break;

    case svn_wc_notify_unlocked:
      SVN_ERR(svn_cmdline_printf(pool, _("'%s' unlocked.\n"),
                                 path_local));
      break;

    case svn_wc_notify_failed_lock:
    case svn_wc_notify_failed_unlock:
      svn_handle_warning2(stderr, n->err, "svn: ");
      break;

    case svn_wc_notify_changelist_set:
      SVN_ERR(svn_cmdline_printf(pool, "A [%s] %s\n",
                                 n->changelist_name, path_local));
      break;

    case svn_wc_notify_changelist_clear:
    case svn_wc_notify_changelist_moved:
      SVN_ERR(svn_cmdline_printf(pool,
                                 "D [%s] %s\n",
                                 n->changelist_name, path_local));
      break;

    case svn_wc_notify_merge_begin:
      if (n->merge_range == NULL)
        SVN_ERR(svn_cmdline_printf(pool,
                                   _("--- Merging differences between "
                                     "repository URLs into '%s':\n"),
                                   path_local));
      else if (n->merge_range->start == n->merge_range->end - 1
          || n->merge_range->start == n->merge_range->end)
        SVN_ERR(svn_cmdline_printf(pool, _("--- Merging r%ld into '%s':\n"),
                                   n->merge_range->end, path_local));
      else if (n->merge_range->start - 1 == n->merge_range->end)
        SVN_ERR(svn_cmdline_printf(pool,
                                   _("--- Reverse-merging r%ld into '%s':\n"),
                                   n->merge_range->start, path_local));
      else if (n->merge_range->start < n->merge_range->end)
        SVN_ERR(svn_cmdline_printf(pool,
                                   _("--- Merging r%ld through r%ld into "
                                     "'%s':\n"),
                                   n->merge_range->start + 1,
                                   n->merge_range->end, path_local));
      else /* n->merge_range->start > n->merge_range->end - 1 */
        SVN_ERR(svn_cmdline_printf(pool,
                                   _("--- Reverse-merging r%ld through r%ld "
                                     "into '%s':\n"),
                                   n->merge_range->start,
                                   n->merge_range->end + 1, path_local));
      break;

    case svn_wc_notify_merge_record_info_begin:
      if (!n->merge_range)
        {
          SVN_ERR(svn_cmdline_printf(pool,
                                     _("--- Recording mergeinfo for merge "
                                       "between repository URLs into '%s':\n"),
                                     path_local));
        }
      else
        {
          if (n->merge_range->start == n->merge_range->end - 1
              || n->merge_range->start == n->merge_range->end)
            SVN_ERR(svn_cmdline_printf(
              pool,
              _("--- Recording mergeinfo for merge of r%ld into '%s':\n"),
              n->merge_range->end, path_local));
          else if (n->merge_range->start - 1 == n->merge_range->end)
            SVN_ERR(svn_cmdline_printf(
              pool,
              _("--- Recording mergeinfo for reverse merge of r%ld into '%s':\n"),
              n->merge_range->start, path_local));
           else if (n->merge_range->start < n->merge_range->end)
             SVN_ERR(svn_cmdline_printf(
               pool,
               _("--- Recording mergeinfo for merge of r%ld through r%ld into '%s':\n"),
               n->merge_range->start + 1, n->merge_range->end, path_local));
           else /* n->merge_range->start > n->merge_range->end - 1 */
             SVN_ERR(svn_cmdline_printf(
               pool,
               _("--- Recording mergeinfo for reverse merge of r%ld through r%ld into '%s':\n"),
               n->merge_range->start, n->merge_range->end + 1, path_local));
        }
      break;

    case svn_wc_notify_merge_elide_info:
      SVN_ERR(svn_cmdline_printf(pool,
                                 _("--- Eliding mergeinfo from '%s':\n"),
                                 path_local));
      break;

    case svn_wc_notify_foreign_merge_begin:
      if (n->merge_range == NULL)
        SVN_ERR(svn_cmdline_printf(pool,
                                   _("--- Merging differences between "
                                     "foreign repository URLs into '%s':\n"),
                                   path_local));
      else if (n->merge_range->start == n->merge_range->end - 1
          || n->merge_range->start == n->merge_range->end)
        SVN_ERR(svn_cmdline_printf(pool,
                                   _("--- Merging (from foreign repository) "
                                     "r%ld into '%s':\n"),
                                   n->merge_range->end, path_local));
      else if (n->merge_range->start - 1 == n->merge_range->end)
        SVN_ERR(svn_cmdline_printf(pool,
                                   _("--- Reverse-merging (from foreign "
                                     "repository) r%ld into '%s':\n"),
                                   n->merge_range->start, path_local));
      else if (n->merge_range->start < n->merge_range->end)
        SVN_ERR(svn_cmdline_printf(pool,
                                   _("--- Merging (from foreign repository) "
                                     "r%ld through r%ld into '%s':\n"),
                                   n->merge_range->start + 1,
                                   n->merge_range->end, path_local));
      else /* n->merge_range->start > n->merge_range->end - 1 */
        SVN_ERR(svn_cmdline_printf(pool,
                                   _("--- Reverse-merging (from foreign "
                                     "repository) r%ld through r%ld into "
                                     "'%s':\n"),
                                   n->merge_range->start,
                                   n->merge_range->end + 1, path_local));
      break;

    case svn_wc_notify_tree_conflict:
      store_path(nb, nb->conflict_stats->tree_conflicts, path_local);
      SVN_ERR(svn_cmdline_printf(pool, "   C %s\n", path_local));
      break;

    case svn_wc_notify_update_shadowed_add:
      nb->received_some_change = TRUE;
      SVN_ERR(svn_cmdline_printf(pool, "   A %s\n", path_local));
      break;

    case svn_wc_notify_update_shadowed_update:
      nb->received_some_change = TRUE;
      SVN_ERR(svn_cmdline_printf(pool, "   U %s\n", path_local));
      break;

    case svn_wc_notify_update_shadowed_delete:
      nb->received_some_change = TRUE;
      SVN_ERR(svn_cmdline_printf(pool, "   D %s\n", path_local));
      break;

    case svn_wc_notify_property_modified:
    case svn_wc_notify_property_added:
      SVN_ERR(svn_cmdline_printf(pool,
                                 _("property '%s' set on '%s'\n"),
                                 n->prop_name, path_local));
      break;

    case svn_wc_notify_property_deleted:
      SVN_ERR(svn_cmdline_printf(pool,
                                 _("property '%s' deleted from '%s'.\n"),
                                 n->prop_name, path_local));
      break;

    case svn_wc_notify_property_deleted_nonexistent:
      SVN_ERR(svn_cmdline_printf(pool,
                                 _("Attempting to delete nonexistent "
                                   "property '%s' on '%s'\n"), n->prop_name,
                                 path_local));
      break;

    case svn_wc_notify_revprop_set:
      SVN_ERR(svn_cmdline_printf(pool,
                           _("property '%s' set on repository revision %ld\n"),
                           n->prop_name, n->revision));
      break;

    case svn_wc_notify_revprop_deleted:
      SVN_ERR(svn_cmdline_printf(pool,
                     _("property '%s' deleted from repository revision %ld\n"),
                     n->prop_name, n->revision));
      break;

    case svn_wc_notify_upgraded_path:
      SVN_ERR(svn_cmdline_printf(pool, _("Upgraded '%s'\n"), path_local));
      break;

    case svn_wc_notify_url_redirect:
      SVN_ERR(svn_cmdline_printf(pool, _("Redirecting to URL '%s':\n"),
                                 n->url));
      break;

    case svn_wc_notify_path_nonexistent:
      SVN_ERR(svn_cmdline_printf(pool, "%s\n",
                apr_psprintf(pool, _("'%s' is not under version control"),
                             path_local)));
      break;

    case svn_wc_notify_conflict_resolver_starting:
      /* Once all operations invoke the interactive conflict resolution after
       * they've completed, we can run svn_cl__notifier_print_conflict_stats()
       * here. */
      break;

    case svn_wc_notify_conflict_resolver_done:
      break;

    case svn_wc_notify_foreign_copy_begin:
      if (n->merge_range == NULL)
        {
          SVN_ERR(svn_cmdline_printf(
                           pool,
                           _("--- Copying from foreign repository URL '%s':\n"),
                           n->url));
        }
      break;

    case svn_wc_notify_move_broken:
      SVN_ERR(svn_cmdline_printf(pool,
                                 _("Breaking move with source path '%s'\n"),
                                 path_local));
      break;

    case svn_wc_notify_cleanup_external:
      SVN_ERR(svn_cmdline_printf
                (pool, _("Performing cleanup on external item at '%s'.\n"),
                 path_local));
      break;

    case svn_wc_notify_commit_finalizing:
      if (nb->sent_first_txdelta)
        {
          SVN_ERR(svn_cmdline_printf(pool, _("done\n")));
        }
      SVN_ERR(svn_cmdline_printf(pool, _("Committing transaction...\n")));
      break;

    default:
      break;
    }

  SVN_ERR(svn_cmdline_fflush(stdout));

  return SVN_NO_ERROR;
}

/* This implements `svn_wc_notify_func2_t'.
 * NOTE: This function can't fail, so we just ignore any print errors. */
static void
notify(void *baton, const svn_wc_notify_t *n, apr_pool_t *pool)
{
  struct notify_baton *nb = baton;
  svn_error_t *err;

  err = notify_body(nb, n, pool);

  /* If we had no errors before, print this error to stderr. Else, don't print
     anything.  The user already knows there were some output errors,
     so there is no point in flooding her with an error per notification. */
  if (err && !nb->had_print_error)
    {
      nb->had_print_error = TRUE;
      /* Issue #3014:
       * Don't print anything on broken pipes. The pipe was likely
       * closed by the process at the other end. We expect that
       * process to perform error reporting as necessary.
       *
       * ### This assumes that there is only one error in a chain for
       * ### SVN_ERR_IO_PIPE_WRITE_ERROR. See svn_cmdline_fputs(). */
      if (err->apr_err != SVN_ERR_IO_PIPE_WRITE_ERROR)
        svn_handle_error2(err, stderr, FALSE, "svn: ");
    }
  svn_error_clear(err);
}

svn_error_t *
svn_cl__get_notifier(svn_wc_notify_func2_t *notify_func_p,
                     void **notify_baton_p,
                     svn_cl__conflict_stats_t *conflict_stats,
                     apr_pool_t *pool)
{
  struct notify_baton *nb = apr_pcalloc(pool, sizeof(*nb));

  nb->received_some_change = FALSE;
  nb->sent_first_txdelta = FALSE;
  nb->is_checkout = FALSE;
  nb->is_export = FALSE;
  nb->is_wc_to_repos_copy = FALSE;
  nb->in_external = 0;
  nb->progress_revision = 0;
  nb->had_print_error = FALSE;
  nb->conflict_stats = conflict_stats;
  SVN_ERR(svn_dirent_get_absolute(&nb->path_prefix, "", pool));

  *notify_func_p = notify;
  *notify_baton_p = nb;
  return SVN_NO_ERROR;
}

svn_error_t *
svn_cl__notifier_mark_checkout(void *baton)
{
  struct notify_baton *nb = baton;

  nb->is_checkout = TRUE;
  return SVN_NO_ERROR;
}

svn_error_t *
svn_cl__notifier_mark_export(void *baton)
{
  struct notify_baton *nb = baton;

  nb->is_export = TRUE;
  return SVN_NO_ERROR;
}

svn_error_t *
svn_cl__notifier_mark_wc_to_repos_copy(void *baton)
{
  struct notify_baton *nb = baton;

  nb->is_wc_to_repos_copy = TRUE;
  return SVN_NO_ERROR;
}

void
svn_cl__check_externals_failed_notify_wrapper(void *baton,
                                              const svn_wc_notify_t *n,
                                              apr_pool_t *pool)
{
  struct svn_cl__check_externals_failed_notify_baton *nwb = baton;

  if (n->action == svn_wc_notify_failed_external)
    nwb->had_externals_error = TRUE;

  if (nwb->wrapped_func)
    nwb->wrapped_func(nwb->wrapped_baton, n, pool);
}
