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. */
49 static char
kind_to_char(svn_client_diff_summarize_kind_t kind)50 kind_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. */
69 static const char *
kind_to_word(svn_client_diff_summarize_kind_t kind)70 kind_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 */
82 struct 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. */
92 static svn_error_t *
summarize_xml(const svn_client_diff_summarize_t * summary,void * baton,apr_pool_t * pool)93 summarize_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. */
141 static svn_error_t *
summarize_regular(const svn_client_diff_summarize_t * summary,void * baton,apr_pool_t * pool)142 summarize_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
184 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)185 svn_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. */
204 svn_error_t *
svn_cl__diff(apr_getopt_t * os,void * baton,apr_pool_t * pool)205 svn_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