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  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. */
56static svn_error_t *
57blame_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
117static svn_error_t *
118print_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. */
170static svn_error_t *
171blame_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. */
242svn_error_t *
243svn_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