blob: fc2de80e13b7cb18869b51098257d92228c805b7 [file] [log] [blame]
/*
* repos_diff.c -- The diff editor for comparing two repository versions
*
* ====================================================================
* Copyright (c) 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/.
* ====================================================================
*/
/* This code uses an svn_delta_edit_fns_t editor driven by a tree delta
* between two repository revisions (REV1 and REV2). For each file
* encountered in the delta the editor constructs two temporary files, one
* for each revision. This necessitates a separate request for the REV1
* version of the file when the delta shows the file being modified or
* deleted. Files that are added by the delta do not require a separate
* request, the REV1 version is empty and the delta is sufficient to
* construct the REV2 version. When both versions of each file have been
* created the diff callback is invoked to display the difference between
* the two files.
*/
#include "svn_client.h"
#include "svn_wc.h"
#include "svn_pools.h"
#include "svn_path.h"
#include "client.h"
/* Overall crawler editor baton.
*/
struct edit_baton {
/* TARGET is a working-copy directory which corresponds to the base
URL open in RA_SESSION below. */
svn_stringbuf_t *target;
/* The callback and calback argument that implement the file comparison
function */
const svn_diff_callbacks_t *diff_callbacks;
void *diff_cmd_baton;
/* Flags whether to diff recursively or not. If set the diff is
recursive. */
svn_boolean_t recurse;
/* RA_LIB is the vtable for making requests to the RA layer, RA_SESSION
is the open session for these requests */
svn_ra_plugin_t *ra_lib;
void *ra_session;
/* The rev1 from the '-r Rev1:Rev2' command line option */
svn_revnum_t revision;
/* The rev2 from the '-r Rev1:Rev2' option, specifically set by
set_target_revision(). */
svn_revnum_t target_revision;
/* A temporary empty file. Used for add/delete differences. This is
cached here so that it can be reused, all empty files are the same. */
svn_stringbuf_t *empty_file;
apr_pool_t *pool;
};
/* Directory level baton.
*/
struct dir_baton {
/* Gets set if the directory is added rather than replaced/unchanged. */
svn_boolean_t added;
/* The path of the directory within the repository */
const char *path;
/* The baton for the parent directory, or null if this is the root of the
hierarchy to be compared. */
struct dir_baton *dir_baton;
/* The overall crawler editor baton. */
struct edit_baton *edit_baton;
/* A cache of any property changes (svn_prop_t) received for this dir. */
apr_array_header_t *propchanges;
/* The pool passed in by add_dir, open_dir, or open_root.
Also, the pool this dir baton is allocated in. */
apr_pool_t *pool;
};
/* File level baton.
*/
struct file_baton {
/* Gets set if the file is added rather than replaced. */
svn_boolean_t added;
/* The path of the file within the repository */
const char *path;
/* The path and APR file handle to the temporary file that contains the
first repository version */
svn_stringbuf_t *path_start_revision;
apr_file_t *file_start_revision;
/* The path and APR file handle to the temporary file that contains the
second repository version. These fields are set when processing
textdelta and file deletion, and will be NULL if there's no
textual difference between the two revisions. */
svn_stringbuf_t *path_end_revision;
apr_file_t *file_end_revision;
/* APPLY_HANDLER/APPLY_BATON represent the delta applcation baton. */
svn_txdelta_window_handler_t apply_handler;
void *apply_baton;
/* The overall crawler editor baton. */
struct edit_baton *edit_baton;
/* A cache of any property changes (svn_prop_t) received for this file. */
apr_array_header_t *propchanges;
/* The pool passed in by add_file or open_file.
Also, the pool this file_baton is allocated in. */
apr_pool_t *pool;
};
/* Data used by the apr pool temp file cleanup handler */
struct temp_file_cleanup_s {
/* The path to the file to be deleted */
svn_stringbuf_t *path;
/* The pool to which the deletion of the file is linked. */
apr_pool_t *pool;
};
/* Create a new directory baton for PATH in POOL. ADDED is set if
* this directory is being added rather than replaced. 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. EDIT_BATON is the overall
* crawler editor baton.
*/
static struct dir_baton *
make_dir_baton (const char *path,
struct dir_baton *parent_baton,
svn_boolean_t added,
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 = parent_baton->edit_baton;
dir_baton->added = added;
dir_baton->pool = pool;
dir_baton->path = apr_pstrdup (pool, path);
dir_baton->propchanges = apr_array_make (pool, 1, sizeof (svn_prop_t));
return dir_baton;
}
/* Create a new file baton for PATH in POOL, which is a child of
* directory PARENT_PATH. ADDED is set if this file is being added
* rather than replaced. EDIT_BATON is a pointer to the global edit
* baton.
*/
static struct file_baton *
make_file_baton (const char *path,
svn_boolean_t added,
void *edit_baton,
apr_pool_t *pool)
{
struct file_baton *file_baton = apr_pcalloc (pool, sizeof (*file_baton));
file_baton->edit_baton = edit_baton;
file_baton->added = added;
file_baton->pool = pool;
file_baton->path = apr_pstrdup (pool, path);
file_baton->propchanges = apr_array_make (pool, 1, sizeof (svn_prop_t));
return file_baton;
}
/* An apr pool cleanup handler, this deletes one of the temporary files.
*/
static apr_status_t
temp_file_plain_cleanup_handler (void *arg)
{
struct temp_file_cleanup_s *s = arg;
return apr_file_remove (s->path->data, s->pool);
}
/* An apr pool cleanup handler, this removes a cleanup handler.
*/
static apr_status_t
temp_file_child_cleanup_handler (void *arg)
{
struct temp_file_cleanup_s *s = arg;
apr_pool_cleanup_kill (s->pool, s, temp_file_plain_cleanup_handler);
return APR_SUCCESS;
}
/* Register a pool cleanup to delete PATH when POOL is destroyed.
*
* The main "gotcha" is that if the process forks a child by calling
* apr_proc_create, then the child's copy of the cleanup handler will run
* and delete the file while the parent still expects it to be around. To
* avoid this a child cleanup handler is also installed to kill the plain
* cleanup handler in the child.
*
* ### TODO: This a candidate to be a general utility function.
*/
static svn_error_t *
temp_file_cleanup_register (svn_stringbuf_t *path,
apr_pool_t *pool)
{
struct temp_file_cleanup_s *s = apr_palloc (pool, sizeof (*s));
s->path = path;
s->pool = pool;
apr_pool_cleanup_register (s->pool, s, temp_file_plain_cleanup_handler,
temp_file_child_cleanup_handler);
return SVN_NO_ERROR;
}
/* Get the repository version of a file. This makes an RA request to
* retrieve the file contents. A pool cleanup handler is installed to
* delete this file.
*
* ### TODO: The editor calls this function to get REV1 of the file. Can we
* get the file props as well? Then get_wc_prop() could return them later
* on enabling the REV1:REV2 request to send diffs.
*/
static svn_error_t *
get_file_from_ra (struct file_baton *b)
{
apr_status_t status;
apr_file_t *file;
svn_stream_t *fstream;
/* ### TODO: Need some apr temp file support */
SVN_ERR (svn_io_open_unique_file (&file, &b->path_start_revision,
"tmp", "", FALSE, b->pool));
/* Install a pool cleanup handler to delete the file */
SVN_ERR (temp_file_cleanup_register (b->path_start_revision, b->pool));
fstream = svn_stream_from_aprfile (file, b->pool);
SVN_ERR (b->edit_baton->ra_lib->get_file (b->edit_baton->ra_session,
b->path,
b->edit_baton->revision,
fstream, NULL, NULL));
status = apr_file_close (file);
if (status)
return svn_error_createf (status, 0, NULL, b->pool,
"failed to close file '%s'",
b->path_start_revision->data);
return SVN_NO_ERROR;
}
/* Create an empty file, the path to the file is returned in EMPTY_FILE
*/
static svn_error_t *
create_empty_file (svn_stringbuf_t **empty_file,
apr_pool_t *pool)
{
apr_status_t status;
apr_file_t *file;
/* ### TODO: Need some apr temp file support */
SVN_ERR (svn_io_open_unique_file (&file, empty_file, "tmp", "", FALSE,
pool));
status = apr_file_close (file);
if (status)
return svn_error_createf (status, 0, NULL, pool,
"failed to create empty file '%s'",
(*empty_file)->data);
return SVN_NO_ERROR;
}
/* Get the empty file associated with the edit baton. This is cached so
* that it can be reused, all empty files are the same.
*/
static svn_error_t *
get_empty_file (struct edit_baton *b,
svn_stringbuf_t **empty_file)
{
/* Create the file if it does not exist */
if (!b->empty_file)
{
SVN_ERR (create_empty_file (&b->empty_file, b->pool));
/* Install a pool cleanup handler to delete the file */
SVN_ERR (temp_file_cleanup_register (b->empty_file, b->pool));
}
*empty_file = b->empty_file;
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)
{
struct edit_baton *eb = edit_baton;
eb->target_revision = target_revision;
return SVN_NO_ERROR;
}
/* An svn_delta_edit_fns_t editor function. The root of the comparison
* hierarchy
*/
static svn_error_t *
open_root (void *edit_baton,
svn_revnum_t base_revision,
apr_pool_t *pool,
void **root_baton)
{
struct edit_baton *eb = edit_baton;
struct dir_baton *dir_baton = apr_pcalloc (pool, sizeof (*dir_baton));
dir_baton->dir_baton = NULL;
dir_baton->edit_baton = eb;
dir_baton->added = FALSE;
dir_baton->pool = pool;
dir_baton->path = eb->target ? apr_pstrdup (pool, eb->target->data) : "";
dir_baton->propchanges = apr_array_make (pool, 1, sizeof (svn_prop_t));
*root_baton = dir_baton;
return SVN_NO_ERROR;
}
/* An svn_delta_edit_fns_t editor function.
*/
static svn_error_t *
delete_entry (const char *path,
svn_revnum_t base_revision,
void *parent_baton,
apr_pool_t *pool)
{
struct dir_baton *pb = parent_baton;
svn_node_kind_t kind;
/* We need to know if this is a directory or a file */
/* ### over ra_dav, this breaks if PATH doesn't exist in HEAD; see
issue #581. Obviously, this kind of misses the point of
passing in a revision. :-) */
SVN_ERR (pb->edit_baton->ra_lib->check_path (&kind,
pb->edit_baton->ra_session,
path,
pb->edit_baton->revision));
switch (kind)
{
case svn_node_file:
{
/* Compare a file being deleted against an empty file */
struct file_baton *b = make_file_baton (path,
FALSE,
pb->edit_baton,
pool);
SVN_ERR (get_file_from_ra (b));
SVN_ERR (get_empty_file(b->edit_baton, &b->path_end_revision));
SVN_ERR (pb->edit_baton->diff_callbacks->file_deleted
(b->path,
b->path_start_revision->data,
b->path_end_revision->data,
b->edit_baton->diff_cmd_baton));
break;
}
case svn_node_dir:
{
SVN_ERR (pb->edit_baton->diff_callbacks->dir_deleted
(pb->path,
pb->edit_baton->diff_cmd_baton));
break;
}
default:
break;
}
return SVN_NO_ERROR;
}
/* An svn_delta_edit_fns_t editor function.
*/
static svn_error_t *
add_directory (const char *path,
void *parent_baton,
const char *copyfrom_path,
svn_revnum_t copyfrom_revision,
apr_pool_t *pool,
void **child_baton)
{
struct dir_baton *pb = parent_baton;
struct dir_baton *b;
/* ### TODO: support copyfrom? */
b = make_dir_baton (path, pb, TRUE, pool);
*child_baton = b;
SVN_ERR (pb->edit_baton->diff_callbacks->dir_added
(path,
pb->edit_baton->diff_cmd_baton));
return SVN_NO_ERROR;
}
/* An svn_delta_edit_fns_t editor function.
*/
static svn_error_t *
open_directory (const char *path,
void *parent_baton,
svn_revnum_t base_revision,
apr_pool_t *pool,
void **child_baton)
{
struct dir_baton *pb = parent_baton;
struct dir_baton *b;
b = make_dir_baton (path, pb, FALSE, pool);
*child_baton = b;
return SVN_NO_ERROR;
}
/* An svn_delta_edit_fns_t editor function.
*/
static svn_error_t *
add_file (const char *path,
void *parent_baton,
const char *copyfrom_path,
svn_revnum_t copyfrom_revision,
apr_pool_t *pool,
void **file_baton)
{
struct dir_baton *pb = parent_baton;
struct file_baton *b;
/* ### TODO: support copyfrom? */
b = make_file_baton (path, TRUE, pb->edit_baton, pool);
*file_baton = b;
SVN_ERR (get_empty_file (b->edit_baton, &b->path_start_revision));
return SVN_NO_ERROR;
}
/* An svn_delta_edit_fns_t editor function.
*/
static svn_error_t *
open_file (const char *path,
void *parent_baton,
svn_revnum_t base_revision,
apr_pool_t *pool,
void **file_baton)
{
struct dir_baton *pb = parent_baton;
struct file_baton *b;
b = make_file_baton (path, FALSE, pb->edit_baton, pool);
*file_baton = b;
SVN_ERR (get_file_from_ra (b));
return SVN_NO_ERROR;
}
/* An svn_delta_edit_fns_t editor function. Do the work of applying the
* text delta.
*/
static svn_error_t *
window_handler (svn_txdelta_window_t *window,
void *window_baton)
{
struct file_baton *b = window_baton;
SVN_ERR (b->apply_handler (window, b->apply_baton));
if (!window)
{
apr_status_t status;
status = apr_file_close (b->file_start_revision);
if (status)
return svn_error_createf (status, 0, NULL, b->pool,
"failed to close file '%s'",
b->path_start_revision->data);
status = apr_file_close (b->file_end_revision);
if (status)
return svn_error_createf (status, 0, NULL, b->pool,
"failed to close file '%s'",
b->path_end_revision->data);
}
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;
apr_status_t status;
/* Open the file to be used as the base for second revision */
status = apr_file_open (&b->file_start_revision, b->path_start_revision->data,
APR_READ, APR_OS_DEFAULT, b->pool);
if (status)
return svn_error_createf (status, 0, NULL, b->pool,
"failed to open file '%s'",
b->path_start_revision->data);
/* Open the file that will become the second revision after applying the
text delta, it starts empty */
SVN_ERR (create_empty_file (&b->path_end_revision, b->pool));
SVN_ERR (temp_file_cleanup_register (b->path_end_revision, b->pool));
status = apr_file_open (&b->file_end_revision, b->path_end_revision->data,
APR_WRITE, APR_OS_DEFAULT, b->pool);
if (status)
return svn_error_createf (status, 0, NULL, b->pool,
"failed to open file '%s'",
b->path_end_revision->data);
svn_txdelta_apply (svn_stream_from_aprfile (b->file_start_revision, b->pool),
svn_stream_from_aprfile (b->file_end_revision, 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.
*/
static svn_error_t *
close_file (void *file_baton)
{
struct file_baton *b = file_baton;
struct edit_baton *eb = b->edit_baton;
if (b->path_end_revision)
{
if (b->added)
SVN_ERR (eb->diff_callbacks->file_added
(b->path,
b->path_start_revision->data,
b->path_end_revision->data,
b->edit_baton->diff_cmd_baton));
else
SVN_ERR (eb->diff_callbacks->file_changed
(b->path,
b->path_start_revision->data,
b->path_end_revision->data,
b->edit_baton->revision,
b->edit_baton->target_revision,
b->edit_baton->diff_cmd_baton));
}
if (b->propchanges->nelts > 0)
{
SVN_ERR (eb->diff_callbacks->props_changed
(b->path,
b->propchanges,
b->edit_baton->diff_cmd_baton));
}
return SVN_NO_ERROR;
}
/* An svn_delta_edit_fns_t editor function.
*/
static svn_error_t *
close_directory (void *dir_baton)
{
struct dir_baton *b = dir_baton;
struct edit_baton *eb = b->edit_baton;
if (b->propchanges->nelts > 0)
{
SVN_ERR (eb->diff_callbacks->props_changed
(b->path,
b->propchanges,
b->edit_baton->diff_cmd_baton));
}
return SVN_NO_ERROR;
}
/* An svn_delta_edit_fns_t editor function.
*/
static svn_error_t *
change_file_prop (void *file_baton,
const char *name,
const svn_string_t *value,
apr_pool_t *pool)
{
struct file_baton *b = file_baton;
svn_prop_t *propchange;
propchange = apr_array_push (b->propchanges);
propchange->name = apr_pstrdup (b->pool, name);
propchange->value = value ? svn_string_dup (value, b->pool) : NULL;
return SVN_NO_ERROR;
}
/* An svn_delta_edit_fns_t editor function.
*/
static svn_error_t *
change_dir_prop (void *dir_baton,
const char *name,
const svn_string_t *value,
apr_pool_t *pool)
{
struct dir_baton *db = dir_baton;
svn_prop_t *propchange;
propchange = apr_array_push (db->propchanges);
propchange->name = apr_pstrdup (db->pool, name);
propchange->value = value ? svn_string_dup (value, db->pool) : NULL;
return SVN_NO_ERROR;
}
/* An svn_delta_edit_fns_t editor function.
*/
static svn_error_t *
close_edit (void *edit_baton)
{
struct edit_baton *eb = edit_baton;
svn_pool_destroy (eb->pool);
return SVN_NO_ERROR;
}
/* Create a repository diff editor and baton.
*/
svn_error_t *
svn_client__get_diff_editor (svn_stringbuf_t *target,
const svn_diff_callbacks_t *diff_callbacks,
void *diff_cmd_baton,
svn_boolean_t recurse,
svn_ra_plugin_t *ra_lib,
void *ra_session,
svn_revnum_t revision,
const svn_delta_editor_t **editor,
void **edit_baton,
apr_pool_t *pool)
{
apr_pool_t *subpool = svn_pool_create (pool);
svn_delta_editor_t *tree_editor = svn_delta_default_editor (subpool);
struct edit_baton *eb = apr_palloc (subpool, sizeof (*eb));
eb->target = target;
eb->diff_callbacks = diff_callbacks;
eb->diff_cmd_baton = diff_cmd_baton;
eb->recurse = recurse;
eb->ra_lib = ra_lib;
eb->ra_session = ra_session;
eb->revision = revision;
eb->empty_file = NULL;
eb->pool = subpool;
tree_editor->set_target_revision = set_target_revision;
tree_editor->open_root = open_root;
tree_editor->delete_entry = delete_entry;
tree_editor->add_directory = add_directory;
tree_editor->open_directory = open_directory;
tree_editor->add_file = add_file;
tree_editor->open_file = open_file;
tree_editor->apply_textdelta = apply_textdelta;
tree_editor->close_file = close_file;
tree_editor->close_directory = close_directory;
tree_editor->change_file_prop = change_file_prop;
tree_editor->change_dir_prop = change_dir_prop;
tree_editor->close_edit = close_edit;
*edit_baton = eb;
*editor = tree_editor;
return SVN_NO_ERROR;
}
/*
* local variables:
* eval: (load-file "../../tools/dev/svn-dev.el")
* end: */