<commit_before>/*
 * util.c: Subversion command line client utility functions. Any
 * functions that need to be shared across subcommands should be put
 * in here.
 *
 * ====================================================================
 * Copyright (c) 2000-2002 CollabNet.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://subversion.tigris.org/license-1.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 *
 * This software consists of voluntary contributions made by many
 * individuals.  For exact contribution history, see the revision
 * history and logs, available at http://subversion.tigris.org/.
 * ====================================================================
 */

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




/*** Includes. ***/

#include <string.h>
#include <ctype.h>
#include <assert.h>

#include <apr_strings.h>
#include <apr_tables.h>
#include <apr_general.h>
#include <apr_lib.h>

#include "svn_wc.h"
#include "svn_client.h"
#include "svn_string.h"
#include "svn_path.h"
#include "svn_delta.h"
#include "svn_error.h"
#include "svn_io.h"
#include "svn_pools.h"
#include "svn_utf.h"
#include "cl.h"



#define DEFAULT_ARRAY_SIZE 5


/* Copy STR into POOL and push the copy onto ARRAY.
   ### todo: Hmm. This should probably find its way into libsvn_subr -Fitz */
static void
array_push_str (apr_array_header_t *array,
                const char *str,
                apr_pool_t *pool)
{
  /* ### Not sure if this function is still necessary.  It used to
     convert str to svn_stringbuf_t * and push it, but now it just
     dups str in pool and pushes the copy.  So its only effect is
     transfer str's lifetime to pool.  Is that something callers are
     depending on? */

  (*((const char **) apr_array_push (array))) = apr_pstrdup (pool, str);
}


void
svn_cl__push_implicit_dot_target (apr_array_header_t *targets,
                                  apr_pool_t *pool)
{
  if (targets->nelts == 0)
    array_push_str (targets, ".", pool);
  assert (targets->nelts);
}


svn_error_t *
svn_cl__parse_num_args (apr_array_header_t **args_p,
                        apr_getopt_t *os,
                        int num_args,
                        apr_pool_t *pool)
{
  int i;
  apr_array_header_t *args
    = apr_array_make (pool, DEFAULT_ARRAY_SIZE, sizeof (const char *));

  /* loop for num_args and add each arg to the args array */
  for (i = 0; i < num_args; i++)
    {
      if (os->ind >= os->argc)
        {
          return svn_error_create (SVN_ERR_CL_ARG_PARSING_ERROR,
                                   0, 0, pool, "");
        }
      array_push_str (args, os->argv[os->ind++], pool);
    }

  *args_p = args;
  return SVN_NO_ERROR;
}

svn_error_t *
svn_cl__parse_all_args (apr_array_header_t **args_p,
                        apr_getopt_t *os,
                        apr_pool_t *pool)
{
  apr_array_header_t *args
    = apr_array_make (pool, DEFAULT_ARRAY_SIZE, sizeof (const char *));

  if (os->ind > os->argc)
    {
      return svn_error_create (SVN_ERR_CL_ARG_PARSING_ERROR, 0, 0, pool, "");
    }
  while (os->ind < os->argc)
    {
      array_push_str (args, os->argv[os->ind++], pool);
    }

  *args_p = args;
  return SVN_NO_ERROR;
}


