/* * 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 #include #include #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'"), svn_dirent_local_style(local_abspath1, scratch_pool), svn_dirent_local_style(local_abspath2, scratch_pool)); 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 ? svn_dirent_local_style(local_abspath1, scratch_pool) : svn_dirent_local_style(local_abspath2, scratch_pool)); return SVN_NO_ERROR; }