blob: 25b2c6069cfaaf58e8811a45cb859433e63ea652 [file] [log] [blame]
/*
* version.c: mod_dav_svn versioning provider functions for Subversion
*
* ====================================================================
* 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 <httpd.h>
#include <mod_dav.h>
#include <apr_tables.h>
#include "svn_fs.h"
#include "svn_xml.h"
#include "svn_repos.h"
#include "svn_dav.h"
#include "dav_svn.h"
/* ### should move these report names to a public header to share with
### the client (and third parties). */
static const dav_report_elem avail_reports[] = {
{ SVN_XML_NAMESPACE, "update-report" },
{ SVN_XML_NAMESPACE, "log-report" },
{ NULL },
};
static dav_error *open_txn(svn_fs_txn_t **ptxn, svn_fs_t *fs,
const char *txn_name, apr_pool_t *pool)
{
svn_error_t *serr;
serr = svn_fs_open_txn(ptxn, fs, txn_name, pool);
if (serr != NULL)
{
if (serr->apr_err == SVN_ERR_FS_NO_SUCH_TRANSACTION)
{
/* ### correct HTTP error? */
return dav_svn_convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
"The transaction specified by the "
"activity does not exist");
}
/* ### correct HTTP error? */
return dav_svn_convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
"There was a problem opening the "
"transaction specified by this "
"activity.");
}
return NULL;
}
static void dav_svn_get_vsn_options(apr_pool_t *p, ap_text_header *phdr)
{
/* Note: we append pieces with care for Web Folders's 63-char limit
on the DAV: header */
ap_text_append(p, phdr,
"version-control,checkout,working-resource");
ap_text_append(p, phdr,
"merge,baseline,activity,version-controlled-collection");
/* ### fork-control? */
}
static dav_error *dav_svn_get_option(const dav_resource *resource,
const ap_xml_elem *elem,
ap_text_header *option)
{
/* ### DAV:version-history-collection-set */
if (elem->ns == AP_XML_NS_DAV_ID)
{
if (strcmp(elem->name, "activity-collection-set") == 0)
{
ap_text_append(resource->pool, option,
"<D:activity-collection-set>");
ap_text_append(resource->pool, option,
dav_svn_build_uri(resource->info->repos,
DAV_SVN_BUILD_URI_ACT_COLLECTION,
SVN_INVALID_REVNUM, NULL,
1 /* add_href */, resource->pool));
ap_text_append(resource->pool, option,
"</D:activity-collection-set>");
}
}
return NULL;
}
static int dav_svn_versionable(const dav_resource *resource)
{
return 0;
}
static dav_auto_version dav_svn_auto_versionable(const dav_resource *resource)
{
return 0;
}
static dav_error *dav_svn_vsn_control(dav_resource *resource,
const char *target)
{
return dav_new_error_tag(resource->pool, HTTP_NOT_IMPLEMENTED,
SVN_ERR_UNSUPPORTED_FEATURE,
"VERSION-CONTROL is not yet implemented.",
SVN_DAV_ERROR_NAMESPACE,
SVN_DAV_ERROR_TAG);
}
static dav_error *dav_svn_checkout(dav_resource *resource,
int auto_checkout,
int is_unreserved, int is_fork_ok,
int create_activity,
apr_array_header_t *activities,
dav_resource **working_resource)
{
const char *txn_name;
svn_error_t *serr;
dav_svn_uri_info parse;
if (resource->type != DAV_RESOURCE_TYPE_VERSION)
{
return dav_new_error_tag(resource->pool, HTTP_METHOD_NOT_ALLOWED,
SVN_ERR_UNSUPPORTED_FEATURE,
"CHECKOUT can only be performed on a version "
"resource [at this time].",
SVN_DAV_ERROR_NAMESPACE,
SVN_DAV_ERROR_TAG);
}
if (create_activity)
{
return dav_new_error_tag(resource->pool, HTTP_NOT_IMPLEMENTED,
SVN_ERR_UNSUPPORTED_FEATURE,
"CHECKOUT can not create an activity at this "
"time. Use MKACTIVITY first.",
SVN_DAV_ERROR_NAMESPACE,
SVN_DAV_ERROR_TAG);
}
if (is_unreserved)
{
return dav_new_error_tag(resource->pool, HTTP_NOT_IMPLEMENTED,
SVN_ERR_UNSUPPORTED_FEATURE,
"Unreserved checkouts are not yet available. "
"A version history may not be checked out more "
"than once, into a specific activity.",
SVN_DAV_ERROR_NAMESPACE,
SVN_DAV_ERROR_TAG);
}
if (activities == NULL)
{
return dav_new_error_tag(resource->pool, HTTP_CONFLICT,
SVN_ERR_INCOMPLETE_DATA,
"An activity must be provided for checkout.",
SVN_DAV_ERROR_NAMESPACE,
SVN_DAV_ERROR_TAG);
}
/* assert: nelts > 0. the below check effectively means > 1. */
if (activities->nelts != 1)
{
return dav_new_error_tag(resource->pool, HTTP_CONFLICT,
SVN_ERR_INCORRECT_PARAMS,
"Only one activity may be specified within the "
"CHECKOUT.",
SVN_DAV_ERROR_NAMESPACE,
SVN_DAV_ERROR_TAG);
}
serr = dav_svn_simple_parse_uri(&parse, resource,
APR_ARRAY_IDX(activities, 0, const char *),
resource->pool);
if (serr != NULL)
{
/* ### is BAD_REQUEST proper? */
return dav_svn_convert_err(serr, HTTP_CONFLICT,
"The activity href could not be parsed "
"properly.");
}
if (parse.activity_id == NULL)
{
return dav_new_error_tag(resource->pool, HTTP_CONFLICT,
SVN_ERR_INCORRECT_PARAMS,
"The provided href is not an activity URI.",
SVN_DAV_ERROR_NAMESPACE,
SVN_DAV_ERROR_TAG);
}
if ((txn_name = dav_svn_get_txn(resource->info->repos,
parse.activity_id)) == NULL)
{
return dav_new_error_tag(resource->pool, HTTP_CONFLICT,
SVN_ERR_APMOD_ACTIVITY_NOT_FOUND,
"The specified activity does not exist.",
SVN_DAV_ERROR_NAMESPACE,
SVN_DAV_ERROR_TAG);
}
/* verify the specified version resource is the "latest", thus allowing
changes to be made. */
if (resource->baselined || resource->info->node_id == NULL)
{
/* a Baseline, or a standard Version Resource which was accessed
via a Label against a VCR within a Baseline Collection. */
/* ### at the moment, this branch is only reached for baselines */
svn_revnum_t youngest;
/* make sure the baseline being checked out is the latest */
serr = svn_fs_youngest_rev(&youngest, resource->info->repos->fs,
resource->pool);
if (serr != NULL)
{
/* ### correct HTTP error? */
return dav_svn_convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
"Could not determine the youngest "
"revision for verification against "
"the baseline being checked out.");
}
if (resource->info->root.rev != youngest)
{
return dav_new_error_tag(resource->pool, HTTP_CONFLICT,
SVN_ERR_APMOD_BAD_BASELINE,
"The specified baseline is not the latest "
"baseline, so it may not be checked out.",
SVN_DAV_ERROR_NAMESPACE,
SVN_DAV_ERROR_TAG);
}
/* ### hmm. what if the transaction root's revision is different
### from this baseline? i.e. somebody created a new revision while
### we are processing this commit.
###
### first question: what does the client *do* with a working
### baseline? knowing that, and how it maps to our backend, then
### we can figure out what to do here. */
}
else
{
/* standard Version Resource */
svn_fs_txn_t *txn;
svn_fs_root_t *txn_root;
dav_error *err;
int matches;
/* open the specified transaction so that we can verify this version
resource corresponds to the current/latest in the transaction. */
if ((err = open_txn(&txn, resource->info->repos->fs, txn_name,
resource->pool)) != NULL)
return err;
serr = svn_fs_txn_root(&txn_root, txn, resource->pool);
if (serr != NULL)
{
/* ### correct HTTP error? */
return dav_svn_convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
"Could not open the transaction tree.");
}
/* assert: repos_path != NULL (for this type of resource) */
serr = svn_fs_txn_path_is_id(&matches, txn_root,
resource->info->repos_path,
resource->info->node_id,
resource->pool);
if (serr != NULL)
{
/* ### correct HTTP error? */
return dav_svn_convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
"Could not match the node ID.");
}
if (!matches)
{
/* The node ID they are trying to change does not match what was
found in the transaction. That means they are trying to modify
an old (out of date) revision of the resource, or they are
trying to modify a *newer* revision.
If the version resource is *newer* than the transaction
root, then the client started a commit, a new revision was
created within the repository, the client fetched the new
resource from that new revision, changed it (or merged in
a prior change), and then attempted to incorporate that
into the commit that was initially started.
So yes, it is possible to happen. And we could copy that new
node into our transaction and then modify it. But screw
that. We can stop the commit, and everything will be fine
again if the user simply restarts it (because we'll use
that new revision as the transaction root, thus incorporating
the new resource, which they will then modify).
*/
#if 1
return dav_new_error_tag
(resource->pool, HTTP_CONFLICT, SVN_ERR_FS_CONFLICT,
"The version resource does not correspond "
"to the resource within the transaction. "
"Either the requested version resource is "
"out of date (needs to be updated), or the "
"requested version resource is newer than "
"the transaction root (restart the "
"commit).",
SVN_DAV_ERROR_NAMESPACE,
SVN_DAV_ERROR_TAG);
#else
/* ### some debugging code */
svn_fs_id_t *res_id;
svn_stringbuf_t *r_id;
svn_stringbuf_t *t_id;
const char *msg;
(void) svn_fs_node_id (&res_id, txn_root,
resource->info->repos_path, resource->pool);
r_id = svn_fs_unparse_id(resource->info->node_id, resource->pool);
t_id = svn_fs_unparse_id(res_id, resource->pool);
msg = apr_psprintf(resource->pool, "id mismatch: r=%s t=%s",
r_id->data, t_id->data);
return dav_new_error_tag(resource->pool, HTTP_CONFLICT,
SVN_ERR_FS_CONFLICT, msg,
SVN_DAV_ERROR_NAMESPACE,
SVN_DAV_ERROR_TAG);
#endif
}
}
*working_resource = dav_svn_create_working_resource(resource,
parse.activity_id,
txn_name);
return NULL;
}
static dav_error *dav_svn_uncheckout(dav_resource *resource)
{
return dav_new_error_tag(resource->pool, HTTP_NOT_IMPLEMENTED,
SVN_ERR_UNSUPPORTED_FEATURE,
"UNCHECKOUT is not yet implemented.",
SVN_DAV_ERROR_NAMESPACE,
SVN_DAV_ERROR_TAG);
}
static dav_error *dav_svn_checkin(dav_resource *resource,
int keep_checked_out,
dav_resource **version_resource)
{
return dav_new_error_tag(resource->pool, HTTP_NOT_IMPLEMENTED,
SVN_ERR_UNSUPPORTED_FEATURE,
"CHECKIN is not yet implemented.",
SVN_DAV_ERROR_NAMESPACE,
SVN_DAV_ERROR_TAG);
}
static dav_error *dav_svn_avail_reports(const dav_resource *resource,
const dav_report_elem **reports)
{
/* ### further restrict to the public space? */
if (resource->type != DAV_RESOURCE_TYPE_REGULAR) {
*reports = NULL;
return NULL;
}
*reports = avail_reports;
return NULL;
}
static int dav_svn_report_label_header_allowed(const ap_xml_doc *doc)
{
return 0;
}
static dav_error *dav_svn_deliver_report(request_rec *r,
const dav_resource *resource,
const ap_xml_doc *doc,
ap_filter_t *output)
{
int ns = dav_svn_find_ns(doc->namespaces, SVN_XML_NAMESPACE);
if (doc->root->ns == ns)
{
/* ### note that these report names should have symbols... */
if (strcmp(doc->root->name, "update-report") == 0)
{
return dav_svn__update_report(resource, doc, output);
}
if (strcmp(doc->root->name, "log-report") == 0)
{
return dav_svn__log_report(resource, doc, output);
}
}
/* ### what is a good error for an unknown report? */
return dav_new_error_tag(resource->pool, HTTP_NOT_IMPLEMENTED,
SVN_ERR_UNSUPPORTED_FEATURE,
"The requested report is unknown.",
SVN_DAV_ERROR_NAMESPACE,
SVN_DAV_ERROR_TAG);
}
static int dav_svn_can_be_activity(const dav_resource *resource)
{
return resource->type == DAV_RESOURCE_TYPE_ACTIVITY && !resource->exists;
}
static dav_error *dav_svn_make_activity(dav_resource *resource)
{
const char *activity_id = resource->info->root.activity_id;
const char *txn_name;
dav_error *err;
/* ### need to check some preconditions? */
err = dav_svn_create_activity(resource->info->repos, &txn_name,
resource->pool);
if (err != NULL)
return err;
err = dav_svn_store_activity(resource->info->repos, activity_id, txn_name);
if (err != NULL)
return err;
/* everything is happy. update the resource */
resource->info->root.txn_name = txn_name;
resource->exists = 1;
return NULL;
}
static dav_error *dav_svn_merge(dav_resource *target, dav_resource *source,
int no_auto_merge, int no_checkout,
ap_xml_elem *prop_elem,
ap_filter_t *output)
{
apr_pool_t *pool;
dav_error *err;
svn_fs_txn_t *txn;
const char *conflict;
svn_error_t *serr;
svn_revnum_t new_rev;
/* We'll use the target's pool for our operation. We happen to know that
it matches the request pool, which (should) have the proper lifetime. */
pool = target->pool;
/* ### what to verify on the target? */
/* ### anything else for the source? */
if (source->type != DAV_RESOURCE_TYPE_ACTIVITY)
{
return dav_new_error_tag(pool, HTTP_METHOD_NOT_ALLOWED,
SVN_ERR_INCORRECT_PARAMS,
"MERGE can only be performed using an activity "
"as the source [at this time].",
SVN_DAV_ERROR_NAMESPACE,
SVN_DAV_ERROR_TAG);
}
/* We will ignore no_auto_merge and no_checkout. We can't do those, but the
client has no way to assert that we *should* do them. This should be fine
because, presumably, the client has no way to do the various checkouts
and things that would necessitate an auto-merge or checkout during the
MERGE processing. */
/* open the transaction that we're going to commit. */
if ((err = open_txn(&txn, source->info->repos->fs,
source->info->root.txn_name, pool)) != NULL)
return err;
/* all righty... commit the bugger. */
serr = svn_repos_fs_commit_txn(&conflict, source->info->repos->repos,
&new_rev, txn);
if (serr != NULL)
{
const char *msg;
svn_fs_abort_txn(txn);
if (serr->apr_err == SVN_ERR_FS_CONFLICT)
{
/* ### we need to convert the conflict path into a URI */
msg = apr_psprintf(pool,
"A conflict occurred during the MERGE "
"processing. The problem occurred with the "
"\"%s\" resource.",
conflict);
}
else
msg = "An error occurred while committing the transaction.";
return dav_svn_convert_err(serr, HTTP_CONFLICT, msg);
}
/* process the response for the new revision. */
return dav_svn__merge_response(output, source->info->repos, new_rev,
prop_elem, pool);
}
const dav_hooks_vsn dav_svn_hooks_vsn = {
dav_svn_get_vsn_options,
dav_svn_get_option,
dav_svn_versionable,
dav_svn_auto_versionable,
dav_svn_vsn_control,
dav_svn_checkout,
dav_svn_uncheckout,
dav_svn_checkin,
dav_svn_avail_reports,
dav_svn_report_label_header_allowed,
dav_svn_deliver_report,
NULL, /* update */
NULL, /* add_label */
NULL, /* remove_label */
NULL, /* can_be_workspace */
NULL, /* make_workspace */
dav_svn_can_be_activity,
dav_svn_make_activity,
dav_svn_merge,
};
/*
* local variables:
* eval: (load-file "../../tools/dev/svn-dev.el")
* end:
*/