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
41typedef 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  int rev_maxlength;
48} blame_baton_t;
49
50
51/*** Code. ***/
52
53/* This implements the svn_client_blame_receiver3_t interface, printing
54   XML to stdout. */
55static svn_error_t *
56blame_receiver_xml(void *baton,
57                   svn_revnum_t start_revnum,
58                   svn_revnum_t end_revnum,
59                   apr_int64_t line_no,
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,
65                   const char *line,
66                   svn_boolean_t local_change,
67                   apr_pool_t *pool)
68{
69  blame_baton_t *bb = baton;
70  svn_cl__opt_state_t *opt_state = bb->opt_state;
71  svn_stringbuf_t *sb = bb->sbuf;
72
73  /* "<entry ...>" */
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",
77                        "line-number",
78                        apr_psprintf(pool, "%" APR_INT64_T_FMT,
79                                     line_no + 1),
80                        SVN_VA_NULL);
81
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),
88                             pool);
89
90  if (opt_state->use_merge_history && SVN_IS_VALID_REVNUM(merged_revision))
91    {
92      /* "<merged>" */
93      svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "merged",
94                            "path", merged_path, SVN_VA_NULL);
95
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),
101                             pool);
102
103      /* "</merged>" */
104      svn_xml_make_close_tag(&sb, pool, "merged");
105
106    }
107
108  /* "</entry>" */
109  svn_xml_make_close_tag(&sb, pool, "entry");
110
111  SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout));
112  svn_stringbuf_setempty(sb);
113
114  return SVN_NO_ERROR;
115}
116
117
118static svn_error_t *
119print_line_info(svn_stream_t *out,
120                svn_revnum_t revision,
121                const char *author,
122                const char *date,
123                const char *path,
124                svn_boolean_t verbose,
125                int rev_maxlength,
126                apr_pool_t *pool)
127{
128  const char *time_utf8;
129  const char *time_stdout;
130  const char *rev_str;
131
132  rev_str = SVN_IS_VALID_REVNUM(revision)
133    ? apr_psprintf(pool, "%*ld", rev_maxlength, revision)
134    : apr_psprintf(pool, "%*s", rev_maxlength, "-");
135
136  if (verbose)
137    {
138      if (date)
139        {
140          SVN_ERR(svn_cl__time_cstring_to_human_cstring(&time_utf8,
141                                                        date, pool));
142          SVN_ERR(svn_cmdline_cstring_from_utf8(&time_stdout, time_utf8,
143                                                pool));
144        }
145      else
146        {
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. */
151          time_stdout = "                                           -";
152        }
153
154      SVN_ERR(svn_stream_printf(out, pool, "%s %10s %s ", rev_str,
155                                author ? author : "         -",
156                                time_stdout));
157
158      if (path)
159        SVN_ERR(svn_stream_printf(out, pool, "%-14s ", path));
160    }
161  else
162    {
163      return svn_stream_printf(out, pool, "%s %10.10s ", rev_str,
164                               author ? author : "         -");
165    }
166
167  return SVN_NO_ERROR;
168}
169
170/* This implements the svn_client_blame_receiver3_t interface. */
171static svn_error_t *
172blame_receiver(void *baton,
173               svn_revnum_t start_revnum,
174               svn_revnum_t end_revnum,
175               apr_int64_t line_no,
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,
181               const char *line,
182               svn_boolean_t local_change,
183               apr_pool_t *pool)
184{
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;
189
190  if (!bb->rev_maxlength)
191    {
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. */
196
197      bb->rev_maxlength = 6;
198      while (max_revnum >= 1000000)
199        {
200          bb->rev_maxlength++;
201          max_revnum = max_revnum / 10;
202        }
203    }
204
205  if (opt_state->use_merge_history)
206    {
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)
213        {
214          SVN_ERR(svn_stream_puts(out, "G "));
215          use_merged = TRUE;
216        }
217      else
218        SVN_ERR(svn_stream_puts(out, "  "));
219    }
220
221  if (use_merged)
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,
228                            bb->rev_maxlength,
229                            pool));
230  else
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,
237                            bb->rev_maxlength,
238                            pool));
239
240  return svn_stream_printf(out, pool, "%s%s", line, APR_EOL_STR);
241}
242
243
244/* This implements the `svn_opt_subcommand_t' interface. */
245svn_error_t *
246svn_cl__blame(apr_getopt_t *os,
247              void *baton,
248              apr_pool_t *pool)
249{
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;
252  apr_pool_t *subpool;
253  apr_array_header_t *targets;
254  blame_baton_t bl;
255  int i;
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;
259
260  SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os,
261                                                      opt_state->targets,
262                                                      ctx, FALSE, pool));
263
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);
267
268  if (opt_state->end_revision.kind == svn_opt_revision_unspecified)
269    {
270      if (opt_state->start_revision.kind != svn_opt_revision_unspecified)
271        {
272          /* In the case that -rX was specified, we actually want to set the
273             range to be -r1:X. */
274
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;
278        }
279      else
280        end_revision_unspecified = TRUE;
281    }
282
283  if (opt_state->start_revision.kind == svn_opt_revision_unspecified)
284    {
285      opt_state->start_revision.kind = svn_opt_revision_number;
286      opt_state->start_revision.value.number = 1;
287    }
288
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.
292  */
293  if (! opt_state->xml)
294    SVN_ERR(svn_stream_for_stdout(&bl.out, pool));
295  else
296    bl.sbuf = svn_stringbuf_create_empty(pool);
297
298  bl.opt_state = opt_state;
299  bl.rev_maxlength = 0;
300
301  subpool = svn_pool_create(pool);
302
303  if (opt_state->extensions)
304    {
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));
308    }
309
310  if (opt_state->xml)
311    {
312      if (opt_state->verbose)
313        return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
314                                _("'verbose' option invalid in XML mode"));
315
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));
321    }
322  else
323    {
324      if (opt_state->incremental)
325        return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
326                                _("'incremental' option only valid in XML "
327                                  "mode"));
328    }
329
330  for (i = 0; i < targets->nelts; i++)
331    {
332      svn_error_t *err;
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;
337
338      svn_pool_clear(subpool);
339      SVN_ERR(svn_cl__check_cancel(ctx->cancel_baton));
340
341      /* Check for a peg revision. */
342      SVN_ERR(svn_opt_parse_path(&peg_revision, &truepath, target,
343                                 subpool));
344
345      if (end_revision_unspecified)
346        {
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;
351          else
352            opt_state->end_revision.kind = svn_opt_revision_working;
353        }
354
355      if (opt_state->xml)
356        {
357          /* "<target ...>" */
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);
365
366          receiver = blame_receiver_xml;
367        }
368      else
369        receiver = blame_receiver;
370
371      err = svn_client_blame5(truepath,
372                              &peg_revision,
373                              &opt_state->start_revision,
374                              &opt_state->end_revision,
375                              diff_options,
376                              opt_state->force,
377                              opt_state->use_merge_history,
378                              receiver,
379                              &bl,
380                              ctx,
381                              subpool);
382
383      if (err)
384        {
385          if (err->apr_err == SVN_ERR_CLIENT_IS_BINARY_FILE)
386            {
387              svn_error_clear(err);
388              SVN_ERR(svn_cmdline_fprintf(stderr, subpool,
389                                          _("Skipping binary file "
390                                            "(use --force to treat as text): "
391                                            "'%s'\n"),
392                                          target));
393            }
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)
398            {
399              svn_handle_warning2(stderr, err, "svn: ");
400              svn_error_clear(err);
401              err = NULL;
402              seen_nonexistent_target = TRUE;
403            }
404          else
405            {
406              return svn_error_trace(err);
407            }
408        }
409      else if (opt_state->xml)
410        {
411          /* "</target>" */
412          svn_xml_make_close_tag(&(bl.sbuf), pool, "target");
413          SVN_ERR(svn_cl__error_checked_fputs(bl.sbuf->data, stdout));
414        }
415
416      if (opt_state->xml)
417        svn_stringbuf_setempty(bl.sbuf);
418    }
419  svn_pool_destroy(subpool);
420  if (opt_state->xml && ! opt_state->incremental)
421    SVN_ERR(svn_cl__xml_print_footer("blame", pool));
422
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"));
428  else
429    return SVN_NO_ERROR;
430}
431