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