2 * log-cmd.c -- Display log messages
4 * ====================================================================
5 * Copyright (c) 2000-2007 CollabNet. All rights reserved.
7 * This software is licensed as described in the file COPYING, which
8 * you should have received as part of this distribution. The terms
9 * are also available at http://subversion.tigris.org/license-1.html.
10 * If newer versions of this license are posted there, you may use a
11 * newer version instead, at your option.
13 * This software consists of voluntary contributions made by many
14 * individuals. For exact contribution history, see the revision
15 * history and logs, available at http://subversion.tigris.org/.
16 * ====================================================================
19 /* ==================================================================== */
25 #define APR_WANT_STRFUNC
26 #define APR_WANT_STDIO
29 #include "svn_client.h"
30 #include "svn_compat.h"
31 #include "svn_string.h"
33 #include "svn_error.h"
34 #include "svn_sorts.h"
37 #include "svn_cmdline.h"
40 #include "svn_private_config.h"
45 /* Baton for log_entry_receiver() and log_entry_receiver_xml(). */
46 struct log_receiver_baton
48 /* Check for cancellation on each invocation of a log receiver. */
49 svn_cancel_func_t cancel_func
;
52 /* Don't print log message body nor its line count. */
53 svn_boolean_t omit_log_message
;
55 /* Stack which keeps track of merge revision nesting, using svn_revnum_t's */
56 apr_array_header_t
*merge_stack
;
58 /* Pool for persistent allocations. */
63 /* The separator between log messages. */
65 "------------------------------------------------------------------------\n"
68 /* Implement `svn_log_entry_receiver_t', printing the logs in
69 * a human-readable and machine-parseable format.
71 * BATON is of type `struct log_receiver_baton'.
73 * First, print a header line. Then if CHANGED_PATHS is non-null,
74 * print all affected paths in a list headed "Changed paths:\n",
75 * immediately following the header line. Then print a newline
76 * followed by the message body, unless BATON->omit_log_message is true.
78 * Here are some examples of the output:
80 * $ svn log -r1847:1846
81 * ------------------------------------------------------------------------
82 * rev 1847: cmpilato | Wed 1 May 2002 15:44:26 | 7 lines
86 * * subversion/libsvn_repos/delta.c
87 * (delta_files): Rework the logic in this function to only call
88 * send_text_deltas if there are deltas to send, and within that case,
89 * only use a real delta stream if the caller wants real text deltas.
91 * ------------------------------------------------------------------------
92 * rev 1846: whoever | Wed 1 May 2002 15:23:41 | 1 line
94 * imagine an example log message here
95 * ------------------------------------------------------------------------
99 * $ svn log -r1847:1846 -v
100 * ------------------------------------------------------------------------
101 * rev 1847: cmpilato | Wed 1 May 2002 15:44:26 | 7 lines
103 * M /trunk/subversion/libsvn_repos/delta.c
105 * Fix for Issue #694.
107 * * subversion/libsvn_repos/delta.c
108 * (delta_files): Rework the logic in this function to only call
109 * send_text_deltas if there are deltas to send, and within that case,
110 * only use a real delta stream if the caller wants real text deltas.
112 * ------------------------------------------------------------------------
113 * rev 1846: whoever | Wed 1 May 2002 15:23:41 | 1 line
115 * M /trunk/notes/fs_dumprestore.txt
116 * M /trunk/subversion/libsvn_repos/dump.c
118 * imagine an example log message here
119 * ------------------------------------------------------------------------
123 * $ svn log -r1847:1846 -q
124 * ------------------------------------------------------------------------
125 * rev 1847: cmpilato | Wed 1 May 2002 15:44:26
126 * ------------------------------------------------------------------------
127 * rev 1846: whoever | Wed 1 May 2002 15:23:41
128 * ------------------------------------------------------------------------
132 * $ svn log -r1847:1846 -qv
133 * ------------------------------------------------------------------------
134 * rev 1847: cmpilato | Wed 1 May 2002 15:44:26
136 * M /trunk/subversion/libsvn_repos/delta.c
137 * ------------------------------------------------------------------------
138 * rev 1846: whoever | Wed 1 May 2002 15:23:41
140 * M /trunk/notes/fs_dumprestore.txt
141 * M /trunk/subversion/libsvn_repos/dump.c
142 * ------------------------------------------------------------------------
146 log_entry_receiver(void *baton
,
147 svn_log_entry_t
*log_entry
,
150 struct log_receiver_baton
*lb
= baton
;
155 /* Number of lines in the msg. */
159 SVN_ERR(lb
->cancel_func(lb
->cancel_baton
));
161 svn_compat_log_revprops_out(&author
, &date
, &message
, log_entry
->revprops
);
163 if (log_entry
->revision
== 0 && message
== NULL
)
166 if (! SVN_IS_VALID_REVNUM(log_entry
->revision
))
168 apr_array_pop(lb
->merge_stack
);
172 /* ### See http://subversion.tigris.org/issues/show_bug.cgi?id=807
173 for more on the fallback fuzzy conversions below. */
176 author
= _("(no author)");
180 /* Convert date to a format for humans. */
181 apr_time_t time_temp
;
183 SVN_ERR(svn_time_from_cstring(&time_temp
, date
, pool
));
184 date
= svn_time_to_human_cstring(time_temp
, pool
);
187 date
= _("(no date)");
189 if (! lb
->omit_log_message
&& message
== NULL
)
192 SVN_ERR(svn_cmdline_printf(pool
,
193 SEP_STRING
"r%ld | %s | %s",
194 log_entry
->revision
, author
, date
));
198 lines
= svn_cstring_count_newlines(message
) + 1;
199 SVN_ERR(svn_cmdline_printf(pool
,
202 : " | %d line", lines
));
205 SVN_ERR(svn_cmdline_printf(pool
, "\n"));
207 if (log_entry
->changed_paths
)
209 apr_array_header_t
*sorted_paths
;
212 /* Get an array of sorted hash keys. */
213 sorted_paths
= svn_sort__hash(log_entry
->changed_paths
,
214 svn_sort_compare_items_as_paths
, pool
);
216 SVN_ERR(svn_cmdline_printf(pool
,
217 _("Changed paths:\n")));
218 for (i
= 0; i
< sorted_paths
->nelts
; i
++)
220 svn_sort__item_t
*item
= &(APR_ARRAY_IDX(sorted_paths
, i
,
222 const char *path
= item
->key
;
223 svn_log_changed_path_t
*log_item
224 = apr_hash_get(log_entry
->changed_paths
, item
->key
, item
->klen
);
225 const char *copy_data
= "";
227 if (log_item
->copyfrom_path
228 && SVN_IS_VALID_REVNUM(log_item
->copyfrom_rev
))
233 log_item
->copyfrom_path
,
234 log_item
->copyfrom_rev
);
236 SVN_ERR(svn_cmdline_printf(pool
, " %c %s%s\n",
237 log_item
->action
, path
,
242 if (lb
->merge_stack
->nelts
> 0)
246 /* Print the result of merge line */
247 SVN_ERR(svn_cmdline_printf(pool
, _("Result of a merge from:")));
248 for (i
= 0; i
< lb
->merge_stack
->nelts
; i
++)
250 svn_revnum_t rev
= APR_ARRAY_IDX(lb
->merge_stack
, i
, svn_revnum_t
);
252 SVN_ERR(svn_cmdline_printf(pool
, " r%ld%c", rev
,
253 i
== lb
->merge_stack
->nelts
- 1 ?
260 /* A blank line always precedes the log message. */
261 SVN_ERR(svn_cmdline_printf(pool
, "\n%s\n", message
));
264 SVN_ERR(svn_cmdline_fflush(stdout
));
266 if (log_entry
->has_children
)
267 APR_ARRAY_PUSH(lb
->merge_stack
, svn_revnum_t
) = log_entry
->revision
;
273 /* This implements `svn_log_entry_receiver_t', printing the logs in XML.
275 * BATON is of type `struct log_receiver_baton'.
277 * Here is an example of the output; note that the "<log>" and
278 * "</log>" tags are not emitted by this function:
280 * $ svn log --xml -r 1648:1649
284 * <author>david</author>
285 * <date>2002-04-06T16:34:51.428043Z</date>
286 * <msg> * packages/rpm/subversion.spec : Now requires apache 2.0.36.
291 * <author>cmpilato</author>
292 * <date>2002-04-06T17:01:28.185136Z</date>
293 * <msg>Fix error handling when the $EDITOR is needed but unavailable. Ah
294 * ... now that's *much* nicer.
296 * * subversion/clients/cmdline/util.c
297 * (svn_cl__edit_externally): Clean up the "no external editor"
299 * (svn_cl__get_log_message): Wrap "no external editor"
300 * errors with helpful hints about the -m and -F options.
302 * * subversion/libsvn_client/commit.c
303 * (svn_client_commit): Actually capture and propogate "no external
304 * editor" errors.</msg>
310 log_entry_receiver_xml(void *baton
,
311 svn_log_entry_t
*log_entry
,
314 struct log_receiver_baton
*lb
= baton
;
315 /* Collate whole log message into sb before printing. */
316 svn_stringbuf_t
*sb
= svn_stringbuf_create("", pool
);
323 SVN_ERR(lb
->cancel_func(lb
->cancel_baton
));
325 svn_compat_log_revprops_out(&author
, &date
, &message
, log_entry
->revprops
);
328 author
= svn_xml_fuzzy_escape(author
, pool
);
330 date
= svn_xml_fuzzy_escape(date
, pool
);
332 message
= svn_xml_fuzzy_escape(message
, pool
);
334 if (log_entry
->revision
== 0 && message
== NULL
)
337 if (! SVN_IS_VALID_REVNUM(log_entry
->revision
))
339 svn_xml_make_close_tag(&sb
, pool
, "logentry");
340 SVN_ERR(svn_cl__error_checked_fputs(sb
->data
, stdout
));
341 apr_array_pop(lb
->merge_stack
);
346 revstr
= apr_psprintf(pool
, "%ld", log_entry
->revision
);
347 /* <logentry revision="xxx"> */
348 svn_xml_make_open_tag(&sb
, pool
, svn_xml_normal
, "logentry",
349 "revision", revstr
, NULL
);
351 /* <author>xxx</author> */
352 svn_cl__xml_tagged_cdata(&sb
, pool
, "author", author
);
354 /* Print the full, uncut, date. This is machine output. */
355 /* According to the docs for svn_log_entry_receiver_t, either
356 NULL or the empty string represents no date. Avoid outputting an
357 empty date element. */
358 if (date
&& date
[0] == '\0')
360 /* <date>xxx</date> */
361 svn_cl__xml_tagged_cdata(&sb
, pool
, "date", date
);
363 if (log_entry
->changed_paths
)
365 apr_hash_index_t
*hi
;
369 svn_xml_make_open_tag(&sb
, pool
, svn_xml_normal
, "paths",
372 for (hi
= apr_hash_first(pool
, log_entry
->changed_paths
);
374 hi
= apr_hash_next(hi
))
378 svn_log_changed_path_t
*log_item
;
380 apr_hash_this(hi
, (void *) &path
, NULL
, &val
);
383 action
[0] = log_item
->action
;
385 if (log_item
->copyfrom_path
386 && SVN_IS_VALID_REVNUM(log_item
->copyfrom_rev
))
388 /* <path action="X" copyfrom-path="xxx" copyfrom-rev="xxx"> */
389 svn_stringbuf_t
*escpath
= svn_stringbuf_create("", pool
);
390 svn_xml_escape_attr_cstring(&escpath
,
391 log_item
->copyfrom_path
, pool
);
392 revstr
= apr_psprintf(pool
, "%ld",
393 log_item
->copyfrom_rev
);
394 svn_xml_make_open_tag(&sb
, pool
, svn_xml_protect_pcdata
, "path",
396 "copyfrom-path", escpath
->data
,
397 "copyfrom-rev", revstr
, NULL
);
401 /* <path action="X"> */
402 svn_xml_make_open_tag(&sb
, pool
, svn_xml_protect_pcdata
, "path",
403 "action", action
, NULL
);
406 svn_xml_escape_cdata_cstring(&sb
, path
, pool
);
407 svn_xml_make_close_tag(&sb
, pool
, "path");
411 svn_xml_make_close_tag(&sb
, pool
, "paths");
417 svn_cl__xml_tagged_cdata(&sb
, pool
, "msg", message
);
420 svn_compat_log_revprops_clear(log_entry
->revprops
);
421 if (log_entry
->revprops
&& apr_hash_count(log_entry
->revprops
) > 0)
423 svn_xml_make_open_tag(&sb
, pool
, svn_xml_normal
, "revprops", NULL
);
424 SVN_ERR(svn_cl__print_xml_prop_hash(&sb
, log_entry
->revprops
,
425 FALSE
, /* name_only */
427 svn_xml_make_close_tag(&sb
, pool
, "revprops");
430 if (log_entry
->has_children
)
431 APR_ARRAY_PUSH(lb
->merge_stack
, svn_revnum_t
) = log_entry
->revision
;
433 svn_xml_make_close_tag(&sb
, pool
, "logentry");
435 SVN_ERR(svn_cl__error_checked_fputs(sb
->data
, stdout
));
441 /* This implements the `svn_opt_subcommand_t' interface. */
443 svn_cl__log(apr_getopt_t
*os
,
447 svn_cl__opt_state_t
*opt_state
= ((svn_cl__cmd_baton_t
*) baton
)->opt_state
;
448 svn_client_ctx_t
*ctx
= ((svn_cl__cmd_baton_t
*) baton
)->ctx
;
449 apr_array_header_t
*targets
;
450 apr_array_header_t
*changelist_targets
= NULL
, *combined_targets
= NULL
;
451 struct log_receiver_baton lb
;
454 svn_opt_revision_t peg_revision
;
455 const char *true_path
;
456 apr_array_header_t
*revprops
;
460 if (opt_state
->all_revprops
)
461 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR
, NULL
,
462 _("'with-all-revprops' option only valid in"
464 if (opt_state
->revprop_table
!= NULL
)
465 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR
, NULL
,
466 _("'with-revprop' option only valid in"
470 /* Before allowing svn_opt_args_to_target_array2() to canonicalize
471 all the targets, we need to build a list of targets made of both
472 ones the user typed, as well as any specified by --changelist. */
473 if (opt_state
->changelist
)
475 SVN_ERR(svn_client_get_changelist(&changelist_targets
,
476 opt_state
->changelist
,
480 if (apr_is_empty_array(changelist_targets
))
481 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR
, NULL
,
482 _("no such changelist '%s'"),
483 opt_state
->changelist
);
486 if (opt_state
->targets
&& changelist_targets
)
487 combined_targets
= apr_array_append(pool
, opt_state
->targets
,
489 else if (opt_state
->targets
)
490 combined_targets
= opt_state
->targets
;
491 else if (changelist_targets
)
492 combined_targets
= changelist_targets
;
494 SVN_ERR(svn_opt_args_to_target_array2(&targets
, os
,
495 combined_targets
, pool
));
497 /* Add "." if user passed 0 arguments */
498 svn_opt_push_implicit_dot_target(targets
, pool
);
500 target
= APR_ARRAY_IDX(targets
, 0, const char *);
502 /* Determine if they really want a two-revision range. */
503 if (opt_state
->used_change_arg
)
505 if (opt_state
->start_revision
.value
.number
<
506 opt_state
->end_revision
.value
.number
)
507 opt_state
->start_revision
= opt_state
->end_revision
;
509 opt_state
->end_revision
= opt_state
->start_revision
;
512 /* Strip peg revision if targets contains an URI. */
513 SVN_ERR(svn_opt_parse_path(&peg_revision
, &true_path
, target
, pool
));
514 APR_ARRAY_IDX(targets
, 0, const char *) = true_path
;
516 if ((opt_state
->start_revision
.kind
!= svn_opt_revision_unspecified
)
517 && (opt_state
->end_revision
.kind
== svn_opt_revision_unspecified
))
519 /* If the user specified exactly one revision, then start rev is
520 set but end is not. We show the log message for just that
521 revision by making end equal to start.
523 Note that if the user requested a single dated revision, then
524 this will cause the same date to be resolved twice. The
525 extra code complexity to get around this slight inefficiency
526 doesn't seem worth it, however. */
528 opt_state
->end_revision
= opt_state
->start_revision
;
530 else if (opt_state
->start_revision
.kind
== svn_opt_revision_unspecified
)
532 /* Default to any specified peg revision. Otherwise, if the
533 first target is an URL, then we default to HEAD:0. Lastly,
534 the default is BASE:0 since WC@HEAD may not exist. */
535 if (peg_revision
.kind
== svn_opt_revision_unspecified
)
537 if (svn_path_is_url(target
))
538 opt_state
->start_revision
.kind
= svn_opt_revision_head
;
540 opt_state
->start_revision
.kind
= svn_opt_revision_base
;
543 opt_state
->start_revision
= peg_revision
;
545 if (opt_state
->end_revision
.kind
== svn_opt_revision_unspecified
)
547 opt_state
->end_revision
.kind
= svn_opt_revision_number
;
548 opt_state
->end_revision
.value
.number
= 0;
552 if (svn_path_is_url(target
))
554 for (i
= 1; i
< targets
->nelts
; i
++)
556 target
= APR_ARRAY_IDX(targets
, i
, const char *);
558 if (svn_path_is_url(target
))
559 return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE
, NULL
,
560 _("Only relative paths can be specified "
565 lb
.cancel_func
= ctx
->cancel_func
;
566 lb
.cancel_baton
= ctx
->cancel_baton
;
567 lb
.omit_log_message
= opt_state
->quiet
;
568 lb
.merge_stack
= apr_array_make(pool
, 0, sizeof(svn_revnum_t
));
571 if (! opt_state
->quiet
)
572 svn_cl__get_notifier(&ctx
->notify_func2
, &ctx
->notify_baton2
, FALSE
,
577 /* If output is not incremental, output the XML header and wrap
578 everything in a top-level element. This makes the output in
579 its entirety a well-formed XML document. */
580 if (! opt_state
->incremental
)
581 SVN_ERR(svn_cl__xml_print_header("log", pool
));
583 if (opt_state
->all_revprops
)
585 else if (opt_state
->revprop_table
!= NULL
)
587 apr_hash_index_t
*hi
;
588 revprops
= apr_array_make(pool
,
589 apr_hash_count(opt_state
->revprop_table
),
591 for (hi
= apr_hash_first(pool
, opt_state
->revprop_table
);
593 hi
= apr_hash_next(hi
))
597 apr_hash_this(hi
, (void *)&property
, NULL
, (void *)&value
);
598 if (value
&& value
->data
[0] != '\0')
599 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR
, NULL
,
600 _("cannot assign with 'with-revprop'"
601 " option (drop the '=')"));
602 APR_ARRAY_PUSH(revprops
, char *) = property
;
607 revprops
= apr_array_make(pool
, 3, sizeof(char *));
608 APR_ARRAY_PUSH(revprops
, const char *) = SVN_PROP_REVISION_AUTHOR
;
609 APR_ARRAY_PUSH(revprops
, const char *) = SVN_PROP_REVISION_DATE
;
610 if (!opt_state
->quiet
)
611 APR_ARRAY_PUSH(revprops
, const char *) = SVN_PROP_REVISION_LOG
;
613 SVN_ERR(svn_client_log4(targets
,
615 &(opt_state
->start_revision
),
616 &(opt_state
->end_revision
),
619 opt_state
->stop_on_copy
,
620 opt_state
->use_merge_history
,
622 log_entry_receiver_xml
,
627 if (! opt_state
->incremental
)
628 SVN_ERR(svn_cl__xml_print_footer("log", pool
));
630 else /* default output format */
632 revprops
= apr_array_make(pool
, 3, sizeof(char *));
633 APR_ARRAY_PUSH(revprops
, const char *) = SVN_PROP_REVISION_AUTHOR
;
634 APR_ARRAY_PUSH(revprops
, const char *) = SVN_PROP_REVISION_DATE
;
635 if (!opt_state
->quiet
)
636 APR_ARRAY_PUSH(revprops
, const char *) = SVN_PROP_REVISION_LOG
;
637 SVN_ERR(svn_client_log4(targets
,
639 &(opt_state
->start_revision
),
640 &(opt_state
->end_revision
),
643 opt_state
->stop_on_copy
,
644 opt_state
->use_merge_history
,
651 if (! opt_state
->incremental
)
652 SVN_ERR(svn_cmdline_printf(pool
, SEP_STRING
));