/* Parse a working-copy or url PATH, looking for an "@" sign, e.g.

         foo/bar/baz@13
         http://blah/bloo@27
         blarg/snarf@HEAD

   If an "@" is found, return the two halves in *TRUEPATH and *REV,
   allocating in POOL.

   If no "@" is found, set *TRUEPATH to PATH and *REV to kind 'unspecified'.
*/
static svn_error_t *
parse_path (svn_client_revision_t *rev,
            const char **truepath,
            const char *path /* UTF-8! */,
            apr_pool_t *pool)
{
  int i;
  apr_pool_t *subpool = svn_pool_create (pool);
  svn_cl__opt_state_t *os = apr_pcalloc (subpool, sizeof(*os));

  /* scanning from right to left, just to be friendly to any
     screwed-up filenames that might *actually* contain @-signs.  :-) */
  for (i = (strlen (path) - 1); i >= 0; i--)
    {
      if (path[i] == '@')
        {
          const char *native_rev;

          SVN_ERR (svn_utf_cstring_from_utf8 (&native_rev, path + i + 1,
                                              subpool));

          if (svn_cl__parse_revision (os, native_rev, subpool))
            return svn_error_createf (SVN_ERR_CL_ARG_PARSING_ERROR,
                                      0, NULL, subpool,
                                      "Syntax error parsing revision \"%s\"",
                                      path + i + 1);

          *truepath = apr_pstrndup (pool, path, i);
          rev->kind = os->start_revision.kind;
          rev->value = os->start_revision.value;

          svn_pool_destroy (subpool);
          return SVN_NO_ERROR;
        }
    }

  /* Didn't find an @-sign. */
  *truepath = path;
  rev->kind = svn_client_revision_unspecified;

  svn_pool_destroy (subpool);
  return SVN_NO_ERROR;
}


svn_error_t *
svn_cl__args_to_target_array (apr_array_header_t **targets_p,
                              apr_getopt_t *os,
			      svn_cl__opt_state_t *opt_state,
                              svn_boolean_t extract_revisions,
                              apr_pool_t *pool)
{
  int i;
  svn_client_revision_t *firstrev = NULL, *secondrev = NULL;
   apr_array_header_t *input_targets =
    apr_array_make (pool, DEFAULT_ARRAY_SIZE, sizeof (const char *));
  apr_array_header_t *output_targets =
    apr_array_make (pool, DEFAULT_ARRAY_SIZE, sizeof (const char *));

  /* Step 1:  create a master array of targets that are in -native-
     encoding, and come from concatenating the targets left by apr_getopt,
     plus any extra targets in opt_state (from the --targets switch.) */

  for (; os->ind < os->argc; os->ind++)
    {
      /* The apr_getopt targets are still in native encoding. */
      const char *raw_target = os->argv[os->ind];
      (*((const char **) apr_array_push (input_targets))) = raw_target;
    }

  if (opt_state->targets)
    {
      for (i = 0; i < opt_state->targets->nelts; i++)
        {
          /* The --targets array have already been converted to UTF-8,
             because we needed to split up the list with svn_cstring_split. */
          const char *raw_target;
          const char *utf8_target = APR_ARRAY_IDX(opt_state->targets,
                                                  i, const char *);
          /* Convert each back to native encoding.  */
          SVN_ERR (svn_utf_cstring_from_utf8 (&raw_target, utf8_target, pool));

          (*((const char **) apr_array_push (input_targets))) = raw_target;
        }
    }

  /* Step 2:  process each target.  */

  for (i = 0; i < input_targets->nelts; i++)
    {
      const char *raw_target = APR_ARRAY_IDX(input_targets, i, const char *);
      const char *target;      /* after all processing is finished */

      /* URLs and wc-paths get treated differently. */
      if (svn_path_is_url (raw_target))
        {
          /* No need to canonicalize a URL's case or path separators. */

          /* convert it to UTF-8 */
          const char *utf8_url;
          SVN_ERR (svn_utf_cstring_to_utf8 (&utf8_url, raw_target,
                                            NULL, pool));

          /* strip any trailing '/' */
          target = svn_path_canonicalize_nts (utf8_url, pool);
        }
      else  /* not a url, so treat as a path */
        {
          const char *base_name = svn_path_basename (raw_target, pool);
          char *truenamed_target;
          apr_status_t apr_err;

          /* If this target is a Subversion administrative directory,
             skip it.  TODO: Perhaps this check should not call the
             target a SVN admin dir unless svn_wc_check_wc passes on
             the target, too? */
          if (! strcmp (base_name, SVN_WC_ADM_DIR_NAME))
            continue;

          /* canonicalize case, and change all separators to '/'. */
          apr_err = apr_filepath_merge (&truenamed_target, "", raw_target,
                                        APR_FILEPATH_TRUENAME, pool);

          /* It's okay for the file to not exist, that just means we have
             to accept the case given to the client. */
          if (APR_STATUS_IS_ENOENT (apr_err))
            /* ### Disagreeable to cast, but it's only a tmp var and we're
               casting it back a bit later on. */
            truenamed_target = (char *) raw_target;
          else if (apr_err)
            return svn_error_createf (apr_err, 0, NULL, pool,
                                      "Error resolving case of %s.",
                                      raw_target);

          /* convert to UTF-8. */
          SVN_ERR (svn_utf_cstring_to_utf8 (&target,
                                            (const char *) truenamed_target,
                                            NULL, pool));
        }

      (*((const char **) apr_array_push (output_targets))) = target;
    }


  /* kff todo: need to remove redundancies from targets before
     passing it to the cmd_func. */

  if (extract_revisions)
    {
      for (i = 0; i < output_targets->nelts; i++)
        {
          const char *truepath;
          svn_client_revision_t temprev;
          const char *path = ((const char **) (output_targets->elts))[i];

          parse_path (&temprev, &truepath, path, pool);

          if (temprev.kind != svn_client_revision_unspecified)
            {
              ((const char **) (output_targets->elts))[i] = truepath;

              if (! firstrev)
                {
                  firstrev = apr_pcalloc (pool, sizeof (*firstrev));
                  firstrev->kind = temprev.kind;
                  firstrev->value = temprev.value;
                }
              else if (! secondrev)
                {
                  secondrev = apr_pcalloc (pool, sizeof (*secondrev));
                  secondrev->kind = temprev.kind;
                  secondrev->value = temprev.value;
                }
              else
                break;
            }
        }

      if (firstrev)
        {
          opt_state->start_revision.kind = firstrev->kind;
          opt_state->start_revision.value = firstrev->value;
        }

      if (secondrev)
        {
          opt_state->end_revision.kind = secondrev->kind;
          opt_state->end_revision.value = secondrev->value;
        }
    }

  *targets_p = output_targets;
  return SVN_NO_ERROR;
}


