| /* |
| * 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: |
| */ |