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