<commit_before>/*
 * main.c: Subversion server inspection tool.
 *
 * ====================================================================
 * 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/.
 * ====================================================================
 */

#include <assert.h>

#include <apr_general.h>
#include <apr_pools.h>
#include <apr_time.h>
#include <apr_file_io.h>
#include <apr_signal.h>

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

#include "svn_cmdline.h"
#include "svn_types.h"
#include "svn_pools.h"
#include "svn_error.h"
#include "svn_path.h"
#include "svn_repos.h"
#include "svn_fs.h"
#include "svn_time.h"
#include "svn_utf.h"
#include "svn_subst.h"
#include "svn_opt.h"
#include "svn_props.h"
#include "svn_diff.h"

#include "svn_private_config.h"



/*** Some convenience macros and types. ***/



/* Option handling. */

static svn_opt_subcommand_t
  subcommand_author,
  subcommand_cat,
  subcommand_changed,
  subcommand_date,
  subcommand_diff,
  subcommand_dirschanged,
  subcommand_help,
  subcommand_history,
  subcommand_info,
  subcommand_lock,
  subcommand_log,
  subcommand_pget,
  subcommand_plist,
  subcommand_tree,
  subcommand_uuid,
  subcommand_youngest;

/* Option codes and descriptions. */
enum 
  { 
    svnlook__version = SVN_OPT_FIRST_LONGOPT_ID,
    svnlook__show_ids,
    svnlook__no_diff_deleted,
    svnlook__no_diff_added,
    svnlook__revprop_opt
  };

/*
 * This must not have more than SVN_OPT_MAX_OPTIONS entries; if you
 * need more, increase that limit first. 
 *
 * The entire list must be terminated with an entry of nulls.
 */
static const apr_getopt_option_t options_table[] =
  {
    {"help",          'h', 0,
     N_("show help on a subcommand")},

    {NULL,            '?', 0,
     N_("show help on a subcommand")},

    {"version",       svnlook__version, 0,
     N_("show version information")},

    {"revision",      'r', 1,
     N_("specify revision number ARG")},

    {"transaction",  't', 1,
     N_("specify transaction name ARG")},

    {"verbose",  'v', 0,
     N_("be verbose")},

    {"show-ids",      svnlook__show_ids, 0,
     N_("show node revision ids for each path")},

    {"no-diff-deleted", svnlook__no_diff_deleted, 0,
     N_("do not print differences for deleted files")},

    {"no-diff-added", svnlook__no_diff_added, 0,
     N_("do not print differences for added files")},

    {"revprop",       svnlook__revprop_opt, 0,
                      N_("operate on a revision property (use with -r)")},

    {0,               0, 0, 0}
  };


/* Array of available subcommands.
 * The entire list must be terminated with an entry of nulls.
 */
static const svn_opt_subcommand_desc_t cmd_table[] =
  {
    {"author", subcommand_author, {0},
     N_("usage: svnlook author REPOS_PATH\n\n"
        "Print the author.\n"),
     {'r', 't'} },
    
    {"cat", subcommand_cat, {0},
     N_("usage: svnlook cat REPOS_PATH FILE_PATH\n\n"
        "Print the contents of a file.  Leading '/' on FILE_PATH is "
        "optional.\n"),
     {'r', 't'} },
    
    {"changed", subcommand_changed, {0},
     N_("usage: svnlook changed REPOS_PATH\n\n"
        "Print the paths that were changed.\n"),
     {'r', 't'} },
    
    {"date", subcommand_date, {0},
     N_("usage: svnlook date REPOS_PATH\n\n"
        "Print the datestamp.\n"),
     {'r', 't'} },

    {"diff", subcommand_diff, {0},
     N_("usage: svnlook diff REPOS_PATH\n\n"
        "Print GNU-style diffs of changed files and properties.\n"),
     {'r', 't', svnlook__no_diff_deleted, svnlook__no_diff_added} },

    {"dirs-changed", subcommand_dirschanged, {0},
     N_("usage: svnlook dirs-changed REPOS_PATH\n\n"
        "Print the directories that were themselves changed (property edits)\n"
        "or whose file children were changed.\n"),
     {'r', 't'} },
    
    {"help", subcommand_help, {"?", "h"},
     N_("usage: svnlook help [SUBCOMMAND...]\n\n"
        "Describe the usage of this program or its subcommands.\n"),
     {svnlook__version} },

    {"history", subcommand_history, {0},
     N_("usage: svnlook history REPOS_PATH [PATH_IN_REPOS]\n\n"
        "Print information about the history of a path in the repository (or\n"
        "the root directory if no path is supplied).\n"),
     {'r', svnlook__show_ids} },

    {"info", subcommand_info, {0},
     N_("usage: svnlook info REPOS_PATH\n\n"
        "Print the author, datestamp, log message size, and log message.\n"),
     {'r', 't'} },

    {"lock", subcommand_lock, {0},
     N_("usage: svnlook lock REPOS_PATH PATH_IN_REPOS\n\n"
        "If a lock exists on a path in the repository, describe it.\n"),
     {0} },

    {"log", subcommand_log, {0},
     N_("usage: svnlook log REPOS_PATH\n\n"
        "Print the log message.\n"),
     {'r', 't'} },

    {"propget", subcommand_pget, {"pget", "pg"},
     N_("usage: svnlook propget REPOS_PATH PROPNAME [PATH_IN_REPOS]\n\n"
        "Print the raw value of a property on a path in the repository.\n"
        "With --revprop, prints the raw value of a revision property.\n"),
     {'r', 't', svnlook__revprop_opt} },

    {"proplist", subcommand_plist, {"plist", "pl"},
     N_("usage: svnlook proplist REPOS_PATH [PATH_IN_REPOS]\n\n"
        "List the properties of a path in the repository, or\n"
        "with the --revprop option, revision properties.\n"
        "With -v, show the property values too.\n"),
     {'r', 't', 'v', svnlook__revprop_opt} },

    {"tree", subcommand_tree, {0},
     N_("usage: svnlook tree REPOS_PATH [PATH_IN_REPOS]\n\n"
        "Print the tree, starting at PATH_IN_REPOS (if supplied, at the root\n"
        "of the tree otherwise), optionally showing node revision ids.\n"),
     {'r', 't', svnlook__show_ids} },

    {"uuid", subcommand_uuid, {0},
     N_("usage: svnlook uuid REPOS_PATH\n\n"
        "Print the repository's UUID.\n"),
     {0} },

    {"youngest", subcommand_youngest, {0},
     N_("usage: svnlook youngest REPOS_PATH\n\n"
        "Print the youngest revision number.\n"),
     {0} },

    { NULL, NULL, {0}, NULL, {0} }
  };


/* Baton for passing option/argument state to a subcommand function. */
struct svnlook_opt_state
{
  const char *repos_path;  /* 'arg0' is always the path to the repository. */
  const char *arg1;        /* Usually an fs path, a propname, or NULL. */
  const char *arg2;        /* Usually an fs path or NULL. */
  svn_revnum_t rev;
  const char *txn;
  svn_boolean_t version;          /* --version */
  svn_boolean_t show_ids;         /* --show-ids */
  svn_boolean_t help;             /* --help */
  svn_boolean_t no_diff_deleted;  /* --no-diff-deleted */
  svn_boolean_t no_diff_added;    /* --no-diff-added */
  svn_boolean_t verbose;          /* --verbose */
  svn_boolean_t revprop;          /* --revprop */
};


typedef struct svnlook_ctxt_t
{
  svn_repos_t *repos;
  svn_fs_t *fs;
  svn_boolean_t is_revision;
  svn_boolean_t show_ids;
  svn_boolean_t no_diff_deleted;
  svn_boolean_t no_diff_added;
  svn_revnum_t rev_id;
  svn_fs_txn_t *txn;
  const char *txn_name /* UTF-8! */;

} svnlook_ctxt_t;

/* A flag to see if we've been cancelled by the client or not. */
static volatile sig_atomic_t cancelled = FALSE;



/*** Helper functions. ***/

/* A signal handler to support cancellation. */
static void
signal_handler (int signum)
{
  apr_signal (signum, SIG_IGN);
  cancelled = TRUE;
}

