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;
53 /* This implements the svn_client_blame_receiver3_t interface, printing
56 blame_receiver_xml(void *baton,
57 svn_revnum_t start_revnum,
58 svn_revnum_t end_revnum,
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,
66 svn_boolean_t local_change,
69 blame_baton_t *bb = baton;
70 svn_cl__opt_state_t *opt_state = bb->opt_state;
71 svn_stringbuf_t *sb = bb->sbuf;
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",
78 apr_psprintf(pool, "%" APR_INT64_T_FMT,
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),
90 if (opt_state->use_merge_history && SVN_IS_VALID_REVNUM(merged_revision))
93 svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "merged",
94 "path", merged_path, SVN_VA_NULL);
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),
104 svn_xml_make_close_tag(&sb, pool, "merged");
109 svn_xml_make_close_tag(&sb, pool, "entry");
111 SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout));
112 svn_stringbuf_setempty(sb);
119 print_line_info(svn_stream_t *out,
120 svn_revnum_t revision,
124 svn_boolean_t verbose,
128 const char *time_utf8;
129 const char *time_stdout;
132 rev_str = SVN_IS_VALID_REVNUM(revision)
133 ? apr_psprintf(pool, "%*ld", rev_maxlength, revision)
134 : apr_psprintf(pool, "%*s", rev_maxlength, "-");
140 SVN_ERR(svn_cl__time_cstring_to_human_cstring(&time_utf8,
142 SVN_ERR(svn_cmdline_cstring_from_utf8(&time_stdout, time_utf8,
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. */
154 SVN_ERR(svn_stream_printf(out, pool, "%s %10s %s ", rev_str,
155 author ? author : " -",
159 SVN_ERR(svn_stream_printf(out, pool, "%-14s ", path));
163 return svn_stream_printf(out, pool, "%s %10.10s ", rev_str,
164 author ? author : " -");
170 /* This implements the svn_client_blame_receiver3_t interface. */
172 blame_receiver(void *baton,
173 svn_revnum_t start_revnum,
174 svn_revnum_t end_revnum,
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,
182 svn_boolean_t local_change,
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;
190 if (!bb->rev_maxlength)
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. */
197 bb->rev_maxlength = 6;
198 while (max_revnum >= 1000000)
201 max_revnum = max_revnum / 10;
205 if (opt_state->use_merge_history)
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)
214 SVN_ERR(svn_stream_puts(out, "G "));
218 SVN_ERR(svn_stream_puts(out, " "));
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,
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,
240 return svn_stream_printf(out, pool, "%s%s", line, APR_EOL_STR);
244 /* This implements the `svn_opt_subcommand_t' interface. */
246 svn_cl__blame(apr_getopt_t *os,
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;
253 apr_array_header_t *targets;
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;
260 SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os,
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);
268 if (opt_state->end_revision.kind == svn_opt_revision_unspecified)
270 if (opt_state->start_revision.kind != svn_opt_revision_unspecified)
272 /* In the case that -rX was specified, we actually want to set the
273 range to be -r1:X. */
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;
280 end_revision_unspecified = TRUE;
283 if (opt_state->start_revision.kind == svn_opt_revision_unspecified)
285 opt_state->start_revision.kind = svn_opt_revision_number;
286 opt_state->start_revision.value.number = 1;
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.
293 if (! opt_state->xml)
294 SVN_ERR(svn_stream_for_stdout(&bl.out, pool));
296 bl.sbuf = svn_stringbuf_create_empty(pool);
298 bl.opt_state = opt_state;
299 bl.rev_maxlength = 0;
301 subpool = svn_pool_create(pool);
303 if (opt_state->extensions)
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));
312 if (opt_state->verbose)
313 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
314 _("'verbose' option invalid in XML mode"));
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));
324 if (opt_state->incremental)
325 return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
326 _("'incremental' option only valid in XML "
330 for (i = 0; i < targets->nelts; i++)
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;
338 svn_pool_clear(subpool);
339 SVN_ERR(svn_cl__check_cancel(ctx->cancel_baton));
341 /* Check for a peg revision. */
342 SVN_ERR(svn_opt_parse_path(&peg_revision, &truepath, target,
345 if (end_revision_unspecified)
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;
352 opt_state->end_revision.kind = svn_opt_revision_working;
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);
366 receiver = blame_receiver_xml;
369 receiver = blame_receiver;
371 err = svn_client_blame5(truepath,
373 &opt_state->start_revision,
374 &opt_state->end_revision,
377 opt_state->use_merge_history,
385 if (err->apr_err == SVN_ERR_CLIENT_IS_BINARY_FILE)
387 svn_error_clear(err);
388 SVN_ERR(svn_cmdline_fprintf(stderr, subpool,
389 _("Skipping binary file "
390 "(use --force to treat as text): "
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)
399 svn_handle_warning2(stderr, err, "svn: ");
400 svn_error_clear(err);
402 seen_nonexistent_target = TRUE;
406 return svn_error_trace(err);
409 else if (opt_state->xml)
412 svn_xml_make_close_tag(&(bl.sbuf), pool, "target");
413 SVN_ERR(svn_cl__error_checked_fputs(bl.sbuf->data, stdout));
417 svn_stringbuf_setempty(bl.sbuf);
419 svn_pool_destroy(subpool);
420 if (opt_state->xml && ! opt_state->incremental)
421 SVN_ERR(svn_cl__xml_print_footer("blame", pool));
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"));