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