blob: 4d0da208bff2340be05dc2cd65bf43539637a59d [file] [log] [blame]
/*
* commit-drive.c: Driver for the WC commit process.
*
* ====================================================================
* Copyright (c) 2000-2002 CollabNet. All rights reserved.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at http://subversion.tigris.org/license-1.html.
* If newer versions of this license are posted there, you may use a
* newer version instead, at your option.
*
* This software consists of voluntary contributions made by many
* individuals. For exact contribution history, see the revision
* history and logs, available at http://subversion.tigris.org/.
* ====================================================================
*/
/* ==================================================================== */
#include <string.h>
#include "apr_pools.h"
#include "apr_hash.h"
#include "client.h"
#include "svn_path.h"
#include "svn_types.h"
#include "svn_pools.h"
#include "svn_wc.h"
#include "svn_sorts.h"
#include <assert.h>
#include <stdlib.h> /* for qsort() */
/*** Uncomment this to turn on commit driver debugging. ***/
/*
#define SVN_CLIENT_COMMIT_DEBUG
*/
/*** Harvesting Commit Candidates ***/
/* If DIR isn't already in the LOCKED_DIRS hash, attempt to lock it.
If the lock is successful, add DIR to the LOCKED_DIRS hash. Use
the hash's pool for adding new items, and POOL for any other
allocations. */
static svn_error_t *
lock_dir (apr_hash_t *locked_dirs,
svn_stringbuf_t *dir,
apr_pool_t *pool)
{
apr_pool_t *hash_pool = apr_hash_pool_get (locked_dirs);
if (! apr_hash_get (locked_dirs, dir->data, dir->len))
{
SVN_ERR (svn_wc_lock (dir, 0, pool));
apr_hash_set (locked_dirs, apr_pstrdup (hash_pool, dir->data),
dir->len, (void *)1);
}
return SVN_NO_ERROR;
}
/* Add a new commit candidate (described by all parameters except
`COMMITTABLES') to the COMMITABLES hash. */
static void
add_committable (apr_hash_t *committables,
svn_stringbuf_t *path,
svn_node_kind_t kind,
svn_stringbuf_t *url,
svn_revnum_t revision,
svn_stringbuf_t *copyfrom_url,
apr_byte_t state_flags)
{
apr_pool_t *pool = apr_hash_pool_get (committables);
const char *repos_name = SVN_CLIENT__SINGLE_REPOS_NAME;
apr_array_header_t *array;
svn_client_commit_item_t *new_item;
/* Sanity checks. */
assert (path && url);
/* ### todo: Get the canonical repository for this item, which will
be the real key for the COMMITTABLES hash, instead of the above
bogosity. */
array = apr_hash_get (committables, repos_name, APR_HASH_KEY_STRING);
/* E-gads! There is no array for this repository yet! Oh, no
problem, we'll just create (and add to the hash) one. */
if (array == NULL)
{
array = apr_array_make (pool, 1, sizeof (new_item));
apr_hash_set (committables, repos_name, APR_HASH_KEY_STRING, array);
}
/* Now update pointer values, ensuring that their allocations live
in POOL. */
new_item = apr_pcalloc (pool, sizeof (*new_item));
new_item->path = svn_stringbuf_dup (path, pool);
new_item->kind = kind;
new_item->url = svn_stringbuf_dup (url, pool);
new_item->revision = revision;
new_item->copyfrom_url = copyfrom_url
? svn_stringbuf_dup (copyfrom_url, pool)
: NULL;
new_item->state_flags = state_flags;
/* Now, add the commit item to the array. */
(*((svn_client_commit_item_t **) apr_array_push (array))) = new_item;
}
/* Recursively search for commit candidates in (and under) PATH (with
entry ENTRY and ancestry URL), and add those candidates to
COMMITTABLES. If in ADDS_ONLY modes, only new additions are
recognized. COPYFROM_URL is the default copyfrom-url for children
of copied directories.
If in COPY_MODE, the entry is treated as if it is destined to be
added with history as URL. */
static svn_error_t *
harvest_committables (apr_hash_t *committables,
apr_hash_t *locked_dirs,
svn_stringbuf_t *path,
svn_stringbuf_t *url,
svn_stringbuf_t *copyfrom_url,
svn_wc_entry_t *entry,
svn_boolean_t adds_only,
svn_boolean_t copy_mode,
apr_pool_t *pool)
{
apr_pool_t *subpool = svn_pool_create (pool);
apr_hash_t *entries = NULL;
svn_boolean_t text_mod = FALSE, prop_mod = FALSE;
apr_byte_t state_flags = 0;
svn_stringbuf_t *p_path = svn_stringbuf_dup (path, pool);
svn_boolean_t tconflict, pconflict;
svn_stringbuf_t *cf_url = NULL;
assert (entry);
assert (url);
/* Make P_PATH the parent dir. */
svn_path_remove_component (p_path);
/* Return error on unknown path kinds. */
if ((entry->kind != svn_node_file) && (entry->kind != svn_node_dir))
return svn_error_create
(SVN_ERR_UNKNOWN_NODE_KIND, 0, NULL, pool, path->data);
/* If this is a directory ... */
if (entry->kind == svn_node_dir)
{
/* ... then try to read its own entries file so we have a full
entry for it (we were going to have to do this eventually to
recurse anyway, so... ) */
svn_wc_entry_t *e = NULL;
if (svn_wc_entries_read (&entries, path, subpool))
entries = NULL;
if ((entries)
&& ((e = apr_hash_get (entries, SVN_WC_ENTRY_THIS_DIR,
APR_HASH_KEY_STRING))))
entry = e;
}
/* Test for a state of conflict, returning an error if an unresolved
conflict exists for this item. */
SVN_ERR (svn_wc_conflicted_p (&tconflict, &pconflict, p_path,
entry, subpool));
if (tconflict || pconflict)
return svn_error_createf (SVN_ERR_WC_FOUND_CONFLICT, 0, NULL, pool,
"Aborting commit: '%s' remains in conflict.",
path->data);
/* If we have our own URL, and we're NOT in COPY_MODE, it wins over
the telescoping one(s). In COPY_MODE, URL will always be the
URL-to-be of the copied item. */
if ((entry->url) && (! copy_mode))
url = entry->url;
/* Check for the deletion case. Deletes can occur only when we are
not in "adds-only mode". They can be either explicit
(schedule==delete) or implicit (schedule==replace==delete+add). */
if ((! adds_only)
&& ((entry->schedule == svn_wc_schedule_delete)
|| (entry->schedule == svn_wc_schedule_replace)))
{
state_flags |= SVN_CLIENT_COMMIT_ITEM_DELETE;
}
/* Check for the trivial addition case. Adds can be explicit
(schedule==add) or implicit (schedule==replace==delete+add). We
also note whether or not this is an add with history here. */
if ((entry->schedule == svn_wc_schedule_add)
|| (entry->schedule == svn_wc_schedule_replace))
{
state_flags |= SVN_CLIENT_COMMIT_ITEM_ADD;
if (entry->copyfrom_url)
{
state_flags |= SVN_CLIENT_COMMIT_ITEM_IS_COPY;
cf_url = entry->copyfrom_url;
adds_only = FALSE;
}
else
{
adds_only = TRUE;
}
}
/* Check for the copied-subtree addition case. */
if ((entry->copied || copy_mode)
&& (entry->schedule == svn_wc_schedule_normal))
{
#if 0 /* ### todo: Find a better way to do this that doesn't require
reading the parent's entry. */
svn_revnum_t p_rev = entry->revision - 1; /* arbitrary non-equal value */
svn_boolean_t wc_root = FALSE;
/* If this is not a WC root then its parent's revision is
admissible for comparitive purposes. */
SVN_ERR (svn_wc_is_wc_root (&wc_root, path, subpool));
if (! wc_root)
{
svn_wc_entry_t *p_entry;
SVN_ERR (svn_wc_entry (&p_entry, p_path, subpool));
p_rev = p_entry->revision;
}
else if (! copy_mode)
return svn_error_createf
(SVN_ERR_WC_CORRUPT, 0, NULL, subpool,
"Did not expect `%s' to be a working copy root", path->data);
if (entry->revision != p_rev)
#endif /* 0 */
{
state_flags |= SVN_CLIENT_COMMIT_ITEM_ADD;
state_flags |= SVN_CLIENT_COMMIT_ITEM_IS_COPY;
adds_only = TRUE;
if (copy_mode)
cf_url = entry->url;
else
cf_url = copyfrom_url;
}
}
/* If an add is scheduled to occur, dig around for some more
information about it. */
if (state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
{
/* See if there are property modifications to send. */
SVN_ERR (svn_wc_props_modified_p (&prop_mod, path, subpool));
/* Regular adds of files have text mods, but for copies we have
to test for textual mods. Directories simply don't have text! */
if (entry->kind == svn_node_file)
{
if (state_flags & SVN_CLIENT_COMMIT_ITEM_IS_COPY)
SVN_ERR (svn_wc_text_modified_p (&text_mod, path, subpool));
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 local mods: text+props for files, props alone for dirs. */
if (entry->kind == svn_node_file)
SVN_ERR (svn_wc_text_modified_p (&text_mod, path, subpool));
SVN_ERR (svn_wc_props_modified_p (&prop_mod, path, subpool));
}
/* 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;
/* Now, if this is something to commit, add it to our list. */
if (state_flags)
{
/* If the commit item is a directory, lock it, else lock its parent. */
if (entry->kind == svn_node_dir)
lock_dir (locked_dirs, path, pool);
else
lock_dir (locked_dirs, p_path, pool);
/* Finally, add the committable item. */
add_committable (committables, path, entry->kind, url,
cf_url ? entry->copyfrom_rev : entry->revision,
cf_url, state_flags);
}
/* For directories, recursively handle each of their entries (except
when the directory is being deleted, unless the deletion is part
of a replacement ... how confusing). */
if ((entries)
&& ((! (state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE))
|| (state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)))
{
apr_hash_index_t *hi;
svn_wc_entry_t *this_entry;
svn_stringbuf_t *full_path = svn_stringbuf_dup (path, subpool);
svn_stringbuf_t *this_url = svn_stringbuf_dup (url, subpool);
svn_stringbuf_t *this_cf_url
= cf_url ? svn_stringbuf_dup (cf_url, subpool) : NULL;
/* Loop over all other entries in this directory, skipping the
"this dir" entry. */
for (hi = apr_hash_first (subpool, entries); hi; hi = apr_hash_next (hi))
{
const void *key;
apr_ssize_t klen;
void *val;
const char *name;
svn_stringbuf_t *used_url = NULL;
/* Get the next entry */
apr_hash_this (hi, &key, &klen, &val);
name = (const char *) key;
/* Skip "this dir" */
if (! strcmp (name, SVN_WC_ENTRY_THIS_DIR))
continue;
/* Name is an entry name; value is an entry structure. */
this_entry = (svn_wc_entry_t *) val;
svn_path_add_component_nts (full_path, name);
if (this_cf_url)
svn_path_add_component_nts (this_cf_url, name);
/* We'll use the entry's URL if it has one and if we aren't
in copy_mode, else, we'll just extend the parent's URL
with the entry's basename. */
if ((! this_entry->url) || (copy_mode))
{
svn_path_add_component_nts (this_url, name);
used_url = this_url;
}
/* Recurse. */
SVN_ERR (harvest_committables
(committables, locked_dirs, full_path,
used_url ? used_url : this_entry->url,
this_cf_url,
(svn_wc_entry_t *)val,
adds_only,
copy_mode,
subpool));
/* Truncate paths back to their pre-loop state. */
svn_stringbuf_chop (full_path, klen + 1);
if (this_cf_url)
svn_stringbuf_chop (this_cf_url, klen + 1);
if (used_url)
svn_stringbuf_chop (this_url, klen + 1);
}
}
/* Destroy the subpool. */
svn_pool_destroy (subpool);
return SVN_NO_ERROR;
}
svn_error_t *
svn_client__harvest_committables (apr_hash_t **committables,
apr_hash_t **locked_dirs,
svn_stringbuf_t *parent_dir,
apr_array_header_t *targets,
apr_pool_t *pool)
{
int i = 0;
svn_stringbuf_t *target = svn_stringbuf_dup (parent_dir, pool);
/* Create the COMMITTABLES hash. */
*committables = apr_hash_make (pool);
/* Create the LOCKED_DIRS hash. */
*locked_dirs = apr_hash_make (pool);
do
{
svn_wc_entry_t *entry;
svn_stringbuf_t *url;
/* Add the relative portion of our full path (if there are no
relative paths, TARGET will just be PARENT_DIR for a single
iteration. */
if (targets->nelts)
svn_path_add_component (target,
(((svn_stringbuf_t **) targets->elts)[i]));
/* No entry? This TARGET isn't even under version control! */
SVN_ERR (svn_wc_entry (&entry, target, pool));
if (! entry)
return svn_error_create
(SVN_ERR_ENTRY_NOT_FOUND, 0, NULL, pool, target->data);
if (! entry->url)
{
svn_stringbuf_t *parent, *basename;
svn_wc_entry_t *p_entry;
svn_boolean_t wc_root;
/* An entry with no URL should only come about when it is
scheduled for addition or replacement. */
if (! ((entry->schedule == svn_wc_schedule_add)
|| (entry->schedule == svn_wc_schedule_replace)))
return svn_error_createf
(SVN_ERR_WC_CORRUPT, 0, NULL, pool,
"Entry for `%s' has no URL, yet is not scheduled for addition",
target->data);
/* Check for WC-root-ness. */
SVN_ERR (svn_wc_is_wc_root (&wc_root, target, pool));
if (wc_root)
return svn_error_createf
(SVN_ERR_ILLEGAL_TARGET, 0, NULL, pool,
"Entry for `%s' has no URL, and none can be derived for it",
target->data);
/* See if the parent is under version control (corruption if it
isn't) and possibly scheduled for addition (illegal target if
it is). */
svn_path_split (target, &parent, &basename, pool);
if (svn_path_is_empty (parent))
parent = svn_stringbuf_create (".", pool);
SVN_ERR (svn_wc_entry (&p_entry, parent, pool));
if (! p_entry)
return svn_error_createf
(SVN_ERR_WC_CORRUPT, 0, NULL, pool,
"Entry for `%s' has no URL, and its parent directory does"
"not appear to be under version control", target->data);
if ((p_entry->schedule == svn_wc_schedule_add)
|| (p_entry->schedule == svn_wc_schedule_replace))
return svn_error_createf
(SVN_ERR_ILLEGAL_TARGET, 0, NULL, pool,
"`%s' is the child an unversioned (or not-yet-versioned)"
"directory. Try committing the directory itself",
target->data);
/* Manufacture a URL for this TARGET. */
url = svn_stringbuf_dup (p_entry->url, pool);
svn_path_add_component (url, basename);
}
else
url = entry->url;
/* If this entry is marked as 'copied' but scheduled normally, then
it should be the child of something else marked for addition with
history. */
if ((entry->copied) && (entry->schedule == svn_wc_schedule_normal))
return svn_error_createf
(SVN_ERR_ILLEGAL_TARGET, 0, NULL, pool,
"Entry for `%s' is marked as `copied' but is not itself scheduled "
"for addition. Perhaps you're committing a target that this "
"inside of an unversioned (or not-yet-versioned) directory?",
target->data);
/* Handle our TARGET. */
SVN_ERR (harvest_committables (*committables, *locked_dirs, target,
url, NULL, entry, FALSE, FALSE, pool));
/* Reset our base path for the next iteration, and increment our
counter. */
svn_stringbuf_chop (target, target->len - parent_dir->len);
i++;
}
while (i < targets->nelts);
return SVN_NO_ERROR;
}
svn_error_t *
svn_client__get_copy_committables (apr_hash_t **committables,
apr_hash_t **locked_dirs,
svn_stringbuf_t *new_url,
svn_stringbuf_t *target,
apr_pool_t *pool)
{
svn_wc_entry_t *entry;
/* Create the COMMITTABLES hash. */
*committables = apr_hash_make (pool);
/* Create the LOCKED_DIRS hash. */
*locked_dirs = apr_hash_make (pool);
/* Read the entry for TARGET. */
SVN_ERR (svn_wc_entry (&entry, target, pool));
if (! entry)
return svn_error_create
(SVN_ERR_ENTRY_NOT_FOUND, 0, NULL, pool, target->data);
/* Handle our TARGET. */
SVN_ERR (harvest_committables (*committables, *locked_dirs, target,
new_url, entry->url, entry,
FALSE, TRUE, pool));
return SVN_NO_ERROR;
}
int svn_client__sort_commit_item_urls (const void *a, const void *b)
{
svn_client_commit_item_t *item1 = *((svn_client_commit_item_t **) a);
svn_client_commit_item_t *item2 = *((svn_client_commit_item_t **) b);
return svn_path_compare_paths (item1->url, item2->url);
}
svn_error_t *
svn_client__condense_commit_items (svn_stringbuf_t **base_url,
apr_array_header_t *commit_items,
apr_pool_t *pool)
{
apr_array_header_t *ci = commit_items; /* convenience */
svn_stringbuf_t *url;
svn_client_commit_item_t *item, *last_item = NULL;
int i;
assert (ci && ci->nelts);
/* Sort our commit items by their URLs. */
qsort (ci->elts, ci->nelts,
ci->elt_size, svn_client__sort_commit_item_urls);
/* Loop through the URLs, finding the longest usable ancestor common
to all of them, and making sure there are no duplicate URLs. */
for (i = 0; i < ci->nelts; i++)
{
item = (((svn_client_commit_item_t **) ci->elts)[i]);
url = item->url;
if ((last_item) && (svn_stringbuf_compare (last_item->url, url)))
return svn_error_createf
(SVN_ERR_CLIENT_DUPLICATE_COMMIT_URL, 0, NULL, pool,
"Cannot commit both `%s' and `%s' as they refer to the same URL.",
item->path->data, last_item->path->data);
/* In the first iteration, our BASE_URL is just are 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 = svn_stringbuf_dup (url, pool);
else
*base_url = svn_path_get_longest_ancestor (*base_url, url, pool);
/* If our BASE_URL is itself a to-be-committed item, and it is
anything other than an already-versioned directory with
property mods, we'll call its parent directory URL the
BASE_URL. Why? Because we can't have a file URL as our base
-- period -- and all other directory operations (removal,
addition, etc.) require that we open that directory's parent
dir first. */
if (((*base_url)->len == url->len)
&& (! ((item->kind == svn_node_dir)
&& item->state_flags == SVN_CLIENT_COMMIT_ITEM_PROP_MODS)))
svn_path_remove_component (*base_url);
/* Stash our item here for the next iteration. */
last_item = item;
}
/* Now that we've settled on a *BASE_URL, go hack that base off
of all of our URLs. */
for (i = 0; i < ci->nelts; i++)
{
url = (((svn_client_commit_item_t **) ci->elts)[i])->url;
if (url->len > (*base_url)->len)
{
memmove (url->data,
url->data + (*base_url)->len + 1,
url->len - (*base_url)->len - 1);
url->len = url->len - (*base_url)->len - 1;
url->data[url->len] = 0;
}
else
{
url->data[0] = 0;
url->len = 0;
}
}
#ifdef SVN_CLIENT_COMMIT_DEBUG
/* ### TEMPORARY CODE ### */
printf ("COMMITTABLES: (base url=%s)\n", (*base_url)->data);
for (i = 0; i < ci->nelts; i++)
{
url = (((svn_client_commit_item_t **) ci->elts)[i])->url;
printf (" %s\n", url->data ? url->data : "");
}
#endif /* SVN_CLIENT_COMMIT_DEBUG */
return SVN_NO_ERROR;
}
static svn_error_t *
init_stack (apr_array_header_t **db_stack,
int *stack_ptr,
const svn_delta_editor_t *editor,
void *edit_baton,
apr_pool_t *pool)
{
void *db;
/* Call the EDITOR's open_root function to get our first directory
baton. */
SVN_ERR (editor->open_root (edit_baton, SVN_INVALID_REVNUM, pool, &db));
/* Now allocate an array for dir_batons and push our first one
there, incrementing our stack pointer. */
*db_stack = apr_array_make (pool, 4, sizeof (void *));
(*((void **) apr_array_push (*db_stack))) = db;
*stack_ptr = 1;
return SVN_NO_ERROR;
}
static svn_error_t *
push_stack (const char *rel_url, /* relative to base url of commit */
apr_array_header_t *db_stack,
int *stack_ptr,
const svn_delta_editor_t *editor,
const char *copyfrom_path,
svn_revnum_t revision,
svn_boolean_t is_add,
apr_pool_t *pool)
{
void *parent_db, *db;
assert (db_stack && db_stack->nelts && *stack_ptr);
/* Call the EDITOR's open_directory function to get a new directory
baton. */
parent_db = ((void **) db_stack->elts)[*stack_ptr - 1];
if (is_add)
SVN_ERR (editor->add_directory (rel_url, parent_db, copyfrom_path,
revision, pool, &db));
else
SVN_ERR (editor->open_directory (rel_url, parent_db, revision, pool, &db));
/* If all our current stack space is in use, push the DB onto the
end of the array (which will allocate more space). Else, we will
just re-use a previously allocated slot. */
if (*stack_ptr == db_stack->nelts)
(*((void **) apr_array_push (db_stack))) = db;
else
((void **) db_stack->elts)[*stack_ptr] = db;
/* Increment our stack pointer and get outta here. */
(*stack_ptr)++;
return SVN_NO_ERROR;
}
static svn_error_t *
pop_stack (apr_array_header_t *db_stack,
int *stack_ptr,
const svn_delta_editor_t *editor)
{
void *db;
/* Decrement our stack pointer. */
(*stack_ptr)--;
/* Close the most recent directory pushed to the stack. */
db = ((void **) db_stack->elts)[*stack_ptr];
return editor->close_directory (db);
}
static int
count_components (const char *path)
{
int count = 1;
const char *instance = path;
if ((strlen (path) == 1) && (path[0] == '/'))
return 0;
do
{
instance++;
instance = strchr (instance, '/');
if (instance)
count++;
}
while (instance);
return count;
}
struct file_mod_t
{
svn_client_commit_item_t *item;
void *file_baton;
};
static svn_error_t *
do_item_commit (const char *url,
svn_client_commit_item_t *item,
const svn_delta_editor_t *editor,
apr_array_header_t *db_stack,
int *stack_ptr,
apr_array_header_t *file_mods,
apr_hash_t *tempfiles,
svn_wc_notify_func_t notify_func,
void *notify_baton,
svn_stringbuf_t *display_dir,
apr_pool_t *pool)
{
svn_node_kind_t kind = item->kind;
void *file_baton = NULL, *parent_baton = NULL, *dir_baton = NULL;
const char *copyfrom_url = item->copyfrom_url
? item->copyfrom_url->data
: NULL;
/* Get the parent dir_baton. */
parent_baton = ((void **) db_stack->elts)[*stack_ptr - 1];
/* If a feedback table was supplied by the application layer,
describe what we're about to do to this item. */
if (notify_func)
{
/* Convert an absolute path into a relative one (for feedback.) */
const char *path = item->path->data + (display_dir->len + 1);
if ((item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
&& (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD))
(*notify_func) (notify_baton, svn_wc_notify_commit_replaced, path);
else if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
(*notify_func) (notify_baton, svn_wc_notify_commit_deleted, path);
else if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
(*notify_func) (notify_baton, svn_wc_notify_commit_added, path);
else if ((item->state_flags & SVN_CLIENT_COMMIT_ITEM_TEXT_MODS)
|| (item->state_flags & SVN_CLIENT_COMMIT_ITEM_PROP_MODS))
(*notify_func) (notify_baton, svn_wc_notify_commit_modified, path);
}
/* If this item is supposed to be deleted, do so. */
if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE)
SVN_ERR (editor->delete_entry (url, item->revision,
parent_baton, pool));
/* If this item is supposed to be added, do so. */
if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
{
if (kind == svn_node_file)
{
SVN_ERR (editor->add_file
(url, parent_baton, copyfrom_url,
item->revision,
pool, &file_baton));
}
else
{
SVN_ERR (push_stack
(url, db_stack, stack_ptr, editor, copyfrom_url,
item->revision,
TRUE, pool));
dir_baton = ((void **) db_stack->elts)[*stack_ptr - 1];
}
}
/* Now handle property mods. */
if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_PROP_MODS)
{
svn_stringbuf_t *tempfile;
if (kind == svn_node_file)
{
if (! file_baton)
SVN_ERR (editor->open_file (url, parent_baton, item->revision,
pool, &file_baton));
}
else
{
if (! dir_baton)
{
SVN_ERR (push_stack (url, db_stack, stack_ptr, editor, NULL,
item->revision, FALSE, pool));
dir_baton = ((void **) db_stack->elts)[*stack_ptr - 1];
}
}
SVN_ERR (svn_wc_transmit_prop_deltas
(item->path, kind, editor,
(kind == svn_node_dir) ? dir_baton : file_baton,
&tempfile, pool));
if (tempfile && tempfiles)
apr_hash_set (tempfiles, tempfile->data, tempfile->len, (void *)1);
}
/* Finally, handle text mods (in that we need to open a file if it
hasn't already been opened, and we need to put the file baton in
our FILES hash). */
if ((kind == svn_node_file)
&& (item->state_flags & SVN_CLIENT_COMMIT_ITEM_TEXT_MODS))
{
struct file_mod_t mod;
if (! file_baton)
SVN_ERR (editor->open_file (url, parent_baton,
item->revision,
pool, &file_baton));
/* Copy in the contents of the mod structure to the array. Note
that this is NOT a copy of a pointer reference, but a copy of
the structure's contents!! */
mod.item = item;
mod.file_baton = file_baton;
(*((struct file_mod_t *) apr_array_push (file_mods))) = mod;
}
/* Close any outstanding file batons that didn't get caught by the
"has local mods" conditional above. */
else if (file_baton)
SVN_ERR (editor->close_file (file_baton));
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 char *base_url,
apr_pool_t *pool);
#endif /* SVN_CLIENT_COMMIT_DEBUG */
svn_error_t *
svn_client__do_commit (svn_stringbuf_t *base_url,
apr_array_header_t *commit_items,
const svn_delta_editor_t *editor,
void *edit_baton,
svn_wc_notify_func_t notify_func,
void *notify_baton,
svn_stringbuf_t *display_dir,
apr_hash_t **tempfiles,
apr_pool_t *pool)
{
apr_array_header_t *db_stack;
apr_array_header_t *file_mods
= apr_array_make (pool, 1, sizeof (struct file_mod_t));
int i, stack_ptr = 0;
#ifdef SVN_CLIENT_COMMIT_DEBUG
{
const svn_delta_editor_t *test_editor;
void *test_edit_baton;
SVN_ERR (get_test_editor (&test_editor, &test_edit_baton,
base_url->data, pool));
svn_delta_compose_editors (&editor, &edit_baton,
editor, edit_baton,
test_editor, test_edit_baton, pool);
}
#endif /* SVN_CLIENT_COMMIT_DEBUG */
/* If the caller wants us to track temporary file creation, create a
hash to store those paths in. */
if (tempfiles)
*tempfiles = apr_hash_make (pool);
/* We start by opening the root. */
SVN_ERR (init_stack (&db_stack, &stack_ptr, editor, edit_baton, pool));
/* Now, loop over the commit items, traversing the URL tree and
driving the editor. */
for (i = 0; i < commit_items->nelts; i++)
{
svn_stringbuf_t *last_url, *item_url, *item_dir, *item_name;
svn_stringbuf_t *common = NULL;
svn_client_commit_item_t *item
= ((svn_client_commit_item_t **) commit_items->elts)[i];
/* Get the next commit item URL. */
item_url = item->url;
/*** Step A - Find the common ancestor of the last commit item
and the current one. For the first iteration, this is just
the empty string. ***/
if (i > 0)
common = svn_path_get_longest_ancestor (last_url, item_url, pool);
if (! common)
common = svn_stringbuf_create ("", pool);
/*** Step B - Close any directories between the last commit item
and the new common ancestor, if any need to be closed.
Sometimes there is nothing to do here (like, for the first
iteration, or when the last commit item was an ancestor of
the current item). ***/
if ((i > 0) && (last_url->len > common->len))
{
char *rel = last_url->data + (common->len ? (common->len + 1) : 0);
int count = count_components (rel);
while (count--)
{
SVN_ERR (pop_stack (db_stack, &stack_ptr, editor));
}
}
/*** Step C - Open any directories between the common ancestor
and the parent of the commit item. ***/
svn_path_split (item_url, &item_dir, &item_name, pool);
if (item_dir->len > common->len)
{
char *rel = apr_pstrdup (pool, item_dir->data);
char *piece = rel + common->len + 1;
while (1)
{
/* Find the first separator. */
piece = strchr (piece, '/');
/* Temporarily replace it with a NULL terminator. */
if (piece)
*piece = 0;
/* Open the subdirectory. */
SVN_ERR (push_stack (rel, db_stack, &stack_ptr,
editor, NULL, SVN_INVALID_REVNUM,
FALSE, pool));
/* If we temporarily replaced a '/' with a NULL,
un-replace it and move our piece pointer to the
character after the '/' we found. If there was no
piece found, though, we're done. */
if (piece)
{
*piece = '/';
piece++;
}
else
break;
}
}
/*** Step D - Commit the item. ***/
SVN_ERR (do_item_commit (item_url->data, item, editor,
db_stack, &stack_ptr, file_mods, *tempfiles,
notify_func, notify_baton, display_dir, pool));
/* Save our state for the next iteration. */
if ((item->kind == svn_node_dir)
&& ((! (item->state_flags & SVN_CLIENT_COMMIT_ITEM_DELETE))
|| (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)))
last_url = item_url;
else
last_url = item_dir;
}
/* Close down any remaining open directory batons. */
while (stack_ptr)
{
SVN_ERR (pop_stack (db_stack, &stack_ptr, editor));
}
/* Transmit outstanding text deltas. */
for (i = 0; i < file_mods->nelts; i++)
{
struct file_mod_t *mod
= ((struct file_mod_t *) file_mods->elts) + i;
svn_client_commit_item_t *item = mod->item;
void *file_baton = mod->file_baton;
svn_stringbuf_t *tempfile;
svn_boolean_t fulltext = FALSE;
if (notify_func)
(*notify_func) (notify_baton, svn_wc_notify_commit_postfix_txdelta,
item->path->data);
if (item->state_flags & SVN_CLIENT_COMMIT_ITEM_ADD)
fulltext = TRUE;
SVN_ERR (svn_wc_transmit_text_deltas (item->path, fulltext,
editor, file_baton,
&tempfile, pool));
if (tempfile && *tempfiles)
apr_hash_set (*tempfiles, tempfile->data, tempfile->len, (void *)1);
}
/* Close the edit. */
SVN_ERR (editor->close_edit (edit_baton));
return SVN_NO_ERROR;
}
svn_client_commit_info_t *
svn_client__make_commit_info (svn_revnum_t revision,
const char *author,
const char *date,
apr_pool_t *pool)
{
svn_client_commit_info_t *info;
if (date || author || SVN_IS_VALID_REVNUM (revision))
{
info = apr_palloc (pool, sizeof (*info));
info->date = date ? apr_pstrdup (pool, date) : NULL;
info->author = author ? apr_pstrdup (pool, author) : NULL;
info->revision = revision;
return info;
}
return NULL;
}
#ifdef SVN_CLIENT_COMMIT_DEBUG
/*** Temporary test editor ***/
struct edit_baton
{
const char *path;
};
static void *
make_baton (const char *path, apr_pool_t *pool)
{
struct edit_baton *new_baton
= apr_pcalloc (pool, sizeof (struct edit_baton *));
new_baton->path = apr_pstrdup (pool, path);
return ((void *) new_baton);
}
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;
printf ("TEST EDIT STARTED (base url=%s)\n", eb->path);
*root_baton = make_baton (eb->path, dir_pool);
return SVN_NO_ERROR;
}
static svn_error_t *
add_item (const char *path,
void *parent_baton,
const char *copyfrom_path,
svn_revnum_t copyfrom_revision,
apr_pool_t *pool,
void **baton)
{
printf (" Adding : %s\n", path);
*baton = make_baton (path, pool);
return SVN_NO_ERROR;
}
static svn_error_t *
delete_entry (const char *path,
svn_revnum_t revision,
void *parent_baton,
apr_pool_t *pool)
{
printf (" Deleting: %s\n", path);
return SVN_NO_ERROR;
}
static svn_error_t *
open_item (const char *path,
void *parent_baton,
svn_revnum_t base_revision,
apr_pool_t *pool,
void **baton)
{
printf (" Opening : %s\n", path);
*baton = make_baton (path, pool);
return SVN_NO_ERROR;
}
static svn_error_t *
close_item (void *baton)
{
struct edit_baton *this = baton;
printf (" Closing : %s\n", this->path);
return SVN_NO_ERROR;
}
static svn_error_t *
change_prop (void *file_baton,
const char *name,
const svn_string_t *value,
apr_pool_t *pool)
{
printf (" PropSet (%s=%s)\n", name, value ? value->data : "");
return SVN_NO_ERROR;
}
static svn_error_t *
apply_textdelta (void *file_baton,
svn_txdelta_window_handler_t *handler,
void **handler_baton)
{
printf (" Transmitting text...\n");
*handler = *handler_baton = NULL;
return SVN_NO_ERROR;
}
static svn_error_t *
close_edit (void *edit_baton)
{
printf ("TEST EDIT COMPLETED\n");
return SVN_NO_ERROR;
}
static svn_error_t *
get_test_editor (const svn_delta_editor_t **editor,
void **edit_baton,
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);
ed->open_root = open_root;
ed->add_directory = add_item;
ed->open_directory = open_item;
ed->close_directory = close_item;
ed->add_file = add_item;
ed->open_file = open_item;
ed->close_file = close_item;
ed->delete_entry = delete_entry;
ed->apply_textdelta = apply_textdelta;
ed->change_dir_prop = change_prop;
ed->change_file_prop = change_prop;
ed->close_edit = close_edit;
*editor = ed;
*edit_baton = eb;
return SVN_NO_ERROR;
}
#endif /* SVN_CLIENT_COMMIT_DEBUG */
/*
* local variables:
* eval: (load-file "../../tools/dev/svn-dev.el")
* end: */