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
41 typedef 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. */
56 static svn_error_t *
blame_receiver_xml(void * baton,apr_int64_t line_no,svn_revnum_t revision,apr_hash_t * rev_props,svn_revnum_t merged_revision,apr_hash_t * merged_rev_props,const char * merged_path,const svn_string_t * line,svn_boolean_t local_change,apr_pool_t * pool)57 blame_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
117 static svn_error_t *
print_line_info(svn_stream_t * out,svn_revnum_t revision,const char * author,const char * date,const char * path,svn_boolean_t verbose,int rev_maxlength,apr_pool_t * pool)118 print_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. */
170 static svn_error_t *
blame_receiver(void * baton,apr_int64_t line_no,svn_revnum_t revision,apr_hash_t * rev_props,svn_revnum_t merged_revision,apr_hash_t * merged_rev_props,const char * merged_path,const svn_string_t * line,svn_boolean_t local_change,apr_pool_t * pool)171 blame_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. */
242 svn_error_t *
svn_cl__blame(apr_getopt_t * os,void * baton,apr_pool_t * pool)243 svn_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