/* Our cancellation callback. */
static svn_error_t *
check_cancel (void *baton)
{
  if (cancelled)
    return svn_error_create (SVN_ERR_CANCELLED, NULL, _("Caught signal"));
  else
    return SVN_NO_ERROR;
}


/* Version compatibility check */
static svn_error_t *
check_lib_versions (void)
{
  static const svn_version_checklist_t checklist[] =
    {
      { "svn_subr",  svn_subr_version },
      { "svn_repos", svn_repos_version },
      { "svn_fs",    svn_fs_version },
      { "svn_delta", svn_delta_version },
      { "svn_diff",  svn_diff_version },
      { NULL, NULL }
    };

  SVN_VERSION_DEFINE (my_version);
  return svn_ver_check_list (&my_version, checklist);
}


/* Get revision or transaction property PROP_NAME for the revision or
   transaction specified in C, allocating in in POOL and placing it in
   *PROP_VALUE. */
static svn_error_t *
get_property (svn_string_t **prop_value,
              svnlook_ctxt_t *c, 
              const char *prop_name,
              apr_pool_t *pool)
{
  svn_string_t *raw_value;

  /* Fetch transaction property... */
  if (! c->is_revision)
    SVN_ERR (svn_fs_txn_prop (&raw_value, c->txn, prop_name, pool));

  /* ...or revision property -- it's your call. */
  else
    SVN_ERR (svn_fs_revision_prop (&raw_value, c->fs, c->rev_id,
                                   prop_name, pool));

  *prop_value = raw_value;

  return SVN_NO_ERROR;
}


static svn_error_t *
get_root (svn_fs_root_t **root,
          svnlook_ctxt_t *c,
          apr_pool_t *pool)
{
  /* Open up the appropriate root (revision or transaction). */
  if (c->is_revision)
    {
      /* If we didn't get a valid revision number, we'll look at the
         youngest revision. */
      if (! SVN_IS_VALID_REVNUM (c->rev_id))
        SVN_ERR (svn_fs_youngest_rev (&(c->rev_id), c->fs, pool));

      SVN_ERR (svn_fs_revision_root (root, c->fs, c->rev_id, pool));
    }
  else
    {
      SVN_ERR (svn_fs_txn_root (root, c->txn, pool));
    }

  return SVN_NO_ERROR;
}




/*** Tree Routines ***/

/* Generate a generic delta tree. */
static svn_error_t *
generate_delta_tree (svn_repos_node_t **tree,
                     svn_repos_t *repos,
                     svn_fs_root_t *root, 
                     svn_revnum_t base_rev, 
                     svn_boolean_t use_copy_history,
                     apr_pool_t *pool)
{
  svn_fs_root_t *base_root;
  const svn_delta_editor_t *editor;
  void *edit_baton;
  apr_pool_t *edit_pool = svn_pool_create (pool);
  svn_fs_t *fs = svn_repos_fs (repos);

  /* Get the base root. */
  SVN_ERR (svn_fs_revision_root (&base_root, fs, base_rev, pool));

  /* Request our editor. */
  SVN_ERR (svn_repos_node_editor (&editor, &edit_baton, repos,
                                  base_root, root, pool, edit_pool));

  /* Drive our editor. */
  SVN_ERR (svn_repos_replay (root, editor, edit_baton, edit_pool));

  /* Return the tree we just built. */
  *tree = svn_repos_node_from_baton (edit_baton);
  svn_pool_destroy (edit_pool);
  return SVN_NO_ERROR;
}




/*** Tree Printing Routines ***/

/* Recursively print only directory nodes that either a) have property
   mods, or b) contains files that have changed. */
static svn_error_t *
print_dirs_changed_tree (svn_repos_node_t *node,
                         const char *path /* UTF-8! */,
                         apr_pool_t *pool)
{
  svn_repos_node_t *tmp_node;
  int print_me = 0;
  const char *full_path;
  apr_pool_t *subpool;

  SVN_ERR (check_cancel (NULL));

  if (! node)
    return SVN_NO_ERROR;

  /* Not a directory?  We're not interested. */
  if (node->kind != svn_node_dir)
    return SVN_NO_ERROR;

  /* Got prop mods?  Excellent. */
  if (node->prop_mod)
    print_me = 1;

  if (! print_me)
    {
      /* Fly through the list of children, checking for modified files. */
      tmp_node = node->child;
      if (tmp_node)
        {
          if ((tmp_node->kind == svn_node_file)
              || (tmp_node->text_mod)
              || (tmp_node->action == 'A')
              || (tmp_node->action == 'D'))
            {
              print_me = 1;
            }
          while (tmp_node->sibling && (! print_me ))
            {
              tmp_node = tmp_node->sibling;
              if ((tmp_node->kind == svn_node_file)
                  || (tmp_node->text_mod)
                  || (tmp_node->action == 'A')
                  || (tmp_node->action == 'D'))
                {
                  print_me = 1;
                }
            }
        }
    }
  
  /* Print the node if it qualifies. */
  if (print_me)
    {
      SVN_ERR (svn_cmdline_printf (pool, "%s/\n", path));
    }

  /* Return here if the node has no children. */
  tmp_node = node->child;
  if (! tmp_node)
    return SVN_NO_ERROR;

  /* Recursively handle the node's children. */
  subpool = svn_pool_create (pool);  
  full_path = svn_path_join (path, tmp_node->name, subpool);
  SVN_ERR (print_dirs_changed_tree (tmp_node, full_path, subpool));
  while (tmp_node->sibling)
    {
      svn_pool_clear (subpool);
      tmp_node = tmp_node->sibling;
      full_path = svn_path_join (path, tmp_node->name, subpool);
      SVN_ERR (print_dirs_changed_tree (tmp_node, full_path, subpool));
    }
  svn_pool_destroy (subpool);

  return SVN_NO_ERROR;
}


/* Recursively print all nodes in the tree that have been modified
   (do not include directories affected only by "bubble-up"). */
static svn_error_t *
print_changed_tree (svn_repos_node_t *node,
                    const char *path /* UTF-8! */,
                    apr_pool_t *pool)
{
  const char *full_path;
  char status[3] = "_ ";
  int print_me = 1;
  apr_pool_t *subpool;

  SVN_ERR (check_cancel (NULL));

  if (! node)
    return SVN_NO_ERROR;

  /* Print the node. */
  if (node->action == 'A')
    status[0] = 'A';
  else if (node->action == 'D')
    status[0] = 'D';
  else if (node->action == 'R')
    {
      if ((! node->text_mod) && (! node->prop_mod))
        print_me = 0;
      if (node->text_mod)
        status[0] = 'U';
      if (node->prop_mod)
        status[1] = 'U';
    }
  else
    print_me = 0;

  /* Print this node unless told to skip it. */
  if (print_me)
    {
      SVN_ERR (svn_cmdline_printf (pool, "%s  %s%s\n",
                                   status,
                                   path,
                                   node->kind == svn_node_dir ? "/" : ""));
    }
  
  /* Return here if the node has no children. */
  node = node->child;
  if (! node)
    return SVN_NO_ERROR;

  /* Recursively handle the node's children. */
  subpool = svn_pool_create (pool);  
  full_path = svn_path_join (path, node->name, subpool);
  SVN_ERR (print_changed_tree (node, full_path, subpool));
  while (node->sibling)
    {
      svn_pool_clear (subpool);
      node = node->sibling;
      full_path = svn_path_join (path, node->name, subpool);
      SVN_ERR (print_changed_tree (node, full_path, subpool));
    }
  svn_pool_destroy (subpool);

  return SVN_NO_ERROR;
}


