diff-cmd.c revision 299742
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
184/* An svn_opt_subcommand_t to handle the 'diff' command.
185   This implements the `svn_opt_subcommand_t' interface. */
186svn_error_t *
187svn_cl__diff(apr_getopt_t *os,
188             void *baton,
189             apr_pool_t *pool)
190{
191  svn_cl__opt_state_t *opt_state = ((svn_cl__cmd_baton_t *) baton)->opt_state;
192  svn_client_ctx_t *ctx = ((svn_cl__cmd_baton_t *) baton)->ctx;
193  apr_array_header_t *options;
194  apr_array_header_t *targets;
195  svn_stream_t *outstream;
196  svn_stream_t *errstream;
197  const char *old_target, *new_target;
198  apr_pool_t *iterpool;
199  svn_boolean_t pegged_diff = FALSE;
200  svn_boolean_t ignore_content_type;
201  svn_boolean_t show_copies_as_adds =
202    opt_state->diff.patch_compatible || opt_state->diff.show_copies_as_adds;
203  svn_boolean_t ignore_properties =
204    opt_state->diff.patch_compatible || opt_state->diff.ignore_properties;
205  int i;
206  struct summarize_baton_t summarize_baton;
207  const svn_client_diff_summarize_func_t summarize_func =
208    (opt_state->xml ? summarize_xml : summarize_regular);
209
210  if (opt_state->extensions)
211    options = svn_cstring_split(opt_state->extensions, " \t\n\r", TRUE, pool);
212  else
213    options = NULL;
214
215  /* Get streams representing stdout and stderr, which is where
216     we'll have the external 'diff' program print to. */
217  SVN_ERR(svn_stream_for_stdout(&outstream, pool));
218  SVN_ERR(svn_stream_for_stderr(&errstream, pool));
219
220  if (opt_state->xml)
221    {
222      svn_stringbuf_t *sb;
223
224      /* Check that the --summarize is passed as well. */
225      if (!opt_state->diff.summarize)
226        return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
227                                _("'--xml' option only valid with "
228                                  "'--summarize' option"));
229
230      SVN_ERR(svn_cl__xml_print_header("diff", pool));
231
232      sb = svn_stringbuf_create_empty(pool);
233      svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "paths", SVN_VA_NULL);
234      SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout));
235    }
236
237  SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os,
238                                                      opt_state->targets,
239                                                      ctx, FALSE, pool));
240
241  if (! opt_state->old_target && ! opt_state->new_target
242      && (targets->nelts == 2)
243      && (svn_path_is_url(APR_ARRAY_IDX(targets, 0, const char *))
244          || svn_path_is_url(APR_ARRAY_IDX(targets, 1, const char *)))
245      && opt_state->start_revision.kind == svn_opt_revision_unspecified
246      && opt_state->end_revision.kind == svn_opt_revision_unspecified)
247    {
248      /* A 2-target diff where one or both targets are URLs. These are
249       * shorthands for some 'svn diff --old X --new Y' invocations. */
250
251      SVN_ERR(svn_opt_parse_path(&opt_state->start_revision, &old_target,
252                                 APR_ARRAY_IDX(targets, 0, const char *),
253                                 pool));
254      SVN_ERR(svn_opt_parse_path(&opt_state->end_revision, &new_target,
255                                 APR_ARRAY_IDX(targets, 1, const char *),
256                                 pool));
257      targets->nelts = 0;
258
259      /* Set default start/end revisions based on target types, in the same
260       * manner as done for the corresponding '--old X --new Y' cases,
261       * (note that we have an explicit --new target) */
262      if (opt_state->start_revision.kind == svn_opt_revision_unspecified)
263        opt_state->start_revision.kind = svn_path_is_url(old_target)
264            ? svn_opt_revision_head : svn_opt_revision_working;
265
266      if (opt_state->end_revision.kind == svn_opt_revision_unspecified)
267        opt_state->end_revision.kind = svn_path_is_url(new_target)
268            ? svn_opt_revision_head : svn_opt_revision_working;
269    }
270  else if (opt_state->old_target)
271    {
272      apr_array_header_t *tmp, *tmp2;
273      svn_opt_revision_t old_rev, new_rev;
274
275      /* The 'svn diff --old=OLD[@OLDREV] [--new=NEW[@NEWREV]]
276         [PATH...]' case matches. */
277
278      tmp = apr_array_make(pool, 2, sizeof(const char *));
279      APR_ARRAY_PUSH(tmp, const char *) = (opt_state->old_target);
280      APR_ARRAY_PUSH(tmp, const char *) = (opt_state->new_target
281                                           ? opt_state->new_target
282                                           : opt_state->old_target);
283
284      SVN_ERR(svn_cl__args_to_target_array_print_reserved(&tmp2, os, tmp,
285                                                          ctx, FALSE, pool));
286
287      /* Check if either or both targets were skipped (e.g. because they
288       * were .svn directories). */
289      if (tmp2->nelts < 2)
290        return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, NULL, NULL);
291
292      SVN_ERR(svn_opt_parse_path(&old_rev, &old_target,
293                                 APR_ARRAY_IDX(tmp2, 0, const char *),
294                                 pool));
295      if (old_rev.kind != svn_opt_revision_unspecified)
296        opt_state->start_revision = old_rev;
297      SVN_ERR(svn_opt_parse_path(&new_rev, &new_target,
298                                 APR_ARRAY_IDX(tmp2, 1, const char *),
299                                 pool));
300      if (new_rev.kind != svn_opt_revision_unspecified)
301        opt_state->end_revision = new_rev;
302
303      /* For URLs, default to HEAD. For WC paths, default to WORKING if
304       * new target is explicit; if new target is implicitly the same as
305       * old target, then default the old to BASE and new to WORKING. */
306      if (opt_state->start_revision.kind == svn_opt_revision_unspecified)
307        opt_state->start_revision.kind = svn_path_is_url(old_target)
308          ? svn_opt_revision_head
309          : (opt_state->new_target
310             ? svn_opt_revision_working : svn_opt_revision_base);
311      if (opt_state->end_revision.kind == svn_opt_revision_unspecified)
312        opt_state->end_revision.kind = svn_path_is_url(new_target)
313          ? svn_opt_revision_head : svn_opt_revision_working;
314    }
315  else if (opt_state->new_target)
316    {
317      return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
318                              _("'--new' option only valid with "
319                                "'--old' option"));
320    }
321  else
322    {
323      svn_boolean_t working_copy_present;
324
325      /* The 'svn diff [-r N[:M]] [TARGET[@REV]...]' case matches. */
326
327      /* Here each target is a pegged object. Find out the starting
328         and ending paths for each target. */
329
330      svn_opt_push_implicit_dot_target(targets, pool);
331
332      old_target = "";
333      new_target = "";
334
335      SVN_ERR_W(svn_cl__assert_homogeneous_target_type(targets),
336        _("'svn diff [-r N[:M]] [TARGET[@REV]...]' does not support mixed "
337          "target types. Try using the --old and --new options or one of "
338          "the shorthand invocations listed in 'svn help diff'."));
339
340      working_copy_present = ! svn_path_is_url(APR_ARRAY_IDX(targets, 0,
341                                                             const char *));
342
343      if (opt_state->start_revision.kind == svn_opt_revision_unspecified
344          && working_copy_present)
345        opt_state->start_revision.kind = svn_opt_revision_base;
346      if (opt_state->end_revision.kind == svn_opt_revision_unspecified)
347        opt_state->end_revision.kind = working_copy_present
348          ? svn_opt_revision_working : svn_opt_revision_head;
349
350      /* Determine if we need to do pegged diffs. */
351      if ((opt_state->start_revision.kind != svn_opt_revision_base
352           && opt_state->start_revision.kind != svn_opt_revision_working)
353          || (opt_state->end_revision.kind != svn_opt_revision_base
354              && opt_state->end_revision.kind != svn_opt_revision_working))
355        pegged_diff = TRUE;
356
357    }
358
359  /* Should we ignore the content-type when deciding what to diff? */
360  if (opt_state->force)
361    {
362      ignore_content_type = TRUE;
363    }
364  else if (ctx->config)
365    {
366      SVN_ERR(svn_config_get_bool(svn_hash_gets(ctx->config,
367                                                SVN_CONFIG_CATEGORY_CONFIG),
368                                  &ignore_content_type,
369                                  SVN_CONFIG_SECTION_MISCELLANY,
370                                  SVN_CONFIG_OPTION_DIFF_IGNORE_CONTENT_TYPE,
371                                  FALSE));
372    }
373  else
374    {
375      ignore_content_type = FALSE;
376    }
377
378  svn_opt_push_implicit_dot_target(targets, pool);
379
380  iterpool = svn_pool_create(pool);
381
382  for (i = 0; i < targets->nelts; ++i)
383    {
384      const char *path = APR_ARRAY_IDX(targets, i, const char *);
385      const char *target1, *target2;
386
387      svn_pool_clear(iterpool);
388      if (! pegged_diff)
389        {
390          /* We can't be tacking URLs onto base paths! */
391          if (svn_path_is_url(path))
392            return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
393                                     _("Path '%s' not relative to base URLs"),
394                                     path);
395
396          if (svn_path_is_url(old_target))
397            target1 = svn_path_url_add_component2(
398                          old_target,
399                          svn_relpath_canonicalize(path, iterpool),
400                          iterpool);
401          else
402            target1 = svn_dirent_join(old_target, path, iterpool);
403
404          if (svn_path_is_url(new_target))
405            target2 = svn_path_url_add_component2(
406                          new_target,
407                          svn_relpath_canonicalize(path, iterpool),
408                          iterpool);
409          else
410            target2 = svn_dirent_join(new_target, path, iterpool);
411
412          if (opt_state->diff.summarize)
413            {
414              summarize_baton.anchor = target1;
415              summarize_baton.ignore_properties = ignore_properties;
416
417              SVN_ERR(svn_client_diff_summarize2(
418                                target1,
419                                &opt_state->start_revision,
420                                target2,
421                                &opt_state->end_revision,
422                                opt_state->depth,
423                                ! opt_state->diff.notice_ancestry,
424                                opt_state->changelists,
425                                summarize_func, &summarize_baton,
426                                ctx, iterpool));
427            }
428          else
429            SVN_ERR(svn_client_diff6(
430                     options,
431                     target1,
432                     &(opt_state->start_revision),
433                     target2,
434                     &(opt_state->end_revision),
435                     NULL,
436                     opt_state->depth,
437                     ! opt_state->diff.notice_ancestry,
438                     opt_state->diff.no_diff_added,
439                     opt_state->diff.no_diff_deleted,
440                     show_copies_as_adds,
441                     ignore_content_type,
442                     ignore_properties,
443                     opt_state->diff.properties_only,
444                     opt_state->diff.use_git_diff_format,
445                     svn_cmdline_output_encoding(pool),
446                     outstream,
447                     errstream,
448                     opt_state->changelists,
449                     ctx, iterpool));
450        }
451      else
452        {
453          const char *truepath;
454          svn_opt_revision_t peg_revision;
455
456          /* First check for a peg revision. */
457          SVN_ERR(svn_opt_parse_path(&peg_revision, &truepath, path,
458                                     iterpool));
459
460          /* Set the default peg revision if one was not specified. */
461          if (peg_revision.kind == svn_opt_revision_unspecified)
462            peg_revision.kind = svn_path_is_url(path)
463              ? svn_opt_revision_head : svn_opt_revision_working;
464
465          if (opt_state->diff.summarize)
466            {
467              summarize_baton.anchor = truepath;
468              summarize_baton.ignore_properties = ignore_properties;
469              SVN_ERR(svn_client_diff_summarize_peg2(
470                                truepath,
471                                &peg_revision,
472                                &opt_state->start_revision,
473                                &opt_state->end_revision,
474                                opt_state->depth,
475                                ! opt_state->diff.notice_ancestry,
476                                opt_state->changelists,
477                                summarize_func, &summarize_baton,
478                                ctx, iterpool));
479            }
480          else
481            SVN_ERR(svn_client_diff_peg6(
482                     options,
483                     truepath,
484                     &peg_revision,
485                     &opt_state->start_revision,
486                     &opt_state->end_revision,
487                     NULL,
488                     opt_state->depth,
489                     ! opt_state->diff.notice_ancestry,
490                     opt_state->diff.no_diff_added,
491                     opt_state->diff.no_diff_deleted,
492                     show_copies_as_adds,
493                     ignore_content_type,
494                     ignore_properties,
495                     opt_state->diff.properties_only,
496                     opt_state->diff.use_git_diff_format,
497                     svn_cmdline_output_encoding(pool),
498                     outstream,
499                     errstream,
500                     opt_state->changelists,
501                     ctx, iterpool));
502        }
503    }
504
505  if (opt_state->xml)
506    {
507      svn_stringbuf_t *sb = svn_stringbuf_create_empty(pool);
508      svn_xml_make_close_tag(&sb, pool, "paths");
509      SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout));
510      SVN_ERR(svn_cl__xml_print_footer("diff", pool));
511    }
512
513  svn_pool_destroy(iterpool);
514
515  return SVN_NO_ERROR;
516}
517