<commit_before>/*
 * main.c: Subversion server inspection tool.
 *
 * ====================================================================
 * Copyright (c) 2000-2003 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 <apr_general.h>
#include <apr_pools.h>
#include <apr_time.h>
#include <apr_thread_proc.h>
#include <apr_file_io.h>

#define APR_WANT_STDIO
#define APR_WANT_STRFUNC
#include <apr_want.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_repos.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"



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

/* Temporary subdirectory created for use by svnlook.  ### todo: we
   need to either find a way to query APR for a suitable (that is,
   writable) temporary directory, or add this #define to the
   svn_private_config.h stuffs (with a default of perhaps "/tmp/.svnlook" */
#define SVNLOOK_TMPDIR       ".svnlook"



/* Option handling. */

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

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

/*
 * 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,
     "show help on a subcommand"},

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

    {"revision",      'r', 1,
     "specify revision number ARG"},

    {"transaction",  't', 1,
     "specify transaction name ARG"},

    {"verbose",  'v', 0,
     "be verbose"},

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

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

    {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},
     "usage: svnlook author REPOS_PATH\n\n"
     "Print the author.\n",
     {'r', 't'} },
    
    {"cat", subcommand_cat, {0},
     "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},
     "usage: svnlook changed REPOS_PATH\n\n"
     "Print the paths that were changed.\n",
     {'r', 't'} },
    
    {"date", subcommand_date, {0},
     "usage: svnlook date REPOS_PATH\n\n"
     "Print the datestamp.\n",
     {'r', 't'} },

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

    {"dirs-changed", subcommand_dirschanged, {0},
     "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"},
     "usage: svn help [SUBCOMMAND1 [SUBCOMMAND2] ...]\n\n"
     "Display this usage message.\n",
     {0} },

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

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

    {"propget", subcommand_pget, {"pget", "pg"},
     "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",
     {'r', 't'} },

    {"proplist", subcommand_plist, {"plist", "pl"},
     "usage: svnlook proplist REPOS_PATH PATH_IN_REPOS\n\n"
     "List the properties of a path in the repository.\n"
     "With -v, show the property values too.\n",
     {'r', 't', 'v'} },

    {"tree", subcommand_tree, {0},
     "usage: svnlook tree REPOS_PATH\n\n"
     "Print the tree, optionally showing node revision ids.\n",
     {'r', 't', svnlook__show_ids} },

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

    {"youngest", subcommand_youngest, {0},
     "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 show_ids;
  svn_boolean_t help;
  svn_boolean_t no_diff_deleted;
  svn_boolean_t verbose;   /* Verbose output. */
};


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_revnum_t rev_id;
  svn_fs_txn_t *txn;
  const char *txn_name /* UTF-8! */;

} svnlook_ctxt_t;