static svn_error_t *
open_writable_binary_file (apr_file_t **fh, 
                           const char *path /* UTF-8! */, 
                           apr_pool_t *pool)
{
  apr_array_header_t *path_pieces;
  svn_error_t *err;
  int i;
  const char *full_path, *dir;
  
  /* Try the easy way to open the file. */
  err = svn_io_file_open (fh, path, 
                          APR_WRITE | APR_CREATE | APR_TRUNCATE | APR_BINARY,
                          APR_OS_DEFAULT, pool);
  if (! err)
    return SVN_NO_ERROR;

  svn_path_split (path, &dir, NULL, pool);

  /* If the file path has no parent, then we've already tried to open
     it as best as we care to try above. */
  if (svn_path_is_empty (dir))
    return err;

  path_pieces = svn_path_decompose (dir, pool);
  if (! path_pieces->nelts)
    return SVN_NO_ERROR;

  full_path = "";
  for (i = 0; i < path_pieces->nelts; i++)
    {
      svn_node_kind_t kind;
      const char *piece = ((const char **) (path_pieces->elts))[i];
      full_path = svn_path_join (full_path, piece, pool);
      SVN_ERR (svn_io_check_resolved_path (full_path, &kind, pool));

      /* Does this path component exist at all? */
      if (kind == svn_node_none)
        {
          SVN_ERR (svn_io_dir_make (full_path, APR_OS_DEFAULT, pool));
        }
      else if (kind != svn_node_dir)
        {
          return svn_error_createf (err->apr_err, err,
                                    _("Error creating dir '%s' (path exists)"),
                                    full_path);
        }
    }

  /* Now that we are ensured that the parent path for this file
     exists, try once more to open it. */
  svn_error_clear (err);
  return svn_io_file_open (fh, path, 
                           APR_WRITE | APR_CREATE | APR_TRUNCATE | APR_BINARY,
                           APR_OS_DEFAULT, pool);
}


static svn_error_t *
dump_contents (apr_file_t *fh,
               svn_fs_root_t *root,
               const char *path /* UTF-8! */,
               apr_pool_t *pool)
{
  svn_stream_t *contents, *file_stream;

  /* Grab the contents and copy them into fh. */
  SVN_ERR (svn_fs_file_contents (&contents, root, path, pool));
  file_stream = svn_stream_from_aprfile (fh, pool);
  SVN_ERR (svn_stream_copy (contents, file_stream, pool));
  return SVN_NO_ERROR;
}


/* Prepare temporary files *TMPFILE1 and *TMPFILE2 for diffing
   PATH1@ROOT1 versus PATH2@ROOT2.  If either ROOT1 or ROOT2 is NULL,
   the temporary file for its path/root will be an empty one.
   Otherwise, its temporary file will contain the contents of that
   path/root in the repository.

   An exception to this is when either path/root has an svn:mime-type
   property set on it which indicates that the file contains
   non-textual data -- in this case, the *IS_BINARY flag is set and no
   temporary files are created.

   Use POOL for all that allocation goodness. */
static svn_error_t *
prepare_tmpfiles (const char **tmpfile1,
                  const char **tmpfile2,
                  svn_boolean_t *is_binary,
                  svn_fs_root_t *root1,
                  const char *path1,
                  svn_fs_root_t *root2,
                  const char *path2,
                  const char *tmpdir,
                  apr_pool_t *pool)
{
  svn_string_t *mimetype;
  apr_file_t *fh;

  /* Init the return values. */
  *tmpfile1 = NULL;
  *tmpfile2 = NULL;
  *is_binary = FALSE;

  assert (path1 && path2);

  /* Check for binary mimetypes.  If either file has a binary
     mimetype, get outta here.  */
  if (root1)
    {
      SVN_ERR (svn_fs_node_prop (&mimetype, root1, path1, 
                                 SVN_PROP_MIME_TYPE, pool));
      if (mimetype && svn_mime_type_is_binary (mimetype->data))
        {
          *is_binary = TRUE;
          return SVN_NO_ERROR;
        }
    }
  if (root2)
    {
      SVN_ERR (svn_fs_node_prop (&mimetype, root2, path2, 
                                 SVN_PROP_MIME_TYPE, pool));
      if (mimetype && svn_mime_type_is_binary (mimetype->data))
        {
          *is_binary = TRUE;
          return SVN_NO_ERROR;
        }
    }

  /* Now, prepare the two temporary files, each of which will either
     be empty, or will have real contents.  The first file we will
     make in our temporary directory. */
  *tmpfile2 = svn_path_join (tmpdir, path2, pool);
  SVN_ERR (open_writable_binary_file (&fh, *tmpfile2, pool));
  if (root2)
    SVN_ERR (dump_contents (fh, root2, path2, pool));
  apr_file_close (fh);

  /* The second file is constructed from the first one's path. */
  SVN_ERR (svn_io_open_unique_file (&fh, tmpfile1, *tmpfile2, 
                                    NULL, FALSE, pool));
  if (root1)
    SVN_ERR (dump_contents (fh, root1, path1, pool));
  apr_file_close (fh);

  return SVN_NO_ERROR;
}


/* Generate a diff label for PATH in ROOT, allocating in POOL. */
static svn_error_t *
generate_label (const char **label,
                svn_fs_root_t *root,
                const char *path,
                apr_pool_t *pool)
{
  svn_fs_t *fs = svn_fs_root_fs (root);
  svn_string_t *date;
  const char *datestr;
  const char *name = NULL;
  svn_revnum_t rev = SVN_INVALID_REVNUM;

  if (svn_fs_is_revision_root (root))
    {
      rev = svn_fs_revision_root_revision (root);
      SVN_ERR (svn_fs_revision_prop (&date, fs, rev, 
                                     SVN_PROP_REVISION_DATE, pool));
    }
  else 
    {
      svn_fs_txn_t *txn;
      name = svn_fs_txn_root_name (root, pool);
      SVN_ERR (svn_fs_open_txn (&txn, fs, name, pool));
      SVN_ERR (svn_fs_txn_prop (&date, txn, SVN_PROP_REVISION_DATE, pool));
    }
  
  if (date)
    {
      datestr = date->data;
      ((char *)datestr)[10] = ' ';
      ((char *)datestr)[19] = '\0';
    }
  else
    {
      datestr = "                      ";
    }

  if (name)
    *label = apr_psprintf (pool, "%s\t%s UTC (txn %s)", 
                           path, datestr, name); 
  else
    *label = apr_psprintf (pool, "%s\t%s UTC (rev %ld)",
                           path, datestr, rev);
  return SVN_NO_ERROR;
}


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


/* Helper function to display differences in properties of a file */
static svn_error_t *
display_prop_diffs (const apr_array_header_t *prop_diffs,
                    apr_hash_t *orig_props,
                    const char *path,
                    apr_pool_t *pool)
{
  int i;

  SVN_ERR (svn_cmdline_printf (pool, "\nProperty changes on: %s\n%s\n",
                               path, under_string));

  for (i = 0; i < prop_diffs->nelts; i++)
    {
      const svn_string_t *orig_value;
      const svn_prop_t *pc = &APR_ARRAY_IDX (prop_diffs, i, svn_prop_t);

      SVN_ERR (check_cancel (NULL));

      if (orig_props)
        orig_value = apr_hash_get (orig_props, pc->name, APR_HASH_KEY_STRING);
      else
        orig_value = NULL;

      SVN_ERR (svn_cmdline_printf (pool, _("Name: %s\n"), pc->name));

      /* 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.
         ### We don't use svn_cmdline_printf here, since we don't know if the
         values are UTF-8. */
      {
        svn_boolean_t val_to_utf8 = svn_prop_is_svn_prop (pc->name);
        const char *printable_val;

        if (orig_value != NULL)
          {
            if (val_to_utf8)
              SVN_ERR (svn_cmdline_cstring_from_utf8 (&printable_val, 
                                                      orig_value->data, pool));
            else
              printable_val = orig_value->data;
            printf ("   - %s\n", printable_val);
          }

        if (pc->value != NULL)
          {
            if (val_to_utf8)
              SVN_ERR (svn_cmdline_cstring_from_utf8
                       (&printable_val, pc->value->data, pool));
            else
              printable_val = pc->value->data;
            printf ("   + %s\n", printable_val);
          }
      }
    }

  SVN_ERR (svn_cmdline_printf (pool, "\n"));
  return svn_cmdline_fflush (stdout);
}



/* Recursively print all nodes in the tree that have been modified
   (do not include directories affected only by "bubble-up"). */
