diff options
| author | Peter Wemm <peter@FreeBSD.org> | 2013-06-18 02:07:41 +0000 | 
|---|---|---|
| committer | Peter Wemm <peter@FreeBSD.org> | 2013-06-18 02:07:41 +0000 | 
| commit | 32547653cc5376642e1231fb644db99933ac8db4 (patch) | |
| tree | 135691142dc0e75a5e5d97b5074d03436435b8e0 /subversion/libsvn_client/commit_util.c | |
Notes
Diffstat (limited to 'subversion/libsvn_client/commit_util.c')
| -rw-r--r-- | subversion/libsvn_client/commit_util.c | 1981 | 
1 files changed, 1981 insertions, 0 deletions
| diff --git a/subversion/libsvn_client/commit_util.c b/subversion/libsvn_client/commit_util.c new file mode 100644 index 000000000000..1e2c50cd9e61 --- /dev/null +++ b/subversion/libsvn_client/commit_util.c @@ -0,0 +1,1981 @@ +/* + * 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" +#include "private/svn_client_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. + +   If the state flag specifies that a lock must be used, store the token in LOCK +   in lock_tokens. + */ +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, +                const char *moved_from_abspath, +                apr_byte_t state_flags, +                apr_hash_t *lock_tokens, +                const svn_lock_t *lock, +                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 = svn_hash_gets(committables->by_repository, repos_root_url); + +  /* 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)); +      svn_hash_sets(committables->by_repository, +                    apr_pstrdup(result_pool, repos_root_url), 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 *)); + +  if (moved_from_abspath) +    new_item->moved_from_abspath = apr_pstrdup(result_pool, +                                               moved_from_abspath); + +  /* Now, add the commit item to the array. */ +  APR_ARRAY_PUSH(array, svn_client_commit_item3_t *) = new_item; + +  /* ... and to the hash. */ +  svn_hash_sets(committables->by_path, new_item->path, new_item); + +  if (lock +      && lock_tokens +      && (state_flags & SVN_CLIENT_COMMIT_ITEM_LOCK_TOKEN)) +    { +      svn_hash_sets(lock_tokens, new_item->url, +                    apr_pstrdup(result_pool, lock->token)); +    } + +  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 *) +      svn_hash_gets(committables->by_path, path); +} + +/* 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_wcroot(&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 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. + +   IS_EXPLICIT_TARGET should always be passed as TRUE, except when +   harvest_committables() calls itself in recursion. This provides a way to +   tell whether LOCAL_ABSPATH was an original target or whether it was reached +   by recursing deeper into a dir target. (This is used to skip all file +   externals that aren't explicit commit targets.) + +   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. */ + +struct harvest_baton +{ +  /* Static data */ +  const char *root_abspath; +  svn_client__committables_t *committables; +  apr_hash_t *lock_tokens; +  const char *commit_relpath; /* Valid for the harvest root */ +  svn_depth_t depth; +  svn_boolean_t just_locked; +  apr_hash_t *changelists; +  apr_hash_t *danglers; +  svn_client__check_url_kind_t check_url_func; +  void *check_url_baton; +  svn_wc_notify_func2_t notify_func; +  void *notify_baton; +  svn_wc_context_t *wc_ctx; +  apr_pool_t *result_pool; + +  /* Harvester state */ +  const char *skip_below_abspath; /* If non-NULL, skip everything below */ +}; + +static svn_error_t * +harvest_status_callback(void *status_baton, +                        const char *local_abspath, +                        const svn_wc_status3_t *status, +                        apr_pool_t *scratch_pool); + +static svn_error_t * +harvest_committables(const char *local_abspath, +                     svn_client__committables_t *committables, +                     apr_hash_t *lock_tokens, +                     const char *copy_mode_relpath, +                     svn_depth_t depth, +                     svn_boolean_t just_locked, +                     apr_hash_t *changelists, +                     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, +                     svn_wc_context_t *wc_ctx, +                     apr_pool_t *result_pool, +                     apr_pool_t *scratch_pool) +{ +  struct harvest_baton baton; + +  SVN_ERR_ASSERT((just_locked && lock_tokens) || !just_locked); + +  baton.root_abspath = local_abspath; +  baton.committables = committables; +  baton.lock_tokens = lock_tokens; +  baton.commit_relpath = copy_mode_relpath; +  baton.depth = depth; +  baton.just_locked = just_locked; +  baton.changelists = changelists; +  baton.danglers = danglers; +  baton.check_url_func = check_url_func; +  baton.check_url_baton = check_url_baton; +  baton.notify_func = notify_func; +  baton.notify_baton = notify_baton; +  baton.wc_ctx = wc_ctx; +  baton.result_pool = result_pool; + +  baton.skip_below_abspath = NULL; + +  SVN_ERR(svn_wc_walk_status(wc_ctx, +                             local_abspath, +                             depth, +                             (copy_mode_relpath != NULL) /* get_all */, +                             FALSE /* no_ignore */, +                             FALSE /* ignore_text_mods */, +                             NULL /* ignore_patterns */, +                             harvest_status_callback, +                             &baton, +                             cancel_func, cancel_baton, +                             scratch_pool)); + +  return SVN_NO_ERROR; +} + +static svn_error_t * +harvest_not_present_for_copy(svn_wc_context_t *wc_ctx, +                             const char *local_abspath, +                             svn_client__committables_t *committables, +                             const char *repos_root_url, +                             const char *commit_relpath, +                             svn_client__check_url_kind_t check_url_func, +                             void *check_url_baton, +                             apr_pool_t *result_pool, +                             apr_pool_t *scratch_pool) +{ +  const apr_array_header_t *children; +  apr_pool_t *iterpool = svn_pool_create(scratch_pool); +  int i; + +  /* A function to retrieve not present children would be nice to have */ +  SVN_ERR(svn_wc__node_get_children_of_working_node( +                                    &children, wc_ctx, local_abspath, TRUE, +                                    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_boolean_t not_present; +      svn_node_kind_t kind; + +      svn_pool_clear(iterpool); + +      SVN_ERR(svn_wc__node_is_not_present(¬_present, NULL, NULL, wc_ctx, +                                          this_abspath, FALSE, scratch_pool)); + +      if (!not_present) +        continue; + +      if (commit_relpath == NULL) +        this_commit_relpath = NULL; +      else +        this_commit_relpath = svn_relpath_join(commit_relpath, name, +                                              iterpool); + +      /* We should check if we should really add a delete operation */ +      if (check_url_func) +        { +          svn_revnum_t parent_rev; +          const char *parent_repos_relpath; +          const char *parent_repos_root_url; +          const char *node_url; + +          /* Determine from what parent we would be the deleted child */ +          SVN_ERR(svn_wc__node_get_origin( +                              NULL, &parent_rev, &parent_repos_relpath, +                              &parent_repos_root_url, NULL, NULL, +                              wc_ctx, +                              svn_dirent_dirname(this_abspath, +                                                  scratch_pool), +                              FALSE, scratch_pool, scratch_pool)); + +          node_url = svn_path_url_add_component2( +                        svn_path_url_add_component2(parent_repos_root_url, +                                                    parent_repos_relpath, +                                                    scratch_pool), +                        svn_dirent_basename(this_abspath, NULL), +                        iterpool); + +          SVN_ERR(check_url_func(check_url_baton, &kind, +                                 node_url, parent_rev, iterpool)); + +          if (kind == svn_node_none) +            continue; /* This node can't be deleted */ +        } +      else +        SVN_ERR(svn_wc_read_kind2(&kind, wc_ctx, this_abspath, +                                  TRUE, TRUE, scratch_pool)); + +      SVN_ERR(add_committable(committables, this_abspath, kind, +                              repos_root_url, +                              this_commit_relpath, +                              SVN_INVALID_REVNUM, +                              NULL /* copyfrom_relpath */, +                              SVN_INVALID_REVNUM /* copyfrom_rev */, +                              NULL /* moved_from_abspath */, +                              SVN_CLIENT_COMMIT_ITEM_DELETE, +                              NULL, NULL, +                              result_pool, scratch_pool)); +    } + +  svn_pool_destroy(iterpool); +  return SVN_NO_ERROR; +} + +/* Implements svn_wc_status_func4_t */ +static svn_error_t * +harvest_status_callback(void *status_baton, +                        const char *local_abspath, +                        const svn_wc_status3_t *status, +                        apr_pool_t *scratch_pool) +{ +  apr_byte_t state_flags = 0; +  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_added; +  svn_boolean_t is_deleted; +  svn_boolean_t is_replaced; +  svn_boolean_t is_op_root; +  svn_revnum_t original_rev; +  const char *original_relpath; +  svn_boolean_t copy_mode; + +  struct harvest_baton *baton = status_baton; +  svn_boolean_t is_harvest_root = +                (strcmp(baton->root_abspath, local_abspath) == 0); +  svn_client__committables_t *committables = baton->committables; +  const char *repos_root_url = status->repos_root_url; +  const char *commit_relpath = NULL; +  svn_boolean_t copy_mode_root = (baton->commit_relpath && is_harvest_root); +  svn_boolean_t just_locked = baton->just_locked; +  apr_hash_t *changelists = baton->changelists; +  svn_wc_notify_func2_t notify_func = baton->notify_func; +  void *notify_baton = baton->notify_baton; +  svn_wc_context_t *wc_ctx = baton->wc_ctx; +  apr_pool_t *result_pool = baton->result_pool; +  const char *moved_from_abspath = NULL; + +  if (baton->commit_relpath) +    commit_relpath = svn_relpath_join( +                        baton->commit_relpath, +                        svn_dirent_skip_ancestor(baton->root_abspath, +                                                 local_abspath), +                        scratch_pool); + +  copy_mode = (commit_relpath != NULL); + +  if (baton->skip_below_abspath +      && svn_dirent_is_ancestor(baton->skip_below_abspath, local_abspath)) +    { +      return SVN_NO_ERROR; +    } +  else +    baton->skip_below_abspath = NULL; /* We have left the skip tree */ + +  /* Return early for nodes that don't have a committable status */ +  switch (status->node_status) +    { +      case svn_wc_status_unversioned: +      case svn_wc_status_ignored: +      case svn_wc_status_external: +      case svn_wc_status_none: +        /* Unversioned nodes aren't committable, but are reported by the status +           walker. +           But if the unversioned node is the root of the walk, we have a user +           error */ +        if (is_harvest_root) +          return svn_error_createf( +                       SVN_ERR_ILLEGAL_TARGET, NULL, +                       _("'%s' is not under version control"), +                       svn_dirent_local_style(local_abspath, scratch_pool)); +        return SVN_NO_ERROR; +      case svn_wc_status_normal: +        /* Status normal nodes aren't modified, so we don't have to commit them +           when we perform a normal commit. But if a node is conflicted we want +           to stop the commit and if we are collecting lock tokens we want to +           look further anyway. + +           When in copy mode we need to compare the revision of the node against +           the parent node to copy mixed-revision base nodes properly */ +        if (!copy_mode && !status->conflicted +            && !(just_locked && status->lock)) +          return SVN_NO_ERROR; +        break; +      default: +        /* Fall through */ +        break; +    } + +  /* 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); + +  /* Save the result for reuse. */ +  matches_changelists = ((changelists == NULL) +                         || (status->changelist != NULL +                             && svn_hash_gets(changelists, status->changelist) +                                != NULL)); + +  /* Early exit. */ +  if (status->kind != svn_node_dir && ! matches_changelists) +    { +      return SVN_NO_ERROR; +    } + +  /* If NODE is in our changelist, then examine it for conflicts. We +     need to bail out if any conflicts exist. +     The status walker checked for conflict marker removal. */ +  if (status->conflicted && matches_changelists) +    { +      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)); +    } +  else if (status->node_status == svn_wc_status_obstructed) +    { +      /* A node's type has changed before attempting to commit. +         This also catches symlink vs non symlink changes */ + +      if (notify_func != NULL) +        { +          notify_func(notify_baton, +                      svn_wc_create_notify(local_abspath, +                                           svn_wc_notify_failed_obstruction, +                                           scratch_pool), +                      scratch_pool); +        } + +      return svn_error_createf( +                    SVN_ERR_NODE_UNEXPECTED_KIND, NULL, +                    _("Node '%s' has unexpectedly changed kind"), +                    svn_dirent_local_style(local_abspath, scratch_pool)); +    } + +  if (status->conflicted && status->kind == svn_node_unknown) +    return SVN_NO_ERROR; /* Ignore delete-delete conflict */ + +  /* 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(&is_added, &is_deleted, +                                         &is_replaced, +                                         &is_op_root, +                                         &node_rev, +                                         &original_rev, &original_relpath, +                                         wc_ctx, local_abspath, +                                         scratch_pool, scratch_pool)); + +  /* Hande file externals only when passed as explicit target. Note that +   * svn_client_commit6() passes all committable externals in as explicit +   * targets iff they count. */ +  if (status->file_external && !is_harvest_root) +    { +      return SVN_NO_ERROR; +    } + +  if (status->node_status == svn_wc_status_missing && matches_changelists) +    { +      /* Added files and directories must exist. See issue #3198. */ +      if (is_added && is_op_root) +        { +          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)); +        } + +      return SVN_NO_ERROR; +    } + +  if (is_deleted && !is_op_root /* && !is_added */) +    return SVN_NO_ERROR; /* Not an operational delete and not an add. */ + +  /* 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; + +  /* 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; + +          if (status->moved_from_abspath && !copy_mode) +            { +              state_flags |= SVN_CLIENT_COMMIT_ITEM_MOVED_HERE; +              moved_from_abspath = status->moved_from_abspath; +            } +        } +    } + +  /* Further copies may occur in copy mode. */ +  else if (copy_mode +           && !(state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)) +    { +      svn_revnum_t dir_rev = SVN_INVALID_REVNUM; + +      if (!copy_mode_root && !status->switched && !is_added) +        SVN_ERR(svn_wc__node_get_base(NULL, &dir_rev, NULL, NULL, NULL, NULL, +                                      wc_ctx, svn_dirent_dirname(local_abspath, +                                                                 scratch_pool), +                                      FALSE /* ignore_enoent */, +                                      FALSE /* show_hidden */, +                                      scratch_pool, scratch_pool)); + +      if (copy_mode_root || status->switched || node_rev != dir_rev) +        { +          state_flags |= (SVN_CLIENT_COMMIT_ITEM_ADD +                          | SVN_CLIENT_COMMIT_ITEM_IS_COPY); + +          if (status->copied) +            { +              /* Copy from original location */ +              cf_rev = original_rev; +              cf_relpath = original_relpath; +            } +          else +            { +              /* Copy BASE location, to represent a mixed-rev or switch copy */ +              cf_rev = status->revision; +              cf_relpath = status->repos_relpath; +            } +        } +    } + +  if (!(state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE) +      || (state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)) +    { +      svn_boolean_t text_mod = FALSE; +      svn_boolean_t prop_mod = FALSE; + +      if (status->kind == svn_node_file) +        { +          /* Check for text modifications on files */ +          if ((state_flags & SVN_CLIENT_COMMIT_ITEM_ADD) +              && ! (state_flags & SVN_CLIENT_COMMIT_ITEM_IS_COPY)) +            { +              text_mod = TRUE; /* Local added files are always modified */ +            } +          else +            text_mod = (status->text_status != svn_wc_status_normal); +        } + +      prop_mod = (status->prop_status != svn_wc_status_normal +                  && status->prop_status != svn_wc_status_none); + +      /* 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 (status->lock && baton->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 (matches_changelists +      && state_flags) +    { +      /* Finally, add the committable item. */ +      SVN_ERR(add_committable(committables, local_abspath, +                              status->kind, +                              repos_root_url, +                              copy_mode +                                      ? commit_relpath +                                      : status->repos_relpath, +                              copy_mode +                                      ? SVN_INVALID_REVNUM +                                      : node_rev, +                              cf_relpath, +                              cf_rev, +                              moved_from_abspath, +                              state_flags, +                              baton->lock_tokens, status->lock, +                              result_pool, scratch_pool)); +    } + +    /* Fetch lock tokens for descendants of deleted BASE nodes. */ +  if (matches_changelists +      && (state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE) +      && !copy_mode +      && SVN_IS_VALID_REVNUM(node_rev) /* && BASE-kind = dir */ +      && baton->lock_tokens) +    { +      apr_hash_t *local_relpath_tokens; +      apr_hash_index_t *hi; + +      SVN_ERR(svn_wc__node_get_lock_tokens_recursive( +                  &local_relpath_tokens, wc_ctx, local_abspath, +                  result_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(baton->lock_tokens, key, klen, val); +        } +    } + +  /* Make sure we check for dangling children on additions + +     We perform this operation on the harvest root, and on roots caused by +     changelist filtering. +  */ +  if (matches_changelists +      && (is_harvest_root || baton->changelists) +      && state_flags +      && is_added +      && baton->danglers) +    { +      /* If a node is added, its parent must exist in the repository at the +         time of committing */ +      apr_hash_t *danglers = baton->danglers; +      svn_boolean_t parent_added; +      const char *parent_abspath = svn_dirent_dirname(local_abspath, +                                                      scratch_pool); + +      /* First check if parent is already in the list of commits +         (Common case for GUI clients that provide a list of commit targets) */ +      if (look_up_committable(committables, parent_abspath, scratch_pool)) +        parent_added = FALSE; /* Skip all expensive checks */ +      else +        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 (!svn_hash_gets(danglers, parent_abspath)) +            { +              svn_hash_sets(danglers, apr_pstrdup(result_pool, parent_abspath), +                            apr_pstrdup(result_pool, local_abspath)); +            } +        } +    } + +  if (is_deleted && !is_added) +    { +      /* Skip all descendants */ +      if (status->kind == svn_node_dir) +        baton->skip_below_abspath = apr_pstrdup(baton->result_pool, +                                                local_abspath); +      return SVN_NO_ERROR; +    } + +  /* Recursively handle each node according to depth, except when the +     node is only being deleted, or is in an added tree (as added trees +     use the normal commit handling). */ +  if (copy_mode && !is_added && !is_deleted && status->kind == svn_node_dir) +    { +      SVN_ERR(harvest_not_present_for_copy(wc_ctx, local_abspath, committables, +                                           repos_root_url, commit_relpath, +                                           baton->check_url_func, +                                           baton->check_url_baton, +                                           result_pool, scratch_pool)); +    } + +  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, +                                 int depth_empty_start, +                                 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; +  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_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); + +      /* 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)); + +      /* Are the remaining items externals with depth empty? */ +      if (i == depth_empty_start) +        depth = svn_depth_empty; + +      SVN_ERR(harvest_committables(target_abspath, +                                   *committables, *lock_tokens, +                                   NULL /* COPY_MODE_RELPATH */, +                                   depth, just_locked, changelist_hash, +                                   danglers, +                                   check_url_func, check_url_baton, +                                   ctx->cancel_func, ctx->cancel_baton, +                                   ctx->notify_func2, ctx->notify_baton2, +                                   ctx->wc_ctx, 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(NULL, NULL, &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(pair->src_abspath_or_url, +                               btn->committables, NULL, +                               commit_relpath, +                               svn_depth_infinity, +                               FALSE,  /* JUST_LOCKED */ +                               NULL /* changelists */, +                               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->ctx->wc_ctx, 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 *); + +      this_item->session_relpath = svn_uri_skip_ancestor(*base_url, +                                                         this_item->url, pool); +    } +#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 = svn_hash_gets(icb->commit_items, +                                                        path); +  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) +        goto fixup_error; +    } + +  /* 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) +        goto fixup_error; + +      /* 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) +                { +                  err = editor->change_file_prop(file_baton, prop->name, +                                                 prop->value, pool); +                } +              else +                { +                  err = editor->change_dir_prop(*dir_baton, prop->name, +                                                prop->value, pool); +                } + +              if (err) +                goto fixup_error; +            } +        } +    } + +  /* 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) +                goto fixup_error; +            } +        } +      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) +                goto fixup_error; +            } +        } + +      /* 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) +        goto fixup_error; + +      /* 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) +                { +                  err = editor->change_file_prop(file_baton, prop->name, +                                           prop->value, pool); +                } +              else +                { +                  err = editor->change_dir_prop(*dir_baton, prop->name, +                                          prop->value, pool); +                } + +              if (err) +                goto fixup_error; +            } +        } +    } + +  /* 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) +            goto fixup_error; +        } + +      /* Add this file mod to the FILE_MODS hash. */ +      mod->item = item; +      mod->file_baton = file_baton; +      svn_hash_sets(file_mods, item->session_relpath, 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) +        goto fixup_error; +    } + +  return SVN_NO_ERROR; + +fixup_error: +  return svn_error_trace(fixup_commit_error(local_abspath, +                                            icb->base_url, +                                            path, kind, +                                            err, ctx, pool)); +} + +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 **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 *)); + +  /* Ditto for the checksums. */ +  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; +      svn_hash_sets(items_hash, path, 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_driver2(editor, edit_baton, paths, TRUE, +                                 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 (sha1_checksums) +        svn_hash_sets(*sha1_checksums, item->path, new_text_base_sha1_checksum); +    } + +  svn_pool_destroy(iterpool); + +  /* Close the edit. */ +  return svn_error_trace(editor->close_edit(edit_baton, scratch_pool)); +} + + +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); +    } +  svn_hash_sets(new_revprop_table, SVN_PROP_REVISION_LOG, +                svn_string_create(log_msg, pool)); +  *revprop_table_out = new_revprop_table; +  return SVN_NO_ERROR; +} | 