void
svn_cl__print_commit_info (svn_client_commit_info_t *commit_info)
{
  if ((commit_info)
      && (SVN_IS_VALID_REVNUM (commit_info->revision)))
    printf ("\nCommitted revision %" SVN_REVNUM_T_FMT ".\n",
            commit_info->revision);

  return;
}


svn_error_t *
svn_cl__edit_externally (const char **edited_contents /* UTF-8! */,
                         const char *base_dir /* UTF-8! */,
                         const char *contents /* UTF-8! */,
                         apr_pool_t *pool)
{
  const char *editor = NULL;
  const char *cmd;
  apr_file_t *tmp_file;
  const char *tmpfile_name;
  const char *contents_native, *tmpfile_native;
  apr_status_t apr_err, apr_err2;
  apr_size_t written;
  apr_finfo_t finfo_before, finfo_after;
  svn_error_t *err = SVN_NO_ERROR;
  int sys_err;

  /* Try to find an editor in the environment. */
  editor = getenv ("SVN_EDITOR");
  if (! editor)
    editor = getenv ("VISUAL");
  if (! editor)
    editor = getenv ("EDITOR");

  /* Abort if there is no editor specified */
  if (! editor)
    return svn_error_create
      (SVN_ERR_CL_NO_EXTERNAL_EDITOR, 0, NULL, pool,
       "None of the environment variables "
       "SVN_EDITOR, VISUAL or EDITOR is set.");

  /* Convert file contents from UTF-8 */
  SVN_ERR (svn_utf_cstring_from_utf8 (&contents_native, contents, pool));

  /* Ask the working copy for a temporary file based on BASE_DIR */
  SVN_ERR (svn_io_open_unique_file
           (&tmp_file, &tmpfile_name,
            svn_path_join (base_dir, "msg", pool), ".tmp", FALSE, pool));

  /*** From here one, any problems that occur require us to cleanup
       the file we just created!! ***/

  /* Dump initial CONTENTS to TMP_FILE. */
  apr_err = apr_file_write_full (tmp_file, contents_native,
                                 strlen (contents_native), &written);

  apr_err2 = apr_file_close (tmp_file);
  if (! apr_err)
    apr_err = apr_err2;

  /* Make sure the whole CONTENTS were written, else return an error. */
  if (apr_err || (written != strlen (contents_native)))
    {
      err = svn_error_createf
        (apr_err ? apr_err : SVN_ERR_INCOMPLETE_DATA, 0, NULL, pool,
         "failed writing '%s'", tmpfile_name);
      goto cleanup;
    }

  err = svn_utf_cstring_from_utf8 (&tmpfile_native, tmpfile_name, pool);
  if (err)
    goto cleanup;

  /* Get information about the temporary file before the user has
     been allowed to edit its contents. */
  apr_err = apr_stat (&finfo_before, tmpfile_native,
                      APR_FINFO_MTIME | APR_FINFO_SIZE, pool);
  if (apr_err)
    {
      err =  svn_error_createf (apr_err, 0, NULL, pool,
                                "failed to stat '%s'", tmpfile_name);
      goto cleanup;
    }

  /* Now, run the editor command line.  */
  cmd = apr_psprintf (pool, "%s %s", editor, tmpfile_native);
  sys_err = system (cmd);
  if (sys_err != 0)
    {
      /* Extracting any meaning from sys_err is platform specific, so just
         use the raw value. */
      err =  svn_error_createf (SVN_ERR_EXTERNAL_PROGRAM, 0, NULL, pool,
                                "system('%s') returned %d", cmd, sys_err);
      goto cleanup;
    }

  /* Get information about the temporary file after the assumed editing. */
  apr_err = apr_stat (&finfo_after, tmpfile_native,
                      APR_FINFO_MTIME | APR_FINFO_SIZE, pool);
  if (apr_err)<commit_msg>Patch from <NAME>, who didn't have time to commit it himself:<commit_after>/*
 * util.c: Subversion command line client utility functions. Any
 * functions that need to be shared across subcommands should be put
 * in here.
 *
 * ====================================================================
 * Copyright (c) 2000-2002 CollabNet.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://subversion.tigris.org/license-1.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 *
 * This software consists of voluntary contributions made by many
 * individuals.  For exact contribution history, see the revision
 * history and logs, available at http://subversion.tigris.org/.
 * ====================================================================
 */

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




/*** Includes. ***/

#include <string.h>
#include <ctype.h>
#include <assert.h>

#include <apr_strings.h>
#include <apr_tables.h>
#include <apr_general.h>
#include <apr_lib.h>

#include "svn_wc.h"
#include "svn_client.h"
#include "svn_string.h"
#include "svn_path.h"
#include "svn_delta.h"
#include "svn_error.h"
#include "svn_io.h"
#include "svn_pools.h"
#include "svn_utf.h"
#include "cl.h"



#define DEFAULT_ARRAY_SIZE 5


/* Copy STR into POOL and push the copy onto ARRAY.
   ### todo: Hmm. This should probably find its way into libsvn_subr -Fitz */
static void
array_push_str (apr_array_header_t *array,
                const char *str,
                apr_pool_t *pool)
{
  /* ### Not sure if this function is still necessary.  It used to
     convert str to svn_stringbuf_t * and push it, but now it just
     dups str in pool and pushes the copy.  So its only effect is
     transfer str's lifetime to pool.  Is that something callers are
     depending on? */

  (*((const char **) apr_array_push (array))) = apr_pstrdup (pool, str);
}


void
svn_cl__push_implicit_dot_target (apr_array_header_t *targets,
                                  apr_pool_t *pool)
{
  if (targets->nelts == 0)
    array_push_str (targets, ".", pool);
  assert (targets->nelts);
}


svn_error_t *
svn_cl__parse_num_args (apr_array_header_t **args_p,
                        apr_getopt_t *os,
                        int num_args,
                        apr_pool_t *pool)
{
  int i;
  apr_array_header_t *args
    = apr_array_make (pool, DEFAULT_ARRAY_SIZE, sizeof (const char *));

  /* loop for num_args and add each arg to the args array */
  for (i = 0; i < num_args; i++)
    {
      if (os->ind >= os->argc)
        {
          return svn_error_create (SVN_ERR_CL_ARG_PARSING_ERROR,
                                   0, 0, pool, "");
        }
      array_push_str (args, os->argv[os->ind++], pool);
    }

  *args_p = args;
  return SVN_NO_ERROR;
}

svn_error_t *
svn_cl__parse_all_args (apr_array_header_t **args_p,
                        apr_getopt_t *os,
                        apr_pool_t *pool)
{
  apr_array_header_t *args
    = apr_array_make (pool, DEFAULT_ARRAY_SIZE, sizeof (const char *));

  if (os->ind > os->argc)
    {
      return svn_error_create (SVN_ERR_CL_ARG_PARSING_ERROR, 0, 0, pool, "");
    }
  while (os->ind < os->argc)
    {
      array_push_str (args, os->argv[os->ind++], pool);
    }

  *args_p = args;
  return SVN_NO_ERROR;
}


/* Parse a working-copy or url PATH, looking for an "@" sign, e.g.

         foo/bar/baz@13
         http://blah/bloo@27
         blarg/snarf@HEAD

   If an "@" is found, return the two halves in *TRUEPATH and *REV,
   allocating in POOL.

   If no "@" is found, set *TRUEPATH to PATH and *REV to kind 'unspecified'.
*/
static svn_error_t *
parse_path (svn_client_revision_t *rev,
            const char **truepath,
            const char *path /* UTF-8! */,
            apr_pool_t *pool)
{
  int i;
  apr_pool_t *subpool = svn_pool_create (pool);
  svn_cl__opt_state_t *os = apr_pcalloc (subpool, sizeof(*os));

  /* scanning from right to left, just to be friendly to any
     screwed-up filenames that might *actually* contain @-signs.  :-) */
  for (i = (strlen (path) - 1); i >= 0; i--)
    {
      if (path[i] == '@')
        {
          const char *native_rev;

          SVN_ERR (svn_utf_cstring_from_utf8 (&native_rev, path + i + 1,
                                              subpool));

          if (svn_cl__parse_revision (os, native_rev, subpool))
            return svn_error_createf (SVN_ERR_CL_ARG_PARSING_ERROR,
                                      0, NULL, subpool,
                                      "Syntax error parsing revision \"%s\"",
                                      path + i + 1);

          *truepath = apr_pstrndup (pool, path, i);
          rev->kind = os->start_revision.kind;
          rev->value = os->start_revision.value;

          svn_pool_destroy (subpool);
          return SVN_NO_ERROR;
        }
    }

  /* Didn't find an @-sign. */
  *truepath = path;
  rev->kind = svn_client_revision_unspecified;

  svn_pool_destroy (subpool);
  return SVN_NO_ERROR;
}


svn_error_t *
svn_cl__args_to_target_array (apr_array_header_t **targets_p,
                              apr_getopt_t *os,
			      svn_cl__opt_state_t *opt_state,
                              svn_boolean_t extract_revisions,
                              apr_pool_t *pool)
{
  int i;
  svn_client_revision_t *firstrev = NULL, *secondrev = NULL;
   apr_array_header_t *input_targets =
    apr_array_make (pool, DEFAULT_ARRAY_SIZE, sizeof (const char *));
  apr_array_header_t *output_targets =
    apr_array_make (pool, DEFAULT_ARRAY_SIZE, sizeof (const char *));

  /* Step 1:  create a master array of targets that are in -native-
     encoding, and come from concatenating the targets left by apr_getopt,
     plus any extra targets in opt_state (from the --targets switch.) */

  for (; os->ind < os->argc; os->ind++)
    {
      /* The apr_getopt targets are still in native encoding. */
      const char *raw_target = os->argv[os->ind];
      (*((const char **) apr_array_push (input_targets))) = raw_target;
    }

  if (opt_state->targets)
    {
      for (i = 0; i < opt_state->targets->nelts; i++)
        {
          /* The --targets array have already been converted to UTF-8,
             because we needed to split up the list with svn_cstring_split. */
          const char *raw_target;
          const char *utf8_target = APR_ARRAY_IDX(opt_state->targets,
                                                  i, const char *);
          /* Convert each back to native encoding.  */
          SVN_ERR (svn_utf_cstring_from_utf8 (&raw_target, utf8_target, pool));

          (*((const char **) apr_array_push (input_targets))) = raw_target;
        }
    }

  /* Step 2:  process each target.  */

  for (i = 0; i < input_targets->nelts; i++)
    {
      const char *raw_target = APR_ARRAY_IDX(input_targets, i, const char *);
      const char *target;      /* after all processing is finished */

      /* URLs and wc-paths get treated differently. */
      if (svn_path_is_url (raw_target))
        {
          /* No need to canonicalize a URL's case or path separators. */

          /* convert it to UTF-8 */
          const char *utf8_url;
          SVN_ERR (svn_utf_cstring_to_utf8 (&utf8_url, raw_target,
                                            NULL, pool));

          /* strip any trailing '/' */
          target = svn_path_canonicalize_nts (utf8_url, pool);
        }
      else  /* not a url, so treat as a path */
        {
          const char *base_name = svn_path_basename (raw_target, pool);
          char *truenamed_target;
          apr_status_t apr_err;

          /* If this target is a Subversion administrative directory,
             skip it.  TODO: Perhaps this check should not call the
             target a SVN admin dir unless svn_wc_check_wc passes on
             the target, too? */
          if (! strcmp (base_name, SVN_WC_ADM_DIR_NAME))
            continue;

          /* canonicalize case, and change all separators to '/'. */
          apr_err = apr_filepath_merge (&truenamed_target, "", raw_target,
                                        APR_FILEPATH_TRUENAME, pool);

          /* It's okay for the file to not exist, that just means we have
             to accept the case given to the client. */
          if (APR_STATUS_IS_ENOENT (apr_err))
            /* ### Disagreeable to cast, but it's only a tmp var and we're
               casting it back a bit later on. */
            truenamed_target = (char *) raw_target;
          else if (apr_err)
            return svn_error_createf (apr_err, 0, NULL, pool,
                                      "Error resolving case of %s.",
                                      raw_target);

          /* convert to UTF-8. */
          SVN_ERR (svn_utf_cstring_to_utf8 (&target,
                                            (const char *) truenamed_target,
                                            NULL, pool));
        }

      (*((const char **) apr_array_push (output_targets))) = target;
    }


  /* kff todo: need to remove redundancies from targets before
     passing it to the cmd_func. */

  if (extract_revisions)
    {
      for (i = 0; i < output_targets->nelts; i++)
        {
          const char *truepath;
          svn_client_revision_t temprev;
          const char *path = ((const char **) (output_targets->elts))[i];

          parse_path (&temprev, &truepath, path, pool);

          if (temprev.kind != svn_client_revision_unspecified)
            {
              ((const char **) (output_targets->elts))[i] = truepath;

              if (! firstrev)
                {
                  firstrev = apr_pcalloc (pool, sizeof (*firstrev));
                  firstrev->kind = temprev.kind;
                  firstrev->value = temprev.value;
                }
              else if (! secondrev)
                {
                  secondrev = apr_pcalloc (pool, sizeof (*secondrev));
                  secondrev->kind = temprev.kind;
                  secondrev->value = temprev.value;
                }
              else
                break;
            }
        }

      if (firstrev)
        {
          opt_state->start_revision.kind = firstrev->kind;
          opt_state->start_revision.value = firstrev->value;
        }

      if (secondrev)
        {
          opt_state->end_revision.kind = secondrev->kind;
          opt_state->end_revision.value = secondrev->value;
        }
    }

  *targets_p = output_targets;
  return SVN_NO_ERROR;
}


void
svn_cl__print_commit_info (svn_client_commit_info_t *commit_info)
{
  if ((commit_info)
      && (SVN_IS_VALID_REVNUM (commit_info->revision)))
    printf ("\nCommitted revision %" SVN_REVNUM_T_FMT ".\n",
            commit_info->revision);

  return;
}


svn_error_t *
svn_cl__edit_externally (const char **edited_contents /* UTF-8! */,
                         const char *base_dir /* UTF-8! */,
                         const char *contents /* UTF-8! */,
                         apr_pool_t *pool)
{
  const char *editor = NULL;
  const char *cmd;
  apr_file_t *tmp_file;
  const char *tmpfile_name;
  const char *contents_native, *tmpfile_native;
  apr_status_t apr_err, apr_err2;
  apr_size_t written;
  apr_finfo_t finfo_before, finfo_after;
  svn_error_t *err = SVN_NO_ERROR;
  int sys_err;

  /* Try to find an editor in the environment. */
  editor = getenv ("SVN_EDITOR");
  if (! editor)
    editor = getenv ("VISUAL");
  if (! editor)
    editor = getenv ("EDITOR");

  /* Abort if there is no editor specified */
  if (! editor)
    return svn_error_create
      (SVN_ERR_CL_NO_EXTERNAL_EDITOR, 0, NULL, pool,
       "None of the environment variables "
       "SVN_EDITOR, VISUAL or EDITOR is set.");

  /* Convert file contents from UTF-8 */
  SVN_ERR (svn_utf_cstring_from_utf8 (&contents_native, contents, pool));

  /* Ask the working copy for a temporary file based on BASE_DIR */
  SVN_ERR (svn_io_open_unique_file
           (&tmp_file, &tmpfile_name,
            svn_path_join (base_dir, "msg", pool), ".tmp", FALSE, pool));

  /*** From here one, any problems that occur require us to cleanup
       the file we just created!! ***/

  /* Dump initial CONTENTS to TMP_FILE. */
  apr_err = apr_file_write_full (tmp_file, contents_native,
                                 strlen (contents_native), &written);

  apr_err2 = apr_file_close (tmp_file);
  if (! apr_err)
    apr_err = apr_err2;

  /* Make sure the whole CONTENTS were written, else return an error. */
  if (apr_err || (written != strlen (contents_native)))
    {
      err = svn_error_createf
        (apr_err ? apr_err : SVN_ERR_INCOMPLETE_DATA, 0, NULL, pool,
         "failed writing '%s'", tmpfile_name);
      goto cleanup;
    }

  err = svn_utf_cstring_from_utf8 (&tmpfile_native, tmpfile_name, pool);
  if (err)
    goto cleanup;

  /* Get information about the temporary file before the user has
     been allowed to edit its contents. */
  apr_err = apr_stat (&finfo_before, tmpfile_native,
                      APR_FINFO_MTIME | APR_FINFO_SIZE, pool);
  if (apr_err)
    {
      err =  svn_error_createf (apr_err, 0, NULL, pool,
                                "failed to stat '%s'", tmpfile_name);
      goto cleanup;
    }

  /* Now, run the editor command line.  */
  cmd = apr_psprintf (pool, "%s \"%s\"", editor, tmpfile_native);
  sys_err = system (cmd);
  if (sys_err != 0)
    {
      /* Extracting any meaning from sys_err is platform specific, so just
         use the raw value. */
      err =  svn_error_createf (SVN_ERR_EXTERNAL_PROGRAM, 0, NULL, pool,
                                "system('%s') returned %d", cmd, sys_err);
      goto cleanup;
    }

  /* Get information about the temporary file after the assumed editing. */
  apr_err = apr_stat (&finfo_after, tmpfile_native,
                      APR_FINFO_MTIME | APR_FINFO_SIZE, pool);
  if (apr_err)
{'fraction_non_alphanumeric': 0.11605615735461802, 'fraction_numerical': 0.004133409350057012, 'mean_word_length': 2.1249304086404632, 'pattern_counts': {'":': 0, '<': 30, '<?xml version=': 0, '>': 112, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 42, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3290783', 'n_tokens_mistral': 9118, 'n_tokens_neox': 8829, 'n_words': 2459}