static svn_error_t *
print_diff_tree (svn_fs_root_t *root,
                 svn_fs_root_t *base_root,
                 svn_repos_node_t *node, 
                 const char *path /* UTF-8! */,
                 const char *base_path /* UTF-8! */,
                 svn_boolean_t no_diff_deleted,
                 svn_boolean_t no_diff_added,
                 const char *tmpdir,
                 apr_pool_t *pool)
{
  const char *orig_path = NULL, *new_path = NULL;
  svn_boolean_t do_diff = FALSE;
  svn_boolean_t is_copy = FALSE;
  svn_boolean_t binary = FALSE;
  apr_pool_t *subpool;

  SVN_ERR (check_cancel (NULL));

  if (! node)
    return SVN_NO_ERROR;

  /* Print copyfrom history for the top node of a copied tree. */
  if ((SVN_IS_VALID_REVNUM (node->copyfrom_rev))
      && (node->copyfrom_path != NULL))
    {
      /* This is ... a copy. */
      is_copy = TRUE;

      /* Propagate the new base.  Copyfrom paths usually start with a
         slash; we remove it for consistency with the target path.
         ### Yes, it would be *much* better for something in the path
             library to be taking care of this! */
      if (node->copyfrom_path[0] == '/')
        base_path = apr_pstrdup (pool, node->copyfrom_path + 1);
      else
        base_path = apr_pstrdup (pool, node->copyfrom_path);

      SVN_ERR (svn_cmdline_printf (pool, _("Copied: %s (from rev %ld, %s)\n"),
                                   path, node->copyfrom_rev, base_path));

      SVN_ERR (svn_fs_revision_root (&base_root,
                                     svn_fs_root_fs (base_root),
                                     node->copyfrom_rev, pool));
    }

  /*** First, we'll just print file content diffs. ***/
  if (node->kind == svn_node_file)
    {
      /* Here's the generalized way we do our diffs:

         - First, we'll check for svn:mime-type properties on the old
           and new files.  If either has such a property, and it
           represents a binary type, we won't actually be doing a real
           diff.
           
         - First, dump the contents of the new version of the file
           into the svnlook temporary directory, building out the
           actual directories that need to be created in order to
           fully represent the filesystem path inside the tmp
           directory.

         - Then, dump the contents of the old version of the file into
           the top level of the svnlook temporary directory using a
           unique temporary file name (we do this *after* putting the
           new version of the file there in case something actually
           versioned has a name that looks like one of our unique
           identifiers).

         - Next, we run 'diff', passing the repository path as the
           label.  

         - Finally, we delete the temporary files (but leave the
           built-out directories in place until after all diff
           handling has been finished).  */
      if ((node->action == 'R') && (node->text_mod))
        {
          do_diff = TRUE;
          SVN_ERR (prepare_tmpfiles (&orig_path, &new_path, &binary,
                                     base_root, base_path, root, path,
                                     tmpdir, pool));
        }
      if ((node->action == 'A') && (node->text_mod))
        {
          do_diff = TRUE;
          SVN_ERR (prepare_tmpfiles (&orig_path, &new_path, &binary,
                                     NULL, base_path, root, path,
                                     tmpdir, pool));
        }
      if (node->action == 'D')
        {
          do_diff = TRUE;
          SVN_ERR (prepare_tmpfiles (&orig_path, &new_path, &binary,
                                     base_root, base_path, NULL, path,
                                     tmpdir, pool));
        }
    }

  if (do_diff)
    {
      if (! is_copy)
        SVN_ERR (svn_cmdline_printf (pool, "%s: %s\n", 
                                     ((node->action == 'A') ? _("Added") :
                                      ((node->action == 'D') ? _("Deleted") :
                                       ((node->action == 'R') ? _("Modified")
                                        : _("Index")))),
                                     path));

      if (!(node->action == 'D' && no_diff_deleted)
          && !(node->action == 'A' && no_diff_added))
        {
          svn_diff_t *diff;

          SVN_ERR (svn_cmdline_printf (pool, "%s\n", equal_string));
          SVN_ERR (svn_cmdline_fflush (stdout));

          if (binary)
            {
              SVN_ERR (svn_cmdline_printf (pool,
                                           _("(Binary files differ)\n")));
            }
          else
            {
              SVN_ERR (svn_diff_file_diff (&diff, orig_path, new_path, pool));
              if (svn_diff_contains_diffs (diff))
                {
                  svn_stream_t *ostream;
                  const char *orig_label, *new_label;

                  SVN_ERR (svn_stream_for_stdout (&ostream, pool));
                  
                  SVN_ERR (generate_label (&orig_label, base_root, 
                                           base_path, pool));
                  SVN_ERR (generate_label (&new_label, root, path, pool));
                  SVN_ERR (svn_diff_file_output_unified (ostream, diff, 
                                                         orig_path, new_path,
                                                         orig_label, new_label,
                                                         pool));
                  SVN_ERR (svn_stream_close (ostream));
                }
            }
        }

      SVN_ERR (svn_cmdline_printf (pool, "\n"));
      SVN_ERR (svn_cmdline_fflush (stdout));
    }
  else if (is_copy)
    {
      SVN_ERR (svn_cmdline_printf (pool, "\n"));
    }

  /* Make sure we delete any temporary files. */
  if (orig_path)
    SVN_ERR (svn_io_remove_file (orig_path, pool));
  if (new_path)
    SVN_ERR (svn_io_remove_file (new_path, pool));

  /*** Now handle property diffs ***/
  if ((node->prop_mod) && (node->action != 'D'))
    {
      apr_hash_t *local_proptable;
      apr_hash_t *base_proptable;
      apr_array_header_t *propchanges, *props;

      SVN_ERR (svn_fs_node_proplist (&local_proptable, root, path, pool));
      if (node->action == 'A')
        base_proptable = apr_hash_make (pool);
      else
        SVN_ERR (svn_fs_node_proplist (&base_proptable, base_root, 
                                       base_path, pool));
      SVN_ERR (svn_prop_diffs (&propchanges, local_proptable, 
                               base_proptable, pool));
      SVN_ERR (svn_categorize_props (propchanges, NULL, NULL, &props, pool));
      if (props->nelts > 0)
        SVN_ERR (display_prop_diffs (props, base_proptable, path, pool));
    }

  /* Return here if the node has no children. */
  node = node->child;
  if (! node)
    return SVN_NO_ERROR;

  /* Recursively handle the node's children. */
  subpool = svn_pool_create (pool);
  SVN_ERR (print_diff_tree (root, base_root, node,
                            svn_path_join (path, node->name, subpool),
                            svn_path_join (base_path, node->name, subpool),
                            no_diff_deleted,
                            no_diff_added,
                            tmpdir,
                            subpool));
  while (node->sibling)
    {
      svn_pool_clear (subpool);
      node = node->sibling;
      SVN_ERR (print_diff_tree (root, base_root, node,
                                svn_path_join (path, node->name, subpool),
                                svn_path_join (base_path, node->name, subpool),
                                no_diff_deleted,
                                no_diff_added,
                                tmpdir,
                                subpool));
    }
  apr_pool_destroy (subpool);

  return SVN_NO_ERROR;
}


/* Recursively print all nodes, and (optionally) their node revision ids.

   ROOT is the revision or transaction root used to build that tree.
   PATH and ID are the current path and node revision id being
   printed, and INDENTATION the number of spaces to prepent to that
   path's printed output.  ID may be NULL if SHOW_IDS is FALSE (in
   which case, ids won't be printed at all).  

   Use POOL for all allocations.  */