/*** Helper functions. ***/
static svn_error_t *
get_property (svn_string_t **prop_value /* native */,
              svnlook_ctxt_t *c, 
              const char *prop_name /* UTF-8! */,
              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));

  if (svn_prop_needs_translation (prop_name))
    SVN_ERR (svn_subst_detranslate_string (&raw_value, raw_value, 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_dir_delta (base_root, "", NULL, root, "",
                                editor, edit_baton, 
                                FALSE, TRUE, FALSE, 
                                use_copy_history, FALSE, 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;

  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)
    {
      const char *path_native;
      SVN_ERR (svn_utf_cstring_from_utf8 (&path_native, path, pool));
      printf ("%s/\n", path_native);
    }

  /* Recursively handle the node's children. */
  tmp_node = node->child;
  if (! tmp_node)
    return SVN_NO_ERROR;

  full_path = svn_path_join (path, tmp_node->name, pool);
  SVN_ERR (print_dirs_changed_tree (tmp_node, full_path, pool));
  while (tmp_node->sibling)
    {
      tmp_node = tmp_node->sibling;
      full_path = svn_path_join (path, tmp_node->name, pool);
      SVN_ERR (print_dirs_changed_tree (tmp_node, full_path, pool));
    }

  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;

  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)
    {
      const char *path_native;
      SVN_ERR (svn_utf_cstring_from_utf8 (&path_native, path, pool));
      printf ("%s  %s%s\n",
              status,
              path_native,
              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. */
  full_path = svn_path_join (path, node->name, pool);
  SVN_ERR (print_changed_tree (node, full_path, pool));
  while (node->sibling)
    {
      node = node->sibling;
      full_path = svn_path_join (path, node->name, pool);
      SVN_ERR (print_changed_tree (node, full_path, pool));
    }

  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 svn_error_createf (err->apr_err, err,
                              "Error opening writable file '%s'", path);

  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_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)
        {
          if (err)
            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. */
  err = svn_io_file_open (fh, path, 
                          APR_WRITE | APR_CREATE | APR_TRUNCATE | APR_BINARY,
                          APR_OS_DEFAULT, pool);
  if (err)
    return svn_error_createf (err->apr_err, err,
                              "Error opening writable file '%s'", path);
    
  return SVN_NO_ERROR;
}


static svn_error_t *
dump_contents (apr_file_t *fh,
               svn_fs_root_t *root,
               const char *path /* UTF-8! */,
               apr_pool_t *pool)
{
  apr_status_t apr_err;
  apr_size_t len, len2;
  svn_stream_t *stream;
  char buffer[1024];

  /* Get a stream to the current file's contents. */
  SVN_ERR (svn_fs_file_contents (&stream, root, path, pool));
  
  /* Now, route that data into our temporary file. */
  while (1)
    {
      len = sizeof (buffer);
      SVN_ERR (svn_stream_read (stream, buffer, &len));
      len2 = len;
      apr_err = apr_file_write (fh, buffer, &len2);
      if ((apr_err) || (len2 != len))
        return svn_error_createf 
          (apr_err ? apr_err : SVN_ERR_INCOMPLETE_DATA, NULL,
           "Error writing contents of '%s'", path);
      if (len != sizeof (buffer))
        break;
    }

  /* And close the file. */
  apr_file_close (fh);
  return SVN_NO_ERROR;
}


/* Generate a diff label for PATH in ROOT, allocating in POOL. */
static const char *
generate_label (svn_fs_root_t *root,
                const char *path,
                apr_pool_t *pool)
{
  const char *label = "";
  if (svn_fs_is_revision_root (root))
    {
      svn_revnum_t rev = svn_fs_revision_root_revision (root);
      label = apr_psprintf (pool, "%s\t(r%" SVN_REVNUM_T_FMT ")", path, rev);
    }
  else 
    {
      /* Only our *new* path can be a transaction */
      label = apr_pstrcat (pool, path, "\t(new)", NULL);
    }
  return label;
}


/* 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,
                 apr_pool_t *pool)
{
  const char *orig_path = NULL, *new_path = NULL, *path_native;
  apr_file_t *fh1, *fh2;
  svn_boolean_t is_copy = FALSE;

  if (! node)
    return SVN_NO_ERROR;

  SVN_ERR (svn_utf_cstring_from_utf8 (&path_native, path, pool));

  /* Print copyfrom history for the top node of a copied tree. */
  if ((SVN_IS_VALID_REVNUM (node->copyfrom_rev))
      && (node->copyfrom_path != NULL))
    {
      const char *base_path_native;

      /* 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_utf_cstring_from_utf8 (&base_path_native, base_path, pool));

      printf ("Copied: %s (from rev %" SVN_REVNUM_T_FMT ", %s)\n",
              path_native, node->copyfrom_rev, base_path_native);

      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, 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))
        {
          new_path = svn_path_join (SVNLOOK_TMPDIR, path, pool);
          SVN_ERR (open_writable_binary_file (&fh1, new_path, pool));
          SVN_ERR (dump_contents (fh1, root, path, pool));
          apr_file_close (fh1);

          SVN_ERR (svn_io_open_unique_file (&fh2, &orig_path, new_path,
                                            NULL, FALSE, pool));
          SVN_ERR (dump_contents
                   (fh2, base_root, base_path, pool));
          apr_file_close (fh2);
        }
      if ((node->action == 'A') && (node->text_mod))
        {
          new_path = svn_path_join (SVNLOOK_TMPDIR, path, pool);
          SVN_ERR (open_writable_binary_file (&fh1, new_path, pool));
          SVN_ERR (dump_contents (fh1, root, path, pool));
          apr_file_close (fh1);

          /* Create an empty file. */
          SVN_ERR (svn_io_open_unique_file (&fh2, &orig_path, new_path,
                                            NULL, FALSE, pool));
          apr_file_close (fh2);
        }
      if (node->action == 'D')
        {
          new_path = svn_path_join (SVNLOOK_TMPDIR, path, pool);
          SVN_ERR (open_writable_binary_file (&fh1, new_path, pool));
          apr_file_close (fh1);

          SVN_ERR (svn_io_open_unique_file (&fh2, &orig_path, new_path,
                                            NULL, FALSE, pool));
          SVN_ERR (dump_contents
                   (fh2, base_root, base_path, pool));
          apr_file_close (fh2);
        }
    }

  if (orig_path && new_path)
    {
      if (! is_copy)
        printf ("%s: %s\n", 
                ((node->action == 'A') ? "Added" : 
                 ((node->action == 'D') ? "Deleted" :
                  ((node->action == 'R') ? "Modified" : "Index"))),
                path_native);

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

          printf ("===========================================================\
===================\n");
          fflush (stdout);

          /* ### TODO: Read the mime-type property of our two diffy
             items, and if either indicates binariness, don't do the
             diff. */
          
          SVN_ERR (svn_diff_file_diff (&diff, orig_path, new_path, pool));
          if (svn_diff_contains_diffs (diff))
            {
              apr_file_t *outhandle;
              apr_status_t apr_err;
              const char *orig_label, *new_label;

              /* Get an apr_file_t representing stdout, which is where
                 we'll have the diff program print to. */
              apr_err = apr_file_open_stdout (&outhandle, pool);
              if (apr_err)
                return svn_error_create 
                  (apr_err, NULL,
                   "print_diff_tree: can't open handle to stdout");

              orig_label = generate_label (base_root, base_path, pool);
              new_label = generate_label (root, path, pool);
              SVN_ERR (svn_diff_file_output_unified (outhandle, diff, 
                                                     orig_path, new_path,
                                                     orig_label, new_label,
                                                     pool));
            }
        }

      printf ("\n");
      fflush (stdout);
    }
  else if (is_copy)
    {
      printf ("\n");
    }
    
  /* Now, delete any temporary files. */
  if (orig_path)
    svn_io_remove_file (orig_path, pool);<commit_msg>* subversion/svnlook/main.c   (print_diff_tree): No longer try to print diffs if either of the     diff files is binary.<commit_after>/*
 * main.c: Subversion server inspection tool.
 *
 * ====================================================================
 * Copyright (c) 2000-2003 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 <apr_general.h>
#include <apr_pools.h>
#include <apr_time.h>
#include <apr_thread_proc.h>
#include <apr_file_io.h>

#define APR_WANT_STDIO
#define APR_WANT_STRFUNC
#include <apr_want.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_repos.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"



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

/* Temporary subdirectory created for use by svnlook.  ### todo: we
   need to either find a way to query APR for a suitable (that is,
   writable) temporary directory, or add this #define to the
   svn_private_config.h stuffs (with a default of perhaps "/tmp/.svnlook" */
