1/* 2 * diff-cmd.c -- Display context diff of a file 3 * 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 12 * 13 * http://www.apache.org/licenses/LICENSE-2.0 14 * 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 20 * under the License. 21 * ==================================================================== 22 */ 23 24/* ==================================================================== */ 25 26 27 28/*** Includes. ***/ 29 30#include "svn_pools.h" 31#include "svn_client.h" 32#include "svn_string.h" 33#include "svn_dirent_uri.h" 34#include "svn_path.h" 35#include "svn_error_codes.h" 36#include "svn_error.h" 37#include "svn_types.h" 38#include "svn_cmdline.h" 39#include "svn_xml.h" 40#include "svn_hash.h" 41#include "cl.h" 42 43#include "svn_private_config.h" 44 45 46/*** Code. ***/ 47 48/* Convert KIND into a single character for display to the user. */ 49static char 50kind_to_char(svn_client_diff_summarize_kind_t kind) 51{ 52 switch (kind) 53 { 54 case svn_client_diff_summarize_kind_modified: 55 return 'M'; 56 57 case svn_client_diff_summarize_kind_added: 58 return 'A'; 59 60 case svn_client_diff_summarize_kind_deleted: 61 return 'D'; 62 63 default: 64 return ' '; 65 } 66} 67 68/* Convert KIND into a word describing the kind to the user. */ 69static const char * 70kind_to_word(svn_client_diff_summarize_kind_t kind) 71{ 72 switch (kind) 73 { 74 case svn_client_diff_summarize_kind_modified: return "modified"; 75 case svn_client_diff_summarize_kind_added: return "added"; 76 case svn_client_diff_summarize_kind_deleted: return "deleted"; 77 default: return "none"; 78 } 79} 80 81/* Baton for summarize_xml and summarize_regular */ 82struct summarize_baton_t 83{ 84 const char *anchor; 85 svn_boolean_t ignore_properties; 86}; 87 88/* Print summary information about a given change as XML, implements the 89 * svn_client_diff_summarize_func_t interface. The @a baton is a 'char *' 90 * representing the either the path to the working copy root or the url 91 * the path the working copy root corresponds to. */ 92static svn_error_t * 93summarize_xml(const svn_client_diff_summarize_t *summary, 94 void *baton, 95 apr_pool_t *pool) 96{ 97 struct summarize_baton_t *b = baton; 98 /* Full path to the object being diffed. This is created by taking the 99 * baton, and appending the target's relative path. */ 100 const char *path = b->anchor; 101 svn_stringbuf_t *sb = svn_stringbuf_create_empty(pool); 102 const char *prop_change; 103 104 if (b->ignore_properties && 105 summary->summarize_kind == svn_client_diff_summarize_kind_normal) 106 return SVN_NO_ERROR; 107 108 /* Tack on the target path, so we can differentiate between different parts 109 * of the output when we're given multiple targets. */ 110 if (svn_path_is_url(path)) 111 { 112 path = svn_path_url_add_component2(path, summary->path, pool); 113 } 114 else 115 { 116 path = svn_dirent_join(path, summary->path, pool); 117 118 /* Convert non-urls to local style, so that things like "" 119 show up as "." */ 120 path = svn_dirent_local_style(path, pool); 121 } 122 123 prop_change = summary->prop_changed ? "modified" : "none"; 124 if (b->ignore_properties) 125 prop_change = "none"; 126 127 svn_xml_make_open_tag(&sb, pool, svn_xml_protect_pcdata, "path", 128 "kind", svn_cl__node_kind_str_xml(summary->node_kind), 129 "item", kind_to_word(summary->summarize_kind), 130 "props", prop_change, 131 SVN_VA_NULL); 132 133 svn_xml_escape_cdata_cstring(&sb, path, pool); 134 svn_xml_make_close_tag(&sb, pool, "path"); 135 136 return svn_cl__error_checked_fputs(sb->data, stdout); 137} 138 139/* Print summary information about a given change, implements the 140 * svn_client_diff_summarize_func_t interface. */ 141static svn_error_t * 142summarize_regular(const svn_client_diff_summarize_t *summary, 143 void *baton, 144 apr_pool_t *pool) 145{ 146 struct summarize_baton_t *b = baton; 147 const char *path = b->anchor; 148 char prop_change; 149 150 if (b->ignore_properties && 151 summary->summarize_kind == svn_client_diff_summarize_kind_normal) 152 return SVN_NO_ERROR; 153 154 /* Tack on the target path, so we can differentiate between different parts 155 * of the output when we're given multiple targets. */ 156 if (svn_path_is_url(path)) 157 { 158 path = svn_path_url_add_component2(path, summary->path, pool); 159 } 160 else 161 { 162 path = svn_dirent_join(path, summary->path, pool); 163 164 /* Convert non-urls to local style, so that things like "" 165 show up as "." */ 166 path = svn_dirent_local_style(path, pool); 167 } 168 169 /* Note: This output format tries to look like the output of 'svn status', 170 * thus the blank spaces where information that is not relevant to 171 * a diff summary would go. */ 172 173 prop_change = summary->prop_changed ? 'M' : ' '; 174 if (b->ignore_properties) 175 prop_change = ' '; 176 177 SVN_ERR(svn_cmdline_printf(pool, "%c%c %s\n", 178 kind_to_char(summary->summarize_kind), 179 prop_change, path)); 180 181 return svn_cmdline_fflush(stdout); 182} 183 184svn_error_t * 185svn_cl__get_diff_summary_writer(svn_client_diff_summarize_func_t *func_p, 186 void **baton_p, 187 svn_boolean_t xml, 188 svn_boolean_t ignore_properties, 189 const char *anchor, 190 apr_pool_t *result_pool, 191 apr_pool_t *scratch_pool) 192{ 193 struct summarize_baton_t *b = apr_pcalloc(result_pool, sizeof(*b)); 194 195 b->anchor = anchor; 196 b->ignore_properties = ignore_properties; 197 *func_p = xml ? summarize_xml : summarize_regular; 198 *baton_p = b; 199 return SVN_NO_ERROR; 200} 201 202/* An svn_opt_subcommand_t to handle the 'diff' command. 203 This implements the `svn_opt_subcommand_t' interface. */ 204svn_error_t * 205svn_cl__diff(apr_getopt_t *os, 206 void *baton, 207 apr_pool_t *pool) 208{ 209 svn_cl__opt_state_t *opt_state = ((svn_cl__cmd_baton_t *) baton)->opt_state; 210 svn_client_ctx_t *ctx = ((svn_cl__cmd_baton_t *) baton)->ctx; 211 apr_array_header_t *options; 212 apr_array_header_t *targets; 213 svn_stream_t *outstream; 214 svn_stream_t *errstream; 215 const char *old_target, *new_target; 216 apr_pool_t *iterpool; 217 svn_boolean_t pegged_diff = FALSE; 218 svn_boolean_t ignore_content_type; 219 svn_boolean_t show_copies_as_adds = 220 opt_state->diff.patch_compatible || opt_state->diff.show_copies_as_adds; 221 svn_boolean_t ignore_properties = 222 opt_state->diff.patch_compatible || opt_state->diff.ignore_properties; 223 int i; 224 225 if (opt_state->extensions) 226 options = svn_cstring_split(opt_state->extensions, " \t\n\r", TRUE, pool); 227 else 228 options = NULL; 229 230 /* Get streams representing stdout and stderr, which is where 231 we'll have the external 'diff' program print to. */ 232 SVN_ERR(svn_stream_for_stdout(&outstream, pool)); 233 SVN_ERR(svn_stream_for_stderr(&errstream, pool)); 234 235 if (opt_state->xml) 236 { 237 svn_stringbuf_t *sb; 238 239 /* Check that the --summarize is passed as well. */ 240 if (!opt_state->diff.summarize) 241 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 242 _("'--xml' option only valid with " 243 "'--summarize' option")); 244 245 SVN_ERR(svn_cl__xml_print_header("diff", pool)); 246 247 sb = svn_stringbuf_create_empty(pool); 248 svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "paths", SVN_VA_NULL); 249 SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout)); 250 } 251 if (opt_state->diff.summarize) 252 { 253 if (opt_state->diff.use_git_diff_format) 254 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 255 _("'%s' not valid with '--summarize' option"), 256 "--git"); 257 if (opt_state->diff.patch_compatible) 258 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 259 _("'%s' not valid with '--summarize' option"), 260 "--patch-compatible"); 261 if (opt_state->diff.show_copies_as_adds) 262 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 263 _("'%s' not valid with '--summarize' option"), 264 "--show-copies-as-adds"); 265 if (opt_state->diff.internal_diff) 266 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 267 _("'%s' not valid with '--summarize' option"), 268 "--internal-diff"); 269 if (opt_state->diff.diff_cmd) 270 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 271 _("'%s' not valid with '--summarize' option"), 272 "--diff-cmd"); 273 if (opt_state->diff.no_diff_added) 274 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 275 _("'%s' not valid with '--summarize' option"), 276 "--no-diff-added"); 277 if (opt_state->diff.no_diff_deleted) 278 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 279 _("'%s' not valid with '--summarize' option"), 280 "--no-diff-deleted"); 281 if (opt_state->force) 282 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 283 _("'%s' not valid with '--summarize' option"), 284 "--force"); 285 /* Not handling ignore-properties, and properties-only as there should 286 be a patch adding support for these being applied soon */ 287 } 288 289 SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os, 290 opt_state->targets, 291 ctx, FALSE, pool)); 292 293 if (! opt_state->old_target && ! opt_state->new_target 294 && (targets->nelts == 2) 295 && (svn_path_is_url(APR_ARRAY_IDX(targets, 0, const char *)) 296 || svn_path_is_url(APR_ARRAY_IDX(targets, 1, const char *))) 297 && opt_state->start_revision.kind == svn_opt_revision_unspecified 298 && opt_state->end_revision.kind == svn_opt_revision_unspecified) 299 { 300 /* A 2-target diff where one or both targets are URLs. These are 301 * shorthands for some 'svn diff --old X --new Y' invocations. */ 302 303 SVN_ERR(svn_opt_parse_path(&opt_state->start_revision, &old_target, 304 APR_ARRAY_IDX(targets, 0, const char *), 305 pool)); 306 SVN_ERR(svn_opt_parse_path(&opt_state->end_revision, &new_target, 307 APR_ARRAY_IDX(targets, 1, const char *), 308 pool)); 309 targets->nelts = 0; 310 311 /* Set default start/end revisions based on target types, in the same 312 * manner as done for the corresponding '--old X --new Y' cases, 313 * (note that we have an explicit --new target) */ 314 if (opt_state->start_revision.kind == svn_opt_revision_unspecified) 315 opt_state->start_revision.kind = svn_path_is_url(old_target) 316 ? svn_opt_revision_head : svn_opt_revision_working; 317 318 if (opt_state->end_revision.kind == svn_opt_revision_unspecified) 319 opt_state->end_revision.kind = svn_path_is_url(new_target) 320 ? svn_opt_revision_head : svn_opt_revision_working; 321 } 322 else if (opt_state->old_target) 323 { 324 apr_array_header_t *tmp, *tmp2; 325 svn_opt_revision_t old_rev, new_rev; 326 327 /* The 'svn diff --old=OLD[@OLDREV] [--new=NEW[@NEWREV]] 328 [PATH...]' case matches. */ 329 330 tmp = apr_array_make(pool, 2, sizeof(const char *)); 331 APR_ARRAY_PUSH(tmp, const char *) = (opt_state->old_target); 332 APR_ARRAY_PUSH(tmp, const char *) = (opt_state->new_target 333 ? opt_state->new_target 334 : opt_state->old_target); 335 336 SVN_ERR(svn_cl__args_to_target_array_print_reserved(&tmp2, os, tmp, 337 ctx, FALSE, pool)); 338 339 /* Check if either or both targets were skipped (e.g. because they 340 * were .svn directories). */ 341 if (tmp2->nelts < 2) 342 return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, NULL, NULL); 343 344 SVN_ERR(svn_opt_parse_path(&old_rev, &old_target, 345 APR_ARRAY_IDX(tmp2, 0, const char *), 346 pool)); 347 if (old_rev.kind != svn_opt_revision_unspecified) 348 opt_state->start_revision = old_rev; 349 SVN_ERR(svn_opt_parse_path(&new_rev, &new_target, 350 APR_ARRAY_IDX(tmp2, 1, const char *), 351 pool)); 352 if (new_rev.kind != svn_opt_revision_unspecified) 353 opt_state->end_revision = new_rev; 354 355 /* For URLs, default to HEAD. For WC paths, default to WORKING if 356 * new target is explicit; if new target is implicitly the same as 357 * old target, then default the old to BASE and new to WORKING. */ 358 if (opt_state->start_revision.kind == svn_opt_revision_unspecified) 359 opt_state->start_revision.kind = svn_path_is_url(old_target) 360 ? svn_opt_revision_head 361 : (opt_state->new_target 362 ? svn_opt_revision_working : svn_opt_revision_base); 363 if (opt_state->end_revision.kind == svn_opt_revision_unspecified) 364 opt_state->end_revision.kind = svn_path_is_url(new_target) 365 ? svn_opt_revision_head : svn_opt_revision_working; 366 } 367 else if (opt_state->new_target) 368 { 369 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 370 _("'--new' option only valid with " 371 "'--old' option")); 372 } 373 else 374 { 375 svn_boolean_t working_copy_present; 376 377 /* The 'svn diff [-r N[:M]] [TARGET[@REV]...]' case matches. */ 378 379 /* Here each target is a pegged object. Find out the starting 380 and ending paths for each target. */ 381 382 svn_opt_push_implicit_dot_target(targets, pool); 383 384 old_target = ""; 385 new_target = ""; 386 387 SVN_ERR_W(svn_cl__assert_homogeneous_target_type(targets), 388 _("'svn diff [-r N[:M]] [TARGET[@REV]...]' does not support mixed " 389 "target types. Try using the --old and --new options or one of " 390 "the shorthand invocations listed in 'svn help diff'.")); 391 392 working_copy_present = ! svn_path_is_url(APR_ARRAY_IDX(targets, 0, 393 const char *)); 394 395 if (opt_state->start_revision.kind == svn_opt_revision_unspecified 396 && working_copy_present) 397 opt_state->start_revision.kind = svn_opt_revision_base; 398 if (opt_state->end_revision.kind == svn_opt_revision_unspecified) 399 opt_state->end_revision.kind = working_copy_present 400 ? svn_opt_revision_working : svn_opt_revision_head; 401 402 /* Determine if we need to do pegged diffs. */ 403 if ((opt_state->start_revision.kind != svn_opt_revision_base 404 && opt_state->start_revision.kind != svn_opt_revision_working) 405 || (opt_state->end_revision.kind != svn_opt_revision_base 406 && opt_state->end_revision.kind != svn_opt_revision_working)) 407 pegged_diff = TRUE; 408 409 } 410 411 /* Should we ignore the content-type when deciding what to diff? */ 412 if (opt_state->force) 413 { 414 ignore_content_type = TRUE; 415 } 416 else if (ctx->config) 417 { 418 SVN_ERR(svn_config_get_bool(svn_hash_gets(ctx->config, 419 SVN_CONFIG_CATEGORY_CONFIG), 420 &ignore_content_type, 421 SVN_CONFIG_SECTION_MISCELLANY, 422 SVN_CONFIG_OPTION_DIFF_IGNORE_CONTENT_TYPE, 423 FALSE)); 424 } 425 else 426 { 427 ignore_content_type = FALSE; 428 } 429 430 svn_opt_push_implicit_dot_target(targets, pool); 431 432 iterpool = svn_pool_create(pool); 433 434 for (i = 0; i < targets->nelts; ++i) 435 { 436 const char *path = APR_ARRAY_IDX(targets, i, const char *); 437 const char *target1, *target2; 438 439 svn_pool_clear(iterpool); 440 if (! pegged_diff) 441 { 442 /* We can't be tacking URLs onto base paths! */ 443 if (svn_path_is_url(path)) 444 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 445 _("Path '%s' not relative to base URLs"), 446 path); 447 448 if (svn_path_is_url(old_target)) 449 target1 = svn_path_url_add_component2( 450 old_target, 451 svn_relpath_canonicalize(path, iterpool), 452 iterpool); 453 else 454 target1 = svn_dirent_join(old_target, path, iterpool); 455 456 if (svn_path_is_url(new_target)) 457 target2 = svn_path_url_add_component2( 458 new_target, 459 svn_relpath_canonicalize(path, iterpool), 460 iterpool); 461 else 462 target2 = svn_dirent_join(new_target, path, iterpool); 463 464 if (opt_state->diff.summarize) 465 { 466 svn_client_diff_summarize_func_t summarize_func; 467 void *summarize_baton; 468 469 SVN_ERR(svn_cl__get_diff_summary_writer( 470 &summarize_func, &summarize_baton, 471 opt_state->xml, ignore_properties, target1, 472 iterpool, iterpool)); 473 SVN_ERR(svn_client_diff_summarize2( 474 target1, 475 &opt_state->start_revision, 476 target2, 477 &opt_state->end_revision, 478 opt_state->depth, 479 ! opt_state->diff.notice_ancestry, 480 opt_state->changelists, 481 summarize_func, summarize_baton, 482 ctx, iterpool)); 483 } 484 else 485 SVN_ERR(svn_client_diff7( 486 options, 487 target1, 488 &(opt_state->start_revision), 489 target2, 490 &(opt_state->end_revision), 491 NULL, 492 opt_state->depth, 493 ! opt_state->diff.notice_ancestry, 494 opt_state->diff.no_diff_added, 495 opt_state->diff.no_diff_deleted, 496 show_copies_as_adds, 497 ignore_content_type, 498 ignore_properties, 499 opt_state->diff.properties_only, 500 opt_state->diff.use_git_diff_format, 501 TRUE /*pretty_print_mergeinfo*/, 502 svn_cmdline_output_encoding(pool), 503 outstream, 504 errstream, 505 opt_state->changelists, 506 ctx, iterpool)); 507 } 508 else 509 { 510 const char *truepath; 511 svn_opt_revision_t peg_revision; 512 513 /* First check for a peg revision. */ 514 SVN_ERR(svn_opt_parse_path(&peg_revision, &truepath, path, 515 iterpool)); 516 517 /* Set the default peg revision if one was not specified. */ 518 if (peg_revision.kind == svn_opt_revision_unspecified) 519 peg_revision.kind = svn_path_is_url(path) 520 ? svn_opt_revision_head : svn_opt_revision_working; 521 522 if (opt_state->diff.summarize) 523 { 524 svn_client_diff_summarize_func_t summarize_func; 525 void *summarize_baton; 526 527 SVN_ERR(svn_cl__get_diff_summary_writer( 528 &summarize_func, &summarize_baton, 529 opt_state->xml, ignore_properties, truepath, 530 iterpool, iterpool)); 531 SVN_ERR(svn_client_diff_summarize_peg2( 532 truepath, 533 &peg_revision, 534 &opt_state->start_revision, 535 &opt_state->end_revision, 536 opt_state->depth, 537 ! opt_state->diff.notice_ancestry, 538 opt_state->changelists, 539 summarize_func, summarize_baton, 540 ctx, iterpool)); 541 } 542 else 543 SVN_ERR(svn_client_diff_peg7( 544 options, 545 truepath, 546 &peg_revision, 547 &opt_state->start_revision, 548 &opt_state->end_revision, 549 NULL, 550 opt_state->depth, 551 ! opt_state->diff.notice_ancestry, 552 opt_state->diff.no_diff_added, 553 opt_state->diff.no_diff_deleted, 554 show_copies_as_adds, 555 ignore_content_type, 556 ignore_properties, 557 opt_state->diff.properties_only, 558 opt_state->diff.use_git_diff_format, 559 TRUE /*pretty_print_mergeinfo*/, 560 svn_cmdline_output_encoding(pool), 561 outstream, 562 errstream, 563 opt_state->changelists, 564 ctx, iterpool)); 565 } 566 } 567 568 if (opt_state->xml) 569 { 570 svn_stringbuf_t *sb = svn_stringbuf_create_empty(pool); 571 svn_xml_make_close_tag(&sb, pool, "paths"); 572 SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout)); 573 SVN_ERR(svn_cl__xml_print_footer("diff", pool)); 574 } 575 576 svn_pool_destroy(iterpool); 577 578 return SVN_NO_ERROR; 579} 580