| /* |
| * diff-cmd.c -- Display context diff of a file |
| * |
| * ==================================================================== |
| * 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 "svn_pools.h" |
| #include "svn_client.h" |
| #include "svn_string.h" |
| #include "svn_dirent_uri.h" |
| #include "svn_path.h" |
| #include "svn_error_codes.h" |
| #include "svn_error.h" |
| #include "svn_types.h" |
| #include "svn_cmdline.h" |
| #include "svn_xml.h" |
| #include "svn_hash.h" |
| #include "cl.h" |
| |
| #include "svn_private_config.h" |
| |
| |
| /*** Code. ***/ |
| |
| /* Convert KIND into a single character for display to the user. */ |
| static char |
| kind_to_char(svn_client_diff_summarize_kind_t kind) |
| { |
| switch (kind) |
| { |
| case svn_client_diff_summarize_kind_modified: |
| return 'M'; |
| |
| case svn_client_diff_summarize_kind_added: |
| return 'A'; |
| |
| case svn_client_diff_summarize_kind_deleted: |
| return 'D'; |
| |
| default: |
| return ' '; |
| } |
| } |
| |
| /* Convert KIND into a word describing the kind to the user. */ |
| static const char * |
| kind_to_word(svn_client_diff_summarize_kind_t kind) |
| { |
| switch (kind) |
| { |
| case svn_client_diff_summarize_kind_modified: return "modified"; |
| case svn_client_diff_summarize_kind_added: return "added"; |
| case svn_client_diff_summarize_kind_deleted: return "deleted"; |
| default: return "none"; |
| } |
| } |
| |
| /* Baton for summarize_xml and summarize_regular */ |
| struct summarize_baton_t |
| { |
| const char *anchor; |
| svn_boolean_t ignore_properties; |
| }; |
| |
| /* Print summary information about a given change as XML, implements the |
| * svn_client_diff_summarize_func_t interface. The @a baton is a 'char *' |
| * representing the either the path to the working copy root or the url |
| * the path the working copy root corresponds to. */ |
| static svn_error_t * |
| summarize_xml(const svn_client_diff_summarize_t *summary, |
| void *baton, |
| apr_pool_t *pool) |
| { |
| struct summarize_baton_t *b = baton; |
| /* Full path to the object being diffed. This is created by taking the |
| * baton, and appending the target's relative path. */ |
| const char *path = b->anchor; |
| svn_stringbuf_t *sb = svn_stringbuf_create_empty(pool); |
| const char *prop_change; |
| |
| if (b->ignore_properties && |
| summary->summarize_kind == svn_client_diff_summarize_kind_normal) |
| return SVN_NO_ERROR; |
| |
| /* Tack on the target path, so we can differentiate between different parts |
| * of the output when we're given multiple targets. */ |
| if (svn_path_is_url(path)) |
| { |
| path = svn_path_url_add_component2(path, summary->path, pool); |
| } |
| else |
| { |
| path = svn_dirent_join(path, summary->path, pool); |
| |
| /* Convert non-urls to local style, so that things like "" |
| show up as "." */ |
| path = svn_dirent_local_style(path, pool); |
| } |
| |
| prop_change = summary->prop_changed ? "modified" : "none"; |
| if (b->ignore_properties) |
| prop_change = "none"; |
| |
| svn_xml_make_open_tag(&sb, pool, svn_xml_protect_pcdata, "path", |
| "kind", svn_cl__node_kind_str_xml(summary->node_kind), |
| "item", kind_to_word(summary->summarize_kind), |
| "props", prop_change, |
| SVN_VA_NULL); |
| |
| svn_xml_escape_cdata_cstring(&sb, path, pool); |
| svn_xml_make_close_tag(&sb, pool, "path"); |
| |
| return svn_cl__error_checked_fputs(sb->data, stdout); |
| } |
| |
| /* Print summary information about a given change, implements the |
| * svn_client_diff_summarize_func_t interface. */ |
| static svn_error_t * |
| summarize_regular(const svn_client_diff_summarize_t *summary, |
| void *baton, |
| apr_pool_t *pool) |
| { |
| struct summarize_baton_t *b = baton; |
| const char *path = b->anchor; |
| char prop_change; |
| |
| if (b->ignore_properties && |
| summary->summarize_kind == svn_client_diff_summarize_kind_normal) |
| return SVN_NO_ERROR; |
| |
| /* Tack on the target path, so we can differentiate between different parts |
| * of the output when we're given multiple targets. */ |
| if (svn_path_is_url(path)) |
| { |
| path = svn_path_url_add_component2(path, summary->path, pool); |
| } |
| else |
| { |
| path = svn_dirent_join(path, summary->path, pool); |
| |
| /* Convert non-urls to local style, so that things like "" |
| show up as "." */ |
| path = svn_dirent_local_style(path, pool); |
| } |
| |
| /* Note: This output format tries to look like the output of 'svn status', |
| * thus the blank spaces where information that is not relevant to |
| * a diff summary would go. */ |
| |
| prop_change = summary->prop_changed ? 'M' : ' '; |
| if (b->ignore_properties) |
| prop_change = ' '; |
| |
| SVN_ERR(svn_cmdline_printf(pool, "%c%c %s\n", |
| kind_to_char(summary->summarize_kind), |
| prop_change, path)); |
| |
| return svn_cmdline_fflush(stdout); |
| } |
| |
| svn_error_t * |
| svn_cl__get_diff_summary_writer(svn_client_diff_summarize_func_t *func_p, |
| void **baton_p, |
| svn_boolean_t xml, |
| svn_boolean_t ignore_properties, |
| const char *anchor, |
| apr_pool_t *result_pool, |
| apr_pool_t *scratch_pool) |
| { |
| struct summarize_baton_t *b = apr_pcalloc(result_pool, sizeof(*b)); |
| |
| b->anchor = anchor; |
| b->ignore_properties = ignore_properties; |
| *func_p = xml ? summarize_xml : summarize_regular; |
| *baton_p = b; |
| return SVN_NO_ERROR; |
| } |
| |
| /* An svn_opt_subcommand_t to handle the 'diff' command. |
| This implements the `svn_opt_subcommand_t' interface. */ |
| svn_error_t * |
| svn_cl__diff(apr_getopt_t *os, |
| void *baton, |
| apr_pool_t *pool) |
| { |
| svn_cl__opt_state_t *opt_state = ((svn_cl__cmd_baton_t *) baton)->opt_state; |
| svn_client_ctx_t *ctx = ((svn_cl__cmd_baton_t *) baton)->ctx; |
| apr_array_header_t *options; |
| apr_array_header_t *targets; |
| svn_stream_t *outstream; |
| svn_stream_t *errstream; |
| const char *old_target, *new_target; |
| apr_pool_t *iterpool; |
| svn_boolean_t pegged_diff = FALSE; |
| svn_boolean_t ignore_content_type; |
| svn_boolean_t show_copies_as_adds = |
| opt_state->diff.patch_compatible || opt_state->diff.show_copies_as_adds; |
| svn_boolean_t ignore_properties = |
| opt_state->diff.patch_compatible || opt_state->diff.ignore_properties; |
| int i; |
| |
| if (opt_state->extensions) |
| options = svn_cstring_split(opt_state->extensions, " \t\n\r", TRUE, pool); |
| else |
| options = NULL; |
| |
| /* Get streams representing stdout and stderr, which is where |
| we'll have the external 'diff' program print to. */ |
| SVN_ERR(svn_stream_for_stdout(&outstream, pool)); |
| SVN_ERR(svn_stream_for_stderr(&errstream, pool)); |
| |
| if (opt_state->xml) |
| { |
| svn_stringbuf_t *sb; |
| |
| /* Check that the --summarize is passed as well. */ |
| if (!opt_state->diff.summarize) |
| return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("'--xml' option only valid with " |
| "'--summarize' option")); |
| |
| SVN_ERR(svn_cl__xml_print_header("diff", pool)); |
| |
| sb = svn_stringbuf_create_empty(pool); |
| svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "paths", SVN_VA_NULL); |
| SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout)); |
| } |
| if (opt_state->diff.summarize) |
| { |
| if (opt_state->diff.use_git_diff_format) |
| return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("'%s' not valid with '--summarize' option"), |
| "--git"); |
| if (opt_state->diff.patch_compatible) |
| return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("'%s' not valid with '--summarize' option"), |
| "--patch-compatible"); |
| if (opt_state->diff.show_copies_as_adds) |
| return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("'%s' not valid with '--summarize' option"), |
| "--show-copies-as-adds"); |
| if (opt_state->diff.internal_diff) |
| return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("'%s' not valid with '--summarize' option"), |
| "--internal-diff"); |
| if (opt_state->diff.diff_cmd) |
| return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("'%s' not valid with '--summarize' option"), |
| "--diff-cmd"); |
| if (opt_state->diff.no_diff_added) |
| return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("'%s' not valid with '--summarize' option"), |
| "--no-diff-added"); |
| if (opt_state->diff.no_diff_deleted) |
| return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("'%s' not valid with '--summarize' option"), |
| "--no-diff-deleted"); |
| if (opt_state->force) |
| return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("'%s' not valid with '--summarize' option"), |
| "--force"); |
| /* Not handling ignore-properties, and properties-only as there should |
| be a patch adding support for these being applied soon */ |
| } |
| |
| SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os, |
| opt_state->targets, |
| ctx, FALSE, pool)); |
| |
| if (! opt_state->old_target && ! opt_state->new_target |
| && (targets->nelts == 2) |
| && (svn_path_is_url(APR_ARRAY_IDX(targets, 0, const char *)) |
| || svn_path_is_url(APR_ARRAY_IDX(targets, 1, const char *))) |
| && opt_state->start_revision.kind == svn_opt_revision_unspecified |
| && opt_state->end_revision.kind == svn_opt_revision_unspecified) |
| { |
| /* A 2-target diff where one or both targets are URLs. These are |
| * shorthands for some 'svn diff --old X --new Y' invocations. */ |
| |
| SVN_ERR(svn_opt_parse_path(&opt_state->start_revision, &old_target, |
| APR_ARRAY_IDX(targets, 0, const char *), |
| pool)); |
| SVN_ERR(svn_opt_parse_path(&opt_state->end_revision, &new_target, |
| APR_ARRAY_IDX(targets, 1, const char *), |
| pool)); |
| targets->nelts = 0; |
| |
| /* Set default start/end revisions based on target types, in the same |
| * manner as done for the corresponding '--old X --new Y' cases, |
| * (note that we have an explicit --new target) */ |
| if (opt_state->start_revision.kind == svn_opt_revision_unspecified) |
| opt_state->start_revision.kind = svn_path_is_url(old_target) |
| ? svn_opt_revision_head : svn_opt_revision_working; |
| |
| if (opt_state->end_revision.kind == svn_opt_revision_unspecified) |
| opt_state->end_revision.kind = svn_path_is_url(new_target) |
| ? svn_opt_revision_head : svn_opt_revision_working; |
| } |
| else if (opt_state->old_target) |
| { |
| apr_array_header_t *tmp, *tmp2; |
| svn_opt_revision_t old_rev, new_rev; |
| |
| /* The 'svn diff --old=OLD[@OLDREV] [--new=NEW[@NEWREV]] |
| [PATH...]' case matches. */ |
| |
| tmp = apr_array_make(pool, 2, sizeof(const char *)); |
| APR_ARRAY_PUSH(tmp, const char *) = (opt_state->old_target); |
| APR_ARRAY_PUSH(tmp, const char *) = (opt_state->new_target |
| ? opt_state->new_target |
| : opt_state->old_target); |
| |
| SVN_ERR(svn_cl__args_to_target_array_print_reserved(&tmp2, os, tmp, |
| ctx, FALSE, pool)); |
| |
| /* Check if either or both targets were skipped (e.g. because they |
| * were .svn directories). */ |
| if (tmp2->nelts < 2) |
| return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, NULL, NULL); |
| |
| SVN_ERR(svn_opt_parse_path(&old_rev, &old_target, |
| APR_ARRAY_IDX(tmp2, 0, const char *), |
| pool)); |
| if (old_rev.kind != svn_opt_revision_unspecified) |
| opt_state->start_revision = old_rev; |
| SVN_ERR(svn_opt_parse_path(&new_rev, &new_target, |
| APR_ARRAY_IDX(tmp2, 1, const char *), |
| pool)); |
| if (new_rev.kind != svn_opt_revision_unspecified) |
| opt_state->end_revision = new_rev; |
| |
| /* For URLs, default to HEAD. For WC paths, default to WORKING if |
| * new target is explicit; if new target is implicitly the same as |
| * old target, then default the old to BASE and new to WORKING. */ |
| if (opt_state->start_revision.kind == svn_opt_revision_unspecified) |
| opt_state->start_revision.kind = svn_path_is_url(old_target) |
| ? svn_opt_revision_head |
| : (opt_state->new_target |
| ? svn_opt_revision_working : svn_opt_revision_base); |
| if (opt_state->end_revision.kind == svn_opt_revision_unspecified) |
| opt_state->end_revision.kind = svn_path_is_url(new_target) |
| ? svn_opt_revision_head : svn_opt_revision_working; |
| } |
| else if (opt_state->new_target) |
| { |
| return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("'--new' option only valid with " |
| "'--old' option")); |
| } |
| else |
| { |
| svn_boolean_t working_copy_present; |
| |
| /* The 'svn diff [-r N[:M]] [TARGET[@REV]...]' case matches. */ |
| |
| /* Here each target is a pegged object. Find out the starting |
| and ending paths for each target. */ |
| |
| svn_opt_push_implicit_dot_target(targets, pool); |
| |
| old_target = ""; |
| new_target = ""; |
| |
| SVN_ERR_W(svn_cl__assert_homogeneous_target_type(targets), |
| _("'svn diff [-r N[:M]] [TARGET[@REV]...]' does not support mixed " |
| "target types. Try using the --old and --new options or one of " |
| "the shorthand invocations listed in 'svn help diff'.")); |
| |
| working_copy_present = ! svn_path_is_url(APR_ARRAY_IDX(targets, 0, |
| const char *)); |
| |
| if (opt_state->start_revision.kind == svn_opt_revision_unspecified |
| && working_copy_present) |
| opt_state->start_revision.kind = svn_opt_revision_base; |
| if (opt_state->end_revision.kind == svn_opt_revision_unspecified) |
| opt_state->end_revision.kind = working_copy_present |
| ? svn_opt_revision_working : svn_opt_revision_head; |
| |
| /* Determine if we need to do pegged diffs. */ |
| if ((opt_state->start_revision.kind != svn_opt_revision_base |
| && opt_state->start_revision.kind != svn_opt_revision_working) |
| || (opt_state->end_revision.kind != svn_opt_revision_base |
| && opt_state->end_revision.kind != svn_opt_revision_working)) |
| pegged_diff = TRUE; |
| |
| } |
| |
| /* Should we ignore the content-type when deciding what to diff? */ |
| if (opt_state->force) |
| { |
| ignore_content_type = TRUE; |
| } |
| else if (ctx->config) |
| { |
| SVN_ERR(svn_config_get_bool(svn_hash_gets(ctx->config, |
| SVN_CONFIG_CATEGORY_CONFIG), |
| &ignore_content_type, |
| SVN_CONFIG_SECTION_MISCELLANY, |
| SVN_CONFIG_OPTION_DIFF_IGNORE_CONTENT_TYPE, |
| FALSE)); |
| } |
| else |
| { |
| ignore_content_type = FALSE; |
| } |
| |
| svn_opt_push_implicit_dot_target(targets, pool); |
| |
| iterpool = svn_pool_create(pool); |
| |
| for (i = 0; i < targets->nelts; ++i) |
| { |
| const char *path = APR_ARRAY_IDX(targets, i, const char *); |
| const char *target1, *target2; |
| |
| svn_pool_clear(iterpool); |
| if (! pegged_diff) |
| { |
| /* We can't be tacking URLs onto base paths! */ |
| if (svn_path_is_url(path)) |
| return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, |
| _("Path '%s' not relative to base URLs"), |
| path); |
| |
| if (svn_path_is_url(old_target)) |
| target1 = svn_path_url_add_component2( |
| old_target, |
| svn_relpath_canonicalize(path, iterpool), |
| iterpool); |
| else |
| target1 = svn_dirent_join(old_target, path, iterpool); |
| |
| if (svn_path_is_url(new_target)) |
| target2 = svn_path_url_add_component2( |
| new_target, |
| svn_relpath_canonicalize(path, iterpool), |
| iterpool); |
| else |
| target2 = svn_dirent_join(new_target, path, iterpool); |
| |
| if (opt_state->diff.summarize) |
| { |
| svn_client_diff_summarize_func_t summarize_func; |
| void *summarize_baton; |
| |
| SVN_ERR(svn_cl__get_diff_summary_writer( |
| &summarize_func, &summarize_baton, |
| opt_state->xml, ignore_properties, target1, |
| iterpool, iterpool)); |
| SVN_ERR(svn_client_diff_summarize2( |
| target1, |
| &opt_state->start_revision, |
| target2, |
| &opt_state->end_revision, |
| opt_state->depth, |
| ! opt_state->diff.notice_ancestry, |
| opt_state->changelists, |
| summarize_func, summarize_baton, |
| ctx, iterpool)); |
| } |
| else |
| SVN_ERR(svn_client_diff7( |
| options, |
| target1, |
| &(opt_state->start_revision), |
| target2, |
| &(opt_state->end_revision), |
| NULL, |
| opt_state->depth, |
| ! opt_state->diff.notice_ancestry, |
| opt_state->diff.no_diff_added, |
| opt_state->diff.no_diff_deleted, |
| show_copies_as_adds, |
| ignore_content_type, |
| ignore_properties, |
| opt_state->diff.properties_only, |
| opt_state->diff.use_git_diff_format, |
| TRUE /*pretty_print_mergeinfo*/, |
| svn_cmdline_output_encoding(pool), |
| outstream, |
| errstream, |
| opt_state->changelists, |
| ctx, iterpool)); |
| } |
| else |
| { |
| const char *truepath; |
| svn_opt_revision_t peg_revision; |
| |
| /* First check for a peg revision. */ |
| SVN_ERR(svn_opt_parse_path(&peg_revision, &truepath, path, |
| iterpool)); |
| |
| /* Set the default peg revision if one was not specified. */ |
| if (peg_revision.kind == svn_opt_revision_unspecified) |
| peg_revision.kind = svn_path_is_url(path) |
| ? svn_opt_revision_head : svn_opt_revision_working; |
| |
| if (opt_state->diff.summarize) |
| { |
| svn_client_diff_summarize_func_t summarize_func; |
| void *summarize_baton; |
| |
| SVN_ERR(svn_cl__get_diff_summary_writer( |
| &summarize_func, &summarize_baton, |
| opt_state->xml, ignore_properties, truepath, |
| iterpool, iterpool)); |
| SVN_ERR(svn_client_diff_summarize_peg2( |
| truepath, |
| &peg_revision, |
| &opt_state->start_revision, |
| &opt_state->end_revision, |
| opt_state->depth, |
| ! opt_state->diff.notice_ancestry, |
| opt_state->changelists, |
| summarize_func, summarize_baton, |
| ctx, iterpool)); |
| } |
| else |
| SVN_ERR(svn_client_diff_peg7( |
| options, |
| truepath, |
| &peg_revision, |
| &opt_state->start_revision, |
| &opt_state->end_revision, |
| NULL, |
| opt_state->depth, |
| ! opt_state->diff.notice_ancestry, |
| opt_state->diff.no_diff_added, |
| opt_state->diff.no_diff_deleted, |
| show_copies_as_adds, |
| ignore_content_type, |
| ignore_properties, |
| opt_state->diff.properties_only, |
| opt_state->diff.use_git_diff_format, |
| TRUE /*pretty_print_mergeinfo*/, |
| svn_cmdline_output_encoding(pool), |
| outstream, |
| errstream, |
| opt_state->changelists, |
| ctx, iterpool)); |
| } |
| } |
| |
| if (opt_state->xml) |
| { |
| svn_stringbuf_t *sb = svn_stringbuf_create_empty(pool); |
| svn_xml_make_close_tag(&sb, pool, "paths"); |
| SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout)); |
| SVN_ERR(svn_cl__xml_print_footer("diff", pool)); |
| } |
| |
| svn_pool_destroy(iterpool); |
| |
| return SVN_NO_ERROR; |
| } |