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