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