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