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_xml.h"
35#include "svn_time.h"
36#include "cl.h"
37
38#include "svn_private_config.h"
39
40typedef struct blame_baton_t
41{
42  svn_cl__opt_state_t *opt_state;
43  svn_stream_t *out;
44  svn_stringbuf_t *sbuf;
45} blame_baton_t;
46
47
48/*** Code. ***/
49
50/* This implements the svn_client_blame_receiver3_t interface, printing
51   XML to stdout. */
52static svn_error_t *
53blame_receiver_xml(void *baton,
54                   svn_revnum_t start_revnum,
55                   svn_revnum_t end_revnum,
56                   apr_int64_t line_no,
57                   svn_revnum_t revision,
58                   apr_hash_t *rev_props,
59                   svn_revnum_t merged_revision,
60                   apr_hash_t *merged_rev_props,
61                   const char *merged_path,
62                   const char *line,
63                   svn_boolean_t local_change,
64                   apr_pool_t *pool)
65{
66  svn_cl__opt_state_t *opt_state =
67    ((blame_baton_t *) baton)->opt_state;
68  svn_stringbuf_t *sb = ((blame_baton_t *) baton)->sbuf;
69
70  /* "<entry ...>" */
71  /* line_no is 0-based, but the rest of the world is probably Pascal
72     programmers, so we make them happy and output 1-based line numbers. */
73  svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "entry",
74                        "line-number",
75                        apr_psprintf(pool, "%" APR_INT64_T_FMT,
76                                     line_no + 1),
77                        NULL);
78
79  if (SVN_IS_VALID_REVNUM(revision))
80    svn_cl__print_xml_commit(&sb, revision,
81                             svn_prop_get_value(rev_props,
82                                                SVN_PROP_REVISION_AUTHOR),
83                             svn_prop_get_value(rev_props,
84                                                SVN_PROP_REVISION_DATE),
85                             pool);
86
87  if (opt_state->use_merge_history && SVN_IS_VALID_REVNUM(merged_revision))
88    {
89      /* "<merged>" */
90      svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "merged",
91                            "path", merged_path, NULL);
92
93      svn_cl__print_xml_commit(&sb, merged_revision,
94                             svn_prop_get_value(merged_rev_props,
95                                                SVN_PROP_REVISION_AUTHOR),
96                             svn_prop_get_value(merged_rev_props,
97                                                SVN_PROP_REVISION_DATE),
98                             pool);
99
100      /* "</merged>" */
101      svn_xml_make_close_tag(&sb, pool, "merged");
102
103    }
104
105  /* "</entry>" */
106  svn_xml_make_close_tag(&sb, pool, "entry");
107
108  SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout));
109  svn_stringbuf_setempty(sb);
110
111  return SVN_NO_ERROR;
112}
113
114
115static svn_error_t *
116print_line_info(svn_stream_t *out,
117                svn_revnum_t revision,
118                const char *author,
119                const char *date,
120                const char *path,
121                svn_boolean_t verbose,
122                svn_revnum_t end_revnum,
123                apr_pool_t *pool)
124{
125  const char *time_utf8;
126  const char *time_stdout;
127  const char *rev_str;
128  int rev_maxlength;
129
130  /* The standard column width for the revision number is 6 characters.
131     If the revision number can potentially be larger (i.e. if the end_revnum
132     is larger than 1000000), we increase the column width as needed. */
133  rev_maxlength = 6;
134  while (end_revnum >= 1000000)
135    {
136      rev_maxlength++;
137      end_revnum = end_revnum / 10;
138    }
139  rev_str = SVN_IS_VALID_REVNUM(revision)
140    ? apr_psprintf(pool, "%*ld", rev_maxlength, revision)
141    : apr_psprintf(pool, "%*s", rev_maxlength, "-");
142
143  if (verbose)
144    {
145      if (date)
146        {
147          SVN_ERR(svn_cl__time_cstring_to_human_cstring(&time_utf8,
148                                                        date, pool));
149          SVN_ERR(svn_cmdline_cstring_from_utf8(&time_stdout, time_utf8,
150                                                pool));
151        }
152      else
153        {
154          /* ### This is a 44 characters long string. It assumes the current
155             format of svn_time_to_human_cstring and also 3 letter
156             abbreviations for the month and weekday names.  Else, the
157             line contents will be misaligned. */
158          time_stdout = "                                           -";
159        }
160
161      SVN_ERR(svn_stream_printf(out, pool, "%s %10s %s ", rev_str,
162                                author ? author : "         -",
163                                time_stdout));
164
165      if (path)
166        SVN_ERR(svn_stream_printf(out, pool, "%-14s ", path));
167    }
168  else
169    {
170      return svn_stream_printf(out, pool, "%s %10.10s ", rev_str,
171                               author ? author : "         -");
172    }
173
174  return SVN_NO_ERROR;
175}
176
177/* This implements the svn_client_blame_receiver3_t interface. */
178static svn_error_t *
179blame_receiver(void *baton,
180               svn_revnum_t start_revnum,
181               svn_revnum_t end_revnum,
182               apr_int64_t line_no,
183               svn_revnum_t revision,
184               apr_hash_t *rev_props,
185               svn_revnum_t merged_revision,
186               apr_hash_t *merged_rev_props,
187               const char *merged_path,
188               const char *line,
189               svn_boolean_t local_change,
190               apr_pool_t *pool)
191{
192  svn_cl__opt_state_t *opt_state =
193    ((blame_baton_t *) baton)->opt_state;
194  svn_stream_t *out = ((blame_baton_t *)baton)->out;
195  svn_boolean_t use_merged = FALSE;
196
197  if (opt_state->use_merge_history)
198    {
199      /* Choose which revision to use.  If they aren't equal, prefer the
200         earliest revision.  Since we do a forward blame, we want to the first
201         revision which put the line in its current state, so we use the
202         earliest revision.  If we ever switch to a backward blame algorithm,
203         we may need to adjust this. */
204      if (merged_revision < revision)
205        {
206          SVN_ERR(svn_stream_puts(out, "G "));
207          use_merged = TRUE;
208        }
209      else
210        SVN_ERR(svn_stream_puts(out, "  "));
211    }
212
213  if (use_merged)
214    SVN_ERR(print_line_info(out, merged_revision,
215                            svn_prop_get_value(merged_rev_props,
216                                               SVN_PROP_REVISION_AUTHOR),
217                            svn_prop_get_value(merged_rev_props,
218                                               SVN_PROP_REVISION_DATE),
219                            merged_path, opt_state->verbose, end_revnum,
220                            pool));
221  else
222    SVN_ERR(print_line_info(out, revision,
223                            svn_prop_get_value(rev_props,
224                                               SVN_PROP_REVISION_AUTHOR),
225                            svn_prop_get_value(rev_props,
226                                               SVN_PROP_REVISION_DATE),
227                            NULL, opt_state->verbose, end_revnum,
228                            pool));
229
230  return svn_stream_printf(out, pool, "%s%s", line, APR_EOL_STR);
231}
232
233
234/* This implements the `svn_opt_subcommand_t' interface. */
235svn_error_t *
236svn_cl__blame(apr_getopt_t *os,
237              void *baton,
238              apr_pool_t *pool)
239{
240  svn_cl__opt_state_t *opt_state = ((svn_cl__cmd_baton_t *) baton)->opt_state;
241  svn_client_ctx_t *ctx = ((svn_cl__cmd_baton_t *) baton)->ctx;
242  apr_pool_t *subpool;
243  apr_array_header_t *targets;
244  blame_baton_t bl;
245  int i;
246  svn_boolean_t end_revision_unspecified = FALSE;
247  svn_diff_file_options_t *diff_options = svn_diff_file_options_create(pool);
248  svn_boolean_t seen_nonexistent_target = FALSE;
249
250  SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os,
251                                                      opt_state->targets,
252                                                      ctx, FALSE, pool));
253
254  /* Blame needs a file on which to operate. */
255  if (! targets->nelts)
256    return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
257
258  if (opt_state->end_revision.kind == svn_opt_revision_unspecified)
259    {
260      if (opt_state->start_revision.kind != svn_opt_revision_unspecified)
261        {
262          /* In the case that -rX was specified, we actually want to set the
263             range to be -r1:X. */
264
265          opt_state->end_revision = opt_state->start_revision;
266          opt_state->start_revision.kind = svn_opt_revision_number;
267          opt_state->start_revision.value.number = 1;
268        }
269      else
270        end_revision_unspecified = TRUE;
271    }
272
273  if (opt_state->start_revision.kind == svn_opt_revision_unspecified)
274    {
275      opt_state->start_revision.kind = svn_opt_revision_number;
276      opt_state->start_revision.value.number = 1;
277    }
278
279  /* The final conclusion from issue #2431 is that blame info
280     is client output (unlike 'svn cat' which plainly cats the file),
281     so the EOL style should be the platform local one.
282  */
283  if (! opt_state->xml)
284    SVN_ERR(svn_stream_for_stdout(&bl.out, pool));
285  else
286    bl.sbuf = svn_stringbuf_create_empty(pool);
287
288  bl.opt_state = opt_state;
289
290  subpool = svn_pool_create(pool);
291
292  if (opt_state->extensions)
293    {
294      apr_array_header_t *opts;
295      opts = svn_cstring_split(opt_state->extensions, " \t\n\r", TRUE, pool);
296      SVN_ERR(svn_diff_file_options_parse(diff_options, opts, pool));
297    }
298
299  if (opt_state->xml)
300    {
301      if (opt_state->verbose)
302        return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
303                                _("'verbose' option invalid in XML mode"));
304
305      /* If output is not incremental, output the XML header and wrap
306         everything in a top-level element.  This makes the output in
307         its entirety a well-formed XML document. */
308      if (! opt_state->incremental)
309        SVN_ERR(svn_cl__xml_print_header("blame", pool));
310    }
311  else
312    {
313      if (opt_state->incremental)
314        return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
315                                _("'incremental' option only valid in XML "
316                                  "mode"));
317    }
318
319  for (i = 0; i < targets->nelts; i++)
320    {
321      svn_error_t *err;
322      const char *target = APR_ARRAY_IDX(targets, i, const char *);
323      const char *truepath;
324      svn_opt_revision_t peg_revision;
325      svn_client_blame_receiver3_t receiver;
326
327      svn_pool_clear(subpool);
328      SVN_ERR(svn_cl__check_cancel(ctx->cancel_baton));
329
330      /* Check for a peg revision. */
331      SVN_ERR(svn_opt_parse_path(&peg_revision, &truepath, target,
332                                 subpool));
333
334      if (end_revision_unspecified)
335        {
336          if (peg_revision.kind != svn_opt_revision_unspecified)
337            opt_state->end_revision = peg_revision;
338          else if (svn_path_is_url(target))
339            opt_state->end_revision.kind = svn_opt_revision_head;
340          else
341            opt_state->end_revision.kind = svn_opt_revision_working;
342        }
343
344      if (opt_state->xml)
345        {
346          /* "<target ...>" */
347          /* We don't output this tag immediately, which avoids creating
348             a target element if this path is skipped. */
349          const char *outpath = truepath;
350          if (! svn_path_is_url(target))
351            outpath = svn_dirent_local_style(truepath, subpool);
352          svn_xml_make_open_tag(&bl.sbuf, pool, svn_xml_normal, "target",
353                                "path", outpath, NULL);
354
355          receiver = blame_receiver_xml;
356        }
357      else
358        receiver = blame_receiver;
359
360      err = svn_client_blame5(truepath,
361                              &peg_revision,
362                              &opt_state->start_revision,
363                              &opt_state->end_revision,
364                              diff_options,
365                              opt_state->force,
366                              opt_state->use_merge_history,
367                              receiver,
368                              &bl,
369                              ctx,
370                              subpool);
371
372      if (err)
373        {
374          if (err->apr_err == SVN_ERR_CLIENT_IS_BINARY_FILE)
375            {
376              svn_error_clear(err);
377              SVN_ERR(svn_cmdline_fprintf(stderr, subpool,
378                                          _("Skipping binary file "
379                                            "(use --force to treat as text): "
380                                            "'%s'\n"),
381                                          target));
382            }
383          else if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND ||
384                   err->apr_err == SVN_ERR_ENTRY_NOT_FOUND ||
385                   err->apr_err == SVN_ERR_FS_NOT_FILE ||
386                   err->apr_err == SVN_ERR_FS_NOT_FOUND)
387            {
388              svn_handle_warning2(stderr, err, "svn: ");
389              svn_error_clear(err);
390              err = NULL;
391              seen_nonexistent_target = TRUE;
392            }
393          else
394            {
395              return svn_error_trace(err);
396            }
397        }
398      else if (opt_state->xml)
399        {
400          /* "</target>" */
401          svn_xml_make_close_tag(&(bl.sbuf), pool, "target");
402          SVN_ERR(svn_cl__error_checked_fputs(bl.sbuf->data, stdout));
403        }
404
405      if (opt_state->xml)
406        svn_stringbuf_setempty(bl.sbuf);
407    }
408  svn_pool_destroy(subpool);
409  if (opt_state->xml && ! opt_state->incremental)
410    SVN_ERR(svn_cl__xml_print_footer("blame", pool));
411
412  if (seen_nonexistent_target)
413    return svn_error_create(
414      SVN_ERR_ILLEGAL_TARGET, NULL,
415      _("Could not perform blame on all targets because some "
416        "targets don't exist"));
417  else
418    return SVN_NO_ERROR;
419}
420