Index: subversion/include/svn_client.h =================================================================== --- subversion/include/svn_client.h (revision 9243) +++ subversion/include/svn_client.h (working copy) @@ -1267,6 +1267,34 @@ apr_pool_t *pool); +/** Set @a *start_url and @a *start_revision (and maybe @a *end_url + * and @a *end_revision) to the revisions and repository URLs of one + * (or two) points of interest along a particular versioned resource's + * line of history. @a path as it exists in "peg revision" @a + * revision identifies that line of history, and @a start and @a end + * specify the point(s) of interest (typically the revisions referred + * to as the "operative range" for a given operation). + * + * @a end may be of kind svn_opt_revision_unspecified (in which case + * @a end_url and @a end_revision are not touched by the function); + * @a start and @a revision may not. + * + * @a ctx is the client context baton. + * + * Use @a pool for all allocations. + */ +svn_error_t * +svn_client_repos_locations (const char **start_url, + svn_opt_revision_t **start_revision, + const char **end_url, + svn_opt_revision_t **end_revision, + const char *path, + const svn_opt_revision_t *revision, + const svn_opt_revision_t *start, + const svn_opt_revision_t *end, + svn_client_ctx_t *ctx, + apr_pool_t *pool); + /* Converting paths to URLs. */ Index: subversion/libsvn_client/client.h =================================================================== --- subversion/libsvn_client/client.h (revision 9243) +++ subversion/libsvn_client/client.h (working copy) @@ -78,18 +78,33 @@ /* Return true if the revision number for REVISION can be determined - * from just the working copy, or false if it can be determined from - * just the repository. - * - * Note: No other kinds of revisions should be possible; but if one - * day there are, this will return true for those kinds. + from just the working copy, or false if it can be determined from + just the repository. + + NOTE: No other kinds of revisions should be possible; but if one + day there are, this will return true for those kinds. */ -svn_boolean_t +svn_boolean_t svn_client__revision_is_local (const svn_opt_revision_t *revision); +/* Given the CHANGED_PATHS and REVISION from an instance of a + svn_log_message_receiver_t function, determine at which location + PATH may be expected in the next log message, and set *PREV_PATH_P + to that value. KIND is the node kind of PATH. Perform all + allocations in POOL. + This is useful for tracking the various changes in location a + particular resource has undergone when performing an RA->get_logs() + operation on that resource. */ +svn_error_t *svn_client__prev_log_path (const char **prev_path_p, + apr_hash_t *changed_paths, + const char *path, + svn_node_kind_t kind, + svn_revnum_t revision, + apr_pool_t *pool); + /* ---------------------------------------------------------------- */ /*** RA callbacks ***/ Index: subversion/libsvn_client/ra.c =================================================================== --- subversion/libsvn_client/ra.c (revision 9243) +++ subversion/libsvn_client/ra.c (working copy) @@ -24,6 +24,7 @@ #include "svn_error.h" #include "svn_pools.h" #include "svn_string.h" +#include "svn_sorts.h" #include "svn_ra.h" #include "svn_wc.h" #include "svn_client.h" @@ -322,3 +323,317 @@ return SVN_NO_ERROR; } + + +struct log_message_baton +{ + /* The kind of the path we're tracing. */ + svn_node_kind_t kind; + + /* The path at which we are trying to find our versioned resource in + the log output. */ + const char *last_path; + + /* Input revisions and output paths; the whole point of this little game. */ + svn_revnum_t start_revision; + const char *start_path; + svn_revnum_t end_revision; + const char *end_path; + svn_revnum_t peg_revision; + const char *peg_path; + + /* Client context baton. */ + svn_client_ctx_t *ctx; + + /* A pool from which to allocate stuff stored in this baton. */ + apr_pool_t *pool; +}; + + +svn_error_t * +svn_client__prev_log_path (const char **prev_path_p, + apr_hash_t *changed_paths, + const char *path, + svn_node_kind_t kind, + svn_revnum_t revision, + apr_pool_t *pool) +{ + svn_log_changed_path_t *change; + const char *prev_path = NULL; + + /* If PATH was explicitly changed in this revision, that makes + things easy -- we keep the path (but check to see). If so, + we'll either use the path, or, if was copied, use its + copyfrom_path. */ + change = apr_hash_get (changed_paths, path, APR_HASH_KEY_STRING); + if (change) + { + if (change->copyfrom_path) + prev_path = apr_pstrdup (pool, change->copyfrom_path); + else + prev_path = path; + } + else if (apr_hash_count (changed_paths)) + { + /* The path was not explicitly changed in this revision. The + fact that we're hearing about this revision implies, then, + that the path was a child of some copied directory. We need + to find that directory, and effective "re-base" our path on + that directory's copyfrom_path. */ + int i; + apr_array_header_t *paths; + + /* Build a sorted list of the changed paths. */ + paths = svn_sort__hash (changed_paths, + svn_sort_compare_items_as_paths, pool); + + /* Now, walk the list of paths backwards, looking a parent of + our path that has copyfrom information. */ + for (i = paths->nelts; i > 0; i--) + { + svn_sort__item_t item = APR_ARRAY_IDX (paths, + i - 1, svn_sort__item_t); + const char *ch_path = item.key; + int len = strlen (ch_path); + + /* See if our path is the child of this change path. If + not, keep looking. */ + if (! ((strncmp (ch_path, path, len) == 0) && (path[len] == '/'))) + continue; + + /* Okay, our path *is* a child of this change path. If + this change was copied, we just need to apply the + portion of our path that is relative to this change's + path, to the change's copyfrom path. Otherwise, this + change isn't really interesting to us, and our search + continues. */ + change = apr_hash_get (changed_paths, ch_path, len); + if (change->copyfrom_path) + { + prev_path = svn_path_join (change->copyfrom_path, + path + len + 1, pool); + break; + } + } + } + + /* If we didn't find what we expected to find, return an error. + (Because directories bubble-up, we get a bunch of logs we might + not want. Be forgiving in that case.) */ + if (! prev_path) + { + if (kind == svn_node_dir) + prev_path = apr_pstrdup (pool, path); + else + return svn_error_createf (APR_EGENERAL, NULL, + "Missing changed-path information for " + "'%s' in revision %" SVN_REVNUM_T_FMT, + path, revision); + } + + *prev_path_p = prev_path; + return SVN_NO_ERROR; +} + + +/* Implements svn_log_message_receiver_t; helper for + svn_client_repos_locations. */ +static svn_error_t * +log_receiver (void *baton, + apr_hash_t *changed_paths, + svn_revnum_t revision, + const char *author, + const char *date, + const char *message, + apr_pool_t *pool) +{ + struct log_message_baton *lmb = baton; + const char *current_path = lmb->last_path; + const char *prev_path; + + /* See if the user is fed up with this time-consuming process yet. */ + if (lmb->ctx->cancel_func) + SVN_ERR (lmb->ctx->cancel_func (lmb->ctx->cancel_baton)); + + /* If we've already determined both our start and end paths, then + frankly, why are we here? Oh well, just do nothing. */ + if (lmb->start_path && lmb->end_path) + return SVN_NO_ERROR; + + /* Determine the paths for any of the revisions for which we haven't + gotten paths already. */ + if ((! lmb->start_path) && (revision <= lmb->start_revision)) + lmb->start_path = apr_pstrdup (lmb->pool, current_path); + if ((! lmb->end_path) && (revision <= lmb->end_revision)) + lmb->end_path = apr_pstrdup (lmb->pool, current_path); + if ((! lmb->peg_path) && (revision <= lmb->peg_revision)) + lmb->peg_path = apr_pstrdup (lmb->pool, current_path); + + /* Figure out at which repository path our object of interest lived + in the previous revision. */ + SVN_ERR (svn_client__prev_log_path (&prev_path, changed_paths, + current_path, lmb->kind, + revision, pool)); + + /* Squirrel away our "next place to look" path (suffer the strcmp + hit to save on allocations). */ + if (strcmp (prev_path, current_path) != 0) + lmb->last_path = apr_pstrdup (lmb->pool, prev_path); + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_client_repos_locations (const char **start_url, + svn_opt_revision_t **start_revision, + const char **end_url, + svn_opt_revision_t **end_revision, + const char *path, + const svn_opt_revision_t *revision, + const svn_opt_revision_t *start, + const svn_opt_revision_t *end, + svn_client_ctx_t *ctx, + apr_pool_t *pool) +{ + const char *repos_url; + struct log_message_baton lmb = { 0 }; + apr_array_header_t *targets; + svn_ra_plugin_t *ra_lib; + void *ra_baton, *session; + const char *url; + svn_revnum_t peg_revnum, start_revnum, end_revnum; + svn_boolean_t reverse = FALSE; + + /* Ensure that we are given some real revision data to work with. + (It's okay if the END is unspecified -- in that case, we'll just + set it to the same thing as START.) */ + if (revision->kind == svn_opt_revision_unspecified + || start->kind == svn_opt_revision_unspecified) + return svn_error_create (SVN_ERR_CLIENT_BAD_REVISION, NULL, NULL); + + /* Ensure that we have a repository URL. */ + SVN_ERR (svn_client_url_from_path (&url, path, pool)); + if (! url) + return svn_error_createf (SVN_ERR_ENTRY_MISSING_URL, NULL, + "'%s' has no URL", path); + + /* Initialize the RA subsystem, and resolve our abstract revision + objects into real revision numbers. */ + SVN_ERR (svn_ra_init_ra_libs (&ra_baton, pool)); + SVN_ERR (svn_ra_get_ra_library (&ra_lib, ra_baton, url, pool)); + SVN_ERR (svn_client__open_ra_session (&session, ra_lib, url, NULL, NULL, + NULL, FALSE, FALSE, ctx, pool)); + SVN_ERR (svn_client__get_revision_number (&peg_revnum, ra_lib, + session, revision, path, pool)); + SVN_ERR (svn_client__get_revision_number (&start_revnum, ra_lib, + session, start, path, pool)); + if (end->kind == svn_opt_revision_unspecified) + end_revnum = start_revnum; + else + SVN_ERR (svn_client__get_revision_number (&end_revnum, ra_lib, + session, end, path, pool)); + + /* Get the path's kind (and bail if it doesn't exist). */ + SVN_ERR (ra_lib->check_path (session, "", peg_revnum, &(lmb.kind), pool)); + if (lmb.kind == svn_node_none) + return svn_error_createf + (SVN_ERR_FS_NOT_FOUND, NULL, + "Unable to find '%s' in peg revision %" SVN_REVNUM_T_FMT, + path, peg_revnum); + + /* If the end revision number is smaller than the start one, we'll + remember to swap the orders of our input and output. */ + if (end_revnum < start_revnum) + { + svn_revnum_t tmp_revnum = start_revnum; + start_revnum = end_revnum; + end_revnum = tmp_revnum; + reverse = TRUE; + } + + /* Populate our log message baton structure. */ + SVN_ERR (ra_lib->get_repos_root (session, &repos_url, pool)); + lmb.last_path = url + strlen (repos_url); + lmb.start_revision = start_revnum; + lmb.end_revision = end_revnum; + lmb.ctx = ctx; + lmb.pool = pool; + + /* If the peg revision is at least as big as our ending revision, we + don't need to search for a path in that peg revision. */ + if (peg_revnum >= end_revnum) + lmb.peg_path = lmb.last_path; + + /* Build a one-item TARGETS array. */ + targets = apr_array_make (pool, 1, sizeof (const char *)); + APR_ARRAY_PUSH (targets, const char *) = ""; + + /* Let the RA layer drive our log information handler, which will do + the work of finding the actual locations for our resource. */ + SVN_ERR (ra_lib->get_log (session, targets, peg_revnum, 1, + TRUE, FALSE, log_receiver, &lmb, pool)); + + /* We'd better have all the paths we were looking for! */ + if (! lmb.start_path) + return svn_error_createf + (APR_EGENERAL, NULL, + "Unable to find repository location for '%s' in revision %" + SVN_REVNUM_T_FMT, path, start_revnum); + if (! lmb.end_path) + return svn_error_createf + (APR_EGENERAL, NULL, + "Unable to find repository location for '%s' in revision %" + SVN_REVNUM_T_FMT, path, end_revnum); + if (! lmb.peg_path) + return svn_error_createf + (APR_EGENERAL, NULL, + "Unable to find repository locations for '%s' in peg revision %" + SVN_REVNUM_T_FMT, path, peg_revnum); + + /* Repository paths might be absolute, but we want to treat them as + relative. */ + if (lmb.start_path[0] == '/') + lmb.start_path = lmb.start_path + 1; + if (lmb.end_path[0] == '/') + lmb.end_path = lmb.end_path + 1; + if (lmb.peg_path[0] == '/') + lmb.peg_path = lmb.peg_path + 1; + + /* If our peg revision was smaller than either of our range + revisions, we need to make sure that our calculated peg path is + the same as what we expected it to be. */ + if (strcmp (url, svn_path_join (repos_url, lmb.peg_path, pool)) != 0) + return svn_error_createf + (APR_EGENERAL, NULL, + "Unable to reliably guess at the line of history you wanted " + "(tried '%s' in revision %" SVN_REVNUM_T_FMT ")", + path, (peg_revnum < end_revnum) ? end_revnum : peg_revnum); + + /* If we reversed out input, we need to reverse out output, too. */ + if (reverse) + { + const char *tmp_path = lmb.start_path; + svn_revnum_t tmp_revision = lmb.start_revision; + lmb.start_path = lmb.end_path; + lmb.start_revision = lmb.end_revision; + lmb.end_path = tmp_path; + lmb.end_revision = tmp_revision; + } + + /* Set our return variables (remembering to reverse them if we had + to reverse our input). */ + *start_url = svn_path_join (repos_url, lmb.start_path, pool); + *start_revision = apr_pcalloc (pool, sizeof (*start_revision)); + (*start_revision)->kind = svn_opt_revision_number; + (*start_revision)->value.number = lmb.start_revision; + if (end->kind != svn_opt_revision_unspecified) + { + *end_url = svn_path_join (repos_url, lmb.end_path, pool); + *end_revision = apr_pcalloc (pool, sizeof (*end_revision)); + (*end_revision)->kind = svn_opt_revision_number; + (*end_revision)->value.number = lmb.end_revision; + } + + return SVN_NO_ERROR; +} Index: subversion/libsvn_client/cat.c =================================================================== --- subversion/libsvn_client/cat.c (revision 9243) +++ subversion/libsvn_client/cat.c (working copy) @@ -49,13 +49,42 @@ svn_string_t *keywords; apr_hash_t *props; const char *url; + svn_opt_revision_t *good_rev; - SVN_ERR (svn_client_url_from_path (&url, path_or_url, pool)); - if (! url) - return svn_error_createf (SVN_ERR_ENTRY_MISSING_URL, NULL, - "'%s' has no URL", path_or_url); + if (svn_path_is_url (path_or_url)) + { + *good_rev = *revision; + url = path_or_url; + } + else + { + /* For a working copy path, don't just use the URL stored in the + entries file. Run the history function to get the object's + original URL in REVISION, it case it was renamed since then. */ + svn_opt_revision_t base_rev, dead_end_rev, start_rev, *ignored_rev; + const char *ignored_url; + dead_end_rev.kind = svn_opt_revision_unspecified; + base_rev.kind = svn_opt_revision_base; + + if (revision->kind == svn_opt_revision_unspecified) + start_rev.kind = svn_opt_revision_base; + else + start_rev = *revision; + + SVN_ERR (svn_client_repos_locations (&url, &good_rev, + &ignored_url, &ignored_rev, + /* peg coords are path@BASE: */ + path_or_url, &base_rev, + /* search range: */ + &start_rev, &dead_end_rev, + ctx, pool)); + } + + /* From here out, 'url' and 'good_rev' are the coordinates to + we actually need to fetch. */ + /* Get the RA library that handles URL. */ SVN_ERR (svn_ra_init_ra_libs (&ra_baton, pool)); SVN_ERR (svn_ra_get_ra_library (&ra_lib, ra_baton, url, pool)); @@ -65,9 +94,9 @@ NULL, FALSE, FALSE, ctx, pool)); - /* Resolve REVISION into a real revnum. */ + /* Resolve good_rev into a real revnum. */ SVN_ERR (svn_client__get_revision_number (&rev, ra_lib, session, - revision, path_or_url, pool)); + good_rev, path_or_url, pool)); if (! SVN_IS_VALID_REVNUM (rev)) SVN_ERR (ra_lib->get_latest_revnum (session, &rev, pool));