1/*
2 * log.c:  return log messages
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#define APR_WANT_STRFUNC
25#include <apr_want.h>
26
27#include <apr_strings.h>
28#include <apr_pools.h>
29
30#include "svn_pools.h"
31#include "svn_client.h"
32#include "svn_compat.h"
33#include "svn_error.h"
34#include "svn_dirent_uri.h"
35#include "svn_hash.h"
36#include "svn_path.h"
37#include "svn_sorts.h"
38#include "svn_props.h"
39
40#include "client.h"
41
42#include "svn_private_config.h"
43#include "private/svn_wc_private.h"
44
45#include <assert.h>
46
47/*** Getting misc. information ***/
48
49/* The baton for use with copyfrom_info_receiver(). */
50typedef struct copyfrom_info_t
51{
52  svn_boolean_t is_first;
53  const char *path;
54  svn_revnum_t rev;
55  apr_pool_t *pool;
56} copyfrom_info_t;
57
58/* A location segment callback for obtaining the copy source of
59   a node at a path and storing it in *BATON (a struct copyfrom_info_t *).
60   Implements svn_location_segment_receiver_t. */
61static svn_error_t *
62copyfrom_info_receiver(svn_location_segment_t *segment,
63                       void *baton,
64                       apr_pool_t *pool)
65{
66  copyfrom_info_t *copyfrom_info = baton;
67
68  /* If we've already identified the copy source, there's nothing more
69     to do.
70     ### FIXME:  We *should* be able to send */
71  if (copyfrom_info->path)
72    return SVN_NO_ERROR;
73
74  /* If this is the first segment, it's not of interest to us. Otherwise
75     (so long as this segment doesn't represent a history gap), it holds
76     our path's previous location (from which it was last copied). */
77  if (copyfrom_info->is_first)
78    {
79      copyfrom_info->is_first = FALSE;
80    }
81  else if (segment->path)
82    {
83      /* The end of the second non-gap segment is the location copied from.  */
84      copyfrom_info->path = apr_pstrdup(copyfrom_info->pool, segment->path);
85      copyfrom_info->rev = segment->range_end;
86
87      /* ### FIXME: We *should* be able to return SVN_ERR_CEASE_INVOCATION
88         ### here so we don't get called anymore. */
89    }
90
91  return SVN_NO_ERROR;
92}
93
94svn_error_t *
95svn_client__get_copy_source(const char **original_repos_relpath,
96                            svn_revnum_t *original_revision,
97                            const char *path_or_url,
98                            const svn_opt_revision_t *revision,
99                            svn_ra_session_t *ra_session,
100                            svn_client_ctx_t *ctx,
101                            apr_pool_t *result_pool,
102                            apr_pool_t *scratch_pool)
103{
104  svn_error_t *err;
105  copyfrom_info_t copyfrom_info = { 0 };
106  apr_pool_t *sesspool = svn_pool_create(scratch_pool);
107  svn_client__pathrev_t *at_loc;
108  const char *old_session_url = NULL;
109
110  copyfrom_info.is_first = TRUE;
111  copyfrom_info.path = NULL;
112  copyfrom_info.rev = SVN_INVALID_REVNUM;
113  copyfrom_info.pool = result_pool;
114
115  if (!ra_session)
116    {
117      SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &at_loc,
118                                                path_or_url, NULL,
119                                                revision, revision,
120                                                ctx, sesspool));
121    }
122  else
123    {
124      const char *url;
125      if (svn_path_is_url(path_or_url))
126        url = path_or_url;
127      else
128        {
129          SVN_ERR(svn_client_url_from_path2(&url, path_or_url, ctx, sesspool,
130                                            sesspool));
131
132          if (! url)
133            return svn_error_createf(SVN_ERR_ENTRY_MISSING_URL, NULL,
134                                     _("'%s' has no URL"), path_or_url);
135        }
136
137      SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url, ra_session,
138                                                url, sesspool));
139
140      err = svn_client__resolve_rev_and_url(&at_loc, ra_session, path_or_url,
141                                            revision, revision, ctx,
142                                            sesspool);
143
144      /* On error reparent back (and return), otherwise reparent to new
145         location */
146      SVN_ERR(svn_error_compose_create(
147                err,
148                svn_ra_reparent(ra_session, err ? old_session_url
149                                                : at_loc->url, sesspool)));
150    }
151
152  /* Find the copy source.  Walk the location segments to find the revision
153     at which this node was created (copied or added). */
154
155  err = svn_ra_get_location_segments(ra_session, "", at_loc->rev, at_loc->rev,
156                                     SVN_INVALID_REVNUM,
157                                     copyfrom_info_receiver, &copyfrom_info,
158                                     scratch_pool);
159
160  if (old_session_url)
161    err = svn_error_compose_create(
162                    err,
163                    svn_ra_reparent(ra_session, old_session_url, sesspool));
164
165  svn_pool_destroy(sesspool);
166
167  if (err)
168    {
169      if (err->apr_err == SVN_ERR_FS_NOT_FOUND ||
170          err->apr_err == SVN_ERR_RA_DAV_REQUEST_FAILED)
171        {
172          /* A locally-added but uncommitted versioned resource won't
173             exist in the repository. */
174            svn_error_clear(err);
175            err = SVN_NO_ERROR;
176
177            *original_repos_relpath = NULL;
178            *original_revision = SVN_INVALID_REVNUM;
179        }
180      return svn_error_trace(err);
181    }
182
183  *original_repos_relpath = copyfrom_info.path;
184  *original_revision = copyfrom_info.rev;
185  return SVN_NO_ERROR;
186}
187
188
189/* compatibility with pre-1.5 servers, which send only author/date/log
190 *revprops in log entries */
191typedef struct pre_15_receiver_baton_t
192{
193  svn_client_ctx_t *ctx;
194  /* ra session for retrieving revprops from old servers */
195  svn_ra_session_t *ra_session;
196  /* caller's list of requested revprops, receiver, and baton */
197  const char *ra_session_url;
198  apr_pool_t *ra_session_pool;
199  const apr_array_header_t *revprops;
200  svn_log_entry_receiver_t receiver;
201  void *baton;
202} pre_15_receiver_baton_t;
203
204static svn_error_t *
205pre_15_receiver(void *baton, svn_log_entry_t *log_entry, apr_pool_t *pool)
206{
207  pre_15_receiver_baton_t *rb = baton;
208
209  if (log_entry->revision == SVN_INVALID_REVNUM)
210    return rb->receiver(rb->baton, log_entry, pool);
211
212  /* If only some revprops are requested, get them one at a time on the
213     second ra connection.  If all are requested, get them all with
214     svn_ra_rev_proplist.  This avoids getting unrequested revprops (which
215     may be arbitrarily large), but means one round-trip per requested
216     revprop.  epg isn't entirely sure which should be optimized for. */
217  if (rb->revprops)
218    {
219      int i;
220      svn_boolean_t want_author, want_date, want_log;
221      want_author = want_date = want_log = FALSE;
222      for (i = 0; i < rb->revprops->nelts; i++)
223        {
224          const char *name = APR_ARRAY_IDX(rb->revprops, i, const char *);
225          svn_string_t *value;
226
227          /* If a standard revprop is requested, we know it is already in
228             log_entry->revprops if available. */
229          if (strcmp(name, SVN_PROP_REVISION_AUTHOR) == 0)
230            {
231              want_author = TRUE;
232              continue;
233            }
234          if (strcmp(name, SVN_PROP_REVISION_DATE) == 0)
235            {
236              want_date = TRUE;
237              continue;
238            }
239          if (strcmp(name, SVN_PROP_REVISION_LOG) == 0)
240            {
241              want_log = TRUE;
242              continue;
243            }
244
245          if (rb->ra_session == NULL)
246            SVN_ERR(svn_client_open_ra_session2(&rb->ra_session,
247                                                rb->ra_session_url, NULL,
248                                                rb->ctx, rb->ra_session_pool,
249                                                pool));
250
251          SVN_ERR(svn_ra_rev_prop(rb->ra_session, log_entry->revision,
252                                  name, &value, pool));
253          if (log_entry->revprops == NULL)
254            log_entry->revprops = apr_hash_make(pool);
255          svn_hash_sets(log_entry->revprops, name, value);
256        }
257      if (log_entry->revprops)
258        {
259          /* Pre-1.5 servers send the standard revprops unconditionally;
260             clear those the caller doesn't want. */
261          if (!want_author)
262            svn_hash_sets(log_entry->revprops, SVN_PROP_REVISION_AUTHOR, NULL);
263          if (!want_date)
264            svn_hash_sets(log_entry->revprops, SVN_PROP_REVISION_DATE, NULL);
265          if (!want_log)
266            svn_hash_sets(log_entry->revprops, SVN_PROP_REVISION_LOG, NULL);
267        }
268    }
269  else
270    {
271      if (rb->ra_session == NULL)
272        SVN_ERR(svn_client_open_ra_session2(&rb->ra_session,
273                                            rb->ra_session_url, NULL,
274                                            rb->ctx, rb->ra_session_pool,
275                                            pool));
276
277      SVN_ERR(svn_ra_rev_proplist(rb->ra_session, log_entry->revision,
278                                  &log_entry->revprops, pool));
279    }
280
281  return rb->receiver(rb->baton, log_entry, pool);
282}
283
284/* limit receiver */
285typedef struct limit_receiver_baton_t
286{
287  int limit;
288  svn_log_entry_receiver_t receiver;
289  void *baton;
290} limit_receiver_baton_t;
291
292static svn_error_t *
293limit_receiver(void *baton, svn_log_entry_t *log_entry, apr_pool_t *pool)
294{
295  limit_receiver_baton_t *rb = baton;
296
297  rb->limit--;
298
299  return rb->receiver(rb->baton, log_entry, pool);
300}
301
302/* Resolve the URLs or WC path in TARGETS as per the svn_client_log5 API.
303
304   The limitations on TARGETS specified by svn_client_log5 are enforced here.
305   So TARGETS can only contain a single WC path or a URL and zero or more
306   relative paths -- anything else will raise an error.
307
308   PEG_REVISION, TARGETS, and CTX are as per svn_client_log5.
309
310   If TARGETS contains a single WC path then set *RA_TARGET to the absolute
311   path of that single path if PEG_REVISION is dependent on the working copy
312   (e.g. PREV).  Otherwise set *RA_TARGET to the corresponding URL for the
313   single WC path.  Set *RELATIVE_TARGETS to an array with a single
314   element "".
315
316   If TARGETS contains only a single URL, then set *RA_TARGET to a copy of
317   that URL and *RELATIVE_TARGETS to an array with a single element "".
318
319   If TARGETS contains a single URL and one or more relative paths, then
320   set *RA_TARGET to a copy of that URL and *RELATIVE_TARGETS to a copy of
321   each relative path after the URL.
322
323   If *PEG_REVISION is svn_opt_revision_unspecified, then *PEG_REVISION is
324   set to svn_opt_revision_head for URLs or svn_opt_revision_working for a
325   WC path.
326
327   *RA_TARGET and *RELATIVE_TARGETS are allocated in RESULT_POOL. */
328static svn_error_t *
329resolve_log_targets(apr_array_header_t **relative_targets,
330                    const char **ra_target,
331                    svn_opt_revision_t *peg_revision,
332                    const apr_array_header_t *targets,
333                    svn_client_ctx_t *ctx,
334                    apr_pool_t *result_pool,
335                    apr_pool_t *scratch_pool)
336{
337  int i;
338  svn_boolean_t url_targets;
339
340  /* Per svn_client_log5, TARGETS contains either a URL followed by zero or
341     more relative paths, or one working copy path. */
342  const char *url_or_path = APR_ARRAY_IDX(targets, 0, const char *);
343
344  /* svn_client_log5 requires at least one target. */
345  if (targets->nelts == 0)
346    return svn_error_create(SVN_ERR_ILLEGAL_TARGET, NULL,
347                            _("No valid target found"));
348
349  /* Initialize the output array.  At a minimum, we need room for one
350     (possibly empty) relpath.  Otherwise, we have to hold a relpath
351     for every item in TARGETS except the first.  */
352  *relative_targets = apr_array_make(result_pool,
353                                     MAX(1, targets->nelts - 1),
354                                     sizeof(const char *));
355
356  if (svn_path_is_url(url_or_path))
357    {
358      /* An unspecified PEG_REVISION for a URL path defaults
359         to svn_opt_revision_head. */
360      if (peg_revision->kind == svn_opt_revision_unspecified)
361        peg_revision->kind = svn_opt_revision_head;
362
363      /* The logic here is this: If we get passed one argument, we assume
364         it is the full URL to a file/dir we want log info for. If we get
365         a URL plus some paths, then we assume that the URL is the base,
366         and that the paths passed are relative to it.  */
367      if (targets->nelts > 1)
368        {
369          /* We have some paths, let's use them. Start after the URL.  */
370          for (i = 1; i < targets->nelts; i++)
371            {
372              const char *target;
373
374              target = APR_ARRAY_IDX(targets, i, const char *);
375
376              if (svn_path_is_url(target) || svn_dirent_is_absolute(target))
377                return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL,
378                                         _("'%s' is not a relative path"),
379                                          target);
380
381              APR_ARRAY_PUSH(*relative_targets, const char *) =
382                apr_pstrdup(result_pool, target);
383            }
384        }
385      else
386        {
387          /* If we have a single URL, then the session will be rooted at
388             it, so just send an empty string for the paths we are
389             interested in. */
390          APR_ARRAY_PUSH(*relative_targets, const char *) = "";
391        }
392
393      /* Remember that our targets are URLs. */
394      url_targets = TRUE;
395    }
396  else /* WC path target. */
397    {
398      const char *target;
399      const char *target_abspath;
400
401      url_targets = FALSE;
402      if (targets->nelts > 1)
403        return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
404                                _("When specifying working copy paths, only "
405                                  "one target may be given"));
406
407      /* An unspecified PEG_REVISION for a working copy path defaults
408         to svn_opt_revision_working. */
409      if (peg_revision->kind == svn_opt_revision_unspecified)
410        peg_revision->kind = svn_opt_revision_working;
411
412      /* Get URLs for each target */
413      target = APR_ARRAY_IDX(targets, 0, const char *);
414
415      SVN_ERR(svn_dirent_get_absolute(&target_abspath, target, scratch_pool));
416      SVN_ERR(svn_wc__node_get_url(&url_or_path, ctx->wc_ctx, target_abspath,
417                                   scratch_pool, scratch_pool));
418      APR_ARRAY_PUSH(*relative_targets, const char *) = "";
419    }
420
421  /* If this is a revision type that requires access to the working copy,
422   * we use our initial target path to figure out where to root the RA
423   * session, otherwise we use our URL. */
424  if (SVN_CLIENT__REVKIND_NEEDS_WC(peg_revision->kind))
425    {
426      if (url_targets)
427        return svn_error_create(SVN_ERR_CLIENT_BAD_REVISION, NULL,
428                                _("PREV, BASE, or COMMITTED revision "
429                                  "keywords are invalid for URL"));
430
431      else
432        SVN_ERR(svn_dirent_get_absolute(
433          ra_target, APR_ARRAY_IDX(targets, 0, const char *), result_pool));
434    }
435  else
436    {
437      *ra_target = apr_pstrdup(result_pool, url_or_path);
438    }
439
440  return SVN_NO_ERROR;
441}
442
443/* Keep track of oldest and youngest opt revs found.
444
445   If REV is younger than *YOUNGEST_REV, or *YOUNGEST_REV is
446   svn_opt_revision_unspecified, then set *YOUNGEST_REV equal to REV.
447
448   If REV is older than *OLDEST_REV, or *OLDEST_REV is
449   svn_opt_revision_unspecified, then set *OLDEST_REV equal to REV. */
450static void
451find_youngest_and_oldest_revs(svn_revnum_t *youngest_rev,
452                              svn_revnum_t *oldest_rev,
453                              svn_revnum_t rev)
454{
455  /* Is REV younger than YOUNGEST_REV? */
456  if (! SVN_IS_VALID_REVNUM(*youngest_rev)
457      || rev > *youngest_rev)
458    *youngest_rev = rev;
459
460  if (! SVN_IS_VALID_REVNUM(*oldest_rev)
461      || rev < *oldest_rev)
462    *oldest_rev = rev;
463}
464
465typedef struct rev_range_t
466{
467  svn_revnum_t range_start;
468  svn_revnum_t range_end;
469} rev_range_t;
470
471/* Convert array of svn_opt_revision_t ranges to an array of svn_revnum_t
472   ranges.
473
474   Given a log target URL_OR_ABSPATH@PEG_REV and an array of
475   svn_opt_revision_range_t's OPT_REV_RANGES, resolve the opt revs in
476   OPT_REV_RANGES to svn_revnum_t's and return these in *REVISION_RANGES, an
477   array of rev_range_t *.
478
479   Set *YOUNGEST_REV and *OLDEST_REV to the youngest and oldest revisions
480   found in *REVISION_RANGES.
481
482   If the repository needs to be contacted to resolve svn_opt_revision_date or
483   svn_opt_revision_head revisions, then the session used to do this is
484   RA_SESSION; it must be an open session to any URL in the right repository.
485*/
486static svn_error_t*
487convert_opt_rev_array_to_rev_range_array(
488  apr_array_header_t **revision_ranges,
489  svn_revnum_t *youngest_rev,
490  svn_revnum_t *oldest_rev,
491  svn_ra_session_t *ra_session,
492  const char *url_or_abspath,
493  const apr_array_header_t *opt_rev_ranges,
494  const svn_opt_revision_t *peg_rev,
495  svn_client_ctx_t *ctx,
496  apr_pool_t *result_pool,
497  apr_pool_t *scratch_pool)
498{
499  int i;
500  svn_revnum_t head_rev = SVN_INVALID_REVNUM;
501
502  /* Initialize the input/output parameters. */
503  *youngest_rev = *oldest_rev = SVN_INVALID_REVNUM;
504
505  /* Convert OPT_REV_RANGES to an array of rev_range_t and find the youngest
506     and oldest revision range that spans all of OPT_REV_RANGES. */
507  *revision_ranges = apr_array_make(result_pool, opt_rev_ranges->nelts,
508                                    sizeof(rev_range_t *));
509
510  for (i = 0; i < opt_rev_ranges->nelts; i++)
511    {
512      svn_opt_revision_range_t *range;
513      rev_range_t *rev_range;
514      svn_boolean_t start_same_as_end = FALSE;
515
516      range = APR_ARRAY_IDX(opt_rev_ranges, i, svn_opt_revision_range_t *);
517
518      /* Right now RANGE can be any valid pair of svn_opt_revision_t's.  We
519         will now convert all RANGEs in place to the corresponding
520         svn_opt_revision_number kind. */
521      if ((range->start.kind != svn_opt_revision_unspecified)
522          && (range->end.kind == svn_opt_revision_unspecified))
523        {
524          /* If the user specified exactly one revision, then start rev is
525           * set but end is not.  We show the log message for just that
526           * revision by making end equal to start.
527           *
528           * Note that if the user requested a single dated revision, then
529           * this will cause the same date to be resolved twice.  The
530           * extra code complexity to get around this slight inefficiency
531           * doesn't seem worth it, however. */
532          range->end = range->start;
533        }
534      else if (range->start.kind == svn_opt_revision_unspecified)
535        {
536          /* Default to any specified peg revision.  Otherwise, if the
537           * first target is a URL, then we default to HEAD:0.  Lastly,
538           * the default is BASE:0 since WC@HEAD may not exist. */
539          if (peg_rev->kind == svn_opt_revision_unspecified)
540            {
541              if (svn_path_is_url(url_or_abspath))
542                range->start.kind = svn_opt_revision_head;
543              else
544                range->start.kind = svn_opt_revision_base;
545            }
546          else
547            range->start = *peg_rev;
548
549          if (range->end.kind == svn_opt_revision_unspecified)
550            {
551              range->end.kind = svn_opt_revision_number;
552              range->end.value.number = 0;
553            }
554        }
555
556      if ((range->start.kind == svn_opt_revision_unspecified)
557          || (range->end.kind == svn_opt_revision_unspecified))
558        {
559          return svn_error_create
560            (SVN_ERR_CLIENT_BAD_REVISION, NULL,
561             _("Missing required revision specification"));
562        }
563
564      /* Does RANGE describe a single svn_opt_revision_t? */
565      if (range->start.kind == range->end.kind)
566        {
567          if (range->start.kind == svn_opt_revision_number)
568            {
569              if (range->start.value.number == range->end.value.number)
570                start_same_as_end = TRUE;
571            }
572          else if (range->start.kind == svn_opt_revision_date)
573            {
574              if (range->start.value.date == range->end.value.date)
575                start_same_as_end = TRUE;
576            }
577          else
578            {
579              start_same_as_end = TRUE;
580            }
581        }
582
583      rev_range = apr_palloc(result_pool, sizeof(*rev_range));
584      SVN_ERR(svn_client__get_revision_number(
585                &rev_range->range_start, &head_rev,
586                ctx->wc_ctx, url_or_abspath, ra_session,
587                &range->start, scratch_pool));
588      if (start_same_as_end)
589        rev_range->range_end = rev_range->range_start;
590      else
591        SVN_ERR(svn_client__get_revision_number(
592                  &rev_range->range_end, &head_rev,
593                  ctx->wc_ctx, url_or_abspath, ra_session,
594                  &range->end, scratch_pool));
595
596      /* Possibly update the oldest and youngest revisions requested. */
597      find_youngest_and_oldest_revs(youngest_rev,
598                                    oldest_rev,
599                                    rev_range->range_start);
600      find_youngest_and_oldest_revs(youngest_rev,
601                                    oldest_rev,
602                                    rev_range->range_end);
603      APR_ARRAY_PUSH(*revision_ranges, rev_range_t *) = rev_range;
604    }
605
606  return SVN_NO_ERROR;
607}
608
609static int
610compare_rev_to_segment(const void *key_p,
611                       const void *element_p)
612{
613  svn_revnum_t rev =
614    * (svn_revnum_t *)key_p;
615  const svn_location_segment_t *segment =
616    *((const svn_location_segment_t * const *) element_p);
617
618  if (rev < segment->range_start)
619    return -1;
620  else if (rev > segment->range_end)
621    return 1;
622  else
623    return 0;
624}
625
626/* Run svn_ra_get_log2 for PATHS, one or more paths relative to RA_SESSION's
627   common parent, for each revision in REVISION_RANGES, an array of
628   rev_range_t.
629
630   RA_SESSION is an open session pointing to ACTUAL_LOC.
631
632   LOG_SEGMENTS is an array of svn_location_segment_t * items representing the
633   history of PATHS from the oldest to youngest revisions found in
634   REVISION_RANGES.
635
636   The TARGETS, LIMIT, DISCOVER_CHANGED_PATHS, STRICT_NODE_HISTORY,
637   INCLUDE_MERGED_REVISIONS, REVPROPS, REAL_RECEIVER, and REAL_RECEIVER_BATON
638   parameters are all as per the svn_client_log5 API. */
639static svn_error_t *
640run_ra_get_log(apr_array_header_t *revision_ranges,
641               apr_array_header_t *paths,
642               apr_array_header_t *log_segments,
643               svn_client__pathrev_t *actual_loc,
644               svn_ra_session_t *ra_session,
645               /* The following are as per svn_client_log5. */
646               const apr_array_header_t *targets,
647               int limit,
648               svn_boolean_t discover_changed_paths,
649               svn_boolean_t strict_node_history,
650               svn_boolean_t include_merged_revisions,
651               const apr_array_header_t *revprops,
652               svn_log_entry_receiver_t real_receiver,
653               void *real_receiver_baton,
654               svn_client_ctx_t *ctx,
655               apr_pool_t *scratch_pool)
656{
657  int i;
658  pre_15_receiver_baton_t rb = {0};
659  apr_pool_t *iterpool;
660  svn_boolean_t has_log_revprops;
661
662  SVN_ERR(svn_ra_has_capability(ra_session, &has_log_revprops,
663                                SVN_RA_CAPABILITY_LOG_REVPROPS,
664                                scratch_pool));
665
666  if (!has_log_revprops)
667    {
668      /* See above pre-1.5 notes. */
669      rb.ctx = ctx;
670
671      /* Create ra session on first use */
672      rb.ra_session_pool = scratch_pool;
673      rb.ra_session_url = actual_loc->url;
674    }
675
676  /* It's a bit complex to correctly handle the special revision words
677   * such as "BASE", "COMMITTED", and "PREV".  For example, if the
678   * user runs
679   *
680   *   $ svn log -rCOMMITTED foo.txt bar.c
681   *
682   * which committed rev should be used?  The younger of the two?  The
683   * first one?  Should we just error?
684   *
685   * None of the above, I think.  Rather, the committed rev of each
686   * target in turn should be used.  This is what most users would
687   * expect, and is the most useful interpretation.  Of course, this
688   * goes for the other dynamic (i.e., local) revision words too.
689   *
690   * Note that the code to do this is a bit more complex than a simple
691   * loop, because the user might run
692   *
693   *    $ svn log -rCOMMITTED:42 foo.txt bar.c
694   *
695   * in which case we want to avoid recomputing the static revision on
696   * every iteration.
697   *
698   * ### FIXME: However, we can't yet handle multiple wc targets anyway.
699   *
700   * We used to iterate over each target in turn, getting the logs for
701   * the named range.  This led to revisions being printed in strange
702   * order or being printed more than once.  This is issue 1550.
703   *
704   * In r851673, jpieper blocked multiple wc targets in svn/log-cmd.c,
705   * meaning this block not only doesn't work right in that case, but isn't
706   * even testable that way (svn has no unit test suite; we can only test
707   * via the svn command).  So, that check is now moved into this function
708   * (see above).
709   *
710   * kfogel ponders future enhancements in r844260:
711   * I think that's okay behavior, since the sense of the command is
712   * that one wants a particular range of logs for *this* file, then
713   * another range for *that* file, and so on.  But we should
714   * probably put some sort of separator header between the log
715   * groups.  Of course, libsvn_client can't just print stuff out --
716   * it has to take a callback from the client to do that.  So we
717   * need to define that callback interface, then have the command
718   * line client pass one down here.
719   *
720   * epg wonders if the repository could send a unified stream of log
721   * entries if the paths and revisions were passed down.
722   */
723  iterpool = svn_pool_create(scratch_pool);
724  for (i = 0; i < revision_ranges->nelts; i++)
725    {
726      const char *old_session_url;
727      const char *path = APR_ARRAY_IDX(targets, 0, const char *);
728      const char *local_abspath_or_url;
729      rev_range_t *range;
730      limit_receiver_baton_t lb;
731      svn_log_entry_receiver_t passed_receiver;
732      void *passed_receiver_baton;
733      const apr_array_header_t *passed_receiver_revprops;
734      svn_location_segment_t **matching_segment;
735      svn_revnum_t younger_rev;
736
737      svn_pool_clear(iterpool);
738
739      if (!svn_path_is_url(path))
740        SVN_ERR(svn_dirent_get_absolute(&local_abspath_or_url, path,
741                                        iterpool));
742      else
743        local_abspath_or_url = path;
744
745      range = APR_ARRAY_IDX(revision_ranges, i, rev_range_t *);
746
747      /* Issue #4355: Account for renames spanning requested
748         revision ranges. */
749      younger_rev = MAX(range->range_start, range->range_end);
750      matching_segment = bsearch(&younger_rev, log_segments->elts,
751                                 log_segments->nelts, log_segments->elt_size,
752                                 compare_rev_to_segment);
753      /* LOG_SEGMENTS is supposed to represent the history of PATHS from
754         the oldest to youngest revs in REVISION_RANGES.  This function's
755         current sole caller svn_client_log5 *should* be providing
756         LOG_SEGMENTS that span the oldest to youngest revs in
757         REVISION_RANGES, even if one or more of the svn_location_segment_t's
758         returned have NULL path members indicating a gap in the history. So
759         MATCHING_SEGMENT should never be NULL, but clearly sometimes it is,
760         see http://svn.haxx.se/dev/archive-2013-06/0522.shtml
761         So to be safe we handle that case. */
762      if (matching_segment == NULL)
763        continue;
764
765      /* A segment with a NULL path means there is gap in the history.
766         We'll just proceed and let svn_ra_get_log2 fail with a useful
767         error...*/
768      if ((*matching_segment)->path != NULL)
769        {
770          /* ...but if there is history, then we must account for issue
771             #4355 and make sure our RA session is pointing at the correct
772             location. */
773          const char *segment_url = svn_path_url_add_component2(
774            actual_loc->repos_root_url, (*matching_segment)->path,
775            scratch_pool);
776          SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url,
777                                                    ra_session,
778                                                    segment_url,
779                                                    scratch_pool));
780        }
781
782      if (has_log_revprops)
783        {
784          passed_receiver = real_receiver;
785          passed_receiver_baton = real_receiver_baton;
786          passed_receiver_revprops = revprops;
787        }
788      else
789        {
790          rb.revprops = revprops;
791          rb.receiver = real_receiver;
792          rb.baton = real_receiver_baton;
793
794          passed_receiver = pre_15_receiver;
795          passed_receiver_baton = &rb;
796          passed_receiver_revprops = svn_compat_log_revprops_in(iterpool);
797        }
798
799      if (limit && revision_ranges->nelts > 1)
800        {
801          lb.limit = limit;
802          lb.receiver = passed_receiver;
803          lb.baton = passed_receiver_baton;
804
805          passed_receiver = limit_receiver;
806          passed_receiver_baton = &lb;
807        }
808
809      SVN_ERR(svn_ra_get_log2(ra_session,
810                              paths,
811                              range->range_start,
812                              range->range_end,
813                              limit,
814                              discover_changed_paths,
815                              strict_node_history,
816                              include_merged_revisions,
817                              passed_receiver_revprops,
818                              passed_receiver,
819                              passed_receiver_baton,
820                              iterpool));
821
822      if (limit && revision_ranges->nelts > 1)
823        {
824          limit = lb.limit;
825          if (limit == 0)
826            {
827              return SVN_NO_ERROR;
828            }
829        }
830    }
831  svn_pool_destroy(iterpool);
832
833  return SVN_NO_ERROR;
834}
835
836/*** Public Interface. ***/
837
838svn_error_t *
839svn_client_log5(const apr_array_header_t *targets,
840                const svn_opt_revision_t *peg_revision,
841                const apr_array_header_t *opt_rev_ranges,
842                int limit,
843                svn_boolean_t discover_changed_paths,
844                svn_boolean_t strict_node_history,
845                svn_boolean_t include_merged_revisions,
846                const apr_array_header_t *revprops,
847                svn_log_entry_receiver_t real_receiver,
848                void *real_receiver_baton,
849                svn_client_ctx_t *ctx,
850                apr_pool_t *pool)
851{
852  svn_ra_session_t *ra_session;
853  const char *old_session_url;
854  const char *ra_target;
855  const char *path_or_url;
856  svn_opt_revision_t youngest_opt_rev;
857  svn_revnum_t youngest_rev;
858  svn_revnum_t oldest_rev;
859  svn_opt_revision_t peg_rev;
860  svn_client__pathrev_t *ra_session_loc;
861  svn_client__pathrev_t *actual_loc;
862  apr_array_header_t *log_segments;
863  apr_array_header_t *revision_ranges;
864  apr_array_header_t *relative_targets;
865
866  if (opt_rev_ranges->nelts == 0)
867    {
868      return svn_error_create
869        (SVN_ERR_CLIENT_BAD_REVISION, NULL,
870         _("Missing required revision specification"));
871    }
872
873  /* Make a copy of PEG_REVISION, we may need to change it to a
874     default value. */
875  peg_rev = *peg_revision;
876
877  SVN_ERR(resolve_log_targets(&relative_targets, &ra_target, &peg_rev,
878                              targets, ctx, pool, pool));
879
880  SVN_ERR(svn_client__ra_session_from_path2(&ra_session, &ra_session_loc,
881                                            ra_target, NULL, &peg_rev, &peg_rev,
882                                            ctx, pool));
883
884  /* Convert OPT_REV_RANGES to an array of rev_range_t and find the youngest
885     and oldest revision range that spans all of OPT_REV_RANGES. */
886  SVN_ERR(convert_opt_rev_array_to_rev_range_array(&revision_ranges,
887                                                   &youngest_rev,
888                                                   &oldest_rev,
889                                                   ra_session,
890                                                   ra_target,
891                                                   opt_rev_ranges, &peg_rev,
892                                                   ctx, pool,  pool));
893
894  /* For some peg revisions we must resolve revision and url via a local path
895     so use the original RA_TARGET. For others, use the potentially corrected
896     (redirected) ra session URL. */
897  if (peg_rev.kind == svn_opt_revision_previous ||
898      peg_rev.kind == svn_opt_revision_base ||
899      peg_rev.kind == svn_opt_revision_committed ||
900      peg_rev.kind == svn_opt_revision_working)
901    path_or_url = ra_target;
902  else
903    path_or_url = ra_session_loc->url;
904
905  /* Make ACTUAL_LOC and RA_SESSION point to the youngest operative rev. */
906  youngest_opt_rev.kind = svn_opt_revision_number;
907  youngest_opt_rev.value.number = youngest_rev;
908  SVN_ERR(svn_client__resolve_rev_and_url(&actual_loc, ra_session,
909                                          path_or_url, &peg_rev,
910                                          &youngest_opt_rev, ctx, pool));
911  SVN_ERR(svn_client__ensure_ra_session_url(&old_session_url, ra_session,
912                                            actual_loc->url, pool));
913
914  /* Save us an RA layer round trip if we are on the repository root and
915     know the result in advance, or if we don't need multiple ranges.
916     All the revision data has already been validated.
917   */
918  if (strcmp(actual_loc->url, actual_loc->repos_root_url) == 0
919      || opt_rev_ranges->nelts <= 1)
920    {
921      svn_location_segment_t *segment = apr_pcalloc(pool, sizeof(*segment));
922      log_segments = apr_array_make(pool, 1, sizeof(segment));
923
924      segment->range_start = oldest_rev;
925      segment->range_end = actual_loc->rev;
926      segment->path = svn_uri_skip_ancestor(actual_loc->repos_root_url,
927                                            actual_loc->url, pool);
928      APR_ARRAY_PUSH(log_segments, svn_location_segment_t *) = segment;
929    }
930  else
931    {
932      /* Get the svn_location_segment_t's representing the requested log
933       * ranges. */
934      SVN_ERR(svn_client__repos_location_segments(&log_segments, ra_session,
935                                                  actual_loc->url,
936                                                  actual_loc->rev, /* peg */
937                                                  actual_loc->rev, /* start */
938                                                  oldest_rev,      /* end */
939                                                  ctx, pool));
940    }
941
942
943  SVN_ERR(run_ra_get_log(revision_ranges, relative_targets, log_segments,
944                         actual_loc, ra_session, targets, limit,
945                         discover_changed_paths, strict_node_history,
946                         include_merged_revisions, revprops,
947                         real_receiver, real_receiver_baton, ctx, pool));
948
949  return SVN_NO_ERROR;
950}
951