2 * diff_local.c: comparing local trees with each other
4 * ====================================================================
5 * Licensed to the Apache Software Foundation (ASF) under one
6 * or more contributor license agreements. See the NOTICE file
7 * distributed with this work for additional information
8 * regarding copyright ownership. The ASF licenses this file
9 * to you under the Apache License, Version 2.0 (the
10 * "License"); you may not use this file except in compliance
11 * with the License. You may obtain a copy of the License at
13 * http://www.apache.org/licenses/LICENSE-2.0
15 * Unless required by applicable law or agreed to in writing,
16 * software distributed under the License is distributed on an
17 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18 * KIND, either express or implied. See the License for the
19 * specific language governing permissions and limitations
21 * ====================================================================
24 /* ==================================================================== */
30 #include <apr_strings.h>
31 #include <apr_pools.h>
34 #include "svn_types.h"
37 #include "svn_client.h"
38 #include "svn_string.h"
39 #include "svn_error.h"
40 #include "svn_dirent_uri.h"
42 #include "svn_pools.h"
43 #include "svn_props.h"
44 #include "svn_sorts.h"
45 #include "svn_subst.h"
48 #include "private/svn_wc_private.h"
50 #include "svn_private_config.h"
53 /* Try to get properties for LOCAL_ABSPATH and return them in the property
54 * hash *PROPS. If there are no properties because LOCAL_ABSPATH is not
55 * versioned, return an empty property hash. */
57 get_props(apr_hash_t **props,
58 const char *local_abspath,
59 svn_wc_context_t *wc_ctx,
60 apr_pool_t *result_pool,
61 apr_pool_t *scratch_pool)
65 err = svn_wc_prop_list2(props, wc_ctx, local_abspath, result_pool,
69 if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND ||
70 err->apr_err == SVN_ERR_WC_NOT_WORKING_COPY)
73 *props = apr_hash_make(result_pool);
75 /* ### Apply autoprops, like 'svn add' would? */
78 return svn_error_trace(err);
84 /* Produce a diff between two arbitrary files at LOCAL_ABSPATH1 and
85 * LOCAL_ABSPATH2, using the diff callbacks from CALLBACKS.
86 * Use PATH as the name passed to diff callbacks.
87 * FILE1_IS_EMPTY and FILE2_IS_EMPTY are used as hints which diff callback
88 * function to use to compare the files (added/deleted/changed).
90 * If ORIGINAL_PROPS_OVERRIDE is not NULL, use it as original properties
91 * instead of reading properties from LOCAL_ABSPATH1. This is required when
92 * a file replaces a directory, where LOCAL_ABSPATH1 is an empty file that
93 * file content must be diffed against, but properties to diff against come
94 * from the replaced directory. */
96 do_arbitrary_files_diff(const char *local_abspath1,
97 const char *local_abspath2,
99 svn_boolean_t file1_is_empty,
100 svn_boolean_t file2_is_empty,
101 apr_hash_t *original_props_override,
102 const svn_wc_diff_callbacks4_t *callbacks,
104 svn_client_ctx_t *ctx,
105 apr_pool_t *scratch_pool)
107 apr_hash_t *original_props;
108 apr_hash_t *modified_props;
109 apr_array_header_t *prop_changes;
110 svn_string_t *original_mime_type = NULL;
111 svn_string_t *modified_mime_type = NULL;
113 if (ctx->cancel_func)
114 SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
116 /* Try to get properties from either file. It's OK if the files do not
117 * have properties, or if they are unversioned. */
118 if (original_props_override)
119 original_props = original_props_override;
121 SVN_ERR(get_props(&original_props, local_abspath1, ctx->wc_ctx,
122 scratch_pool, scratch_pool));
123 SVN_ERR(get_props(&modified_props, local_abspath2, ctx->wc_ctx,
124 scratch_pool, scratch_pool));
126 SVN_ERR(svn_prop_diffs(&prop_changes, modified_props, original_props,
129 /* Try to determine the mime-type of each file. */
130 original_mime_type = svn_hash_gets(original_props, SVN_PROP_MIME_TYPE);
131 if (!file1_is_empty && !original_mime_type)
133 const char *mime_type;
134 SVN_ERR(svn_io_detect_mimetype2(&mime_type, local_abspath1,
135 ctx->mimetypes_map, scratch_pool));
138 original_mime_type = svn_string_create(mime_type, scratch_pool);
141 modified_mime_type = svn_hash_gets(modified_props, SVN_PROP_MIME_TYPE);
142 if (!file2_is_empty && !modified_mime_type)
144 const char *mime_type;
145 SVN_ERR(svn_io_detect_mimetype2(&mime_type, local_abspath1,
146 ctx->mimetypes_map, scratch_pool));
149 modified_mime_type = svn_string_create(mime_type, scratch_pool);
152 /* Produce the diff. */
153 if (file1_is_empty && !file2_is_empty)
154 SVN_ERR(callbacks->file_added(NULL, NULL, NULL, path,
155 local_abspath1, local_abspath2,
156 /* ### TODO get real revision info
157 * for versioned files? */
158 SVN_INVALID_REVNUM, SVN_INVALID_REVNUM,
160 original_mime_type->data : NULL,
162 modified_mime_type->data : NULL,
163 /* ### TODO get copyfrom? */
164 NULL, SVN_INVALID_REVNUM,
165 prop_changes, original_props,
166 diff_baton, scratch_pool));
167 else if (!file1_is_empty && file2_is_empty)
168 SVN_ERR(callbacks->file_deleted(NULL, NULL, path,
169 local_abspath1, local_abspath2,
171 original_mime_type->data : NULL,
173 modified_mime_type->data : NULL,
175 diff_baton, scratch_pool));
182 /* We have two files, which may or may not be the same.
184 ### Our caller assumes that we should ignore symlinks here and
185 handle them as normal paths. Perhaps that should change?
187 SVN_ERR(svn_stream_open_readonly(&file1, local_abspath1, scratch_pool,
190 SVN_ERR(svn_stream_open_readonly(&file2, local_abspath2, scratch_pool,
193 /* Wrap with normalization, etc. if necessary */
196 val = svn_hash_gets(original_props, SVN_PROP_EOL_STYLE);
200 svn_subst_eol_style_t style;
202 svn_subst_eol_style_from_value(&style, &eol, val->data);
204 /* ### Ignoring keywords */
206 file1 = svn_subst_stream_translated(file1, eol, TRUE,
214 val = svn_hash_gets(modified_props, SVN_PROP_EOL_STYLE);
218 svn_subst_eol_style_t style;
220 svn_subst_eol_style_from_value(&style, &eol, val->data);
222 /* ### Ignoring keywords */
224 file2 = svn_subst_stream_translated(file2, eol, TRUE,
230 SVN_ERR(svn_stream_contents_same2(&same, file1, file2, scratch_pool));
232 if (! same || prop_changes->nelts > 0)
234 /* ### We should probably pass the normalized data we created using
235 the subst streams as that is what diff users expect */
236 SVN_ERR(callbacks->file_changed(NULL, NULL, NULL, path,
237 same ? NULL : local_abspath1,
238 same ? NULL : local_abspath2,
239 /* ### TODO get real revision info
240 * for versioned files? */
241 SVN_INVALID_REVNUM /* rev1 */,
242 SVN_INVALID_REVNUM /* rev2 */,
244 original_mime_type->data : NULL,
246 modified_mime_type->data : NULL,
247 prop_changes, original_props,
248 diff_baton, scratch_pool));
255 struct arbitrary_diff_walker_baton {
256 /* The root directories of the trees being compared. */
257 const char *root1_abspath;
258 const char *root2_abspath;
260 /* TRUE if recursing within an added subtree of root2_abspath that
261 * does not exist in root1_abspath. */
262 svn_boolean_t recursing_within_added_subtree;
264 /* TRUE if recursing within an administrative (.i.e. .svn) directory. */
265 svn_boolean_t recursing_within_adm_dir;
267 /* The absolute path of the adm dir if RECURSING_WITHIN_ADM_DIR is TRUE.
268 * Else this is NULL.*/
269 const char *adm_dir_abspath;
271 /* A path to an empty file used for diffs that add/delete files. */
272 const char *empty_file_abspath;
274 const svn_wc_diff_callbacks4_t *callbacks;
276 svn_client_ctx_t *ctx;
278 } arbitrary_diff_walker_baton;
280 /* Forward declaration needed because this function has a cyclic
281 * dependency with do_arbitrary_dirs_diff(). */
283 arbitrary_diff_walker(void *baton, const char *local_abspath,
284 const apr_finfo_t *finfo,
285 apr_pool_t *scratch_pool);
287 /* Another forward declaration. */
289 arbitrary_diff_this_dir(struct arbitrary_diff_walker_baton *b,
290 const char *local_abspath,
292 apr_pool_t *scratch_pool);
294 /* Produce a diff of depth DEPTH between two arbitrary directories at
295 * LOCAL_ABSPATH1 and LOCAL_ABSPATH2, using the provided diff callbacks
296 * to show file changes and, for versioned nodes, property changes.
298 * If ROOT_ABSPATH1 and ROOT_ABSPATH2 are not NULL, show paths in diffs
299 * relative to these roots, rather than relative to LOCAL_ABSPATH1 and
300 * LOCAL_ABSPATH2. This is needed when crawling a subtree that exists
301 * only within LOCAL_ABSPATH2. */
303 do_arbitrary_dirs_diff(const char *local_abspath1,
304 const char *local_abspath2,
305 const char *root_abspath1,
306 const char *root_abspath2,
308 const svn_wc_diff_callbacks4_t *callbacks,
310 svn_client_ctx_t *ctx,
311 apr_pool_t *scratch_pool)
313 apr_file_t *empty_file;
314 svn_node_kind_t kind1;
316 struct arbitrary_diff_walker_baton b;
318 /* If LOCAL_ABSPATH1 is not a directory, crawl LOCAL_ABSPATH2 instead
319 * and compare it to LOCAL_ABSPATH1, showing only additions.
320 * This case can only happen during recursion from arbitrary_diff_walker(),
321 * because do_arbitrary_nodes_diff() prevents this from happening at
322 * the root of the comparison. */
323 SVN_ERR(svn_io_check_resolved_path(local_abspath1, &kind1, scratch_pool));
324 b.recursing_within_added_subtree = (kind1 != svn_node_dir);
326 b.root1_abspath = root_abspath1 ? root_abspath1 : local_abspath1;
327 b.root2_abspath = root_abspath2 ? root_abspath2 : local_abspath2;
328 b.recursing_within_adm_dir = FALSE;
329 b.adm_dir_abspath = NULL;
330 b.callbacks = callbacks;
331 b.diff_baton = diff_baton;
333 b.pool = scratch_pool;
335 SVN_ERR(svn_io_open_unique_file3(&empty_file, &b.empty_file_abspath,
336 NULL, svn_io_file_del_on_pool_cleanup,
337 scratch_pool, scratch_pool));
339 if (depth <= svn_depth_immediates)
340 SVN_ERR(arbitrary_diff_this_dir(&b, local_abspath1, depth, scratch_pool));
341 else if (depth == svn_depth_infinity)
342 SVN_ERR(svn_io_dir_walk2(b.recursing_within_added_subtree ? local_abspath2
344 0, arbitrary_diff_walker, &b, scratch_pool));
348 /* Produce a diff of depth DEPTH for the directory at LOCAL_ABSPATH,
349 * using information from the arbitrary_diff_walker_baton B.
350 * LOCAL_ABSPATH is the path being crawled and can be on either side
351 * of the diff depending on baton->recursing_within_added_subtree. */
353 arbitrary_diff_this_dir(struct arbitrary_diff_walker_baton *b,
354 const char *local_abspath,
356 apr_pool_t *scratch_pool)
358 const char *local_abspath1;
359 const char *local_abspath2;
360 svn_node_kind_t kind1;
361 svn_node_kind_t kind2;
362 const char *child_relpath;
363 apr_hash_t *dirents1;
364 apr_hash_t *dirents2;
365 apr_hash_t *merged_dirents;
366 apr_array_header_t *sorted_dirents;
368 apr_pool_t *iterpool;
370 if (b->recursing_within_adm_dir)
372 if (svn_dirent_skip_ancestor(b->adm_dir_abspath, local_abspath))
376 b->recursing_within_adm_dir = FALSE;
377 b->adm_dir_abspath = NULL;
380 else if (svn_wc_is_adm_dir(svn_dirent_basename(local_abspath, NULL),
383 b->recursing_within_adm_dir = TRUE;
384 b->adm_dir_abspath = apr_pstrdup(b->pool, local_abspath);
388 if (b->recursing_within_added_subtree)
389 child_relpath = svn_dirent_skip_ancestor(b->root2_abspath, local_abspath);
391 child_relpath = svn_dirent_skip_ancestor(b->root1_abspath, local_abspath);
395 local_abspath1 = svn_dirent_join(b->root1_abspath, child_relpath,
397 SVN_ERR(svn_io_check_resolved_path(local_abspath1, &kind1, scratch_pool));
399 local_abspath2 = svn_dirent_join(b->root2_abspath, child_relpath,
401 SVN_ERR(svn_io_check_resolved_path(local_abspath2, &kind2, scratch_pool));
403 if (depth > svn_depth_empty)
405 if (kind1 == svn_node_dir)
406 SVN_ERR(svn_io_get_dirents3(&dirents1, local_abspath1,
407 TRUE, /* only_check_type */
408 scratch_pool, scratch_pool));
410 dirents1 = apr_hash_make(scratch_pool);
413 if (kind2 == svn_node_dir)
415 apr_hash_t *original_props;
416 apr_hash_t *modified_props;
417 apr_array_header_t *prop_changes;
419 /* Show any property changes for this directory. */
420 SVN_ERR(get_props(&original_props, local_abspath1, b->ctx->wc_ctx,
421 scratch_pool, scratch_pool));
422 SVN_ERR(get_props(&modified_props, local_abspath2, b->ctx->wc_ctx,
423 scratch_pool, scratch_pool));
424 SVN_ERR(svn_prop_diffs(&prop_changes, modified_props, original_props,
426 if (prop_changes->nelts > 0)
427 SVN_ERR(b->callbacks->dir_props_changed(NULL, NULL, child_relpath,
428 FALSE /* was_added */,
429 prop_changes, original_props,
433 if (depth > svn_depth_empty)
435 /* Read directory entries. */
436 SVN_ERR(svn_io_get_dirents3(&dirents2, local_abspath2,
437 TRUE, /* only_check_type */
438 scratch_pool, scratch_pool));
441 else if (depth > svn_depth_empty)
442 dirents2 = apr_hash_make(scratch_pool);
444 if (depth <= svn_depth_empty)
447 /* Compare dirents1 to dirents2 and show added/deleted/changed files. */
448 merged_dirents = apr_hash_merge(scratch_pool, dirents1, dirents2,
450 sorted_dirents = svn_sort__hash(merged_dirents,
451 svn_sort_compare_items_as_paths,
453 iterpool = svn_pool_create(scratch_pool);
454 for (i = 0; i < sorted_dirents->nelts; i++)
456 svn_sort__item_t elt = APR_ARRAY_IDX(sorted_dirents, i, svn_sort__item_t);
457 const char *name = elt.key;
458 svn_io_dirent2_t *dirent1;
459 svn_io_dirent2_t *dirent2;
460 const char *child1_abspath;
461 const char *child2_abspath;
463 svn_pool_clear(iterpool);
465 if (b->ctx->cancel_func)
466 SVN_ERR(b->ctx->cancel_func(b->ctx->cancel_baton));
468 if (strcmp(name, SVN_WC_ADM_DIR_NAME) == 0)
471 dirent1 = svn_hash_gets(dirents1, name);
474 dirent1 = svn_io_dirent2_create(iterpool);
475 dirent1->kind = svn_node_none;
477 dirent2 = svn_hash_gets(dirents2, name);
480 dirent2 = svn_io_dirent2_create(iterpool);
481 dirent2->kind = svn_node_none;
484 child1_abspath = svn_dirent_join(local_abspath1, name, iterpool);
485 child2_abspath = svn_dirent_join(local_abspath2, name, iterpool);
487 if (dirent1->special)
488 SVN_ERR(svn_io_check_resolved_path(child1_abspath, &dirent1->kind,
490 if (dirent2->special)
491 SVN_ERR(svn_io_check_resolved_path(child1_abspath, &dirent2->kind,
494 if (dirent1->kind == svn_node_dir &&
495 dirent2->kind == svn_node_dir)
497 if (depth == svn_depth_immediates)
499 /* Not using the walker, so show property diffs on these dirs. */
500 SVN_ERR(do_arbitrary_dirs_diff(child1_abspath, child2_abspath,
501 b->root1_abspath, b->root2_abspath,
503 b->callbacks, b->diff_baton,
508 /* Either the walker will visit these directories (with
509 * depth=infinity) and they will be processed as 'this dir'
510 * later, or we're showing file children only (depth=files). */
516 /* Files that exist only in dirents1. */
517 if (dirent1->kind == svn_node_file &&
518 (dirent2->kind == svn_node_dir || dirent2->kind == svn_node_none))
519 SVN_ERR(do_arbitrary_files_diff(child1_abspath, b->empty_file_abspath,
520 svn_relpath_join(child_relpath, name,
523 b->callbacks, b->diff_baton,
526 /* Files that exist only in dirents2. */
527 if (dirent2->kind == svn_node_file &&
528 (dirent1->kind == svn_node_dir || dirent1->kind == svn_node_none))
530 apr_hash_t *original_props;
532 SVN_ERR(get_props(&original_props, child1_abspath, b->ctx->wc_ctx,
533 scratch_pool, scratch_pool));
534 SVN_ERR(do_arbitrary_files_diff(b->empty_file_abspath, child2_abspath,
535 svn_relpath_join(child_relpath, name,
537 TRUE, FALSE, original_props,
538 b->callbacks, b->diff_baton,
542 /* Files that exist in dirents1 and dirents2. */
543 if (dirent1->kind == svn_node_file && dirent2->kind == svn_node_file)
544 SVN_ERR(do_arbitrary_files_diff(child1_abspath, child2_abspath,
545 svn_relpath_join(child_relpath, name,
548 b->callbacks, b->diff_baton,
549 b->ctx, scratch_pool));
551 /* Directories that only exist in dirents2. These aren't crawled
552 * by this walker so we have to crawl them separately. */
553 if (depth > svn_depth_files &&
554 dirent2->kind == svn_node_dir &&
555 (dirent1->kind == svn_node_file || dirent1->kind == svn_node_none))
556 SVN_ERR(do_arbitrary_dirs_diff(child1_abspath, child2_abspath,
557 b->root1_abspath, b->root2_abspath,
558 depth <= svn_depth_immediates
560 : svn_depth_infinity ,
561 b->callbacks, b->diff_baton,
565 svn_pool_destroy(iterpool);
570 /* An implementation of svn_io_walk_func_t.
571 * Note: LOCAL_ABSPATH is the path being crawled and can be on either side
572 * of the diff depending on baton->recursing_within_added_subtree. */
574 arbitrary_diff_walker(void *baton, const char *local_abspath,
575 const apr_finfo_t *finfo,
576 apr_pool_t *scratch_pool)
578 struct arbitrary_diff_walker_baton *b = baton;
580 if (b->ctx->cancel_func)
581 SVN_ERR(b->ctx->cancel_func(b->ctx->cancel_baton));
583 if (finfo->filetype != APR_DIR)
586 SVN_ERR(arbitrary_diff_this_dir(b, local_abspath, svn_depth_infinity,
593 svn_client__arbitrary_nodes_diff(const char *local_abspath1,
594 const char *local_abspath2,
596 const svn_wc_diff_callbacks4_t *callbacks,
598 svn_client_ctx_t *ctx,
599 apr_pool_t *scratch_pool)
601 svn_node_kind_t kind1;
602 svn_node_kind_t kind2;
604 SVN_ERR(svn_io_check_resolved_path(local_abspath1, &kind1, scratch_pool));
605 SVN_ERR(svn_io_check_resolved_path(local_abspath2, &kind2, scratch_pool));
608 return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
609 _("'%s' is not the same node kind as '%s'"),
610 svn_dirent_local_style(local_abspath1,
612 svn_dirent_local_style(local_abspath2,
615 if (depth == svn_depth_unknown)
616 depth = svn_depth_infinity;
618 if (kind1 == svn_node_file)
619 SVN_ERR(do_arbitrary_files_diff(local_abspath1, local_abspath2,
620 svn_dirent_basename(local_abspath1,
623 callbacks, diff_baton,
625 else if (kind1 == svn_node_dir)
626 SVN_ERR(do_arbitrary_dirs_diff(local_abspath1, local_abspath2,
628 callbacks, diff_baton,
631 return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
632 _("'%s' is not a file or directory"),
633 kind1 == svn_node_none
634 ? svn_dirent_local_style(local_abspath1,
636 : svn_dirent_local_style(local_abspath2,