In the command-line client, forbid
[svn.git] / subversion / svn / log-cmd.c
blobf9eeaa29e8d36dc69bb33b85bfaca35a7e30ba80
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, _("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 ?
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 apr_array_header_t *changelist_targets = NULL, *combined_targets = NULL;
451 struct log_receiver_baton lb;
452 const char *target;
453 int i;
454 svn_opt_revision_t peg_revision;
455 const char *true_path;
456 apr_array_header_t *revprops;
458 if (!opt_state->xml)
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"
463 " XML mode"));
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"
467 " XML mode"));
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,
478 ctx,
479 pool));
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,
488 changelist_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;
508 else
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;
539 else
540 opt_state->start_revision.kind = svn_opt_revision_base;
542 else
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 "
561 "after a URL"));
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));
569 lb.pool = pool;
571 if (! opt_state->quiet)
572 svn_cl__get_notifier(&ctx->notify_func2, &ctx->notify_baton2, FALSE,
573 FALSE, FALSE, pool);
575 if (opt_state->xml)
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)
584 revprops = NULL;
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),
590 sizeof(char *));
591 for (hi = apr_hash_first(pool, opt_state->revprop_table);
592 hi != NULL;
593 hi = apr_hash_next(hi))
595 char *property;
596 svn_string_t *value;
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;
605 else
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,
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_xml,
623 &lb,
624 ctx,
625 pool));
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,
638 &peg_revision,
639 &(opt_state->start_revision),
640 &(opt_state->end_revision),
641 opt_state->limit,
642 opt_state->verbose,
643 opt_state->stop_on_copy,
644 opt_state->use_merge_history,
645 revprops,
646 log_entry_receiver,
647 &lb,
648 ctx,
649 pool));
651 if (! opt_state->incremental)
652 SVN_ERR(svn_cmdline_printf(pool, SEP_STRING));
655 return SVN_NO_ERROR;