1/*
2 * log-cmd.c -- Display log messages
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#include <apr_fnmatch.h>
25
26#include "svn_client.h"
27#include "svn_compat.h"
28#include "svn_dirent_uri.h"
29#include "svn_string.h"
30#include "svn_path.h"
31#include "svn_error.h"
32#include "svn_sorts.h"
33#include "svn_xml.h"
34#include "svn_time.h"
35#include "svn_cmdline.h"
36#include "svn_props.h"
37#include "svn_pools.h"
38
39#include "private/svn_cmdline_private.h"
40#include "private/svn_sorts_private.h"
41#include "private/svn_utf_private.h"
42
43#include "cl.h"
44#include "cl-log.h"
45
46#include "svn_private_config.h"
47
48
49/*** Code. ***/
50
51
52
53/* Display a diff of the subtree TARGET_PATH_OR_URL@TARGET_PEG_REVISION as
54 * it changed in the revision that LOG_ENTRY describes.
55 *
56 * Restrict the diff to depth DEPTH.  Pass DIFF_EXTENSIONS along to the diff
57 * subroutine.
58 *
59 * Write the diff to OUTSTREAM and write any stderr output to ERRSTREAM.
60 * ### How is exit code handled? 0 and 1 -> SVN_NO_ERROR, else an svn error?
61 * ### Should we get rid of ERRSTREAM and use svn_error_t instead?
62 */
63static svn_error_t *
64display_diff(const svn_log_entry_t *log_entry,
65             const char *target_path_or_url,
66             const svn_opt_revision_t *target_peg_revision,
67             svn_depth_t depth,
68             const char *diff_extensions,
69             svn_stream_t *outstream,
70             svn_stream_t *errstream,
71             svn_client_ctx_t *ctx,
72             apr_pool_t *pool)
73{
74  apr_array_header_t *diff_options;
75  svn_opt_revision_t start_revision;
76  svn_opt_revision_t end_revision;
77
78  /* Fall back to "" to get options initialized either way. */
79  if (diff_extensions)
80    diff_options = svn_cstring_split(diff_extensions, " \t\n\r",
81                                     TRUE, pool);
82  else
83    diff_options = NULL;
84
85  start_revision.kind = svn_opt_revision_number;
86  start_revision.value.number = log_entry->revision - 1;
87  end_revision.kind = svn_opt_revision_number;
88  end_revision.value.number = log_entry->revision;
89
90  SVN_ERR(svn_stream_puts(outstream, "\n"));
91  SVN_ERR(svn_client_diff_peg7(diff_options,
92                               target_path_or_url,
93                               target_peg_revision,
94                               &start_revision, &end_revision,
95                               NULL,
96                               depth,
97                               FALSE /* ignore ancestry */,
98                               FALSE /* no diff added */,
99                               TRUE  /* no diff deleted */,
100                               FALSE /* show copies as adds */,
101                               FALSE /* ignore content type */,
102                               FALSE /* ignore prop diff */,
103                               FALSE /* properties only */,
104                               FALSE /* use git diff format */,
105                               TRUE  /* pretty_print_mergeinfo */,
106                               svn_cmdline_output_encoding(pool),
107                               outstream,
108                               errstream,
109                               NULL,
110                               ctx, pool));
111  SVN_ERR(svn_stream_puts(outstream, _("\n")));
112  return SVN_NO_ERROR;
113}
114
115/* Return TRUE if STR matches PATTERN. Else, return FALSE. Assumes that
116 * PATTERN is a UTF-8 string prepared for case- and accent-insensitive
117 * comparison via svn_utf__xfrm(). */
118static svn_boolean_t
119match(const char *pattern, const char *str, svn_membuf_t *buf)
120{
121  svn_error_t *err;
122
123  err = svn_utf__xfrm(&str, str, strlen(str), TRUE, TRUE, buf);
124  if (err)
125    {
126      /* Can't match invalid data. */
127      svn_error_clear(err);
128      return FALSE;
129    }
130
131  return apr_fnmatch(pattern, str, 0) == APR_SUCCESS;
132}
133
134/* Return TRUE if SEARCH_PATTERN matches the AUTHOR, DATE, LOG_MESSAGE,
135 * or a path in the set of keys of the CHANGED_PATHS hash. Else, return FALSE.
136 * Any of AUTHOR, DATE, LOG_MESSAGE, and CHANGED_PATHS may be NULL. */
137static svn_boolean_t
138match_search_pattern(const char *search_pattern,
139                     const char *author,
140                     const char *date,
141                     const char *log_message,
142                     apr_hash_t *changed_paths,
143                     svn_membuf_t *buf,
144                     apr_pool_t *pool)
145{
146  /* Match any substring containing the pattern, like UNIX 'grep' does. */
147  const char *pattern = apr_psprintf(pool, "*%s*", search_pattern);
148
149  /* Does the author match the search pattern? */
150  if (author && match(pattern, author, buf))
151    return TRUE;
152
153  /* Does the date the search pattern? */
154  if (date && match(pattern, date, buf))
155    return TRUE;
156
157  /* Does the log message the search pattern? */
158  if (log_message && match(pattern, log_message, buf))
159    return TRUE;
160
161  if (changed_paths)
162    {
163      apr_hash_index_t *hi;
164
165      /* Does a changed path match the search pattern? */
166      for (hi = apr_hash_first(pool, changed_paths);
167           hi;
168           hi = apr_hash_next(hi))
169        {
170          const char *path = apr_hash_this_key(hi);
171          svn_log_changed_path2_t *log_item;
172
173          if (match(pattern, path, buf))
174            return TRUE;
175
176          /* Match copy-from paths, too. */
177          log_item = apr_hash_this_val(hi);
178          if (log_item->copyfrom_path
179              && SVN_IS_VALID_REVNUM(log_item->copyfrom_rev)
180              && match(pattern, log_item->copyfrom_path, buf))
181            return TRUE;
182        }
183    }
184
185  return FALSE;
186}
187
188/* Match all search patterns in SEARCH_PATTERNS against AUTHOR, DATE, MESSAGE,
189 * and CHANGED_PATHS. Return TRUE if any pattern matches, else FALSE.
190 * BUF and SCRATCH_POOL are used for temporary allocations. */
191static svn_boolean_t
192match_search_patterns(apr_array_header_t *search_patterns,
193                      const char *author,
194                      const char *date,
195                      const char *message,
196                      apr_hash_t *changed_paths,
197                      svn_membuf_t *buf,
198                      apr_pool_t *scratch_pool)
199{
200  int i;
201  svn_boolean_t match = FALSE;
202  apr_pool_t *iterpool = svn_pool_create(scratch_pool);
203
204  for (i = 0; i < search_patterns->nelts; i++)
205    {
206      apr_array_header_t *pattern_group;
207      int j;
208
209      pattern_group = APR_ARRAY_IDX(search_patterns, i, apr_array_header_t *);
210
211      /* All patterns within the group must match. */
212      for (j = 0; j < pattern_group->nelts; j++)
213        {
214          const char *pattern;
215
216          svn_pool_clear(iterpool);
217
218          pattern = APR_ARRAY_IDX(pattern_group, j, const char *);
219          match = match_search_pattern(pattern, author, date, message,
220                                       changed_paths, buf, iterpool);
221          if (!match)
222            break;
223        }
224
225      match = (match && j == pattern_group->nelts);
226      if (match)
227        break;
228    }
229  svn_pool_destroy(iterpool);
230
231  return match;
232}
233
234/* Implement `svn_log_entry_receiver_t', printing the logs in
235 * a human-readable and machine-parseable format.
236 *
237 * BATON is of type `svn_cl__log_receiver_baton'.
238 *
239 * First, print a header line.  Then if CHANGED_PATHS is non-null,
240 * print all affected paths in a list headed "Changed paths:\n",
241 * immediately following the header line.  Then print a newline
242 * followed by the message body, unless BATON->omit_log_message is true.
243 *
244 * Here are some examples of the output:
245 *
246 * $ svn log -r1847:1846
247 * ------------------------------------------------------------------------
248 * rev 1847:  cmpilato | Wed 1 May 2002 15:44:26 | 7 lines
249 *
250 * Fix for Issue #694.
251 *
252 * * subversion/libsvn_repos/delta.c
253 *   (delta_files): Rework the logic in this function to only call
254 * send_text_deltas if there are deltas to send, and within that case,
255 * only use a real delta stream if the caller wants real text deltas.
256 *
257 * ------------------------------------------------------------------------
258 * rev 1846:  whoever | Wed 1 May 2002 15:23:41 | 1 line
259 *
260 * imagine an example log message here
261 * ------------------------------------------------------------------------
262 *
263 * Or:
264 *
265 * $ svn log -r1847:1846 -v
266 * ------------------------------------------------------------------------
267 * rev 1847:  cmpilato | Wed 1 May 2002 15:44:26 | 7 lines
268 * Changed paths:
269 *    M /trunk/subversion/libsvn_repos/delta.c
270 *
271 * Fix for Issue #694.
272 *
273 * * subversion/libsvn_repos/delta.c
274 *   (delta_files): Rework the logic in this function to only call
275 * send_text_deltas if there are deltas to send, and within that case,
276 * only use a real delta stream if the caller wants real text deltas.
277 *
278 * ------------------------------------------------------------------------
279 * rev 1846:  whoever | Wed 1 May 2002 15:23:41 | 1 line
280 * Changed paths:
281 *    M /trunk/notes/fs_dumprestore.txt
282 *    M /trunk/subversion/libsvn_repos/dump.c
283 *
284 * imagine an example log message here
285 * ------------------------------------------------------------------------
286 *
287 * Or:
288 *
289 * $ svn log -r1847:1846 -q
290 * ------------------------------------------------------------------------
291 * rev 1847:  cmpilato | Wed 1 May 2002 15:44:26
292 * ------------------------------------------------------------------------
293 * rev 1846:  whoever | Wed 1 May 2002 15:23:41
294 * ------------------------------------------------------------------------
295 *
296 * Or:
297 *
298 * $ svn log -r1847:1846 -qv
299 * ------------------------------------------------------------------------
300 * rev 1847:  cmpilato | Wed 1 May 2002 15:44:26
301 * Changed paths:
302 *    M /trunk/subversion/libsvn_repos/delta.c
303 * ------------------------------------------------------------------------
304 * rev 1846:  whoever | Wed 1 May 2002 15:23:41
305 * Changed paths:
306 *    M /trunk/notes/fs_dumprestore.txt
307 *    M /trunk/subversion/libsvn_repos/dump.c
308 * ------------------------------------------------------------------------
309 *
310 */
311svn_error_t *
312svn_cl__log_entry_receiver(void *baton,
313                           svn_log_entry_t *log_entry,
314                           apr_pool_t *pool)
315{
316  svn_cl__log_receiver_baton *lb = baton;
317  const char *author;
318  const char *date;
319  const char *message;
320
321  if (lb->ctx->cancel_func)
322    SVN_ERR(lb->ctx->cancel_func(lb->ctx->cancel_baton));
323
324  svn_compat_log_revprops_out(&author, &date, &message, log_entry->revprops);
325
326  if (log_entry->revision == 0 && message == NULL)
327    return SVN_NO_ERROR;
328
329  if (! SVN_IS_VALID_REVNUM(log_entry->revision))
330    {
331      if (lb->merge_stack)
332        apr_array_pop(lb->merge_stack);
333
334      return SVN_NO_ERROR;
335    }
336
337  /* ### See https://issues.apache.org/jira/browse/SVN-807
338     for more on the fallback fuzzy conversions below. */
339
340  if (author == NULL)
341    author = _("(no author)");
342
343  if (date && date[0])
344    /* Convert date to a format for humans. */
345    SVN_ERR(svn_cl__time_cstring_to_human_cstring(&date, date, pool));
346  else
347    date = _("(no date)");
348
349  if (! lb->omit_log_message && message == NULL)
350    message = "";
351
352  if (lb->search_patterns &&
353      ! match_search_patterns(lb->search_patterns, author, date, message,
354                              log_entry->changed_paths2, &lb->buffer, pool))
355    {
356      if (log_entry->has_children)
357        {
358          if (! lb->merge_stack)
359            lb->merge_stack = apr_array_make(lb->pool, 1, sizeof(svn_revnum_t));
360
361          APR_ARRAY_PUSH(lb->merge_stack, svn_revnum_t) = log_entry->revision;
362        }
363
364      return SVN_NO_ERROR;
365    }
366
367  SVN_ERR(svn_cmdline_printf(pool,
368                             SVN_CL__LOG_SEP_STRING "r%ld | %s | %s",
369                             log_entry->revision, author, date));
370
371  if (message != NULL)
372    {
373      /* Number of lines in the msg. */
374      int lines = svn_cstring_count_newlines(message) + 1;
375
376      SVN_ERR(svn_cmdline_printf(pool,
377                                 Q_(" | %d line", " | %d lines", lines),
378                                 lines));
379    }
380
381  SVN_ERR(svn_cmdline_printf(pool, "\n"));
382
383  if (log_entry->changed_paths2)
384    {
385      apr_array_header_t *sorted_paths;
386      int i;
387      apr_pool_t *iterpool;
388
389      /* Get an array of sorted hash keys. */
390      sorted_paths = svn_sort__hash(log_entry->changed_paths2,
391                                    svn_sort_compare_items_as_paths, pool);
392
393      SVN_ERR(svn_cmdline_printf(pool,
394                                 _("Changed paths:\n")));
395      iterpool = svn_pool_create(pool);
396      for (i = 0; i < sorted_paths->nelts; i++)
397        {
398          svn_sort__item_t *item = &(APR_ARRAY_IDX(sorted_paths, i,
399                                                   svn_sort__item_t));
400          const char *path = item->key;
401          svn_log_changed_path2_t *log_item = item->value;
402          const char *copy_data = "";
403
404          svn_pool_clear(iterpool);
405
406          if (lb->ctx->cancel_func)
407            SVN_ERR(lb->ctx->cancel_func(lb->ctx->cancel_baton));
408
409          if (log_item->copyfrom_path
410              && SVN_IS_VALID_REVNUM(log_item->copyfrom_rev))
411            {
412              copy_data
413                = apr_psprintf(iterpool,
414                               _(" (from %s:%ld)"),
415                               log_item->copyfrom_path,
416                               log_item->copyfrom_rev);
417            }
418          SVN_ERR(svn_cmdline_printf(iterpool, "   %c %s%s\n",
419                                     log_item->action, path,
420                                     copy_data));
421        }
422      svn_pool_destroy(iterpool);
423    }
424
425  if (lb->merge_stack && lb->merge_stack->nelts > 0)
426    {
427      int i;
428      apr_pool_t *iterpool;
429
430      /* Print the result of merge line */
431      if (log_entry->subtractive_merge)
432        SVN_ERR(svn_cmdline_printf(pool, _("Reverse merged via:")));
433      else
434        SVN_ERR(svn_cmdline_printf(pool, _("Merged via:")));
435      iterpool = svn_pool_create(pool);
436      for (i = 0; i < lb->merge_stack->nelts; i++)
437        {
438          svn_revnum_t rev = APR_ARRAY_IDX(lb->merge_stack, i, svn_revnum_t);
439
440          svn_pool_clear(iterpool);
441          SVN_ERR(svn_cmdline_printf(iterpool, " r%ld%c", rev,
442                                     i == lb->merge_stack->nelts - 1 ?
443                                                                  '\n' : ','));
444        }
445      svn_pool_destroy(iterpool);
446    }
447
448  if (message != NULL)
449    {
450      /* A blank line always precedes the log message. */
451      SVN_ERR(svn_cmdline_printf(pool, "\n%s\n", message));
452    }
453
454  SVN_ERR(svn_cmdline_fflush(stdout));
455  SVN_ERR(svn_cmdline_fflush(stderr));
456
457  /* Print a diff if requested. */
458  if (lb->show_diff)
459    {
460      svn_stream_t *outstream;
461      svn_stream_t *errstream;
462
463      SVN_ERR(svn_stream_for_stdout(&outstream, pool));
464      SVN_ERR(svn_stream_for_stderr(&errstream, pool));
465
466      SVN_ERR(display_diff(log_entry,
467                           lb->target_path_or_url, &lb->target_peg_revision,
468                           lb->depth, lb->diff_extensions,
469                           outstream, errstream,
470                           lb->ctx, pool));
471
472      SVN_ERR(svn_stream_close(outstream));
473      SVN_ERR(svn_stream_close(errstream));
474    }
475
476  if (log_entry->has_children)
477    {
478      if (! lb->merge_stack)
479        lb->merge_stack = apr_array_make(lb->pool, 1, sizeof(svn_revnum_t));
480
481      APR_ARRAY_PUSH(lb->merge_stack, svn_revnum_t) = log_entry->revision;
482    }
483
484  return SVN_NO_ERROR;
485}
486
487
488/* This implements `svn_log_entry_receiver_t', printing the logs in XML.
489 *
490 * BATON is of type `svn_cl__log_receiver_baton'.
491 *
492 * Here is an example of the output; note that the "<log>" and
493 * "</log>" tags are not emitted by this function:
494 *
495 * $ svn log --xml -r 1648:1649
496 * <log>
497 * <logentry
498 *    revision="1648">
499 * <author>david</author>
500 * <date>2002-04-06T16:34:51.428043Z</date>
501 * <msg> * packages/rpm/subversion.spec : Now requires apache 2.0.36.
502 * </msg>
503 * </logentry>
504 * <logentry
505 *    revision="1649">
506 * <author>cmpilato</author>
507 * <date>2002-04-06T17:01:28.185136Z</date>
508 * <msg>Fix error handling when the $EDITOR is needed but unavailable.  Ah
509 * ... now that&apos;s *much* nicer.
510 *
511 * * subversion/clients/cmdline/util.c
512 *   (svn_cl__edit_externally): Clean up the &quot;no external editor&quot;
513 *   error message.
514 *   (svn_cl__get_log_message): Wrap &quot;no external editor&quot;
515 *   errors with helpful hints about the -m and -F options.
516 *
517 * * subversion/libsvn_client/commit.c
518 *   (svn_client_commit): Actually capture and propagate &quot;no external
519 *   editor&quot; errors.</msg>
520 * </logentry>
521 * </log>
522 *
523 */
524svn_error_t *
525svn_cl__log_entry_receiver_xml(void *baton,
526                               svn_log_entry_t *log_entry,
527                               apr_pool_t *pool)
528{
529  svn_cl__log_receiver_baton *lb = baton;
530  /* Collate whole log message into sb before printing. */
531  svn_stringbuf_t *sb = svn_stringbuf_create_empty(pool);
532  char *revstr;
533  const char *author;
534  const char *date;
535  const char *message;
536
537  if (lb->ctx->cancel_func)
538    SVN_ERR(lb->ctx->cancel_func(lb->ctx->cancel_baton));
539
540  svn_compat_log_revprops_out(&author, &date, &message, log_entry->revprops);
541
542  if (log_entry->revision == 0 && message == NULL)
543    return SVN_NO_ERROR;
544
545  if (! SVN_IS_VALID_REVNUM(log_entry->revision))
546    {
547      svn_xml_make_close_tag(&sb, pool, "logentry");
548      SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout));
549      if (lb->merge_stack)
550        apr_array_pop(lb->merge_stack);
551
552      return SVN_NO_ERROR;
553    }
554
555  /* Match search pattern before XML-escaping. */
556  if (lb->search_patterns &&
557      ! match_search_patterns(lb->search_patterns, author, date, message,
558                              log_entry->changed_paths2, &lb->buffer, pool))
559    {
560      if (log_entry->has_children)
561        {
562          if (! lb->merge_stack)
563            lb->merge_stack = apr_array_make(lb->pool, 1, sizeof(svn_revnum_t));
564
565          APR_ARRAY_PUSH(lb->merge_stack, svn_revnum_t) = log_entry->revision;
566        }
567
568      return SVN_NO_ERROR;
569    }
570
571  if (author)
572    author = svn_xml_fuzzy_escape(author, pool);
573  if (date)
574    date = svn_xml_fuzzy_escape(date, pool);
575  if (message)
576    message = svn_xml_fuzzy_escape(message, pool);
577
578  revstr = apr_psprintf(pool, "%ld", log_entry->revision);
579  /* <logentry revision="xxx"> */
580  if (lb->merge_stack && lb->merge_stack->nelts > 0)
581    {
582      svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "logentry",
583                            "revision", revstr, "reverse-merge",
584                            log_entry->subtractive_merge ? "true" : "false",
585                            SVN_VA_NULL);
586
587    }
588  else
589    {
590        svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "logentry",
591                              "revision", revstr, SVN_VA_NULL);
592    }
593
594  /* <author>xxx</author> */
595  svn_cl__xml_tagged_cdata(&sb, pool, "author", author);
596
597  /* Print the full, uncut, date.  This is machine output. */
598  /* According to the docs for svn_log_entry_receiver_t, either
599     NULL or the empty string represents no date.  Avoid outputting an
600     empty date element. */
601  if (date && date[0] == '\0')
602    date = NULL;
603  /* <date>xxx</date> */
604  svn_cl__xml_tagged_cdata(&sb, pool, "date", date);
605
606  if (log_entry->changed_paths2)
607    {
608      apr_array_header_t *sorted_paths;
609      int i;
610
611      /* <paths> */
612      svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "paths",
613                            SVN_VA_NULL);
614
615      /* Get an array of sorted hash keys. */
616      sorted_paths = svn_sort__hash(log_entry->changed_paths2,
617                                    svn_sort_compare_items_as_paths, pool);
618
619      for (i = 0; i < sorted_paths->nelts; i++)
620        {
621          svn_sort__item_t *item = &(APR_ARRAY_IDX(sorted_paths, i,
622                                                   svn_sort__item_t));
623          const char *path = item->key;
624          svn_log_changed_path2_t *log_item = item->value;
625          char action[2];
626
627          action[0] = log_item->action;
628          action[1] = '\0';
629          if (log_item->copyfrom_path
630              && SVN_IS_VALID_REVNUM(log_item->copyfrom_rev))
631            {
632              /* <path action="X" copyfrom-path="xxx" copyfrom-rev="xxx"> */
633              revstr = apr_psprintf(pool, "%ld",
634                                    log_item->copyfrom_rev);
635              svn_xml_make_open_tag(&sb, pool, svn_xml_protect_pcdata, "path",
636                                    "action", action,
637                                    "copyfrom-path", log_item->copyfrom_path,
638                                    "copyfrom-rev", revstr,
639                                    "kind", svn_cl__node_kind_str_xml(
640                                                     log_item->node_kind),
641                                    "text-mods", svn_tristate__to_word(
642                                                     log_item->text_modified),
643                                    "prop-mods", svn_tristate__to_word(
644                                                     log_item->props_modified),
645                                    SVN_VA_NULL);
646            }
647          else
648            {
649              /* <path action="X"> */
650              svn_xml_make_open_tag(&sb, pool, svn_xml_protect_pcdata, "path",
651                                    "action", action,
652                                    "kind", svn_cl__node_kind_str_xml(
653                                                     log_item->node_kind),
654                                    "text-mods", svn_tristate__to_word(
655                                                     log_item->text_modified),
656                                    "prop-mods", svn_tristate__to_word(
657                                                     log_item->props_modified),
658                                    SVN_VA_NULL);
659            }
660          /* xxx</path> */
661          svn_xml_escape_cdata_cstring(&sb, path, pool);
662          svn_xml_make_close_tag(&sb, pool, "path");
663        }
664
665      /* </paths> */
666      svn_xml_make_close_tag(&sb, pool, "paths");
667    }
668
669  if (message != NULL)
670    {
671      /* <msg>xxx</msg> */
672      svn_cl__xml_tagged_cdata(&sb, pool, "msg", message);
673    }
674
675  svn_compat_log_revprops_clear(log_entry->revprops);
676  if (log_entry->revprops && apr_hash_count(log_entry->revprops) > 0)
677    {
678      svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "revprops", SVN_VA_NULL);
679      SVN_ERR(svn_cmdline__print_xml_prop_hash(&sb, log_entry->revprops,
680                                               FALSE, /* name_only */
681                                               FALSE, pool));
682      svn_xml_make_close_tag(&sb, pool, "revprops");
683    }
684
685  if (log_entry->has_children)
686    {
687      if (! lb->merge_stack)
688        lb->merge_stack = apr_array_make(lb->pool, 1, sizeof(svn_revnum_t));
689
690      APR_ARRAY_PUSH(lb->merge_stack, svn_revnum_t) = log_entry->revision;
691    }
692  else
693    svn_xml_make_close_tag(&sb, pool, "logentry");
694
695  return svn_cl__error_checked_fputs(sb->data, stdout);
696}
697
698
699/* This implements the `svn_opt_subcommand_t' interface. */
700svn_error_t *
701svn_cl__log(apr_getopt_t *os,
702            void *baton,
703            apr_pool_t *pool)
704{
705  svn_cl__opt_state_t *opt_state = ((svn_cl__cmd_baton_t *) baton)->opt_state;
706  svn_client_ctx_t *ctx = ((svn_cl__cmd_baton_t *) baton)->ctx;
707  apr_array_header_t *targets;
708  svn_cl__log_receiver_baton lb;
709  const char *target;
710  int i;
711  apr_array_header_t *revprops;
712
713  if (!opt_state->xml)
714    {
715      if (opt_state->all_revprops)
716        return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
717                                _("'with-all-revprops' option only valid in"
718                                  " XML mode"));
719      if (opt_state->no_revprops)
720        return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
721                                _("'with-no-revprops' option only valid in"
722                                  " XML mode"));
723      if (opt_state->revprop_table != NULL)
724        return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
725                                _("'with-revprop' option only valid in"
726                                  " XML mode"));
727    }
728  else
729    {
730      if (opt_state->show_diff)
731        return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
732                                _("'diff' option is not supported in "
733                                  "XML mode"));
734    }
735
736  if (opt_state->diff.diff_cmd && (! opt_state->show_diff))
737    return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
738                            _("'diff-cmd' option requires 'diff' "
739                              "option"));
740  if (opt_state->diff.internal_diff && (! opt_state->show_diff))
741    return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
742                            _("'internal-diff' option requires "
743                              "'diff' option"));
744  if (opt_state->extensions && (! opt_state->show_diff))
745    return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
746                            _("'extensions' option requires 'diff' "
747                              "option"));
748
749  if (opt_state->depth != svn_depth_unknown && (! opt_state->show_diff))
750    return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
751                            _("'depth' option requires 'diff' option"));
752
753  SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os,
754                                                      opt_state->targets,
755                                                      ctx, FALSE, pool));
756
757  /* Add "." if user passed 0 arguments */
758  svn_opt_push_implicit_dot_target(targets, pool);
759
760  /* Determine if they really want a two-revision range. */
761  if (opt_state->used_change_arg)
762    {
763      if (opt_state->used_revision_arg && opt_state->revision_ranges->nelts > 1)
764        {
765          return svn_error_create
766            (SVN_ERR_CLIENT_BAD_REVISION, NULL,
767             _("-c and -r are mutually exclusive"));
768        }
769      for (i = 0; i < opt_state->revision_ranges->nelts; i++)
770        {
771          svn_opt_revision_range_t *range;
772          range = APR_ARRAY_IDX(opt_state->revision_ranges, i,
773                                svn_opt_revision_range_t *);
774          if (range->start.value.number < range->end.value.number)
775            range->start.value.number++;
776          else
777            range->end.value.number++;
778        }
779    }
780
781  /* Parse the first target into path-or-url and peg revision. */
782  target = APR_ARRAY_IDX(targets, 0, const char *);
783  SVN_ERR(svn_opt_parse_path(&lb.target_peg_revision, &lb.target_path_or_url,
784                             target, pool));
785  if (lb.target_peg_revision.kind == svn_opt_revision_unspecified)
786    lb.target_peg_revision.kind = (svn_path_is_url(target)
787                                     ? svn_opt_revision_head
788                                     : svn_opt_revision_working);
789  APR_ARRAY_IDX(targets, 0, const char *) = lb.target_path_or_url;
790
791  if (svn_path_is_url(target))
792    {
793      for (i = 1; i < targets->nelts; i++)
794        {
795          target = APR_ARRAY_IDX(targets, i, const char *);
796
797          if (svn_path_is_url(target) || target[0] == '/')
798            return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
799                                     _("Only relative paths can be specified"
800                                       " after a URL for 'svn log', "
801                                       "but '%s' is not a relative path"),
802                                     target);
803        }
804    }
805
806  lb.ctx = ctx;
807  lb.omit_log_message = opt_state->quiet;
808  lb.show_diff = opt_state->show_diff;
809  lb.depth = opt_state->depth == svn_depth_unknown ? svn_depth_infinity
810                                                   : opt_state->depth;
811  lb.diff_extensions = opt_state->extensions;
812  lb.merge_stack = NULL;
813  lb.search_patterns = opt_state->search_patterns;
814  svn_membuf__create(&lb.buffer, 0, pool);
815  lb.pool = pool;
816
817  if (opt_state->xml)
818    {
819      /* If output is not incremental, output the XML header and wrap
820         everything in a top-level element. This makes the output in
821         its entirety a well-formed XML document. */
822      if (! opt_state->incremental)
823        SVN_ERR(svn_cl__xml_print_header("log", pool));
824
825      if (opt_state->all_revprops)
826        revprops = NULL;
827      else if(opt_state->no_revprops)
828        {
829          revprops = apr_array_make(pool, 0, sizeof(char *));
830        }
831      else if (opt_state->revprop_table != NULL)
832        {
833          apr_hash_index_t *hi;
834          revprops = apr_array_make(pool,
835                                    apr_hash_count(opt_state->revprop_table),
836                                    sizeof(char *));
837          for (hi = apr_hash_first(pool, opt_state->revprop_table);
838               hi != NULL;
839               hi = apr_hash_next(hi))
840            {
841              const char *property = apr_hash_this_key(hi);
842              svn_string_t *value = apr_hash_this_val(hi);
843
844              if (value && value->data[0] != '\0')
845                return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
846                                         _("cannot assign with 'with-revprop'"
847                                           " option (drop the '=')"));
848              APR_ARRAY_PUSH(revprops, const char *) = property;
849            }
850        }
851      else
852        {
853          revprops = apr_array_make(pool, 3, sizeof(char *));
854          APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_AUTHOR;
855          APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_DATE;
856          if (!opt_state->quiet)
857            APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_LOG;
858        }
859      SVN_ERR(svn_client_log5(targets,
860                              &lb.target_peg_revision,
861                              opt_state->revision_ranges,
862                              opt_state->limit,
863                              opt_state->verbose,
864                              opt_state->stop_on_copy,
865                              opt_state->use_merge_history,
866                              revprops,
867                              svn_cl__log_entry_receiver_xml,
868                              &lb,
869                              ctx,
870                              pool));
871
872      if (! opt_state->incremental)
873        SVN_ERR(svn_cl__xml_print_footer("log", pool));
874    }
875  else  /* default output format */
876    {
877      revprops = apr_array_make(pool, 3, sizeof(char *));
878      APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_AUTHOR;
879      APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_DATE;
880      if (!opt_state->quiet)
881        APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_LOG;
882      SVN_ERR(svn_client_log5(targets,
883                              &lb.target_peg_revision,
884                              opt_state->revision_ranges,
885                              opt_state->limit,
886                              opt_state->verbose,
887                              opt_state->stop_on_copy,
888                              opt_state->use_merge_history,
889                              revprops,
890                              svn_cl__log_entry_receiver,
891                              &lb,
892                              ctx,
893                              pool));
894
895      if (! opt_state->incremental)
896        SVN_ERR(svn_cmdline_printf(pool, SVN_CL__LOG_SEP_STRING));
897    }
898
899  return SVN_NO_ERROR;
900}
901