/*
* commit_util.c: Driver for the WC commit process.
*
* ====================================================================
* Copyright (c) 2000-2006 CollabNet. All rights reserved.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at http://subversion.tigris.org/license-1.html.
* If newer versions of this license are posted there, you may use a
* newer version instead, at your option.
*
* This software consists of voluntary contributions made by many
* individuals. For exact contribution history, see the revision
* history and logs, available at http://subversion.tigris.org/.
* ====================================================================
*/
/* ==================================================================== */
#include <string.h>
#include <apr_pools.h>
#include <apr_hash.h>
#include "client.h"
#include "svn_path.h"
#include "svn_types.h"
#include "svn_pools.h"
#include "svn_wc.h"
#include "svn_props.h"
#include "svn_md5.h"
#include <assert.h>
#include <stdlib.h> /* for qsort() */
#include "svn_private_config.h"
/*** Uncomment this to turn on commit driver debugging. ***/
/*
#define SVN_CLIENT_COMMIT_DEBUG
*/
/*** Harvesting Commit Candidates ***/
/* Add a new commit candidate (described by all parameters except
`COMMITTABLES') to the COMMITABLES hash. */
static void
add_committable(apr_hash_t *committables,
const char *path,
svn_node_kind_t kind,
const char *url,
svn_revnum_t revision,
const char *copyfrom_url,
svn_revnum_t copyfrom_rev,
apr_byte_t state_flags)
{
apr_pool_t *pool = apr_hash_pool_get(committables);
const char *repos_name = SVN_CLIENT__SINGLE_REPOS_NAME;
apr_array_header_t *array;
svn_client_commit_item2_t *new_item;
/* Sanity checks. */
assert(path && url);
/* ### todo: Get the canonical repository for this item, which will
be the real key for the COMMITTABLES hash, instead of the above
bogosity. */
array = apr_hash_get(committables, repos_name, APR_HASH_KEY_STRING);
/* E-gads! There is no array for this repository yet! Oh, no
problem, we'll just create (and add to the hash) one. */
if (array == NULL)
{
array = apr_array_make(pool, 1, sizeof(new_item));
apr_hash_set(committables, repos_name, APR_HASH_KEY_STRING, array);
}
/* Now update pointer values, ensuring that their allocations live
in POOL. */
new_item = apr_pcalloc(pool, sizeof(*new_item));
new_item->path = apr_pstrdup(pool, path);
new_item->kind = kind;
new_item->url = apr_pstrdup(pool, url);
new_item->revision = revision;
new_item->copyfrom_url = copyfrom_url
? apr_pstrdup(pool, copyfrom_url) : NULL;
new_item->copyfrom_rev = copyfrom_rev;
new_item->state_flags = state_flags;
new_item->wcprop_changes = apr_array_make(pool, 1, sizeof(svn_prop_t *));
/* Now, add the commit item to the array. */
APR_ARRAY_PUSH(array, svn_client_commit_item2_t *) = new_item;
}
static svn_error_t *
check_prop_mods(svn_boolean_t *props_changed,
svn_boolean_t *eol_prop_changed,
const char *path,
svn_wc_adm_access_t *adm_access,
apr_pool_t *pool)
{
apr_array_header_t *prop_mods;
int i;
*eol_prop_changed = *props_changed = FALSE;
SVN_ERR(svn_wc_props_modified_p(props_changed, path, adm_access, pool));
if (! *props_changed)
return SVN_NO_ERROR;
SVN_ERR(svn_wc_get_prop_diffs(&prop_mods, NULL, path, adm_access, pool));
for (i = 0; i < prop_mods->nelts; i++)
{
svn_prop_t *prop_mod = &APR_ARRAY_IDX(prop_mods, i, svn_prop_t);
if (strcmp(prop_mod->name, SVN_PROP_EOL_STYLE) == 0)
*eol_prop_changed = TRUE;
}
return SVN_NO_ERROR;
}
/* If there is a commit item for PATH in COMMITTABLES, return it, else
return NULL. Use POOL for temporary allocation only. */
static svn_client_commit_item2_t *
look_up_committable(apr_hash_t *committables,
const char *path,
apr_pool_t *pool)
{
apr_hash_index_t *hi;
for (hi = apr_hash_first(pool, committables); hi; hi = apr_hash_next(hi))
{
const void *key;
void *val;
apr_array_header_t *these_committables;
int i;
apr_hash_this(hi, &key, NULL, &val);
these_committables = val;
for (i = 0; i < these_committables->nelts; i++)
{
svn_client_commit_item2_t *this_committable
= APR_ARRAY_IDX(these_committables, i,
svn_client_commit_item2_t *);
if (strcmp(this_committable->path, path) == 0)
return this_committable;
}
}
return NULL;
}
/* This implements the svn_wc_entry_callbacks_t->found_entry interface. */
static svn_error_t *
add_lock_token(const char *path, const svn_wc_entry_t *entry,
void *walk_baton, apr_pool_t *pool)
{
apr_hash_t *lock_tokens = walk_baton;
apr_pool_t *token_pool = apr_hash_pool_get(lock_tokens);
/* I want every lock-token I can get my dirty hands on!
If this entry is switched, so what. We will send an irrelevant lock
token. */
if (entry->url && entry->lock_token)
apr_hash_set(lock_tokens, apr_pstrdup(token_pool, entry->url),
APR_HASH_KEY_STRING,
apr_pstrdup(token_pool, entry->lock_token));
return SVN_NO_ERROR;
}
/* Entry walker callback table to add lock tokens in an hierarchy. */
static svn_wc_entry_callbacks_t add_tokens_callbacks = {
add_lock_token
};
/* Recursively search for commit candidates in (and under) PATH (with
entry ENTRY and ancestry URL), and add those candidates to
COMMITTABLES. If in ADDS_ONLY modes, only new additions are
recognized. COPYFROM_URL is the default copyfrom-url for children
of copied directories. NONRECURSIVE indicates that this function
will not recurse into subdirectories of PATH when PATH is itself a
directory. Lock tokens of candidates will be added to LOCK_TOKENS, if
non-NULL. JUST_LOCKED indicates whether to treat non-modified items with
lock tokens as commit candidates.
If in COPY_MODE, treat the entry as if it is destined to be added
with history as URL, and add 'deleted' entries to COMMITTABLES as
items to delete in the copy destination.
If CTX->CANCEL_FUNC is non-null, call it with CTX->CANCEL_BATON to see
if the user has cancelled the operation. */
static svn_error_t *
harvest_committables(apr_hash_t *committables,
apr_hash_t *lock_tokens,
const char *path,
svn_wc_adm_access_t *adm_access,
const char *url,
const char *copyfrom_url,
const svn_wc_entry_t *entry,
const svn_wc_entry_t *parent_entry,
svn_boolean_t adds_only,
svn_boolean_t copy_mode,
svn_boolean_t nonrecursive,
svn_boolean_t just_locked,
svn_client_ctx_t *ctx,
apr_pool_t *pool)
{
apr_hash_t *entries = NULL;
svn_boolean_t text_mod = FALSE, prop_mod = FALSE;
apr_byte_t state_flags = 0;
svn_node_kind_t kind;
const char *p_path;
svn_boolean_t tc, pc;
const char *cf_url = NULL;
svn_revnum_t cf_rev = entry->copyfrom_rev;
const svn_string_t *propval;
svn_boolean_t is_special;
apr_pool_t *token_pool = (lock_tokens ? apr_hash_pool_get(lock_tokens)
: NULL);
/* Early out if the item is already marked as committable. */
if (look_up_committable(committables, path, pool))
return SVN_NO_ERROR;
assert(entry);
assert(url);
if (ctx->cancel_func)
SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
/* Make P_PATH the parent dir. */
p_path = svn_path_dirname(path, pool);
/* Return error on unknown path kinds. We check both the entry and
the node itself, since a path might have changed kind since its
entry was written. */
if ((entry->kind != svn_node_file) && (entry->kind != svn_node_dir))
return svn_error_createf
(SVN_ERR_NODE_UNKNOWN_KIND, NULL, _("Unknown entry kind for '%s'"),
svn_path_local_style(path, pool));
SVN_ERR(svn_io_check_special_path(path, &kind, &is_special, pool));
if ((kind != svn_node_file)
&& (kind != svn_node_dir)
&& (kind != svn_node_none))
{
return svn_error_createf
(SVN_ERR_NODE_UNKNOWN_KIND, NULL,
_("Unknown entry kind for '%s'"),
svn_path_local_style(path, pool));
}
/* Verify that the node's type has not changed before attempting to
commit. */
SVN_ERR(svn_wc_prop_get(&propval, SVN_PROP_SPECIAL, path, adm_access,
pool));
if ((((! propval) && (is_special))
#ifdef HAVE_SYMLINK
|| ((propval) && (! is_special))
#endif /* HAVE_SYMLINK */
) && (kind != svn_node_none))
{
return svn_error_createf
(SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
_("Entry '%s' has unexpectedly changed special status"),
svn_path_local_style(path, pool));
}
/* Get a fully populated entry for PATH if we can, and check for
conflicts. If this is a directory ... */
if (entry->kind == svn_node_dir)
{
/* ... then try to read its own entries file so we have a full
entry for it (we were going to have to do this eventually to
recurse anyway, so... ) */
svn_error_t *err;
const svn_wc_entry_t *e = NULL;
err = svn_wc_entries_read(&entries, adm_access, copy_mode, pool);
/* If we failed to get an entries hash for the directory, no
sweat. Cleanup and move along. */
if (err)
{
svn_error_clear(err);
entries = NULL;
}
/* If we got an entries hash, and the "this dir" entry is
present, override our current ENTRY with it, and check for
conflicts. */
if ((entries) && ((e = apr_hash_get(entries, SVN_WC_ENTRY_THIS_DIR,
APR_HASH_KEY_STRING))))
{
entry = e;
SVN_ERR(svn_wc_conflicted_p(&tc, &pc, path, entry, pool));
}
/* No new entry? Just check the parent's pointer for
conflicts. */
else
{
SVN_ERR(svn_wc_conflicted_p(&tc, &pc, p_path, entry, pool));
}
}
/* If this is not a directory, check for conflicts using the
parent's path. */
else
{
SVN_ERR(svn_wc_conflicted_p(&tc, &pc, p_path, entry, pool));
}
/* Bail now if any conflicts exist for the ENTRY. */
if (tc || pc)
return svn_error_createf(SVN_ERR_WC_FOUND_CONFLICT, NULL,
_("Aborting commit: '%s' remains in conflict"),
svn_path_local_style(path, pool));
/* If we have our own URL, and we're NOT in COPY_MODE, it wins over
the telescoping one(s). In COPY_MODE, URL will always be the
URL-to-be of the copied item. */
if ((entry->url) && (! copy_mode))
url = entry->url;
/* Check for the deletion case. Deletes occur only when not in
"adds-only mode". We use the SVN_CLIENT_COMMIT_ITEM_DELETE flag
to represent two slightly different conditions:
- The entry is marked as 'deleted'. When copying a mixed-rev wc,
we still need to send a delete for that entry, otherwise the
object will wrongly exist in the repository copy.
- The entry is scheduled for deletion or replacement, which case
we need to send a delete either way.
*/
if ((! adds_only)
&& ((entry->deleted && entry->schedule == svn_wc_schedule_normal)
|| (entry->schedule == svn_wc_schedule_delete)
|| (entry->schedule == svn_wc_schedule_replace)))
{
state_flags |= SVN_CLIENT_COMMIT_ITEM_DELETE;
}
/* Check for the trivial addition case. Adds can be explicit
(schedule == add) or implicit (schedule == replace ::= delete+add).
We also note whether or not this is an add with history here. */
if ((entry->schedule == svn_wc_schedule_add)
|| (entry->schedule == svn_wc_schedule_replace))
{
state_flags |= SVN_CLIENT_COMMIT_ITEM_ADD;
if (entry->copyfrom_url)
{
state_flags |= SVN_CLIENT_COMMIT_ITEM_IS_COPY;
cf_url = entry->copyfrom_url;
adds_only = FALSE;
}
else
{
adds_only = TRUE;
}
}
/* Check for the copied-subtree addition case. */
if ((entry->copied || copy_mode)
&& (! entry->deleted)
&& (entry->schedule == svn_wc_schedule_normal))
{
svn_revnum_t p_rev = entry->revision - 1; /* arbitrary non-equal value */
svn_boolean_t wc_root = FALSE;
/* If this is not a WC root then its parent's revision is
admissible for comparitive purposes. */
SVN_ERR(svn_wc_is_wc_root(&wc_root, path, adm_access, pool));
if (! wc_root)
{
if (parent_entry)
p_rev = parent_entry->revision;
}
else if (! copy_mode)
return svn_error_createf
(SVN_ERR_WC_CORRUPT, NULL,
_("Did not expect '%s' to be a working copy root"),
svn_path_local_style(path, pool));
/* If the ENTRY's revision differs from that of its parent, we
have to explicitly commit ENTRY as a copy. */
if (entry->revision != p_rev)
{
state_flags |= SVN_CLIENT_COMMIT_ITEM_ADD;
state_flags |= SVN_CLIENT_COMMIT_ITEM_IS_COPY;
adds_only = FALSE;
cf_rev = entry->revision;
if (copy_mode)
cf_url = entry->url;
else if (copyfrom_url)
cf_url = copyfrom_url;
else /* ### See issue #830 */
return svn_error_createf
(SVN_ERR_BAD_URL, NULL,
_("Commit item '%s' has copy flag but no copyfrom URL"),
svn_path_local_style(path, pool));
}
}
/* If an add is scheduled to occur, dig around for some more
information about it. */
if (state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
{
svn_boolean_t eol_prop_changed;
/* See if there are property modifications to send. */
SVN_ERR(check_prop_mods(&prop_mod, &eol_prop_changed, path,
adm_access, pool));
/* Regular adds of files have text mods, but for copies we have
to test for textual mods. Directories simply don't have text! */
if (entry->kind == svn_node_file)
{
/* Check for text mods. If EOL_PROP_CHANGED is TRUE, then
we need to force a translated byte-for-byte comparison
against the text-base so that a timestamp comparison
won't bail out early. Depending on how the svn:eol-style
prop was changed, we might have to send new text to the
server to match the new newline style. */
if (state_flags & SVN_CLIENT_COMMIT_ITEM_IS_COPY)
SVN_ERR(svn_wc_text_modified_p(&text_mod, path,
eol_prop_changed, adm_access,
pool));
else
text_mod = TRUE;
}
}
/* Else, if we aren't deleting this item, we'll have to look for
local text or property mods to determine if the path might be
committable. */
else if (! (state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE))
{
svn_boolean_t eol_prop_changed;
/* See if there are property modifications to send. */
SVN_ERR(check_prop_mods(&prop_mod, &eol_prop_changed, path,
adm_access, pool));
/* Check for text mods on files. If EOL_PROP_CHANGED is TRUE,
then we need to force a translated byte-for-byte comparison
against the text-base so that a timestamp comparison won't
bail out early. Depending on how the svn:eol-style prop was
changed, we might have to send new text to the server to
match the new newline style. */
if (entry->kind == svn_node_file)
SVN_ERR(svn_wc_text_modified_p(&text_mod, path, eol_prop_changed,
adm_access, pool));
}
/* Set text/prop modification flags accordingly. */
if (text_mod)
state_flags |= SVN_CLIENT_COMMIT_ITEM_TEXT_MODS;
if (prop_mod)
state_flags |= SVN_CLIENT_COMMIT_ITEM_PROP_MODS;
/* If the entry has a lock token and it is already a commit candidate,
or the caller wants unmodified locked items to be treated as
such, note this fact. */
if (entry->lock_token
&& (state_flags || just_locked))
state_flags |= SVN_CLIENT_COMMIT_ITEM_LOCK_TOKEN;
/* Now, if this is something to commit, add it to our list. */
if (state_flags)
{
/* Finally, add the committable item. */
add_committable(committables, path, entry->kind, url,
entry->revision,
cf_url,
cf_rev,
state_flags);
if (lock_tokens && entry->lock_token)
apr_hash_set(lock_tokens, apr_pstrdup(token_pool, url),
APR_HASH_KEY_STRING,
apr_pstrdup(token_pool, entry->lock_token));
}
/* For directories, recursively handle each of their entries (except
when the directory is being deleted, unless the deletion is part
of a replacement ... how confusing). Oh, and don't recurse at
all if this is a nonrecursive commit. ### We'll probably make
the whole 'nonrecursive' concept go away soon and be replaced
with the more sophisticated Depth0|Depth1|DepthInfinity. */
if (entries && (! nonrecursive)
&& ((! (state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE))
|| (state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)))
{
apr_hash_index_t *hi;
const svn_wc_entry_t *this_entry;
apr_pool_t *loop_pool = svn_pool_create(pool);
/* Loop over all other entries in this directory, skipping the
"this dir" entry. */
for (hi = apr_hash_first(pool, entries);
hi;
hi = apr_hash_next(hi))
{
const void *key;
void *val;
const char *name;
const char *full_path;
const char *used_url = NULL;
const char *name_uri = NULL;
const char *this_cf_url = cf_url ? cf_url : copyfrom_url;
svn_wc_adm_access_t *dir_access = adm_access;
svn_pool_clear(loop_pool);
/* Get the next entry. Name is an entry name; value is an
entry structure. */
apr_hash_this(hi, &key, NULL, &val);
name = key;
/* Skip "this dir" */
if (! strcmp(name, SVN_WC_ENTRY_THIS_DIR))
continue;
this_entry = val;
name_uri = svn_path_uri_encode(name, loop_pool);
full_path = svn_path_join(path, name, loop_pool);
if (this_cf_url)
this_cf_url = svn_path_join(this_cf_url, name_uri, loop_pool);
/* We'll use the entry's URL if it has one and if we aren't
in copy_mode, else, we'll just extend the parent's URL
with the entry's basename. */
if ((! this_entry->url) || (copy_mode))
used_url = svn_path_join(url, name_uri, loop_pool);
/* Recurse. */
if (this_entry->kind == svn_node_dir)
{
svn_error_t *lockerr;
lockerr = svn_wc_adm_retrieve(&dir_access, adm_access,
full_path, loop_pool);
if (lockerr)
{
if (lockerr->apr_err == SVN_ERR_WC_NOT_LOCKED)
{
/* A missing, schedule-delete child dir is
allowable. Just don't try to recurse. */
svn_node_kind_t childkind;
svn_error_t *err = svn_io_check_path(full_path,
&childkind,
loop_pool);
if (! err
&& childkind == svn_node_none
&& this_entry->schedule == svn_wc_schedule_delete)
{
add_committable(committables, full_path,
this_entry->kind, used_url,
SVN_INVALID_REVNUM,
NULL,
SVN_INVALID_REVNUM,
SVN_CLIENT_COMMIT_ITEM_DELETE);
svn_error_clear(lockerr);
continue; /* don't recurse! */
}
else
{
svn_error_clear(err);
return lockerr;
}
}
else
return lockerr;
}
}
else
dir_access = adm_access;
SVN_ERR(harvest_committables
(committables, lock_tokens, full_path, dir_access,
used_url ? used_url : this_entry->url,
this_cf_url,
this_entry,
entry,
adds_only,
copy_mode,
FALSE, just_locked,
ctx,
loop_pool));
}
svn_pool_destroy(loop_pool);
}
/* Fetch lock tokens for descendants of deleted directories. */
if (lock_tokens && entry->kind == svn_node_dir
&& (state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE))
{
SVN_ERR(svn_wc_walk_entries2(path, adm_access, &add_tokens_callbacks,
lock_tokens, FALSE, ctx->cancel_func,
ctx->cancel_baton, pool));
}
return SVN_NO_ERROR;
}
svn_error_t *
svn_client__harvest_committables(apr_hash_t **committables,
apr_hash_t **lock_tokens,
svn_wc_adm_access_t *parent_dir,
apr_array_header_t *targets,
svn_boolean_t nonrecursive,
svn_boolean_t just_locked,
svn_client_ctx_t *ctx,
apr_pool_t *pool)
{
int i = 0;
svn_wc_adm_access_t *dir_access;
apr_pool_t *subpool = svn_pool_create(pool);
/* It's possible that one of the named targets has a parent that is
* itself scheduled for addition or replacement -- that is, the
* parent is not yet versioned in the repository. This is okay, as
* long as the parent itself is part of this same commit, either
* directly, or by virtue of a grandparent, great-grandparent, etc,
* being part of the commit.
*
* Since we don't know what's included in the commit until we've
* harvested all the targets, we can't reliably check this as we
* go. So in `danglers', we record named targets whose parents
* are unversioned, then after harvesting the total commit group, we
* check to make sure those parents are included.
*
* Each key of danglers is an unversioned parent. The (const char *)
* value is one of that parent's children which is named as part of
* the commit; the child is included only to make a better error
* message.
*
* (The reason we don't bother to check unnamed -- i.e, implicit --
* targets is that they can only join the commit if their parents
* did too, so this situation can't arise for them.)
*/
apr_hash_t *danglers = apr_hash_make(pool);
/* Create the COMMITTABLES hash. */
*committables = apr_hash_make(pool);
/* And the LOCK_TOKENS dito. */
*lock_tokens = apr_hash_make(pool);
do
{
svn_wc_adm_access_t *adm_access;
const svn_wc_entry_t *entry;
const char *target;
svn_pool_clear(subpool);
/* Add the relative portion of our full path (if there are no
relative paths, TARGET will just be PARENT_DIR for a single
iteration. */
target = svn_path_join_many(subpool,
svn_wc_adm_access_path(parent_dir),
targets->nelts
? (((const char **) targets->elts)[i])
: NULL,
NULL);
/* No entry? This TARGET isn't even under version control! */
SVN_ERR(svn_wc_adm_probe_retrieve(&adm_access, parent_dir,
target, subpool));
SVN_ERR(svn_wc_entry(&entry, target, adm_access, FALSE, subpool));
if (! entry)
return svn_error_createf
(SVN_ERR_ENTRY_NOT_FOUND, NULL,
_("'%s' is not under version control"), target);
if (! entry->url)
return svn_error_createf(SVN_ERR_WC_CORRUPT, NULL,
_("Entry for '%s' has no URL"),
svn_path_local_style(target, pool));
/* We have to be especially careful around entries scheduled for
addition or replacement. */
if ((entry->schedule == svn_wc_schedule_add)
|| (entry->schedule == svn_wc_schedule_replace))
{
const char *parent, *base_name;
svn_wc_adm_access_t *parent_access;
const svn_wc_entry_t *p_entry = NULL;
svn_error_t *err;
svn_path_split(target, &parent, &base_name, subpool);
err = svn_wc_adm_retrieve(&parent_access, parent_dir,
parent, subpool);
if (err && err->apr_err == SVN_ERR_WC_NOT_LOCKED)
{
svn_error_clear(err);
SVN_ERR(svn_wc_adm_open3(&parent_access, NULL, parent,
FALSE, 0, ctx->cancel_func,
ctx->cancel_baton, subpool));
}
else if (err)
{
return err;
}
SVN_ERR(svn_wc_entry(&p_entry, parent, parent_access,
FALSE, subpool));
if (! p_entry)
return svn_error_createf
(SVN_ERR_WC_CORRUPT, NULL,
_("'%s' is scheduled for addition within unversioned parent"),
svn_path_local_style(target, pool));
if ((p_entry->schedule == svn_wc_schedule_add)
|| (p_entry->schedule == svn_wc_schedule_replace))
{
/* Copy the parent and target into pool; subpool
lasts only for this loop iteration, and we check
danglers after the loop is over. */
apr_hash_set(danglers, apr_pstrdup(pool, parent),
APR_HASH_KEY_STRING,
apr_pstrdup(pool, target));
}
}
/* If this entry is marked as 'copied' but scheduled normally, then
it should be the child of something else marked for addition with
history. */
if ((entry->copied) && (entry->schedule == svn_wc_schedule_normal))
return svn_error_createf
(SVN_ERR_ILLEGAL_TARGET, NULL,
_("Entry for '%s' is marked as 'copied' but is not itself scheduled"
"\nfor addition. Perhaps you're committing a target that is\n"
"inside an unversioned (or not-yet-versioned) directory?"),
svn_path_local_style(target, pool));
/* Handle our TARGET. */
SVN_ERR(svn_wc_adm_retrieve(&dir_access, parent_dir,
(entry->kind == svn_node_dir
? target
: svn_path_dirname(target, subpool)),
subpool));
SVN_ERR(harvest_committables(*committables, *lock_tokens, target, dir_access,
entry->url, NULL, entry, NULL, FALSE,
FALSE, nonrecursive, just_locked, ctx,
subpool));
i++;
}
while (i < targets->nelts);
/* Make sure that every path in danglers is part of the commit. */
{
apr_hash_index_t *hi;
for (hi = apr_hash_first(pool, danglers); hi; hi = apr_hash_next(hi))
{
const void *key;
void *val;
const char *dangling_parent, *dangling_child;
/* Get the next entry. Name is an entry name; value is an
entry structure. */
apr_hash_this(hi, &key, NULL, &val);
dangling_parent = key;
dangling_child = val;
if (! look_up_committable(*committables, dangling_parent, pool))
{
return svn_error_createf
(SVN_ERR_ILLEGAL_TARGET, NULL,
_("'%s' is not under version control "
"and is not part of the commit, "
"yet its child '%s' is part of the commit"),
/* Probably one or both of these is an entry, but
safest to local_stylize just in case. */
svn_path_local_style(dangling_parent, pool),
svn_path_local_style(dangling_child, pool));
}
}
}
svn_pool_destroy(subpool);
return SVN_NO_ERROR;
}
svn_error_t *
svn_client__get_copy_committables(apr_hash_t **committables,
const char *new_url,
const char *target,
svn_wc_adm_access_t *adm_access,
svn_client_ctx_t *ctx,
apr_pool_t *pool)
{
const svn_wc_entry_t *entry;
/* Create the COMMITTABLES hash. */
*committables = apr_hash_make(pool);
/* Read the entry for TARGET. */
SVN_ERR(svn_wc_entry(&entry, target, adm_access, FALSE, pool));
if (! entry)
return svn_error_createf
(SVN_ERR_ENTRY_NOT_FOUND, NULL, _("'%s' is not under version control"),
svn_path_local_style(target, pool));
/* Handle our TARGET. */
SVN_ERR(harvest_committables(*committables, NULL, target,
adm_access, new_url, entry->url, entry, NULL,
FALSE, TRUE, FALSE, FALSE, ctx, pool));
return SVN_NO_ERROR;
}
int svn_client__sort_commit_item_urls(const void *a, const void *b)
{
const svn_client_commit_item2_t *item1
= *((const svn_client_commit_item2_t * const *) a);
const svn_client_commit_item2_t *item2
= *((const svn_client_commit_item2_t * const *) b);
return svn_path_compare_paths(item1->url, item2->url);
}
svn_error_t *
svn_client__condense_commit_items(const char **base_url,
apr_array_header_t *commit_items,
apr_pool_t *pool)
{
apr_array_header_t *ci = commit_items; /* convenience */
const char *url;
svn_client_commit_item2_t *item, *last_item = NULL;
int i;
assert(ci && ci->nelts);
/* Sort our commit items by their URLs. */
qsort(ci->elts, ci->nelts,
ci->elt_size, svn_client__sort_commit_item_urls);
/* Loop through the URLs, finding the longest usable ancestor common
to all of them, and making sure there are no duplicate URLs. */
for (i = 0; i < ci->nelts; i++)
{
item = APR_ARRAY_IDX(ci, i, svn_client_commit_item2_t *);
url = item->url;
if ((last_item) && (strcmp(last_item->url, url) == 0))
return svn_error_createf
(SVN_ERR_CLIENT_DUPLICATE_COMMIT_URL, NULL,
_("Cannot commit both '%s' and '%s' as they refer to the same URL"),
svn_path_local_style(item->path, pool),
svn_path_local_style(last_item->path, pool));
/* In the first iteration, our BASE_URL is just our only
encountered commit URL to date. After that, we find the
longest ancestor between the current BASE_URL and the current
commit URL. */
if (i == 0)
*base_url = apr_pstrdup(pool, url);
else
*base_url = svn_path_get_longest_ancestor(*base_url, url, pool);
/* If our BASE_URL is itself a to-be-committed item, and it is
anything other than an already-versioned directory with
property mods, we'll call its parent directory URL the
BASE_URL. Why? Because we can't have a file URL as our base
-- period -- and all other directory operations (removal,
addition, etc.) require that we open that directory's parent
dir first. */
/* ### I don't understand the strlen()s here, hmmm. -kff */
if ((strlen(*base_url) == strlen(url))
&& (! ((item->kind == svn_node_dir)
&& item->state_flags == SVN_CLIENT_COMMIT_ITEM_PROP_MODS)))
*base_url = svn_path_dirname(*base_url, pool);
/* Stash our item here for the next iteration. */
last_item = item;
}
/* Now that we've settled on a *BASE_URL, go hack that base off
of all of our URLs. */
for (i = 0; i < ci->nelts; i++)
{
svn_client_commit_item2_t *this_item
= APR_ARRAY_IDX(ci, i, svn_client_commit_item2_t *);
int url_len = strlen(this_item->url);
int base_url_len = strlen(*base_url);
if (url_len > base_url_len)
this_item->url = apr_pstrdup(pool, this_item->url + base_url_len + 1);
else
this_item->url = "";
}
#ifdef SVN_CLIENT_COMMIT_DEBUG
/* ### TEMPORARY CODE ### */
fprintf(stderr, "COMMITTABLES: (base URL=%s)\n", *base_url);
fprintf(stderr, " FLAGS REV REL-URL (COPY-URL)\n");
for (i = 0; i < ci->nelts; i++)
{
svn_client_commit_item2_t *this_item
= APR_ARRAY_IDX(ci, i, svn_client_commit_item2_t *);
char flags[6];
flags[0] = (this_item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
? 'a' : '-';
flags[1] = (this_item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
? 'd' : '-';
flags[2] = (this_item->state_flags & SVN_CLIENT_COMMIT_ITEM_TEXT_MODS)
? 't' : '-';
flags[3] = (this_item->state_flags & SVN_CLIENT_COMMIT_ITEM_PROP_MODS)
? 'p' : '-';
flags[4] = (this_item->state_flags & SVN_CLIENT_COMMIT_ITEM_IS_COPY)
? 'c' : '-';
flags[5] = '\0';
fprintf(stderr, " %s %6ld '%s' (%s)\n",
flags,
this_item->revision,
this_item->url ? this_item->url : "",
this_item->copyfrom_url ? this_item->copyfrom_url : "none");
}
#endif /* SVN_CLIENT_COMMIT_DEBUG */
return SVN_NO_ERROR;
}
struct file_mod_t
{
svn_client_commit_item2_t *item;
void *file_baton;
};
/* A baton for use with the path-based editor driver */
struct path_driver_cb_baton
{
svn_wc_adm_access_t *adm_access; /* top-level access baton */
const svn_delta_editor_t *editor; /* commit editor */
void *edit_baton; /* commit editor's baton */
apr_hash_t *file_mods; /* hash: path->file_mod_t */
apr_hash_t *tempfiles; /* hash of tempfiles created */
const char *notify_path_prefix; /* notification path prefix */
svn_client_ctx_t *ctx; /* client context baton */
apr_hash_t *commit_items; /* the committables */
};
/* This implements svn_delta_path_driver_cb_func_t */
static svn_error_t *
do_item_commit(void **dir_baton,
void *parent_baton,
void *callback_baton,
const char *path,
apr_pool_t *pool)
{
struct path_driver_cb_baton *cb_baton = callback_baton;
svn_client_commit_item2_t *item = apr_hash_get(cb_baton->commit_items,
path, APR_HASH_KEY_STRING);
svn_node_kind_t kind = item->kind;
void *file_baton = NULL;
const char *copyfrom_url = NULL;
apr_pool_t *file_pool = NULL;
svn_wc_adm_access_t *adm_access = cb_baton->adm_access;
const svn_delta_editor_t *editor = cb_baton->editor;
apr_hash_t *file_mods = cb_baton->file_mods;
apr_hash_t *tempfiles = cb_baton->tempfiles;
const char *notify_path_prefix = cb_baton->notify_path_prefix;
svn_client_ctx_t *ctx = cb_baton->ctx;
/* Do some initializations. */
*dir_baton = NULL;
if (item->copyfrom_url)
copyfrom_url = item->copyfrom_url;
/* If this is a file with textual mods, we'll be keeping its baton
around until the end of the commit. So just lump its memory into
a single, big, all-the-file-batons-in-here pool. Otherwise, we
can just use POOL, and trust our caller to clean that mess up. */
if ((kind == svn_node_file)
&& (item->state_flags & SVN_CLIENT_COMMIT_ITEM_TEXT_MODS))
file_pool = apr_hash_pool_get(file_mods);
else
file_pool = pool;
/* Call the cancellation function. */
if (ctx->cancel_func)
SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
/* Validation. */
if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_IS_COPY)
{
if (! copyfrom_url)
return svn_error_createf
(SVN_ERR_BAD_URL, NULL,
_("Commit item '%s' has copy flag but no copyfrom URL"),
svn_path_local_style(path, pool));
if (! SVN_IS_VALID_REVNUM(item->copyfrom_rev))
return svn_error_createf
(SVN_ERR_CLIENT_BAD_REVISION, NULL,
_("Commit item '%s' has copy flag but an invalid revision"),
svn_path_local_style(path, pool));
}
/* If a feedback table was supplied by the application layer,
describe what we're about to do to this item. */
if (ctx->notify_func2)
{
/* Convert an absolute path into a relative one (if possible.) */
const char *npath = NULL;
svn_wc_notify_t *notify;
if (notify_path_prefix)
{
if (strcmp(notify_path_prefix, item->path))
npath = svn_path_is_child(notify_path_prefix, item->path, pool);
else
npath = ".";
}
if (! npath)
npath = item->path; /* Otherwise just use full path */
if ((item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
&& (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD))
{
/* We don't print the "(bin)" notice for binary files when
replacing, only when adding. So we don't bother to get
the mime-type here. */
notify = svn_wc_create_notify(npath, svn_wc_notify_commit_replaced,
pool);
}
else if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
{
notify = svn_wc_create_notify(npath, svn_wc_notify_commit_deleted,
pool);
}
else if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
{
notify = svn_wc_create_notify(npath, svn_wc_notify_commit_added,
pool);
if (item->kind == svn_node_file)
{
const svn_string_t *propval;
SVN_ERR(svn_wc_prop_get
(&propval, SVN_PROP_MIME_TYPE, item->path, adm_access,
pool));
if (propval)
notify->mime_type = propval->data;
}
}
else if ((item->state_flags & SVN_CLIENT_COMMIT_ITEM_TEXT_MODS)
|| (item->state_flags & SVN_CLIENT_COMMIT_ITEM_PROP_MODS))
{
notify = svn_wc_create_notify(npath, svn_wc_notify_commit_modified,
pool);
if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_TEXT_MODS)
notify->content_state = svn_wc_notify_state_changed;
else
notify->content_state = svn_wc_notify_state_unchanged;
if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_PROP_MODS)
notify->prop_state = svn_wc_notify_state_changed;
else
notify->prop_state = svn_wc_notify_state_unchanged;
}
else
notify = NULL;
if (notify)
{
notify->kind = item->kind;
(*ctx->notify_func2)(ctx->notify_baton2, notify, pool);
}
}
/* If this item is supposed to be deleted, do so. */
if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
{
assert(parent_baton);
SVN_ERR(editor->delete_entry(path, item->revision,
parent_baton, pool));
}
/* If this item is supposed to be added, do so. */
if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
{
if (kind == svn_node_file)
{
assert(parent_baton);
SVN_ERR(editor->add_file
(path, parent_baton, copyfrom_url,
copyfrom_url ? item->copyfrom_rev : SVN_INVALID_REVNUM,
file_pool, &file_baton));
}
else
{
assert(parent_baton);
SVN_ERR(editor->add_directory
(path, parent_baton, copyfrom_url,
copyfrom_url ? item->copyfrom_rev : SVN_INVALID_REVNUM,
pool, dir_baton));
}
}
/* Now handle property mods. */
if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_PROP_MODS)
{
const char *tempfile;
const svn_wc_entry_t *tmp_entry;
if (kind == svn_node_file)
{
if (! file_baton)
{
assert(parent_baton);
SVN_ERR(editor->open_file(path, parent_baton,
item->revision,
file_pool, &file_baton));
}
}
else
{
if (! *dir_baton)
{
if (! parent_baton)
{
SVN_ERR(editor->open_root
(cb_baton->edit_baton, item->revision,
pool, dir_baton));
}
else
{
SVN_ERR(editor->open_directory
(path, parent_baton, item->revision,
pool, dir_baton));
}
}
}
SVN_ERR(svn_wc_entry(&tmp_entry, item->path, adm_access, TRUE, pool));
SVN_ERR(svn_wc_transmit_prop_deltas
(item->path, adm_access, tmp_entry, editor,
(kind == svn_node_dir) ? *dir_baton : file_baton,
&tempfile, pool));
if (tempfile && tempfiles)
{
tempfile = apr_pstrdup(apr_hash_pool_get(tempfiles), tempfile);
apr_hash_set(tempfiles, tempfile, APR_HASH_KEY_STRING, (void *)1);
}
}
/* Finally, handle text mods (in that we need to open a file if it
hasn't already been opened, and we need to put the file baton in
our FILES hash). */
if ((kind == svn_node_file)
&& (item->state_flags & SVN_CLIENT_COMMIT_ITEM_TEXT_MODS))
{
struct file_mod_t *mod = apr_palloc(file_pool, sizeof(*mod));
if (! file_baton)
{
assert(parent_baton);
SVN_ERR(editor->open_file(path, parent_baton,
item->revision,
file_pool, &file_baton));
}
/* Add this file mod to the FILE_MODS hash. */
mod->item = item;
mod->file_baton = file_baton;
apr_hash_set(file_mods, item->url, APR_HASH_KEY_STRING, mod);
}
else if (file_baton)
{
/* Close any outstanding file batons that didn't get caught by
the "has local mods" conditional above. */
SVN_ERR(editor->close_file(file_baton, NULL, file_pool));
}
return SVN_NO_ERROR;
}
#ifdef SVN_CLIENT_COMMIT_DEBUG
/* Prototype for function below */
static svn_error_t *get_test_editor(const svn_delta_editor_t **editor,
void **edit_baton,
const svn_delta_editor_t *real_editor,
void *real_eb,
const char *base_url,
apr_pool_t *pool);
#endif /* SVN_CLIENT_COMMIT_DEBUG */
svn_error_t *
svn_client__do_commit(const char *base_url,
apr_array_header_t *commit_items,
svn_wc_adm_access_t *adm_access,
const svn_delta_editor_t *editor,
void *edit_baton,
const char *notify_path_prefix,
apr_hash_t **tempfiles,
apr_hash_t **digests,
svn_client_ctx_t *ctx,
apr_pool_t *pool)
{
apr_hash_t *file_mods = apr_hash_make(pool);
apr_hash_t *items_hash = apr_hash_make(pool);
apr_pool_t *subpool = svn_pool_create(pool);
apr_hash_index_t *hi;
int i;
struct path_driver_cb_baton cb_baton;
apr_array_header_t *paths =
apr_array_make(pool, commit_items->nelts, sizeof(const char *));
#ifdef SVN_CLIENT_COMMIT_DEBUG
{
SVN_ERR(get_test_editor(&editor, &edit_baton,
editor, edit_baton,
base_url, pool));
}
#endif /* SVN_CLIENT_COMMIT_DEBUG */
/* If the caller wants us to track temporary file creation, create a
hash to store those paths in. */
if (tempfiles)
*tempfiles = apr_hash_make(pool);
/* Ditto for the md5 digests. */
if (digests)
*digests = apr_hash_make(pool);
/* Build a hash from our COMMIT_ITEMS array, keyed on the
URI-decoded relative paths (which come from the item URLs). And
keep an array of those decoded paths, too. */
for (i = 0; i < commit_items->nelts; i++)
{
svn_client_commit_item2_t *item =
APR_ARRAY_IDX(commit_items, i, svn_client_commit_item2_t *);
const char *path = svn_path_uri_decode(item->url, pool);
apr_hash_set(items_hash, path, APR_HASH_KEY_STRING, item);
APR_ARRAY_PUSH(paths, const char *) = path;
}
/* Setup the callback baton. */
cb_baton.adm_access = adm_access;
cb_baton.editor = editor;
cb_baton.edit_baton = edit_baton;
cb_baton.file_mods = file_mods;
cb_baton.tempfiles = tempfiles ? *tempfiles : NULL;
cb_baton.notify_path_prefix = notify_path_prefix;
cb_baton.ctx = ctx;
cb_baton.commit_items = items_hash;
/* Drive the commit editor! */
SVN_ERR(svn_delta_path_driver(editor, edit_baton, SVN_INVALID_REVNUM,
paths, do_item_commit, &cb_baton, pool));
/* Transmit outstanding text deltas. */
for (hi = apr_hash_first(pool, file_mods); hi; hi = apr_hash_next(hi))
{
const void *key;
apr_ssize_t klen;
struct file_mod_t *mod;
svn_client_commit_item2_t *item;
void *val;
void *file_baton;
const char *tempfile, *dir_path;
unsigned char digest[APR_MD5_DIGESTSIZE];
svn_boolean_t fulltext = FALSE;
svn_wc_adm_access_t *item_access;
svn_pool_clear(subpool);
/* Get the next entry. */
apr_hash_this(hi, &key, &klen, &val);
mod = val;
/* Transmit the entry. */
item = mod->item;
file_baton = mod->file_baton;
if (ctx->cancel_func)
SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
if (ctx->notify_func2)
{
svn_wc_notify_t *notify;
const char *npath = NULL;
if (notify_path_prefix)
{
if (strcmp(notify_path_prefix, item->path) != 0)
npath = svn_path_is_child(notify_path_prefix, item->path,
subpool);
else
npath = ".";
}
if (! npath)
npath = item->path;
notify = svn_wc_create_notify(npath,
svn_wc_notify_commit_postfix_txdelta,
subpool);
notify->kind = svn_node_file;
(*ctx->notify_func2)(ctx->notify_baton2, notify, subpool);
}
if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
fulltext = TRUE;
dir_path = svn_path_dirname(item->path, subpool);
SVN_ERR(svn_wc_adm_retrieve(&item_access, adm_access, dir_path,
subpool));
SVN_ERR(svn_wc_transmit_text_deltas2(&tempfile, digest, item->path,
item_access, fulltext, editor,
file_baton, subpool));
if (tempfile && *tempfiles)
{
tempfile = apr_pstrdup(apr_hash_pool_get(*tempfiles), tempfile);
apr_hash_set(*tempfiles, tempfile, APR_HASH_KEY_STRING, (void *)1);
}
if (digests)
{
unsigned char *new_digest = apr_pmemdup(apr_hash_pool_get(*digests),
digest, APR_MD5_DIGESTSIZE);
apr_hash_set(*digests, item->path, APR_HASH_KEY_STRING, new_digest);
}
}
svn_pool_destroy(subpool);
/* Close the edit. */
SVN_ERR(editor->close_edit(edit_baton, pool));
return SVN_NO_ERROR;
}
/* Commit callback baton */
struct commit_baton {
svn_commit_info_t **info;
apr_pool_t *pool;
};
svn_error_t *svn_client__commit_get_baton(void **baton,
svn_commit_info_t **info,
apr_pool_t *pool)
{
struct commit_baton *cb = apr_pcalloc(pool, sizeof(*cb));
cb->info = info;
cb->pool = pool;
*baton = cb;
return SVN_NO_ERROR;
}
svn_error_t *svn_client__commit_callback(const svn_commit_info_t *commit_info,
void *baton,
apr_pool_t *pool)
{
struct commit_baton *cb = baton;
*(cb->info) = svn_commit_info_dup(commit_info, cb->pool);
return SVN_NO_ERROR;
}
#ifdef SVN_CLIENT_COMMIT_DEBUG
/*** Temporary test editor ***/
struct edit_baton
{
const char *path;
const svn_delta_editor_t *real_editor;
void *real_eb;
};
struct item_baton
{
struct edit_baton *eb;
void *real_baton;
const char *path;
};
static struct item_baton *
make_baton(struct edit_baton *eb,
void *real_baton,
const char *path,
apr_pool_t *pool)
{
struct item_baton *new_baton = apr_pcalloc(pool, sizeof(*new_baton));
new_baton->eb = eb;
new_baton->real_baton = real_baton;
new_baton->path = apr_pstrdup(pool, path);
return new_baton;
}
static svn_error_t *
set_target_revision(void *edit_baton,
svn_revnum_t target_revision,
apr_pool_t *pool)
{
struct edit_baton *eb = edit_baton;
return (*eb->real_editor->set_target_revision)(eb->real_eb,
target_revision,
pool);
}
static svn_error_t *
open_root(void *edit_baton,
svn_revnum_t base_revision,
apr_pool_t *dir_pool,
void **root_baton)
{
struct edit_baton *eb = edit_baton;
struct item_baton *new_baton = make_baton(eb, NULL, eb->path, dir_pool);
fprintf(stderr, "TEST EDIT STARTED (base URL=%s)\n", eb->path);
*root_baton = new_baton;
return (*eb->real_editor->open_root)(eb->real_eb,
base_revision,
dir_pool,
&new_baton->real_baton);
}
static svn_error_t *
add_file(const char *path,
void *parent_baton,
const char *copyfrom_path,
svn_revnum_t copyfrom_revision,
apr_pool_t *pool,
void **baton)
{
struct item_baton *db = parent_baton;
struct item_baton *new_baton = make_baton(db->eb, NULL, path, pool);
const char *copystuffs = "";
if (copyfrom_path && SVN_IS_VALID_REVNUM(copyfrom_revision))
copystuffs = apr_psprintf(pool,
" (copied from %s:%ld)",
copyfrom_path,
copyfrom_revision);
fprintf(stderr, " Adding : %s%s\n", path, copystuffs);
*baton = new_baton;
return (*db->eb->real_editor->add_file)(path, db->real_baton,
copyfrom_path, copyfrom_revision,
pool, &new_baton->real_baton);
}
static svn_error_t *
delete_entry(const char *path,
svn_revnum_t revision,
void *parent_baton,
apr_pool_t *pool)
{
struct item_baton *db = parent_baton;
fprintf(stderr, " Deleting: %s\n", path);
return (*db->eb->real_editor->delete_entry)(path, revision,
db->real_baton, pool);
}
static svn_error_t *
open_file(const char *path,
void *parent_baton,
svn_revnum_t base_revision,
apr_pool_t *pool,
void **baton)
{
struct item_baton *db = parent_baton;
struct item_baton *new_baton = make_baton(db->eb, NULL, path, pool);
fprintf(stderr, " Opening : %s\n", path);
*baton = new_baton;
return (*db->eb->real_editor->open_file)(path, db->real_baton,
base_revision, pool,
&new_baton->real_baton);
}
static svn_error_t *
close_file(void *baton, const char *text_checksum, apr_pool_t *pool)
{
struct item_baton *fb = baton;
fprintf(stderr, " Closing : %s\n", fb->path);
return (*fb->eb->real_editor->close_file)(fb->real_baton,
text_checksum, pool);
}
static svn_error_t *
change_file_prop(void *file_baton,
const char *name,
const svn_string_t *value,
apr_pool_t *pool)
{
struct item_baton *fb = file_baton;
fprintf(stderr, " PropSet (%s=%s)\n", name, value ? value->data : "");
return (*fb->eb->real_editor->change_file_prop)(fb->real_baton,
name, value, pool);
}
static svn_error_t *
apply_textdelta(void *file_baton,
const char *base_checksum,
apr_pool_t *pool,
svn_txdelta_window_handler_t *handler,
void **handler_baton)
{
struct item_baton *fb = file_baton;
fprintf(stderr, " Transmitting text...\n");
return (*fb->eb->real_editor->apply_textdelta)(fb->real_baton,
base_checksum, pool,
handler, handler_baton);
}
static svn_error_t *
close_edit(void *edit_baton, apr_pool_t *pool)
{
struct edit_baton *eb = edit_baton;
fprintf(stderr, "TEST EDIT COMPLETED\n");
return (*eb->real_editor->close_edit)(eb->real_eb, pool);
}
static svn_error_t *
add_directory(const char *path,
void *parent_baton,
const char *copyfrom_path,
svn_revnum_t copyfrom_revision,
apr_pool_t *pool,
void **baton)
{
struct item_baton *db = parent_baton;
struct item_baton *new_baton = make_baton(db->eb, NULL, path, pool);
const char *copystuffs = "";
if (copyfrom_path && SVN_IS_VALID_REVNUM(copyfrom_revision))
copystuffs = apr_psprintf(pool,
" (copied from %s:%ld)",
copyfrom_path,
copyfrom_revision);
fprintf(stderr, " Adding : %s%s\n", path, copystuffs);
*baton = new_baton;
return (*db->eb->real_editor->add_directory)(path,
db->real_baton,
copyfrom_path,
copyfrom_revision,
pool,
&new_baton->real_baton);
}
static svn_error_t *
open_directory(const char *path,
void *parent_baton,
svn_revnum_t base_revision,
apr_pool_t *pool,
void **baton)
{
struct item_baton *db = parent_baton;
struct item_baton *new_baton = make_baton(db->eb, NULL, path, pool);
fprintf(stderr, " Opening : %s\n", path);
*baton = new_baton;
return (*db->eb->real_editor->open_directory)(path, db->real_baton,
base_revision, pool,
&new_baton->real_baton);
}
static svn_error_t *
change_dir_prop(void *dir_baton,
const char *name,
const svn_string_t *value,
apr_pool_t *pool)
{
struct item_baton *db = dir_baton;
fprintf(stderr, " PropSet (%s=%s)\n", name, value ? value->data : "");
return (*db->eb->real_editor->change_dir_prop)(db->real_baton,
name, value, pool);
}
static svn_error_t *
close_directory(void *baton, apr_pool_t *pool)
{
struct item_baton *db = baton;
fprintf(stderr, " Closing : %s\n", db->path);
return (*db->eb->real_editor->close_directory)(db->real_baton, pool);
}
static svn_error_t *
abort_edit(void *edit_baton, apr_pool_t *pool)
{
struct edit_baton *eb = edit_baton;
fprintf(stderr, "TEST EDIT ABORTED\n");
return (*eb->real_editor->abort_edit)(eb->real_eb, pool);
}
static svn_error_t *
get_test_editor(const svn_delta_editor_t **editor,
void **edit_baton,
const svn_delta_editor_t *real_editor,
void *real_eb,
const char *base_url,
apr_pool_t *pool)
{
svn_delta_editor_t *ed = svn_delta_default_editor(pool);
struct edit_baton *eb = apr_pcalloc(pool, sizeof(*eb));
eb->path = apr_pstrdup(pool, base_url);
eb->real_editor = real_editor;
eb->real_eb = real_eb;
/* We don't implement absent_file() or absent_directory() in this
editor, because presumably commit would never send that. */
ed->set_target_revision = set_target_revision;
ed->open_root = open_root;
ed->add_directory = add_directory;
ed->open_directory = open_directory;
ed->close_directory = close_directory;
ed->add_file = add_file;
ed->open_file = open_file;
ed->close_file = close_file;
ed->delete_entry = delete_entry;
ed->apply_textdelta = apply_textdelta;
ed->change_dir_prop = change_dir_prop;
ed->change_file_prop = change_file_prop;
ed->close_edit = close_edit;
ed->abort_edit = abort_edit;
*editor = ed;
*edit_baton = eb;
return SVN_NO_ERROR;
}
#endif /* SVN_CLIENT_COMMIT_DEBUG */
svn_error_t * svn_client__get_log_msg(const char **log_msg,
const char **tmp_file,
const apr_array_header_t *commit_items,
svn_client_ctx_t *ctx,
apr_pool_t *pool)
{
/* client provided new callback function. simply forward call to him */
if (ctx->log_msg_func2)
return (*ctx->log_msg_func2)(log_msg, tmp_file, commit_items,
ctx->log_msg_baton2, pool);
/* client want use old (pre 1.3) API, therefore build
* svn_client_commit_item_t array */
if (ctx->log_msg_func)
{
int i;
svn_error_t * err;
svn_client_commit_item_t *old_item;
apr_pool_t * subpool = svn_pool_create(pool);
apr_array_header_t *old_commit_items
= apr_array_make(subpool, commit_items->nelts, sizeof(old_item));
for (i = 0; i < commit_items->nelts; i++)
{
svn_client_commit_item2_t *item =
APR_ARRAY_IDX(commit_items, i, svn_client_commit_item2_t *);
old_item = apr_pcalloc(subpool, sizeof(*old_item));
old_item->path = item->path;
old_item->kind = item->kind;
old_item->url = item->url;
/* pre 1.3 API use revision field for copyfrom_rev and revision
* depeding of copyfrom_url */
old_item->revision = item->copyfrom_url ?
item->copyfrom_rev : item->revision;
old_item->copyfrom_url = item->copyfrom_url;
old_item->state_flags = item->state_flags;
old_item->wcprop_changes = item->wcprop_changes;
APR_ARRAY_PUSH(old_commit_items, svn_client_commit_item_t *)
= old_item;
}
err = (*ctx->log_msg_func)(log_msg, tmp_file, old_commit_items,
ctx->log_msg_baton, pool);
svn_pool_destroy(subpool);
return err;
}
*log_msg = "";
*tmp_file = NULL;
return SVN_NO_ERROR;
}
syntax highlighted by Code2HTML, v. 0.9.1