Great, thanks Philip!
Apologies that this can't get reviewed & applied immediately (need to
finish M6 stuff first), but it is near the top of the list and will
get reviewed very soon.  Watch issue #422.
-K
Philip Martin <pmartin@uklinux.net> writes:
> My editor/crawler patch to provide 'svn diff -r' is now ready. I have
> removed my earlier rdiff command and integrated the behaviour into the
> existing diff command. Plain 'svn diff' remains a local only diff, I
> haven't changed that code or behavior. Only when the -r option is
> specified is the new repository diff code used.
> 
> There are a few outstanding issues, marked TODO in the code, however I
> think the code is in a state to be merged now.  I've written some
> tests, however I have never even looked at python before, so that code
> may be a bit clunky :-)
> 
> This patch, against revision 394, was produced using the new code.
> 
> Philip
> 
> 
> 
> * subversion/include/svn_wc.h: Add svn_wc_diff_cmd_t the new diff
>   callback, and svn_wc_get_diff_editor
> 
> * subversion/libsvn_wc/diff.c: Create. Provides
>   svn_wc_get_diff_editor.
> 
>   This code uses an svn_delta_edit_fns_t editor driven by
>   svn_wc_crawl_revisions (like the update command) to retrieve the
>   differences between the working copy and the requested repository
>   version. Rather than updating the working copy, this new editor creates
>   temporary files that contain the pristine repository versions. When the
>   crawler closes the files the editor calls back to a client layer
>   function to compare the working copy and the temporary file. There is
>   only ever one temporary file in existence at any time.
> 
>   When the crawler closes a directory, the editor then calls back to the
>   client layer to compare any remaining files that may have been modified
>   locally. Added directories do not have corresponding temporary
>   directories created, as they are not needed.
> 
> * subversion/libsvn_wc/wc.h: Add svn_wc__empty_file_path,
>   svn_wc__open_empty_file and svn_wc__close_empty_file.
> 
> * subversion/libsvn_wc/adm_files.c: Add svn_wc__empty_file_path,
>   svn_wc__open_empty_file, and svn_wc__close_empty_file to support a
>   permanent empty file in the admin area. init_adm: create the empty
>   file.
> 
> * subversion/include/svn_client.h: Add svn_client_diff
> 
> * subversion/libsvn_client/diff.c: Added svn_client_diff, the client
>   interface to the new diff code. Added svn_client__diff_cmd, the
>   client layer callback to compare two files.
> 
> * subversion/clients/cmdline/diff-cmd.c: Added svn_cl__wc_repository_diff 
>   a substitute 'svn diff' command using the new diff code. svn_cl__diff
>   calls svn_cl__wc_repository_diff when the -r option is given.
> 
> * subversion/clients/cmdline/main.c: modify the usage string to
>   reflect the new diff functionality
> 
> * subversion/tests/clients/cmdline/diff_tests.py: Create. Provides
>   tests of the new diff code.
> 
> 
> 
> Index: ./subversion/include/svn_wc.h
> ===================================================================
> --- ./subversion/include/svn_wc.h
> +++ ./subversion/include/svn_wc.h	Mon Nov  5 01:41:13 2001
> @@ -691,6 +691,41 @@
>  
>  /*** Diffs ***/
>  
> +
> +/* The function type called by the diff editor when it has determined the
> + * two files that are to be diffed.
> + *
> + * PATH1 and PATH2 are the two files to be compared, these files
> + * exist. Since the PATH1 file may be temporary, it is possible that it is
> + * not in the "correct" location in the working copy, if this is the case
> + * then LABEL will be non-null and will contain the "correct" location.
> + *
> + * BATON is passed through by the diff editor.
> + */
> +typedef svn_error_t *(*svn_wc_diff_cmd_t)(svn_stringbuf_t *path1,
> +                                          svn_stringbuf_t *path2,
> +                                          svn_stringbuf_t *label,
> +                                          void *baton);
> +
> +
> +/* Return an EDITOR/EDIT_BATON for diffing a working copy against the
> + * repository.
> + *
> + * ANCHOR and TARGET have the same meaning as svn_wc_get_update_editor.
> + *
> + * DIFF_CMD/DIFF_CMD_BATON are the function/baton to be called when two
> + * files are to be compared.
> + */
> +svn_error_t *svn_wc_get_diff_editor (svn_stringbuf_t *anchor,
> +                                     svn_stringbuf_t *target,
> +                                     svn_wc_diff_cmd_t diff_cmd,
> +                                     void *diff_cmd_baton,
> +                                     svn_boolean_t recurse,
> +                                     const svn_delta_edit_fns_t **editor,
> +                                     void **edit_baton,
> +                                     apr_pool_t *pool);
> +
> +
>  /* Given a PATH to a wc file, return a PRISTINE_PATH which points to a
>     pristine version of the file.  This is needed so clients can do
>     diffs.  If the WC has no text-base, return a NULL instead of a
> Index: ./subversion/include/svn_client.h
> ===================================================================
> --- ./subversion/include/svn_client.h
> +++ ./subversion/include/svn_client.h	Sat Nov  3 17:29:27 2001
> @@ -397,6 +397,25 @@
>                        apr_pool_t *pool);
>  
>  
> +/* Given a PATH in the working copy, compare the working copy against a
> + * revision in the repository. The repository revision can be specified as
> + * REVISION, or as a time TM.
> + *
> + * If PATH is a directory and RECURSE is true, this will be a recursive
> + * operation.
> + *
> + * DIFF_OPTIONS is used to pass additional command line options to the diff
> + * processes invoked to compare files.
> + */
> +svn_error_t *svn_client_diff (svn_stringbuf_t *path,
> +                              apr_array_header_t *diff_options,
> +                              svn_client_auth_baton_t *auth_baton,
> +                              svn_revnum_t revision,
> +                              apr_time_t tm,
> +                              svn_boolean_t recurse,
> +                              apr_pool_t *pool);
> +
> +
>  /* Recursively cleanup a working copy directory DIR, finishing any
>     incomplete operations, removing lockfiles, etc. */
>  svn_error_t *
> Index: ./subversion/libsvn_wc/diff.c
> ===================================================================
> --- ./subversion/libsvn_wc/diff.c
> +++ ./subversion/libsvn_wc/diff.c	Mon Nov  5 01:39:24 2001
> @@ -0,0 +1,747 @@
> +/*
> + * diff.c -- The diff editor for comparing the working copy against the
> + *           repository.
> + *
> + * ====================================================================
> + * Copyright (c) 2001 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/.
> + * ====================================================================
> + */
> +
> +/*
> + * This code uses an svn_delta_edit_fns_t editor driven by
> + * svn_wc_crawl_revisions (like the update command) to retrieve the
> + * differences between the working copy and the requested repository
> + * version. Rather than updating the working copy, this new editor creates
> + * temporary files that contain the pristine repository versions. When the
> + * crawler closes the files the editor calls back to a client layer
> + * function to compare the working copy and the temporary file. There is
> + * only ever one temporary file in existence at any time.
> + *
> + * When the crawler closes a directory, the editor then calls back to the
> + * client layer to compare any remaining files that may have been modified
> + * locally. Added directories do not have corresponding temporary
> + * directories created, as they are not needed.
> + *
> + * ### TODO: It might be better if the temporary files were not created in
> + * the admin's temp area, but in a more general area (/tmp, $TMPDIR) as
> + * then diff could be run on a read-only working copy.
> + *
> + * ### TODO: Provide an interface to directory_elements_diff that can
> + * be called by the client, to do local only diff without contacting the
> + * repository.
> + *
> + * ### TODO: Replacements where the node kind changes needs support. It
> + * mostly works when the change is in the repository, but not when it is
> + * in the working copy.
> + *
> + * ### TODO: Do we need to support copyfrom?
> + *
> + */
> +
> +#include <apr_hash.h>
> +#include "wc.h"
> +#include "svn_pools.h"
> +#include <assert.h>
> +
> +/* Overall diff editor baton.
> + *
> + * ANCHOR/TARGET represent the base of the hierarchy to be compared.
> + * DIFF_CMD/DIFF_CMD_BATON provide the callback that implements the file
> + * comparison function.
> + *
> + * RECURSE flags whether to diff recursively or not.
> + */
> +struct edit_baton {
> +  svn_stringbuf_t *anchor;
> +  svn_stringbuf_t *target;
> +  svn_wc_diff_cmd_t diff_cmd;
> +  void *diff_cmd_baton;
> +  svn_boolean_t recurse;
> +  apr_pool_t *pool;
> +};
> +
> +/* Directory level baton.
> + *
> + * ADDED gets set if the directory is added rather than replaced/unchanged.
> + *
> + * PATH is the "correct" path of the directory, but it may not exist in the
> + * working copy.
> + *
> + * ENTRIES is a list of directory elements that have already been compared
> + * and thus can be ignored when recursively comparing the directory.
> + *
> + * DIR_BATON is the baton for the parent directory, it will be null if this
> + * is the root of the hierarchy to be compared. EDIT_BATON is the overall
> + * editor baton.
> + */
> +struct dir_baton {
> +  svn_boolean_t added;
> +  svn_stringbuf_t *path;
> +  apr_hash_t *compared;
> +  struct dir_baton *dir_baton;
> +  struct edit_baton *edit_baton;
> +  apr_pool_t *pool;
> +};
> +
> +/* File level baton.
> + *
> + * ADDED gets set if the directory is added rather than replaced.
> + *
> + * PATH is the "correct" path of the file, but it may not exist in the
> + * working copy. WC_PATH is the ancestor path in the working copy that does
> + * exist.
> + *
> + * When generating the repository version of the file, ORIGINAL_FILE is
> + * base version of the file. TEMP_FILE is the pristine repository file
> + * obtained by applying the repository diffs to ORIGINAL_FILE.
> + *
> + * APPLY_HANDLER/APPLY_BATON represent the delta applcation baton.
> + *
> + * DIR_BATON is the baton for the parent directory, it will be null if this
> + * is the root of the hierarchy to be compared. EDIT_BATON is the overall
> + * editor baton.
> + */
> +struct file_baton {
> +  svn_boolean_t added;
> +  svn_stringbuf_t *path;
> +  svn_stringbuf_t *wc_path;
> +  apr_file_t *original_file;
> +  apr_file_t *temp_file;
> +  svn_txdelta_window_handler_t apply_handler;
> +  void *apply_baton;
> +  struct dir_baton *dir_baton;
> +  struct edit_baton *edit_baton;
> +  apr_pool_t *pool;
> +};
> +
> +/* Create a new directory baton. PARENT_BATON is the baton of the parent
> + * directory, it will be null if this is the root of the comparison
> + * hierarchy. The directory and its parent may or may not exist in the
> + * working copy.
> + */
> +static struct dir_baton *
> +make_dir_baton (svn_stringbuf_t *name,
> +                struct dir_baton *parent_baton,
> +                struct edit_baton *edit_baton,
> +                apr_pool_t *pool)
> +{
> +  struct dir_baton *dir_baton = apr_pcalloc (pool, sizeof (*dir_baton));
> +
> +  dir_baton->dir_baton = parent_baton;
> +  dir_baton->edit_baton = edit_baton;
> +  dir_baton->pool = svn_pool_create (pool);
> +
> +  dir_baton->compared = apr_hash_make (dir_baton->pool);
> +
> +  if (parent_baton)
> +    {
> +      /* The path is allocated in the parent's pool since it will be put
> +         into the parent's list of already diff'd entries. */
> +      dir_baton->path = svn_stringbuf_dup (parent_baton->path,
> +                                           parent_baton->pool);
> +    }
> +  else
> +    {
> +      dir_baton->path = svn_stringbuf_dup (edit_baton->anchor, dir_baton->pool);
> +    }
> +
> +  if (name)
> +    svn_path_add_component (dir_baton->path, name, svn_path_local_style);
> +
> +  return dir_baton;
> +}
> +
> +/* Create a new file baton. PARENT_BATON is the baton of the parent
> + * directory. The file and directory may or may not exist in the working
> + * copy.
> + */
> +static struct file_baton *
> +make_file_baton (svn_stringbuf_t *name,
> +                 struct dir_baton *parent_baton)
> +{
> +  struct file_baton *file_baton = apr_pcalloc (parent_baton->pool,
> +                                               sizeof (*file_baton));
> +
> +  file_baton->dir_baton = parent_baton;
> +  file_baton->edit_baton = parent_baton->edit_baton;
> +  file_baton->pool = svn_pool_create (parent_baton->pool);
> +
> +  /* The path is allocated in the directory's pool since it will be put
> +     into the directory's list of already diff'd entries. */
> +  file_baton->path = svn_stringbuf_dup (parent_baton->path, parent_baton->pool);
> +  svn_path_add_component (file_baton->path, name, svn_path_local_style);
> +
> +  /* If the parent directory is added rather than replaced it does not
> +     exist in the working copy. Determine a working copy path that does
> +     exist. */
> +  if (parent_baton->added)
> +    {
> +      struct dir_baton *wc_dir_baton = parent_baton;
> +
> +      /* Ascend until a directory is not being added, this will be a
> +         directory that does exist. This must terminate since the root of
> +         the comparison cannot be added. */
> +      while (wc_dir_baton->added)
> +        wc_dir_baton = wc_dir_baton->dir_baton;
> +
> +      file_baton->wc_path = svn_stringbuf_dup (wc_dir_baton->path,
> +                                               file_baton->pool);
> +      svn_path_add_component (file_baton->wc_path, name, svn_path_local_style);
> +    }
> +  else
> +    {
> +      file_baton->wc_path = file_baton->path;
> +    }
> +
> +  return file_baton;
> +}
> +
> +
> +/* Called by directory_elements_diff when a file is to be compared. At this
> + * stage we are dealing with a file that does exist in the working copy.
> + *
> + * ### TODO: Need to work on replace if the new filename used to be a
> + * directory.
> + */
> +static svn_error_t *
> +file_diff (struct dir_baton *dir_baton,
> +           svn_stringbuf_t *path,
> +           svn_wc_entry_t *entry,
> +           svn_boolean_t deleted)
> +{
> +  svn_wc_diff_cmd_t diff_cmd = dir_baton->edit_baton->diff_cmd;
> +  svn_stringbuf_t *pristine_copy, *empty_file;
> +  svn_boolean_t modified;
> +  enum svn_wc_schedule_t schedule = entry->schedule;
> +
> +  /* If the directory is being deleted, then this file will need to be
> +     deleted. */
> +  if (deleted)
> +    schedule = svn_wc_schedule_delete;
> +
> +  switch (schedule)
> +    {
> +      /* Replace is treated like a delete plus an add: two
> +         comparisons are generated, first one for the delete and
> +         then one for the add. */
> +    case svn_wc_schedule_replace:
> +    case svn_wc_schedule_delete:
> +      pristine_copy = svn_wc__text_base_path (path, FALSE, dir_baton->pool);
> +      empty_file = svn_wc__empty_file_path (path, dir_baton->pool);
> +
> +      SVN_ERR (diff_cmd (pristine_copy, empty_file, path,
> +                         dir_baton->edit_baton->diff_cmd_baton));
> +
> +      /* Replace will fallthrough! */
> +      if (schedule == svn_wc_schedule_delete)
> +        break;
> +
> +    case svn_wc_schedule_add:
> +      empty_file = svn_wc__empty_file_path (path, dir_baton->pool);
> +
> +      SVN_ERR (diff_cmd (empty_file, path, path,
> +                         dir_baton->edit_baton->diff_cmd_baton));
> +      break;
> +
> +    default:
> +      SVN_ERR (svn_wc_text_modified_p (&modified, path, dir_baton->pool));
> +      if (modified)
> +        {
> +          pristine_copy = svn_wc__text_base_path (path, FALSE, dir_baton->pool);
> +          SVN_ERR (diff_cmd (pristine_copy, path, path,
> +                             dir_baton->edit_baton->diff_cmd_baton));
> +        }
> +      break;
> +    }
> +
> +  return SVN_NO_ERROR;
> +}
> +
> +/* Called when the directory is closed to compare any elements that have
> + * not yet been compared. This identifies local, working copy only
> + * changes. At this stage we are dealing with files/directories that do
> + * exist in the working copy.
> + */
> +static svn_error_t *
> +directory_elements_diff (struct dir_baton *dir_baton,
> +                         svn_boolean_t deleted)
> +{
> +  apr_hash_t *entries;
> +  apr_hash_index_t *hi;
> +  svn_boolean_t in_anchor_not_target;
> +
> +  /* This directory should have been been unchanged or replaced, not
> +     added. */
> +  assert (!dir_baton->added);
> +
> +  /* Determine if this is the anchor directory if the anchor is different
> +     to the target */
> +  if (dir_baton->edit_baton->target
> +      && !svn_path_compare_paths (dir_baton->path,
> +                                  dir_baton->edit_baton->anchor,
> +                                  svn_path_local_style))
> +    in_anchor_not_target = TRUE;
> +  else
> +    in_anchor_not_target = FALSE;
> +
> +  SVN_ERR (svn_wc_entries_read (&entries, dir_baton->path, dir_baton->pool));
> +
> +  for (hi = apr_hash_first (dir_baton->pool, entries); hi;
> +       hi = apr_hash_next (hi))
> +    {
> +      svn_wc_entry_t *entry;
> +      const char *key;
> +      svn_stringbuf_t *path, *name;
> +      struct dir_baton *subdir_baton;
> +
> +      apr_hash_this(hi, (const void **)&key, NULL, (void **)&entry);
> +
> +      /* Skip entry for the directory itself. */
> +      if (strcmp (key, SVN_WC_ENTRY_THIS_DIR) == 0)
> +        continue;
> +
> +      /* Skip this entry if we are in the anchor and neither the anchor nor
> +         the entry is the target */
> +      name = svn_stringbuf_create (key, dir_baton->pool);
> +      if (in_anchor_not_target
> +          && !svn_stringbuf_compare (dir_baton->edit_baton->target, name))
> +        continue;
> +
> +      path = svn_stringbuf_dup (dir_baton->path, dir_baton->pool);
> +      svn_path_add_component (path, name, svn_path_local_style);
> +
> +      /* Skip entry if it is in the list of entries already diff'd. */
> +      if (apr_hash_get (dir_baton->compared, path->data, path->len))
> +        continue;
> +
> +      switch (entry->kind)
> +        {
> +        case svn_node_file:
> +          SVN_ERR (file_diff (dir_baton, path, entry, deleted));
> +          break;
> +
> +        case svn_node_dir:
> +          if (entry->schedule == svn_wc_schedule_replace)
> +            {
> +              /* ### TODO: Don't know how to do this bit. How do I get
> +                 information about what is being replaced? If it was a
> +                 directory then the directory elements are also going to be
> +                 deleted. We need to show deletion diffs for these
> +                 files. If it was a file we need to show a deletion diff
> +                 for that file. */
> +            }
> +
> +          /* Check the subdir if in the anchor (the subdir is the target), or
> +             if recursive */
> +          if (in_anchor_not_target || dir_baton->edit_baton->recurse)
> +            {
> +              subdir_baton = make_dir_baton (name, dir_baton,
> +                                             dir_baton->edit_baton,
> +                                             dir_baton->pool);
> +              subdir_baton->added = FALSE;
> +
> +              SVN_ERR (directory_elements_diff (subdir_baton, deleted));
> +              svn_pool_destroy (subdir_baton->pool);
> +            }
> +          break;
> +
> +        default:
> +          break;
> +        }
> +    }
> +
> +  return SVN_NO_ERROR;
> +}
> +
> +/* An svn_delta_edit_fns_t editor function. The root of the comparison
> + * hierarchy
> + */
> +static svn_error_t *
> +set_target_revision (void *edit_baton, svn_revnum_t target_revision)
> +{
> +  return SVN_NO_ERROR;
> +}
> +
> +/* An svn_delta_edit_fns_t editor function. The root of the comparison
> + * hierarchy
> + */
> +static svn_error_t *
> +replace_root (void *edit_baton,
> +              svn_revnum_t base_revision,
> +              void **root_baton)
> +{
> +  struct edit_baton *eb = edit_baton;
> +  struct dir_baton *b;
> +
> +  b = make_dir_baton (NULL, NULL, eb, eb->pool);
> +  *root_baton = b;
> +
> +  return SVN_NO_ERROR;
> +}
> +
> +/* An svn_delta_edit_fns_t editor function.
> + */
> +static svn_error_t *
> +delete_entry (svn_stringbuf_t *name,
> +              void *parent_baton)
> +{
> +  struct dir_baton *pb = parent_baton;
> +  apr_pool_t *pool = svn_pool_create (pb->pool);
> +  svn_stringbuf_t *path = svn_stringbuf_dup (pb->path, pool);
> +  svn_wc_entry_t *entry;
> +  struct dir_baton *b;
> +
> +  svn_path_add_component (path, name, svn_path_local_style);
> +  SVN_ERR (svn_wc_entry (&entry, path, pool));
> +  switch (entry->kind)
> +    {
> +    case svn_node_file:
> +      /* For a file that is being deleted show a diff with an empty
> +         file. */
> +      SVN_ERR (pb->edit_baton->diff_cmd (path,
> +                                         svn_wc__empty_file_path (path, pool),
> +                                         NULL,
> +                                         pb->edit_baton->diff_cmd_baton));
> +      break;
> +
> +    case svn_node_dir:
> +      b = make_dir_baton (name, pb, pb->edit_baton, pool);
> +      b->added = FALSE;
> +      SVN_ERR (directory_elements_diff (b, TRUE));
> +      break;
> +
> +    default:
> +      break;
> +    }
> +
> +  svn_pool_destroy (pool);
> +
> +  return SVN_NO_ERROR;
> +}
> +
> +/* An svn_delta_edit_fns_t editor function.
> + */
> +static svn_error_t *
> +add_directory (svn_stringbuf_t *name,
> +               void *parent_baton,
> +               svn_stringbuf_t *copyfrom_path,
> +               svn_revnum_t copyfrom_revision,
> +               void **child_baton)
> +{
> +  struct dir_baton *pb = parent_baton;
> +  struct dir_baton *b;
> +
> +  /* ### TODO: support copyfrom? */
> +
> +  b = make_dir_baton (name, pb, pb->edit_baton, pb->pool);
> +  *child_baton = b;
> +  b->added = TRUE;
> +
> +  return SVN_NO_ERROR;
> +}
> +
> +/* An svn_delta_edit_fns_t editor function.
> + */
> +static svn_error_t *
> +replace_directory (svn_stringbuf_t *name,
> +                   void *parent_baton,
> +                   svn_revnum_t base_revision,
> +                   void **child_baton)
> +{
> +  struct dir_baton *pb = parent_baton;
> +  struct dir_baton *b;
> +
> +  b = make_dir_baton (name, pb, pb->edit_baton, pb->pool);
> +  *child_baton = b;
> +  b->added = FALSE;
> +
> +  return SVN_NO_ERROR;
> +}
> +
> +/* An svn_delta_edit_fns_t editor function.  When a directory is closed,
> + * all the directory elements that have been added or replaced will already
> + * have been diff'd. However there may be other elements in the working
> + * copy that have not yet been considered.
> + */
> +static svn_error_t *
> +close_directory (void *dir_baton)
> +{
> +  struct dir_baton *b = dir_baton;
> +
> +  /* Skip added directories, they can only contain added elements all of
> +     which have already been diff'd. */
> +  if (!b->added)
> +    SVN_ERR (directory_elements_diff (dir_baton, FALSE));
> +
> +  /* Mark this directory as compared in the parent directory's baton. */
> +  if (b->dir_baton)
> +    {
> +      apr_hash_set (b->dir_baton->compared, b->path->data, b->path->len,
> +                    (void*)TRUE);
> +    }
> +
> +  svn_pool_destroy (b->pool);
> +
> +  return SVN_NO_ERROR;
> +}
> +
> +/* An svn_delta_edit_fns_t editor function.
> + */
> +static svn_error_t *
> +add_file (svn_stringbuf_t *name,
> +          void *parent_baton,
> +          svn_stringbuf_t *copyfrom_path,
> +          svn_revnum_t copyfrom_revision,
> +          void **file_baton)
> +{
> +  struct dir_baton *pb = parent_baton;
> +  struct file_baton *b;
> +
> +  /* ### TODO: support copyfrom? */
> +
> +  b = make_file_baton (name, pb);
> +  *file_baton = b;
> +
> +  b->added = TRUE;
> +
> +  return SVN_NO_ERROR;
> +}
> +
> +/* An svn_delta_edit_fns_t editor function.
> + */
> +static svn_error_t *
> +replace_file (svn_stringbuf_t *name,
> +              void *parent_baton,
> +              svn_revnum_t base_revision,
> +              void **file_baton)
> +{
> +  struct dir_baton *pb = parent_baton;
> +  struct file_baton *b;
> +
> +  b = make_file_baton (name, pb);
> +  *file_baton = b;
> +
> +  b->added = FALSE;
> +
> +  return SVN_NO_ERROR;
> +}
> +
> +/* An svn_delta_edit_fns_t editor function.  Do the work of applying the
> + * text delta.
> + *
> + * NOTE: make sure the temporary file gets deleted if an error occurs
> + */
> +static svn_error_t *
> +window_handler (svn_txdelta_window_t *window,
> +                void *window_baton)
> +{
> +  struct file_baton *b = window_baton;
> +  svn_error_t *err1, *err2 = SVN_NO_ERROR, *err3 = SVN_NO_ERROR;
> +
> +  err1 = b->apply_handler (window, b->apply_baton);
> +
> +  if (!window)
> +    {
> +      err2 = svn_wc__close_text_base (b->temp_file, b->wc_path, 0, b->pool);
> +
> +      if (b->added)
> +        {
> +          err3 = svn_wc__close_empty_file (b->original_file, b->wc_path,
> +                                           b->pool);
> +        }
> +      else
> +        {
> +          err3 = svn_wc__close_text_base (b->original_file, b->wc_path, 0,
> +                                          b->pool);
> +        }
> +    }
> +
> +  /* Remove the temporary file if an error occured. */
> +  if (err1 != SVN_NO_ERROR || err2 != SVN_NO_ERROR || err3 != SVN_NO_ERROR)
> +    {
> +      svn_stringbuf_t *temp_file_path
> +        = svn_wc__text_base_path (b->wc_path, TRUE, b->pool);
> +
> +      apr_file_remove (temp_file_path->data, b->pool);
> +
> +      /* Return the first error encountered. */
> +      if (err1)
> +        return err1;
> +      if (err2)
> +        return err2;
> +      return err3;
> +    }
> +
> +  return SVN_NO_ERROR;
> +}
> +
> +/* An svn_delta_edit_fns_t editor function.
> + */
> +static svn_error_t *
> +apply_textdelta (void *file_baton,
> +                 svn_txdelta_window_handler_t *handler,
> +                 void **handler_baton)
> +{
> +  struct file_baton *b = file_baton;
> +
> +  if (b->added)
> +    {
> +      /* An empty file is the starting point if the file is being added */
> +      SVN_ERR (svn_wc__open_empty_file (&b->original_file, b->wc_path,
> +                                        b->pool));
> +    }
> +  else
> +    {
> +      /* The current text-base is the starting point if replacing */
> +      SVN_ERR (svn_wc__open_text_base (&b->original_file, b->wc_path,
> +                                       APR_READ, b->pool));
> +    }
> +
> +  /* This will be the new version. It is created in the temporary part of
> +   * the admin area. This file exists until the diff callback is run.
> +   *
> +   * NOTE: need to be very careful with the error handling from this point
> +   * on, until the temporary file is deleted, to ensure that it is deleted
> +   * if an error occurs.
> +   */
> +  SVN_ERR (svn_wc__open_text_base (&b->temp_file, b->wc_path,
> +                                   (APR_WRITE | APR_TRUNCATE | APR_CREATE),
> +                                   b->pool));
> +
> +  svn_txdelta_apply (svn_stream_from_aprfile (b->original_file, b->pool),
> +                     svn_stream_from_aprfile (b->temp_file, b->pool),
> +                     b->pool,
> +                     &b->apply_handler, &b->apply_baton);
> +
> +  *handler = window_handler;
> +  *handler_baton = file_baton;
> +  return SVN_NO_ERROR;
> +}
> +
> +/* An svn_delta_edit_fns_t editor function.  When the file is closed we
> + * have a temporary file containing a pristine version of the repository
> + * file. This can be compared against the working copy.
> + *
> + * NOTE: make sure the temporary file gets deleted if an error occurs
> + */
> +static svn_error_t *
> +close_file (void *file_baton)
> +{
> +  struct file_baton *b = file_baton;
> +  svn_wc_diff_cmd_t diff_cmd = b->edit_baton->diff_cmd;
> +  svn_error_t *err;
> +
> +  /* The path to the temporary copy of the pristine repository version. */
> +  svn_stringbuf_t *temp_file_path
> +    = svn_wc__text_base_path (b->wc_path, TRUE, b->pool);
> +
> +  if (b->added)
> +    {
> +      /* If the file is being added, it doesn't exist in the working
> +         copy. Compare against an empty file. */
> +      err = diff_cmd (svn_wc__empty_file_path (b->wc_path, b->pool),
> +                      temp_file_path, b->path,
> +                      b->edit_baton->diff_cmd_baton);
> +    }
> +  else
> +    {
> +      /* Compare with the working copy. */
> +      err = diff_cmd (b->path, temp_file_path, NULL,
> +                      b->edit_baton->diff_cmd_baton);
> +    }
> +
> +  /* Remove the temporary file. */
> +  apr_file_remove (temp_file_path->data, b->pool);
> +
> +  /* Add this file to the parent directory's list of elements that have
> +     been compared. */
> +  apr_hash_set (b->dir_baton->compared, b->path->data, b->path->len,
> +                (void*)TRUE);
> +
> +  svn_pool_destroy (b->pool);
> +
> +  return err;
> +}
> +
> +/* An svn_delta_edit_fns_t editor function.
> + */
> +static svn_error_t *
> +close_edit (void *edit_baton)
> +{
> +  /* ### TODO: If the root has not been replaced, then we need to do this
> +     to pick up local changes. Can this happen? Well, at the meoment, it
> +     does if I replace a file with a directory, but I don't know if that is
> +     supposed to be supported yet. I would do something like this...
> +  */
> +# if 0
> +  struct edit_baton *eb = edit_baton;
> +
> +  if (!eb->root_replaced)
> +    {
> +      struct dir_baton *b;
> +
> +      b = make_dir_baton (NULL, NULL, eb, eb->pool);
> +      SVN_ERR (directory_elements_diff (b, FALSE));
> +      svn_pool_destroy (b->pool);
> +    }
> +#endif
> +
> +  return SVN_NO_ERROR;
> +}
> +
> +/* Create a diff editor and baton.
> + */
> +svn_error_t *
> +svn_wc_get_diff_editor (svn_stringbuf_t *anchor,
> +                        svn_stringbuf_t *target,
> +                        svn_wc_diff_cmd_t diff_cmd,
> +                        void *diff_cmd_baton,
> +                        svn_boolean_t recurse,
> +                        const svn_delta_edit_fns_t **editor,
> +                        void **edit_baton,
> +                        apr_pool_t *pool)
> +{
> +  svn_delta_edit_fns_t *tree_editor = svn_delta_default_editor (pool);
> +  struct edit_baton *eb = apr_palloc (pool, sizeof (*eb));
> +
> +  eb->anchor = anchor;
> +  eb->target = target;
> +  eb->diff_cmd = diff_cmd;
> +  eb->diff_cmd_baton = diff_cmd_baton;
> +  eb->recurse = recurse;
> +  eb->pool = pool;
> +
> +  tree_editor->set_target_revision = set_target_revision;
> +  tree_editor->replace_root = replace_root;
> +  tree_editor->delete_entry = delete_entry;
> +  tree_editor->add_directory = add_directory;
> +  tree_editor->replace_directory = replace_directory;
> +  tree_editor->close_directory = close_directory;
> +  tree_editor->add_file = add_file;
> +  tree_editor->replace_file = replace_file;
> +  tree_editor->apply_textdelta = apply_textdelta;
> +  tree_editor->close_file = close_file;
> +  tree_editor->close_edit = close_edit;
> +
> +  *edit_baton = eb;
> +  *editor = tree_editor;
> +
> +  return SVN_NO_ERROR;
> +}
> +
> +
> +/* ----------------------------------------------------------------
> + * local variables:
> + * eval: (load-file "../svn-dev.el")
> + * end:
> + */
> Index: ./subversion/libsvn_wc/wc.h
> ===================================================================
> --- ./subversion/libsvn_wc/wc.h
> +++ ./subversion/libsvn_wc/wc.h	Sat Nov  3 13:25:55 2001
> @@ -100,6 +100,7 @@
>  #define SVN_WC__ADM_LOG                 "log"
>  #define SVN_WC__ADM_KILLME              "KILLME"
>  #define SVN_WC__ADM_AUTH_DIR            "auth"
> +#define SVN_WC__ADM_EMPTY_FILE          "empty-file"
>  
>  
>  /* The basename of the ".prej" file, if a directory ever has property
> @@ -111,6 +112,11 @@
>  svn_stringbuf_t *svn_wc__adm_subdir (apr_pool_t *pool);
>  
>  
> +/* Return the path to the empty file in the adm area of PATH */
> +svn_stringbuf_t *svn_wc__empty_file_path (const svn_stringbuf_t *path,
> +                                          apr_pool_t *pool);
> +
> +
>  /* Return a path to something in PATH's administrative area.
>   * Return path to the thing in the tmp area if TMP is non-zero.
>   * Varargs are (const char *)'s, the final one must be NULL.
> @@ -182,6 +188,19 @@
>  svn_error_t *svn_wc__remove_adm_file (svn_stringbuf_t *path,
>                                        apr_pool_t *pool,
>                                        ...);
> +
> +/* Open *readonly* the empty file in the in adm area of PATH */
> +svn_error_t *svn_wc__open_empty_file (apr_file_t **handle,
> +                                      svn_stringbuf_t *path,
> +                                      apr_pool_t *pool);
> +
> +/* Close the empty file in the adm area of PATH. FP was obtain from
> + * svn_wc__open_empty_file().
> + */
> +svn_error_t *svn_wc__close_empty_file (apr_file_t *fp,
> +                                       svn_stringbuf_t *path,
> +                                       apr_pool_t *pool);
> +
>  
>  /* Open the text-base for FILE.
>   * FILE can be any kind of path ending with a filename.
> Index: ./subversion/libsvn_wc/adm_files.c
> ===================================================================
> --- ./subversion/libsvn_wc/adm_files.c
> +++ ./subversion/libsvn_wc/adm_files.c	Sat Nov  3 13:25:36 2001
> @@ -376,6 +376,16 @@
>    return thing_path (path, SVN_WC__ADM_TEXT_BASE, tmp, pool);
>  }
>  
> +svn_stringbuf_t *
> +svn_wc__empty_file_path (const svn_stringbuf_t *path,
> +                         apr_pool_t *pool)
> +{
> +  svn_stringbuf_t *empty_file_path = svn_stringbuf_dup (path, pool);
> +  svn_path_remove_component (empty_file_path, svn_path_local_style);
> +  extend_with_adm_name (empty_file_path, 0, pool, SVN_WC__ADM_EMPTY_FILE, NULL);
> +  return empty_file_path;
> +}
> +
>  
>  static svn_error_t *
>  prop_path_internal (svn_stringbuf_t **prop_path,
> @@ -707,6 +717,30 @@
>  
>  
>  svn_error_t *
> +svn_wc__open_empty_file (apr_file_t **handle,
> +                         svn_stringbuf_t *path,
> +                         apr_pool_t *pool)
> +{
> +  svn_stringbuf_t *newpath;
> +  svn_path_split (path, &newpath, NULL, svn_path_local_style, pool);
> +  return open_adm_file (handle, newpath, APR_READ, pool,
> +                        SVN_WC__ADM_EMPTY_FILE, NULL);
> +}
> +
> +
> +svn_error_t *
> +svn_wc__close_empty_file (apr_file_t *fp,
> +                          svn_stringbuf_t *path,
> +                          apr_pool_t *pool)
> +{
> +  svn_stringbuf_t *newpath, *basename;
> +  svn_path_split (path, &newpath, &basename, svn_path_local_style, pool);
> +  return close_adm_file (fp, newpath, 0, pool,
> +                         SVN_WC__ADM_EMPTY_FILE, NULL);
> +}
> +
> +
> +svn_error_t *
>  svn_wc__open_text_base (apr_file_t **handle,
>                          svn_stringbuf_t *path,
>                          apr_int32_t flags,
> @@ -1180,6 +1214,14 @@
>  
>    /* SVN_WC__ADM_ENTRIES */
>    SVN_ERR (svn_wc__entries_init (path, ancestor_path, pool));
> +
> +  /* SVN_WC__ADM_EMPTY_FILE exists because sometimes an readable, empty
> +     file is required (in the repository diff for example). Creating such a
> +     file temporarily, only to delete it again, would appear to be less
> +     efficient than just having one around. It doesn't take up much space
> +     after all. */
> +  SVN_ERR (svn_wc__make_adm_thing (path, SVN_WC__ADM_EMPTY_FILE, svn_node_file,
> +                                   APR_UREAD, 0, pool));
>  
>    /* THIS FILE MUST BE CREATED LAST: 
>       After this exists, the dir is considered complete. */
> Index: ./subversion/libsvn_client/diff.c
> ===================================================================
> --- ./subversion/libsvn_client/diff.c
> +++ ./subversion/libsvn_client/diff.c	Mon Nov  5 01:51:43 2001
> @@ -1,5 +1,5 @@
>  /*
> - * diff.c:  return two temporary file paths that can be diffed
> + * diff.c: Compare working copy with text-base or repository.
>   *
>   * ====================================================================
>   * Copyright (c) 2000-2001 CollabNet.  All rights reserved.
> @@ -33,7 +33,97 @@
>  #include "svn_path.h"
>  #include "svn_test.h"
>  #include "svn_io.h"
> +#include "svn_pools.h"
> +#include "client.h"
> +#include "svn_private_config.h"         /* for SVN_CLIENT_DIFF */
> +#include <assert.h>
> +
> +struct diff_cmd_baton {
> +  apr_array_header_t *options;
> +  apr_pool_t *pool;
> +};
> +
> +/* This is an svn_wc_diff_cmd_t callback */
> +static svn_error_t *
> +svn_client__diff_cmd (svn_stringbuf_t *path1,
> +                      svn_stringbuf_t *path2,
> +                      svn_stringbuf_t *label,
> +                      void *baton)
> +{
> +  struct diff_cmd_baton *diff_cmd_baton = baton;
> +  apr_status_t status;
> +  const char **args;
> +  int i = 0;
> +  int nargs = 4; /* Eek! A magic number! It's checked by a later assert */
> +  apr_file_t *outhandle = NULL;
> +
> +  /* Use a subpool here because the pool comes right through from the top
> +     level.*/
> +  apr_pool_t *subpool = svn_pool_create (diff_cmd_baton->pool);
> +
> +  /* Get an apr_file_t representing stdout, which is where we'll have
> +     the diff program print to. */
> +  status = apr_file_open_stdout (&outhandle, subpool);
> +  if (status)
> +    return svn_error_create (status, 0, NULL, subpool,
> +                             "error: can't open handle to stdout");
> +
> +  /* Execute local diff command on these two paths, print to stdout. */
> +
> +  if (label)
> +    nargs += 2; /* Another magic number checked by a later assert */
> +  if (diff_cmd_baton->options->nelts)
> +    nargs += diff_cmd_baton->options->nelts;
> +  else
> +    ++nargs;
> +  args = apr_palloc(subpool, nargs*sizeof(char*));
> +
> +  args[i++] = SVN_CLIENT_DIFF;  /* the autoconfiscated system diff program */
> +  if (diff_cmd_baton->options->nelts)
> +    {
> +      /* Add the user specified diff options to the diff command line. */
> +      int j;
> +      for (j = 0; j < diff_cmd_baton->options->nelts; ++j)
> +        args[i++]
> +          = ((svn_stringbuf_t **)(diff_cmd_baton->options->elts))[j]->data;
> +    }
> +  else
> +    {
> +      /* The user didn't specify any options, default to unified diff. */
> +      args[i++] = "-u";
> +    }
> +  if (label)
> +    {
> +      args[i++] = "-L";
> +      args[i++] = label->data;
> +    }
> +  args[i++] = path1->data;
> +  args[i++] = path2->data;
> +  args[i++] = NULL;
> +  assert (i==nargs);
> +
> +  /* ### TODO: This printf is NOT "my final answer" -- placeholder for
> +     real work to be done. */ 
> +  if (label)
> +    apr_file_printf (outhandle, "Index: %s\n", label->data);
> +  else
> +    apr_file_printf (outhandle, "Index: %s\n", path1->data);
> +  apr_file_printf (outhandle, "===================================================================\n");
> +
> +  SVN_ERR(svn_io_run_cmd (".", SVN_CLIENT_DIFF, args, NULL, NULL,
> +                          NULL, outhandle, NULL, subpool));
> +
> +  /* TODO: Handle exit code == 2 (i.e. errors with diff) here */
> +  
> +  /* TODO: someday we'll need to worry about whether we're going to need to
> +     write a diff plug-in mechanism that makes use of the two paths,
> +     instead of just blindly running SVN_CLIENT_DIFF.
> +  */
> +
> +  svn_pool_destroy (subpool);
>  
> +  return SVN_NO_ERROR;
> +}
>  
>  
>  
> @@ -59,10 +149,73 @@
>    return SVN_NO_ERROR;
>  }
>  
> +/* Compare working copy against the repository */
> +svn_error_t *
> +svn_client_diff (svn_stringbuf_t *path,
> +                 apr_array_header_t *diff_options,
> +                 svn_client_auth_baton_t *auth_baton,
> +                 svn_revnum_t revision,
> +                 apr_time_t tm,
> +                 svn_boolean_t recurse,
> +                 apr_pool_t *pool)
> +{
> +  svn_stringbuf_t *anchor, *target;
> +  svn_wc_entry_t *entry;
> +  svn_stringbuf_t *URL;
> +  void *ra_baton, *session, *cb_baton;
> +  svn_ra_plugin_t *ra_lib;
> +  svn_ra_callbacks_t *ra_callbacks;
> +  const svn_ra_reporter_t *reporter;
> +  void *report_baton;
> +  const svn_delta_edit_fns_t *diff_editor;
> +  void *diff_edit_baton;
> +  struct diff_cmd_baton diff_cmd_baton = { diff_options, pool };
> +
> +  /* If both REVISION and TM are specified, this is an error.
> +     They mostly likely contradict one another. */
> +  if ((revision != SVN_INVALID_REVNUM) && tm)
> +    return
> +      svn_error_create(SVN_ERR_CL_MUTUALLY_EXCLUSIVE_ARGS, 0, NULL, pool,
> +                       "Cannot specify _both_ revision and time.");
> +
> +  SVN_ERR (svn_wc_get_actual_target (path, &anchor, &target, pool));
> +  SVN_ERR (svn_wc_entry (&entry, anchor, pool));
> +  URL = svn_stringbuf_create (entry->ancestor->data, pool);
> +
> +  SVN_ERR (svn_ra_init_ra_libs (&ra_baton, pool));
> +  SVN_ERR (svn_ra_get_ra_library (&ra_lib, ra_baton, URL->data, pool));
> +
> +  SVN_ERR (svn_client__get_ra_callbacks (&ra_callbacks, &cb_baton,
> +                                         auth_baton,
> +                                         anchor,
> +                                         TRUE,
> +                                         TRUE,
> +                                         pool));
> +  SVN_ERR (ra_lib->open (&session, URL, ra_callbacks, cb_baton, pool));
> +
> +  if (tm)
> +    SVN_ERR (ra_lib->get_dated_revision (session, &revision, tm));
> +
> +  SVN_ERR (svn_wc_get_diff_editor (anchor, target,
> +                                   svn_client__diff_cmd, &diff_cmd_baton,
> +                                   recurse,
> +                                   &diff_editor, &diff_edit_baton,
> +                                   pool));
> +
> +  SVN_ERR (ra_lib->do_update (session,
> +                              &reporter, &report_baton,
> +                              revision,
> +                              target,
> +                              recurse,
> +                              diff_editor, diff_edit_baton));
>  
> +  SVN_ERR (svn_wc_crawl_revisions (path, reporter, report_baton,
> +                                   FALSE, FALSE, recurse, pool));
>  
> +  SVN_ERR (ra_lib->close (session));
>  
> -
> +  return SVN_NO_ERROR;
> +}
>  
>  
>  
> Index: ./subversion/clients/cmdline/diff-cmd.c
> ===================================================================
> --- ./subversion/clients/cmdline/diff-cmd.c
> +++ ./subversion/clients/cmdline/diff-cmd.c	Sat Nov  3 21:40:30 2001
> @@ -34,6 +34,56 @@
>  
>  /*** Code. ***/
>  
> +/* Compare working copy against a given repository version. */
> +static svn_error_t *
> +svn_cl__wc_repository_diff (apr_getopt_t *os,
> +                            svn_cl__opt_state_t *opt_state,
> +                            apr_pool_t *pool)
> +{
> +  apr_array_header_t *options;
> +  apr_array_header_t *targets;
> +  apr_array_header_t *condensed_targets;
> +  svn_client_auth_baton_t *auth_baton;
> +  int i;
> +
> +  if (opt_state->start_revision != opt_state->end_revision)
> +    {
> +      return svn_error_createf (SVN_ERR_UNSUPPORTED_FEATURE, 0, NULL, pool,
> +                                "Only a single revision can be specified");
> +    }
> +
> +  options = svn_cl__stringlist_to_array(opt_state->extensions, pool);
> +
> +  targets = svn_cl__args_to_target_array (os, pool);
> +  svn_cl__push_implicit_dot_target (targets, pool);
> +  SVN_ERR (svn_path_remove_redundancies (&condensed_targets,
> +                                         targets,
> +                                         svn_path_local_style,
> +                                         pool));
> +
> +  auth_baton = svn_cl__make_auth_baton (opt_state, pool);
> +
> +  for (i = 0; i < condensed_targets->nelts; ++i)
> +    {
> +      svn_stringbuf_t *target
> +        = ((svn_stringbuf_t **) (condensed_targets->elts))[i];
> +      svn_stringbuf_t *parent_dir, *entry;
> +
> +      SVN_ERR (svn_wc_get_actual_target (target, &parent_dir, &entry, pool));
> +      
> +      SVN_ERR (svn_client_diff (target,
> +                                options,
> +                                auth_baton,
> +                                opt_state->start_revision,
> +                                opt_state->start_date,
> +                                opt_state->nonrecursive ? FALSE : TRUE,
> +                                pool));
> +    }
> +
> +  return SVN_NO_ERROR;
> +}
> +
> +/* An svn_cl__cmd_proc_t to handle the 'diff' command. */
>  svn_error_t *
>  svn_cl__diff (apr_getopt_t *os,
>                svn_cl__opt_state_t *opt_state,
> @@ -45,6 +95,15 @@
>    svn_boolean_t recurse = TRUE;
>    int i;
>  
> +  /* If a revision has been specified then compare against the repository.
> +     This won't catch 'svn diff -rHEAD:1' since that doesn't change the
> +     revisons from their default value. */
> +  if (opt_state->start_revision != SVN_INVALID_REVNUM
> +      || opt_state->end_revision != 1)
> +    {
> +      return svn_cl__wc_repository_diff (os, opt_state, pool);
> +    }
> +
>    options = svn_cl__stringlist_to_array(opt_state->extensions, pool);
>    targets = svn_cl__args_to_target_array(os, pool);
>  
> @@ -88,6 +147,7 @@
>    
>    return SVN_NO_ERROR;
>  }
> +
>  
>  
>  /* 
> Index: ./subversion/clients/cmdline/main.c
> ===================================================================
> --- ./subversion/clients/cmdline/main.c
> +++ ./subversion/clients/cmdline/main.c	Sat Nov  3 17:29:33 2001
> @@ -141,7 +141,8 @@
>    { "st",         TRUE, NULL, NULL },
>   
>    { "diff",       FALSE, svn_cl__diff,
> -    "Display local file changes as contextual diffs.\n"
> +    "Display local changes in the working copy, or changes between the\n"
> +    "working copy and the repository if a revision is given.\n"
>      "usage: diff [TARGETS]\n" },
>    { "di",         TRUE, NULL, NULL },
>  
> Index: ./subversion/tests/clients/cmdline/diff_tests.py
> ===================================================================
> --- ./subversion/tests/clients/cmdline/diff_tests.py
> +++ ./subversion/tests/clients/cmdline/diff_tests.py	Mon Nov  5 01:47:31 2001
> @@ -0,0 +1,498 @@
> +#!/usr/bin/env python
> +#
> +#  diff_tests.py:  some basic diff tests
> +#
> +#  Subversion is a tool for revision control. 
> +#  See http://subversion.tigris.org for more information.
> +#    
> +# ====================================================================
> +# Copyright (c) 2001 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.
> +#
> +######################################################################
> +
> +# General modules
> +import shutil, string, sys, re, os.path, traceback
> +
> +try:
> +  import svntest
> +except SyntaxError:
> +  sys.stderr.write('[SKIPPED] ')
> +  print "<<< Please make sure you have Python 2 or better! >>>"
> +  traceback.print_exc(None,sys.stdout)
> +  raise SystemExit
> +
> +# Quick macro for auto-generating sandbox names
> +def sandbox(x):
> +  return "diff_tests-" + `test_list.index(x)`
> +
> +
> +######################################################################
> +# Diff output checker
> +#
> +# Looks for the correct filenames and a suitable number of +/- lines
> +# depending on whether this is an addition, modification or deletion.
> +
> +def check_diff_output(diff_output, name, diff_type):
> +  "check diff output"
> +
> +  i_re = re.compile('^Index:')
> +  d_re = re.compile('^Index: \\./' + name)
> +  p_re = re.compile('^--- \\./' + name)
> +  add_re = re.compile('^\\+')
> +  sub_re = re.compile('^-')
> +
> +  i = 0
> +  while i < len(diff_output) - 4:
> +
> +    # identify a possible diff
> +    if (d_re.match(diff_output[i])
> +        and p_re.match(diff_output[i+2])):
> +
> +      # count lines added and deleted
> +      i += 4
> +      add_lines = 0
> +      sub_lines = 0
> +      while i < len(diff_output) and not i_re.match(diff_output[i]):
> +        if add_re.match(diff_output[i][0]):
> +          add_lines += 1
> +        if sub_re.match(diff_output[i][0]):
> +          sub_lines += 1
> +        i += 1
> +
> +      # check if this looks like the right sort of diff
> +      if add_lines > 0 and sub_lines == 0 and diff_type == 'A':
> +        return 0
> +      if sub_lines > 0 and add_lines == 0 and diff_type == 'D':
> +        return 0
> +      if add_lines > 0 and sub_lines > 0 and diff_type == 'M':
> +        return 0
> +
> +    else:
> +      i += 1
> +
> +  # no suitable diff found
> +  return 1
> +
> +def count_diff_output(diff_output):
> +  "count the number of file diffs in the output"
> +
> +  i_re = re.compile('Index:')
> +  diff_count = 0
> +  i = 0
> +  while i < len(diff_output) - 4:
> +    if i_re.match(diff_output[i]):
> +      i += 4
> +      diff_count += 1
> +    else:
> +      i += 1
> +
> +  return diff_count
> +
> +######################################################################
> +# Changes makers and change checkers
> +
> +def update_a_file():
> +  "update a file"
> +  svntest.main.file_append(os.path.join('A', 'B', 'E', 'alpha'), "new atext")
> +  return 0
> +  
> +def check_update_a_file(diff_output):
> +  "check diff for update a file"
> +  return check_diff_output(diff_output,
> +                           os.path.join('A', 'B', 'E', 'alpha'),
> +                           'M')
> +
> +#----------------------------------------------------------------------
> +
> +def add_a_file():
> +  "add a file"
> +  svntest.main.file_append(os.path.join('A', 'B', 'E', 'theta'), "theta")
> +  svntest.main.run_svn(None, 'add', os.path.join('A', 'B', 'E', 'theta'))
> +  return 0
> +
> +def check_add_a_file(diff_output):
> +  "check diff for add a file"
> +  return check_diff_output(diff_output,
> +                           os.path.join('A', 'B', 'E', 'theta'),
> +                           'A')
> +
> +def check_add_a_file_reverse(diff_output):
> +  "check diff for add a file"
> +  return check_diff_output(diff_output,
> +                           os.path.join('A', 'B', 'E', 'theta'),
> +                           'D')
> +
> +#----------------------------------------------------------------------
> +
> +def add_a_file_in_a_subdir():
> +  "add a file in a subdir"
> +  os.mkdir(os.path.join('A', 'B', 'T'))
> +  svntest.main.run_svn(None, 'add', os.path.join('A', 'B', 'T'))
> +  svntest.main.file_append(os.path.join('A', 'B', 'T', 'phi'), "phi")
> +  svntest.main.run_svn(None, 'add', os.path.join('A', 'B', 'T', 'phi'))
> +  return 0
> +
> +def check_add_a_file_in_a_subdir(diff_output):
> +  "check diff for add a file in a subdir"
> +  return check_diff_output(diff_output,
> +                           os.path.join('A', 'B', 'T', 'phi'),
> +                           'A')
> +
> +def check_add_a_file_in_a_subdir_reverse(diff_output):
> +  "check diff for add a file in a subdir"
> +  return check_diff_output(diff_output,
> +                           os.path.join('A', 'B', 'T', 'phi'),
> +                           'D')
> +
> +#----------------------------------------------------------------------
> +
> +def replace_a_file_with_a_dir():
> +  "replace a file with a dir"
> +  svntest.main.run_svn(None, 'rm', os.path.join('A', 'B', 'lambda'))
> +  os.remove(os.path.join('A', 'B', 'lambda'))
> +  os.mkdir(os.path.join('A', 'B', 'lambda'))
> +  svntest.main.run_svn(None, 'add', os.path.join('A', 'B', 'lambda'))
> +  return 0
> +
> +def check_replace_a_file_with_a_dir(diff_output):
> +  "check diff for replace a file with a dir"
> +  return check_diff_output(diff_output,
> +                           os.path.join('A', 'B', 'lambda'),
> +                           'D')
> +
> +#----------------------------------------------------------------------
> +
> +def replace_a_file():
> +  "replace a file"
> +  svntest.main.run_svn(None, 'rm', os.path.join('A', 'D', 'G', 'rho'))
> +  svntest.main.file_append(os.path.join('A', 'D', 'G', 'rho'), "new rho")
> +  svntest.main.run_svn(None, 'add', os.path.join('A', 'D', 'G', 'rho'))
> +  return 0
> +
> +def check_replace_a_file(diff_output):
> +  "check diff for replace a file"
> +  if check_diff_output(diff_output,
> +                       os.path.join('A', 'D', 'G', 'rho'),
> +                       'D'):
> +    return 1
> +  if check_diff_output(diff_output,
> +                       os.path.join('A', 'D', 'G', 'rho'),
> +                       'A'):
> +    return 1
> +  return 0
> +    
> +#----------------------------------------------------------------------
> +
> +def replace_a_dir_and_file():
> +  "replace a dir and file"
> +  svntest.main.run_svn(None, 'rm', os.path.join('A', 'B', 'E'))
> +  shutil.rmtree(os.path.join('A', 'B', 'E'))
> +  os.mkdir(os.path.join('A', 'B', 'E'))
> +  svntest.main.file_append(os.path.join('A', 'B', 'E', 'alpha'), "new alpha")
> +  svntest.main.run_svn(None, 'add', os.path.join('A', 'B', 'E'))
> +  svntest.main.run_svn(None, 'add', os.path.join('A', 'B', 'E', 'alpha'))
> +  return 0
> +
> +def check_replace_a_dir_and_file(diff_output):
> +  "check diff for replace a dir and file"
> +  if check_diff_output(diff_output,
> +                       os.path.join('A', 'B', 'E', 'alpha'),
> +                       'D'):
> +    return 1
> +  if  check_diff_output(diff_output,
> +                        os.path.join('A', 'B', 'E', 'alpha'),
> +                        'A'):
> +    return 1
> +  return 0
> +  
> +#----------------------------------------------------------------------
> +
> +def update_three_files():
> +  "update three files"
> +  svntest.main.file_append(os.path.join('A', 'D', 'gamma'), "new gamma")
> +  svntest.main.file_append(os.path.join('A', 'D', 'G', 'tau'), "new tau")
> +  svntest.main.file_append(os.path.join('A', 'D', 'H', 'psi'), "new psi")
> +  return 0
> +
> +def check_update_three_files(diff_output):
> +  "check update three files"
> +  if check_diff_output(diff_output,
> +                        os.path.join('A', 'D', 'gamma'),
> +                        'M'):
> +    return 1
> +  if check_diff_output(diff_output,
> +                        os.path.join('A', 'D', 'G', 'tau'),
> +                        'M'):
> +    return 1
> +  if check_diff_output(diff_output,
> +                        os.path.join('A', 'D', 'H', 'psi'),
> +                        'M'):
> +    return 1
> +  return 0
> +                        
> +
> +#----------------------------------------------------------------------
> +
> +### TODO: subversion doesn't support this yet
> +def replace_a_dir_with_a_file():
> +  "replace a dir with a file"
> +  svntest.main.run_svn(None, 'rm', os.path.join('A', 'B', 'E'))
> +  shutil.rmtree(os.path.join('A', 'B', 'E'))
> +  svntest.main.file_append(os.path.join('A', 'B', 'E'), "new E")
> +  svntest.main.run_svn(None, 'add', os.path.join('A', 'B', 'E'))
> +  return 0
> +
> +######################################################################
> +# make a change, check the diff, commit the change, check the diff
> +
> +def change_diff_commit_diff(wc_dir, revision, change_fn, check_fn):
> +  "make a change, diff, commit, update and diff again"
> +
> +  was_cwd = os.getcwd()
> +  os.chdir(wc_dir)
> +
> +  svntest.main.run_svn(None, 'up', '-rHEAD')
> +
> +  change_fn()
> +
> +  diff_output, err_output = svntest.main.run_svn(None, 'diff', '-rHEAD')
> +  if check_fn(diff_output):
> +    os.chdir(was_cwd)
> +    return 1
> +
> +  svntest.main.run_svn(None, 'ci')
> +  svntest.main.run_svn(None, 'up', '-r', revision)
> +  diff_output, err_output = svntest.main.run_svn(None, 'diff', '-rHEAD')
> +  if check_fn(diff_output):
> +    os.chdir(was_cwd)
> +    return 1
> +
> +  os.chdir(was_cwd)
> +  return 0
> +
> +######################################################################
> +# update, check the diff
> +
> +def just_diff(wc_dir, rev_up, rev_check, check_fn):
> +  "check that the given diff is seen"
> +
> +  was_cwd = os.getcwd()
> +  os.chdir(wc_dir)
> +
> +  svntest.main.run_svn(None, 'up', '-r', rev_up)
> +  diff_output, err_output = svntest.main.run_svn(None, 'diff', '-r', rev_check)
> +  if check_fn(diff_output): return 1
> +
> +  os.chdir(was_cwd)
> +  return 0
> +
> +
> +######################################################################
> +# Tests
> +#
> +
> +# test 1
> +def diff_update_a_file():
> +  "update a file"
> +
> +  sbox = sandbox(diff_update_a_file)
> +  wc_dir = os.path.join(svntest.main.general_wc_dir, sbox)
> +  if svntest.actions.make_repo_and_wc(sbox): return 1
> +
> +  return change_diff_commit_diff(wc_dir, 1,
> +                                 update_a_file,
> +                                 check_update_a_file)
> +
> +# test 2
> +def diff_add_a_file():
> +  "add a file"
> +
> +  sbox = sandbox(diff_add_a_file)
> +  wc_dir = os.path.join(svntest.main.general_wc_dir, sbox)
> +  if svntest.actions.make_repo_and_wc(sbox): return 1
> +
> +  return change_diff_commit_diff(wc_dir, 1,
> +                                 add_a_file,
> +                                 check_add_a_file)
> +
> +#test 3
> +def diff_add_a_file_in_a_subdir():
> +  "add a file in an added directory"
> +
> +  sbox = sandbox(diff_add_a_file_in_a_subdir)
> +  wc_dir = os.path.join(svntest.main.general_wc_dir, sbox)
> +  if svntest.actions.make_repo_and_wc(sbox): return 1
> +
> +  return change_diff_commit_diff(wc_dir, 1,
> +                                 add_a_file_in_a_subdir,
> +                                 check_add_a_file_in_a_subdir)
> +
> +# test 4
> +def diff_replace_a_file_with_a_dir():
> +  "replace a file with a directory"
> +
> +  sbox = sandbox(diff_replace_a_file_with_a_dir)
> +  wc_dir = os.path.join(svntest.main.general_wc_dir, sbox)
> +  if svntest.actions.make_repo_and_wc(sbox): return 1
> +
> +  ### TODO: this bit will fail, 'svn diff' doesn't support this in the wc
> +  change_diff_commit_diff(wc_dir, 1,
> +                          replace_a_file_with_a_dir,
> +                          check_replace_a_file_with_a_dir)
> +
> +  # However it does support it in the repository, so force the commit
> +  # and then diff the repository
> +  svntest.main.run_svn(None, 'ci', wc_dir)
> +
> +  if just_diff(wc_dir, 1, 2, check_replace_a_file_with_a_dir): return 1
> +  return 0
> +  
> +
> +# test 5
> +def diff_replace_a_file():
> +  "replace a file with a file"
> +
> +  sbox = sandbox(diff_replace_a_file)
> +  wc_dir = os.path.join(svntest.main.general_wc_dir, sbox)
> +  if svntest.actions.make_repo_and_wc(sbox): return 1
> +
> +  return change_diff_commit_diff(wc_dir, 1,
> +                                 replace_a_file,
> +                                 check_replace_a_file)
> +
> +# test 6
> +def diff_two_add_reverse():
> +  "two commits diff'd forwards and backwards"
> +
> +  sbox = sandbox(diff_two_add_reverse)
> +  wc_dir = os.path.join(svntest.main.general_wc_dir, sbox)
> +  if svntest.actions.make_repo_and_wc(sbox): return 1
> +
> +  # rev 2
> +  if change_diff_commit_diff(wc_dir, 1,
> +                             add_a_file,
> +                             check_add_a_file):
> +    return 1
> +
> +  #rev 3
> +  if change_diff_commit_diff(wc_dir, 2,
> +                             add_a_file_in_a_subdir,
> +                             check_add_a_file_in_a_subdir):
> +    return 1
> +
> +  # check diffs both ways
> +  if just_diff(wc_dir, 1, 3, check_add_a_file_in_a_subdir):
> +    return 1
> +  if just_diff(wc_dir, 1, 3, check_add_a_file):
> +    return 1
> +  if just_diff(wc_dir, 3, 1, check_add_a_file_in_a_subdir_reverse):
> +    return 1
> +  if just_diff(wc_dir, 3, 1, check_add_a_file_reverse):
> +    return 1
> +  
> +  return 0
> +
> +# test 7
> +def diff_replace_a_dir_and_file():
> +  "replace a file/directory with a fil/directory"
> +
> +  sbox = sandbox(diff_replace_a_dir_and_file)
> +  wc_dir = os.path.join(svntest.main.general_wc_dir, sbox)
> +  if svntest.actions.make_repo_and_wc(sbox): return 1
> +
> +  ### TODO: this bit will fail, 'svn diff' doesn't support this in the wc
> +  change_diff_commit_diff(wc_dir, 1,
> +                          replace_a_dir_and_file,
> +                          check_replace_a_dir_and_file)
> +
> +  # However it does support it in the repository, so force the commit
> +  # and then diff the repository
> +  svntest.main.run_svn(None, 'ci', wc_dir)
> +
> +  if just_diff(wc_dir, 1, 2, check_replace_a_dir_and_file): return 1
> +  return 0
> +
> +# test 8
> +def diff_non_recursive():
> +  "check non-recursive behaviour"
> +
> +  sbox = sandbox(diff_non_recursive)
> +  wc_dir = os.path.join(svntest.main.general_wc_dir, sbox)
> +  if svntest.actions.make_repo_and_wc(sbox): return 1
> +
> +  if change_diff_commit_diff(wc_dir, 1,
> +                             update_three_files,
> +                             check_update_three_files):
> +    return 1
> +
> +  # The changes are in:   ./A/D/gamma
> +  #                       ./A/D/G/tau
> +  #                       ./A/D/H/psi
> +
> +  # When checking D recursively there are three changes. When checking
> +  # D non-recursively there is only one change. When checking G
> +  # recursively, there is only one change even though D is the anchor
> +  
> +  # full diff has three changes
> +  diff_output, err_output = svntest.main.run_svn(None, 'diff', '-rHEAD',
> +                                                 os.path.join(wc_dir, 'A', 'D'))
> +  if count_diff_output(diff_output) != 3:
> +    return 1
> +
> +  # non-recursive has one change
> +  diff_output, err_output = svntest.main.run_svn(None, 'diff', '-rHEAD', '-n',
> +                                                 os.path.join(wc_dir, 'A', 'D'))
> +  if count_diff_output(diff_output) != 1:
> +    return 1
> +
> +  # diffing a directory doesn't pick up other diffs in the anchor
> +  diff_output, err_output = svntest.main.run_svn(None, 'diff', '-rHEAD',
> +                                                 os.path.join(wc_dir,
> +                                                              'A', 'D', 'G'))
> +  if count_diff_output(diff_output) != 1:
> +    return 1
> +  
> +  return 0
> +
> +########################################################################
> +# Run the tests
> +
> +
> +# list all tests here, starting with None:
> +test_list = [ None,
> +              diff_update_a_file,
> +              diff_add_a_file,
> +              diff_add_a_file_in_a_subdir,
> +              diff_replace_a_file_with_a_dir,
> +              diff_replace_a_file,
> +              diff_two_add_reverse,
> +              diff_replace_a_dir_and_file,
> +              diff_non_recursive
> +             ]
> +
> +if __name__ == '__main__':
> +  
> +  ## run the main test routine on them:
> +  err = svntest.main.run_tests(test_list)
> +
> +  ## remove all scratchwork: the 'pristine' repository, greek tree, etc.
> +  ## This ensures that an 'import' will happen the next time we run.
> +  if os.path.exists(svntest.main.temp_dir):
> +    shutil.rmtree(svntest.main.temp_dir)
> +
> +  ## return whatever main() returned to the OS.
> +  sys.exit(err)
> +
> +
> +### End of file.
> +# local variables:
> +# eval: (load-file "../../../svn-dev.el")
> +# end:
> +
> +  
> 
> ---------------------------------------------------------------------
> To unsubscribe, e-mail: dev-unsubscribe@subversion.tigris.org
> For additional commands, e-mail: dev-help@subversion.tigris.org
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@subversion.tigris.org
For additional commands, e-mail: dev-help@subversion.tigris.org
Received on Sat Oct 21 14:36:48 2006