/*
* commit_util.c: Driver for the WC commit process.
*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*/
/* ==================================================================== */
#include <string.h>
#include <apr_pools.h>
#include <apr_hash.h>
#include <apr_md5.h>
#include "client.h"
#include "svn_dirent_uri.h"
#include "svn_path.h"
#include "svn_types.h"
#include "svn_pools.h"
#include "svn_props.h"
#include "svn_iter.h"
#include "svn_hash.h"
#include <assert.h>
#include <stdlib.h> /* for qsort() */
#include "svn_private_config.h"
#include "private/svn_wc_private.h"
/*** Uncomment this to turn on commit driver debugging. ***/
/*
#define SVN_CLIENT_COMMIT_DEBUG
*/
/* Wrap an RA error in a nicer error if one is available. */
static svn_error_t *
fixup_commit_error(const char *local_abspath,
const char *base_url,
const char *path,
svn_node_kind_t kind,
svn_error_t *err,
svn_client_ctx_t *ctx,
apr_pool_t *scratch_pool)
{
if (err->apr_err == SVN_ERR_FS_NOT_FOUND
|| err->apr_err == SVN_ERR_FS_ALREADY_EXISTS
|| err->apr_err == SVN_ERR_FS_TXN_OUT_OF_DATE
|| err->apr_err == SVN_ERR_RA_DAV_PATH_NOT_FOUND
|| err->apr_err == SVN_ERR_RA_DAV_ALREADY_EXISTS
|| svn_error_find_cause(err, SVN_ERR_RA_OUT_OF_DATE))
{
if (ctx->notify_func2)
{
svn_wc_notify_t *notify;
if (local_abspath)
notify = svn_wc_create_notify(local_abspath,
svn_wc_notify_failed_out_of_date,
scratch_pool);
else
notify = svn_wc_create_notify_url(
svn_path_url_add_component2(base_url, path,
scratch_pool),
svn_wc_notify_failed_out_of_date,
scratch_pool);
notify->kind = kind;
notify->err = err;
ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
}
return svn_error_createf(SVN_ERR_WC_NOT_UP_TO_DATE, err,
(kind == svn_node_dir
? _("Directory '%s' is out of date")
: _("File '%s' is out of date")),
local_abspath
? svn_dirent_local_style(local_abspath,
scratch_pool)
: svn_path_url_add_component2(base_url,
path,
scratch_pool));
}
else if (svn_error_find_cause(err, SVN_ERR_FS_NO_LOCK_TOKEN)
|| err->apr_err == SVN_ERR_FS_LOCK_OWNER_MISMATCH
|| err->apr_err == SVN_ERR_RA_NOT_LOCKED)
{
if (ctx->notify_func2)
{
svn_wc_notify_t *notify;
if (local_abspath)
notify = svn_wc_create_notify(local_abspath,
svn_wc_notify_failed_locked,
scratch_pool);
else
notify = svn_wc_create_notify_url(
svn_path_url_add_component2(base_url, path,
scratch_pool),
svn_wc_notify_failed_locked,
scratch_pool);
notify->kind = kind;
notify->err = err;
ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
}
return svn_error_createf(SVN_ERR_CLIENT_NO_LOCK_TOKEN, err,
(kind == svn_node_dir
? _("Directory '%s' is locked in another working copy")
: _("File '%s' is locked in another working copy")),
local_abspath
? svn_dirent_local_style(local_abspath,
scratch_pool)
: svn_path_url_add_component2(base_url,
path,
scratch_pool));
}
else if (svn_error_find_cause(err, SVN_ERR_RA_DAV_FORBIDDEN)
|| err->apr_err == SVN_ERR_AUTHZ_UNWRITABLE)
{
if (ctx->notify_func2)
{
svn_wc_notify_t *notify;
if (local_abspath)
notify = svn_wc_create_notify(
local_abspath,
svn_wc_notify_failed_forbidden_by_server,
scratch_pool);
else
notify = svn_wc_create_notify_url(
svn_path_url_add_component2(base_url, path,
scratch_pool),
svn_wc_notify_failed_forbidden_by_server,
scratch_pool);
notify->kind = kind;
notify->err = err;
ctx->notify_func2(ctx->notify_baton2, notify, scratch_pool);
}
return svn_error_createf(SVN_ERR_CLIENT_FORBIDDEN_BY_SERVER, err,
(kind == svn_node_dir
? _("Changing directory '%s' is forbidden by the server")
: _("Changing file '%s' is forbidden by the server")),
local_abspath
? svn_dirent_local_style(local_abspath,
scratch_pool)
: svn_path_url_add_component2(base_url,
path,
scratch_pool));
}
else
return err;
}
/*** Harvesting Commit Candidates ***/
/* Add a new commit candidate (described by all parameters except
`COMMITTABLES') to the COMMITTABLES hash. All of the commit item's
members are allocated out of RESULT_POOL. */
static svn_error_t *
add_committable(svn_client__committables_t *committables,
const char *local_abspath,
svn_node_kind_t kind,
const char *repos_root_url,
const char *repos_relpath,
svn_revnum_t revision,
const char *copyfrom_relpath,
svn_revnum_t copyfrom_rev,
apr_byte_t state_flags,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
apr_array_header_t *array;
svn_client_commit_item3_t *new_item;
/* Sanity checks. */
SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
SVN_ERR_ASSERT(repos_root_url && repos_relpath);
/* ### 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->by_repository,
repos_root_url,
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(result_pool, 1, sizeof(new_item));
apr_hash_set(committables->by_repository,
apr_pstrdup(result_pool, repos_root_url),
APR_HASH_KEY_STRING, array);
}
/* Now update pointer values, ensuring that their allocations live
in POOL. */
new_item = svn_client_commit_item3_create(result_pool);
new_item->path = apr_pstrdup(result_pool, local_abspath);
new_item->kind = kind;
new_item->url = svn_path_url_add_component2(repos_root_url,
repos_relpath,
result_pool);
new_item->revision = revision;
new_item->copyfrom_url = copyfrom_relpath
? svn_path_url_add_component2(repos_root_url,
copyfrom_relpath,
result_pool)
: NULL;
new_item->copyfrom_rev = copyfrom_rev;
new_item->state_flags = state_flags;
new_item->incoming_prop_changes = apr_array_make(result_pool, 1,
sizeof(svn_prop_t *));
/* Now, add the commit item to the array. */
APR_ARRAY_PUSH(array, svn_client_commit_item3_t *) = new_item;
/* ... and to the hash. */
apr_hash_set(committables->by_path,
new_item->path,
APR_HASH_KEY_STRING,
new_item);
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_item3_t *
look_up_committable(svn_client__committables_t *committables,
const char *path,
apr_pool_t *pool)
{
return (svn_client_commit_item3_t *)
apr_hash_get(committables->by_path, path, APR_HASH_KEY_STRING);
}
/* Helper for harvest_committables().
* If ENTRY is a dir, return an SVN_ERR_WC_FOUND_CONFLICT error when
* encountering a tree-conflicted immediate child node. However, do
* not consider immediate children that are outside the bounds of DEPTH.
*
* TODO ### WC_CTX and LOCAL_ABSPATH ...
* ENTRY, DEPTH, CHANGELISTS and POOL are the same ones
* originally received by harvest_committables().
*
* Tree-conflicts information is stored in the victim's immediate parent.
* In some cases of an absent tree-conflicted victim, the tree-conflict
* information in its parent dir is the only indication that the node
* is under version control. This function is necessary for this
* particular case. In all other cases, this simply bails out a little
* bit earlier. */
static svn_error_t *
bail_on_tree_conflicted_children(svn_wc_context_t *wc_ctx,
const char *local_abspath,
svn_node_kind_t kind,
svn_depth_t depth,
apr_hash_t *changelists,
svn_wc_notify_func2_t notify_func,
void *notify_baton,
apr_pool_t *pool)
{
apr_hash_t *conflicts;
apr_hash_index_t *hi;
if ((depth == svn_depth_empty)
|| (kind != svn_node_dir))
/* There can't possibly be tree-conflicts information here. */
return SVN_NO_ERROR;
SVN_ERR(svn_wc__get_all_tree_conflicts(&conflicts, wc_ctx, local_abspath,
pool, pool));
if (!conflicts)
return SVN_NO_ERROR;
for (hi = apr_hash_first(pool, conflicts); hi; hi = apr_hash_next(hi))
{
const svn_wc_conflict_description2_t *conflict =
svn__apr_hash_index_val(hi);
if ((conflict->node_kind == svn_node_dir) &&
(depth == svn_depth_files))
continue;
/* So we've encountered a conflict that is included in DEPTH.
Bail out. But if there are CHANGELISTS, avoid bailing out
on an item that doesn't match the CHANGELISTS. */
if (!svn_wc__changelist_match(wc_ctx, local_abspath, changelists, pool))
continue;
/* At this point, a conflict was found, and either there were no
changelists, or the changelists matched. Bail out already! */
if (notify_func != NULL)
{
notify_func(notify_baton,
svn_wc_create_notify(local_abspath,
svn_wc_notify_failed_conflict,
pool),
pool);
}
return svn_error_createf(
SVN_ERR_WC_FOUND_CONFLICT, NULL,
_("Aborting commit: '%s' remains in conflict"),
svn_dirent_local_style(conflict->local_abspath, pool));
}
return SVN_NO_ERROR;
}
/* Helper function for svn_client__harvest_committables().
* Determine whether we are within a tree-conflicted subtree of the
* working copy and return an SVN_ERR_WC_FOUND_CONFLICT error if so. */
static svn_error_t *
bail_on_tree_conflicted_ancestor(svn_wc_context_t *wc_ctx,
const char *local_abspath,
svn_wc_notify_func2_t notify_func,
void *notify_baton,
apr_pool_t *scratch_pool)
{
const char *wcroot_abspath;
SVN_ERR(svn_wc__get_wc_root(&wcroot_abspath, wc_ctx, local_abspath,
scratch_pool, scratch_pool));
local_abspath = svn_dirent_dirname(local_abspath, scratch_pool);
while(svn_dirent_is_ancestor(wcroot_abspath, local_abspath))
{
svn_boolean_t tree_conflicted;
/* Check if the parent has tree conflicts */
SVN_ERR(svn_wc_conflicted_p3(NULL, NULL, &tree_conflicted,
wc_ctx, local_abspath, scratch_pool));
if (tree_conflicted)
{
if (notify_func != NULL)
{
notify_func(notify_baton,
svn_wc_create_notify(local_abspath,
svn_wc_notify_failed_conflict,
scratch_pool),
scratch_pool);
}
return svn_error_createf(
SVN_ERR_WC_FOUND_CONFLICT, NULL,
_("Aborting commit: '%s' remains in tree-conflict"),
svn_dirent_local_style(local_abspath, scratch_pool));
}
/* Step outwards */
if (svn_dirent_is_root(local_abspath, strlen(local_abspath)))
break;
else
local_abspath = svn_dirent_dirname(local_abspath, scratch_pool);
}
return SVN_NO_ERROR;
}
/* Recursively search for commit candidates in (and under) LOCAL_ABSPATH using
WC_CTX and add those candidates to COMMITTABLES. If in ADDS_ONLY modes,
only new additions are recognized.
DEPTH indicates how to treat files and subdirectories of LOCAL_ABSPATH
when LOCAL_ABSPATH is itself a directory; see
svn_client__harvest_committables() for its behavior.
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 COMMIT_RELPATH is not NULL, treat not-added nodes as if it is destined to
be added as COMMIT_RELPATH, and add 'deleted' entries to COMMITTABLES as
items to delete in the copy destination. COPY_MODE_ROOT should be set TRUE
for the first call for which COPY_MODE is TRUE, i.e. not for for the
recursive calls, and FALSE otherwise.
If CHANGELISTS is non-NULL, it is a hash whose keys are const char *
changelist names used as a restrictive filter
when harvesting committables; that is, don't add a path to
COMMITTABLES unless it's a member of one of those changelists.
DANGLERS is a hash table mapping const char* absolute paths of a parent
to a const char * absolute path of a child. See the comment about
danglers at the top of svn_client__harvest_committables().
If CANCEL_FUNC is non-null, call it with CANCEL_BATON to see
if the user has cancelled the operation.
Any items added to COMMITTABLES are allocated from the COMITTABLES
hash pool, not POOL. SCRATCH_POOL is used for temporary allocations. */
static svn_error_t *
harvest_committables(svn_wc_context_t *wc_ctx,
const char *local_abspath,
svn_client__committables_t *committables,
apr_hash_t *lock_tokens,
const char *repos_root_url,
const char *commit_relpath,
svn_boolean_t copy_mode_root,
svn_depth_t depth,
svn_boolean_t just_locked,
apr_hash_t *changelists,
svn_boolean_t skip_files,
svn_boolean_t skip_dirs,
apr_hash_t *danglers,
svn_client__check_url_kind_t check_url_func,
void *check_url_baton,
svn_cancel_func_t cancel_func,
void *cancel_baton,
svn_wc_notify_func2_t notify_func,
void *notify_baton,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
svn_boolean_t text_mod = FALSE;
svn_boolean_t prop_mod = FALSE;
apr_byte_t state_flags = 0;
svn_node_kind_t working_kind;
svn_node_kind_t db_kind;
const char *node_relpath;
const char *node_lock_token;
svn_revnum_t node_rev;
const char *cf_relpath = NULL;
svn_revnum_t cf_rev = SVN_INVALID_REVNUM;
svn_boolean_t matches_changelists;
svn_boolean_t is_special;
svn_boolean_t is_added;
svn_boolean_t is_deleted;
svn_boolean_t is_replaced;
svn_boolean_t is_not_present;
svn_boolean_t is_excluded;
svn_boolean_t is_op_root;
svn_boolean_t is_symlink;
svn_boolean_t conflicted;
const char *node_changelist;
svn_boolean_t is_update_root;
svn_revnum_t original_rev;
const char *original_relpath;
svn_boolean_t copy_mode = (commit_relpath != NULL);
SVN_ERR_ASSERT(svn_dirent_is_absolute(local_abspath));
/* Early out if the item is already marked as committable. */
if (look_up_committable(committables, local_abspath, scratch_pool))
return SVN_NO_ERROR;
SVN_ERR_ASSERT((copy_mode && commit_relpath)
|| (! copy_mode && ! commit_relpath));
SVN_ERR_ASSERT((copy_mode_root && copy_mode) || ! copy_mode_root);
SVN_ERR_ASSERT((just_locked && lock_tokens) || !just_locked);
if (cancel_func)
SVN_ERR(cancel_func(cancel_baton));
/* 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. */
SVN_ERR(svn_wc__node_get_commit_status(&db_kind, &is_added, &is_deleted,
&is_replaced,
&is_not_present, &is_excluded,
&is_op_root, &is_symlink,
&node_rev, &node_relpath,
&original_rev, &original_relpath,
&conflicted,
&node_changelist,
&prop_mod, &is_update_root,
&node_lock_token,
wc_ctx, local_abspath,
scratch_pool, scratch_pool));
if ((skip_files && db_kind == svn_node_file) || is_excluded)
return SVN_NO_ERROR;
if (!node_relpath && commit_relpath)
node_relpath = commit_relpath;
SVN_ERR(svn_io_check_special_path(local_abspath, &working_kind, &is_special,
scratch_pool));
/* ### In 1.6 an obstructed dir would fail when locking before we
got here. Locking now doesn't fail so perhaps we should do
some sort of checking here. */
if ((working_kind != svn_node_file)
&& (working_kind != svn_node_dir)
&& (working_kind != svn_node_none))
{
return svn_error_createf
(SVN_ERR_NODE_UNKNOWN_KIND, NULL,
_("Unknown entry kind for '%s'"),
svn_dirent_local_style(local_abspath, scratch_pool));
}
/* Save the result for reuse. */
matches_changelists = ((changelists == NULL)
|| (node_changelist != NULL
&& apr_hash_get(changelists, node_changelist,
APR_HASH_KEY_STRING) != NULL));
/* Early exit. */
if (working_kind != svn_node_dir && working_kind != svn_node_none
&& ! matches_changelists)
{
return SVN_NO_ERROR;
}
/* Verify that the node's type has not changed before attempting to
commit. */
if ((((!is_symlink) && (is_special))
#ifdef HAVE_SYMLINK
|| (is_symlink && (! is_special))
#endif /* HAVE_SYMLINK */
) && (working_kind != svn_node_none))
{
return svn_error_createf
(SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
_("Entry '%s' has unexpectedly changed special status"),
svn_dirent_local_style(local_abspath, scratch_pool));
}
if (copy_mode
&& is_update_root
&& db_kind == svn_node_file)
{
if (copy_mode)
return SVN_NO_ERROR;
}
/* If NODE is in our changelist, then examine it for conflicts. We
need to bail out if any conflicts exist. */
if (conflicted && matches_changelists)
{
svn_boolean_t tc, pc, treec;
SVN_ERR(svn_wc_conflicted_p3(&tc, &pc, &treec, wc_ctx,
local_abspath, scratch_pool));
if (tc || pc || treec)
{
if (notify_func != NULL)
{
notify_func(notify_baton,
svn_wc_create_notify(local_abspath,
svn_wc_notify_failed_conflict,
scratch_pool),
scratch_pool);
}
return svn_error_createf(
SVN_ERR_WC_FOUND_CONFLICT, NULL,
_("Aborting commit: '%s' remains in conflict"),
svn_dirent_local_style(local_abspath, scratch_pool));
}
}
if (is_deleted && !is_op_root /* && !is_added */)
return SVN_NO_ERROR; /* Not an operational delete and not an add. */
if (node_relpath == NULL)
SVN_ERR(svn_wc__node_get_repos_relpath(&node_relpath,
wc_ctx, local_abspath,
scratch_pool, scratch_pool));
/* Check for the deletion case.
* We delete explicitly deleted nodes (duh!)
* We delete not-present children of copies
* We delete nodes that directly replace a node in its ancestor
*/
if (is_deleted || is_replaced)
state_flags |= SVN_CLIENT_COMMIT_ITEM_DELETE;
else if (is_not_present)
{
if (! copy_mode)
return SVN_NO_ERROR;
/* We should check if we should really add a delete operation */
if (check_url_func)
{
svn_revnum_t revision;
const char *repos_relpath;
svn_node_kind_t kind;
/* Determine from what parent we would be the deleted child */
SVN_ERR(svn_wc__node_get_origin(NULL, &revision, &repos_relpath,
NULL, NULL, NULL, wc_ctx,
svn_dirent_dirname(local_abspath,
scratch_pool),
FALSE, scratch_pool, scratch_pool));
repos_relpath = svn_relpath_join(repos_relpath,
svn_dirent_basename(local_abspath,
NULL),
scratch_pool);
SVN_ERR(check_url_func(check_url_baton, &kind,
svn_path_url_add_component2(repos_root_url,
repos_relpath,
scratch_pool),
revision, scratch_pool));
if (kind == svn_node_none)
return SVN_NO_ERROR; /* This node can't be deleted */
}
state_flags |= SVN_CLIENT_COMMIT_ITEM_DELETE;
}
/* Check for adds and copies */
if (is_added && is_op_root)
{
/* Root of local add or copy */
state_flags |= SVN_CLIENT_COMMIT_ITEM_ADD;
if (original_relpath)
{
/* Root of copy */
state_flags |= SVN_CLIENT_COMMIT_ITEM_IS_COPY;
cf_relpath = original_relpath;
cf_rev = original_rev;
}
}
/* Further additions occur in copy mode. */
if (copy_mode
&& (!is_added || copy_mode_root)
&& !(state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE))
{
svn_revnum_t dir_rev;
if (!copy_mode_root)
SVN_ERR(svn_wc__node_get_base_rev(&dir_rev, wc_ctx,
svn_dirent_dirname(local_abspath,
scratch_pool),
scratch_pool));
if (copy_mode_root || node_rev != dir_rev)
{
state_flags |= SVN_CLIENT_COMMIT_ITEM_ADD;
SVN_ERR(svn_wc__node_get_origin(NULL, &cf_rev,
&cf_relpath, NULL,
NULL, NULL,
wc_ctx, local_abspath, FALSE,
scratch_pool, scratch_pool));
if (cf_relpath)
state_flags |= SVN_CLIENT_COMMIT_ITEM_IS_COPY;
}
}
/* If an add is scheduled to occur, dig around for some more
information about it. */
if (state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
{
/* First of all, the working file or directory must exist.
See issue #3198. */
if (working_kind == svn_node_none)
{
if (notify_func != NULL)
{
notify_func(notify_baton,
svn_wc_create_notify(local_abspath,
svn_wc_notify_failed_missing,
scratch_pool),
scratch_pool);
}
return svn_error_createf(
SVN_ERR_WC_PATH_NOT_FOUND, NULL,
_("'%s' is scheduled for addition, but is missing"),
svn_dirent_local_style(local_abspath, scratch_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 (db_kind == svn_node_file)
{
/* Check for text mods. */
if (state_flags & SVN_CLIENT_COMMIT_ITEM_IS_COPY)
SVN_ERR(svn_wc_text_modified_p2(&text_mod, wc_ctx, local_abspath,
FALSE, scratch_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))
{
/* 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 (db_kind == svn_node_file)
SVN_ERR(svn_wc_text_modified_p2(&text_mod, wc_ctx, local_abspath,
FALSE, scratch_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 (node_lock_token && lock_tokens && (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)
{
if (matches_changelists)
{
/* Finally, add the committable item. */
SVN_ERR(add_committable(committables, local_abspath, db_kind,
repos_root_url,
copy_mode
? commit_relpath
: node_relpath,
copy_mode
? SVN_INVALID_REVNUM
: node_rev,
cf_relpath,
cf_rev,
state_flags,
result_pool, scratch_pool));
if (state_flags & SVN_CLIENT_COMMIT_ITEM_LOCK_TOKEN)
apr_hash_set(lock_tokens,
svn_path_url_add_component2(
repos_root_url, node_relpath,
apr_hash_pool_get(lock_tokens)),
APR_HASH_KEY_STRING,
apr_pstrdup(apr_hash_pool_get(lock_tokens),
node_lock_token));
}
}
/* Fetch lock tokens for descendants of deleted nodes. */
if (lock_tokens
&& (state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE))
{
apr_hash_t *local_relpath_tokens;
apr_hash_index_t *hi;
apr_pool_t *token_pool = apr_hash_pool_get(lock_tokens);
SVN_ERR(svn_wc__node_get_lock_tokens_recursive(
&local_relpath_tokens, wc_ctx, local_abspath,
token_pool, scratch_pool));
/* Add tokens to existing hash. */
for (hi = apr_hash_first(scratch_pool, local_relpath_tokens);
hi;
hi = apr_hash_next(hi))
{
const void *key;
apr_ssize_t klen;
void * val;
apr_hash_this(hi, &key, &klen, &val);
apr_hash_set(lock_tokens, key, klen, val);
}
}
/* Make sure we check for dangling children on additions */
if (state_flags && is_added && danglers)
{
/* If a node is added, it's parent must exist in the repository at the
time of committing */
svn_boolean_t parent_added;
const char *parent_abspath = svn_dirent_dirname(local_abspath,
scratch_pool);
SVN_ERR(svn_wc__node_is_added(&parent_added, wc_ctx, parent_abspath,
scratch_pool));
if (parent_added)
{
const char *copy_root_abspath;
svn_boolean_t parent_is_copy;
/* The parent is added, so either it is a copy, or a locally added
* directory. In either case, we require the op-root of the parent
* to be part of the commit. See issue #4059. */
SVN_ERR(svn_wc__node_get_origin(&parent_is_copy, NULL, NULL, NULL,
NULL, ©_root_abspath,
wc_ctx, parent_abspath,
FALSE, scratch_pool, scratch_pool));
if (parent_is_copy)
parent_abspath = copy_root_abspath;
if (!apr_hash_get(danglers, parent_abspath, APR_HASH_KEY_STRING))
{
apr_hash_set(danglers,
apr_pstrdup(result_pool, parent_abspath),
APR_HASH_KEY_STRING,
apr_pstrdup(result_pool, local_abspath));
}
}
}
if (db_kind != svn_node_dir || depth <= svn_depth_empty)
return SVN_NO_ERROR;
SVN_ERR(bail_on_tree_conflicted_children(wc_ctx, local_abspath,
db_kind, depth, changelists,
notify_func, notify_baton,
scratch_pool));
/* Recursively handle each node according to depth, except when the
node is only being deleted. */
if ((! (state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE))
|| (state_flags & SVN_CLIENT_COMMIT_ITEM_ADD))
{
const apr_array_header_t *children;
apr_pool_t *iterpool = svn_pool_create(scratch_pool);
int i;
svn_depth_t depth_below_here = depth;
if (depth < svn_depth_infinity)
depth_below_here = svn_depth_empty; /* Stop recursing */
SVN_ERR(svn_wc__node_get_children_of_working_node(
&children, wc_ctx, local_abspath, copy_mode,
scratch_pool, iterpool));
for (i = 0; i < children->nelts; i++)
{
const char *this_abspath = APR_ARRAY_IDX(children, i, const char *);
const char *name = svn_dirent_basename(this_abspath, NULL);
const char *this_commit_relpath;
svn_pool_clear(iterpool);
if (commit_relpath == NULL)
this_commit_relpath = NULL;
else
this_commit_relpath = svn_relpath_join(commit_relpath, name,
iterpool);
SVN_ERR(harvest_committables(wc_ctx, this_abspath,
committables, lock_tokens,
repos_root_url,
this_commit_relpath,
FALSE, /* COPY_MODE_ROOT */
depth_below_here,
just_locked,
changelists,
(depth < svn_depth_files),
(depth < svn_depth_immediates),
NULL, /* danglers */
check_url_func, check_url_baton,
cancel_func, cancel_baton,
notify_func, notify_baton,
result_pool,
iterpool));
}
svn_pool_destroy(iterpool);
}
return SVN_NO_ERROR;
}
/* Baton for handle_descendants */
struct handle_descendants_baton
{
svn_wc_context_t *wc_ctx;
svn_cancel_func_t cancel_func;
void *cancel_baton;
svn_client__check_url_kind_t check_url_func;
void *check_url_baton;
};
/* Helper for the commit harvesters */
static svn_error_t *
handle_descendants(void *baton,
const void *key, apr_ssize_t klen, void *val,
apr_pool_t *pool)
{
struct handle_descendants_baton *hdb = baton;
apr_array_header_t *commit_items = val;
apr_pool_t *iterpool = svn_pool_create(pool);
int i;
for (i = 0; i < commit_items->nelts; i++)
{
svn_client_commit_item3_t *item =
APR_ARRAY_IDX(commit_items, i, svn_client_commit_item3_t *);
const apr_array_header_t *absent_descendants;
int j;
/* Is this a copy operation? */
if (!(item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
|| ! item->copyfrom_url)
continue;
if (hdb->cancel_func)
SVN_ERR(hdb->cancel_func(hdb->cancel_baton));
svn_pool_clear(iterpool);
SVN_ERR(svn_wc__get_not_present_descendants(&absent_descendants,
hdb->wc_ctx, item->path,
iterpool, iterpool));
for (j = 0; j < absent_descendants->nelts; j++)
{
int k;
svn_boolean_t found_item = FALSE;
svn_node_kind_t kind;
const char *relpath = APR_ARRAY_IDX(absent_descendants, j,
const char *);
const char *local_abspath = svn_dirent_join(item->path, relpath,
iterpool);
/* If the path has a commit operation, we do nothing.
(It will be deleted by the operation) */
for (k = 0; k < commit_items->nelts; k++)
{
svn_client_commit_item3_t *cmt_item =
APR_ARRAY_IDX(commit_items, k, svn_client_commit_item3_t *);
if (! strcmp(cmt_item->path, local_abspath))
{
found_item = TRUE;
break;
}
}
if (found_item)
continue; /* We have an explicit delete or replace for this path */
/* ### Need a sub-iterpool? */
if (hdb->check_url_func)
{
const char *from_url = svn_path_url_add_component2(
item->copyfrom_url, relpath,
iterpool);
SVN_ERR(hdb->check_url_func(hdb->check_url_baton,
&kind, from_url, item->copyfrom_rev,
iterpool));
if (kind == svn_node_none)
continue; /* This node is already deleted */
}
else
kind = svn_node_unknown; /* 'Ok' for a delete of something */
{
/* Add a new commit item that describes the delete */
apr_pool_t *result_pool = commit_items->pool;
svn_client_commit_item3_t *new_item
= svn_client_commit_item3_create(result_pool);
new_item->path = svn_dirent_join(item->path, relpath,
result_pool);
new_item->kind = kind;
new_item->url = svn_path_url_add_component2(item->url, relpath,
result_pool);
new_item->revision = SVN_INVALID_REVNUM;
new_item->state_flags = SVN_CLIENT_COMMIT_ITEM_DELETE;
new_item->incoming_prop_changes = apr_array_make(result_pool, 1,
sizeof(svn_prop_t *));
APR_ARRAY_PUSH(commit_items, svn_client_commit_item3_t *)
= new_item;
}
}
}
svn_pool_destroy(iterpool);
return SVN_NO_ERROR;
}
/* Allocate and initialize the COMMITTABLES structure from POOL.
*/
static void
create_committables(svn_client__committables_t **committables,
apr_pool_t *pool)
{
*committables = apr_palloc(pool, sizeof(**committables));
(*committables)->by_repository = apr_hash_make(pool);
(*committables)->by_path = apr_hash_make(pool);
}
svn_error_t *
svn_client__harvest_committables(svn_client__committables_t **committables,
apr_hash_t **lock_tokens,
const char *base_dir_abspath,
const apr_array_header_t *targets,
svn_depth_t depth,
svn_boolean_t just_locked,
const apr_array_header_t *changelists,
svn_client__check_url_kind_t check_url_func,
void *check_url_baton,
svn_client_ctx_t *ctx,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
int i;
apr_pool_t *iterpool = svn_pool_create(scratch_pool);
apr_hash_t *changelist_hash = NULL;
svn_wc_context_t *wc_ctx = ctx->wc_ctx;
struct handle_descendants_baton hdb;
apr_hash_index_t *hi;
/* 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
* do not yet exist in the repository. Then after harvesting the total
* commit group, we check to make sure those parents are included.
*
* Each key of danglers is a parent which does not exist in the
* repository. 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(scratch_pool);
SVN_ERR_ASSERT(svn_dirent_is_absolute(base_dir_abspath));
/* Create the COMMITTABLES structure. */
create_committables(committables, result_pool);
/* And the LOCK_TOKENS dito. */
*lock_tokens = apr_hash_make(result_pool);
/* If we have a list of changelists, convert that into a hash with
changelist keys. */
if (changelists && changelists->nelts)
SVN_ERR(svn_hash_from_cstring_keys(&changelist_hash, changelists,
scratch_pool));
for (i = 0; i < targets->nelts; ++i)
{
const char *target_abspath;
svn_node_kind_t kind;
const char *repos_root_url;
svn_pool_clear(iterpool);
/* Add the relative portion to the base abspath. */
target_abspath = svn_dirent_join(base_dir_abspath,
APR_ARRAY_IDX(targets, i, const char *),
iterpool);
SVN_ERR(svn_wc_read_kind(&kind, wc_ctx, target_abspath,
FALSE, /* show_hidden */
iterpool));
if (kind == svn_node_none)
{
/* If a target of the commit is a tree-conflicted node that
* has no entry (e.g. locally deleted), issue a proper tree-
* conflicts error instead of a "not under version control". */
const svn_wc_conflict_description2_t *conflict;
SVN_ERR(svn_wc__get_tree_conflict(&conflict, wc_ctx, target_abspath,
iterpool, iterpool));
if (conflict != NULL)
return svn_error_createf(
SVN_ERR_WC_FOUND_CONFLICT, NULL,
_("Aborting commit: '%s' remains in conflict"),
svn_dirent_local_style(conflict->local_abspath,
iterpool));
else
return svn_error_createf(
SVN_ERR_ILLEGAL_TARGET, NULL,
_("'%s' is not under version control"),
svn_dirent_local_style(target_abspath, iterpool));
}
SVN_ERR(svn_wc__node_get_repos_info(&repos_root_url, NULL, wc_ctx,
target_abspath,
result_pool, iterpool));
/* Handle our TARGET. */
/* Make sure this isn't inside a working copy subtree that is
* marked as tree-conflicted. */
SVN_ERR(bail_on_tree_conflicted_ancestor(ctx->wc_ctx, target_abspath,
ctx->notify_func2,
ctx->notify_baton2,
iterpool));
SVN_ERR(harvest_committables(ctx->wc_ctx, target_abspath,
*committables, *lock_tokens,
repos_root_url,
NULL /* COMMIT_RELPATH */,
FALSE /* COPY_MODE_ROOT */,
depth, just_locked, changelist_hash,
FALSE, FALSE,
danglers,
check_url_func, check_url_baton,
ctx->cancel_func, ctx->cancel_baton,
ctx->notify_func2, ctx->notify_baton2,
result_pool, iterpool));
}
hdb.wc_ctx = ctx->wc_ctx;
hdb.cancel_func = ctx->cancel_func;
hdb.cancel_baton = ctx->cancel_baton;
hdb.check_url_func = check_url_func;
hdb.check_url_baton = check_url_baton;
SVN_ERR(svn_iter_apr_hash(NULL, (*committables)->by_repository,
handle_descendants, &hdb, iterpool));
/* Make sure that every path in danglers is part of the commit. */
for (hi = apr_hash_first(scratch_pool, danglers); hi; hi = apr_hash_next(hi))
{
const char *dangling_parent = svn__apr_hash_index_key(hi);
svn_pool_clear(iterpool);
if (! look_up_committable(*committables, dangling_parent, iterpool))
{
const char *dangling_child = svn__apr_hash_index_val(hi);
if (ctx->notify_func2 != NULL)
{
svn_wc_notify_t *notify;
notify = svn_wc_create_notify(dangling_child,
svn_wc_notify_failed_no_parent,
scratch_pool);
ctx->notify_func2(ctx->notify_baton2, notify, iterpool);
}
return svn_error_createf(
SVN_ERR_ILLEGAL_TARGET, NULL,
_("'%s' is not known to exist in the repository "
"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_dirent_local_style(dangling_parent, iterpool),
svn_dirent_local_style(dangling_child, iterpool));
}
}
svn_pool_destroy(iterpool);
return SVN_NO_ERROR;
}
struct copy_committables_baton
{
svn_client_ctx_t *ctx;
svn_client__committables_t *committables;
apr_pool_t *result_pool;
svn_client__check_url_kind_t check_url_func;
void *check_url_baton;
};
static svn_error_t *
harvest_copy_committables(void *baton, void *item, apr_pool_t *pool)
{
struct copy_committables_baton *btn = baton;
svn_client__copy_pair_t *pair = *(svn_client__copy_pair_t **)item;
const char *repos_root_url;
const char *commit_relpath;
struct handle_descendants_baton hdb;
/* Read the entry for this SRC. */
SVN_ERR_ASSERT(svn_dirent_is_absolute(pair->src_abspath_or_url));
SVN_ERR(svn_wc__node_get_repos_info(&repos_root_url, NULL, btn->ctx->wc_ctx,
pair->src_abspath_or_url,
pool, pool));
commit_relpath = svn_uri_skip_ancestor(repos_root_url,
pair->dst_abspath_or_url, pool);
/* Handle this SRC. */
SVN_ERR(harvest_committables(btn->ctx->wc_ctx,
pair->src_abspath_or_url,
btn->committables, NULL,
repos_root_url,
commit_relpath,
TRUE, /* COPY_MODE_ROOT */
svn_depth_infinity,
FALSE, /* JUST_LOCKED */
NULL,
FALSE, FALSE, /* skip files, dirs */
NULL,
btn->check_url_func,
btn->check_url_baton,
btn->ctx->cancel_func,
btn->ctx->cancel_baton,
btn->ctx->notify_func2,
btn->ctx->notify_baton2,
btn->result_pool, pool));
hdb.wc_ctx = btn->ctx->wc_ctx;
hdb.cancel_func = btn->ctx->cancel_func;
hdb.cancel_baton = btn->ctx->cancel_baton;
hdb.check_url_func = btn->check_url_func;
hdb.check_url_baton = btn->check_url_baton;
SVN_ERR(svn_iter_apr_hash(NULL, btn->committables->by_repository,
handle_descendants, &hdb, pool));
return SVN_NO_ERROR;
}
svn_error_t *
svn_client__get_copy_committables(svn_client__committables_t **committables,
const apr_array_header_t *copy_pairs,
svn_client__check_url_kind_t check_url_func,
void *check_url_baton,
svn_client_ctx_t *ctx,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
struct copy_committables_baton btn;
/* Create the COMMITTABLES structure. */
create_committables(committables, result_pool);
btn.ctx = ctx;
btn.committables = *committables;
btn.result_pool = result_pool;
btn.check_url_func = check_url_func;
btn.check_url_baton = check_url_baton;
/* For each copy pair, harvest the committables for that pair into the
committables hash. */
return svn_iter_apr_array(NULL, copy_pairs,
harvest_copy_committables, &btn, scratch_pool);
}
int svn_client__sort_commit_item_urls(const void *a, const void *b)
{
const svn_client_commit_item3_t *item1
= *((const svn_client_commit_item3_t * const *) a);
const svn_client_commit_item3_t *item2
= *((const svn_client_commit_item3_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_item3_t *item, *last_item = NULL;
int i;
SVN_ERR_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_item3_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_dirent_local_style(item->path, pool),
svn_dirent_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_uri_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_uri_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 and store it as session_relpath. */
for (i = 0; i < ci->nelts; i++)
{
svn_client_commit_item3_t *this_item
= APR_ARRAY_IDX(ci, i, svn_client_commit_item3_t *);
size_t url_len = strlen(this_item->url);
size_t base_url_len = strlen(*base_url);
if (url_len > base_url_len)
this_item->session_relpath = svn_uri__is_child(*base_url,
this_item->url, pool);
else
this_item->session_relpath = "";
}
#ifdef SVN_CLIENT_COMMIT_DEBUG
/* ### TEMPORARY CODE ### */
SVN_DBG(("COMMITTABLES: (base URL=%s)\n", *base_url));
SVN_DBG((" FLAGS REV REL-URL (COPY-URL)\n"));
for (i = 0; i < ci->nelts; i++)
{
svn_client_commit_item3_t *this_item
= APR_ARRAY_IDX(ci, i, svn_client_commit_item3_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';
SVN_DBG((" %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
{
const svn_client_commit_item3_t *item;
void *file_baton;
};
/* A baton for use while driving a path-based editor driver for commit */
struct item_commit_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 */
const char *notify_path_prefix; /* notification path prefix
(NULL is okay, else abs path) */
svn_client_ctx_t *ctx; /* client context baton */
apr_hash_t *commit_items; /* the committables */
const char *base_url; /* The session url for the commit */
};
/* Drive CALLBACK_BATON->editor with the change described by the item in
* CALLBACK_BATON->commit_items that is keyed by PATH. If the change
* includes a text mod, however, call the editor's file_open() function
* but do not send the text mod to the editor; instead, add a mapping of
* "item-url => (commit-item, file-baton)" into CALLBACK_BATON->file_mods.
*
* Before driving the editor, call the cancellation and notification
* callbacks in CALLBACK_BATON->ctx, if present.
*
* 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 item_commit_baton *icb = callback_baton;
const svn_client_commit_item3_t *item = apr_hash_get(icb->commit_items,
path,
APR_HASH_KEY_STRING);
svn_node_kind_t kind = item->kind;
void *file_baton = NULL;
apr_pool_t *file_pool = NULL;
const svn_delta_editor_t *editor = icb->editor;
apr_hash_t *file_mods = icb->file_mods;
svn_client_ctx_t *ctx = icb->ctx;
svn_error_t *err;
const char *local_abspath = NULL;
/* Do some initializations. */
*dir_baton = NULL;
if (item->kind != svn_node_none && item->path)
{
/* We always get an absolute path, see svn_client_commit_item3_t. */
SVN_ERR_ASSERT(svn_dirent_is_absolute(item->path));
local_abspath = item->path;
}
/* 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 (! item->copyfrom_url)
return svn_error_createf
(SVN_ERR_BAD_URL, NULL,
_("Commit item '%s' has copy flag but no copyfrom URL"),
svn_dirent_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_dirent_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 && item->path)
{
const char *npath = item->path;
svn_wc_notify_t *notify;
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. */
if (item->copyfrom_url)
notify = svn_wc_create_notify(npath,
svn_wc_notify_commit_copied_replaced,
pool);
else
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)
{
if (item->copyfrom_url)
notify = svn_wc_create_notify(npath, svn_wc_notify_commit_copied,
pool);
else
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_get2(&propval, ctx->wc_ctx, local_abspath,
SVN_PROP_MIME_TYPE, pool, 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;
notify->path_prefix = icb->notify_path_prefix;
(*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)
{
SVN_ERR_ASSERT(parent_baton);
err = editor->delete_entry(path, item->revision,
parent_baton, pool);
if (err)
return svn_error_trace(fixup_commit_error(local_abspath,
icb->base_url,
path, item->kind,
err, ctx, 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)
{
SVN_ERR_ASSERT(parent_baton);
err = editor->add_file(
path, parent_baton, item->copyfrom_url,
item->copyfrom_url ? item->copyfrom_rev : SVN_INVALID_REVNUM,
file_pool, &file_baton);
}
else /* May be svn_node_none when adding parent dirs for a copy. */
{
SVN_ERR_ASSERT(parent_baton);
err = editor->add_directory(
path, parent_baton, item->copyfrom_url,
item->copyfrom_url ? item->copyfrom_rev : SVN_INVALID_REVNUM,
pool, dir_baton);
}
if (err)
return svn_error_trace(fixup_commit_error(local_abspath,
icb->base_url,
path, kind, err,
ctx, pool));
/* Set other prop-changes, if available in the baton */
if (item->outgoing_prop_changes)
{
svn_prop_t *prop;
apr_array_header_t *prop_changes = item->outgoing_prop_changes;
int ctr;
for (ctr = 0; ctr < prop_changes->nelts; ctr++)
{
prop = APR_ARRAY_IDX(prop_changes, ctr, svn_prop_t *);
if (kind == svn_node_file)
{
editor->change_file_prop(file_baton, prop->name,
prop->value, pool);
}
else
{
editor->change_dir_prop(*dir_baton, prop->name,
prop->value, pool);
}
}
}
}
/* Now handle property mods. */
if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_PROP_MODS)
{
if (kind == svn_node_file)
{
if (! file_baton)
{
SVN_ERR_ASSERT(parent_baton);
err = editor->open_file(path, parent_baton,
item->revision,
file_pool, &file_baton);
if (err)
return svn_error_trace(fixup_commit_error(local_abspath,
icb->base_url,
path, kind,
err, ctx,
pool));
}
}
else
{
if (! *dir_baton)
{
if (! parent_baton)
{
err = editor->open_root(icb->edit_baton, item->revision,
pool, dir_baton);
}
else
{
err = editor->open_directory(path, parent_baton,
item->revision,
pool, dir_baton);
}
if (err)
return svn_error_trace(fixup_commit_error(local_abspath,
icb->base_url,
path, kind,
err, ctx,
pool));
}
}
/* When committing a directory that no longer exists in the
repository, a "not found" error does not occur immediately
upon opening the directory. It appears here during the delta
transmisssion. */
err = svn_wc_transmit_prop_deltas2(
ctx->wc_ctx, local_abspath, editor,
(kind == svn_node_dir) ? *dir_baton : file_baton, pool);
if (err)
return svn_error_trace(fixup_commit_error(local_abspath,
icb->base_url,
path, kind, err,
ctx, pool));
/* Make any additional client -> repository prop changes. */
if (item->outgoing_prop_changes)
{
svn_prop_t *prop;
int i;
for (i = 0; i < item->outgoing_prop_changes->nelts; i++)
{
prop = APR_ARRAY_IDX(item->outgoing_prop_changes, i,
svn_prop_t *);
if (kind == svn_node_file)
{
editor->change_file_prop(file_baton, prop->name,
prop->value, pool);
}
else
{
editor->change_dir_prop(*dir_baton, prop->name,
prop->value, pool);
}
}
}
}
/* 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)
{
SVN_ERR_ASSERT(parent_baton);
err = editor->open_file(path, parent_baton,
item->revision,
file_pool, &file_baton);
if (err)
return svn_error_trace(fixup_commit_error(local_abspath,
icb->base_url,
path, kind,
err, ctx, pool));
}
/* Add this file mod to the FILE_MODS hash. */
mod->item = item;
mod->file_baton = file_baton;
apr_hash_set(file_mods, item->session_relpath, 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. */
err = editor->close_file(file_baton, NULL, file_pool);
if (err)
return svn_error_trace(fixup_commit_error(local_abspath,
icb->base_url,
path, kind,
err, ctx, 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,
const apr_array_header_t *commit_items,
const svn_delta_editor_t *editor,
void *edit_baton,
const char *notify_path_prefix,
apr_hash_t **md5_checksums,
apr_hash_t **sha1_checksums,
svn_client_ctx_t *ctx,
apr_pool_t *result_pool,
apr_pool_t *scratch_pool)
{
apr_hash_t *file_mods = apr_hash_make(scratch_pool);
apr_hash_t *items_hash = apr_hash_make(scratch_pool);
apr_pool_t *iterpool = svn_pool_create(scratch_pool);
apr_hash_index_t *hi;
int i;
struct item_commit_baton cb_baton;
apr_array_header_t *paths =
apr_array_make(scratch_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, scratch_pool));
}
#endif /* SVN_CLIENT_COMMIT_DEBUG */
/* Ditto for the checksums. */
if (md5_checksums)
*md5_checksums = apr_hash_make(result_pool);
if (sha1_checksums)
*sha1_checksums = apr_hash_make(result_pool);
/* Build a hash from our COMMIT_ITEMS array, keyed on the
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_item3_t *item =
APR_ARRAY_IDX(commit_items, i, svn_client_commit_item3_t *);
const char *path = item->session_relpath;
apr_hash_set(items_hash, path, APR_HASH_KEY_STRING, item);
APR_ARRAY_PUSH(paths, const char *) = path;
}
/* Setup the callback baton. */
cb_baton.editor = editor;
cb_baton.edit_baton = edit_baton;
cb_baton.file_mods = file_mods;
cb_baton.notify_path_prefix = notify_path_prefix;
cb_baton.ctx = ctx;
cb_baton.commit_items = items_hash;
cb_baton.base_url = base_url;
/* Drive the commit editor! */
SVN_ERR(svn_delta_path_driver(editor, edit_baton, SVN_INVALID_REVNUM,
paths, do_item_commit, &cb_baton,
scratch_pool));
/* Transmit outstanding text deltas. */
for (hi = apr_hash_first(scratch_pool, file_mods);
hi;
hi = apr_hash_next(hi))
{
struct file_mod_t *mod = svn__apr_hash_index_val(hi);
const svn_client_commit_item3_t *item = mod->item;
const svn_checksum_t *new_text_base_md5_checksum;
const svn_checksum_t *new_text_base_sha1_checksum;
svn_boolean_t fulltext = FALSE;
svn_error_t *err;
svn_pool_clear(iterpool);
/* Transmit the entry. */
if (ctx->cancel_func)
SVN_ERR(ctx->cancel_func(ctx->cancel_baton));
if (ctx->notify_func2)
{
svn_wc_notify_t *notify;
notify = svn_wc_create_notify(item->path,
svn_wc_notify_commit_postfix_txdelta,
iterpool);
notify->kind = svn_node_file;
notify->path_prefix = notify_path_prefix;
ctx->notify_func2(ctx->notify_baton2, notify, iterpool);
}
/* If the node has no history, transmit full text */
if ((item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
&& ! (item->state_flags & SVN_CLIENT_COMMIT_ITEM_IS_COPY))
fulltext = TRUE;
err = svn_wc_transmit_text_deltas3(&new_text_base_md5_checksum,
&new_text_base_sha1_checksum,
ctx->wc_ctx, item->path,
fulltext, editor, mod->file_baton,
result_pool, iterpool);
if (err)
{
svn_pool_destroy(iterpool); /* Close tempfiles */
return svn_error_trace(fixup_commit_error(item->path,
base_url,
item->session_relpath,
svn_node_file,
err, ctx, scratch_pool));
}
if (md5_checksums)
apr_hash_set(*md5_checksums, item->path, APR_HASH_KEY_STRING,
new_text_base_md5_checksum);
if (sha1_checksums)
apr_hash_set(*sha1_checksums, item->path, APR_HASH_KEY_STRING,
new_text_base_sha1_checksum);
}
svn_pool_destroy(iterpool);
/* Close the edit. */
return svn_error_trace(editor->close_edit(edit_baton, scratch_pool));
}
#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)
{
if (ctx->log_msg_func3)
{
/* The client provided a callback function for the current API.
Forward the call to it directly. */
return (*ctx->log_msg_func3)(log_msg, tmp_file, commit_items,
ctx->log_msg_baton3, pool);
}
else if (ctx->log_msg_func2 || ctx->log_msg_func)
{
/* The client provided a pre-1.5 (or pre-1.3) API callback
function. Convert the commit_items list to the appropriate
type, and forward call to it. */
svn_error_t *err;
apr_pool_t *scratch_pool = svn_pool_create(pool);
apr_array_header_t *old_commit_items =
apr_array_make(scratch_pool, commit_items->nelts, sizeof(void*));
int i;
for (i = 0; i < commit_items->nelts; i++)
{
svn_client_commit_item3_t *item =
APR_ARRAY_IDX(commit_items, i, svn_client_commit_item3_t *);
if (ctx->log_msg_func2)
{
svn_client_commit_item2_t *old_item =
apr_pcalloc(scratch_pool, sizeof(*old_item));
old_item->path = item->path;
old_item->kind = item->kind;
old_item->url = item->url;
old_item->revision = item->revision;
old_item->copyfrom_url = item->copyfrom_url;
old_item->copyfrom_rev = item->copyfrom_rev;
old_item->state_flags = item->state_flags;
old_item->wcprop_changes = item->incoming_prop_changes;
APR_ARRAY_PUSH(old_commit_items, svn_client_commit_item2_t *) =
old_item;
}
else /* ctx->log_msg_func */
{
svn_client_commit_item_t *old_item =
apr_pcalloc(scratch_pool, sizeof(*old_item));
old_item->path = item->path;
old_item->kind = item->kind;
old_item->url = item->url;
/* The pre-1.3 API used the 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->incoming_prop_changes;
APR_ARRAY_PUSH(old_commit_items, svn_client_commit_item_t *) =
old_item;
}
}
if (ctx->log_msg_func2)
err = (*ctx->log_msg_func2)(log_msg, tmp_file, old_commit_items,
ctx->log_msg_baton2, pool);
else
err = (*ctx->log_msg_func)(log_msg, tmp_file, old_commit_items,
ctx->log_msg_baton, pool);
svn_pool_destroy(scratch_pool);
return err;
}
else
{
/* No log message callback was provided by the client. */
*log_msg = "";
*tmp_file = NULL;
return SVN_NO_ERROR;
}
}
svn_error_t *
svn_client__ensure_revprop_table(apr_hash_t **revprop_table_out,
const apr_hash_t *revprop_table_in,
const char *log_msg,
svn_client_ctx_t *ctx,
apr_pool_t *pool)
{
apr_hash_t *new_revprop_table;
if (revprop_table_in)
{
if (svn_prop_has_svn_prop(revprop_table_in, pool))
return svn_error_create(SVN_ERR_CLIENT_PROPERTY_NAME, NULL,
_("Standard properties can't be set "
"explicitly as revision properties"));
new_revprop_table = apr_hash_copy(pool, revprop_table_in);
}
else
{
new_revprop_table = apr_hash_make(pool);
}
apr_hash_set(new_revprop_table, SVN_PROP_REVISION_LOG, APR_HASH_KEY_STRING,
svn_string_create(log_msg, pool));
*revprop_table_out = new_revprop_table;
return SVN_NO_ERROR;
}