blob: 4ffe672f9ef1fc3fe040a470a68d9b331ed41222 [file] [log] [blame]
/*
* update.c: handle the update-report request and response
*
* ====================================================================
* 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 <apr_pools.h>
#include <apr_strings.h>
#include <apr_xml.h>
#include <mod_dav.h>
#include "svn_pools.h"
#include "svn_repos.h"
#include "svn_fs.h"
#include "svn_xml.h"
#include "svn_path.h"
#include "dav_svn.h"
typedef struct {
const dav_resource *resource;
/* the revision we are updating to. used to generated IDs. */
svn_fs_root_t *rev_root;
const char *anchor;
/* if doing a regular update, then dst_path == anchor. if this is a
'switch' operation, then this field is the fs path that is being
switched to. This path needs to telescope in the update-editor
just like 'anchor' above; it's used for retrieving CR's and
vsn-url's during the edit. */
const char *dst_path;
/* this buffers the output for a bit and is automatically flushed,
at appropriate times, by the Apache filter system. */
apr_bucket_brigade *bb;
/* where to deliver the output */
ap_filter_t *output;
} update_ctx_t;
typedef struct {
apr_pool_t *pool;
update_ctx_t *uc;
const char *path; /* a telescoping extension of uc->anchor */
const char *path2; /* a telescoping extension of uc->dst_path */
svn_boolean_t added;
apr_array_header_t *changed_props;
apr_array_header_t *removed_props;
} item_baton_t;
#define DIR_OR_FILE(is_dir) ((is_dir) ? "directory" : "file")
static item_baton_t *make_child_baton(item_baton_t *parent, const char *name,
svn_boolean_t is_dir)
{
item_baton_t *baton;
apr_pool_t *pool;
if (is_dir)
pool = svn_pool_create(parent->pool);
else
pool = parent->pool;
baton = apr_pcalloc(pool, sizeof(*baton));
baton->pool = pool;
baton->uc = parent->uc;
/* Telescope the path based on uc->anchor. */
if (parent->path[1] == '\0') /* must be "/" */
baton->path = apr_pstrcat(pool, "/", name, NULL);
else
baton->path = apr_pstrcat(pool, parent->path, "/", name, NULL);
/* Telescope the path based on uc->dst_path in the exact same way. */
if (parent->path2[1] == '\0') /* must be "/" */
baton->path2 = apr_pstrcat(pool, "/", name, NULL);
else
baton->path2 = apr_pstrcat(pool, parent->path2, "/", name, NULL);
return baton;
}
static void send_xml(update_ctx_t *uc, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
(void) apr_brigade_vprintf(uc->bb, ap_filter_flush, uc->output, fmt, ap);
va_end(ap);
}
static void send_vsn_url(item_baton_t *baton)
{
svn_error_t *serr;
svn_fs_id_t *id;
svn_stringbuf_t *stable_id;
const char *href;
/* note: baton->path has a leading "/" */
serr = svn_fs_node_id(&id, baton->uc->rev_root, baton->path2, baton->pool);
if (serr != NULL)
{
/* ### what to do? */
return;
}
stable_id = svn_fs_unparse_id(id, baton->pool);
svn_stringbuf_appendcstr(stable_id, baton->path2);
href = dav_svn_build_uri(baton->uc->resource->info->repos,
DAV_SVN_BUILD_URI_VERSION,
SVN_INVALID_REVNUM, stable_id->data,
0 /* add_href */, baton->pool);
send_xml(baton->uc,
"<D:checked-in><D:href>%s</D:href></D:checked-in>" DEBUG_CR,
apr_xml_quote_string (baton->pool, href, 1));
}
static void add_helper(svn_boolean_t is_dir,
const char *name,
item_baton_t *parent,
svn_stringbuf_t *copyfrom_path,
svn_revnum_t copyfrom_revision,
void **child_baton)
{
item_baton_t *child;
const char *qname;
child = make_child_baton(parent, name, is_dir);
child->added = TRUE;
qname = apr_xml_quote_string(child->pool, name, 1);
if (copyfrom_path == NULL)
send_xml(child->uc, "<S:add-%s name=\"%s\">" DEBUG_CR,
DIR_OR_FILE(is_dir), qname);
else
{
const char *qcopy;
qcopy = apr_xml_quote_string(child->pool, copyfrom_path->data, 1);
send_xml(child->uc,
"<S:add-%s name=\"%s\" "
"copyfrom-path=\"%s\" copyfrom-rev=\"%ld\"/>" DEBUG_CR,
DIR_OR_FILE(is_dir),
qname, qcopy, copyfrom_revision);
}
send_vsn_url(child);
*child_baton = child;
}
static void open_helper(svn_boolean_t is_dir,
const char *name,
item_baton_t *parent,
svn_revnum_t base_revision,
void **child_baton)
{
item_baton_t *child;
const char *qname;
child = make_child_baton(parent, name, is_dir);
qname = apr_xml_quote_string(child->pool, name, 1);
/* ### Sat 24 Nov 2001: leaving this as "replace-" while clients get
upgraded. Will change to "open-" soon. -kff */
send_xml(child->uc, "<S:replace-%s name=\"%s\" rev=\"%ld\">" DEBUG_CR,
DIR_OR_FILE(is_dir), qname, base_revision);
send_vsn_url(child);
*child_baton = child;
}
static void close_helper(svn_boolean_t is_dir, item_baton_t *baton)
{
int i;
/* ### ack! binary names won't float here! */
if (baton->removed_props && (! baton->added))
{
svn_stringbuf_t *qname;
for (i = 0; i < baton->removed_props->nelts; i++)
{
/* We already XML-escaped the property name in change_xxx_prop. */
qname = ((svn_stringbuf_t **)(baton->removed_props->elts))[i];
send_xml(baton->uc, "<S:remove-prop name=\"%s\"/>" DEBUG_CR,
qname->data);
}
}
if (baton->changed_props && (! baton->added))
{
/* ### for now, we will simply tell the client to fetch all the
props */
send_xml(baton->uc, "<S:fetch-props/>" DEBUG_CR);
}
/* Unconditionally output the 3 CR-related properties right here.
### later on, compress via the 'scattered table' solution as
discussed with gstein. -bmcs */
{
svn_revnum_t committed_rev = SVN_INVALID_REVNUM;
svn_string_t *committed_date = NULL;
svn_string_t *last_author = NULL;
/* Get the CR and two derivative props. ### check for error returns. */
svn_fs_node_created_rev(&committed_rev,
baton->uc->rev_root, baton->path2, baton->pool);
svn_fs_revision_prop(&committed_date,
baton->uc->resource->info->repos->fs,
committed_rev, SVN_PROP_REVISION_DATE, baton->pool);
svn_fs_revision_prop(&last_author,
baton->uc->resource->info->repos->fs,
committed_rev, SVN_PROP_REVISION_AUTHOR, baton->pool);
/* ### grrr, these DAV: property names are already #defined in
ra_dav.h, and statically defined in liveprops.c. And now
they're hardcoded here. Isn't there some header file that both
sides of the network can share?? */
send_xml(baton->uc, "<S:prop>");
send_xml(baton->uc, "<D:version-name>%ld</D:version-name>",
committed_rev);
if (committed_date)
send_xml(baton->uc, "<D:creationdate>%s</D:creationdate>",
committed_date->data);
else
send_xml(baton->uc, "<S:remove-prop name=\"creationdate\"/>");
if (last_author)
send_xml(baton->uc, "<D:creator-displayname>%s</D:creator-displayname>",
last_author->data);
else
send_xml(baton->uc, "<S:remove-prop name=\"creator-displayname\"/>");
send_xml(baton->uc, "</S:prop>\n");
}
if (baton->added)
send_xml(baton->uc, "</S:add-%s>" DEBUG_CR, DIR_OR_FILE(is_dir));
else
/* ### Sat 24 Nov 2001: leaving this as "replace-" while clients get
upgraded. Will change to "open-" soon. -kff */
send_xml(baton->uc, "</S:replace-%s>" DEBUG_CR, DIR_OR_FILE(is_dir));
}
static svn_error_t * upd_set_target_revision(void *edit_baton,
svn_revnum_t target_revision)
{
update_ctx_t *uc = edit_baton;
send_xml(uc,
DAV_XML_HEADER DEBUG_CR
"<S:update-report xmlns:S=\"" SVN_XML_NAMESPACE "\" "
"xmlns:D=\"DAV:\">" DEBUG_CR
"<S:target-revision rev=\"%ld\"/>" DEBUG_CR, target_revision);
return NULL;
}
static svn_error_t * upd_open_root(void *edit_baton,
svn_revnum_t base_revision,
void **root_baton)
{
update_ctx_t *uc = edit_baton;
apr_pool_t *pool = svn_pool_create(uc->resource->pool);
item_baton_t *b = apr_pcalloc(pool, sizeof(*b));
/* note that we create a subpool; the root_baton is passed to the
close_directory callback, where we will destroy the pool. */
b->uc = uc;
b->pool = pool;
b->path = uc->anchor;
b->path2 = uc->dst_path;
*root_baton = b;
/* ### Sat 24 Nov 2001: leaving this as "replace-" while clients get
upgraded. Will change to "open-" soon. -kff */
send_xml(uc, "<S:replace-directory rev=\"%ld\">" DEBUG_CR, base_revision);
send_vsn_url(b);
return NULL;
}
static svn_error_t * upd_delete_entry(svn_stringbuf_t *name,
svn_revnum_t revision,
void *parent_baton)
{
item_baton_t *parent = parent_baton;
const char *qname;
qname = apr_xml_quote_string(parent->pool, name->data, 1);
send_xml(parent->uc, "<S:delete-entry name=\"%s\"/>" DEBUG_CR, qname);
return NULL;
}
static svn_error_t * upd_add_directory(svn_stringbuf_t *name,
void *parent_baton,
svn_stringbuf_t *copyfrom_path,
svn_revnum_t copyfrom_revision,
void **child_baton)
{
add_helper(TRUE /* is_dir */,
name->data, parent_baton, copyfrom_path, copyfrom_revision,
child_baton);
return NULL;
}
static svn_error_t * upd_open_directory(svn_stringbuf_t *name,
void *parent_baton,
svn_revnum_t base_revision,
void **child_baton)
{
open_helper(TRUE /* is_dir */,
name->data, parent_baton, base_revision, child_baton);
return NULL;
}
static svn_error_t * upd_change_xxx_prop(void *baton,
svn_stringbuf_t *name,
svn_stringbuf_t *value)
{
item_baton_t *b = baton;
svn_stringbuf_t *qname;
qname = svn_stringbuf_create (apr_xml_quote_string (b->pool, name->data, 1),
b->pool);
if (value)
{
if (! b->changed_props)
b->changed_props = apr_array_make (b->pool, 1, sizeof (name));
(*((svn_stringbuf_t **)(apr_array_push (b->changed_props)))) = qname;
}
else
{
if (! b->removed_props)
b->removed_props = apr_array_make (b->pool, 1, sizeof (name));
(*((svn_stringbuf_t **)(apr_array_push (b->removed_props)))) = qname;
}
return NULL;
}
static svn_error_t * upd_close_directory(void *dir_baton)
{
item_baton_t *dir = dir_baton;
close_helper(TRUE /* is_dir */, dir);
svn_pool_destroy(dir->pool);
return NULL;
}
static svn_error_t * upd_add_file(svn_stringbuf_t *name,
void *parent_baton,
svn_stringbuf_t *copyfrom_path,
svn_revnum_t copyfrom_revision,
void **file_baton)
{
add_helper(FALSE /* is_dir */,
name->data, parent_baton, copyfrom_path, copyfrom_revision,
file_baton);
return NULL;
}
static svn_error_t * upd_open_file(svn_stringbuf_t *name,
void *parent_baton,
svn_revnum_t base_revision,
void **file_baton)
{
open_helper(FALSE /* is_dir */,
name->data, parent_baton, base_revision, file_baton);
return NULL;
}
static svn_error_t * noop_handler(svn_txdelta_window_t *window, void *baton)
{
return NULL;
}
static svn_error_t * upd_apply_textdelta(void *file_baton,
svn_txdelta_window_handler_t *handler,
void **handler_baton)
{
item_baton_t *file = file_baton;
/* if we added the file, then no need to tell the client to fetch it */
if (!file->added)
send_xml(file->uc, "<S:fetch-file/>" DEBUG_CR);
*handler = noop_handler;
return NULL;
}
static svn_error_t * upd_close_file(void *file_baton)
{
close_helper(FALSE /* is_dir */, file_baton);
return NULL;
}
static svn_error_t * upd_close_edit(void *edit_baton)
{
update_ctx_t *uc = edit_baton;
send_xml(uc, "</S:update-report>" DEBUG_CR);
return NULL;
}
dav_error * dav_svn__update_report(const dav_resource *resource,
const apr_xml_doc *doc,
ap_filter_t *output)
{
svn_delta_edit_fns_t *editor;
apr_xml_elem *child;
void *rbaton;
update_ctx_t uc = { 0 };
svn_revnum_t revnum = SVN_INVALID_REVNUM;
int ns;
svn_error_t *serr;
const char *dst_path = NULL;
const char *dir_delta_target = NULL;
const dav_svn_repos *repos = resource->info->repos;
const char *target = NULL;
svn_boolean_t recurse = TRUE;
if (resource->type != DAV_RESOURCE_TYPE_REGULAR)
{
return dav_new_error(resource->pool, HTTP_CONFLICT, 0,
"This report can only be run against a "
"version-controlled resource.");
}
ns = dav_svn_find_ns(doc->namespaces, SVN_XML_NAMESPACE);
if (ns == -1)
{
return dav_new_error(resource->pool, HTTP_BAD_REQUEST, 0,
"The request does not contain the 'svn:' "
"namespace, so it is not going to have an "
"svn:target-revision element. That element "
"is required.");
}
for (child = doc->root->first_child; child != NULL; child = child->next)
{
if (child->ns == ns && strcmp(child->name, "target-revision") == 0)
{
/* ### assume no white space, no child elems, etc */
revnum = SVN_STR_TO_REV(child->first_cdata.first->text);
}
if (child->ns == ns && strcmp(child->name, "dst-path") == 0)
{
/* ### assume no white space, no child elems, etc */
dav_svn_uri_info this_info;
/* split up the 2nd public URL. */
serr = dav_svn_simple_parse_uri(&this_info, resource,
child->first_cdata.first->text,
resource->pool);
if (serr != NULL)
{
return dav_svn_convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
"Could not parse dst-path URL.");
}
dst_path = apr_pstrdup(resource->pool, this_info.repos_path);
}
if (child->ns == ns && strcmp(child->name, "update-target") == 0)
{
/* ### assume no white space, no child elems, etc */
target = child->first_cdata.first->text;
}
if (child->ns == ns && strcmp(child->name, "recursive") == 0)
{
/* ### assume no white space, no child elems, etc */
if (strcmp(child->first_cdata.first->text, "no") == 0)
recurse = FALSE;
}
}
if (revnum == SVN_INVALID_REVNUM)
{
serr = svn_fs_youngest_rev(&revnum, repos->fs, resource->pool);
if (serr != NULL)
{
return dav_svn_convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
"Could not determine the youngest "
"revision for the update process.");
}
}
/* If dst_path never came over the wire, then assume this is a
normal update. */
if (dst_path == NULL)
{
/* All vsn-urls and CR props should be mined from the normal
anchor of the update: */
dst_path = apr_pstrdup(resource->pool, resource->info->repos_path);
/* The 2nd argument to dir_delta should be [anchor + target]. */
dir_delta_target = dst_path;
if (target)
dir_delta_target = apr_pstrcat(resource->pool,
dir_delta_target, "/", target, NULL);
}
else /* this is some kind of 'switch' operation */
{
/* All vsn-urls and CR props will be mined from dst_path, which
should already be equal to the fs portion of the extra URL we
received. */
/* The 2nd argument to dir_delta should be the fs portion the
extra URL. */
dir_delta_target = dst_path;
}
editor = svn_delta_old_default_editor(resource->pool);
editor->set_target_revision = upd_set_target_revision;
editor->open_root = upd_open_root;
editor->delete_entry = upd_delete_entry;
editor->add_directory = upd_add_directory;
editor->open_directory = upd_open_directory;
editor->change_dir_prop = upd_change_xxx_prop;
editor->close_directory = upd_close_directory;
editor->add_file = upd_add_file;
editor->open_file = upd_open_file;
editor->apply_textdelta = upd_apply_textdelta;
editor->change_file_prop = upd_change_xxx_prop;
editor->close_file = upd_close_file;
editor->close_edit = upd_close_edit;
uc.resource = resource;
uc.output = output;
uc.anchor = resource->info->repos_path;
uc.dst_path = dst_path;
uc.bb = apr_brigade_create(resource->pool, output->c->bucket_alloc);
/* Get the root of the revision we want to update to. This will be used
to generated stable id values. */
serr = svn_fs_revision_root(&uc.rev_root, repos->fs, revnum, resource->pool);
if (serr != NULL)
{
return dav_svn_convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
"The revision root could not be created.");
}
/* When we call svn_repos_finish_report, it will ultimately run
dir_delta() between REPOS_PATH/TARGET and TARGET_PATH. In the
case of an update or status, these paths should be identical. In
the case of a switch, they should be different. */
serr = svn_repos_begin_report(&rbaton, revnum, repos->username,
repos->repos,
resource->info->repos_path, target,
dir_delta_target,
FALSE, /* don't send text-deltas */
recurse,
editor, &uc, resource->pool);
if (serr != NULL)
{
return dav_svn_convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
"The state report gatherer could not be "
"created.");
}
/* scan the XML doc for state information */
for (child = doc->root->first_child; child != NULL; child = child->next)
if (child->ns == ns)
{
if (strcmp(child->name, "entry") == 0)
{
svn_revnum_t rev;
const char *path;
/* ### assume first/only attribute is the rev */
rev = SVN_STR_TO_REV(child->attr->value);
/* get cdata, stipping whitespace */
path = dav_xml_get_cdata(child, resource->pool, 1);
serr = svn_repos_set_path(rbaton, path, rev);
if (serr != NULL)
{
/* ### This removes the fs txn. todo: check error. */
svn_repos_abort_report(rbaton);
return dav_svn_convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
"A failure occurred while "
"recording one of the items of "
"working copy state.");
}
}
else if (strcmp(child->name, "missing") == 0)
{
const char *path;
/* get cdata, stipping whitespace */
path = dav_xml_get_cdata(child, resource->pool, 1);
serr = svn_repos_delete_path(rbaton, path);
if (serr != NULL)
{
/* ### This removes the fs txn. todo: check error. */
svn_repos_abort_report(rbaton);
return dav_svn_convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
"A failure occurred while "
"recording one of the (missing) "
"items of working copy state.");
}
}
}
/* this will complete the report, and then drive our editor to generate
the response to the client. */
serr = svn_repos_finish_report(rbaton);
/* flush the contents of the brigade */
ap_fflush(output, uc.bb);
if (serr != NULL)
{
/* ### This removes the fs txn. todo: check error. */
svn_repos_abort_report(rbaton);
return dav_svn_convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
"A failure occurred during the completion "
"and response generation for the update "
"report.");
}
return NULL;
}
/*
* local variables:
* eval: (load-file "../../tools/dev/svn-dev.el")
* end:
*/