/* * log-cmd.c -- Display log messages * * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * ==================================================================== */ #define APR_WANT_STRFUNC #define APR_WANT_STDIO #include #include "svn_client.h" #include "svn_compat.h" #include "svn_dirent_uri.h" #include "svn_string.h" #include "svn_path.h" #include "svn_error.h" #include "svn_sorts.h" #include "svn_xml.h" #include "svn_time.h" #include "svn_cmdline.h" #include "svn_props.h" #include "svn_pools.h" #include "cl.h" #include "svn_private_config.h" /*** Code. ***/ /* Baton for log_entry_receiver() and log_entry_receiver_xml(). */ struct log_receiver_baton { /* Client context. */ svn_client_ctx_t *ctx; /* The URL target of the log operation. */ const char *target_url; /* Don't print log message body nor its line count. */ svn_boolean_t omit_log_message; /* Whether to show diffs in the log. (maps to --diff) */ svn_boolean_t show_diff; /* Depth applied to diff output. */ svn_depth_t depth; /* Diff arguments received from command line. */ const char *diff_extensions; /* Stack which keeps track of merge revision nesting, using svn_revnum_t's */ apr_array_header_t *merge_stack; /* Pool for persistent allocations. */ apr_pool_t *pool; }; /* The separator between log messages. */ #define SEP_STRING \ "------------------------------------------------------------------------\n" /* Implement `svn_log_entry_receiver_t', printing the logs in * a human-readable and machine-parseable format. * * BATON is of type `struct log_receiver_baton'. * * First, print a header line. Then if CHANGED_PATHS is non-null, * print all affected paths in a list headed "Changed paths:\n", * immediately following the header line. Then print a newline * followed by the message body, unless BATON->omit_log_message is true. * * Here are some examples of the output: * * $ svn log -r1847:1846 * ------------------------------------------------------------------------ * rev 1847: cmpilato | Wed 1 May 2002 15:44:26 | 7 lines * * Fix for Issue #694. * * * subversion/libsvn_repos/delta.c * (delta_files): Rework the logic in this function to only call * send_text_deltas if there are deltas to send, and within that case, * only use a real delta stream if the caller wants real text deltas. * * ------------------------------------------------------------------------ * rev 1846: whoever | Wed 1 May 2002 15:23:41 | 1 line * * imagine an example log message here * ------------------------------------------------------------------------ * * Or: * * $ svn log -r1847:1846 -v * ------------------------------------------------------------------------ * rev 1847: cmpilato | Wed 1 May 2002 15:44:26 | 7 lines * Changed paths: * M /trunk/subversion/libsvn_repos/delta.c * * Fix for Issue #694. * * * subversion/libsvn_repos/delta.c * (delta_files): Rework the logic in this function to only call * send_text_deltas if there are deltas to send, and within that case, * only use a real delta stream if the caller wants real text deltas. * * ------------------------------------------------------------------------ * rev 1846: whoever | Wed 1 May 2002 15:23:41 | 1 line * Changed paths: * M /trunk/notes/fs_dumprestore.txt * M /trunk/subversion/libsvn_repos/dump.c * * imagine an example log message here * ------------------------------------------------------------------------ * * Or: * * $ svn log -r1847:1846 -q * ------------------------------------------------------------------------ * rev 1847: cmpilato | Wed 1 May 2002 15:44:26 * ------------------------------------------------------------------------ * rev 1846: whoever | Wed 1 May 2002 15:23:41 * ------------------------------------------------------------------------ * * Or: * * $ svn log -r1847:1846 -qv * ------------------------------------------------------------------------ * rev 1847: cmpilato | Wed 1 May 2002 15:44:26 * Changed paths: * M /trunk/subversion/libsvn_repos/delta.c * ------------------------------------------------------------------------ * rev 1846: whoever | Wed 1 May 2002 15:23:41 * Changed paths: * M /trunk/notes/fs_dumprestore.txt * M /trunk/subversion/libsvn_repos/dump.c * ------------------------------------------------------------------------ * */ static svn_error_t * log_entry_receiver(void *baton, svn_log_entry_t *log_entry, apr_pool_t *pool) { struct log_receiver_baton *lb = baton; const char *author; const char *date; const char *message; if (lb->ctx->cancel_func) SVN_ERR(lb->ctx->cancel_func(lb->ctx->cancel_baton)); svn_compat_log_revprops_out(&author, &date, &message, log_entry->revprops); if (log_entry->revision == 0 && message == NULL) return SVN_NO_ERROR; if (! SVN_IS_VALID_REVNUM(log_entry->revision)) { apr_array_pop(lb->merge_stack); return SVN_NO_ERROR; } /* ### See http://subversion.tigris.org/issues/show_bug.cgi?id=807 for more on the fallback fuzzy conversions below. */ if (author == NULL) author = _("(no author)"); if (date && date[0]) /* Convert date to a format for humans. */ SVN_ERR(svn_cl__time_cstring_to_human_cstring(&date, date, pool)); else date = _("(no date)"); if (! lb->omit_log_message && message == NULL) message = ""; SVN_ERR(svn_cmdline_printf(pool, SEP_STRING "r%ld | %s | %s", log_entry->revision, author, date)); if (message != NULL) { /* Number of lines in the msg. */ int lines = svn_cstring_count_newlines(message) + 1; SVN_ERR(svn_cmdline_printf(pool, Q_(" | %d line", " | %d lines", lines), lines)); } SVN_ERR(svn_cmdline_printf(pool, "\n")); if (log_entry->changed_paths2) { apr_array_header_t *sorted_paths; int i; /* Get an array of sorted hash keys. */ sorted_paths = svn_sort__hash(log_entry->changed_paths2, svn_sort_compare_items_as_paths, pool); SVN_ERR(svn_cmdline_printf(pool, _("Changed paths:\n"))); for (i = 0; i < sorted_paths->nelts; i++) { svn_sort__item_t *item = &(APR_ARRAY_IDX(sorted_paths, i, svn_sort__item_t)); const char *path = item->key; svn_log_changed_path2_t *log_item = apr_hash_get(log_entry->changed_paths2, item->key, item->klen); const char *copy_data = ""; if (lb->ctx->cancel_func) SVN_ERR(lb->ctx->cancel_func(lb->ctx->cancel_baton)); if (log_item->copyfrom_path && SVN_IS_VALID_REVNUM(log_item->copyfrom_rev)) { copy_data = apr_psprintf(pool, _(" (from %s:%ld)"), log_item->copyfrom_path, log_item->copyfrom_rev); } SVN_ERR(svn_cmdline_printf(pool, " %c %s%s\n", log_item->action, path, copy_data)); } } if (lb->merge_stack->nelts > 0) { int i; /* Print the result of merge line */ if (log_entry->subtractive_merge) SVN_ERR(svn_cmdline_printf(pool, _("Reverse merged via:"))); else SVN_ERR(svn_cmdline_printf(pool, _("Merged via:"))); for (i = 0; i < lb->merge_stack->nelts; i++) { svn_revnum_t rev = APR_ARRAY_IDX(lb->merge_stack, i, svn_revnum_t); SVN_ERR(svn_cmdline_printf(pool, " r%ld%c", rev, i == lb->merge_stack->nelts - 1 ? '\n' : ',')); } } if (message != NULL) { /* A blank line always precedes the log message. */ SVN_ERR(svn_cmdline_printf(pool, "\n%s\n", message)); } /* Print a diff if requested. */ if (lb->show_diff) { apr_file_t *outfile; apr_file_t *errfile; apr_array_header_t *diff_options; apr_status_t status; svn_opt_revision_t start_revision; svn_opt_revision_t end_revision; svn_error_t *err; if ((status = apr_file_open_stdout(&outfile, pool))) return svn_error_wrap_apr(status, _("Can't open stdout")); if ((status = apr_file_open_stderr(&errfile, pool))) return svn_error_wrap_apr(status, _("Can't open stderr")); /* Fall back to "" to get options initialized either way. */ if (lb->diff_extensions) diff_options = svn_cstring_split(lb->diff_extensions, " \t\n\r", TRUE, pool); else diff_options = NULL; start_revision.kind = svn_opt_revision_number; start_revision.value.number = log_entry->revision - 1; end_revision.kind = svn_opt_revision_number; end_revision.value.number = log_entry->revision; SVN_ERR(svn_cmdline_printf(pool, _("\n"))); err = svn_client_diff5(diff_options, lb->target_url, &start_revision, lb->target_url, &end_revision, NULL, lb->depth, FALSE, /* ignore ancestry */ TRUE, /* no diff deleted */ FALSE, /* show copies as adds */ FALSE, /* ignore content type */ FALSE, /* use git diff format */ svn_cmdline_output_encoding(pool), outfile, errfile, NULL, lb->ctx, pool); if (err) { /* We get a "path not found" error in case the revision created * lb->target_url. Try to show a diff from the parent instead. */ if (err->apr_err == SVN_ERR_FS_NOT_FOUND) { const char *parent; apr_pool_t *iterpool; svn_error_clear(err); parent = svn_uri_dirname(lb->target_url, pool); iterpool = svn_pool_create(pool); while (strcmp(parent, lb->target_url) != 0) { svn_pool_clear(iterpool); err = svn_client_diff5(diff_options, parent, &start_revision, parent, &end_revision, NULL, lb->depth, FALSE, /* ignore ancestry */ TRUE, /* no diff deleted */ FALSE, /* show copies as adds */ FALSE, /* ignore content type */ FALSE, /* use git diff format */ svn_cmdline_output_encoding(iterpool), outfile, errfile, NULL, lb->ctx, iterpool); if (err == SVN_NO_ERROR) break; else { if (err->apr_err == SVN_ERR_FS_NOT_FOUND) { svn_error_clear(err); parent = svn_uri_dirname(parent, pool); continue; } if (err->apr_err == SVN_ERR_RA_ILLEGAL_URL || err->apr_err == SVN_ERR_AUTHZ_UNREADABLE || err->apr_err == SVN_ERR_RA_LOCAL_REPOS_OPEN_FAILED) { svn_error_clear(err); break; } return svn_error_trace(err); } } svn_pool_destroy(iterpool); } else return svn_error_trace(err); } SVN_ERR(svn_cmdline_printf(pool, _("\n"))); } SVN_ERR(svn_cmdline_fflush(stdout)); SVN_ERR(svn_cmdline_fflush(stderr)); if (log_entry->has_children) APR_ARRAY_PUSH(lb->merge_stack, svn_revnum_t) = log_entry->revision; return SVN_NO_ERROR; } /* This implements `svn_log_entry_receiver_t', printing the logs in XML. * * BATON is of type `struct log_receiver_baton'. * * Here is an example of the output; note that the "" and * "" tags are not emitted by this function: * * $ svn log --xml -r 1648:1649 * * * david * 2002-04-06T16:34:51.428043Z * * packages/rpm/subversion.spec : Now requires apache 2.0.36. * * * * cmpilato * 2002-04-06T17:01:28.185136Z * Fix error handling when the $EDITOR is needed but unavailable. Ah * ... now that's *much* nicer. * * * subversion/clients/cmdline/util.c * (svn_cl__edit_externally): Clean up the "no external editor" * error message. * (svn_cl__get_log_message): Wrap "no external editor" * errors with helpful hints about the -m and -F options. * * * subversion/libsvn_client/commit.c * (svn_client_commit): Actually capture and propagate "no external * editor" errors. * * * */ static svn_error_t * log_entry_receiver_xml(void *baton, svn_log_entry_t *log_entry, apr_pool_t *pool) { struct log_receiver_baton *lb = baton; /* Collate whole log message into sb before printing. */ svn_stringbuf_t *sb = svn_stringbuf_create("", pool); char *revstr; const char *author; const char *date; const char *message; if (lb->ctx->cancel_func) SVN_ERR(lb->ctx->cancel_func(lb->ctx->cancel_baton)); svn_compat_log_revprops_out(&author, &date, &message, log_entry->revprops); if (author) author = svn_xml_fuzzy_escape(author, pool); if (date) date = svn_xml_fuzzy_escape(date, pool); if (message) message = svn_xml_fuzzy_escape(message, pool); if (log_entry->revision == 0 && message == NULL) return SVN_NO_ERROR; if (! SVN_IS_VALID_REVNUM(log_entry->revision)) { svn_xml_make_close_tag(&sb, pool, "logentry"); SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout)); apr_array_pop(lb->merge_stack); return SVN_NO_ERROR; } revstr = apr_psprintf(pool, "%ld", log_entry->revision); /* */ svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "logentry", "revision", revstr, NULL); /* xxx */ svn_cl__xml_tagged_cdata(&sb, pool, "author", author); /* Print the full, uncut, date. This is machine output. */ /* According to the docs for svn_log_entry_receiver_t, either NULL or the empty string represents no date. Avoid outputting an empty date element. */ if (date && date[0] == '\0') date = NULL; /* xxx */ svn_cl__xml_tagged_cdata(&sb, pool, "date", date); if (log_entry->changed_paths2) { apr_hash_index_t *hi; /* */ svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "paths", NULL); for (hi = apr_hash_first(pool, log_entry->changed_paths2); hi != NULL; hi = apr_hash_next(hi)) { const char *path = svn__apr_hash_index_key(hi); svn_log_changed_path2_t *log_item = svn__apr_hash_index_val(hi); char action[2]; action[0] = log_item->action; action[1] = '\0'; if (log_item->copyfrom_path && SVN_IS_VALID_REVNUM(log_item->copyfrom_rev)) { /* */ revstr = apr_psprintf(pool, "%ld", log_item->copyfrom_rev); svn_xml_make_open_tag(&sb, pool, svn_xml_protect_pcdata, "path", "action", action, "copyfrom-path", log_item->copyfrom_path, "copyfrom-rev", revstr, "kind", svn_cl__node_kind_str_xml( log_item->node_kind), "text-mods", svn_tristate__to_word( log_item->text_modified), "prop-mods", svn_tristate__to_word( log_item->props_modified), NULL); } else { /* */ svn_xml_make_open_tag(&sb, pool, svn_xml_protect_pcdata, "path", "action", action, "kind", svn_cl__node_kind_str_xml(log_item->node_kind), NULL); } /* xxx */ svn_xml_escape_cdata_cstring(&sb, path, pool); svn_xml_make_close_tag(&sb, pool, "path"); } /* */ svn_xml_make_close_tag(&sb, pool, "paths"); } if (message != NULL) { /* xxx */ svn_cl__xml_tagged_cdata(&sb, pool, "msg", message); } svn_compat_log_revprops_clear(log_entry->revprops); if (log_entry->revprops && apr_hash_count(log_entry->revprops) > 0) { svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "revprops", NULL); SVN_ERR(svn_cl__print_xml_prop_hash(&sb, log_entry->revprops, FALSE, /* name_only */ pool)); svn_xml_make_close_tag(&sb, pool, "revprops"); } if (log_entry->has_children) APR_ARRAY_PUSH(lb->merge_stack, svn_revnum_t) = log_entry->revision; else svn_xml_make_close_tag(&sb, pool, "logentry"); return svn_cl__error_checked_fputs(sb->data, stdout); } /* This implements the `svn_opt_subcommand_t' interface. */ svn_error_t * svn_cl__log(apr_getopt_t *os, void *baton, apr_pool_t *pool) { svn_cl__opt_state_t *opt_state = ((svn_cl__cmd_baton_t *) baton)->opt_state; svn_client_ctx_t *ctx = ((svn_cl__cmd_baton_t *) baton)->ctx; apr_array_header_t *targets; struct log_receiver_baton lb; const char *target; int i; svn_opt_revision_t peg_revision; const char *true_path; apr_array_header_t *revprops; if (!opt_state->xml) { if (opt_state->all_revprops) return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, _("'with-all-revprops' option only valid in" " XML mode")); if (opt_state->no_revprops) return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, _("'with-no-revprops' option only valid in" " XML mode")); if (opt_state->revprop_table != NULL) return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, _("'with-revprop' option only valid in" " XML mode")); } else { if (opt_state->show_diff) return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, _("'diff' option is not supported in " "XML mode")); } if (opt_state->quiet && opt_state->show_diff) return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, _("'quiet' and 'diff' options are " "mutually exclusive")); if (opt_state->diff_cmd && (! opt_state->show_diff)) return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, _("'diff-cmd' option requires 'diff' " "option")); if (opt_state->internal_diff && (! opt_state->show_diff)) return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, _("'internal-diff' option requires " "'diff' option")); if (opt_state->extensions && (! opt_state->show_diff)) return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, _("'extensions' option requires 'diff' " "option")); if (opt_state->depth != svn_depth_unknown && (! opt_state->show_diff)) return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, _("'depth' option requires 'diff' option")); SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os, opt_state->targets, ctx, FALSE, pool)); /* Add "." if user passed 0 arguments */ svn_opt_push_implicit_dot_target(targets, pool); target = APR_ARRAY_IDX(targets, 0, const char *); /* Determine if they really want a two-revision range. */ if (opt_state->used_change_arg) { if (opt_state->used_revision_arg && opt_state->revision_ranges->nelts > 1) { return svn_error_create (SVN_ERR_CLIENT_BAD_REVISION, NULL, _("-c and -r are mutually exclusive")); } for (i = 0; i < opt_state->revision_ranges->nelts; i++) { svn_opt_revision_range_t *range; range = APR_ARRAY_IDX(opt_state->revision_ranges, i, svn_opt_revision_range_t *); if (range->start.value.number < range->end.value.number) range->start.value.number++; else range->end.value.number++; } } /* Strip peg revision. */ SVN_ERR(svn_opt_parse_path(&peg_revision, &true_path, target, pool)); APR_ARRAY_IDX(targets, 0, const char *) = true_path; if (svn_path_is_url(target)) { for (i = 1; i < targets->nelts; i++) { target = APR_ARRAY_IDX(targets, i, const char *); if (svn_path_is_url(target) || target[0] == '/') return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, _("Only relative paths can be specified" " after a URL for 'svn log', " "but '%s' is not a relative path"), target); } } lb.ctx = ctx; lb.omit_log_message = opt_state->quiet; SVN_ERR(svn_client_url_from_path2(&lb.target_url, true_path, ctx, pool, pool)); lb.show_diff = opt_state->show_diff; lb.depth = opt_state->depth == svn_depth_unknown ? svn_depth_infinity : opt_state->depth; lb.diff_extensions = opt_state->extensions; lb.merge_stack = apr_array_make(pool, 0, sizeof(svn_revnum_t)); lb.pool = pool; if (opt_state->xml) { /* If output is not incremental, output the XML header and wrap everything in a top-level element. This makes the output in its entirety a well-formed XML document. */ if (! opt_state->incremental) SVN_ERR(svn_cl__xml_print_header("log", pool)); if (opt_state->all_revprops) revprops = NULL; else if(opt_state->no_revprops) { revprops = apr_array_make(pool, 0, sizeof(char *)); } else if (opt_state->revprop_table != NULL) { apr_hash_index_t *hi; revprops = apr_array_make(pool, apr_hash_count(opt_state->revprop_table), sizeof(char *)); for (hi = apr_hash_first(pool, opt_state->revprop_table); hi != NULL; hi = apr_hash_next(hi)) { const char *property = svn__apr_hash_index_key(hi); svn_string_t *value = svn__apr_hash_index_val(hi); if (value && value->data[0] != '\0') return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, _("cannot assign with 'with-revprop'" " option (drop the '=')")); APR_ARRAY_PUSH(revprops, const char *) = property; } } else { revprops = apr_array_make(pool, 3, sizeof(char *)); APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_AUTHOR; APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_DATE; if (!opt_state->quiet) APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_LOG; } SVN_ERR(svn_client_log5(targets, &peg_revision, opt_state->revision_ranges, opt_state->limit, opt_state->verbose, opt_state->stop_on_copy, opt_state->use_merge_history, revprops, log_entry_receiver_xml, &lb, ctx, pool)); if (! opt_state->incremental) SVN_ERR(svn_cl__xml_print_footer("log", pool)); } else /* default output format */ { revprops = apr_array_make(pool, 3, sizeof(char *)); APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_AUTHOR; APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_DATE; if (!opt_state->quiet) APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_LOG; SVN_ERR(svn_client_log5(targets, &peg_revision, opt_state->revision_ranges, opt_state->limit, opt_state->verbose, opt_state->stop_on_copy, opt_state->use_merge_history, revprops, log_entry_receiver, &lb, ctx, pool)); if (! opt_state->incremental) SVN_ERR(svn_cmdline_printf(pool, SEP_STRING)); } return SVN_NO_ERROR; }