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