2 * log.c: return log messages
4 * ====================================================================
5 * Copyright (c) 2000-2008 CollabNet. All rights reserved.
7 * This software is licensed as described in the file COPYING, which
8 * you should have received as part of this distribution. The terms
9 * are also available at http://subversion.tigris.org/license-1.html.
10 * If newer versions of this license are posted there, you may use a
11 * newer version instead, at your option.
13 * This software consists of voluntary contributions made by many
14 * individuals. For exact contribution history, see the revision
15 * history and logs, available at http://subversion.tigris.org/.
16 * ====================================================================
19 /* ==================================================================== */
25 #define APR_WANT_STRFUNC
28 #include <apr_strings.h>
29 #include <apr_pools.h>
33 #include "svn_pools.h"
34 #include "svn_client.h"
35 #include "svn_compat.h"
36 #include "svn_error.h"
38 #include "svn_sorts.h"
40 #include "svn_private_config.h"
41 #include "private/svn_wc_private.h"
44 /*** Getting misc. information ***/
46 /* A log callback conforming to the svn_log_entry_receiver_t
47 interface for obtaining the last revision of a node at a path and
48 storing it in *BATON (an svn_revnum_t). */
50 revnum_receiver(void *baton
,
51 svn_log_entry_t
*log_entry
,
54 if (SVN_IS_VALID_REVNUM(log_entry
->revision
))
55 *((svn_revnum_t
*) baton
) = log_entry
->revision
;
61 svn_client__oldest_rev_at_path(svn_revnum_t
*oldest_rev
,
62 svn_ra_session_t
*ra_session
,
67 apr_array_header_t
*rel_paths
= apr_array_make(pool
, 1, sizeof(rel_path
));
68 apr_array_header_t
*revprops
= apr_array_make(pool
, 0, sizeof(char *));
69 *oldest_rev
= SVN_INVALID_REVNUM
;
70 APR_ARRAY_PUSH(rel_paths
, const char *) = rel_path
;
72 /* Trace back in history to find the revision at which this node
73 was created (copied or added). */
74 return svn_ra_get_log2(ra_session
, rel_paths
, 1, rev
, 1, FALSE
, TRUE
,
75 FALSE
, revprops
, revnum_receiver
, oldest_rev
, pool
);
78 /* The baton for use with copyfrom_info_receiver(). */
81 const char *target_path
;
87 /* A log callback conforming to the svn_log_message_receiver_t
88 interface for obtaining the copy source of a node at a path and
89 storing it in *BATON (a struct copyfrom_info_t *).
90 Implements svn_log_entry_receiver_t. */
92 copyfrom_info_receiver(void *baton
,
93 svn_log_entry_t
*log_entry
,
96 copyfrom_info_t
*copyfrom_info
= baton
;
97 if (copyfrom_info
->path
)
98 /* The copy source has already been found. */
101 if (log_entry
->changed_paths
)
105 svn_log_changed_path_t
*changed_path
;
106 /* Sort paths into depth-first order. */
107 apr_array_header_t
*sorted_changed_paths
=
108 svn_sort__hash(log_entry
->changed_paths
,
109 svn_sort_compare_items_as_paths
, pool
);
111 for (i
= (sorted_changed_paths
->nelts
-1) ; i
>= 0 ; i
--)
113 svn_sort__item_t
*item
= &APR_ARRAY_IDX(sorted_changed_paths
, i
,
116 changed_path
= item
->value
;
118 /* Consider only the path we're interested in. */
119 if (changed_path
->copyfrom_path
&&
120 SVN_IS_VALID_REVNUM(changed_path
->copyfrom_rev
) &&
121 svn_path_is_ancestor(path
, copyfrom_info
->target_path
))
123 /* Copy source found! Determine path and note revision. */
124 if (strcmp(path
, copyfrom_info
->target_path
) == 0)
126 /* We have the details for a direct copy to
127 copyfrom_info->target_path. */
128 copyfrom_info
->path
=
129 apr_pstrdup(copyfrom_info
->pool
,
130 changed_path
->copyfrom_path
);
134 /* We have a parent of copyfrom_info->target_path. */
135 copyfrom_info
->path
=
136 apr_pstrcat(copyfrom_info
->pool
,
137 changed_path
->copyfrom_path
,
138 copyfrom_info
->target_path
+
141 copyfrom_info
->rev
= changed_path
->copyfrom_rev
;
150 svn_client__get_copy_source(const char *path_or_url
,
151 const svn_opt_revision_t
*revision
,
152 const char **copyfrom_path
,
153 svn_revnum_t
*copyfrom_rev
,
154 svn_client_ctx_t
*ctx
,
158 copyfrom_info_t copyfrom_info
= { NULL
, NULL
, SVN_INVALID_REVNUM
, pool
};
159 apr_array_header_t
*targets
= apr_array_make(pool
, 1, sizeof(path_or_url
));
160 apr_pool_t
*sesspool
= svn_pool_create(pool
);
161 svn_ra_session_t
*ra_session
;
165 SVN_ERR(svn_client__ra_session_from_path(&ra_session
, &at_rev
, &at_url
,
169 SVN_ERR(svn_client__path_relative_to_root(©from_info
.target_path
,
170 path_or_url
, NULL
, TRUE
,
171 ra_session
, NULL
, pool
));
172 APR_ARRAY_PUSH(targets
, const char *) = "";
174 /* Find the copy source. Trace back in history to find the revision
175 at which this node was created (copied or added). */
176 err
= svn_ra_get_log2(ra_session
, targets
, at_rev
, 1, 0, TRUE
,
178 apr_array_make(pool
, 0, sizeof(const char *)),
179 copyfrom_info_receiver
, ©from_info
, pool
);
181 svn_pool_destroy(sesspool
);
185 if (err
->apr_err
== SVN_ERR_FS_NOT_FOUND
||
186 err
->apr_err
== SVN_ERR_RA_DAV_REQUEST_FAILED
)
188 /* A locally-added but uncommitted versioned resource won't
189 exist in the repository. */
190 svn_error_clear(err
);
193 *copyfrom_path
= NULL
;
194 *copyfrom_rev
= SVN_INVALID_REVNUM
;
199 *copyfrom_path
= copyfrom_info
.path
;
200 *copyfrom_rev
= copyfrom_info
.rev
;
205 /* compatibility with pre-1.5 servers, which send only author/date/log
206 *revprops in log entries */
209 svn_client_ctx_t
*ctx
;
210 /* ra session for retrieving revprops from old servers */
211 svn_ra_session_t
*ra_session
;
212 /* caller's list of requested revprops, receiver, and baton */
213 const apr_array_header_t
*revprops
;
214 svn_log_entry_receiver_t receiver
;
216 } pre_15_receiver_baton_t
;
219 pre_15_receiver(void *baton
, svn_log_entry_t
*log_entry
, apr_pool_t
*pool
)
221 pre_15_receiver_baton_t
*rb
= baton
;
223 if (log_entry
->revision
== SVN_INVALID_REVNUM
)
224 return rb
->receiver(rb
->baton
, log_entry
, pool
);
226 /* If only some revprops are requested, get them one at a time on the
227 second ra connection. If all are requested, get them all with
228 svn_ra_rev_proplist. This avoids getting unrequested revprops (which
229 may be arbitrarily large), but means one round-trip per requested
230 revprop. epg isn't entirely sure which should be optimized for. */
234 svn_boolean_t want_author
, want_date
, want_log
;
235 want_author
= want_date
= want_log
= FALSE
;
236 for (i
= 0; i
< rb
->revprops
->nelts
; i
++)
238 const char *name
= APR_ARRAY_IDX(rb
->revprops
, i
, const char *);
241 /* If a standard revprop is requested, we know it is already in
242 log_entry->revprops if available. */
243 if (strcmp(name
, SVN_PROP_REVISION_AUTHOR
) == 0)
248 if (strcmp(name
, SVN_PROP_REVISION_DATE
) == 0)
253 if (strcmp(name
, SVN_PROP_REVISION_LOG
) == 0)
258 SVN_ERR(svn_ra_rev_prop(rb
->ra_session
, log_entry
->revision
,
259 name
, &value
, pool
));
260 if (log_entry
->revprops
== NULL
)
261 log_entry
->revprops
= apr_hash_make(pool
);
262 apr_hash_set(log_entry
->revprops
, (const void *)name
,
263 APR_HASH_KEY_STRING
, (const void *)value
);
265 if (log_entry
->revprops
)
267 /* Pre-1.5 servers send the standard revprops unconditionally;
268 clear those the caller doesn't want. */
270 apr_hash_set(log_entry
->revprops
, SVN_PROP_REVISION_AUTHOR
,
271 APR_HASH_KEY_STRING
, NULL
);
273 apr_hash_set(log_entry
->revprops
, SVN_PROP_REVISION_DATE
,
274 APR_HASH_KEY_STRING
, NULL
);
276 apr_hash_set(log_entry
->revprops
, SVN_PROP_REVISION_LOG
,
277 APR_HASH_KEY_STRING
, NULL
);
282 SVN_ERR(svn_ra_rev_proplist(rb
->ra_session
, log_entry
->revision
,
283 &log_entry
->revprops
, pool
));
286 return rb
->receiver(rb
->baton
, log_entry
, pool
);
290 /*** Public Interface. ***/
294 svn_client_log4(const apr_array_header_t
*targets
,
295 const svn_opt_revision_t
*peg_revision
,
296 const svn_opt_revision_t
*start
,
297 const svn_opt_revision_t
*end
,
299 svn_boolean_t discover_changed_paths
,
300 svn_boolean_t strict_node_history
,
301 svn_boolean_t include_merged_revisions
,
302 const apr_array_header_t
*revprops
,
303 svn_log_entry_receiver_t real_receiver
,
304 void *real_receiver_baton
,
305 svn_client_ctx_t
*ctx
,
308 svn_ra_session_t
*ra_session
;
309 const char *url_or_path
;
310 const char *actual_url
;
311 apr_array_header_t
*condensed_targets
;
312 svn_revnum_t ignored_revnum
;
313 svn_opt_revision_t session_opt_rev
;
314 const char *ra_target
;
316 if ((start
->kind
== svn_opt_revision_unspecified
)
317 || (end
->kind
== svn_opt_revision_unspecified
))
319 return svn_error_create
320 (SVN_ERR_CLIENT_BAD_REVISION
, NULL
,
321 _("Missing required revision specification"));
324 url_or_path
= APR_ARRAY_IDX(targets
, 0, const char *);
326 /* Use the passed URL, if there is one. */
327 if (svn_path_is_url(url_or_path
))
329 if (peg_revision
->kind
== svn_opt_revision_base
330 || peg_revision
->kind
== svn_opt_revision_committed
331 || peg_revision
->kind
== svn_opt_revision_previous
)
332 return svn_error_create
333 (SVN_ERR_CLIENT_BAD_REVISION
, NULL
,
334 _("Revision type requires a working copy path, not a URL"));
336 /* Initialize this array, since we'll be building it below */
337 condensed_targets
= apr_array_make(pool
, 1, sizeof(const char *));
339 /* The logic here is this: If we get passed one argument, we assume
340 it is the full URL to a file/dir we want log info for. If we get
341 a URL plus some paths, then we assume that the URL is the base,
342 and that the paths passed are relative to it. */
343 if (targets
->nelts
> 1)
347 /* We have some paths, let's use them. Start after the URL. */
348 for (i
= 1; i
< targets
->nelts
; i
++)
349 APR_ARRAY_PUSH(condensed_targets
, const char *) =
350 APR_ARRAY_IDX(targets
, i
, const char *);
354 /* If we have a single URL, then the session will be rooted at
355 it, so just send an empty string for the paths we are
357 APR_ARRAY_PUSH(condensed_targets
, const char *) = "";
362 svn_wc_adm_access_t
*adm_access
;
363 apr_array_header_t
*target_urls
;
364 apr_array_header_t
*real_targets
;
365 apr_pool_t
*iterpool
;
368 /* See FIXME about multiple wc targets, below. */
369 if (targets
->nelts
> 1)
370 return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE
, NULL
,
371 _("When specifying working copy paths, only "
372 "one target may be given"));
374 /* Get URLs for each target */
375 target_urls
= apr_array_make(pool
, 1, sizeof(const char *));
376 real_targets
= apr_array_make(pool
, 1, sizeof(const char *));
377 iterpool
= svn_pool_create(pool
);
378 for (i
= 0; i
< targets
->nelts
; i
++)
380 const svn_wc_entry_t
*entry
;
382 const char *target
= APR_ARRAY_IDX(targets
, i
, const char *);
384 svn_pool_clear(iterpool
);
385 SVN_ERR(svn_wc_adm_probe_open3(&adm_access
, NULL
, target
,
386 FALSE
, 0, ctx
->cancel_func
,
387 ctx
->cancel_baton
, iterpool
));
388 SVN_ERR(svn_wc__entry_versioned(&entry
, target
, adm_access
, FALSE
,
392 return svn_error_createf
393 (SVN_ERR_ENTRY_MISSING_URL
, NULL
,
394 _("Entry '%s' has no URL"),
395 svn_path_local_style(target
, pool
));
397 URL
= apr_pstrdup(pool
, entry
->url
);
398 SVN_ERR(svn_wc_adm_close(adm_access
));
399 APR_ARRAY_PUSH(target_urls
, const char *) = URL
;
400 APR_ARRAY_PUSH(real_targets
, const char *) = target
;
402 svn_pool_destroy(iterpool
);
404 /* if we have no valid target_urls, just exit. */
405 if (target_urls
->nelts
== 0)
408 /* Find the base URL and condensed targets relative to it. */
409 SVN_ERR(svn_path_condense_targets(&url_or_path
, &condensed_targets
,
410 target_urls
, TRUE
, pool
));
412 if (condensed_targets
->nelts
== 0)
413 APR_ARRAY_PUSH(condensed_targets
, const char *) = "";
415 /* 'targets' now becomes 'real_targets', which has bogus,
416 unversioned things removed from it. */
417 targets
= real_targets
;
420 /* Determine the revision to open the RA session to. */
421 if (start
->kind
== svn_opt_revision_number
&&
422 end
->kind
== svn_opt_revision_number
)
423 session_opt_rev
= (start
->value
.number
> end
->value
.number
?
425 else if (start
->kind
== svn_opt_revision_date
&&
426 end
->kind
== svn_opt_revision_date
)
427 session_opt_rev
= (start
->value
.date
> end
->value
.date
? *start
: *end
);
429 session_opt_rev
.kind
= svn_opt_revision_unspecified
;
432 /* If this is a revision type that requires access to the working copy,
433 * we use our initial target path to figure out where to root the RA
434 * session, otherwise we use our URL. */
435 if (peg_revision
->kind
== svn_opt_revision_base
436 || peg_revision
->kind
== svn_opt_revision_committed
437 || peg_revision
->kind
== svn_opt_revision_previous
438 || peg_revision
->kind
== svn_opt_revision_working
)
439 SVN_ERR(svn_path_condense_targets(&ra_target
, NULL
, targets
, TRUE
, pool
));
441 ra_target
= url_or_path
;
443 SVN_ERR(svn_client__ra_session_from_path(&ra_session
, &ignored_revnum
,
444 &actual_url
, ra_target
, NULL
,
445 peg_revision
, &session_opt_rev
,
449 /* It's a bit complex to correctly handle the special revision words
450 * such as "BASE", "COMMITTED", and "PREV". For example, if the
453 * $ svn log -rCOMMITTED foo.txt bar.c
455 * which committed rev should be used? The younger of the two? The
456 * first one? Should we just error?
458 * None of the above, I think. Rather, the committed rev of each
459 * target in turn should be used. This is what most users would
460 * expect, and is the most useful interpretation. Of course, this
461 * goes for the other dynamic (i.e., local) revision words too.
463 * Note that the code to do this is a bit more complex than a simple
464 * loop, because the user might run
466 * $ svn log -rCOMMITTED:42 foo.txt bar.c
468 * in which case we want to avoid recomputing the static revision on
471 * ### FIXME: However, we can't yet handle multiple wc targets anyway.
473 * We used to iterate over each target in turn, getting the logs for
474 * the named range. This led to revisions being printed in strange
475 * order or being printed more than once. This is issue 1550.
477 * In r11599, jpieper blocked multiple wc targets in svn/log-cmd.c,
478 * meaning this block not only doesn't work right in that case, but isn't
479 * even testable that way (svn has no unit test suite; we can only test
480 * via the svn command). So, that check is now moved into this function
483 * kfogel ponders future enhancements in r4186:
484 * I think that's okay behavior, since the sense of the command is
485 * that one wants a particular range of logs for *this* file, then
486 * another range for *that* file, and so on. But we should
487 * probably put some sort of separator header between the log
488 * groups. Of course, libsvn_client can't just print stuff out --
489 * it has to take a callback from the client to do that. So we
490 * need to define that callback interface, then have the command
491 * line client pass one down here.
493 * epg wonders if the repository could send a unified stream of log
494 * entries if the paths and revisions were passed down.
497 svn_revnum_t start_revnum
, end_revnum
, youngest_rev
= SVN_INVALID_REVNUM
;
498 const char *path
= APR_ARRAY_IDX(targets
, 0, const char *);
499 svn_boolean_t has_log_revprops
;
501 SVN_ERR(svn_client__get_revision_number
502 (&start_revnum
, &youngest_rev
, ra_session
, start
, path
, pool
));
503 SVN_ERR(svn_client__get_revision_number
504 (&end_revnum
, &youngest_rev
, ra_session
, end
, path
, pool
));
506 SVN_ERR(svn_ra_has_capability(ra_session
, &has_log_revprops
,
507 SVN_RA_CAPABILITY_LOG_REVPROPS
, pool
));
508 if (has_log_revprops
)
509 return svn_ra_get_log2(ra_session
,
514 discover_changed_paths
,
516 include_merged_revisions
,
523 /* See above pre-1.5 notes. */
524 pre_15_receiver_baton_t rb
;
526 SVN_ERR(svn_client_open_ra_session(&rb
.ra_session
, actual_url
,
528 rb
.revprops
= revprops
;
529 rb
.receiver
= real_receiver
;
530 rb
.baton
= real_receiver_baton
;
531 SVN_ERR(svn_client__ra_session_from_path(&ra_session
,
533 &actual_url
, ra_target
, NULL
,
537 return svn_ra_get_log2(ra_session
,
542 discover_changed_paths
,
544 include_merged_revisions
,
545 svn_compat_log_revprops_in(pool
),
554 svn_client_log3(const apr_array_header_t
*targets
,
555 const svn_opt_revision_t
*peg_revision
,
556 const svn_opt_revision_t
*start
,
557 const svn_opt_revision_t
*end
,
559 svn_boolean_t discover_changed_paths
,
560 svn_boolean_t strict_node_history
,
561 svn_log_message_receiver_t receiver
,
562 void *receiver_baton
,
563 svn_client_ctx_t
*ctx
,
566 svn_log_entry_receiver_t receiver2
;
567 void *receiver2_baton
;
569 svn_compat_wrap_log_receiver(&receiver2
, &receiver2_baton
,
570 receiver
, receiver_baton
,
573 return svn_client_log4(targets
, peg_revision
, start
, end
, limit
,
574 discover_changed_paths
, strict_node_history
, FALSE
,
575 svn_compat_log_revprops_in(pool
),
576 receiver2
, receiver2_baton
, ctx
, pool
);
580 svn_client_log2(const apr_array_header_t
*targets
,
581 const svn_opt_revision_t
*start
,
582 const svn_opt_revision_t
*end
,
584 svn_boolean_t discover_changed_paths
,
585 svn_boolean_t strict_node_history
,
586 svn_log_message_receiver_t receiver
,
587 void *receiver_baton
,
588 svn_client_ctx_t
*ctx
,
591 svn_opt_revision_t peg_revision
;
592 peg_revision
.kind
= svn_opt_revision_unspecified
;
593 return svn_client_log3(targets
, &peg_revision
, start
, end
, limit
,
594 discover_changed_paths
, strict_node_history
,
595 receiver
, receiver_baton
, ctx
, pool
);
599 svn_client_log(const apr_array_header_t
*targets
,
600 const svn_opt_revision_t
*start
,
601 const svn_opt_revision_t
*end
,
602 svn_boolean_t discover_changed_paths
,
603 svn_boolean_t strict_node_history
,
604 svn_log_message_receiver_t receiver
,
605 void *receiver_baton
,
606 svn_client_ctx_t
*ctx
,
609 svn_error_t
*err
= SVN_NO_ERROR
;
611 err
= svn_client_log2(targets
, start
, end
, 0, discover_changed_paths
,
612 strict_node_history
, receiver
, receiver_baton
, ctx
,
615 /* Special case: If there have been no commits, we'll get an error
616 * for requesting log of a revision higher than 0. But the
617 * default behavior of "svn log" is to give revisions HEAD through
618 * 1, on the assumption that HEAD >= 1.
620 * So if we got that error for that reason, and it looks like the
621 * user was just depending on the defaults (rather than explicitly
622 * requesting the log for revision 1), then we don't error. Instead
623 * we just invoke the receiver manually on a hand-constructed log
624 * message for revision 0.
626 * See also http://subversion.tigris.org/issues/show_bug.cgi?id=692.
628 if (err
&& (err
->apr_err
== SVN_ERR_FS_NO_SUCH_REVISION
)
629 && (start
->kind
== svn_opt_revision_head
)
630 && ((end
->kind
== svn_opt_revision_number
)
631 && (end
->value
.number
== 1)))
634 /* We don't need to check if HEAD is 0, because that must be the case,
635 * by logical deduction: The revision range specified is HEAD:1.
636 * HEAD cannot not exist, so the revision to which "no such revision"
637 * applies is 1. If revision 1 does not exist, then HEAD is 0.
638 * Hence, we deduce the repository is empty without needing access
639 * to further information. */
641 svn_error_clear(err
);
644 /* Log receivers are free to handle revision 0 specially... But
645 just in case some don't, we make up a message here. */
646 SVN_ERR(receiver(receiver_baton
,
647 NULL
, 0, "", "", _("No commits in repository"),