1251881Speter/* 2251881Speter * blame-cmd.c -- Display blame information 3251881Speter * 4251881Speter * ==================================================================== 5251881Speter * Licensed to the Apache Software Foundation (ASF) under one 6251881Speter * or more contributor license agreements. See the NOTICE file 7251881Speter * distributed with this work for additional information 8251881Speter * regarding copyright ownership. The ASF licenses this file 9251881Speter * to you under the Apache License, Version 2.0 (the 10251881Speter * "License"); you may not use this file except in compliance 11251881Speter * with the License. You may obtain a copy of the License at 12251881Speter * 13251881Speter * http://www.apache.org/licenses/LICENSE-2.0 14251881Speter * 15251881Speter * Unless required by applicable law or agreed to in writing, 16251881Speter * software distributed under the License is distributed on an 17251881Speter * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18251881Speter * KIND, either express or implied. See the License for the 19251881Speter * specific language governing permissions and limitations 20251881Speter * under the License. 21251881Speter * ==================================================================== 22251881Speter */ 23251881Speter 24251881Speter 25251881Speter/*** Includes. ***/ 26251881Speter 27251881Speter#include "svn_client.h" 28251881Speter#include "svn_error.h" 29251881Speter#include "svn_dirent_uri.h" 30251881Speter#include "svn_path.h" 31251881Speter#include "svn_pools.h" 32251881Speter#include "svn_props.h" 33251881Speter#include "svn_cmdline.h" 34251881Speter#include "svn_xml.h" 35251881Speter#include "svn_time.h" 36251881Speter#include "cl.h" 37251881Speter 38251881Speter#include "svn_private_config.h" 39251881Speter 40251881Spetertypedef struct blame_baton_t 41251881Speter{ 42251881Speter svn_cl__opt_state_t *opt_state; 43251881Speter svn_stream_t *out; 44251881Speter svn_stringbuf_t *sbuf; 45251881Speter} blame_baton_t; 46251881Speter 47251881Speter 48251881Speter/*** Code. ***/ 49251881Speter 50251881Speter/* This implements the svn_client_blame_receiver3_t interface, printing 51251881Speter XML to stdout. */ 52251881Speterstatic svn_error_t * 53251881Speterblame_receiver_xml(void *baton, 54251881Speter svn_revnum_t start_revnum, 55251881Speter svn_revnum_t end_revnum, 56251881Speter apr_int64_t line_no, 57251881Speter svn_revnum_t revision, 58251881Speter apr_hash_t *rev_props, 59251881Speter svn_revnum_t merged_revision, 60251881Speter apr_hash_t *merged_rev_props, 61251881Speter const char *merged_path, 62251881Speter const char *line, 63251881Speter svn_boolean_t local_change, 64251881Speter apr_pool_t *pool) 65251881Speter{ 66251881Speter svn_cl__opt_state_t *opt_state = 67251881Speter ((blame_baton_t *) baton)->opt_state; 68251881Speter svn_stringbuf_t *sb = ((blame_baton_t *) baton)->sbuf; 69251881Speter 70251881Speter /* "<entry ...>" */ 71251881Speter /* line_no is 0-based, but the rest of the world is probably Pascal 72251881Speter programmers, so we make them happy and output 1-based line numbers. */ 73251881Speter svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "entry", 74251881Speter "line-number", 75251881Speter apr_psprintf(pool, "%" APR_INT64_T_FMT, 76251881Speter line_no + 1), 77251881Speter NULL); 78251881Speter 79251881Speter if (SVN_IS_VALID_REVNUM(revision)) 80251881Speter svn_cl__print_xml_commit(&sb, revision, 81251881Speter svn_prop_get_value(rev_props, 82251881Speter SVN_PROP_REVISION_AUTHOR), 83251881Speter svn_prop_get_value(rev_props, 84251881Speter SVN_PROP_REVISION_DATE), 85251881Speter pool); 86251881Speter 87251881Speter if (opt_state->use_merge_history && SVN_IS_VALID_REVNUM(merged_revision)) 88251881Speter { 89251881Speter /* "<merged>" */ 90251881Speter svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "merged", 91251881Speter "path", merged_path, NULL); 92251881Speter 93251881Speter svn_cl__print_xml_commit(&sb, merged_revision, 94251881Speter svn_prop_get_value(merged_rev_props, 95251881Speter SVN_PROP_REVISION_AUTHOR), 96251881Speter svn_prop_get_value(merged_rev_props, 97251881Speter SVN_PROP_REVISION_DATE), 98251881Speter pool); 99251881Speter 100251881Speter /* "</merged>" */ 101251881Speter svn_xml_make_close_tag(&sb, pool, "merged"); 102251881Speter 103251881Speter } 104251881Speter 105251881Speter /* "</entry>" */ 106251881Speter svn_xml_make_close_tag(&sb, pool, "entry"); 107251881Speter 108251881Speter SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout)); 109251881Speter svn_stringbuf_setempty(sb); 110251881Speter 111251881Speter return SVN_NO_ERROR; 112251881Speter} 113251881Speter 114251881Speter 115251881Speterstatic svn_error_t * 116251881Speterprint_line_info(svn_stream_t *out, 117251881Speter svn_revnum_t revision, 118251881Speter const char *author, 119251881Speter const char *date, 120251881Speter const char *path, 121251881Speter svn_boolean_t verbose, 122251881Speter svn_revnum_t end_revnum, 123251881Speter apr_pool_t *pool) 124251881Speter{ 125251881Speter const char *time_utf8; 126251881Speter const char *time_stdout; 127251881Speter const char *rev_str; 128251881Speter int rev_maxlength; 129251881Speter 130251881Speter /* The standard column width for the revision number is 6 characters. 131251881Speter If the revision number can potentially be larger (i.e. if the end_revnum 132251881Speter is larger than 1000000), we increase the column width as needed. */ 133251881Speter rev_maxlength = 6; 134251881Speter while (end_revnum >= 1000000) 135251881Speter { 136251881Speter rev_maxlength++; 137251881Speter end_revnum = end_revnum / 10; 138251881Speter } 139251881Speter rev_str = SVN_IS_VALID_REVNUM(revision) 140251881Speter ? apr_psprintf(pool, "%*ld", rev_maxlength, revision) 141251881Speter : apr_psprintf(pool, "%*s", rev_maxlength, "-"); 142251881Speter 143251881Speter if (verbose) 144251881Speter { 145251881Speter if (date) 146251881Speter { 147251881Speter SVN_ERR(svn_cl__time_cstring_to_human_cstring(&time_utf8, 148251881Speter date, pool)); 149251881Speter SVN_ERR(svn_cmdline_cstring_from_utf8(&time_stdout, time_utf8, 150251881Speter pool)); 151251881Speter } 152251881Speter else 153251881Speter { 154251881Speter /* ### This is a 44 characters long string. It assumes the current 155251881Speter format of svn_time_to_human_cstring and also 3 letter 156251881Speter abbreviations for the month and weekday names. Else, the 157251881Speter line contents will be misaligned. */ 158251881Speter time_stdout = " -"; 159251881Speter } 160251881Speter 161251881Speter SVN_ERR(svn_stream_printf(out, pool, "%s %10s %s ", rev_str, 162251881Speter author ? author : " -", 163251881Speter time_stdout)); 164251881Speter 165251881Speter if (path) 166251881Speter SVN_ERR(svn_stream_printf(out, pool, "%-14s ", path)); 167251881Speter } 168251881Speter else 169251881Speter { 170251881Speter return svn_stream_printf(out, pool, "%s %10.10s ", rev_str, 171251881Speter author ? author : " -"); 172251881Speter } 173251881Speter 174251881Speter return SVN_NO_ERROR; 175251881Speter} 176251881Speter 177251881Speter/* This implements the svn_client_blame_receiver3_t interface. */ 178251881Speterstatic svn_error_t * 179251881Speterblame_receiver(void *baton, 180251881Speter svn_revnum_t start_revnum, 181251881Speter svn_revnum_t end_revnum, 182251881Speter apr_int64_t line_no, 183251881Speter svn_revnum_t revision, 184251881Speter apr_hash_t *rev_props, 185251881Speter svn_revnum_t merged_revision, 186251881Speter apr_hash_t *merged_rev_props, 187251881Speter const char *merged_path, 188251881Speter const char *line, 189251881Speter svn_boolean_t local_change, 190251881Speter apr_pool_t *pool) 191251881Speter{ 192251881Speter svn_cl__opt_state_t *opt_state = 193251881Speter ((blame_baton_t *) baton)->opt_state; 194251881Speter svn_stream_t *out = ((blame_baton_t *)baton)->out; 195251881Speter svn_boolean_t use_merged = FALSE; 196251881Speter 197251881Speter if (opt_state->use_merge_history) 198251881Speter { 199251881Speter /* Choose which revision to use. If they aren't equal, prefer the 200251881Speter earliest revision. Since we do a forward blame, we want to the first 201251881Speter revision which put the line in its current state, so we use the 202251881Speter earliest revision. If we ever switch to a backward blame algorithm, 203251881Speter we may need to adjust this. */ 204251881Speter if (merged_revision < revision) 205251881Speter { 206251881Speter SVN_ERR(svn_stream_puts(out, "G ")); 207251881Speter use_merged = TRUE; 208251881Speter } 209251881Speter else 210251881Speter SVN_ERR(svn_stream_puts(out, " ")); 211251881Speter } 212251881Speter 213251881Speter if (use_merged) 214251881Speter SVN_ERR(print_line_info(out, merged_revision, 215251881Speter svn_prop_get_value(merged_rev_props, 216251881Speter SVN_PROP_REVISION_AUTHOR), 217251881Speter svn_prop_get_value(merged_rev_props, 218251881Speter SVN_PROP_REVISION_DATE), 219251881Speter merged_path, opt_state->verbose, end_revnum, 220251881Speter pool)); 221251881Speter else 222251881Speter SVN_ERR(print_line_info(out, revision, 223251881Speter svn_prop_get_value(rev_props, 224251881Speter SVN_PROP_REVISION_AUTHOR), 225251881Speter svn_prop_get_value(rev_props, 226251881Speter SVN_PROP_REVISION_DATE), 227251881Speter NULL, opt_state->verbose, end_revnum, 228251881Speter pool)); 229251881Speter 230251881Speter return svn_stream_printf(out, pool, "%s%s", line, APR_EOL_STR); 231251881Speter} 232251881Speter 233251881Speter 234251881Speter/* This implements the `svn_opt_subcommand_t' interface. */ 235251881Spetersvn_error_t * 236251881Spetersvn_cl__blame(apr_getopt_t *os, 237251881Speter void *baton, 238251881Speter apr_pool_t *pool) 239251881Speter{ 240251881Speter svn_cl__opt_state_t *opt_state = ((svn_cl__cmd_baton_t *) baton)->opt_state; 241251881Speter svn_client_ctx_t *ctx = ((svn_cl__cmd_baton_t *) baton)->ctx; 242251881Speter apr_pool_t *subpool; 243251881Speter apr_array_header_t *targets; 244251881Speter blame_baton_t bl; 245251881Speter int i; 246251881Speter svn_boolean_t end_revision_unspecified = FALSE; 247251881Speter svn_diff_file_options_t *diff_options = svn_diff_file_options_create(pool); 248251881Speter svn_boolean_t seen_nonexistent_target = FALSE; 249251881Speter 250251881Speter SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os, 251251881Speter opt_state->targets, 252251881Speter ctx, FALSE, pool)); 253251881Speter 254251881Speter /* Blame needs a file on which to operate. */ 255251881Speter if (! targets->nelts) 256251881Speter return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL); 257251881Speter 258251881Speter if (opt_state->end_revision.kind == svn_opt_revision_unspecified) 259251881Speter { 260251881Speter if (opt_state->start_revision.kind != svn_opt_revision_unspecified) 261251881Speter { 262251881Speter /* In the case that -rX was specified, we actually want to set the 263251881Speter range to be -r1:X. */ 264251881Speter 265251881Speter opt_state->end_revision = opt_state->start_revision; 266251881Speter opt_state->start_revision.kind = svn_opt_revision_number; 267251881Speter opt_state->start_revision.value.number = 1; 268251881Speter } 269251881Speter else 270251881Speter end_revision_unspecified = TRUE; 271251881Speter } 272251881Speter 273251881Speter if (opt_state->start_revision.kind == svn_opt_revision_unspecified) 274251881Speter { 275251881Speter opt_state->start_revision.kind = svn_opt_revision_number; 276251881Speter opt_state->start_revision.value.number = 1; 277251881Speter } 278251881Speter 279251881Speter /* The final conclusion from issue #2431 is that blame info 280251881Speter is client output (unlike 'svn cat' which plainly cats the file), 281251881Speter so the EOL style should be the platform local one. 282251881Speter */ 283251881Speter if (! opt_state->xml) 284251881Speter SVN_ERR(svn_stream_for_stdout(&bl.out, pool)); 285251881Speter else 286251881Speter bl.sbuf = svn_stringbuf_create_empty(pool); 287251881Speter 288251881Speter bl.opt_state = opt_state; 289251881Speter 290251881Speter subpool = svn_pool_create(pool); 291251881Speter 292251881Speter if (opt_state->extensions) 293251881Speter { 294251881Speter apr_array_header_t *opts; 295251881Speter opts = svn_cstring_split(opt_state->extensions, " \t\n\r", TRUE, pool); 296251881Speter SVN_ERR(svn_diff_file_options_parse(diff_options, opts, pool)); 297251881Speter } 298251881Speter 299251881Speter if (opt_state->xml) 300251881Speter { 301251881Speter if (opt_state->verbose) 302251881Speter return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 303251881Speter _("'verbose' option invalid in XML mode")); 304251881Speter 305251881Speter /* If output is not incremental, output the XML header and wrap 306251881Speter everything in a top-level element. This makes the output in 307251881Speter its entirety a well-formed XML document. */ 308251881Speter if (! opt_state->incremental) 309251881Speter SVN_ERR(svn_cl__xml_print_header("blame", pool)); 310251881Speter } 311251881Speter else 312251881Speter { 313251881Speter if (opt_state->incremental) 314251881Speter return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 315251881Speter _("'incremental' option only valid in XML " 316251881Speter "mode")); 317251881Speter } 318251881Speter 319251881Speter for (i = 0; i < targets->nelts; i++) 320251881Speter { 321251881Speter svn_error_t *err; 322251881Speter const char *target = APR_ARRAY_IDX(targets, i, const char *); 323251881Speter const char *truepath; 324251881Speter svn_opt_revision_t peg_revision; 325251881Speter svn_client_blame_receiver3_t receiver; 326251881Speter 327251881Speter svn_pool_clear(subpool); 328251881Speter SVN_ERR(svn_cl__check_cancel(ctx->cancel_baton)); 329251881Speter 330251881Speter /* Check for a peg revision. */ 331251881Speter SVN_ERR(svn_opt_parse_path(&peg_revision, &truepath, target, 332251881Speter subpool)); 333251881Speter 334251881Speter if (end_revision_unspecified) 335251881Speter { 336251881Speter if (peg_revision.kind != svn_opt_revision_unspecified) 337251881Speter opt_state->end_revision = peg_revision; 338251881Speter else if (svn_path_is_url(target)) 339251881Speter opt_state->end_revision.kind = svn_opt_revision_head; 340251881Speter else 341251881Speter opt_state->end_revision.kind = svn_opt_revision_working; 342251881Speter } 343251881Speter 344251881Speter if (opt_state->xml) 345251881Speter { 346251881Speter /* "<target ...>" */ 347251881Speter /* We don't output this tag immediately, which avoids creating 348251881Speter a target element if this path is skipped. */ 349251881Speter const char *outpath = truepath; 350251881Speter if (! svn_path_is_url(target)) 351251881Speter outpath = svn_dirent_local_style(truepath, subpool); 352251881Speter svn_xml_make_open_tag(&bl.sbuf, pool, svn_xml_normal, "target", 353251881Speter "path", outpath, NULL); 354251881Speter 355251881Speter receiver = blame_receiver_xml; 356251881Speter } 357251881Speter else 358251881Speter receiver = blame_receiver; 359251881Speter 360251881Speter err = svn_client_blame5(truepath, 361251881Speter &peg_revision, 362251881Speter &opt_state->start_revision, 363251881Speter &opt_state->end_revision, 364251881Speter diff_options, 365251881Speter opt_state->force, 366251881Speter opt_state->use_merge_history, 367251881Speter receiver, 368251881Speter &bl, 369251881Speter ctx, 370251881Speter subpool); 371251881Speter 372251881Speter if (err) 373251881Speter { 374251881Speter if (err->apr_err == SVN_ERR_CLIENT_IS_BINARY_FILE) 375251881Speter { 376251881Speter svn_error_clear(err); 377251881Speter SVN_ERR(svn_cmdline_fprintf(stderr, subpool, 378251881Speter _("Skipping binary file " 379251881Speter "(use --force to treat as text): " 380251881Speter "'%s'\n"), 381251881Speter target)); 382251881Speter } 383251881Speter else if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND || 384251881Speter err->apr_err == SVN_ERR_ENTRY_NOT_FOUND || 385251881Speter err->apr_err == SVN_ERR_FS_NOT_FILE || 386251881Speter err->apr_err == SVN_ERR_FS_NOT_FOUND) 387251881Speter { 388251881Speter svn_handle_warning2(stderr, err, "svn: "); 389251881Speter svn_error_clear(err); 390251881Speter err = NULL; 391251881Speter seen_nonexistent_target = TRUE; 392251881Speter } 393251881Speter else 394251881Speter { 395251881Speter return svn_error_trace(err); 396251881Speter } 397251881Speter } 398251881Speter else if (opt_state->xml) 399251881Speter { 400251881Speter /* "</target>" */ 401251881Speter svn_xml_make_close_tag(&(bl.sbuf), pool, "target"); 402251881Speter SVN_ERR(svn_cl__error_checked_fputs(bl.sbuf->data, stdout)); 403251881Speter } 404251881Speter 405251881Speter if (opt_state->xml) 406251881Speter svn_stringbuf_setempty(bl.sbuf); 407251881Speter } 408251881Speter svn_pool_destroy(subpool); 409251881Speter if (opt_state->xml && ! opt_state->incremental) 410251881Speter SVN_ERR(svn_cl__xml_print_footer("blame", pool)); 411251881Speter 412251881Speter if (seen_nonexistent_target) 413251881Speter return svn_error_create( 414251881Speter SVN_ERR_ILLEGAL_TARGET, NULL, 415251881Speter _("Could not perform blame on all targets because some " 416251881Speter "targets don't exist")); 417251881Speter else 418251881Speter return SVN_NO_ERROR; 419251881Speter} 420