diff options
Diffstat (limited to 'subversion/libsvn_client/diff_local.c')
-rw-r--r-- | subversion/libsvn_client/diff_local.c | 633 |
1 files changed, 633 insertions, 0 deletions
diff --git a/subversion/libsvn_client/diff_local.c b/subversion/libsvn_client/diff_local.c new file mode 100644 index 000000000000..cc7184f12cba --- /dev/null +++ b/subversion/libsvn_client/diff_local.c @@ -0,0 +1,633 @@ +/* + * diff_local.c: comparing local trees with each other + * + * ==================================================================== + * 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. + * ==================================================================== + */ + +/* ==================================================================== */ + + + +/*** Includes. ***/ + +#include <apr_strings.h> +#include <apr_pools.h> +#include <apr_hash.h> +#include "svn_hash.h" +#include "svn_types.h" +#include "svn_wc.h" +#include "svn_diff.h" +#include "svn_client.h" +#include "svn_string.h" +#include "svn_error.h" +#include "svn_dirent_uri.h" +#include "svn_io.h" +#include "svn_pools.h" +#include "svn_props.h" +#include "svn_sorts.h" +#include "svn_subst.h" +#include "client.h" + +#include "private/svn_wc_private.h" + +#include "svn_private_config.h" + + +/* Try to get properties for LOCAL_ABSPATH and return them in the property + * hash *PROPS. If there are no properties because LOCAL_ABSPATH is not + * versioned, return an empty property hash. */ +static svn_error_t * +get_props(apr_hash_t **props, + const char *local_abspath, + svn_wc_context_t *wc_ctx, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_error_t *err; + + err = svn_wc_prop_list2(props, wc_ctx, local_abspath, result_pool, + scratch_pool); + if (err) + { + if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND || + err->apr_err == SVN_ERR_WC_NOT_WORKING_COPY) + { + svn_error_clear(err); + *props = apr_hash_make(result_pool); + + /* ### Apply autoprops, like 'svn add' would? */ + } + else + return svn_error_trace(err); + } + + return SVN_NO_ERROR; +} + +/* Produce a diff between two arbitrary files at LOCAL_ABSPATH1 and + * LOCAL_ABSPATH2, using the diff callbacks from CALLBACKS. + * Use PATH as the name passed to diff callbacks. + * FILE1_IS_EMPTY and FILE2_IS_EMPTY are used as hints which diff callback + * function to use to compare the files (added/deleted/changed). + * + * If ORIGINAL_PROPS_OVERRIDE is not NULL, use it as original properties + * instead of reading properties from LOCAL_ABSPATH1. This is required when + * a file replaces a directory, where LOCAL_ABSPATH1 is an empty file that + * file content must be diffed against, but properties to diff against come + * from the replaced directory. */ +static svn_error_t * +do_arbitrary_files_diff(const char *local_abspath1, + const char *local_abspath2, + const char *path, + svn_boolean_t file1_is_empty, + svn_boolean_t file2_is_empty, + apr_hash_t *original_props_override, + const svn_wc_diff_callbacks4_t *callbacks, + void *diff_baton, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + apr_hash_t *original_props; + apr_hash_t *modified_props; + apr_array_header_t *prop_changes; + svn_string_t *original_mime_type = NULL; + svn_string_t *modified_mime_type = NULL; + + if (ctx->cancel_func) + SVN_ERR(ctx->cancel_func(ctx->cancel_baton)); + + /* Try to get properties from either file. It's OK if the files do not + * have properties, or if they are unversioned. */ + if (original_props_override) + original_props = original_props_override; + else + SVN_ERR(get_props(&original_props, local_abspath1, ctx->wc_ctx, + scratch_pool, scratch_pool)); + SVN_ERR(get_props(&modified_props, local_abspath2, ctx->wc_ctx, + scratch_pool, scratch_pool)); + + SVN_ERR(svn_prop_diffs(&prop_changes, modified_props, original_props, + scratch_pool)); + + /* Try to determine the mime-type of each file. */ + original_mime_type = svn_hash_gets(original_props, SVN_PROP_MIME_TYPE); + if (!file1_is_empty && !original_mime_type) + { + const char *mime_type; + SVN_ERR(svn_io_detect_mimetype2(&mime_type, local_abspath1, + ctx->mimetypes_map, scratch_pool)); + + if (mime_type) + original_mime_type = svn_string_create(mime_type, scratch_pool); + } + + modified_mime_type = svn_hash_gets(modified_props, SVN_PROP_MIME_TYPE); + if (!file2_is_empty && !modified_mime_type) + { + const char *mime_type; + SVN_ERR(svn_io_detect_mimetype2(&mime_type, local_abspath1, + ctx->mimetypes_map, scratch_pool)); + + if (mime_type) + modified_mime_type = svn_string_create(mime_type, scratch_pool); + } + + /* Produce the diff. */ + if (file1_is_empty && !file2_is_empty) + SVN_ERR(callbacks->file_added(NULL, NULL, NULL, path, + local_abspath1, local_abspath2, + /* ### TODO get real revision info + * for versioned files? */ + SVN_INVALID_REVNUM, SVN_INVALID_REVNUM, + original_mime_type ? + original_mime_type->data : NULL, + modified_mime_type ? + modified_mime_type->data : NULL, + /* ### TODO get copyfrom? */ + NULL, SVN_INVALID_REVNUM, + prop_changes, original_props, + diff_baton, scratch_pool)); + else if (!file1_is_empty && file2_is_empty) + SVN_ERR(callbacks->file_deleted(NULL, NULL, path, + local_abspath1, local_abspath2, + original_mime_type ? + original_mime_type->data : NULL, + modified_mime_type ? + modified_mime_type->data : NULL, + original_props, + diff_baton, scratch_pool)); + else + { + svn_stream_t *file1; + svn_stream_t *file2; + svn_boolean_t same; + svn_string_t *val; + /* We have two files, which may or may not be the same. + + ### Our caller assumes that we should ignore symlinks here and + handle them as normal paths. Perhaps that should change? + */ + SVN_ERR(svn_stream_open_readonly(&file1, local_abspath1, scratch_pool, + scratch_pool)); + + SVN_ERR(svn_stream_open_readonly(&file2, local_abspath2, scratch_pool, + scratch_pool)); + + /* Wrap with normalization, etc. if necessary */ + if (original_props) + { + val = svn_hash_gets(original_props, SVN_PROP_EOL_STYLE); + + if (val) + { + svn_subst_eol_style_t style; + const char *eol; + svn_subst_eol_style_from_value(&style, &eol, val->data); + + /* ### Ignoring keywords */ + if (eol) + file1 = svn_subst_stream_translated(file1, eol, TRUE, + NULL, FALSE, + scratch_pool); + } + } + + if (modified_props) + { + val = svn_hash_gets(modified_props, SVN_PROP_EOL_STYLE); + + if (val) + { + svn_subst_eol_style_t style; + const char *eol; + svn_subst_eol_style_from_value(&style, &eol, val->data); + + /* ### Ignoring keywords */ + if (eol) + file2 = svn_subst_stream_translated(file2, eol, TRUE, + NULL, FALSE, + scratch_pool); + } + } + + SVN_ERR(svn_stream_contents_same2(&same, file1, file2, scratch_pool)); + + if (! same || prop_changes->nelts > 0) + { + /* ### We should probably pass the normalized data we created using + the subst streams as that is what diff users expect */ + SVN_ERR(callbacks->file_changed(NULL, NULL, NULL, path, + same ? NULL : local_abspath1, + same ? NULL : local_abspath2, + /* ### TODO get real revision info + * for versioned files? */ + SVN_INVALID_REVNUM /* rev1 */, + SVN_INVALID_REVNUM /* rev2 */, + original_mime_type ? + original_mime_type->data : NULL, + modified_mime_type ? + modified_mime_type->data : NULL, + prop_changes, original_props, + diff_baton, scratch_pool)); + } + } + + return SVN_NO_ERROR; +} + +struct arbitrary_diff_walker_baton { + /* The root directories of the trees being compared. */ + const char *root1_abspath; + const char *root2_abspath; + + /* TRUE if recursing within an added subtree of root2_abspath that + * does not exist in root1_abspath. */ + svn_boolean_t recursing_within_added_subtree; + + /* TRUE if recursing within an administrative (.i.e. .svn) directory. */ + svn_boolean_t recursing_within_adm_dir; + + /* The absolute path of the adm dir if RECURSING_WITHIN_ADM_DIR is TRUE. + * Else this is NULL.*/ + const char *adm_dir_abspath; + + /* A path to an empty file used for diffs that add/delete files. */ + const char *empty_file_abspath; + + const svn_wc_diff_callbacks4_t *callbacks; + void *diff_baton; + svn_client_ctx_t *ctx; + apr_pool_t *pool; +} arbitrary_diff_walker_baton; + +/* Forward declaration needed because this function has a cyclic + * dependency with do_arbitrary_dirs_diff(). */ +static svn_error_t * +arbitrary_diff_walker(void *baton, const char *local_abspath, + const apr_finfo_t *finfo, + apr_pool_t *scratch_pool); + +/* Another forward declaration. */ +static svn_error_t * +arbitrary_diff_this_dir(struct arbitrary_diff_walker_baton *b, + const char *local_abspath, + svn_depth_t depth, + apr_pool_t *scratch_pool); + +/* Produce a diff of depth DEPTH between two arbitrary directories at + * LOCAL_ABSPATH1 and LOCAL_ABSPATH2, using the provided diff callbacks + * to show file changes and, for versioned nodes, property changes. + * + * If ROOT_ABSPATH1 and ROOT_ABSPATH2 are not NULL, show paths in diffs + * relative to these roots, rather than relative to LOCAL_ABSPATH1 and + * LOCAL_ABSPATH2. This is needed when crawling a subtree that exists + * only within LOCAL_ABSPATH2. */ +static svn_error_t * +do_arbitrary_dirs_diff(const char *local_abspath1, + const char *local_abspath2, + const char *root_abspath1, + const char *root_abspath2, + svn_depth_t depth, + const svn_wc_diff_callbacks4_t *callbacks, + void *diff_baton, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + apr_file_t *empty_file; + svn_node_kind_t kind1; + + struct arbitrary_diff_walker_baton b; + + /* If LOCAL_ABSPATH1 is not a directory, crawl LOCAL_ABSPATH2 instead + * and compare it to LOCAL_ABSPATH1, showing only additions. + * This case can only happen during recursion from arbitrary_diff_walker(), + * because do_arbitrary_nodes_diff() prevents this from happening at + * the root of the comparison. */ + SVN_ERR(svn_io_check_resolved_path(local_abspath1, &kind1, scratch_pool)); + b.recursing_within_added_subtree = (kind1 != svn_node_dir); + + b.root1_abspath = root_abspath1 ? root_abspath1 : local_abspath1; + b.root2_abspath = root_abspath2 ? root_abspath2 : local_abspath2; + b.recursing_within_adm_dir = FALSE; + b.adm_dir_abspath = NULL; + b.callbacks = callbacks; + b.diff_baton = diff_baton; + b.ctx = ctx; + b.pool = scratch_pool; + + SVN_ERR(svn_io_open_unique_file3(&empty_file, &b.empty_file_abspath, + NULL, svn_io_file_del_on_pool_cleanup, + scratch_pool, scratch_pool)); + + if (depth <= svn_depth_immediates) + SVN_ERR(arbitrary_diff_this_dir(&b, local_abspath1, depth, scratch_pool)); + else if (depth == svn_depth_infinity) + SVN_ERR(svn_io_dir_walk2(b.recursing_within_added_subtree ? local_abspath2 + : local_abspath1, + 0, arbitrary_diff_walker, &b, scratch_pool)); + return SVN_NO_ERROR; +} + +/* Produce a diff of depth DEPTH for the directory at LOCAL_ABSPATH, + * using information from the arbitrary_diff_walker_baton B. + * LOCAL_ABSPATH is the path being crawled and can be on either side + * of the diff depending on baton->recursing_within_added_subtree. */ +static svn_error_t * +arbitrary_diff_this_dir(struct arbitrary_diff_walker_baton *b, + const char *local_abspath, + svn_depth_t depth, + apr_pool_t *scratch_pool) +{ + const char *local_abspath1; + const char *local_abspath2; + svn_node_kind_t kind1; + svn_node_kind_t kind2; + const char *child_relpath; + apr_hash_t *dirents1; + apr_hash_t *dirents2; + apr_hash_t *merged_dirents; + apr_array_header_t *sorted_dirents; + int i; + apr_pool_t *iterpool; + + if (b->recursing_within_adm_dir) + { + if (svn_dirent_skip_ancestor(b->adm_dir_abspath, local_abspath)) + return SVN_NO_ERROR; + else + { + b->recursing_within_adm_dir = FALSE; + b->adm_dir_abspath = NULL; + } + } + else if (svn_wc_is_adm_dir(svn_dirent_basename(local_abspath, NULL), + scratch_pool)) + { + b->recursing_within_adm_dir = TRUE; + b->adm_dir_abspath = apr_pstrdup(b->pool, local_abspath); + return SVN_NO_ERROR; + } + + if (b->recursing_within_added_subtree) + child_relpath = svn_dirent_skip_ancestor(b->root2_abspath, local_abspath); + else + child_relpath = svn_dirent_skip_ancestor(b->root1_abspath, local_abspath); + if (!child_relpath) + return SVN_NO_ERROR; + + local_abspath1 = svn_dirent_join(b->root1_abspath, child_relpath, + scratch_pool); + SVN_ERR(svn_io_check_resolved_path(local_abspath1, &kind1, scratch_pool)); + + local_abspath2 = svn_dirent_join(b->root2_abspath, child_relpath, + scratch_pool); + SVN_ERR(svn_io_check_resolved_path(local_abspath2, &kind2, scratch_pool)); + + if (depth > svn_depth_empty) + { + if (kind1 == svn_node_dir) + SVN_ERR(svn_io_get_dirents3(&dirents1, local_abspath1, + TRUE, /* only_check_type */ + scratch_pool, scratch_pool)); + else + dirents1 = apr_hash_make(scratch_pool); + } + + if (kind2 == svn_node_dir) + { + apr_hash_t *original_props; + apr_hash_t *modified_props; + apr_array_header_t *prop_changes; + + /* Show any property changes for this directory. */ + SVN_ERR(get_props(&original_props, local_abspath1, b->ctx->wc_ctx, + scratch_pool, scratch_pool)); + SVN_ERR(get_props(&modified_props, local_abspath2, b->ctx->wc_ctx, + scratch_pool, scratch_pool)); + SVN_ERR(svn_prop_diffs(&prop_changes, modified_props, original_props, + scratch_pool)); + if (prop_changes->nelts > 0) + SVN_ERR(b->callbacks->dir_props_changed(NULL, NULL, child_relpath, + FALSE /* was_added */, + prop_changes, original_props, + b->diff_baton, + scratch_pool)); + + if (depth > svn_depth_empty) + { + /* Read directory entries. */ + SVN_ERR(svn_io_get_dirents3(&dirents2, local_abspath2, + TRUE, /* only_check_type */ + scratch_pool, scratch_pool)); + } + } + else if (depth > svn_depth_empty) + dirents2 = apr_hash_make(scratch_pool); + + if (depth <= svn_depth_empty) + return SVN_NO_ERROR; + + /* Compare dirents1 to dirents2 and show added/deleted/changed files. */ + merged_dirents = apr_hash_merge(scratch_pool, dirents1, dirents2, + NULL, NULL); + sorted_dirents = svn_sort__hash(merged_dirents, + svn_sort_compare_items_as_paths, + scratch_pool); + iterpool = svn_pool_create(scratch_pool); + for (i = 0; i < sorted_dirents->nelts; i++) + { + svn_sort__item_t elt = APR_ARRAY_IDX(sorted_dirents, i, svn_sort__item_t); + const char *name = elt.key; + svn_io_dirent2_t *dirent1; + svn_io_dirent2_t *dirent2; + const char *child1_abspath; + const char *child2_abspath; + + svn_pool_clear(iterpool); + + if (b->ctx->cancel_func) + SVN_ERR(b->ctx->cancel_func(b->ctx->cancel_baton)); + + if (strcmp(name, SVN_WC_ADM_DIR_NAME) == 0) + continue; + + dirent1 = svn_hash_gets(dirents1, name); + if (!dirent1) + { + dirent1 = svn_io_dirent2_create(iterpool); + dirent1->kind = svn_node_none; + } + dirent2 = svn_hash_gets(dirents2, name); + if (!dirent2) + { + dirent2 = svn_io_dirent2_create(iterpool); + dirent2->kind = svn_node_none; + } + + child1_abspath = svn_dirent_join(local_abspath1, name, iterpool); + child2_abspath = svn_dirent_join(local_abspath2, name, iterpool); + + if (dirent1->special) + SVN_ERR(svn_io_check_resolved_path(child1_abspath, &dirent1->kind, + iterpool)); + if (dirent2->special) + SVN_ERR(svn_io_check_resolved_path(child1_abspath, &dirent2->kind, + iterpool)); + + if (dirent1->kind == svn_node_dir && + dirent2->kind == svn_node_dir) + { + if (depth == svn_depth_immediates) + { + /* Not using the walker, so show property diffs on these dirs. */ + SVN_ERR(do_arbitrary_dirs_diff(child1_abspath, child2_abspath, + b->root1_abspath, b->root2_abspath, + svn_depth_empty, + b->callbacks, b->diff_baton, + b->ctx, iterpool)); + } + else + { + /* Either the walker will visit these directories (with + * depth=infinity) and they will be processed as 'this dir' + * later, or we're showing file children only (depth=files). */ + continue; + } + + } + + /* Files that exist only in dirents1. */ + if (dirent1->kind == svn_node_file && + (dirent2->kind == svn_node_dir || dirent2->kind == svn_node_none)) + SVN_ERR(do_arbitrary_files_diff(child1_abspath, b->empty_file_abspath, + svn_relpath_join(child_relpath, name, + iterpool), + FALSE, TRUE, NULL, + b->callbacks, b->diff_baton, + b->ctx, iterpool)); + + /* Files that exist only in dirents2. */ + if (dirent2->kind == svn_node_file && + (dirent1->kind == svn_node_dir || dirent1->kind == svn_node_none)) + { + apr_hash_t *original_props; + + SVN_ERR(get_props(&original_props, child1_abspath, b->ctx->wc_ctx, + scratch_pool, scratch_pool)); + SVN_ERR(do_arbitrary_files_diff(b->empty_file_abspath, child2_abspath, + svn_relpath_join(child_relpath, name, + iterpool), + TRUE, FALSE, original_props, + b->callbacks, b->diff_baton, + b->ctx, iterpool)); + } + + /* Files that exist in dirents1 and dirents2. */ + if (dirent1->kind == svn_node_file && dirent2->kind == svn_node_file) + SVN_ERR(do_arbitrary_files_diff(child1_abspath, child2_abspath, + svn_relpath_join(child_relpath, name, + iterpool), + FALSE, FALSE, NULL, + b->callbacks, b->diff_baton, + b->ctx, scratch_pool)); + + /* Directories that only exist in dirents2. These aren't crawled + * by this walker so we have to crawl them separately. */ + if (depth > svn_depth_files && + dirent2->kind == svn_node_dir && + (dirent1->kind == svn_node_file || dirent1->kind == svn_node_none)) + SVN_ERR(do_arbitrary_dirs_diff(child1_abspath, child2_abspath, + b->root1_abspath, b->root2_abspath, + depth <= svn_depth_immediates + ? svn_depth_empty + : svn_depth_infinity , + b->callbacks, b->diff_baton, + b->ctx, iterpool)); + } + + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +/* An implementation of svn_io_walk_func_t. + * Note: LOCAL_ABSPATH is the path being crawled and can be on either side + * of the diff depending on baton->recursing_within_added_subtree. */ +static svn_error_t * +arbitrary_diff_walker(void *baton, const char *local_abspath, + const apr_finfo_t *finfo, + apr_pool_t *scratch_pool) +{ + struct arbitrary_diff_walker_baton *b = baton; + + if (b->ctx->cancel_func) + SVN_ERR(b->ctx->cancel_func(b->ctx->cancel_baton)); + + if (finfo->filetype != APR_DIR) + return SVN_NO_ERROR; + + SVN_ERR(arbitrary_diff_this_dir(b, local_abspath, svn_depth_infinity, + scratch_pool)); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_client__arbitrary_nodes_diff(const char *local_abspath1, + const char *local_abspath2, + svn_depth_t depth, + const svn_wc_diff_callbacks4_t *callbacks, + void *diff_baton, + svn_client_ctx_t *ctx, + apr_pool_t *scratch_pool) +{ + svn_node_kind_t kind1; + svn_node_kind_t kind2; + + SVN_ERR(svn_io_check_resolved_path(local_abspath1, &kind1, scratch_pool)); + SVN_ERR(svn_io_check_resolved_path(local_abspath2, &kind2, scratch_pool)); + + if (kind1 != kind2) + return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL, + _("'%s' is not the same node kind as '%s'"), + local_abspath1, local_abspath2); + + if (depth == svn_depth_unknown) + depth = svn_depth_infinity; + + if (kind1 == svn_node_file) + SVN_ERR(do_arbitrary_files_diff(local_abspath1, local_abspath2, + svn_dirent_basename(local_abspath1, + scratch_pool), + FALSE, FALSE, NULL, + callbacks, diff_baton, + ctx, scratch_pool)); + else if (kind1 == svn_node_dir) + SVN_ERR(do_arbitrary_dirs_diff(local_abspath1, local_abspath2, + NULL, NULL, depth, + callbacks, diff_baton, + ctx, scratch_pool)); + else + return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL, + _("'%s' is not a file or directory"), + kind1 == svn_node_none ? + local_abspath1 : local_abspath2); + return SVN_NO_ERROR; +} |