1251881Speter/*
2251881Speter * blame-cmd.c -- Display blame information
3251881Speter *
4251881Speter * ====================================================================
5251881Speter *    Licensed to the Apache Software Foundation (ASF) under one
6251881Speter *    or more contributor license agreements.  See the NOTICE file
7251881Speter *    distributed with this work for additional information
8251881Speter *    regarding copyright ownership.  The ASF licenses this file
9251881Speter *    to you under the Apache License, Version 2.0 (the
10251881Speter *    "License"); you may not use this file except in compliance
11251881Speter *    with the License.  You may obtain a copy of the License at
12251881Speter *
13251881Speter *      http://www.apache.org/licenses/LICENSE-2.0
14251881Speter *
15251881Speter *    Unless required by applicable law or agreed to in writing,
16251881Speter *    software distributed under the License is distributed on an
17251881Speter *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18251881Speter *    KIND, either express or implied.  See the License for the
19251881Speter *    specific language governing permissions and limitations
20251881Speter *    under the License.
21251881Speter * ====================================================================
22251881Speter */
23251881Speter
24251881Speter
25251881Speter/*** Includes. ***/
26251881Speter
27251881Speter#include "svn_client.h"
28251881Speter#include "svn_error.h"
29251881Speter#include "svn_dirent_uri.h"
30251881Speter#include "svn_path.h"
31251881Speter#include "svn_pools.h"
32251881Speter#include "svn_props.h"
33251881Speter#include "svn_cmdline.h"
34251881Speter#include "svn_xml.h"
35251881Speter#include "svn_time.h"
36251881Speter#include "cl.h"
37251881Speter
38251881Speter#include "svn_private_config.h"
39251881Speter
40251881Spetertypedef struct blame_baton_t
41251881Speter{
42251881Speter  svn_cl__opt_state_t *opt_state;
43251881Speter  svn_stream_t *out;
44251881Speter  svn_stringbuf_t *sbuf;
45251881Speter} blame_baton_t;
46251881Speter
47251881Speter
48251881Speter/*** Code. ***/
49251881Speter
50251881Speter/* This implements the svn_client_blame_receiver3_t interface, printing
51251881Speter   XML to stdout. */
52251881Speterstatic svn_error_t *
53251881Speterblame_receiver_xml(void *baton,
54251881Speter                   svn_revnum_t start_revnum,
55251881Speter                   svn_revnum_t end_revnum,
56251881Speter                   apr_int64_t line_no,
57251881Speter                   svn_revnum_t revision,
58251881Speter                   apr_hash_t *rev_props,
59251881Speter                   svn_revnum_t merged_revision,
60251881Speter                   apr_hash_t *merged_rev_props,
61251881Speter                   const char *merged_path,
62251881Speter                   const char *line,
63251881Speter                   svn_boolean_t local_change,
64251881Speter                   apr_pool_t *pool)
65251881Speter{
66251881Speter  svn_cl__opt_state_t *opt_state =
67251881Speter    ((blame_baton_t *) baton)->opt_state;
68251881Speter  svn_stringbuf_t *sb = ((blame_baton_t *) baton)->sbuf;
69251881Speter
70251881Speter  /* "<entry ...>" */
71251881Speter  /* line_no is 0-based, but the rest of the world is probably Pascal
72251881Speter     programmers, so we make them happy and output 1-based line numbers. */
73251881Speter  svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "entry",
74251881Speter                        "line-number",
75251881Speter                        apr_psprintf(pool, "%" APR_INT64_T_FMT,
76251881Speter                                     line_no + 1),
77251881Speter                        NULL);
78251881Speter
79251881Speter  if (SVN_IS_VALID_REVNUM(revision))
80251881Speter    svn_cl__print_xml_commit(&sb, revision,
81251881Speter                             svn_prop_get_value(rev_props,
82251881Speter                                                SVN_PROP_REVISION_AUTHOR),
83251881Speter                             svn_prop_get_value(rev_props,
84251881Speter                                                SVN_PROP_REVISION_DATE),
85251881Speter                             pool);
86251881Speter
87251881Speter  if (opt_state->use_merge_history && SVN_IS_VALID_REVNUM(merged_revision))
88251881Speter    {
89251881Speter      /* "<merged>" */
90251881Speter      svn_xml_make_open_tag(&sb, pool, svn_xml_normal, "merged",
91251881Speter                            "path", merged_path, NULL);
92251881Speter
93251881Speter      svn_cl__print_xml_commit(&sb, merged_revision,
94251881Speter                             svn_prop_get_value(merged_rev_props,
95251881Speter                                                SVN_PROP_REVISION_AUTHOR),
96251881Speter                             svn_prop_get_value(merged_rev_props,
97251881Speter                                                SVN_PROP_REVISION_DATE),
98251881Speter                             pool);
99251881Speter
100251881Speter      /* "</merged>" */
101251881Speter      svn_xml_make_close_tag(&sb, pool, "merged");
102251881Speter
103251881Speter    }
104251881Speter
105251881Speter  /* "</entry>" */
106251881Speter  svn_xml_make_close_tag(&sb, pool, "entry");
107251881Speter
108251881Speter  SVN_ERR(svn_cl__error_checked_fputs(sb->data, stdout));
109251881Speter  svn_stringbuf_setempty(sb);
110251881Speter
111251881Speter  return SVN_NO_ERROR;
112251881Speter}
113251881Speter
114251881Speter
115251881Speterstatic svn_error_t *
116251881Speterprint_line_info(svn_stream_t *out,
117251881Speter                svn_revnum_t revision,
118251881Speter                const char *author,
119251881Speter                const char *date,
120251881Speter                const char *path,
121251881Speter                svn_boolean_t verbose,
122251881Speter                svn_revnum_t end_revnum,
123251881Speter                apr_pool_t *pool)
124251881Speter{
125251881Speter  const char *time_utf8;
126251881Speter  const char *time_stdout;
127251881Speter  const char *rev_str;
128251881Speter  int rev_maxlength;
129251881Speter
130251881Speter  /* The standard column width for the revision number is 6 characters.
131251881Speter     If the revision number can potentially be larger (i.e. if the end_revnum
132251881Speter     is larger than 1000000), we increase the column width as needed. */
133251881Speter  rev_maxlength = 6;
134251881Speter  while (end_revnum >= 1000000)
135251881Speter    {
136251881Speter      rev_maxlength++;
137251881Speter      end_revnum = end_revnum / 10;
138251881Speter    }
139251881Speter  rev_str = SVN_IS_VALID_REVNUM(revision)
140251881Speter    ? apr_psprintf(pool, "%*ld", rev_maxlength, revision)
141251881Speter    : apr_psprintf(pool, "%*s", rev_maxlength, "-");
142251881Speter
143251881Speter  if (verbose)
144251881Speter    {
145251881Speter      if (date)
146251881Speter        {
147251881Speter          SVN_ERR(svn_cl__time_cstring_to_human_cstring(&time_utf8,
148251881Speter                                                        date, pool));
149251881Speter          SVN_ERR(svn_cmdline_cstring_from_utf8(&time_stdout, time_utf8,
150251881Speter                                                pool));
151251881Speter        }
152251881Speter      else
153251881Speter        {
154251881Speter          /* ### This is a 44 characters long string. It assumes the current
155251881Speter             format of svn_time_to_human_cstring and also 3 letter
156251881Speter             abbreviations for the month and weekday names.  Else, the
157251881Speter             line contents will be misaligned. */
158251881Speter          time_stdout = "                                           -";
159251881Speter        }
160251881Speter
161251881Speter      SVN_ERR(svn_stream_printf(out, pool, "%s %10s %s ", rev_str,
162251881Speter                                author ? author : "         -",
163251881Speter                                time_stdout));
164251881Speter
165251881Speter      if (path)
166251881Speter        SVN_ERR(svn_stream_printf(out, pool, "%-14s ", path));
167251881Speter    }
168251881Speter  else
169251881Speter    {
170251881Speter      return svn_stream_printf(out, pool, "%s %10.10s ", rev_str,
171251881Speter                               author ? author : "         -");
172251881Speter    }
173251881Speter
174251881Speter  return SVN_NO_ERROR;
175251881Speter}
176251881Speter
177251881Speter/* This implements the svn_client_blame_receiver3_t interface. */
178251881Speterstatic svn_error_t *
179251881Speterblame_receiver(void *baton,
180251881Speter               svn_revnum_t start_revnum,
181251881Speter               svn_revnum_t end_revnum,
182251881Speter               apr_int64_t line_no,
183251881Speter               svn_revnum_t revision,
184251881Speter               apr_hash_t *rev_props,
185251881Speter               svn_revnum_t merged_revision,
186251881Speter               apr_hash_t *merged_rev_props,
187251881Speter               const char *merged_path,
188251881Speter               const char *line,
189251881Speter               svn_boolean_t local_change,
190251881Speter               apr_pool_t *pool)
191251881Speter{
192251881Speter  svn_cl__opt_state_t *opt_state =
193251881Speter    ((blame_baton_t *) baton)->opt_state;
194251881Speter  svn_stream_t *out = ((blame_baton_t *)baton)->out;
195251881Speter  svn_boolean_t use_merged = FALSE;
196251881Speter
197251881Speter  if (opt_state->use_merge_history)
198251881Speter    {
199251881Speter      /* Choose which revision to use.  If they aren't equal, prefer the
200251881Speter         earliest revision.  Since we do a forward blame, we want to the first
201251881Speter         revision which put the line in its current state, so we use the
202251881Speter         earliest revision.  If we ever switch to a backward blame algorithm,
203251881Speter         we may need to adjust this. */
204251881Speter      if (merged_revision < revision)
205251881Speter        {
206251881Speter          SVN_ERR(svn_stream_puts(out, "G "));
207251881Speter          use_merged = TRUE;
208251881Speter        }
209251881Speter      else
210251881Speter        SVN_ERR(svn_stream_puts(out, "  "));
211251881Speter    }
212251881Speter
213251881Speter  if (use_merged)
214251881Speter    SVN_ERR(print_line_info(out, merged_revision,
215251881Speter                            svn_prop_get_value(merged_rev_props,
216251881Speter                                               SVN_PROP_REVISION_AUTHOR),
217251881Speter                            svn_prop_get_value(merged_rev_props,
218251881Speter                                               SVN_PROP_REVISION_DATE),
219251881Speter                            merged_path, opt_state->verbose, end_revnum,
220251881Speter                            pool));
221251881Speter  else
222251881Speter    SVN_ERR(print_line_info(out, revision,
223251881Speter                            svn_prop_get_value(rev_props,
224251881Speter                                               SVN_PROP_REVISION_AUTHOR),
225251881Speter                            svn_prop_get_value(rev_props,
226251881Speter                                               SVN_PROP_REVISION_DATE),
227251881Speter                            NULL, opt_state->verbose, end_revnum,
228251881Speter                            pool));
229251881Speter
230251881Speter  return svn_stream_printf(out, pool, "%s%s", line, APR_EOL_STR);
231251881Speter}
232251881Speter
233251881Speter
234251881Speter/* This implements the `svn_opt_subcommand_t' interface. */
235251881Spetersvn_error_t *
236251881Spetersvn_cl__blame(apr_getopt_t *os,
237251881Speter              void *baton,
238251881Speter              apr_pool_t *pool)
239251881Speter{
240251881Speter  svn_cl__opt_state_t *opt_state = ((svn_cl__cmd_baton_t *) baton)->opt_state;
241251881Speter  svn_client_ctx_t *ctx = ((svn_cl__cmd_baton_t *) baton)->ctx;
242251881Speter  apr_pool_t *subpool;
243251881Speter  apr_array_header_t *targets;
244251881Speter  blame_baton_t bl;
245251881Speter  int i;
246251881Speter  svn_boolean_t end_revision_unspecified = FALSE;
247251881Speter  svn_diff_file_options_t *diff_options = svn_diff_file_options_create(pool);
248251881Speter  svn_boolean_t seen_nonexistent_target = FALSE;
249251881Speter
250251881Speter  SVN_ERR(svn_cl__args_to_target_array_print_reserved(&targets, os,
251251881Speter                                                      opt_state->targets,
252251881Speter                                                      ctx, FALSE, pool));
253251881Speter
254251881Speter  /* Blame needs a file on which to operate. */
255251881Speter  if (! targets->nelts)
256251881Speter    return svn_error_create(SVN_ERR_CL_INSUFFICIENT_ARGS, 0, NULL);
257251881Speter
258251881Speter  if (opt_state->end_revision.kind == svn_opt_revision_unspecified)
259251881Speter    {
260251881Speter      if (opt_state->start_revision.kind != svn_opt_revision_unspecified)
261251881Speter        {
262251881Speter          /* In the case that -rX was specified, we actually want to set the
263251881Speter             range to be -r1:X. */
264251881Speter
265251881Speter          opt_state->end_revision = opt_state->start_revision;
266251881Speter          opt_state->start_revision.kind = svn_opt_revision_number;
267251881Speter          opt_state->start_revision.value.number = 1;
268251881Speter        }
269251881Speter      else
270251881Speter        end_revision_unspecified = TRUE;
271251881Speter    }
272251881Speter
273251881Speter  if (opt_state->start_revision.kind == svn_opt_revision_unspecified)
274251881Speter    {
275251881Speter      opt_state->start_revision.kind = svn_opt_revision_number;
276251881Speter      opt_state->start_revision.value.number = 1;
277251881Speter    }
278251881Speter
279251881Speter  /* The final conclusion from issue #2431 is that blame info
280251881Speter     is client output (unlike 'svn cat' which plainly cats the file),
281251881Speter     so the EOL style should be the platform local one.
282251881Speter  */
283251881Speter  if (! opt_state->xml)
284251881Speter    SVN_ERR(svn_stream_for_stdout(&bl.out, pool));
285251881Speter  else
286251881Speter    bl.sbuf = svn_stringbuf_create_empty(pool);
287251881Speter
288251881Speter  bl.opt_state = opt_state;
289251881Speter
290251881Speter  subpool = svn_pool_create(pool);
291251881Speter
292251881Speter  if (opt_state->extensions)
293251881Speter    {
294251881Speter      apr_array_header_t *opts;
295251881Speter      opts = svn_cstring_split(opt_state->extensions, " \t\n\r", TRUE, pool);
296251881Speter      SVN_ERR(svn_diff_file_options_parse(diff_options, opts, pool));
297251881Speter    }
298251881Speter
299251881Speter  if (opt_state->xml)
300251881Speter    {
301251881Speter      if (opt_state->verbose)
302251881Speter        return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
303251881Speter                                _("'verbose' option invalid in XML mode"));
304251881Speter
305251881Speter      /* If output is not incremental, output the XML header and wrap
306251881Speter         everything in a top-level element.  This makes the output in
307251881Speter         its entirety a well-formed XML document. */
308251881Speter      if (! opt_state->incremental)
309251881Speter        SVN_ERR(svn_cl__xml_print_header("blame", pool));
310251881Speter    }
311251881Speter  else
312251881Speter    {
313251881Speter      if (opt_state->incremental)
314251881Speter        return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
315251881Speter                                _("'incremental' option only valid in XML "
316251881Speter                                  "mode"));
317251881Speter    }
318251881Speter
319251881Speter  for (i = 0; i < targets->nelts; i++)
320251881Speter    {
321251881Speter      svn_error_t *err;
322251881Speter      const char *target = APR_ARRAY_IDX(targets, i, const char *);
323251881Speter      const char *truepath;
324251881Speter      svn_opt_revision_t peg_revision;
325251881Speter      svn_client_blame_receiver3_t receiver;
326251881Speter
327251881Speter      svn_pool_clear(subpool);
328251881Speter      SVN_ERR(svn_cl__check_cancel(ctx->cancel_baton));
329251881Speter
330251881Speter      /* Check for a peg revision. */
331251881Speter      SVN_ERR(svn_opt_parse_path(&peg_revision, &truepath, target,
332251881Speter                                 subpool));
333251881Speter
334251881Speter      if (end_revision_unspecified)
335251881Speter        {
336251881Speter          if (peg_revision.kind != svn_opt_revision_unspecified)
337251881Speter            opt_state->end_revision = peg_revision;
338251881Speter          else if (svn_path_is_url(target))
339251881Speter            opt_state->end_revision.kind = svn_opt_revision_head;
340251881Speter          else
341251881Speter            opt_state->end_revision.kind = svn_opt_revision_working;
342251881Speter        }
343251881Speter
344251881Speter      if (opt_state->xml)
345251881Speter        {
346251881Speter          /* "<target ...>" */
347251881Speter          /* We don't output this tag immediately, which avoids creating
348251881Speter             a target element if this path is skipped. */
349251881Speter          const char *outpath = truepath;
350251881Speter          if (! svn_path_is_url(target))
351251881Speter            outpath = svn_dirent_local_style(truepath, subpool);
352251881Speter          svn_xml_make_open_tag(&bl.sbuf, pool, svn_xml_normal, "target",
353251881Speter                                "path", outpath, NULL);
354251881Speter
355251881Speter          receiver = blame_receiver_xml;
356251881Speter        }
357251881Speter      else
358251881Speter        receiver = blame_receiver;
359251881Speter
360251881Speter      err = svn_client_blame5(truepath,
361251881Speter                              &peg_revision,
362251881Speter                              &opt_state->start_revision,
363251881Speter                              &opt_state->end_revision,
364251881Speter                              diff_options,
365251881Speter                              opt_state->force,
366251881Speter                              opt_state->use_merge_history,
367251881Speter                              receiver,
368251881Speter                              &bl,
369251881Speter                              ctx,
370251881Speter                              subpool);
371251881Speter
372251881Speter      if (err)
373251881Speter        {
374251881Speter          if (err->apr_err == SVN_ERR_CLIENT_IS_BINARY_FILE)
375251881Speter            {
376251881Speter              svn_error_clear(err);
377251881Speter              SVN_ERR(svn_cmdline_fprintf(stderr, subpool,
378251881Speter                                          _("Skipping binary file "
379251881Speter                                            "(use --force to treat as text): "
380251881Speter                                            "'%s'\n"),
381251881Speter                                          target));
382251881Speter            }
383251881Speter          else if (err->apr_err == SVN_ERR_WC_PATH_NOT_FOUND ||
384251881Speter                   err->apr_err == SVN_ERR_ENTRY_NOT_FOUND ||
385251881Speter                   err->apr_err == SVN_ERR_FS_NOT_FILE ||
386251881Speter                   err->apr_err == SVN_ERR_FS_NOT_FOUND)
387251881Speter            {
388251881Speter              svn_handle_warning2(stderr, err, "svn: ");
389251881Speter              svn_error_clear(err);
390251881Speter              err = NULL;
391251881Speter              seen_nonexistent_target = TRUE;
392251881Speter            }
393251881Speter          else
394251881Speter            {
395251881Speter              return svn_error_trace(err);
396251881Speter            }
397251881Speter        }
398251881Speter      else if (opt_state->xml)
399251881Speter        {
400251881Speter          /* "</target>" */
401251881Speter          svn_xml_make_close_tag(&(bl.sbuf), pool, "target");
402251881Speter          SVN_ERR(svn_cl__error_checked_fputs(bl.sbuf->data, stdout));
403251881Speter        }
404251881Speter
405251881Speter      if (opt_state->xml)
406251881Speter        svn_stringbuf_setempty(bl.sbuf);
407251881Speter    }
408251881Speter  svn_pool_destroy(subpool);
409251881Speter  if (opt_state->xml && ! opt_state->incremental)
410251881Speter    SVN_ERR(svn_cl__xml_print_footer("blame", pool));
411251881Speter
412251881Speter  if (seen_nonexistent_target)
413251881Speter    return svn_error_create(
414251881Speter      SVN_ERR_ILLEGAL_TARGET, NULL,
415251881Speter      _("Could not perform blame on all targets because some "
416251881Speter        "targets don't exist"));
417251881Speter  else
418251881Speter    return SVN_NO_ERROR;
419251881Speter}
420