static svn_error_t *
print_tree (svn_fs_root_t *root,
            const char *path /* UTF-8! */,
            const svn_fs_id_t *id,
            svn_boolean_t is_dir,
            int indentation,
            svn_boolean_t show_ids,
            apr_pool_t *pool)
{
  apr_pool_t *subpool;
  int i;
  const char *name_native;
  apr_hash_t *entries;
  apr_hash_index_t *hi;

  SVN_ERR (check_cancel (NULL));

  /* Print the indentation. */
  for (i = 0; i < indentation; i++)
    {
      SVN_ERR (svn_cmdline_fputs (" ", stdout, pool));
    }

  /* Print the node. */
  SVN_ERR (svn_utf_cstring_from_utf8 (&name_native, 
                                      svn_path_basename (path, pool), 
                                      pool));
  SVN_ERR (svn_cmdline_printf (pool, "%s%s",
                               svn_path_basename (path, pool),
                               is_dir ? "/" : ""));

  if (show_ids)
    {
      svn_string_t *unparsed_id = NULL;
      if (id)
        unparsed_id = svn_fs_unparse_id (id, pool);
      SVN_ERR (svn_cmdline_printf (pool, " <%s>",
                                   unparsed_id<commit_msg>* subversion/svnlook/main.c (print_tree): Remove redundant UTF-8 code.<commit_after>/*
 * main.c: Subversion server inspection tool.
 *
 * ====================================================================
 * 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/.
 * ====================================================================
 */

#include <assert.h>

#include <apr_general.h>
#include <apr_pools.h>
#include <apr_time.h>
#include <apr_file_io.h>
#include <apr_signal.h>

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

#include "svn_cmdline.h"
#include "svn_types.h"
#include "svn_pools.h"
#include "svn_error.h"
#include "svn_path.h"
#include "svn_repos.h"
#include "svn_fs.h"
#include "svn_time.h"
#include "svn_utf.h"
#include "svn_subst.h"
#include "svn_opt.h"
#include "svn_props.h"
#include "svn_diff.h"

#include "svn_private_config.h"



/*** Some convenience macros and types. ***/



/* Option handling. */

static svn_opt_subcommand_t
  subcommand_author,
  subcommand_cat,
  subcommand_changed,
  subcommand_date,
  subcommand_diff,
  subcommand_dirschanged,
  subcommand_help,
  subcommand_history,
  subcommand_info,
  subcommand_lock,
  subcommand_log,
  subcommand_pget,
  subcommand_plist,
  subcommand_tree,
  subcommand_uuid,
  subcommand_youngest;

/* Option codes and descriptions. */
enum 
  { 
    svnlook__version = SVN_OPT_FIRST_LONGOPT_ID,
    svnlook__show_ids,
    svnlook__no_diff_deleted,
    svnlook__no_diff_added,
    svnlook__revprop_opt
  };

/*
 * This must not have more than SVN_OPT_MAX_OPTIONS entries; if you
 * need more, increase that limit first. 
 *
 * The entire list must be terminated with an entry of nulls.
 */
static const apr_getopt_option_t options_table[] =
  {
    {"help",          'h', 0,
     N_("show help on a subcommand")},

    {NULL,            '?', 0,
     N_("show help on a subcommand")},

    {"version",       svnlook__version, 0,
     N_("show version information")},

    {"revision",      'r', 1,
     N_("specify revision number ARG")},

    {"transaction",  't', 1,
     N_("specify transaction name ARG")},

    {"verbose",  'v', 0,
     N_("be verbose")},

    {"show-ids",      svnlook__show_ids, 0,
     N_("show node revision ids for each path")},

    {"no-diff-deleted", svnlook__no_diff_deleted, 0,
     N_("do not print differences for deleted files")},

    {"no-diff-added", svnlook__no_diff_added, 0,
     N_("do not print differences for added files")},

    {"revprop",       svnlook__revprop_opt, 0,
                      N_("operate on a revision property (use with -r)")},

    {0,               0, 0, 0}
  };


/* Array of available subcommands.
 * The entire list must be terminated with an entry of nulls.
 */
static const svn_opt_subcommand_desc_t cmd_table[] =
  {
    {"author", subcommand_author, {0},
     N_("usage: svnlook author REPOS_PATH\n\n"
        "Print the author.\n"),
     {'r', 't'} },
    
    {"cat", subcommand_cat, {0},
     N_("usage: svnlook cat REPOS_PATH FILE_PATH\n\n"
        "Print the contents of a file.  Leading '/' on FILE_PATH is "
        "optional.\n"),
     {'r', 't'} },
    
    {"changed", subcommand_changed, {0},
     N_("usage: svnlook changed REPOS_PATH\n\n"
        "Print the paths that were changed.\n"),
     {'r', 't'} },
    
    {"date", subcommand_date, {0},
     N_("usage: svnlook date REPOS_PATH\n\n"
        "Print the datestamp.\n"),
     {'r', 't'} },

    {"diff", subcommand_diff, {0},
     N_("usage: svnlook diff REPOS_PATH\n\n"
        "Print GNU-style diffs of changed files and properties.\n"),
     {'r', 't', svnlook__no_diff_deleted, svnlook__no_diff_added} },

    {"dirs-changed", subcommand_dirschanged, {0},
     N_("usage: svnlook dirs-changed REPOS_PATH\n\n"
        "Print the directories that were themselves changed (property edits)\n"
        "or whose file children were changed.\n"),
     {'r', 't'} },
    
    {"help", subcommand_help, {"?", "h"},
     N_("usage: svnlook help [SUBCOMMAND...]\n\n"
        "Describe the usage of this program or its subcommands.\n"),
     {svnlook__version} },

    {"history", subcommand_history, {0},
     N_("usage: svnlook history REPOS_PATH [PATH_IN_REPOS]\n\n"
        "Print information about the history of a path in the repository (or\n"
        "the root directory if no path is supplied).\n"),
     {'r', svnlook__show_ids} },

    {"info", subcommand_info, {0},
     N_("usage: svnlook info REPOS_PATH\n\n"
        "Print the author, datestamp, log message size, and log message.\n"),
     {'r', 't'} },

    {"lock", subcommand_lock, {0},
     N_("usage: svnlook lock REPOS_PATH PATH_IN_REPOS\n\n"
        "If a lock exists on a path in the repository, describe it.\n"),
     {0} },

    {"log", subcommand_log, {0},
     N_("usage: svnlook log REPOS_PATH\n\n"
        "Print the log message.\n"),
     {'r', 't'} },

    {"propget", subcommand_pget, {"pget", "pg"},
     N_("usage: svnlook propget REPOS_PATH PROPNAME [PATH_IN_REPOS]\n\n"
        "Print the raw value of a property on a path in the repository.\n"
        "With --revprop, prints the raw value of a revision property.\n"),
     {'r', 't', svnlook__revprop_opt} },

    {"proplist", subcommand_plist, {"plist", "pl"},
     N_("usage: svnlook proplist REPOS_PATH [PATH_IN_REPOS]\n\n"
        "List the properties of a path in the repository, or\n"
        "with the --revprop option, revision properties.\n"
        "With -v, show the property values too.\n"),
     {'r', 't', 'v', svnlook__revprop_opt} },

    {"tree", subcommand_tree, {0},
     N_("usage: svnlook tree REPOS_PATH [PATH_IN_REPOS]\n\n"
        "Print the tree, starting at PATH_IN_REPOS (if supplied, at the root\n"
        "of the tree otherwise), optionally showing node revision ids.\n"),
     {'r', 't', svnlook__show_ids} },

    {"uuid", subcommand_uuid, {0},
     N_("usage: svnlook uuid REPOS_PATH\n\n"
        "Print the repository's UUID.\n"),
     {0} },

    {"youngest", subcommand_youngest, {0},
     N_("usage: svnlook youngest REPOS_PATH\n\n"
        "Print the youngest revision number.\n"),
     {0} },

    { NULL, NULL, {0}, NULL, {0} }
  };


/* Baton for passing option/argument state to a subcommand function. */
struct svnlook_opt_state
{
  const char *repos_path;  /* 'arg0' is always the path to the repository. */
  const char *arg1;        /* Usually an fs path, a propname, or NULL. */
  const char *arg2;        /* Usually an fs path or NULL. */
  svn_revnum_t rev;
  const char *txn;
  svn_boolean_t version;          /* --version */
  svn_boolean_t show_ids;         /* --show-ids */
  svn_boolean_t help;             /* --help */
  svn_boolean_t no_diff_deleted;  /* --no-diff-deleted */
  svn_boolean_t no_diff_added;    /* --no-diff-added */
  svn_boolean_t verbose;          /* --verbose */
  svn_boolean_t revprop;          /* --revprop */
};


typedef struct svnlook_ctxt_t
{
  svn_repos_t *repos;
  svn_fs_t *fs;
  svn_boolean_t is_revision;
  svn_boolean_t show_ids;
  svn_boolean_t no_diff_deleted;
  svn_boolean_t no_diff_added;
  svn_revnum_t rev_id;
  svn_fs_txn_t *txn;
  const char *txn_name /* UTF-8! */;

} svnlook_ctxt_t;

/* A flag to see if we've been cancelled by the client or not. */
static volatile sig_atomic_t cancelled = FALSE;



/*** Helper functions. ***/

/* A signal handler to support cancellation. */
static void
signal_handler (int signum)
{
  apr_signal (signum, SIG_IGN);
  cancelled = TRUE;
}

/* Our cancellation callback. */
static svn_error_t *
check_cancel (void *baton)
{
  if (cancelled)
    return svn_error_create (SVN_ERR_CANCELLED, NULL, _("Caught signal"));
  else
    return SVN_NO_ERROR;
}


/* Version compatibility check */
static svn_error_t *
check_lib_versions (void)
{
  static const svn_version_checklist_t checklist[] =
    {
      { "svn_subr",  svn_subr_version },
      { "svn_repos", svn_repos_version },
      { "svn_fs",    svn_fs_version },
      { "svn_delta", svn_delta_version },
      { "svn_diff",  svn_diff_version },
      { NULL, NULL }
    };

  SVN_VERSION_DEFINE (my_version);
  return svn_ver_check_list (&my_version, checklist);
}


/* Get revision or transaction property PROP_NAME for the revision or
   transaction specified in C, allocating in in POOL and placing it in
   *PROP_VALUE. */
static svn_error_t *
get_property (svn_string_t **prop_value,
              svnlook_ctxt_t *c, 
              const char *prop_name,
              apr_pool_t *pool)
{
  svn_string_t *raw_value;

  /* Fetch transaction property... */
  if (! c->is_revision)
    SVN_ERR (svn_fs_txn_prop (&raw_value, c->txn, prop_name, pool));

  /* ...or revision property -- it's your call. */
  else
    SVN_ERR (svn_fs_revision_prop (&raw_value, c->fs, c->rev_id,
                                   prop_name, pool));

  *prop_value = raw_value;

  return SVN_NO_ERROR;
}


static svn_error_t *
get_root (svn_fs_root_t **root,
          svnlook_ctxt_t *c,
          apr_pool_t *pool)
{
  /* Open up the appropriate root (revision or transaction). */
  if (c->is_revision)
    {
      /* If we didn't get a valid revision number, we'll look at the
         youngest revision. */
      if (! SVN_IS_VALID_REVNUM (c->rev_id))
        SVN_ERR (svn_fs_youngest_rev (&(c->rev_id), c->fs, pool));

      SVN_ERR (svn_fs_revision_root (root, c->fs, c->rev_id, pool));
    }
  else
    {
      SVN_ERR (svn_fs_txn_root (root, c->txn, pool));
    }

  return SVN_NO_ERROR;
}




/*** Tree Routines ***/

/* Generate a generic delta tree. */
static svn_error_t *
generate_delta_tree (svn_repos_node_t **tree,
                     svn_repos_t *repos,
                     svn_fs_root_t *root, 
                     svn_revnum_t base_rev, 
                     svn_boolean_t use_copy_history,
                     apr_pool_t *pool)
{
  svn_fs_root_t *base_root;
  const svn_delta_editor_t *editor;
  void *edit_baton;
  apr_pool_t *edit_pool = svn_pool_create (pool);
  svn_fs_t *fs = svn_repos_fs (repos);

  /* Get the base root. */
  SVN_ERR (svn_fs_revision_root (&base_root, fs, base_rev, pool));

  /* Request our editor. */
  SVN_ERR (svn_repos_node_editor (&editor, &edit_baton, repos,
                                  base_root, root, pool, edit_pool));

  /* Drive our editor. */
  SVN_ERR (svn_repos_replay (root, editor, edit_baton, edit_pool));

  /* Return the tree we just built. */
  *tree = svn_repos_node_from_baton (edit_baton);
  svn_pool_destroy (edit_pool);
  return SVN_NO_ERROR;
}




/*** Tree Printing Routines ***/

/* Recursively print only directory nodes that either a) have property
   mods, or b) contains files that have changed. */
static svn_error_t *
print_dirs_changed_tree (svn_repos_node_t *node,
                         const char *path /* UTF-8! */,
                         apr_pool_t *pool)
{
  svn_repos_node_t *tmp_node;
  int print_me = 0;
  const char *full_path;
  apr_pool_t *subpool;

  SVN_ERR (check_cancel (NULL));

  if (! node)
    return SVN_NO_ERROR;

  /* Not a directory?  We're not interested. */
  if (node->kind != svn_node_dir)
    return SVN_NO_ERROR;

  /* Got prop mods?  Excellent. */
  if (node->prop_mod)
    print_me = 1;

  if (! print_me)
    {
      /* Fly through the list of children, checking for modified files. */
      tmp_node = node->child;
      if (tmp_node)
        {
          if ((tmp_node->kind == svn_node_file)
              || (tmp_node->text_mod)
              || (tmp_node->action == 'A')
              || (tmp_node->action == 'D'))
            {
              print_me = 1;
            }
          while (tmp_node->sibling && (! print_me ))
            {
              tmp_node = tmp_node->sibling;
              if ((tmp_node->kind == svn_node_file)
                  || (tmp_node->text_mod)
                  || (tmp_node->action == 'A')
                  || (tmp_node->action == 'D'))
                {
                  print_me = 1;
                }
            }
        }
    }
  
  /* Print the node if it qualifies. */
  if (print_me)
    {
      SVN_ERR (svn_cmdline_printf (pool, "%s/\n", path));
    }

  /* Return here if the node has no children. */
  tmp_node = node->child;
  if (! tmp_node)
    return SVN_NO_ERROR;

  /* Recursively handle the node's children. */
  subpool = svn_pool_create (pool);  
  full_path = svn_path_join (path, tmp_node->name, subpool);
  SVN_ERR (print_dirs_changed_tree (tmp_node, full_path, subpool));
  while (tmp_node->sibling)
    {
      svn_pool_clear (subpool);
      tmp_node = tmp_node->sibling;
      full_path = svn_path_join (path, tmp_node->name, subpool);
      SVN_ERR (print_dirs_changed_tree (tmp_node, full_path, subpool));
    }
  svn_pool_destroy (subpool);

  return SVN_NO_ERROR;
}


/* Recursively print all nodes in the tree that have been modified
   (do not include directories affected only by "bubble-up"). */
static svn_error_t *
print_changed_tree (svn_repos_node_t *node,
                    const char *path /* UTF-8! */,
                    apr_pool_t *pool)
{
  const char *full_path;
  char status[3] = "_ ";
  int print_me = 1;
  apr_pool_t *subpool;

  SVN_ERR (check_cancel (NULL));

  if (! node)
    return SVN_NO_ERROR;

  /* Print the node. */
  if (node->action == 'A')
    status[0] = 'A';
  else if (node->action == 'D')
    status[0] = 'D';
  else if (node->action == 'R')
    {
      if ((! node->text_mod) && (! node->prop_mod))
        print_me = 0;
      if (node->text_mod)
        status[0] = 'U';
      if (node->prop_mod)
        status[1] = 'U';
    }
  else
    print_me = 0;

  /* Print this node unless told to skip it. */
  if (print_me)
    {
      SVN_ERR (svn_cmdline_printf (pool, "%s  %s%s\n",
                                   status,
                                   path,
                                   node->kind == svn_node_dir ? "/" : ""));
    }
  
  /* Return here if the node has no children. */
  node = node->child;
  if (! node)
    return SVN_NO_ERROR;

  /* Recursively handle the node's children. */
  subpool = svn_pool_create (pool);  
  full_path = svn_path_join (path, node->name, subpool);
  SVN_ERR (print_changed_tree (node, full_path, subpool));
  while (node->sibling)
    {
      svn_pool_clear (subpool);
      node = node->sibling;
      full_path = svn_path_join (path, node->name, subpool);
      SVN_ERR (print_changed_tree (node, full_path, subpool));
    }
  svn_pool_destroy (subpool);

  return SVN_NO_ERROR;
}


static svn_error_t *
open_writable_binary_file (apr_file_t **fh, 
                           const char *path /* UTF-8! */, 
                           apr_pool_t *pool)
{
  apr_array_header_t *path_pieces;
  svn_error_t *err;
  int i;
  const char *full_path, *dir;
  
  /* Try the easy way to open the file. */
  err = svn_io_file_open (fh, path, 
                          APR_WRITE | APR_CREATE | APR_TRUNCATE | APR_BINARY,
                          APR_OS_DEFAULT, pool);
  if (! err)
    return SVN_NO_ERROR;

  svn_path_split (path, &dir, NULL, pool);

  /* If the file path has no parent, then we've already tried to open
     it as best as we care to try above. */
  if (svn_path_is_empty (dir))
    return err;

  path_pieces = svn_path_decompose (dir, pool);
  if (! path_pieces->nelts)
    return SVN_NO_ERROR;

  full_path = "";
  for (i = 0; i < path_pieces->nelts; i++)
    {
      svn_node_kind_t kind;
      const char *piece = ((const char **) (path_pieces->elts))[i];
      full_path = svn_path_join (full_path, piece, pool);
      SVN_ERR (svn_io_check_resolved_path (full_path, &kind, pool));

      /* Does this path component exist at all? */
      if (kind == svn_node_none)
        {
          SVN_ERR (svn_io_dir_make (full_path, APR_OS_DEFAULT, pool));
        }
      else if (kind != svn_node_dir)
        {
          return svn_error_createf (err->apr_err, err,
                                    _("Error creating dir '%s' (path exists)"),
                                    full_path);
        }
    }

  /* Now that we are ensured that the parent path for this file
     exists, try once more to open it. */
  svn_error_clear (err);
  return svn_io_file_open (fh, path, 
                           APR_WRITE | APR_CREATE | APR_TRUNCATE | APR_BINARY,
                           APR_OS_DEFAULT, pool);
}


static svn_error_t *
dump_contents (apr_file_t *fh,
               svn_fs_root_t *root,
               const char *path /* UTF-8! */,
               apr_pool_t *pool)
{
  svn_stream_t *contents, *file_stream;

  /* Grab the contents and copy them into fh. */
  SVN_ERR (svn_fs_file_contents (&contents, root, path, pool));
  file_stream = svn_stream_from_aprfile (fh, pool);
  SVN_ERR (svn_stream_copy (contents, file_stream, pool));
  return SVN_NO_ERROR;
}


/* Prepare temporary files *TMPFILE1 and *TMPFILE2 for diffing
   PATH1@ROOT1 versus PATH2@ROOT2.  If either ROOT1 or ROOT2 is NULL,
   the temporary file for its path/root will be an empty one.
   Otherwise, its temporary file will contain the contents of that
   path/root in the repository.

   An exception to this is when either path/root has an svn:mime-type
   property set on it which indicates that the file contains
   non-textual data -- in this case, the *IS_BINARY flag is set and no
   temporary files are created.

   Use POOL for all that allocation goodness. */
static svn_error_t *
prepare_tmpfiles (const char **tmpfile1,
                  const char **tmpfile2,
                  svn_boolean_t *is_binary,
                  svn_fs_root_t *root1,
                  const char *path1,
                  svn_fs_root_t *root2,
                  const char *path2,
                  const char *tmpdir,
                  apr_pool_t *pool)
{
  svn_string_t *mimetype;
  apr_file_t *fh;

  /* Init the return values. */
  *tmpfile1 = NULL;
  *tmpfile2 = NULL;
  *is_binary = FALSE;

  assert (path1 && path2);

  /* Check for binary mimetypes.  If either file has a binary
     mimetype, get outta here.  */
  if (root1)
    {
      SVN_ERR (svn_fs_node_prop (&mimetype, root1, path1, 
                                 SVN_PROP_MIME_TYPE, pool));
      if (mimetype && svn_mime_type_is_binary (mimetype->data))
        {
          *is_binary = TRUE;
          return SVN_NO_ERROR;
        }
    }
  if (root2)
    {
      SVN_ERR (svn_fs_node_prop (&mimetype, root2, path2, 
                                 SVN_PROP_MIME_TYPE, pool));
      if (mimetype && svn_mime_type_is_binary (mimetype->data))
        {
          *is_binary = TRUE;
          return SVN_NO_ERROR;
        }
    }

  /* Now, prepare the two temporary files, each of which will either
     be empty, or will have real contents.  The first file we will
     make in our temporary directory. */
  *tmpfile2 = svn_path_join (tmpdir, path2, pool);
  SVN_ERR (open_writable_binary_file (&fh, *tmpfile2, pool));
  if (root2)
    SVN_ERR (dump_contents (fh, root2, path2, pool));
  apr_file_close (fh);

  /* The second file is constructed from the first one's path. */
  SVN_ERR (svn_io_open_unique_file (&fh, tmpfile1, *tmpfile2, 
                                    NULL, FALSE, pool));
  if (root1)
    SVN_ERR (dump_contents (fh, root1, path1, pool));
  apr_file_close (fh);

  return SVN_NO_ERROR;
}


/* Generate a diff label for PATH in ROOT, allocating in POOL. */
static svn_error_t *
generate_label (const char **label,
                svn_fs_root_t *root,
                const char *path,
                apr_pool_t *pool)
{
  svn_fs_t *fs = svn_fs_root_fs (root);
  svn_string_t *date;
  const char *datestr;
  const char *name = NULL;
  svn_revnum_t rev = SVN_INVALID_REVNUM;

  if (svn_fs_is_revision_root (root))
    {
      rev = svn_fs_revision_root_revision (root);
      SVN_ERR (svn_fs_revision_prop (&date, fs, rev, 
                                     SVN_PROP_REVISION_DATE, pool));
    }
  else 
    {
      svn_fs_txn_t *txn;
      name = svn_fs_txn_root_name (root, pool);
      SVN_ERR (svn_fs_open_txn (&txn, fs, name, pool));
      SVN_ERR (svn_fs_txn_prop (&date, txn, SVN_PROP_REVISION_DATE, pool));
    }
  
  if (date)
    {
      datestr = date->data;
      ((char *)datestr)[10] = ' ';
      ((char *)datestr)[19] = '\0';
    }
  else
    {
      datestr = "                      ";
    }

  if (name)
    *label = apr_psprintf (pool, "%s\t%s UTC (txn %s)", 
                           path, datestr, name); 
  else
    *label = apr_psprintf (pool, "%s\t%s UTC (rev %ld)",
                           path, datestr, rev);
  return SVN_NO_ERROR;
}


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


/* Helper function to display differences in properties of a file */
static svn_error_t *
display_prop_diffs (const apr_array_header_t *prop_diffs,
                    apr_hash_t *orig_props,
                    const char *path,
                    apr_pool_t *pool)
{
  int i;

  SVN_ERR (svn_cmdline_printf (pool, "\nProperty changes on: %s\n%s\n",
                               path, under_string));

  for (i = 0; i < prop_diffs->nelts; i++)
    {
      const svn_string_t *orig_value;
      const svn_prop_t *pc = &APR_ARRAY_IDX (prop_diffs, i, svn_prop_t);

      SVN_ERR (check_cancel (NULL));

      if (orig_props)
        orig_value = apr_hash_get (orig_props, pc->name, APR_HASH_KEY_STRING);
      else
        orig_value = NULL;

      SVN_ERR (svn_cmdline_printf (pool, _("Name: %s\n"), pc->name));

      /* 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.
         ### We don't use svn_cmdline_printf here, since we don't know if the
         values are UTF-8. */
      {
        svn_boolean_t val_to_utf8 = svn_prop_is_svn_prop (pc->name);
        const char *printable_val;

        if (orig_value != NULL)
          {
            if (val_to_utf8)
              SVN_ERR (svn_cmdline_cstring_from_utf8 (&printable_val, 
                                                      orig_value->data, pool));
            else
              printable_val = orig_value->data;
            printf ("   - %s\n", printable_val);
          }

        if (pc->value != NULL)
          {
            if (val_to_utf8)
              SVN_ERR (svn_cmdline_cstring_from_utf8
                       (&printable_val, pc->value->data, pool));
            else
              printable_val = pc->value->data;
            printf ("   + %s\n", printable_val);
          }
      }
    }

  SVN_ERR (svn_cmdline_printf (pool, "\n"));
  return svn_cmdline_fflush (stdout);
}



/* Recursively print all nodes in the tree that have been modified
   (do not include directories affected only by "bubble-up"). */
static svn_error_t *
print_diff_tree (svn_fs_root_t *root,
                 svn_fs_root_t *base_root,
                 svn_repos_node_t *node, 
                 const char *path /* UTF-8! */,
                 const char *base_path /* UTF-8! */,
                 svn_boolean_t no_diff_deleted,
                 svn_boolean_t no_diff_added,
                 const char *tmpdir,
                 apr_pool_t *pool)
{
  const char *orig_path = NULL, *new_path = NULL;
  svn_boolean_t do_diff = FALSE;
  svn_boolean_t is_copy = FALSE;
  svn_boolean_t binary = FALSE;
  apr_pool_t *subpool;

  SVN_ERR (check_cancel (NULL));

  if (! node)
    return SVN_NO_ERROR;

  /* Print copyfrom history for the top node of a copied tree. */
  if ((SVN_IS_VALID_REVNUM (node->copyfrom_rev))
      && (node->copyfrom_path != NULL))
    {
      /* This is ... a copy. */
      is_copy = TRUE;

      /* Propagate the new base.  Copyfrom paths usually start with a
         slash; we remove it for consistency with the target path.
         ### Yes, it would be *much* better for something in the path
             library to be taking care of this! */
      if (node->copyfrom_path[0] == '/')
        base_path = apr_pstrdup (pool, node->copyfrom_path + 1);
      else
        base_path = apr_pstrdup (pool, node->copyfrom_path);

      SVN_ERR (svn_cmdline_printf (pool, _("Copied: %s (from rev %ld, %s)\n"),
                                   path, node->copyfrom_rev, base_path));

      SVN_ERR (svn_fs_revision_root (&base_root,
                                     svn_fs_root_fs (base_root),
                                     node->copyfrom_rev, pool));
    }

  /*** First, we'll just print file content diffs. ***/
  if (node->kind == svn_node_file)
    {
      /* Here's the generalized way we do our diffs:

         - First, we'll check for svn:mime-type properties on the old
           and new files.  If either has such a property, and it
           represents a binary type, we won't actually be doing a real
           diff.
           
         - First, dump the contents of the new version of the file
           into the svnlook temporary directory, building out the
           actual directories that need to be created in order to
           fully represent the filesystem path inside the tmp
           directory.

         - Then, dump the contents of the old version of the file into
           the top level of the svnlook temporary directory using a
           unique temporary file name (we do this *after* putting the
           new version of the file there in case something actually
           versioned has a name that looks like one of our unique
           identifiers).

         - Next, we run 'diff', passing the repository path as the
           label.  

         - Finally, we delete the temporary files (but leave the
           built-out directories in place until after all diff
           handling has been finished).  */
      if ((node->action == 'R') && (node->text_mod))
        {
          do_diff = TRUE;
          SVN_ERR (prepare_tmpfiles (&orig_path, &new_path, &binary,
                                     base_root, base_path, root, path,
                                     tmpdir, pool));
        }
      if ((node->action == 'A') && (node->text_mod))
        {
          do_diff = TRUE;
          SVN_ERR (prepare_tmpfiles (&orig_path, &new_path, &binary,
                                     NULL, base_path, root, path,
                                     tmpdir, pool));
        }
      if (node->action == 'D')
        {
          do_diff = TRUE;
          SVN_ERR (prepare_tmpfiles (&orig_path, &new_path, &binary,
                                     base_root, base_path, NULL, path,
                                     tmpdir, pool));
        }
    }

  if (do_diff)
    {
      if (! is_copy)
        SVN_ERR (svn_cmdline_printf (pool, "%s: %s\n", 
                                     ((node->action == 'A') ? _("Added") :
                                      ((node->action == 'D') ? _("Deleted") :
                                       ((node->action == 'R') ? _("Modified")
                                        : _("Index")))),
                                     path));

      if (!(node->action == 'D' && no_diff_deleted)
          && !(node->action == 'A' && no_diff_added))
        {
          svn_diff_t *diff;

          SVN_ERR (svn_cmdline_printf (pool, "%s\n", equal_string));
          SVN_ERR (svn_cmdline_fflush (stdout));

          if (binary)
            {
              SVN_ERR (svn_cmdline_printf (pool,
                                           _("(Binary files differ)\n")));
            }
          else
            {
              SVN_ERR (svn_diff_file_diff (&diff, orig_path, new_path, pool));
              if (svn_diff_contains_diffs (diff))
                {
                  svn_stream_t *ostream;
                  const char *orig_label, *new_label;

                  SVN_ERR (svn_stream_for_stdout (&ostream, pool));
                  
                  SVN_ERR (generate_label (&orig_label, base_root, 
                                           base_path, pool));
                  SVN_ERR (generate_label (&new_label, root, path, pool));
                  SVN_ERR (svn_diff_file_output_unified (ostream, diff, 
                                                         orig_path, new_path,
                                                         orig_label, new_label,
                                                         pool));
                  SVN_ERR (svn_stream_close (ostream));
                }
            }
        }

      SVN_ERR (svn_cmdline_printf (pool, "\n"));
      SVN_ERR (svn_cmdline_fflush (stdout));
    }
  else if (is_copy)
    {
      SVN_ERR (svn_cmdline_printf (pool, "\n"));
    }

  /* Make sure we delete any temporary files. */
  if (orig_path)
    SVN_ERR (svn_io_remove_file (orig_path, pool));
  if (new_path)
    SVN_ERR (svn_io_remove_file (new_path, pool));

  /*** Now handle property diffs ***/
  if ((node->prop_mod) && (node->action != 'D'))
    {
      apr_hash_t *local_proptable;
      apr_hash_t *base_proptable;
      apr_array_header_t *propchanges, *props;

      SVN_ERR (svn_fs_node_proplist (&local_proptable, root, path, pool));
      if (node->action == 'A')
        base_proptable = apr_hash_make (pool);
      else
        SVN_ERR (svn_fs_node_proplist (&base_proptable, base_root, 
                                       base_path, pool));
      SVN_ERR (svn_prop_diffs (&propchanges, local_proptable, 
                               base_proptable, pool));
      SVN_ERR (svn_categorize_props (propchanges, NULL, NULL, &props, pool));
      if (props->nelts > 0)
        SVN_ERR (display_prop_diffs (props, base_proptable, path, pool));
    }

  /* Return here if the node has no children. */
  node = node->child;
  if (! node)
    return SVN_NO_ERROR;

  /* Recursively handle the node's children. */
  subpool = svn_pool_create (pool);
  SVN_ERR (print_diff_tree (root, base_root, node,
                            svn_path_join (path, node->name, subpool),
                            svn_path_join (base_path, node->name, subpool),
                            no_diff_deleted,
                            no_diff_added,
                            tmpdir,
                            subpool));
  while (node->sibling)
    {
      svn_pool_clear (subpool);
      node = node->sibling;
      SVN_ERR (print_diff_tree (root, base_root, node,
                                svn_path_join (path, node->name, subpool),
                                svn_path_join (base_path, node->name, subpool),
                                no_diff_deleted,
                                no_diff_added,
                                tmpdir,
                                subpool));
    }
  apr_pool_destroy (subpool);

  return SVN_NO_ERROR;
}


/* Recursively print all nodes, and (optionally) their node revision ids.

   ROOT is the revision or transaction root used to build that tree.
   PATH and ID are the current path and node revision id being
   printed, and INDENTATION the number of spaces to prepent to that
   path's printed output.  ID may be NULL if SHOW_IDS is FALSE (in
   which case, ids won't be printed at all).  

   Use POOL for all allocations.  */
static svn_error_t *
print_tree (svn_fs_root_t *root,
            const char *path /* UTF-8! */,
            const svn_fs_id_t *id,
            svn_boolean_t is_dir,
            int indentation,
            svn_boolean_t show_ids,
            apr_pool_t *pool)
{
  apr_pool_t *subpool;
  int i;
  apr_hash_t *entries;
  apr_hash_index_t *hi;

  SVN_ERR (check_cancel (NULL));

  /* Print the indentation. */
  for (i = 0; i < indentation; i++)
    {
      SVN_ERR (svn_cmdline_fputs (" ", stdout, pool));
    }

  /* Print the node. */
  SVN_ERR (svn_cmdline_printf (pool, "%s%s",
                               svn_path_basename (path, pool),
                               is_dir ? "/" : ""));

  if (show_ids)
    {
      svn_string_t *unparsed_id = NULL;
      if (id)
        unparsed_id = svn_fs_unparse_id (id, pool);
      SVN_ERR (svn_cmdline_printf (pool, " <%s>",
                                   unparsed_id
{'fraction_non_alphanumeric': 0.11571180290880263, 'fraction_numerical': 0.003597890050919291, 'mean_word_length': 2.1046478606588415, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 199, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 82, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7189200', 'n_tokens_mistral': 22202, 'n_tokens_neox': 21556, 'n_words': 5737}