blob: d01b790d34b593288542a5d045a37797ff2c7258 [file] [log] [blame]
/*
* diff.c: comparing
*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*/
/* ==================================================================== */
/* We define this here to remove any further warnings about the usage of
experimental functions in this file. */
#define SVN_EXPERIMENTAL
/*** Includes. ***/
#include <apr_strings.h>
#include <apr_pools.h>
#include <apr_hash.h>
#include "svn_types.h"
#include "svn_hash.h"
#include "svn_wc.h"
#include "svn_diff.h"
#include "svn_mergeinfo.h"
#include "svn_client.h"
#include "svn_string.h"
#include "svn_error.h"
#include "svn_dirent_uri.h"
#include "svn_path.h"
#include "svn_io.h"
#include "svn_utf.h"
#include "svn_pools.h"
#include "svn_config.h"
#include "svn_props.h"
#include "svn_subst.h"
#include "client.h"
#include "private/svn_client_shelf.h"
#include "private/svn_wc_private.h"
#include "private/svn_diff_private.h"
#include "private/svn_subr_private.h"
#include "private/svn_io_private.h"
#include "private/svn_ra_private.h"
#include "svn_private_config.h"
/* Utilities */
#define DIFF_REVNUM_NONEXISTENT ((svn_revnum_t) -100)
#define MAKE_ERR_BAD_RELATIVE_PATH(path, relative_to_dir) \
svn_error_createf(SVN_ERR_BAD_RELATIVE_PATH, NULL, \
_("Path '%s' must be an immediate child of " \
"the directory '%s'"), path, relative_to_dir)
/* State provided by the diff drivers; used by the diff writer */
typedef struct diff_driver_info_t
{
/* The anchor to prefix before wc paths */
const char *anchor;
/* Relative path of ra session from repos_root_url.
Used only in printing git diff headers. The repository-root-relative
path of ... ### what user-visible property of the diff? */
const char *session_relpath;
/* Used only in printing git diff headers. Used to find the
repository-root-relative path of a WC path. */
svn_wc_context_t *wc_ctx;
/* The original targets passed to the diff command. We may need
these to construct distinctive diff labels when comparing the
same relative path in the same revision, under different anchors
(for example, when comparing a trunk against a branch). */
const char *orig_path_1;
const char *orig_path_2;
} diff_driver_info_t;
/* Calculate the repository relative path of DIFF_RELPATH, using
* SESSION_RELPATH and WC_CTX, and return the result in *REPOS_RELPATH.
* ORIG_TARGET is the related original target passed to the diff command,
* and may be used to derive leading path components missing from PATH.
* ANCHOR is the local path where the diff editor is anchored.
* Do all allocations in POOL. */
static svn_error_t *
make_repos_relpath(const char **repos_relpath,
const char *diff_relpath,
const char *orig_target,
const char *session_relpath,
svn_wc_context_t *wc_ctx,
const char *anchor,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
const char *local_abspath;
if (! session_relpath
|| (anchor && !svn_path_is_url(orig_target)))
{
svn_error_t *err;
/* We're doing a WC-WC diff, so we can retrieve all information we
* need from the working copy. */
SVN_ERR(svn_dirent_get_absolute(&local_abspath,
svn_dirent_join(anchor, diff_relpath,
scratch_pool),
scratch_pool));
err = svn_wc__node_get_repos_info(NULL, repos_relpath, NULL, NULL,
wc_ctx, local_abspath,
result_pool, scratch_pool);
if (!session_relpath
|| ! err
|| (err && err->apr_err != SVN_ERR_WC_PATH_NOT_FOUND))
{
return svn_error_trace(err);
}
/* The path represents a local working copy path, but does not
exist. Fall through to calculate an in-repository location
based on the ra session */
/* ### Maybe we should use the nearest existing ancestor instead? */
svn_error_clear(err);
}
*repos_relpath = svn_relpath_join(session_relpath, diff_relpath,
result_pool);
return SVN_NO_ERROR;
}
/* Adjust paths to handle the case when we're dealing with different anchors.
*
* Set *INDEX_PATH to the new relative path. Set *LABEL_PATH1 and
* *LABEL_PATH2 to that path annotated with the unique parts of ORIG_PATH_1
* and ORIG_PATH_2 respectively, like this:
*
* INDEX_PATH: "path"
* LABEL_PATH1: "path\t(.../branches/branch1)"
* LABEL_PATH2: "path\t(.../trunk)"
*
* Make the output paths relative to RELATIVE_TO_DIR (if not null) by
* removing it from the beginning of (ANCHOR + RELPATH).
*
* ANCHOR (if not null) is the local path where the diff editor is anchored.
* RELPATH is the path to the changed node within the diff editor, so
* relative to ANCHOR.
*
* RELATIVE_TO_DIR and ANCHOR are of the same form -- either absolute local
* paths or relative paths relative to the same base.
*
* ORIG_PATH_1 and ORIG_PATH_2 represent the two original target paths or
* URLs passed to the diff command.
*
* Allocate results in RESULT_POOL (or as a pointer to RELPATH) and
* temporary data in SCRATCH_POOL.
*/
static svn_error_t *
adjust_paths_for_diff_labels(const char **index_path,
const char **label_path1,
const char **label_path2,
const char *relative_to_dir,
const char *anchor,
const char *relpath,
const char *orig_path_1,
const char *orig_path_2,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
const char *new_path = relpath;
const char *new_path1 = orig_path_1;
const char *new_path2 = orig_path_2;
if (anchor)
new_path = svn_dirent_join(anchor, new_path, result_pool);
if (relative_to_dir)
{
/* Possibly adjust the paths shown in the output (see issue #2723). */
const char *child_path = svn_dirent_is_child(relative_to_dir, new_path,
result_pool);
if (child_path)
new_path = child_path;
else if (! strcmp(relative_to_dir, new_path))
new_path = ".";
else
return MAKE_ERR_BAD_RELATIVE_PATH(new_path, relative_to_dir);
}
{
apr_size_t len;
svn_boolean_t is_url1;
svn_boolean_t is_url2;
/* ### Holy cow. Due to anchor/target weirdness, we can't
simply join dwi->orig_path_1 with path, ditto for
orig_path_2. That will work when they're directory URLs, but
not for file URLs. Nor can we just use anchor1 and anchor2
from do_diff(), at least not without some more logic here.
What a nightmare.
For now, to distinguish the two paths, we'll just put the
unique portions of the original targets in parentheses after
the received path, with ellipses for handwaving. This makes
the labels a bit clumsy, but at least distinctive. Better
solutions are possible, they'll just take more thought. */
/* ### BH: We can now just construct the repos_relpath, etc. as the
anchor is available. See also make_repos_relpath() */
/* Remove the common prefix of NEW_PATH1 and NEW_PATH2. */
is_url1 = svn_path_is_url(new_path1);
is_url2 = svn_path_is_url(new_path2);
if (is_url1 && is_url2)
len = strlen(svn_uri_get_longest_ancestor(new_path1, new_path2,
scratch_pool));
else if (!is_url1 && !is_url2)
len = strlen(svn_dirent_get_longest_ancestor(new_path1, new_path2,
scratch_pool));
else
len = 0; /* Path and URL */
new_path1 += len;
new_path2 += len;
}
/* ### Should diff labels print paths in local style? Is there
already a standard for this? In any case, this code depends on
a particular style, so not calling svn_dirent_local_style() on the
paths below.*/
if (new_path[0] == '\0')
new_path = ".";
if (new_path1[0] == '\0')
new_path1 = new_path;
else if (svn_path_is_url(new_path1))
new_path1 = apr_psprintf(result_pool, "%s\t(%s)", new_path, new_path1);
else if (new_path1[0] == '/')
new_path1 = apr_psprintf(result_pool, "%s\t(...%s)", new_path, new_path1);
else
new_path1 = apr_psprintf(result_pool, "%s\t(.../%s)", new_path, new_path1);
if (new_path2[0] == '\0')
new_path2 = new_path;
else if (svn_path_is_url(new_path2))
new_path2 = apr_psprintf(result_pool, "%s\t(%s)", new_path, new_path2);
else if (new_path2[0] == '/')
new_path2 = apr_psprintf(result_pool, "%s\t(...%s)", new_path, new_path2);
else
new_path2 = apr_psprintf(result_pool, "%s\t(.../%s)", new_path, new_path2);
*index_path = new_path;
*label_path1 = new_path1;
*label_path2 = new_path2;
return SVN_NO_ERROR;
}
/* Generate a label for the diff output for file PATH at revision REVNUM.
If REVNUM is invalid then it is assumed to be the current working
copy. Assumes the paths are already in the desired style (local
vs internal). Allocate the label in RESULT-POOL. */
static const char *
diff_label(const char *path,
svn_revnum_t revnum,
apr_pool_t *result_pool)
{
const char *label;
if (revnum >= 0)
label = apr_psprintf(result_pool, _("%s\t(revision %ld)"), path, revnum);
else if (revnum == DIFF_REVNUM_NONEXISTENT)
label = apr_psprintf(result_pool, _("%s\t(nonexistent)"), path);
else /* SVN_INVALID_REVNUM */
label = apr_psprintf(result_pool, _("%s\t(working copy)"), path);
return label;
}
/* Standard modes produced in git style diffs */
static const int exec_mode = 0755;
static const int noexec_mode = 0644;
static const int kind_file_mode = 0100000;
/*static const kind_dir_mode = 0040000;*/
static const int kind_symlink_mode = 0120000;
/* Print a git diff header for an addition within a diff between PATH1 and
* PATH2 to the stream OS using HEADER_ENCODING. */
static svn_error_t *
print_git_diff_header_added(svn_stream_t *os, const char *header_encoding,
const char *path1, const char *path2,
svn_boolean_t exec_bit,
svn_boolean_t symlink_bit,
apr_pool_t *scratch_pool)
{
int new_mode = (exec_bit ? exec_mode : noexec_mode)
| (symlink_bit ? kind_symlink_mode : kind_file_mode);
SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool,
"diff --git a/%s b/%s%s",
path1, path2, APR_EOL_STR));
SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool,
"new file mode %06o" APR_EOL_STR,
new_mode));
return SVN_NO_ERROR;
}
/* Print a git diff header for a deletion within a diff between PATH1 and
* PATH2 to the stream OS using HEADER_ENCODING. */
static svn_error_t *
print_git_diff_header_deleted(svn_stream_t *os, const char *header_encoding,
const char *path1, const char *path2,
svn_boolean_t exec_bit,
svn_boolean_t symlink_bit,
apr_pool_t *scratch_pool)
{
int old_mode = (exec_bit ? exec_mode : noexec_mode)
| (symlink_bit ? kind_symlink_mode : kind_file_mode);
SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool,
"diff --git a/%s b/%s%s",
path1, path2, APR_EOL_STR));
SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool,
"deleted file mode %06o" APR_EOL_STR,
old_mode));
return SVN_NO_ERROR;
}
/* Print a git diff header for a copy from COPYFROM_PATH to PATH to the stream
* OS using HEADER_ENCODING. */
static svn_error_t *
print_git_diff_header_copied(svn_stream_t *os, const char *header_encoding,
const char *copyfrom_path,
svn_revnum_t copyfrom_rev,
const char *path,
apr_pool_t *scratch_pool)
{
SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool,
"diff --git a/%s b/%s%s",
copyfrom_path, path, APR_EOL_STR));
if (copyfrom_rev != SVN_INVALID_REVNUM)
SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool,
"copy from %s@%ld%s", copyfrom_path,
copyfrom_rev, APR_EOL_STR));
else
SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool,
"copy from %s%s", copyfrom_path,
APR_EOL_STR));
SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool,
"copy to %s%s", path, APR_EOL_STR));
return SVN_NO_ERROR;
}
/* Print a git diff header for a rename from COPYFROM_PATH to PATH to the
* stream OS using HEADER_ENCODING. */
static svn_error_t *
print_git_diff_header_renamed(svn_stream_t *os, const char *header_encoding,
const char *copyfrom_path, const char *path,
apr_pool_t *scratch_pool)
{
SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool,
"diff --git a/%s b/%s%s",
copyfrom_path, path, APR_EOL_STR));
SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool,
"rename from %s%s", copyfrom_path,
APR_EOL_STR));
SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool,
"rename to %s%s", path, APR_EOL_STR));
return SVN_NO_ERROR;
}
/* Print a git diff header for a modification within a diff between PATH1 and
* PATH2 to the stream OS using HEADER_ENCODING. */
static svn_error_t *
print_git_diff_header_modified(svn_stream_t *os, const char *header_encoding,
const char *path1, const char *path2,
apr_pool_t *scratch_pool)
{
SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool,
"diff --git a/%s b/%s%s",
path1, path2, APR_EOL_STR));
return SVN_NO_ERROR;
}
/* Helper function for print_git_diff_header */
static svn_error_t *
maybe_print_mode_change(svn_stream_t *os,
const char *header_encoding,
svn_boolean_t exec_bit1,
svn_boolean_t exec_bit2,
svn_boolean_t symlink_bit1,
svn_boolean_t symlink_bit2,
const char *git_index_shas,
apr_pool_t *scratch_pool)
{
int old_mode = (exec_bit1 ? exec_mode : noexec_mode)
| (symlink_bit1 ? kind_symlink_mode : kind_file_mode);
int new_mode = (exec_bit2 ? exec_mode : noexec_mode)
| (symlink_bit2 ? kind_symlink_mode : kind_file_mode);
if (old_mode == new_mode)
{
if (git_index_shas)
SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool,
"index %s %06o" APR_EOL_STR,
git_index_shas, old_mode));
return SVN_NO_ERROR;
}
SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool,
"old mode %06o" APR_EOL_STR, old_mode));
SVN_ERR(svn_stream_printf_from_utf8(os, header_encoding, scratch_pool,
"new mode %06o" APR_EOL_STR, new_mode));
return SVN_NO_ERROR;
}
/* Print a git diff header showing the OPERATION to the stream OS using
* HEADER_ENCODING.
*
* Return suitable diff labels for the git diff in *LABEL1 and *LABEL2.
*
* REV1 and REV2 are the revisions being diffed.
* COPYFROM_PATH and COPYFROM_REV indicate where the
* diffed item was copied from.
* Use SCRATCH_POOL for temporary allocations. */
static svn_error_t *
print_git_diff_header(svn_stream_t *os,
const char **label1, const char **label2,
svn_diff_operation_kind_t operation,
svn_revnum_t rev1,
svn_revnum_t rev2,
const char *diff_relpath,
const char *copyfrom_path,
svn_revnum_t copyfrom_rev,
apr_hash_t *left_props,
apr_hash_t *right_props,
const char *git_index_shas,
const char *header_encoding,
const diff_driver_info_t *ddi,
apr_pool_t *scratch_pool)
{
const char *repos_relpath1;
const char *repos_relpath2;
const char *copyfrom_repos_relpath = NULL;
svn_boolean_t exec_bit1 = (svn_prop_get_value(left_props,
SVN_PROP_EXECUTABLE) != NULL);
svn_boolean_t exec_bit2 = (svn_prop_get_value(right_props,
SVN_PROP_EXECUTABLE) != NULL);
svn_boolean_t symlink_bit1 = (svn_prop_get_value(left_props,
SVN_PROP_SPECIAL) != NULL);
svn_boolean_t symlink_bit2 = (svn_prop_get_value(right_props,
SVN_PROP_SPECIAL) != NULL);
SVN_ERR(make_repos_relpath(&repos_relpath1, diff_relpath,
ddi->orig_path_1,
ddi->session_relpath,
ddi->wc_ctx,
ddi->anchor,
scratch_pool, scratch_pool));
SVN_ERR(make_repos_relpath(&repos_relpath2, diff_relpath,
ddi->orig_path_2,
ddi->session_relpath,
ddi->wc_ctx,
ddi->anchor,
scratch_pool, scratch_pool));
if (copyfrom_path)
SVN_ERR(make_repos_relpath(&copyfrom_repos_relpath, copyfrom_path,
ddi->orig_path_2,
ddi->session_relpath,
ddi->wc_ctx,
ddi->anchor,
scratch_pool, scratch_pool));
if (operation == svn_diff_op_deleted)
{
SVN_ERR(print_git_diff_header_deleted(os, header_encoding,
repos_relpath1, repos_relpath2,
exec_bit1, symlink_bit1,
scratch_pool));
*label1 = diff_label(apr_psprintf(scratch_pool, "a/%s", repos_relpath1),
rev1, scratch_pool);
*label2 = diff_label(apr_psprintf(scratch_pool, "b/%s", repos_relpath2),
rev2, scratch_pool);
}
else if (operation == svn_diff_op_copied)
{
SVN_ERR(print_git_diff_header_copied(os, header_encoding,
copyfrom_path, copyfrom_rev,
repos_relpath2,
scratch_pool));
*label1 = diff_label(apr_psprintf(scratch_pool, "a/%s", copyfrom_path),
rev1, scratch_pool);
*label2 = diff_label(apr_psprintf(scratch_pool, "b/%s", repos_relpath2),
rev2, scratch_pool);
SVN_ERR(maybe_print_mode_change(os, header_encoding,
exec_bit1, exec_bit2,
symlink_bit1, symlink_bit2,
git_index_shas,
scratch_pool));
}
else if (operation == svn_diff_op_added)
{
SVN_ERR(print_git_diff_header_added(os, header_encoding,
repos_relpath1, repos_relpath2,
exec_bit2, symlink_bit2,
scratch_pool));
*label1 = diff_label(apr_psprintf(scratch_pool, "a/%s", repos_relpath1),
rev1, scratch_pool);
*label2 = diff_label(apr_psprintf(scratch_pool, "b/%s", repos_relpath2),
rev2, scratch_pool);
}
else if (operation == svn_diff_op_modified)
{
SVN_ERR(print_git_diff_header_modified(os, header_encoding,
repos_relpath1, repos_relpath2,
scratch_pool));
*label1 = diff_label(apr_psprintf(scratch_pool, "a/%s", repos_relpath1),
rev1, scratch_pool);
*label2 = diff_label(apr_psprintf(scratch_pool, "b/%s", repos_relpath2),
rev2, scratch_pool);
SVN_ERR(maybe_print_mode_change(os, header_encoding,
exec_bit1, exec_bit2,
symlink_bit1, symlink_bit2,
git_index_shas,
scratch_pool));
}
else if (operation == svn_diff_op_moved)
{
SVN_ERR(print_git_diff_header_renamed(os, header_encoding,
copyfrom_path, repos_relpath2,
scratch_pool));
*label1 = diff_label(apr_psprintf(scratch_pool, "a/%s", copyfrom_path),
rev1, scratch_pool);
*label2 = diff_label(apr_psprintf(scratch_pool, "b/%s", repos_relpath2),
rev2, scratch_pool);
SVN_ERR(maybe_print_mode_change(os, header_encoding,
exec_bit1, exec_bit2,
symlink_bit1, symlink_bit2,
git_index_shas,
scratch_pool));
}
return SVN_NO_ERROR;
}
/* Print the "Index:" and "=====" lines.
* Show the paths in platform-independent format ('/' separators)
*/
static svn_error_t *
print_diff_index_header(svn_stream_t *outstream,
const char *header_encoding,
const char *index_path,
const char *suffix,
apr_pool_t *scratch_pool)
{
SVN_ERR(svn_stream_printf_from_utf8(outstream,
header_encoding, scratch_pool,
"Index: %s%s" APR_EOL_STR
SVN_DIFF__EQUAL_STRING APR_EOL_STR,
index_path, suffix));
return SVN_NO_ERROR;
}
/* A helper func that writes out verbal descriptions of property diffs
to OUTSTREAM. Of course, OUTSTREAM will probably be whatever was
passed to svn_client_diff7(), which is probably stdout.
### FIXME needs proper docstring
If USE_GIT_DIFF_FORMAT is TRUE, print git diff headers, which always
show paths relative to the repository root. DDI->session_relpath and
DDI->wc_ctx are needed to normalize paths relative the repository root,
and are ignored if USE_GIT_DIFF_FORMAT is FALSE.
If @a pretty_print_mergeinfo is true, then describe 'svn:mergeinfo'
property changes in a human-readable form that says what changes were
merged or reverse merged; otherwise (or if the mergeinfo property values
don't parse correctly) display them just like any other property.
*/
static svn_error_t *
display_prop_diffs(const apr_array_header_t *propchanges,
apr_hash_t *left_props,
apr_hash_t *right_props,
const char *diff_relpath,
svn_revnum_t rev1,
svn_revnum_t rev2,
const char *encoding,
svn_stream_t *outstream,
const char *relative_to_dir,
svn_boolean_t show_diff_header,
svn_boolean_t use_git_diff_format,
svn_boolean_t pretty_print_mergeinfo,
const diff_driver_info_t *ddi,
svn_cancel_func_t cancel_func,
void *cancel_baton,
apr_pool_t *scratch_pool)
{
const char *repos_relpath1 = NULL;
const char *index_path;
const char *label_path1, *label_path2;
if (use_git_diff_format)
{
SVN_ERR(make_repos_relpath(&repos_relpath1, diff_relpath, ddi->orig_path_1,
ddi->session_relpath, ddi->wc_ctx, ddi->anchor,
scratch_pool, scratch_pool));
}
/* If we're creating a diff on the wc root, path would be empty. */
SVN_ERR(adjust_paths_for_diff_labels(&index_path,
&label_path1, &label_path2,
relative_to_dir, ddi->anchor,
diff_relpath,
ddi->orig_path_1, ddi->orig_path_2,
scratch_pool, scratch_pool));
if (show_diff_header)
{
const char *label1;
const char *label2;
label1 = diff_label(label_path1, rev1, scratch_pool);
label2 = diff_label(label_path2, rev2, scratch_pool);
SVN_ERR(print_diff_index_header(outstream, encoding,
index_path, "", scratch_pool));
if (use_git_diff_format)
SVN_ERR(print_git_diff_header(outstream, &label1, &label2,
svn_diff_op_modified,
rev1, rev2,
diff_relpath,
NULL, SVN_INVALID_REVNUM,
left_props, right_props,
NULL,
encoding, ddi, scratch_pool));
/* --- label1
* +++ label2 */
SVN_ERR(svn_diff__unidiff_write_header(
outstream, encoding, label1, label2, scratch_pool));
}
SVN_ERR(svn_stream_printf_from_utf8(outstream, encoding, scratch_pool,
APR_EOL_STR
"Property changes on: %s"
APR_EOL_STR,
use_git_diff_format
? repos_relpath1
: index_path));
SVN_ERR(svn_stream_printf_from_utf8(outstream, encoding, scratch_pool,
SVN_DIFF__UNDER_STRING APR_EOL_STR));
SVN_ERR(svn_diff__display_prop_diffs(
outstream, encoding, propchanges, left_props,
pretty_print_mergeinfo,
-1 /* context_size */,
cancel_func, cancel_baton, scratch_pool));
return SVN_NO_ERROR;
}
/*-----------------------------------------------------------------*/
/*** Callbacks for 'svn diff', invoked by the repos-diff editor. ***/
/* Diff writer state */
typedef struct diff_writer_info_t
{
/* If non-null, the external diff command to invoke. */
const char *diff_cmd;
/* This is allocated in this struct's pool or a higher-up pool. */
union {
/* If 'diff_cmd' is null, then this is the parsed options to
pass to the internal libsvn_diff implementation. */
svn_diff_file_options_t *for_internal;
/* Else if 'diff_cmd' is non-null, then... */
struct {
/* ...this is an argument array for the external command, and */
const char **argv;
/* ...this is the length of argv. */
int argc;
} for_external;
} options;
apr_pool_t *pool;
svn_stream_t *outstream;
svn_stream_t *errstream;
const char *header_encoding;
/* Set this if you want diff output even for binary files. */
svn_boolean_t force_binary;
/* The directory that diff target paths should be considered as
relative to for output generation (see issue #2723). */
const char *relative_to_dir;
/* Whether property differences are ignored. */
svn_boolean_t ignore_properties;
/* Whether to show only property changes. */
svn_boolean_t properties_only;
/* Whether we're producing a git-style diff. */
svn_boolean_t use_git_diff_format;
/* Whether addition of a file is summarized versus showing a full diff. */
svn_boolean_t no_diff_added;
/* Whether deletion of a file is summarized versus showing a full diff. */
svn_boolean_t no_diff_deleted;
/* Whether to ignore copyfrom information when showing adds */
svn_boolean_t show_copies_as_adds;
/* Whether to show mergeinfo prop changes in human-readable form */
svn_boolean_t pretty_print_mergeinfo;
/* Empty files for creating diffs or NULL if not used yet */
const char *empty_file;
svn_cancel_func_t cancel_func;
void *cancel_baton;
struct diff_driver_info_t ddi;
} diff_writer_info_t;
/* An helper for diff_dir_props_changed, diff_file_changed and diff_file_added
*/
static svn_error_t *
diff_props_changed(const char *diff_relpath,
svn_revnum_t rev1,
svn_revnum_t rev2,
const apr_array_header_t *propchanges,
apr_hash_t *left_props,
apr_hash_t *right_props,
svn_boolean_t show_diff_header,
diff_writer_info_t *dwi,
apr_pool_t *scratch_pool)
{
apr_array_header_t *props;
/* If property differences are ignored, there's nothing to do. */
if (dwi->ignore_properties)
return SVN_NO_ERROR;
SVN_ERR(svn_categorize_props(propchanges, NULL, NULL, &props,
scratch_pool));
if (props->nelts > 0)
{
/* We're using the revnums from the dwi since there's
* no revision argument to the svn_wc_diff_callback_t
* dir_props_changed(). */
SVN_ERR(display_prop_diffs(props, left_props, right_props,
diff_relpath,
rev1,
rev2,
dwi->header_encoding,
dwi->outstream,
dwi->relative_to_dir,
show_diff_header,
dwi->use_git_diff_format,
dwi->pretty_print_mergeinfo,
&dwi->ddi,
dwi->cancel_func,
dwi->cancel_baton,
scratch_pool));
}
return SVN_NO_ERROR;
}
/* Given a file ORIG_TMPFILE, return a path to a temporary file that lives at
* least as long as RESULT_POOL, containing the git-like represention of
* ORIG_TMPFILE */
static svn_error_t *
transform_link_to_git(const char **new_tmpfile,
const char **git_sha1,
const char *orig_tmpfile,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
apr_file_t *orig;
apr_file_t *gitlike;
svn_stringbuf_t *line;
*git_sha1 = NULL;
SVN_ERR(svn_io_file_open(&orig, orig_tmpfile, APR_READ, APR_OS_DEFAULT,
scratch_pool));
SVN_ERR(svn_io_open_unique_file3(&gitlike, new_tmpfile, NULL,
svn_io_file_del_on_pool_cleanup,
result_pool, scratch_pool));
SVN_ERR(svn_io_file_readline(orig, &line, NULL, NULL, 2 * APR_PATH_MAX + 2,
scratch_pool, scratch_pool));
if (line->len > 5 && !strncmp(line->data, "link ", 5))
{
const char *sz_str;
svn_checksum_t *checksum;
svn_stringbuf_remove(line, 0, 5);
SVN_ERR(svn_io_file_write_full(gitlike, line->data, line->len,
NULL, scratch_pool));
/* git calculates the sha over "blob X\0" + the actual data,
where X is the decimal size of the blob. */
sz_str = apr_psprintf(scratch_pool, "blob %u", (unsigned int)line->len);
svn_stringbuf_insert(line, 0, sz_str, strlen(sz_str) + 1);
SVN_ERR(svn_checksum(&checksum, svn_checksum_sha1,
line->data, line->len, scratch_pool));
*git_sha1 = svn_checksum_to_cstring(checksum, result_pool);
}
else
{
/* Not a link... so can't convert */
*new_tmpfile = apr_pstrdup(result_pool, orig_tmpfile);
}
SVN_ERR(svn_io_file_close(orig, scratch_pool));
SVN_ERR(svn_io_file_close(gitlike, scratch_pool));
return SVN_NO_ERROR;
}
/* Show differences between TMPFILE1 and TMPFILE2. DIFF_RELPATH, REV1, and
REV2 are used in the headers to indicate the file and revisions.
If either side has an svn:mime-type property that indicates 'binary'
content, then if DWI->force_binary is set, attempt to produce the
diff in the usual way, otherwise produce a 'GIT binary diff' in git mode
or print a warning message in non-git mode.
If FORCE_DIFF is TRUE, always write a diff, even for empty diffs.
Set *WROTE_HEADER to TRUE if a diff header was written */
static svn_error_t *
diff_content_changed(svn_boolean_t *wrote_header,
const char *diff_relpath,
const char *tmpfile1,
const char *tmpfile2,
svn_revnum_t rev1,
svn_revnum_t rev2,
apr_hash_t *left_props,
apr_hash_t *right_props,
svn_diff_operation_kind_t operation,
svn_boolean_t force_diff,
const char *copyfrom_path,
svn_revnum_t copyfrom_rev,
diff_writer_info_t *dwi,
apr_pool_t *scratch_pool)
{
const char *rel_to_dir = dwi->relative_to_dir;
svn_stream_t *outstream = dwi->outstream;
const char *label1, *label2;
svn_boolean_t mt1_binary = FALSE, mt2_binary = FALSE;
const char *index_path;
const char *label_path1, *label_path2;
const char *mimetype1 = svn_prop_get_value(left_props, SVN_PROP_MIME_TYPE);
const char *mimetype2 = svn_prop_get_value(right_props, SVN_PROP_MIME_TYPE);
const char *index_shas = NULL;
/* If only property differences are shown, there's nothing to do. */
if (dwi->properties_only)
return SVN_NO_ERROR;
/* Generate the diff headers. */
SVN_ERR(adjust_paths_for_diff_labels(&index_path,
&label_path1, &label_path2,
rel_to_dir, dwi->ddi.anchor,
diff_relpath,
dwi->ddi.orig_path_1, dwi->ddi.orig_path_2,
scratch_pool, scratch_pool));
label1 = diff_label(label_path1, rev1, scratch_pool);
label2 = diff_label(label_path2, rev2, scratch_pool);
/* Possible easy-out: if either mime-type is binary and force was not
specified, don't attempt to generate a viewable diff at all.
Print a warning and exit. */
if (mimetype1)
mt1_binary = svn_mime_type_is_binary(mimetype1);
if (mimetype2)
mt2_binary = svn_mime_type_is_binary(mimetype2);
if (dwi->use_git_diff_format)
{
const char *l_hash = NULL;
const char *r_hash = NULL;
/* Change symlinks to their 'git like' plain format */
if (svn_prop_get_value(left_props, SVN_PROP_SPECIAL))
SVN_ERR(transform_link_to_git(&tmpfile1, &l_hash, tmpfile1,
scratch_pool, scratch_pool));
if (svn_prop_get_value(right_props, SVN_PROP_SPECIAL))
SVN_ERR(transform_link_to_git(&tmpfile2, &r_hash, tmpfile2,
scratch_pool, scratch_pool));
if (l_hash && r_hash)
{
/* The symlink has changed. But we can't tell the user of the
diff whether we are writing git diffs or svn diffs of the
symlink... except when we add a git-like index line */
l_hash = apr_pstrndup(scratch_pool, l_hash, 8);
r_hash = apr_pstrndup(scratch_pool, r_hash, 8);
index_shas = apr_psprintf(scratch_pool, "%8s..%8s",
l_hash, r_hash);
}
}
if (! dwi->force_binary && (mt1_binary || mt2_binary))
{
/* Print out the diff header. */
SVN_ERR(print_diff_index_header(outstream, dwi->header_encoding,
index_path, "", scratch_pool));
*wrote_header = TRUE;
/* ### Print git diff headers. */
if (dwi->use_git_diff_format)
{
svn_stream_t *left_stream;
svn_stream_t *right_stream;
SVN_ERR(print_git_diff_header(outstream,
&label1, &label2,
operation,
rev1, rev2,
diff_relpath,
copyfrom_path, copyfrom_rev,
left_props, right_props,
index_shas,
dwi->header_encoding,
&dwi->ddi, scratch_pool));
SVN_ERR(svn_stream_open_readonly(&left_stream, tmpfile1,
scratch_pool, scratch_pool));
SVN_ERR(svn_stream_open_readonly(&right_stream, tmpfile2,
scratch_pool, scratch_pool));
SVN_ERR(svn_diff_output_binary(outstream,
left_stream, right_stream,
dwi->cancel_func, dwi->cancel_baton,
scratch_pool));
}
else
{
SVN_ERR(svn_stream_printf_from_utf8(outstream,
dwi->header_encoding, scratch_pool,
_("Cannot display: file marked as a binary type.%s"),
APR_EOL_STR));
if (mt1_binary && !mt2_binary)
SVN_ERR(svn_stream_printf_from_utf8(outstream,
dwi->header_encoding, scratch_pool,
"svn:mime-type = %s" APR_EOL_STR, mimetype1));
else if (mt2_binary && !mt1_binary)
SVN_ERR(svn_stream_printf_from_utf8(outstream,
dwi->header_encoding, scratch_pool,
"svn:mime-type = %s" APR_EOL_STR, mimetype2));
else if (mt1_binary && mt2_binary)
{
if (strcmp(mimetype1, mimetype2) == 0)
SVN_ERR(svn_stream_printf_from_utf8(outstream,
dwi->header_encoding, scratch_pool,
"svn:mime-type = %s" APR_EOL_STR,
mimetype1));
else
SVN_ERR(svn_stream_printf_from_utf8(outstream,
dwi->header_encoding, scratch_pool,
"svn:mime-type = (%s, %s)" APR_EOL_STR,
mimetype1, mimetype2));
}
}
/* Exit early. */
return SVN_NO_ERROR;
}
if (dwi->diff_cmd)
{
svn_stream_t *errstream = dwi->errstream;
apr_file_t *outfile;
apr_file_t *errfile;
const char *outfilename;
const char *errfilename;
svn_stream_t *stream;
int exitcode;
/* Print out the diff header. */
SVN_ERR(print_diff_index_header(outstream, dwi->header_encoding,
index_path, "", scratch_pool));
*wrote_header = TRUE;
/* ### Do we want to add git diff headers here too? I'd say no. The
* ### 'Index' and '===' line is something subversion has added. The rest
* ### is up to the external diff application. We may be dealing with
* ### a non-git compatible diff application.*/
/* We deal in streams, but svn_io_run_diff2() deals in file handles,
so we may need to make temporary files and then copy the contents
to our stream. */
outfile = svn_stream__aprfile(outstream);
if (outfile)
outfilename = NULL;
else
SVN_ERR(svn_io_open_unique_file3(&outfile, &outfilename, NULL,
svn_io_file_del_on_pool_cleanup,
scratch_pool, scratch_pool));
errfile = svn_stream__aprfile(errstream);
if (errfile)
errfilename = NULL;
else
SVN_ERR(svn_io_open_unique_file3(&errfile, &errfilename, NULL,
svn_io_file_del_on_pool_cleanup,
scratch_pool, scratch_pool));
SVN_ERR(svn_io_run_diff2(".",
dwi->options.for_external.argv,
dwi->options.for_external.argc,
label1, label2,
tmpfile1, tmpfile2,
&exitcode, outfile, errfile,
dwi->diff_cmd, scratch_pool));
/* Now, open and copy our files to our output streams. */
if (outfilename)
{
SVN_ERR(svn_io_file_close(outfile, scratch_pool));
SVN_ERR(svn_stream_open_readonly(&stream, outfilename,
scratch_pool, scratch_pool));
SVN_ERR(svn_stream_copy3(stream, svn_stream_disown(outstream,
scratch_pool),
NULL, NULL, scratch_pool));
}
if (errfilename)
{
SVN_ERR(svn_io_file_close(errfile, scratch_pool));
SVN_ERR(svn_stream_open_readonly(&stream, errfilename,
scratch_pool, scratch_pool));
SVN_ERR(svn_stream_copy3(stream, svn_stream_disown(errstream,
scratch_pool),
NULL, NULL, scratch_pool));
}
}
else /* use libsvn_diff to generate the diff */
{
svn_diff_t *diff;
SVN_ERR(svn_diff_file_diff_2(&diff, tmpfile1, tmpfile2,
dwi->options.for_internal,
scratch_pool));
if (force_diff
|| dwi->use_git_diff_format
|| svn_diff_contains_diffs(diff))
{
/* Print out the diff header. */
SVN_ERR(print_diff_index_header(outstream, dwi->header_encoding,
index_path, "", scratch_pool));
*wrote_header = TRUE;
if (dwi->use_git_diff_format)
{
SVN_ERR(print_git_diff_header(outstream,
&label1, &label2,
operation,
rev1, rev2,
diff_relpath,
copyfrom_path, copyfrom_rev,
left_props, right_props,
index_shas,
dwi->header_encoding,
&dwi->ddi, scratch_pool));
}
/* Output the actual diff */
if (force_diff || svn_diff_contains_diffs(diff))
SVN_ERR(svn_diff_file_output_unified4(outstream, diff,
tmpfile1, tmpfile2, label1, label2,
dwi->header_encoding, rel_to_dir,
dwi->options.for_internal->show_c_function,
dwi->options.for_internal->context_size,
dwi->cancel_func, dwi->cancel_baton,
scratch_pool));
}
}
return SVN_NO_ERROR;
}
/* An svn_diff_tree_processor_t callback. */
static svn_error_t *
diff_file_changed(const char *relpath,
const svn_diff_source_t *left_source,
const svn_diff_source_t *right_source,
const char *left_file,
const char *right_file,
/*const*/ apr_hash_t *left_props,
/*const*/ apr_hash_t *right_props,
svn_boolean_t file_modified,
const apr_array_header_t *prop_changes,
void *file_baton,
const struct svn_diff_tree_processor_t *processor,
apr_pool_t *scratch_pool)
{
diff_writer_info_t *dwi = processor->baton;
svn_boolean_t wrote_header = FALSE;
if (file_modified)
SVN_ERR(diff_content_changed(&wrote_header, relpath,
left_file, right_file,
left_source->revision,
right_source->revision,
left_props, right_props,
svn_diff_op_modified, FALSE,
NULL,
SVN_INVALID_REVNUM, dwi,
scratch_pool));
if (prop_changes->nelts > 0)
SVN_ERR(diff_props_changed(relpath,
left_source->revision,
right_source->revision, prop_changes,
left_props, right_props, !wrote_header,
dwi, scratch_pool));
return SVN_NO_ERROR;
}
/* Because the repos-diff editor passes at least one empty file to
each of these next two functions, they can be dumb wrappers around
the main workhorse routine. */
/* An svn_diff_tree_processor_t callback. */
static svn_error_t *
diff_file_added(const char *relpath,
const svn_diff_source_t *copyfrom_source,
const svn_diff_source_t *right_source,
const char *copyfrom_file,
const char *right_file,
/*const*/ apr_hash_t *copyfrom_props,
/*const*/ apr_hash_t *right_props,
void *file_baton,
const struct svn_diff_tree_processor_t *processor,
apr_pool_t *scratch_pool)
{
diff_writer_info_t *dwi = processor->baton;
svn_boolean_t wrote_header = FALSE;
const char *left_file;
apr_hash_t *left_props;
apr_array_header_t *prop_changes;
if (dwi->no_diff_added)
{
const char *index_path = relpath;
if (dwi->ddi.anchor)
index_path = svn_dirent_join(dwi->ddi.anchor, relpath,
scratch_pool);
SVN_ERR(print_diff_index_header(dwi->outstream, dwi->header_encoding,
index_path, " (added)",
scratch_pool));
wrote_header = TRUE;
return SVN_NO_ERROR;
}
/* During repos->wc diff of a copy revision numbers obtained
* from the working copy are always SVN_INVALID_REVNUM. */
if (copyfrom_source && !dwi->show_copies_as_adds)
{
left_file = copyfrom_file;
left_props = copyfrom_props ? copyfrom_props : apr_hash_make(scratch_pool);
}
else
{
if (!dwi->empty_file)
SVN_ERR(svn_io_open_unique_file3(NULL, &dwi->empty_file,
NULL, svn_io_file_del_on_pool_cleanup,
dwi->pool, scratch_pool));
left_file = dwi->empty_file;
left_props = apr_hash_make(scratch_pool);
copyfrom_source = NULL;
copyfrom_file = NULL;
}
SVN_ERR(svn_prop_diffs(&prop_changes, right_props, left_props, scratch_pool));
if (copyfrom_source && right_file)
SVN_ERR(diff_content_changed(&wrote_header, relpath,
left_file, right_file,
copyfrom_source->revision,
right_source->revision,
left_props, right_props,
copyfrom_source->moved_from_relpath
? svn_diff_op_moved
: svn_diff_op_copied,
TRUE /* force diff output */,
copyfrom_source->moved_from_relpath
? copyfrom_source->moved_from_relpath
: copyfrom_source->repos_relpath,
copyfrom_source->revision,
dwi, scratch_pool));
else if (right_file)
SVN_ERR(diff_content_changed(&wrote_header, relpath,
left_file, right_file,
DIFF_REVNUM_NONEXISTENT,
right_source->revision,
left_props, right_props,
svn_diff_op_added,
TRUE /* force diff output */,
NULL, SVN_INVALID_REVNUM,
dwi, scratch_pool));
if (prop_changes->nelts > 0)
SVN_ERR(diff_props_changed(relpath,
copyfrom_source ? copyfrom_source->revision
: DIFF_REVNUM_NONEXISTENT,
right_source->revision,
prop_changes,
left_props, right_props,
! wrote_header, dwi, scratch_pool));
return SVN_NO_ERROR;
}
/* An svn_diff_tree_processor_t callback. */
static svn_error_t *
diff_file_deleted(const char *relpath,
const svn_diff_source_t *left_source,
const char *left_file,
/*const*/ apr_hash_t *left_props,
void *file_baton,
const struct svn_diff_tree_processor_t *processor,
apr_pool_t *scratch_pool)
{
diff_writer_info_t *dwi = processor->baton;
if (dwi->no_diff_deleted)
{
const char *index_path = relpath;
if (dwi->ddi.anchor)
index_path = svn_dirent_join(dwi->ddi.anchor, relpath,
scratch_pool);
SVN_ERR(print_diff_index_header(dwi->outstream, dwi->header_encoding,
index_path, " (deleted)",
scratch_pool));
}
else
{
svn_boolean_t wrote_header = FALSE;
if (!dwi->empty_file)
SVN_ERR(svn_io_open_unique_file3(NULL, &dwi->empty_file,
NULL, svn_io_file_del_on_pool_cleanup,
dwi->pool, scratch_pool));
if (left_file)
SVN_ERR(diff_content_changed(&wrote_header, relpath,
left_file, dwi->empty_file,
left_source->revision,
DIFF_REVNUM_NONEXISTENT,
left_props,
NULL,
svn_diff_op_deleted, FALSE,
NULL, SVN_INVALID_REVNUM,
dwi,
scratch_pool));
if (left_props && apr_hash_count(left_props))
{
apr_array_header_t *prop_changes;
SVN_ERR(svn_prop_diffs(&prop_changes, apr_hash_make(scratch_pool),
left_props, scratch_pool));
SVN_ERR(diff_props_changed(relpath,
left_source->revision,
DIFF_REVNUM_NONEXISTENT,
prop_changes,
left_props, NULL,
! wrote_header, dwi, scratch_pool));
}
}
return SVN_NO_ERROR;
}
/* An svn_wc_diff_callbacks4_t function. */
static svn_error_t *
diff_dir_changed(const char *relpath,
const svn_diff_source_t *left_source,
const svn_diff_source_t *right_source,
/*const*/ apr_hash_t *left_props,
/*const*/ apr_hash_t *right_props,
const apr_array_header_t *prop_changes,
void *dir_baton,
const struct svn_diff_tree_processor_t *processor,
apr_pool_t *scratch_pool)
{
diff_writer_info_t *dwi = processor->baton;
SVN_ERR(diff_props_changed(relpath,
left_source->revision,
right_source->revision,
prop_changes,
left_props, right_props,
TRUE /* show_diff_header */,
dwi,
scratch_pool));
return SVN_NO_ERROR;
}
/* An svn_diff_tree_processor_t callback. */
static svn_error_t *
diff_dir_added(const char *relpath,
const svn_diff_source_t *copyfrom_source,
const svn_diff_source_t *right_source,
/*const*/ apr_hash_t *copyfrom_props,
/*const*/ apr_hash_t *right_props,
void *dir_baton,
const struct svn_diff_tree_processor_t *processor,
apr_pool_t *scratch_pool)
{
diff_writer_info_t *dwi = processor->baton;
apr_hash_t *left_props;
apr_array_header_t *prop_changes;
if (dwi->no_diff_added)
return SVN_NO_ERROR;
if (copyfrom_source && !dwi->show_copies_as_adds)
{
left_props = copyfrom_props ? copyfrom_props
: apr_hash_make(scratch_pool);
}
else
{
left_props = apr_hash_make(scratch_pool);
copyfrom_source = NULL;
}
SVN_ERR(svn_prop_diffs(&prop_changes, right_props, left_props,
scratch_pool));
return svn_error_trace(diff_props_changed(relpath,
copyfrom_source ? copyfrom_source->revision
: DIFF_REVNUM_NONEXISTENT,
right_source->revision,
prop_changes,
left_props, right_props,
TRUE /* show_diff_header */,
dwi,
scratch_pool));
}
/* An svn_diff_tree_processor_t callback. */
static svn_error_t *
diff_dir_deleted(const char *relpath,
const svn_diff_source_t *left_source,
/*const*/ apr_hash_t *left_props,
void *dir_baton,
const struct svn_diff_tree_processor_t *processor,
apr_pool_t *scratch_pool)
{
diff_writer_info_t *dwi = processor->baton;
apr_array_header_t *prop_changes;
apr_hash_t *right_props;
if (dwi->no_diff_deleted)
return SVN_NO_ERROR;
right_props = apr_hash_make(scratch_pool);
SVN_ERR(svn_prop_diffs(&prop_changes, right_props,
left_props, scratch_pool));
SVN_ERR(diff_props_changed(relpath,
left_source->revision,
DIFF_REVNUM_NONEXISTENT,
prop_changes,
left_props, right_props,
TRUE /* show_diff_header */,
dwi,
scratch_pool));
return SVN_NO_ERROR;
}
/*-----------------------------------------------------------------*/
/** The logic behind 'svn diff' and 'svn merge'. */
/* Hi! This is a comment left behind by Karl, and Ben is too afraid
to erase it at this time, because he's not fully confident that all
this knowledge has been grokked yet.
There are five cases:
1. path is not a URL and start_revision != end_revision
2. path is not a URL and start_revision == end_revision
3. path is a URL and start_revision != end_revision
4. path is a URL and start_revision == end_revision
5. path is not a URL and no revisions given
With only one distinct revision the working copy provides the
other. When path is a URL there is no working copy. Thus
1: compare repository versions for URL corresponding to working copy
2: compare working copy against repository version
3: compare repository versions for URL
4: nothing to do.
5: compare working copy against text-base
Case 4 is not as stupid as it looks, for example it may occur if
the user specifies two dates that resolve to the same revision. */
/** Check if paths PATH_OR_URL1 and PATH_OR_URL2 are urls and if the
* revisions REVISION1 and REVISION2 are local. If PEG_REVISION is not
* unspecified, ensure that at least one of the two revisions is not
* BASE or WORKING.
* If PATH_OR_URL1 can only be found in the repository, set *IS_REPOS1
* to TRUE. If PATH_OR_URL2 can only be found in the repository, set
* *IS_REPOS2 to TRUE. */
static svn_error_t *
check_paths(svn_boolean_t *is_repos1,
svn_boolean_t *is_repos2,
const char *path_or_url1,
const char *path_or_url2,
const svn_opt_revision_t *revision1,
const svn_opt_revision_t *revision2,
const svn_opt_revision_t *peg_revision)
{
svn_boolean_t is_local_rev1, is_local_rev2;
/* Verify our revision arguments in light of the paths. */
if ((revision1->kind == svn_opt_revision_unspecified)
|| (revision2->kind == svn_opt_revision_unspecified))
return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL,
_("Not all required revisions are specified"));
/* Revisions can be said to be local or remote.
* BASE and WORKING are local revisions. */
is_local_rev1 =
((revision1->kind == svn_opt_revision_base)
|| (revision1->kind == svn_opt_revision_working));
is_local_rev2 =
((revision2->kind == svn_opt_revision_base)
|| (revision2->kind == svn_opt_revision_working));
if (peg_revision->kind != svn_opt_revision_unspecified &&
is_local_rev1 && is_local_rev2)
return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL,
_("At least one revision must be something other "
"than BASE or WORKING when diffing a URL"));
/* Working copy paths with non-local revisions get turned into
URLs. We don't do that here, though. We simply record that it
needs to be done, which is information that helps us choose our
diff helper function. */
*is_repos1 = ! is_local_rev1 || svn_path_is_url(path_or_url1);
*is_repos2 = ! is_local_rev2 || svn_path_is_url(path_or_url2);
return SVN_NO_ERROR;
}
/* Raise an error if the diff target URL does not exist at REVISION.
* If REVISION does not equal OTHER_REVISION, mention both revisions in
* the error message. Use RA_SESSION to contact the repository.
* Use POOL for temporary allocations. */
static svn_error_t *
check_diff_target_exists(const char *url,
svn_revnum_t revision,
svn_revnum_t other_revision,
svn_ra_session_t *ra_session,
apr_pool_t *pool)
{
svn_node_kind_t kind;
const char *session_url;
SVN_ERR(svn_ra_get_session_url(ra_session, &session_url, pool));
if (strcmp(url, session_url) != 0)
SVN_ERR(svn_ra_reparent(ra_session, url, pool));
SVN_ERR(svn_ra_check_path(ra_session, "", revision, &kind, pool));
if (kind == svn_node_none)
{
if (revision == other_revision)
return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
_("Diff target '%s' was not found in the "
"repository at revision '%ld'"),
url, revision);
else
return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
_("Diff target '%s' was not found in the "
"repository at revision '%ld' or '%ld'"),
url, revision, other_revision);
}
if (strcmp(url, session_url) != 0)
SVN_ERR(svn_ra_reparent(ra_session, session_url, pool));
return SVN_NO_ERROR;
}
/** Prepare a repos repos diff between PATH_OR_URL1 and
* PATH_OR_URL2@PEG_REVISION, in the revision range REVISION1:REVISION2.
*
* Return the resolved URL and peg revision pairs in *URL1, *REV1 and in
* *URL2, *REV2.
*
* Return suitable anchor URL and target pairs in *ANCHOR1, *TARGET1 and
* in *ANCHOR2, *TARGET2, corresponding to *URL1 and *URL2.
*
* (The choice of anchor URLs here appears to be: start with *URL1, *URL2;
* then take the parent dir on both sides, unless either of *URL1 or *URL2
* is the repository root or the parent dir of *URL1 is unreadable.)
*
* Set *KIND1 and *KIND2 to the node kinds of *URL1 and *URL2, and verify
* that at least one of the diff targets exists.
*
* Set *RA_SESSION to an RA session parented at the URL *ANCHOR1.
*
* Use client context CTX. Do all allocations in POOL. */
static svn_error_t *
diff_prepare_repos_repos(const char **url1,
const char **url2,
svn_revnum_t *rev1,
svn_revnum_t *rev2,
const char **anchor1,
const char **anchor2,
const char **target1,
const char **target2,
svn_node_kind_t *kind1,
svn_node_kind_t *kind2,
svn_ra_session_t **ra_session,
svn_client_ctx_t *ctx,
const char *path_or_url1,
const char *path_or_url2,
const svn_opt_revision_t *revision1,
const svn_opt_revision_t *revision2,
const svn_opt_revision_t *peg_revision,
apr_pool_t *pool)
{
const char *local_abspath1 = NULL;
const char *local_abspath2 = NULL;
const char *repos_root_url;
const char *wri_abspath = NULL;
svn_client__pathrev_t *resolved1;
svn_client__pathrev_t *resolved2 = NULL;
enum svn_opt_revision_kind peg_kind = peg_revision->kind;
if (!svn_path_is_url(path_or_url2))
{
SVN_ERR(svn_dirent_get_absolute(&local_abspath2, path_or_url2, pool));
SVN_ERR(svn_wc__node_get_url(url2, ctx->wc_ctx, local_abspath2,
pool, pool));
wri_abspath = local_abspath2;
}
else
*url2 = apr_pstrdup(pool, path_or_url2);
if (!svn_path_is_url(path_or_url1))
{
SVN_ERR(svn_dirent_get_absolute(&local_abspath1, path_or_url1, pool));
wri_abspath = local_abspath1;
}
SVN_ERR(svn_client_open_ra_session2(ra_session, *url2, wri_abspath,
ctx, pool, pool));
/* If we are performing a pegged diff, we need to find out what our
actual URLs will be. */
if (peg_kind != svn_opt_revision_unspecified
|| path_or_url1 == path_or_url2
|| local_abspath2)
{
svn_error_t *err;
err = svn_client__resolve_rev_and_url(&resolved2,
*ra_session, path_or_url2,
peg_revision, revision2,
ctx, pool);
if (err)
{
if (err->apr_err != SVN_ERR_CLIENT_UNRELATED_RESOURCES
&& err->apr_err != SVN_ERR_FS_NOT_FOUND)
return svn_error_trace(err);
svn_error_clear(err);
resolved2 = NULL;
}
}
else
resolved2 = NULL;
if (peg_kind != svn_opt_revision_unspecified
|| path_or_url1 == path_or_url2
|| local_abspath1)
{
svn_error_t *err;
err = svn_client__resolve_rev_and_url(&resolved1,
*ra_session, path_or_url1,
peg_revision, revision1,
ctx, pool);
if (err)
{
if (err->apr_err != SVN_ERR_CLIENT_UNRELATED_RESOURCES
&& err->apr_err != SVN_ERR_FS_NOT_FOUND)
return svn_error_trace(err);
svn_error_clear(err);
resolved1 = NULL;
}
}
else
resolved1 = NULL;
if (resolved1)
{
*url1 = resolved1->url;
*rev1 = resolved1->rev;
}
else
{
/* It would be nice if we could just return an error when resolving a
location fails... But in many such cases we prefer diffing against
a non-existent location to show adds or removes (see issue #4153) */
if (resolved2
&& (peg_kind != svn_opt_revision_unspecified
|| path_or_url1 == path_or_url2))
*url1 = resolved2->url;
else if (! local_abspath1)
*url1 = path_or_url1;
else
SVN_ERR(svn_wc__node_get_url(url1, ctx->wc_ctx, local_abspath1,
pool, pool));
SVN_ERR(svn_client__get_revision_number(rev1, NULL, ctx->wc_ctx,
local_abspath1 /* may be NULL */,
*ra_session, revision1, pool));
}
if (resolved2)
{
*url2 = resolved2->url;
*rev2 = resolved2->rev;
}
else
{
/* It would be nice if we could just return an error when resolving a
location fails... But in many such cases we prefer diffing against
a non-existent location to show adds or removes (see issue #4153) */
if (resolved1
&& (peg_kind != svn_opt_revision_unspecified
|| path_or_url1 == path_or_url2))
*url2 = resolved1->url;
/* else keep url2 */
SVN_ERR(svn_client__get_revision_number(rev2, NULL, ctx->wc_ctx,
local_abspath2 /* may be NULL */,
*ra_session, revision2, pool));
}
/* Resolve revision and get path kind for the second target. */
SVN_ERR(svn_ra_reparent(*ra_session, *url2, pool));
SVN_ERR(svn_ra_check_path(*ra_session, "", *rev2, kind2, pool));
/* Do the same for the first target. */
SVN_ERR(svn_ra_reparent(*ra_session, *url1, pool));
SVN_ERR(svn_ra_check_path(*ra_session, "", *rev1, kind1, pool));
/* Either both URLs must exist at their respective revisions,
* or one of them may be missing from one side of the diff. */
if (*kind1 == svn_node_none && *kind2 == svn_node_none)
{
if (strcmp(*url1, *url2) == 0)
return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
_("Diff target '%s' was not found in the "
"repository at revisions '%ld' and '%ld'"),
*url1, *rev1, *rev2);
else
return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL,
_("Diff targets '%s' and '%s' were not found "
"in the repository at revisions '%ld' and "
"'%ld'"),
*url1, *url2, *rev1, *rev2);
}
else if (*kind1 == svn_node_none)
SVN_ERR(check_diff_target_exists(*url1, *rev2, *rev1, *ra_session, pool));
else if (*kind2 == svn_node_none)
SVN_ERR(check_diff_target_exists(*url2, *rev1, *rev2, *ra_session, pool));
SVN_ERR(svn_ra_get_repos_root2(*ra_session, &repos_root_url, pool));
/* Choose useful anchors and targets for our two URLs. */
*anchor1 = *url1;
*anchor2 = *url2;
*target1 = "";
*target2 = "";
/* If none of the targets is the repository root open the parent directory
to allow describing replacement of the target itself */
if (strcmp(*url1, repos_root_url) != 0
&& strcmp(*url2, repos_root_url) != 0)
{
svn_node_kind_t ignored_kind;
svn_error_t *err;
svn_uri_split(anchor1, target1, *url1, pool);
svn_uri_split(anchor2, target2, *url2, pool);
SVN_ERR(svn_ra_reparent(*ra_session, *anchor1, pool));
/* We might not have the necessary rights to read the root now.
(It is ok to pass a revision here where the node doesn't exist) */
err = svn_ra_check_path(*ra_session, "", *rev1, &ignored_kind, pool);
if (err && (err->apr_err == SVN_ERR_RA_DAV_FORBIDDEN
|| err->apr_err == SVN_ERR_RA_NOT_AUTHORIZED))
{
svn_error_clear(err);
/* Ok, lets undo the reparent...
We can't report replacements this way, but at least we can
report changes on the descendants */
*anchor1 = svn_path_url_add_component2(*anchor1, *target1, pool);
*anchor2 = svn_path_url_add_component2(*anchor2, *target2, pool);
*target1 = "";
*target2 = "";
SVN_ERR(svn_ra_reparent(*ra_session, *anchor1, pool));
}
else
SVN_ERR(err);
}
return SVN_NO_ERROR;
}
/* A Theoretical Note From Ben, regarding do_diff().
This function is really svn_client_diff7(). If you read the public
API description for svn_client_diff7(), it sounds quite Grand. It
sounds really generalized and abstract and beautiful: that it will
diff any two paths, be they working-copy paths or URLs, at any two
revisions.
Now, the *reality* is that we have exactly three 'tools' for doing
diffing, and thus this routine is built around the use of the three
tools. Here they are, for clarity:
- svn_wc_diff: assumes both paths are the same wcpath.
compares wcpath@BASE vs. wcpath@WORKING
- svn_wc_get_diff_editor: compares some URL@REV vs. wcpath@WORKING
- svn_client__get_diff_editor: compares some URL1@REV1 vs. URL2@REV2
Since Subversion 1.8 we also have a variant of svn_wc_diff called
svn_client__arbitrary_nodes_diff, that allows handling WORKING-WORKING
comparisons between nodes in the working copy.
So the truth of the matter is, if the caller's arguments can't be
pigeonholed into one of these use-cases, we currently bail with a
friendly apology.
Perhaps someday a brave soul will truly make svn_client_diff7()
perfectly general. For now, we live with the 90% case. Certainly,
the commandline client only calls this function in legal ways.
When there are other users of svn_client.h, maybe this will become
a more pressing issue.
*/
/* Return a "you can't do that" error, optionally wrapping another
error CHILD_ERR. */
static svn_error_t *
unsupported_diff_error(svn_error_t *child_err)
{
return svn_error_create(SVN_ERR_INCORRECT_PARAMS, child_err,
_("Sorry, svn_client_diff7 was called in a way "
"that is not yet supported"));
}
/* Perform a diff between two working-copy paths.
PATH1 and PATH2 are both working copy paths. REVISION1 and
REVISION2 are their respective revisions.
For now, require PATH1=PATH2, REVISION1='base', REVISION2='working',
otherwise return an error.
Anchor DIFF_PROCESSOR at the requested diff targets.
All other options are the same as those passed to svn_client_diff7(). */
static svn_error_t *
diff_wc_wc(const char *path1,
const svn_opt_revision_t *revision1,
const char *path2,
const svn_opt_revision_t *revision2,
svn_depth_t depth,
svn_boolean_t ignore_ancestry,
const apr_array_header_t *changelists,
const svn_diff_tree_processor_t *diff_processor,
svn_client_ctx_t *ctx,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
const char *abspath1;
SVN_ERR_ASSERT(! svn_path_is_url(path1));
SVN_ERR_ASSERT(! svn_path_is_url(path2));
SVN_ERR(svn_dirent_get_absolute(&abspath1, path1, scratch_pool));
/* Currently we support only the case where path1 and path2 are the
same path. */
if ((strcmp(path1, path2) != 0)
|| (! ((revision1->kind == svn_opt_revision_base)
&& (revision2->kind == svn_opt_revision_working))))
return unsupported_diff_error(
svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
_("A non-URL diff at this time must be either from "
"a path's base to the same path's working version "
"or between the working versions of two paths"
)));
SVN_ERR(svn_wc__diff7(TRUE,
ctx->wc_ctx, abspath1, depth,
ignore_ancestry, changelists,
diff_processor,
ctx->cancel_func, ctx->cancel_baton,
result_pool, scratch_pool));
return SVN_NO_ERROR;
}
/* Perform a diff between two repository paths.
PATH_OR_URL1 and PATH_OR_URL2 may be either URLs or the working copy paths.
REVISION1 and REVISION2 are their respective revisions.
If PEG_REVISION is specified, PATH_OR_URL2 is the path at the peg revision,
and the actual two paths compared are determined by following copy
history from PATH_OR_URL2.
If DDI is null, anchor the DIFF_PROCESSOR at the requested diff
targets. (This case is used by diff-summarize.)
If DDI is non-null: Set DDI->orig_path_* to the two diff target URLs as
resolved at the given revisions; set DDI->anchor to an anchor WC path
if either of PATH_OR_URL* is given as a WC path, else to null; set
DDI->session_relpath to the repository-relpath of the anchor URL for
DDI->orig_path_1. Anchor the DIFF_PROCESSOR at the anchor chosen
for the underlying diff implementation if the target on either side
is a file, else at the actual requested targets.
(The choice of WC anchor implemented here for DDI->anchor appears to
be: choose PATH_OR_URL2 (if it's a WC path) or else PATH_OR_URL1 (if
it's a WC path); then take its parent dir unless both resolved URLs
refer to directories.)
(For the choice of URL anchor for DDI->session_relpath, see
diff_prepare_repos_repos().)
### Bizarre anchoring. TODO: always anchor DIFF_PROCESSOR at the
requested targets.
All other options are the same as those passed to svn_client_diff7(). */
static svn_error_t *
diff_repos_repos(struct diff_driver_info_t *ddi,
const char *path_or_url1,
const char *path_or_url2,
const svn_opt_revision_t *revision1,
const svn_opt_revision_t *revision2,
const svn_opt_revision_t *peg_revision,
svn_depth_t depth,
svn_boolean_t ignore_ancestry,
svn_boolean_t text_deltas,
const svn_diff_tree_processor_t *diff_processor,
svn_client_ctx_t *ctx,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
svn_ra_session_t *extra_ra_session;
const svn_ra_reporter3_t *reporter;
void *reporter_baton;
const svn_delta_editor_t *diff_editor;
void *diff_edit_baton;
const char *url1;
const char *url2;
svn_revnum_t rev1;
svn_revnum_t rev2;
svn_node_kind_t kind1;
svn_node_kind_t kind2;
const char *anchor1;
const char *anchor2;
const char *target1;
const char *target2;
svn_ra_session_t *ra_session;
/* Prepare info for the repos repos diff. */
SVN_ERR(diff_prepare_repos_repos(&url1, &url2, &rev1, &rev2,
&anchor1, &anchor2, &target1, &target2,
&kind1, &kind2, &ra_session,
ctx, path_or_url1, path_or_url2,
revision1, revision2, peg_revision,
scratch_pool));
/* Set up the repos_diff editor on BASE_PATH, if available.
Otherwise, we just use "". */
if (ddi)
{
/* Get actual URLs. */
ddi->orig_path_1 = url1;
ddi->orig_path_2 = url2;
/* This should be moved to the diff writer
- path_or_url are provided by the caller
- target1 is available as *root_relpath
- (kind1 != svn_node_dir || kind2 != svn_node_dir) = !*root_is_dir */
if (!svn_path_is_url(path_or_url2))
ddi->anchor = path_or_url2;
else if (!svn_path_is_url(path_or_url1))
ddi->anchor = path_or_url1;
else
ddi->anchor = NULL;
if (*target1 && ddi->anchor
&& (kind1 != svn_node_dir || kind2 != svn_node_dir))
ddi->anchor = svn_dirent_dirname(ddi->anchor, result_pool);
}
/* The repository can bring in a new working copy, but not delete
everything. Luckily our new diff handler can just be reversed. */
if (kind2 == svn_node_none)
{
const char *str_tmp;
svn_revnum_t rev_tmp;
str_tmp = url2;
url2 = url1;
url1 = str_tmp;
rev_tmp = rev2;
rev2 = rev1;
rev1 = rev_tmp;
str_tmp = anchor2;
anchor2 = anchor1;
anchor1 = str_tmp;
str_tmp = target2;
target2 = target1;
target1 = str_tmp;
diff_processor = svn_diff__tree_processor_reverse_create(diff_processor,
scratch_pool);
}
/* Filter the first path component using a filter processor, until we fixed
the diff processing to handle this directly */
if (!ddi)
{
diff_processor = svn_diff__tree_processor_filter_create(
diff_processor, target1, scratch_pool);
}
else if ((kind1 != svn_node_file && kind2 != svn_node_file)
&& target1[0] != '\0')
{
diff_processor = svn_diff__tree_processor_filter_create(
diff_processor, target1, scratch_pool);
}
/* Now, we open an extra RA session to the correct anchor
location for URL1. This is used during the editor calls to fetch file
contents. */
SVN_ERR(svn_ra__dup_session(&extra_ra_session, ra_session, anchor1,
scratch_pool, scratch_pool));
if (ddi)
{
const char *repos_root_url;
const char *session_url;
SVN_ERR(svn_ra_get_repos_root2(ra_session, &repos_root_url,
scratch_pool));
SVN_ERR(svn_ra_get_session_url(ra_session, &session_url,
scratch_pool));
ddi->session_relpath = svn_uri_skip_ancestor(repos_root_url,
session_url,
result_pool);
}
SVN_ERR(svn_client__get_diff_editor2(
&diff_editor, &diff_edit_baton,
extra_ra_session, depth,
rev1,
text_deltas,
diff_processor,
ctx->cancel_func, ctx->cancel_baton,
scratch_pool));
/* We want to switch our txn into URL2 */
SVN_ERR(svn_ra_do_diff3(ra_session, &reporter, &reporter_baton,
rev2, target1,
depth, ignore_ancestry, text_deltas,
url2, diff_editor, diff_edit_baton, scratch_pool));
/* Drive the reporter; do the diff. */
SVN_ERR(reporter->set_path(reporter_baton, "", rev1,
svn_depth_infinity,
FALSE, NULL,
scratch_pool));
return svn_error_trace(
reporter->finish_report(reporter_baton, scratch_pool));
}
/* Perform a diff between a repository path and a working-copy path.
PATH_OR_URL1 may be either a URL or a working copy path. PATH2 is a
working copy path. REVISION1 is the revision of URL1. If PEG_REVISION1
is specified, then PATH_OR_URL1 is the path in the peg revision, and the
actual repository path to be compared is determined by following copy
history.
REVISION_KIND2 specifies which revision should be reported from the
working copy (BASE or WORKING)
If REVERSE is TRUE, the diff will be reported in reverse.
If DDI is null, anchor the DIFF_PROCESSOR at the requested diff
targets. (This case is used by diff-summarize.)
If DDI is non-null: Set DDI->orig_path_* to the URLs of the two diff
targets as resolved at the given revisions; set DDI->anchor to a WC path
anchor for PATH2; set DDI->session_relpath to the repository-relpath of
the URL of that same anchor WC path.
All other options are the same as those passed to svn_client_diff7(). */
static svn_error_t *
diff_repos_wc(struct diff_driver_info_t *ddi,
const char *path_or_url1,
const svn_opt_revision_t *revision1,
const svn_opt_revision_t *peg_revision1,
const char *path2,
enum svn_opt_revision_kind revision2_kind,
svn_boolean_t reverse,
svn_depth_t depth,
svn_boolean_t ignore_ancestry,
const apr_array_header_t *changelists,
const svn_diff_tree_processor_t *diff_processor,
svn_client_ctx_t *ctx,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
const char *anchor, *anchor_url, *target;
svn_ra_session_t *ra_session;
svn_depth_t diff_depth;
const svn_ra_reporter3_t *reporter;
void *reporter_baton;
const svn_delta_editor_t *diff_editor;
void *diff_edit_baton;
svn_boolean_t rev2_is_base = (revision2_kind == svn_opt_revision_base);
svn_boolean_t server_supports_depth;
const char *abspath_or_url1;
const char *abspath2;
const char *anchor_abspath;
svn_boolean_t is_copy;
svn_revnum_t cf_revision;
const char *cf_repos_relpath;
const char *cf_repos_root_url;
svn_depth_t cf_depth;
const char *copy_root_abspath;
const char *target_url;
svn_client__pathrev_t *loc1;
SVN_ERR_ASSERT(! svn_path_is_url(path2));
if (!svn_path_is_url(path_or_url1))
{
SVN_ERR(svn_dirent_get_absolute(&abspath_or_url1, path_or_url1,
scratch_pool));
}
else
{
abspath_or_url1 = path_or_url1;
}
SVN_ERR(svn_dirent_get_absolute(&abspath2, path2, scratch_pool));
/* Check if our diff target is a copied node. */
SVN_ERR(svn_wc__node_get_origin(&is_copy,
&cf_revision,
&cf_repos_relpath,
&cf_repos_root_url,
NULL, &cf_depth,
&copy_root_abspath,
ctx->wc_ctx, abspath2,
FALSE, scratch_pool, scratch_pool));
SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &loc1,
path_or_url1, abspath2,
peg_revision1, revision1,
ctx, scratch_pool));
if (revision2_kind == svn_opt_revision_base || !is_copy)
{
/* Convert path_or_url1 to a URL to feed to do_diff. */
SVN_ERR(svn_wc_get_actual_target2(&anchor, &target, ctx->wc_ctx, path2,
scratch_pool, scratch_pool));
/* Handle the ugly case where target is ".." */
if (*target && !svn_path_is_single_path_component(target))
{
anchor = svn_dirent_join(anchor, target, scratch_pool);
target = "";
}
/* Fetch the URL of the anchor directory. */
SVN_ERR(svn_dirent_get_absolute(&anchor_abspath, anchor, scratch_pool));
SVN_ERR(svn_wc__node_get_url(&anchor_url, ctx->wc_ctx, anchor_abspath,
scratch_pool, scratch_pool));
SVN_ERR_ASSERT(anchor_url != NULL);
target_url = NULL;
}
else /* is_copy && revision2->kind != svn_opt_revision_base */
{
#if 0
svn_node_kind_t kind;
#endif
/* ### Ugly hack ahead ###
*
* We're diffing a locally copied/moved node.
* Describe the copy source to the reporter instead of the copy itself.
* Doing the latter would generate a single add_directory() call to the
* diff editor which results in an unexpected diff (the copy would
* be shown as deleted).
*
* ### But if we will receive any real changes from the repositor we
* will most likely fail to apply them as the wc diff editor assumes
* that we have the data to which the change applies in BASE...
*/
target_url = svn_path_url_add_component2(cf_repos_root_url,
cf_repos_relpath,
scratch_pool);
#if 0
/*SVN_ERR(svn_wc_read_kind2(&kind, ctx->wc_ctx, abspath2, FALSE, FALSE,
scratch_pool));
if (kind != svn_node_dir
|| strcmp(copy_root_abspath, abspath2) != 0) */
#endif
{
/* We are looking at a subdirectory of the repository,
We can describe the parent directory as the anchor..
### This 'appears to work', but that is really dumb luck
### for the simple cases in the test suite */
anchor_abspath = svn_dirent_dirname(abspath2, scratch_pool);
anchor_url = svn_path_url_add_component2(cf_repos_root_url,
svn_relpath_dirname(
cf_repos_relpath,
scratch_pool),
scratch_pool);
target = svn_dirent_basename(abspath2, NULL);
anchor = svn_dirent_dirname(path2, scratch_pool);
}
#if 0
else
{
/* This code, while ok can't be enabled without causing test
* failures. The repository will send some changes against
* BASE for nodes that don't have BASE...
*/
anchor_abspath = abspath2;
anchor_url = svn_path_url_add_component2(cf_repos_root_url,
cf_repos_relpath,
scratch_pool);
anchor = path2;
target = "";
}
#endif
}
SVN_ERR(svn_ra_reparent(ra_session, anchor_url, scratch_pool));
if (ddi)
{
const char *repos_root_url;
ddi->anchor = anchor;
if (!reverse)
{
ddi->orig_path_1 = apr_pstrdup(result_pool, loc1->url);
ddi->orig_path_2 =
svn_path_url_add_component2(anchor_url, target, result_pool);
}
else
{
ddi->orig_path_1 =
svn_path_url_add_component2(anchor_url, target, result_pool);
ddi->orig_path_2 = apr_pstrdup(result_pool, loc1->url);
}
SVN_ERR(svn_ra_get_repos_root2(ra_session, &repos_root_url,
scratch_pool));
ddi->session_relpath = svn_uri_skip_ancestor(repos_root_url,
anchor_url,
result_pool);
}
else
{
diff_processor = svn_diff__tree_processor_filter_create(
diff_processor, target, scratch_pool);
}
if (reverse)
diff_processor = svn_diff__tree_processor_reverse_create(diff_processor, scratch_pool);
/* Use the diff editor to generate the diff. */
SVN_ERR(svn_ra_has_capability(ra_session, &server_supports_depth,
SVN_RA_CAPABILITY_DEPTH, scratch_pool));
SVN_ERR(svn_wc__get_diff_editor(&diff_editor, &diff_edit_baton,
ctx->wc_ctx,
anchor_abspath,
target,
depth,
ignore_ancestry,
rev2_is_base,
reverse,
server_supports_depth,
changelists,
diff_processor,
ctx->cancel_func, ctx->cancel_baton,
scratch_pool, scratch_pool));
if (depth != svn_depth_infinity)
diff_depth = depth;
else
diff_depth = svn_depth_unknown;
/* Tell the RA layer we want a delta to change our txn to URL1 */
SVN_ERR(svn_ra_do_diff3(ra_session,
&reporter, &reporter_baton,
loc1->rev,
target,
diff_depth,
ignore_ancestry,
TRUE, /* text_deltas */
loc1->url,
diff_editor, diff_edit_baton,
scratch_pool));
if (is_copy && revision2_kind != svn_opt_revision_base)
{
/* Report the copy source. */
if (cf_depth == svn_depth_unknown)
cf_depth = svn_depth_infinity;
/* Reporting the in-wc revision as r0, makes the repository send
everything as added, which avoids using BASE for pristine information,
which is not there (or unrelated) for a copy */
SVN_ERR(reporter->set_path(reporter_baton, "",
ignore_ancestry ? 0 : cf_revision,
cf_depth, FALSE, NULL, scratch_pool));
if (*target)
SVN_ERR(reporter->link_path(reporter_baton, target,
target_url,
ignore_ancestry ? 0 : cf_revision,
cf_depth, FALSE, NULL, scratch_pool));
/* Finish the report to generate the diff. */
SVN_ERR(reporter->finish_report(reporter_baton, scratch_pool));
}
else
{
/* Create a txn mirror of path2; the diff editor will print
diffs in reverse. :-) */
SVN_ERR(svn_wc_crawl_revisions5(ctx->wc_ctx, abspath2,
reporter, reporter_baton,
FALSE, depth, TRUE,
(! server_supports_depth),
FALSE,
ctx->cancel_func, ctx->cancel_baton,
NULL, NULL, /* notification is N/A */
scratch_pool));
}
return SVN_NO_ERROR;
}
/* Run diff on shelf SHELF_NAME, if it exists.
*/
static svn_error_t *
diff_shelf(const char *shelf_name,
const char *target_abspath,
svn_depth_t depth,
svn_boolean_t ignore_ancestry,
const svn_diff_tree_processor_t *diff_processor,
svn_client_ctx_t *ctx,
apr_pool_t *scratch_pool)
{
svn_error_t *err;
svn_client__shelf_t *shelf;
svn_client__shelf_version_t *shelf_version;
const char *wc_relpath;
err = svn_client__shelf_open_existing(&shelf,
shelf_name, target_abspath,
ctx, scratch_pool);
if (err && err->apr_err == SVN_ERR_ILLEGAL_TARGET)
{
svn_error_clear(err);
return SVN_NO_ERROR;
}
else
SVN_ERR(err);
SVN_ERR(svn_client__shelf_version_open(&shelf_version,
shelf, shelf->max_version,
scratch_pool, scratch_pool));
wc_relpath = svn_dirent_skip_ancestor(shelf->wc_root_abspath, target_abspath);
SVN_ERR(svn_client__shelf_diff(shelf_version, wc_relpath,
depth, ignore_ancestry,
diff_processor, scratch_pool));
SVN_ERR(svn_client__shelf_close(shelf, scratch_pool));
return SVN_NO_ERROR;
}
/* Run diff on all shelves named in CHANGELISTS by a changelist name
* of the form "svn:shelf:SHELF_NAME", if they exist.
*/
static svn_error_t *
diff_shelves(const apr_array_header_t *changelists,
const char *target_abspath,
svn_depth_t depth,
svn_boolean_t ignore_ancestry,
const svn_diff_tree_processor_t *diff_processor,
svn_client_ctx_t *ctx,
apr_pool_t *scratch_pool)
{
static const char PREFIX[] = "svn:shelf:";
static const int PREFIX_LEN = 10;
int i;
if (! changelists)
return SVN_NO_ERROR;
for (i = 0; i < changelists->nelts; i++)
{
const char *cl = APR_ARRAY_IDX(changelists, i, const char *);
if (strncmp(cl, PREFIX, PREFIX_LEN) == 0)
{
const char *shelf_name = cl + PREFIX_LEN;
SVN_ERR(diff_shelf(shelf_name, target_abspath,
depth, ignore_ancestry,
diff_processor, ctx, scratch_pool));
}
}
return SVN_NO_ERROR;
}
/* This is basically just the guts of svn_client_diff[_summarize][_peg]6(). */
static svn_error_t *
do_diff(diff_driver_info_t *ddi,
const char *path_or_url1,
const char *path_or_url2,
const svn_opt_revision_t *revision1,
const svn_opt_revision_t *revision2,
const svn_opt_revision_t *peg_revision,
svn_boolean_t no_peg_revision,
svn_depth_t depth,
svn_boolean_t ignore_ancestry,
const apr_array_header_t *changelists,
svn_boolean_t text_deltas,
const svn_diff_tree_processor_t *diff_processor,
svn_client_ctx_t *ctx,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
svn_boolean_t is_repos1;
svn_boolean_t is_repos2;
/* Check if paths/revisions are urls/local. */
SVN_ERR(check_paths(&is_repos1, &is_repos2, path_or_url1, path_or_url2,
revision1, revision2, peg_revision));
if (is_repos1)
{
if (is_repos2)
{
/* Ignores changelists. */
SVN_ERR(diff_repos_repos(ddi,
path_or_url1, path_or_url2,
revision1, revision2,
peg_revision, depth, ignore_ancestry,
text_deltas,
diff_processor, ctx,
result_pool, scratch_pool));
}
else /* path_or_url2 is a working copy path */
{
SVN_ERR(diff_repos_wc(ddi,
path_or_url1, revision1,
no_peg_revision ? revision1
: peg_revision,
path_or_url2, revision2->kind,
FALSE, depth,
ignore_ancestry, changelists,
diff_processor, ctx,
result_pool, scratch_pool));
}
}
else /* path_or_url1 is a working copy path */
{
if (is_repos2)
{
SVN_ERR(diff_repos_wc(ddi,
path_or_url2, revision2,
no_peg_revision ? revision2
: peg_revision,
path_or_url1,
revision1->kind,
TRUE, depth,
ignore_ancestry, changelists,
diff_processor, ctx,
result_pool, scratch_pool));
}
else /* path_or_url2 is a working copy path */
{
if (revision1->kind == svn_opt_revision_working
&& revision2->kind == svn_opt_revision_working)
{
const char *abspath1;
const char *abspath2;
SVN_ERR(svn_dirent_get_absolute(&abspath1, path_or_url1,
scratch_pool));
SVN_ERR(svn_dirent_get_absolute(&abspath2, path_or_url2,
scratch_pool));
if (ddi)
{
svn_node_kind_t kind1, kind2;
SVN_ERR(svn_io_check_resolved_path(abspath1, &kind1,
scratch_pool));
SVN_ERR(svn_io_check_resolved_path(abspath2, &kind2,
scratch_pool));
if (kind1 == svn_node_dir && kind2 == svn_node_dir)
{
ddi->anchor = "";
}
else
{
ddi->anchor = svn_dirent_basename(abspath1, NULL);
}
ddi->orig_path_1 = path_or_url1;
ddi->orig_path_2 = path_or_url2;
}
/* Ignores changelists, ignore_ancestry */
SVN_ERR(svn_client__arbitrary_nodes_diff(abspath1, abspath2,
depth,
diff_processor,
ctx, scratch_pool));
}
else
{
if (ddi)
{
ddi->anchor = path_or_url1;
ddi->orig_path_1 = path_or_url1;
ddi->orig_path_2 = path_or_url2;
}
{
const char *abspath1;
SVN_ERR(svn_dirent_get_absolute(&abspath1, path_or_url1,
scratch_pool));
SVN_ERR(diff_shelves(changelists, abspath1,
depth, ignore_ancestry,
diff_processor, ctx, scratch_pool));
}
SVN_ERR(diff_wc_wc(path_or_url1, revision1,
path_or_url2, revision2,
depth, ignore_ancestry, changelists,
diff_processor, ctx,
result_pool, scratch_pool));
}
}
}
return SVN_NO_ERROR;
}
/* Initialize DWI.diff_cmd and DWI.options,
* according to OPTIONS and CONFIG. CONFIG and OPTIONS may be null.
* Allocate the fields in RESULT_POOL, which should be at least as long-lived
* as the pool DWI itself is allocated in.
*/
static svn_error_t *
create_diff_writer_info(diff_writer_info_t *dwi,
const apr_array_header_t *options,
apr_hash_t *config, apr_pool_t *result_pool)
{
const char *diff_cmd = NULL;
/* See if there is a diff command and/or diff arguments. */
if (config)
{
svn_config_t *cfg = svn_hash_gets(config, SVN_CONFIG_CATEGORY_CONFIG);
svn_config_get(cfg, &diff_cmd, SVN_CONFIG_SECTION_HELPERS,
SVN_CONFIG_OPTION_DIFF_CMD, NULL);
if (options == NULL)
{
const char *diff_extensions;
svn_config_get(cfg, &diff_extensions, SVN_CONFIG_SECTION_HELPERS,
SVN_CONFIG_OPTION_DIFF_EXTENSIONS, NULL);
if (diff_extensions)
options = svn_cstring_split(diff_extensions, " \t\n\r", TRUE,
result_pool);
}
}
if (options == NULL)
options = apr_array_make(result_pool, 0, sizeof(const char *));
if (diff_cmd)
SVN_ERR(svn_path_cstring_to_utf8(&dwi->diff_cmd, diff_cmd,
result_pool));
else
dwi->diff_cmd = NULL;
/* If there was a command, arrange options to pass to it. */
if (dwi->diff_cmd)
{
const char **argv = NULL;
int argc = options->nelts;
if (argc)
{
int i;
argv = apr_palloc(result_pool, argc * sizeof(char *));
for (i = 0; i < argc; i++)
SVN_ERR(svn_utf_cstring_to_utf8(&argv[i],
APR_ARRAY_IDX(options, i, const char *), result_pool));
}
dwi->options.for_external.argv = argv;
dwi->options.for_external.argc = argc;
}
else /* No command, so arrange options for internal invocation instead. */
{
dwi->options.for_internal = svn_diff_file_options_create(result_pool);
SVN_ERR(svn_diff_file_options_parse(dwi->options.for_internal,
options, result_pool));
}
return SVN_NO_ERROR;
}
/* Set up *DIFF_PROCESSOR and *DDI for normal and git-style diffs (but not
* summary diffs).
*/
static svn_error_t *
get_diff_processor(svn_diff_tree_processor_t **diff_processor,
struct diff_driver_info_t **ddi,
const apr_array_header_t *options,
const char *relative_to_dir,
svn_boolean_t no_diff_added,
svn_boolean_t no_diff_deleted,
svn_boolean_t show_copies_as_adds,
svn_boolean_t ignore_content_type,
svn_boolean_t ignore_properties,
svn_boolean_t properties_only,
svn_boolean_t use_git_diff_format,
svn_boolean_t pretty_print_mergeinfo,
const char *header_encoding,
svn_stream_t *outstream,
svn_stream_t *errstream,
svn_client_ctx_t *ctx,
apr_pool_t *pool)
{
diff_writer_info_t *dwi = apr_pcalloc(pool, sizeof(*dwi));
svn_diff_tree_processor_t *processor;
/* setup callback and baton */
SVN_ERR(create_diff_writer_info(dwi, options,
ctx->config, pool));
dwi->pool = pool;
dwi->outstream = outstream;
dwi->errstream = errstream;
dwi->header_encoding = header_encoding;
dwi->force_binary = ignore_content_type;
dwi->ignore_properties = ignore_properties;
dwi->properties_only = properties_only;
dwi->relative_to_dir = relative_to_dir;
dwi->use_git_diff_format = use_git_diff_format;
dwi->no_diff_added = no_diff_added;
dwi->no_diff_deleted = no_diff_deleted;
dwi->show_copies_as_adds = show_copies_as_adds;
dwi->pretty_print_mergeinfo = pretty_print_mergeinfo;
dwi->cancel_func = ctx->cancel_func;
dwi->cancel_baton = ctx->cancel_baton;
dwi->ddi.wc_ctx = ctx->wc_ctx;
dwi->ddi.session_relpath = NULL;
dwi->ddi.anchor = NULL;
processor = svn_diff__tree_processor_create(dwi, pool);
processor->dir_added = diff_dir_added;
processor->dir_changed = diff_dir_changed;
processor->dir_deleted = diff_dir_deleted;
processor->file_added = diff_file_added;
processor->file_changed = diff_file_changed;
processor->file_deleted = diff_file_deleted;
*diff_processor = processor;
*ddi = &dwi->ddi;
return SVN_NO_ERROR;
}
svn_error_t *
svn_client__get_diff_writer_svn(
svn_diff_tree_processor_t **diff_processor,
const char *anchor,
const char *orig_path_1,
const char *orig_path_2,
const apr_array_header_t *options,
const char *relative_to_dir,
svn_boolean_t no_diff_added,
svn_boolean_t no_diff_deleted,
svn_boolean_t show_copies_as_adds,
svn_boolean_t ignore_content_type,
svn_boolean_t ignore_properties,
svn_boolean_t properties_only,
svn_boolean_t pretty_print_mergeinfo,
const char *header_encoding,
svn_stream_t *outstream,
svn_stream_t *errstream,
svn_client_ctx_t *ctx,
apr_pool_t *pool)
{
struct diff_driver_info_t *ddi;
SVN_ERR(get_diff_processor(diff_processor, &ddi,
options,
relative_to_dir,
no_diff_added,
no_diff_deleted,
show_copies_as_adds,
ignore_content_type,
ignore_properties,
properties_only,
FALSE /*use_git_diff_format*/,
pretty_print_mergeinfo,
header_encoding,
outstream, errstream,
ctx, pool));
ddi->anchor = anchor;
ddi->orig_path_1 = orig_path_1;
ddi->orig_path_2 = orig_path_2;
return SVN_NO_ERROR;
}
/*----------------------------------------------------------------------- */
/*** Public Interfaces. ***/
/* Display context diffs between two PATH/REVISION pairs. Each of
these inputs will be one of the following:
- a repository URL at a given revision.
- a working copy path, ignoring local mods.
- a working copy path, including local mods.
We can establish a matrix that shows the nine possible types of
diffs we expect to support.
` . DST || URL:rev | WC:base | WC:working |
` . || | | |
SRC ` . || | | |
============++============+============+============+
URL:rev || (*) | (*) | (*) |
|| | | |
|| | | |
|| | | |
------------++------------+------------+------------+
WC:base || (*) | |
|| | New svn_wc_diff which |
|| | is smart enough to |
|| | handle two WC paths |
------------++------------+ and their related +
WC:working || (*) | text-bases and working |
|| | files. This operation |
|| | is entirely local. |
|| | |
------------++------------+------------+------------+
* These cases require server communication.
*/
svn_error_t *
svn_client_diff7(const apr_array_header_t *options,
const char *path_or_url1,
const svn_opt_revision_t *revision1,
const char *path_or_url2,
const svn_opt_revision_t *revision2,
const char *relative_to_dir,
svn_depth_t depth,
svn_boolean_t ignore_ancestry,
svn_boolean_t no_diff_added,
svn_boolean_t no_diff_deleted,
svn_boolean_t show_copies_as_adds,
svn_boolean_t ignore_content_type,
svn_boolean_t ignore_properties,
svn_boolean_t properties_only,
svn_boolean_t use_git_diff_format,
svn_boolean_t pretty_print_mergeinfo,
const char *header_encoding,
svn_stream_t *outstream,
svn_stream_t *errstream,
const apr_array_header_t *changelists,
svn_client_ctx_t *ctx,
apr_pool_t *pool)
{
svn_opt_revision_t peg_revision;
svn_diff_tree_processor_t *diff_processor;
struct diff_driver_info_t *ddi;
if (ignore_properties && properties_only)
return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
_("Cannot ignore properties and show only "
"properties at the same time"));
/* We will never do a pegged diff from here. */
peg_revision.kind = svn_opt_revision_unspecified;
/* --show-copies-as-adds and --git imply --notice-ancestry */
if (show_copies_as_adds || use_git_diff_format)
ignore_ancestry = FALSE;
SVN_ERR(get_diff_processor(&diff_processor, &ddi,
options,
relative_to_dir,
no_diff_added,
no_diff_deleted,
show_copies_as_adds,
ignore_content_type,
ignore_properties,
properties_only,
use_git_diff_format,
pretty_print_mergeinfo,
header_encoding,
outstream, errstream,
ctx, pool));
return svn_error_trace(do_diff(ddi,
path_or_url1, path_or_url2,
revision1, revision2,
&peg_revision, TRUE /* no_peg_revision */,
depth, ignore_ancestry, changelists,
TRUE /* text_deltas */,
diff_processor, ctx, pool, pool));
}
svn_error_t *
svn_client_diff_peg7(const apr_array_header_t *options,
const char *path_or_url,
const svn_opt_revision_t *peg_revision,
const svn_opt_revision_t *start_revision,
const svn_opt_revision_t *end_revision,
const char *relative_to_dir,
svn_depth_t depth,
svn_boolean_t ignore_ancestry,
svn_boolean_t no_diff_added,
svn_boolean_t no_diff_deleted,
svn_boolean_t show_copies_as_adds,
svn_boolean_t ignore_content_type,
svn_boolean_t ignore_properties,
svn_boolean_t properties_only,
svn_boolean_t use_git_diff_format,
svn_boolean_t pretty_print_mergeinfo,
const char *header_encoding,
svn_stream_t *outstream,
svn_stream_t *errstream,
const apr_array_header_t *changelists,
svn_client_ctx_t *ctx,
apr_pool_t *pool)
{
svn_diff_tree_processor_t *diff_processor;
struct diff_driver_info_t *ddi;
if (ignore_properties && properties_only)
return svn_error_create(SVN_ERR_INCORRECT_PARAMS, NULL,
_("Cannot ignore properties and show only "
"properties at the same time"));
/* --show-copies-as-adds and --git imply --notice-ancestry */
if (show_copies_as_adds || use_git_diff_format)
ignore_ancestry = FALSE;
SVN_ERR(get_diff_processor(&diff_processor, &ddi,
options,
relative_to_dir,
no_diff_added,
no_diff_deleted,
show_copies_as_adds,
ignore_content_type,
ignore_properties,
properties_only,
use_git_diff_format,
pretty_print_mergeinfo,
header_encoding,
outstream, errstream,
ctx, pool));
return svn_error_trace(do_diff(ddi,
path_or_url, path_or_url,
start_revision, end_revision,
peg_revision, FALSE /* no_peg_revision */,
depth, ignore_ancestry, changelists,
TRUE /* text_deltas */,
diff_processor, ctx, pool, pool));
}
svn_error_t *
svn_client_diff_summarize2(const char *path_or_url1,
const svn_opt_revision_t *revision1,
const char *path_or_url2,
const svn_opt_revision_t *revision2,
svn_depth_t depth,
svn_boolean_t ignore_ancestry,
const apr_array_header_t *changelists,
svn_client_diff_summarize_func_t summarize_func,
void *summarize_baton,
svn_client_ctx_t *ctx,
apr_pool_t *pool)
{
svn_diff_tree_processor_t *diff_processor;
svn_opt_revision_t peg_revision;
/* We will never do a pegged diff from here. */
peg_revision.kind = svn_opt_revision_unspecified;
SVN_ERR(svn_client__get_diff_summarize_callbacks(&diff_processor,
summarize_func, summarize_baton,
pool, pool));
return svn_error_trace(do_diff(NULL,
path_or_url1, path_or_url2,
revision1, revision2,
&peg_revision, TRUE /* no_peg_revision */,
depth, ignore_ancestry, changelists,
FALSE /* text_deltas */,
diff_processor, ctx, pool, pool));
}
svn_error_t *
svn_client_diff_summarize_peg2(const char *path_or_url,
const svn_opt_revision_t *peg_revision,
const svn_opt_revision_t *start_revision,
const svn_opt_revision_t *end_revision,
svn_depth_t depth,
svn_boolean_t ignore_ancestry,
const apr_array_header_t *changelists,
svn_client_diff_summarize_func_t summarize_func,
void *summarize_baton,
svn_client_ctx_t *ctx,
apr_pool_t *pool)
{
svn_diff_tree_processor_t *diff_processor;
SVN_ERR(svn_client__get_diff_summarize_callbacks(&diff_processor,
summarize_func, summarize_baton,
pool, pool));
return svn_error_trace(do_diff(NULL,
path_or_url, path_or_url,
start_revision, end_revision,
peg_revision, FALSE /* no_peg_revision */,
depth, ignore_ancestry, changelists,
FALSE /* text_deltas */,
diff_processor, ctx, pool, pool));
}