Followup to r29625: fix getopt tests.
[svn.git] / subversion / svn / log-cmd.c
blob95c22bcc6026dfe041a0efcb853717874f392358
1 /*
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 /* ==================================================================== */
23 /*** Includes. ***/
25 #define APR_WANT_STRFUNC
26 #define APR_WANT_STDIO
27 #include <apr_want.h>
29 #include "svn_client.h"
30 #include "svn_compat.h"
31 #include "svn_string.h"
32 #include "svn_path.h"
33 #include "svn_error.h"
34 #include "svn_sorts.h"
35 #include "svn_xml.h"
36 #include "svn_time.h"
37 #include "svn_cmdline.h"
38 #include "cl.h"
40 #include "svn_private_config.h"
43 /*** Code. ***/
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;
50 void *cancel_baton;
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. */
59 apr_pool_t *pool;
63 /* The separator between log messages. */
64 #define SEP_STRING \
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
84 * Fix for Issue #694.
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 * ------------------------------------------------------------------------
97 * Or:
99 * $ svn log -r1847:1846 -v
100 * ------------------------------------------------------------------------
101 * rev 1847: cmpilato | Wed 1 May 2002 15:44:26 | 7 lines
102 * Changed paths:
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
114 * Changed paths:
115 * M /trunk/notes/fs_dumprestore.txt
116 * M /trunk/subversion/libsvn_repos/dump.c
118 * imagine an example log message here
119 * ------------------------------------------------------------------------
121 * Or:
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 * ------------------------------------------------------------------------
130 * Or:
132 * $ svn log -r1847:1846 -qv
133 * ------------------------------------------------------------------------
134 * rev 1847: cmpilato | Wed 1 May 2002 15:44:26
135 * Changed paths:
136 * M /trunk/subversion/libsvn_repos/delta.c
137 * ------------------------------------------------------------------------
138 * rev 1846: whoever | Wed 1 May 2002 15:23:41
139 * Changed paths:
140 * M /trunk/notes/fs_dumprestore.txt
141 * M /trunk/subversion/libsvn_repos/dump.c
142 * ------------------------------------------------------------------------
145 static svn_error_t *
146 log_entry_receiver(void *baton,
147 svn_log_entry_t *log_entry,
148 apr_pool_t *pool)
150 struct log_receiver_baton *lb = baton;
151 const char *author;
152 const char *date;
153 const char *message;
155 /* Number of lines in the msg. */
156 int lines;
158 if (lb->cancel_func)
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)
164 return SVN_NO_ERROR;
166 if (! SVN_IS_VALID_REVNUM(log_entry->revision))
168 apr_array_pop(lb->merge_stack);
169 return SVN_NO_ERROR;
172 /* ### See http://subversion.tigris.org/issues/show_bug.cgi?id=807
173 for more on the fallback fuzzy conversions below. */
175 if (author == NULL)
176 author = _("(no author)");
178 if (date && date[0])
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);
186 else
187 date = _("(no date)");
189 if (! lb->omit_log_message && message == NULL)
190 message = "";
192 SVN_ERR(svn_cmdline_printf(pool,
193 SEP_STRING "r%ld | %s | %s",
194 log_entry->revision, author, date));
196 if (message != NULL)
198 lines = svn_cstring_count_newlines(message) + 1;
199 SVN_ERR(svn_cmdline_printf(pool,
200 (lines != 1)
201 ? " | %d lines"
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;
210 int i;
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,
221 svn_sort__item_t));
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))
230 copy_data
231 = apr_psprintf(pool,
232 _(" (from %s:%ld)"),
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,
238 copy_data));
242 if (lb->merge_stack->nelts > 0)
244 int i;
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 ?
254 '\n' : ','));
258 if (message != NULL)
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;
269 return SVN_NO_ERROR;
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
281 * <log>
282 * <logentry
283 * revision="1648">
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.
287 * </msg>
288 * </logentry>
289 * <logentry
290 * revision="1649">
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&apos;s *much* nicer.
296 * * subversion/clients/cmdline/util.c
297 * (svn_cl__edit_externally): Clean up the &quot;no external editor&quot;
298 * error message.
299 * (svn_cl__get_log_message): Wrap &quot;no external editor&quot;
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 &quot;no external
304 * editor&quot; errors.</msg>
305 * </logentry>
306 * </log>
309 static svn_error_t *
310 log_entry_receiver_xml(void *baton,
311 svn_log_entry_t *log_entry,
312 apr_pool_t *pool)
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);
317 char *revstr;
318 const char *author;
319 const char *date;
320 const char *message;
322 if (lb->cancel_func)
323 SVN_ERR(lb->cancel_func(lb->cancel_baton));
325 svn_compat_log_revprops_out(&author, &date, &message, log_entry->revprops);
327 if (author)
328 author = svn_xml_fuzzy_escape(author, pool);
329 if (date)
330 date = svn_xml_fuzzy_escape(date, pool);
331 if (message)
332 message = svn_xml_fuzzy_escape(message, pool);
334 if (log_entry->revision == 0 && message == NULL)
335 return SVN_NO_ERROR;
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);
343 return SVN_NO_ERROR;
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')
359 date = NULL;
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;
366 char *path;
368 /* <paths> */
369 svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "paths",
370 NULL);
372 for (hi = apr_hash_first(pool, log_entry->changed_paths);
373 hi != NULL;
374 hi = apr_hash_next(hi))
376 void *val;
377 char action[2];
378 svn_log_changed_path_t *log_item;
380 apr_hash_this(hi, (void *) &path, NULL, &val);
381 log_item = val;
383 action[0] = log_item->action;
384 action[1] = '\0';
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",
395 "action", action,
396 "copyfrom-path", escpath->data,
397 "copyfrom-rev", revstr, NULL);
399 else
401 /* <path action="X"> */
402 svn_xml_make_open_tag(&sb, pool, svn_xml_protect_pcdata, "path",
403 "action", action, NULL);
405 /* xxx</path> */
406 svn_xml_escape_cdata_cstring(&sb, path, pool);
407 svn_xml_make_close_tag(&sb, pool, "path");
410 /* </paths> */
411 svn_xml_make_close_tag(&sb, pool, "paths");
414 if (message != NULL)
416 /* <msg>xxx</msg> */
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 */
426 pool));
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;
432 else
433 svn_xml_make_close_tag(&sb, pool, "logentry");
435 SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout));
437 return SVN_NO_ERROR;
441 /* This implements the `svn_opt_subcommand_t' interface. */
442 svn_error_t *
443 svn_cl__log(apr_getopt_t *os,
444 void *baton,
445 apr_pool_t *pool)
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;
451 const char *target;
452 int i;
453 svn_opt_revision_t peg_revision;
454 const char *true_path;
455 apr_array_header_t *revprops;
457 if (!opt_state->xml)
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"
462 " XML mode"));
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"
466 " XML mode"));
469 SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os,
470 opt_state->targets,
471 pool));
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;
484 else
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;
515 else
516 opt_state->start_revision.kind = svn_opt_revision_base;
518 else
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 "
537 "after a URL"));
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));
545 lb.pool = pool;
547 if (! opt_state->quiet)
548 svn_cl__get_notifier(&ctx->notify_func2, &ctx->notify_baton2, FALSE,
549 FALSE, FALSE, pool);
551 if (opt_state->xml)
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)
560 revprops = NULL;
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),
566 sizeof(char *));
567 for (hi = apr_hash_first(pool, opt_state->revprop_table);
568 hi != NULL;
569 hi = apr_hash_next(hi))
571 char *property;
572 svn_string_t *value;
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;
581 else
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,
590 &peg_revision,
591 &(opt_state->start_revision),
592 &(opt_state->end_revision),
593 opt_state->limit,
594 opt_state->verbose,
595 opt_state->stop_on_copy,
596 opt_state->use_merge_history,
597 revprops,
598 log_entry_receiver_xml,
599 &lb,
600 ctx,
601 pool));
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,
614 &peg_revision,
615 &(opt_state->start_revision),
616 &(opt_state->end_revision),
617 opt_state->limit,
618 opt_state->verbose,
619 opt_state->stop_on_copy,
620 opt_state->use_merge_history,
621 revprops,
622 log_entry_receiver,
623 &lb,
624 ctx,
625 pool));
627 if (! opt_state->incremental)
628 SVN_ERR(svn_cmdline_printf(pool, SEP_STRING));
631 return SVN_NO_ERROR;