2 * blame-cmd.c -- Display blame information
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
13 * http://www.apache.org/licenses/LICENSE-2.0
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
21 * ====================================================================
27 #include "svn_client.h"
28 #include "svn_error.h"
29 #include "svn_dirent_uri.h"
31 #include "svn_pools.h"
32 #include "svn_props.h"
33 #include "svn_cmdline.h"
34 #include "svn_sorts.h"
39 #include "svn_private_config.h"
41 typedef struct blame_baton_t
43 svn_cl__opt_state_t *opt_state;
45 svn_stringbuf_t *sbuf;
47 svn_revnum_t start_revnum, end_revnum;
54 /* This implements the svn_client_blame_receiver3_t interface, printing
57 blame_receiver_xml(void *baton,
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,
68 blame_baton_t *bb = baton;
69 svn_cl__opt_state_t *opt_state = bb->opt_state;
70 svn_stringbuf_t *sb = bb->sbuf;
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",
77 apr_psprintf(pool, "%" APR_INT64_T_FMT,
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),
89 if (opt_state->use_merge_history && SVN_IS_VALID_REVNUM(merged_revision))
92 svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "merged",
93 "path", merged_path, SVN_VA_NULL);
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),
103 svn_xml_make_close_tag(&sb, pool, "merged");
108 svn_xml_make_close_tag(&sb, pool, "entry");
110 SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout));
111 svn_stringbuf_setempty(sb);
118 print_line_info(svn_stream_t *out,
119 svn_revnum_t revision,
123 svn_boolean_t verbose,
127 const char *time_utf8;
128 const char *time_stdout;
131 rev_str = SVN_IS_VALID_REVNUM(revision)
132 ? apr_psprintf(pool, "%*ld", rev_maxlength, revision)
133 : apr_psprintf(pool, "%*s", rev_maxlength, "-");
139 SVN_ERR(svn_cl__time_cstring_to_human_cstring(&time_utf8,
141 SVN_ERR(svn_cmdline_cstring_from_utf8(&time_stdout, time_utf8,
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. */
153 SVN_ERR(svn_stream_printf(out, pool, "%s %10s %s ", rev_str,
154 author ? author : " -",
158 SVN_ERR(svn_stream_printf(out, pool, "%-14s ", path));
162 return svn_stream_printf(out, pool, "%s %10.10s ", rev_str,
163 author ? author : " -");
169 /* This implements the svn_client_blame_receiver3_t interface. */
171 blame_receiver(void *baton,
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,
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;
187 if (!bb->rev_maxlength)
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. */
194 bb->rev_maxlength = 6;
195 while (max_revnum >= 1000000)
198 max_revnum = max_revnum / 10;
202 if (opt_state->use_merge_history)
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)
211 SVN_ERR(svn_stream_puts(out, "G "));
215 SVN_ERR(svn_stream_puts(out, " "));
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,
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,
237 return svn_stream_printf(out, pool, "%s%s", line->data, APR_EOL_STR);
241 /* This implements the `svn_opt_subcommand_t' interface. */
243 svn_cl__blame(apr_getopt_t *os,
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;
250 apr_array_header_t *targets;
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;
257 SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os,
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);
265 if (opt_state->end_revision.kind == svn_opt_revision_unspecified)
267 if (opt_state->start_revision.kind != svn_opt_revision_unspecified)
269 /* In the case that -rX was specified, we actually want to set the
270 range to be -r1:X. */
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;
277 end_revision_unspecified = TRUE;
280 if (opt_state->start_revision.kind == svn_opt_revision_unspecified)
282 opt_state->start_revision.kind = svn_opt_revision_number;
283 opt_state->start_revision.value.number = 1;
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.
290 if (! opt_state->xml)
291 SVN_ERR(svn_stream_for_stdout(&bl.out, pool));
293 bl.sbuf = svn_stringbuf_create_empty(pool);
295 bl.opt_state = opt_state;
296 bl.rev_maxlength = 0;
298 subpool = svn_pool_create(pool);
300 if (opt_state->extensions)
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));
309 if (opt_state->verbose)
310 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
311 _("'verbose' option invalid in XML mode"));
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));
321 if (opt_state->incremental)
322 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
323 _("'incremental' option only valid in XML "
327 for (i = 0; i < targets->nelts; i++)
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;
335 svn_pool_clear(subpool);
336 SVN_ERR(svn_cl__check_cancel(ctx->cancel_baton));
338 /* Check for a peg revision. */
339 SVN_ERR(svn_opt_parse_path(&peg_revision, &truepath, target,
342 if (end_revision_unspecified)
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;
349 opt_state->end_revision.kind = svn_opt_revision_working;
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);
363 receiver = blame_receiver_xml;
366 receiver = blame_receiver;
368 err = svn_client_blame6(&bl.start_revnum, &bl.end_revnum,
371 &opt_state->start_revision,
372 &opt_state->end_revision,
375 opt_state->use_merge_history,
383 if (err->apr_err == SVN_ERR_CLIENT_IS_BINARY_FILE)
385 svn_error_clear(err);
386 SVN_ERR(svn_cmdline_fprintf(stderr, subpool,
387 _("Skipping binary file "
388 "(use --force to treat as text): "
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)
397 svn_handle_warning2(stderr, err, "svn: ");
398 svn_error_clear(err);
400 seen_nonexistent_target = TRUE;
404 return svn_error_trace(err);
407 else if (opt_state->xml)
410 svn_xml_make_close_tag(&(bl.sbuf), pool, "target");
411 SVN_ERR(svn_cl__error_checked_fputs(bl.sbuf->data, stdout));
415 svn_stringbuf_setempty(bl.sbuf);
417 svn_pool_destroy(subpool);
418 if (opt_state->xml && ! opt_state->incremental)
419 SVN_ERR(svn_cl__xml_print_footer("blame", pool));
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"));