#define SVNLOOK_TMPDIR       ".svnlook"



/* Option handling. */

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

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

/*
 * 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,
     "show help on a subcommand"},

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

    {"revision",      'r', 1,
     "specify revision number ARG"},

    {"transaction",  't', 1,
     "specify transaction name ARG"},

    {"verbose",  'v', 0,
     "be verbose"},

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

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

    {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},
     "usage: svnlook author REPOS_PATH\n\n"
     "Print the author.\n",
     {'r', 't'} },
    
    {"cat", subcommand_cat, {0},
     "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},
     "usage: svnlook changed REPOS_PATH\n\n"
     "Print the paths that were changed.\n",
     {'r', 't'} },
    
    {"date", subcommand_date, {0},
     "usage: svnlook date REPOS_PATH\n\n"
     "Print the datestamp.\n",
     {'r', 't'} },

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

    {"dirs-changed", subcommand_dirschanged, {0},
     "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"},
     "usage: svn help [SUBCOMMAND1 [SUBCOMMAND2] ...]\n\n"
     "Display this usage message.\n",
     {0} },

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

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

    {"propget", subcommand_pget, {"pget", "pg"},
     "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",
     {'r', 't'} },

    {"proplist", subcommand_plist, {"plist", "pl"},
     "usage: svnlook proplist REPOS_PATH PATH_IN_REPOS\n\n"
     "List the properties of a path in the repository.\n"
     "With -v, show the property values too.\n",
     {'r', 't', 'v'} },

    {"tree", subcommand_tree, {0},
     "usage: svnlook tree REPOS_PATH\n\n"
     "Print the tree, optionally showing node revision ids.\n",
     {'r', 't', svnlook__show_ids} },

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

    {"youngest", subcommand_youngest, {0},
     "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 show_ids;
  svn_boolean_t help;
  svn_boolean_t no_diff_deleted;
  svn_boolean_t verbose;   /* Verbose output. */
};


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_revnum_t rev_id;
  svn_fs_txn_t *txn;
  const char *txn_name /* UTF-8! */;

} svnlook_ctxt_t;




