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
, _("Merged via:")));
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 struct log_receiver_baton lb
;
453 svn_opt_revision_t peg_revision
;
454 const char *true_path
;
455 apr_array_header_t
*revprops
;
459 if (opt_state
->all_revprops
)
460 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR
, NULL
,
461 _("'with-all-revprops' option only valid in"
463 if (opt_state
->revprop_table
!= NULL
)
464 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR
, NULL
,
465 _("'with-revprop' option only valid in"
469 SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets
, os
,
473 /* Add "." if user passed 0 arguments */
474 svn_opt_push_implicit_dot_target(targets
, pool
);
476 target
= APR_ARRAY_IDX(targets
, 0, const char *);
478 /* Determine if they really want a two-revision range. */
479 if (opt_state
->used_change_arg
)
481 if (opt_state
->start_revision
.value
.number
<
482 opt_state
->end_revision
.value
.number
)
483 opt_state
->start_revision
= opt_state
->end_revision
;
485 opt_state
->end_revision
= opt_state
->start_revision
;
488 /* Strip peg revision if targets contains an URI. */
489 SVN_ERR(svn_opt_parse_path(&peg_revision
, &true_path
, target
, pool
));
490 APR_ARRAY_IDX(targets
, 0, const char *) = true_path
;
492 if ((opt_state
->start_revision
.kind
!= svn_opt_revision_unspecified
)
493 && (opt_state
->end_revision
.kind
== svn_opt_revision_unspecified
))
495 /* If the user specified exactly one revision, then start rev is
496 set but end is not. We show the log message for just that
497 revision by making end equal to start.
499 Note that if the user requested a single dated revision, then
500 this will cause the same date to be resolved twice. The
501 extra code complexity to get around this slight inefficiency
502 doesn't seem worth it, however. */
504 opt_state
->end_revision
= opt_state
->start_revision
;
506 else if (opt_state
->start_revision
.kind
== svn_opt_revision_unspecified
)
508 /* Default to any specified peg revision. Otherwise, if the
509 first target is an URL, then we default to HEAD:0. Lastly,
510 the default is BASE:0 since WC@HEAD may not exist. */
511 if (peg_revision
.kind
== svn_opt_revision_unspecified
)
513 if (svn_path_is_url(target
))
514 opt_state
->start_revision
.kind
= svn_opt_revision_head
;
516 opt_state
->start_revision
.kind
= svn_opt_revision_base
;
519 opt_state
->start_revision
= peg_revision
;
521 if (opt_state
->end_revision
.kind
== svn_opt_revision_unspecified
)
523 opt_state
->end_revision
.kind
= svn_opt_revision_number
;
524 opt_state
->end_revision
.value
.number
= 0;
528 if (svn_path_is_url(target
))
530 for (i
= 1; i
< targets
->nelts
; i
++)
532 target
= APR_ARRAY_IDX(targets
, i
, const char *);
534 if (svn_path_is_url(target
))
535 return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE
, NULL
,
536 _("Only relative paths can be specified "
541 lb
.cancel_func
= ctx
->cancel_func
;
542 lb
.cancel_baton
= ctx
->cancel_baton
;
543 lb
.omit_log_message
= opt_state
->quiet
;
544 lb
.merge_stack
= apr_array_make(pool
, 0, sizeof(svn_revnum_t
));
547 if (! opt_state
->quiet
)
548 svn_cl__get_notifier(&ctx
->notify_func2
, &ctx
->notify_baton2
, FALSE
,
553 /* If output is not incremental, output the XML header and wrap
554 everything in a top-level element. This makes the output in
555 its entirety a well-formed XML document. */
556 if (! opt_state
->incremental
)
557 SVN_ERR(svn_cl__xml_print_header("log", pool
));
559 if (opt_state
->all_revprops
)
561 else if (opt_state
->revprop_table
!= NULL
)
563 apr_hash_index_t
*hi
;
564 revprops
= apr_array_make(pool
,
565 apr_hash_count(opt_state
->revprop_table
),
567 for (hi
= apr_hash_first(pool
, opt_state
->revprop_table
);
569 hi
= apr_hash_next(hi
))
573 apr_hash_this(hi
, (void *)&property
, NULL
, (void *)&value
);
574 if (value
&& value
->data
[0] != '\0')
575 return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR
, NULL
,
576 _("cannot assign with 'with-revprop'"
577 " option (drop the '=')"));
578 APR_ARRAY_PUSH(revprops
, char *) = property
;
583 revprops
= apr_array_make(pool
, 3, sizeof(char *));
584 APR_ARRAY_PUSH(revprops
, const char *) = SVN_PROP_REVISION_AUTHOR
;
585 APR_ARRAY_PUSH(revprops
, const char *) = SVN_PROP_REVISION_DATE
;
586 if (!opt_state
->quiet
)
587 APR_ARRAY_PUSH(revprops
, const char *) = SVN_PROP_REVISION_LOG
;
589 SVN_ERR(svn_client_log4(targets
,
591 &(opt_state
->start_revision
),
592 &(opt_state
->end_revision
),
595 opt_state
->stop_on_copy
,
596 opt_state
->use_merge_history
,
598 log_entry_receiver_xml
,
603 if (! opt_state
->incremental
)
604 SVN_ERR(svn_cl__xml_print_footer("log", pool
));
606 else /* default output format */
608 revprops
= apr_array_make(pool
, 3, sizeof(char *));
609 APR_ARRAY_PUSH(revprops
, const char *) = SVN_PROP_REVISION_AUTHOR
;
610 APR_ARRAY_PUSH(revprops
, const char *) = SVN_PROP_REVISION_DATE
;
611 if (!opt_state
->quiet
)
612 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
,
627 if (! opt_state
->incremental
)
628 SVN_ERR(svn_cmdline_printf(pool
, SEP_STRING
));