1/* 2 * blame-cmd.c -- Display blame information 3 * 4 * ==================================================================== 5 * Licensed to the Apache Software Foundation (ASF) under one 6 * or more contributor license agreements. See the NOTICE file 7 * distributed with this work for additional information 8 * regarding copyright ownership. The ASF licenses this file 9 * to you under the Apache License, Version 2.0 (the 10 * "License"); you may not use this file except in compliance 11 * with the License. You may obtain a copy of the License at 12 * 13 * http://www.apache.org/licenses/LICENSE-2.0 14 * 15 * Unless required by applicable law or agreed to in writing, 16 * software distributed under the License is distributed on an 17 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 18 * KIND, either express or implied. See the License for the 19 * specific language governing permissions and limitations 20 * under the License. 21 * ==================================================================== 22 */ 23 24 25/*** Includes. ***/ 26 27#include "svn_client.h" 28#include "svn_error.h" 29#include "svn_dirent_uri.h" 30#include "svn_path.h" 31#include "svn_pools.h" 32#include "svn_props.h" 33#include "svn_cmdline.h" 34#include "svn_sorts.h" 35#include "svn_xml.h" 36#include "svn_time.h" 37#include "cl.h" 38 39#include "svn_private_config.h" 40 41typedef struct blame_baton_t 42{ 43 svn_cl__opt_state_t *opt_state; 44 svn_stream_t *out; 45 svn_stringbuf_t *sbuf; 46 47 svn_revnum_t start_revnum, end_revnum; 48 int rev_maxlength; 49} blame_baton_t; 50 51 52/*** Code. ***/ 53 54/* This implements the svn_client_blame_receiver3_t interface, printing 55 XML to stdout. */ 56static svn_error_t * 57blame_receiver_xml(void *baton, 58 apr_int64_t line_no, 59 svn_revnum_t revision, 60 apr_hash_t *rev_props, 61 svn_revnum_t merged_revision, 62 apr_hash_t *merged_rev_props, 63 const char *merged_path, 64 const svn_string_t *line, 65 svn_boolean_t local_change, 66 apr_pool_t *pool) 67{ 68 blame_baton_t *bb = baton; 69 svn_cl__opt_state_t *opt_state = bb->opt_state; 70 svn_stringbuf_t *sb = bb->sbuf; 71 72 /* "<entry ...>" */ 73 /* line_no is 0-based, but the rest of the world is probably Pascal 74 programmers, so we make them happy and output 1-based line numbers. */ 75 svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "entry", 76 "line-number", 77 apr_psprintf(pool, "%" APR_INT64_T_FMT, 78 line_no + 1), 79 SVN_VA_NULL); 80 81 if (SVN_IS_VALID_REVNUM(revision)) 82 svn_cl__print_xml_commit(&sb, revision, 83 svn_prop_get_value(rev_props, 84 SVN_PROP_REVISION_AUTHOR), 85 svn_prop_get_value(rev_props, 86 SVN_PROP_REVISION_DATE), 87 pool); 88 89 if (opt_state->use_merge_history && SVN_IS_VALID_REVNUM(merged_revision)) 90 { 91 /* "<merged>" */ 92 svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "merged", 93 "path", merged_path, SVN_VA_NULL); 94 95 svn_cl__print_xml_commit(&sb, merged_revision, 96 svn_prop_get_value(merged_rev_props, 97 SVN_PROP_REVISION_AUTHOR), 98 svn_prop_get_value(merged_rev_props, 99 SVN_PROP_REVISION_DATE), 100 pool); 101 102 /* "</merged>" */ 103 svn_xml_make_close_tag(&sb, pool, "merged"); 104 105 } 106 107 /* "</entry>" */ 108 svn_xml_make_close_tag(&sb, pool, "entry"); 109 110 SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout)); 111 svn_stringbuf_setempty(sb); 112 113 return SVN_NO_ERROR; 114} 115 116 117static svn_error_t * 118print_line_info(svn_stream_t *out, 119 svn_revnum_t revision, 120 const char *author, 121 const char *date, 122 const char *path, 123 svn_boolean_t verbose, 124 int rev_maxlength, 125 apr_pool_t *pool) 126{ 127 const char *time_utf8; 128 const char *time_stdout; 129 const char *rev_str; 130 131 rev_str = SVN_IS_VALID_REVNUM(revision) 132 ? apr_psprintf(pool, "%*ld", rev_maxlength, revision) 133 : apr_psprintf(pool, "%*s", rev_maxlength, "-"); 134 135 if (verbose) 136 { 137 if (date) 138 { 139 SVN_ERR(svn_cl__time_cstring_to_human_cstring(&time_utf8, 140 date, pool)); 141 SVN_ERR(svn_cmdline_cstring_from_utf8(&time_stdout, time_utf8, 142 pool)); 143 } 144 else 145 { 146 /* ### This is a 44 characters long string. It assumes the current 147 format of svn_time_to_human_cstring and also 3 letter 148 abbreviations for the month and weekday names. Else, the 149 line contents will be misaligned. */ 150 time_stdout = " -"; 151 } 152 153 SVN_ERR(svn_stream_printf(out, pool, "%s %10s %s ", rev_str, 154 author ? author : " -", 155 time_stdout)); 156 157 if (path) 158 SVN_ERR(svn_stream_printf(out, pool, "%-14s ", path)); 159 } 160 else 161 { 162 return svn_stream_printf(out, pool, "%s %10.10s ", rev_str, 163 author ? author : " -"); 164 } 165 166 return SVN_NO_ERROR; 167} 168 169/* This implements the svn_client_blame_receiver3_t interface. */ 170static svn_error_t * 171blame_receiver(void *baton, 172 apr_int64_t line_no, 173 svn_revnum_t revision, 174 apr_hash_t *rev_props, 175 svn_revnum_t merged_revision, 176 apr_hash_t *merged_rev_props, 177 const char *merged_path, 178 const svn_string_t *line, 179 svn_boolean_t local_change, 180 apr_pool_t *pool) 181{ 182 blame_baton_t *bb = baton; 183 svn_cl__opt_state_t *opt_state = bb->opt_state; 184 svn_stream_t *out = bb->out; 185 svn_boolean_t use_merged = FALSE; 186 187 if (!bb->rev_maxlength) 188 { 189 svn_revnum_t max_revnum = MAX(bb->start_revnum, bb->end_revnum); 190 /* The standard column width for the revision number is 6 characters. 191 If the revision number can potentially be larger (i.e. if the end_revnum 192 is larger than 1000000), we increase the column width as needed. */ 193 194 bb->rev_maxlength = 6; 195 while (max_revnum >= 1000000) 196 { 197 bb->rev_maxlength++; 198 max_revnum = max_revnum / 10; 199 } 200 } 201 202 if (opt_state->use_merge_history) 203 { 204 /* Choose which revision to use. If they aren't equal, prefer the 205 earliest revision. Since we do a forward blame, we want to the first 206 revision which put the line in its current state, so we use the 207 earliest revision. If we ever switch to a backward blame algorithm, 208 we may need to adjust this. */ 209 if (merged_revision < revision) 210 { 211 SVN_ERR(svn_stream_puts(out, "G ")); 212 use_merged = TRUE; 213 } 214 else 215 SVN_ERR(svn_stream_puts(out, " ")); 216 } 217 218 if (use_merged) 219 SVN_ERR(print_line_info(out, merged_revision, 220 svn_prop_get_value(merged_rev_props, 221 SVN_PROP_REVISION_AUTHOR), 222 svn_prop_get_value(merged_rev_props, 223 SVN_PROP_REVISION_DATE), 224 merged_path, opt_state->verbose, 225 bb->rev_maxlength, 226 pool)); 227 else 228 SVN_ERR(print_line_info(out, revision, 229 svn_prop_get_value(rev_props, 230 SVN_PROP_REVISION_AUTHOR), 231 svn_prop_get_value(rev_props, 232 SVN_PROP_REVISION_DATE), 233 NULL, opt_state->verbose, 234 bb->rev_maxlength, 235 pool)); 236 237 return svn_stream_printf(out, pool, "%s%s", line->data, APR_EOL_STR); 238} 239 240 241/* This implements the `svn_opt_subcommand_t' interface. */ 242svn_error_t * 243svn_cl__blame(apr_getopt_t *os, 244 void *baton, 245 apr_pool_t *pool) 246{ 247 svn_cl__opt_state_t *opt_state = ((svn_cl__cmd_baton_t *) baton)->opt_state; 248 svn_client_ctx_t *ctx = ((svn_cl__cmd_baton_t *) baton)->ctx; 249 apr_pool_t *subpool; 250 apr_array_header_t *targets; 251 blame_baton_t bl; 252 int i; 253 svn_boolean_t end_revision_unspecified = FALSE; 254 svn_diff_file_options_t *diff_options = svn_diff_file_options_create(pool); 255 svn_boolean_t seen_nonexistent_target = FALSE; 256 257 SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os, 258 opt_state->targets, 259 ctx, FALSE, pool)); 260 261 /* Blame needs a file on which to operate. */ 262 if (! targets->nelts) 263 return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL); 264 265 if (opt_state->end_revision.kind == svn_opt_revision_unspecified) 266 { 267 if (opt_state->start_revision.kind != svn_opt_revision_unspecified) 268 { 269 /* In the case that -rX was specified, we actually want to set the 270 range to be -r1:X. */ 271 272 opt_state->end_revision = opt_state->start_revision; 273 opt_state->start_revision.kind = svn_opt_revision_number; 274 opt_state->start_revision.value.number = 1; 275 } 276 else 277 end_revision_unspecified = TRUE; 278 } 279 280 if (opt_state->start_revision.kind == svn_opt_revision_unspecified) 281 { 282 opt_state->start_revision.kind = svn_opt_revision_number; 283 opt_state->start_revision.value.number = 1; 284 } 285 286 /* The final conclusion from issue #2431 is that blame info 287 is client output (unlike 'svn cat' which plainly cats the file), 288 so the EOL style should be the platform local one. 289 */ 290 if (! opt_state->xml) 291 SVN_ERR(svn_stream_for_stdout(&bl.out, pool)); 292 else 293 bl.sbuf = svn_stringbuf_create_empty(pool); 294 295 bl.opt_state = opt_state; 296 bl.rev_maxlength = 0; 297 298 subpool = svn_pool_create(pool); 299 300 if (opt_state->extensions) 301 { 302 apr_array_header_t *opts; 303 opts = svn_cstring_split(opt_state->extensions, " \t\n\r", TRUE, pool); 304 SVN_ERR(svn_diff_file_options_parse(diff_options, opts, pool)); 305 } 306 307 if (opt_state->xml) 308 { 309 if (opt_state->verbose) 310 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 311 _("'verbose' option invalid in XML mode")); 312 313 /* If output is not incremental, output the XML header and wrap 314 everything in a top-level element. This makes the output in 315 its entirety a well-formed XML document. */ 316 if (! opt_state->incremental) 317 SVN_ERR(svn_cl__xml_print_header("blame", pool)); 318 } 319 else 320 { 321 if (opt_state->incremental) 322 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, 323 _("'incremental' option only valid in XML " 324 "mode")); 325 } 326 327 for (i = 0; i < targets->nelts; i++) 328 { 329 svn_error_t *err; 330 const char *target = APR_ARRAY_IDX(targets, i, const char *); 331 const char *truepath; 332 svn_opt_revision_t peg_revision; 333 svn_client_blame_receiver4_t receiver; 334 335 svn_pool_clear(subpool); 336 SVN_ERR(svn_cl__check_cancel(ctx->cancel_baton)); 337 338 /* Check for a peg revision. */ 339 SVN_ERR(svn_opt_parse_path(&peg_revision, &truepath, target, 340 subpool)); 341 342 if (end_revision_unspecified) 343 { 344 if (peg_revision.kind != svn_opt_revision_unspecified) 345 opt_state->end_revision = peg_revision; 346 else if (svn_path_is_url(target)) 347 opt_state->end_revision.kind = svn_opt_revision_head; 348 else 349 opt_state->end_revision.kind = svn_opt_revision_working; 350 } 351 352 if (opt_state->xml) 353 { 354 /* "<target ...>" */ 355 /* We don't output this tag immediately, which avoids creating 356 a target element if this path is skipped. */ 357 const char *outpath = truepath; 358 if (! svn_path_is_url(target)) 359 outpath = svn_dirent_local_style(truepath, subpool); 360 svn_xml_make_open_tag(&bl.sbuf, pool, svn_xml_normal, "target", 361 "path", outpath, SVN_VA_NULL); 362 363 receiver = blame_receiver_xml; 364 } 365 else 366 receiver = blame_receiver; 367 368 err = svn_client_blame6(&bl.start_revnum, &bl.end_revnum, 369 truepath, 370 &peg_revision, 371 &opt_state->start_revision, 372 &opt_state->end_revision, 373 diff_options, 374 opt_state->force, 375 opt_state->use_merge_history, 376 receiver, 377 &bl, 378 ctx, 379 subpool); 380 381 if (err) 382 { 383 if (err->apr_err == SVN_ERR_CLIENT_IS_BINARY_FILE) 384 { 385 svn_error_clear(err); 386 SVN_ERR(svn_cmdline_fprintf(stderr, subpool, 387 _("Skipping binary file " 388 "(use --force to treat as text): " 389 "'%s'\n"), 390 target)); 391 } 392 else if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND || 393 err->apr_err == SVN_ERR_ENTRY_NOT_FOUND || 394 err->apr_err == SVN_ERR_FS_NOT_FILE || 395 err->apr_err == SVN_ERR_FS_NOT_FOUND) 396 { 397 svn_handle_warning2(stderr, err, "svn: "); 398 svn_error_clear(err); 399 err = NULL; 400 seen_nonexistent_target = TRUE; 401 } 402 else 403 { 404 return svn_error_trace(err); 405 } 406 } 407 else if (opt_state->xml) 408 { 409 /* "</target>" */ 410 svn_xml_make_close_tag(&(bl.sbuf), pool, "target"); 411 SVN_ERR(svn_cl__error_checked_fputs(bl.sbuf->data, stdout)); 412 } 413 414 if (opt_state->xml) 415 svn_stringbuf_setempty(bl.sbuf); 416 } 417 svn_pool_destroy(subpool); 418 if (opt_state->xml && ! opt_state->incremental) 419 SVN_ERR(svn_cl__xml_print_footer("blame", pool)); 420 421 if (seen_nonexistent_target) 422 return svn_error_create( 423 SVN_ERR_ILLEGAL_TARGET, NULL, 424 _("Could not perform blame on all targets because some " 425 "targets don't exist")); 426 else 427 return SVN_NO_ERROR; 428} 429