/*** Helper functions. ***/
static svn_error_t *
get_property (svn_string_t **prop_value /* native */,
              svnlook_ctxt_t *c, 
              const char *prop_name /* UTF-8! */,
              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));

  if (svn_prop_needs_translation (prop_name))
    SVN_ERR (svn_subst_detranslate_string (&raw_value, raw_value, 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_dir_delta (base_root, "", NULL, root, "",
                                editor, edit_baton, 
                                FALSE, TRUE, FALSE, 
                                use_copy_history, FALSE, 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;

  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)
    {
      const char *path_native;
      SVN_ERR (svn_utf_cstring_from_utf8 (&path_native, path, pool));
      printf ("%s/\n", path_native);
    }

  /* Recursively handle the node's children. */
  tmp_node = node->child;
  if (! tmp_node)
    return SVN_NO_ERROR;

  full_path = svn_path_join (path, tmp_node->name, pool);
  SVN_ERR (print_dirs_changed_tree (tmp_node, full_path, pool));
  while (tmp_node->sibling)
    {
      tmp_node = tmp_node->sibling;
      full_path = svn_path_join (path, tmp_node->name, pool);
      SVN_ERR (print_dirs_changed_tree (tmp_node, full_path, pool));
    }

  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;

  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)
    {
      const char *path_native;
      SVN_ERR (svn_utf_cstring_from_utf8 (&path_native, path, pool));
      printf ("%s  %s%s\n",
              status,
              path_native,
              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. */
  full_path = svn_path_join (path, node->name, pool);
  SVN_ERR (print_changed_tree (node, full_path, pool));
  while (node->sibling)
    {
      node = node->sibling;
      full_path = svn_path_join (path, node->name, pool);
      SVN_ERR (print_changed_tree (node, full_path, pool));
    }

  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 svn_error_createf (err->apr_err, err,
                              "Error opening writable file '%s'", path);

  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_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)
        {
          if (err)
            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. */
  err = svn_io_file_open (fh, path, 
                          APR_WRITE | APR_CREATE | APR_TRUNCATE | APR_BINARY,
                          APR_OS_DEFAULT, pool);
  if (err)
    return svn_error_createf (err->apr_err, err,
                              "Error opening writable file '%s'", path);
    
  return SVN_NO_ERROR;
}


static svn_error_t *
dump_contents (apr_file_t *fh,
               svn_fs_root_t *root,
               const char *path /* UTF-8! */,
               apr_pool_t *pool)
{
  apr_status_t apr_err;
  apr_size_t len, len2;
  svn_stream_t *stream;
  char buffer[1024];

  /* Get a stream to the current file's contents. */
  SVN_ERR (svn_fs_file_contents (&stream, root, path, pool));
  
  /* Now, route that data into our temporary file. */
  while (1)
    {
      len = sizeof (buffer);
      SVN_ERR (svn_stream_read (stream, buffer, &len));
      len2 = len;
      apr_err = apr_file_write (fh, buffer, &len2);
      if ((apr_err) || (len2 != len))
        return svn_error_createf 
          (apr_err ? apr_err : SVN_ERR_INCOMPLETE_DATA, NULL,
           "Error writing contents of '%s'", path);
      if (len != sizeof (buffer))
        break;
    }

  /* And close the file. */
  apr_file_close (fh);
  return SVN_NO_ERROR;
}


/* Generate a diff label for PATH in ROOT, allocating in POOL. */
static const char *
generate_label (svn_fs_root_t *root,
                const char *path,
                apr_pool_t *pool)
{
  const char *label = "";
  if (svn_fs_is_revision_root (root))
    {
      svn_revnum_t rev = svn_fs_revision_root_revision (root);
      label = apr_psprintf (pool, "%s\t(r%" SVN_REVNUM_T_FMT ")", path, rev);
    }
  else 
    {
      /* Only our *new* path can be a transaction */
      label = apr_pstrcat (pool, path, "\t(new)", NULL);
    }
  return label;
}


/* 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,
                 apr_pool_t *pool)
{
  const char *orig_path = NULL, *new_path = NULL, *path_native;
  apr_file_t *fh1, *fh2;
  svn_boolean_t is_copy = FALSE;

  if (! node)
    return SVN_NO_ERROR;

  SVN_ERR (svn_utf_cstring_from_utf8 (&path_native, path, pool));

  /* Print copyfrom history for the top node of a copied tree. */
  if ((SVN_IS_VALID_REVNUM (node->copyfrom_rev))
      && (node->copyfrom_path != NULL))
    {
      const char *base_path_native;

      /* 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_utf_cstring_from_utf8 (&base_path_native, base_path, pool));

      printf ("Copied: %s (from rev %" SVN_REVNUM_T_FMT ", %s)\n",
              path_native, node->copyfrom_rev, base_path_native);

      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, 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))
        {
          new_path = svn_path_join (SVNLOOK_TMPDIR, path, pool);
          SVN_ERR (open_writable_binary_file (&fh1, new_path, pool));
          SVN_ERR (dump_contents (fh1, root, path, pool));
          apr_file_close (fh1);

          SVN_ERR (svn_io_open_unique_file (&fh2, &orig_path, new_path,
                                            NULL, FALSE, pool));
          SVN_ERR (dump_contents
                   (fh2, base_root, base_path, pool));
          apr_file_close (fh2);
        }
      if ((node->action == 'A') && (node->text_mod))
        {
          new_path = svn_path_join (SVNLOOK_TMPDIR, path, pool);
          SVN_ERR (open_writable_binary_file (&fh1, new_path, pool));
          SVN_ERR (dump_contents (fh1, root, path, pool));
          apr_file_close (fh1);

          /* Create an empty file. */
          SVN_ERR (svn_io_open_unique_file (&fh2, &orig_path, new_path,
                                            NULL, FALSE, pool));
          apr_file_close (fh2);
        }
      if (node->action == 'D')
        {
          new_path = svn_path_join (SVNLOOK_TMPDIR, path, pool);
          SVN_ERR (open_writable_binary_file (&fh1, new_path, pool));
          apr_file_close (fh1);

          SVN_ERR (svn_io_open_unique_file (&fh2, &orig_path, new_path,
                                            NULL, FALSE, pool));
          SVN_ERR (dump_contents
                   (fh2, base_root, base_path, pool));
          apr_file_close (fh2);
        }
    }

  if (orig_path && new_path)
    {
      if (! is_copy)
        printf ("%s: %s\n", 
                ((node->action == 'A') ? "Added" : 
                 ((node->action == 'D') ? "Deleted" :
                  ((node->action == 'R') ? "Modified" : "Index"))),
                path_native);

      if ((! no_diff_deleted) || (node->action != 'D'))
        {
          svn_diff_t *diff;
          svn_string_t *mimetype;
          svn_boolean_t binary = FALSE;

          printf ("===========================================================\
===================\n");
          fflush (stdout);

          /* Read the mime-type property of our two diffy items, and
             if either indicates binariness, don't do the diff. */
          SVN_ERR (svn_fs_node_prop (&mimetype, root, path,
                                     SVN_PROP_MIME_TYPE, pool));
          if (mimetype && svn_mime_type_is_binary (mimetype->data))
            binary = TRUE;
          if (! binary)
            {
              SVN_ERR (svn_fs_node_prop (&mimetype, base_root, base_path,
                                         SVN_PROP_MIME_TYPE, pool));
              if (mimetype && svn_mime_type_is_binary (mimetype->data))
                binary = TRUE;
            }

          if (binary)
            {
              printf ("(Binary files differ)\n");
            }
          else
            {
              SVN_ERR (svn_diff_file_diff (&diff, orig_path, new_path, pool));
              if (svn_diff_contains_diffs (diff))
                {
                  apr_file_t *outhandle;
                  apr_status_t apr_err;
                  const char *orig_label, *new_label;

                  /* Get an apr_file_t representing stdout, which is where
                     we'll have the diff program print to. */
                  apr_err = apr_file_open_stdout (&outhandle, pool);
                  if (apr_err)
                    return svn_error_create 
                      (apr_err, NULL,
                       "print_diff_tree: can't open handle to stdout");
                  
                  orig_label = generate_label (base_root, base_path, pool);
                  new_label = generate_label (root, path, pool);
                  SVN_ERR (svn_diff_file_output_unified (outhandle, diff, 
                                                         orig_path, new_path,
                                                         orig_label, new_label,
                                                         pool));
                }
            }
        }

      printf ("\n");
      fflush (stdout);
    }
  else if (is_copy)
    {
      printf ("\n");
    }
    
  /* Now, delete any temporary files. */
  if (orig_path)
    svn_io_remove_file (orig_path, pool);
{'fraction_non_alphanumeric': 0.123547306909457, 'fraction_numerical': 0.004158368168785976, 'mean_word_length': 2.171073634533972, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 147, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 39, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2456119', 'n_tokens_mistral': 15630, 'n_tokens_neox': 15143, 'n_words': 4084}