/* * editor.c: Editor for modifying FS transactions * * ==================================================================== * 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 #include "svn_types.h" #include "svn_error.h" #include "svn_pools.h" #include "svn_fs.h" #include "svn_props.h" #include "svn_path.h" #include "svn_private_config.h" #include "fs-loader.h" #include "private/svn_fspath.h" #include "private/svn_fs_private.h" #include "private/svn_editor.h" struct edit_baton { /* The transaction associated with this editor. */ svn_fs_txn_t *txn; /* Has this editor been completed? */ svn_boolean_t completed; /* We sometimes need the cancellation beyond what svn_editor_t provides */ svn_cancel_func_t cancel_func; void *cancel_baton; /* The pool that the txn lives within. When we create a ROOT, it will be allocated within a subpool of this. The root will be closed in complete/abort and that subpool will be destroyed. This pool SHOULD NOT be used for any allocations. */ apr_pool_t *txn_pool; /* This is the root from the txn. Use get_root() to fetch/create this member as appropriate. */ svn_fs_root_t *root; }; #define FSPATH(relpath, pool) apr_pstrcat(pool, "/", relpath, NULL) #define UNUSED(x) ((void)(x)) static svn_error_t * get_root(svn_fs_root_t **root, struct edit_baton *eb) { if (eb->root == NULL) SVN_ERR(svn_fs_txn_root(&eb->root, eb->txn, eb->txn_pool)); *root = eb->root; return SVN_NO_ERROR; } /* Apply each property in PROPS to the node at FSPATH in ROOT. */ static svn_error_t * add_new_props(svn_fs_root_t *root, const char *fspath, apr_hash_t *props, apr_pool_t *scratch_pool) { apr_pool_t *iterpool = svn_pool_create(scratch_pool); apr_hash_index_t *hi; /* ### it would be nice to have svn_fs_set_node_props(). but since we ### don't... add each property to the node. this is a new node, so ### we don't need to worry about deleting props. just adding. */ for (hi = apr_hash_first(scratch_pool, props); hi; hi = apr_hash_next(hi)) { const char *name = svn__apr_hash_index_key(hi); const svn_string_t *value = svn__apr_hash_index_val(hi); svn_pool_clear(iterpool); SVN_ERR(svn_fs_change_node_prop(root, fspath, name, value, iterpool)); } svn_pool_destroy(iterpool); return SVN_NO_ERROR; } static svn_error_t * alter_props(svn_fs_root_t *root, const char *fspath, apr_hash_t *props, apr_pool_t *scratch_pool) { apr_pool_t *iterpool = svn_pool_create(scratch_pool); apr_hash_t *old_props; apr_array_header_t *propdiffs; int i; SVN_ERR(svn_fs_node_proplist(&old_props, root, fspath, scratch_pool)); SVN_ERR(svn_prop_diffs(&propdiffs, props, old_props, scratch_pool)); for (i = 0; i < propdiffs->nelts; ++i) { const svn_prop_t *prop = &APR_ARRAY_IDX(propdiffs, i, svn_prop_t); svn_pool_clear(iterpool); /* Add, change, or delete properties. */ SVN_ERR(svn_fs_change_node_prop(root, fspath, prop->name, prop->value, iterpool)); } svn_pool_destroy(iterpool); return SVN_NO_ERROR; } static svn_error_t * set_text(svn_fs_root_t *root, const char *fspath, const svn_checksum_t *checksum, svn_stream_t *contents, svn_cancel_func_t cancel_func, void *cancel_baton, apr_pool_t *scratch_pool) { svn_stream_t *fs_contents; /* ### We probably don't have an MD5 checksum, so no digest is available ### for svn_fs_apply_text() to validate. It would be nice to have an ### FS API that takes our CHECKSUM/CONTENTS pair (and PROPS!). */ SVN_ERR(svn_fs_apply_text(&fs_contents, root, fspath, NULL /* result_checksum */, scratch_pool)); SVN_ERR(svn_stream_copy3(contents, fs_contents, cancel_func, cancel_baton, scratch_pool)); return SVN_NO_ERROR; } /* The caller wants to modify REVISION of FSPATH. Is that allowed? */ static svn_error_t * can_modify(svn_fs_root_t *txn_root, const char *fspath, svn_revnum_t revision, apr_pool_t *scratch_pool) { svn_revnum_t created_rev; /* Out-of-dateness check: compare the created-rev of the node in the txn against the created-rev of FSPATH. */ SVN_ERR(svn_fs_node_created_rev(&created_rev, txn_root, fspath, scratch_pool)); /* Uncommitted nodes (eg. a descendent of a copy/move/rotate destination) have no (committed) revision number. Let the caller go ahead and modify these nodes. Note: strictly speaking, they might be performing an "illegal" edit in certain cases, but let's just assume they're Good Little Boys. If CREATED_REV is invalid, that means it's already mutable in the txn, which means it has already passed this out-of-dateness check. (Usually, this happens when looking at a parent directory of an already-modified node) */ if (!SVN_IS_VALID_REVNUM(created_rev)) return SVN_NO_ERROR; /* If the node is immutable (has a revision), then the caller should have supplied a valid revision number [that they expect to change]. The checks further below will determine the out-of-dateness of the specified revision. */ /* ### ugh. descendents of copy/move/rotate destinations carry along ### their original immutable state and (thus) a valid CREATED_REV. ### but they are logically uncommitted, so the caller will pass ### SVN_INVALID_REVNUM. (technically, the caller could provide ### ORIGINAL_REV, but that is semantically incorrect for the Ev2 ### API). ### ### for now, we will assume the caller knows what they are doing ### and an invalid revision implies such a descendent. in the ### future, we could examine the ancestor chain looking for a ### copy/move/rotate-here node and allow the modification (and the ### converse: if no such ancestor, the caller must specify the ### correct/intended revision to modify). */ #if 1 if (!SVN_IS_VALID_REVNUM(revision)) return SVN_NO_ERROR; #else if (!SVN_IS_VALID_REVNUM(revision)) /* ### use a custom error code? */ return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL, _("Revision for modifying '%s' is required"), fspath); #endif if (revision < created_rev) { /* We asked to change a node that is *older* than what we found in the transaction. The client is out of date. */ return svn_error_createf(SVN_ERR_FS_OUT_OF_DATE, NULL, _("'%s' is out of date; try updating"), fspath); } if (revision > created_rev) { /* We asked to change a node that is *newer* than what we found in the transaction. Given that the transaction was based off of 'youngest', then either: - the caller asked to modify a future node - the caller has committed more revisions since this txn was constructed, and is asking to modify a node in one of those new revisions. In either case, the node may not have changed in those new revisions; use the node's ID to determine this case. */ const svn_fs_id_t *txn_noderev_id; svn_fs_root_t *rev_root; const svn_fs_id_t *new_noderev_id; /* The ID of the node that we would be modifying in the txn */ SVN_ERR(svn_fs_node_id(&txn_noderev_id, txn_root, fspath, scratch_pool)); /* Get the ID from the future/new revision. */ SVN_ERR(svn_fs_revision_root(&rev_root, svn_fs_root_fs(txn_root), revision, scratch_pool)); SVN_ERR(svn_fs_node_id(&new_noderev_id, rev_root, fspath, scratch_pool)); svn_fs_close_root(rev_root); /* Has the target node changed in the future? */ if (svn_fs_compare_ids(txn_noderev_id, new_noderev_id) != 0) { /* Restarting the commit will base the txn on the future/new revision, allowing the modification at REVISION. */ /* ### use a custom error code */ return svn_error_createf(SVN_ERR_FS_CONFLICT, NULL, _("'%s' has been modified since the " "commit began (restart the commit)"), fspath); } } return SVN_NO_ERROR; } /* Can we create a node at FSPATH in TXN_ROOT? If something already exists at that path, then the client MAY be out of date. We then have to see if the path was created/modified in this transaction. IOW, it is new and can be replaced without problem. Note: the editor protocol disallows double-modifications. This is to ensure somebody does not accidentally overwrite another file due to being out-of-date. */ static svn_error_t * can_create(svn_fs_root_t *txn_root, const char *fspath, apr_pool_t *scratch_pool) { svn_node_kind_t kind; const char *cur_fspath; SVN_ERR(svn_fs_check_path(&kind, txn_root, fspath, scratch_pool)); if (kind == svn_node_none) return SVN_NO_ERROR; /* ### I'm not sure if this works perfectly. We might have an ancestor ### that was modified as a result of a change on a cousin. We might ### misinterpret that as a *-here node which brought along this ### child. Need to write a test to verify. We may also be able to ### test the ancestor to determine if it has been *-here in this ### txn, or just a simple modification. */ /* Are any of the parents copied/moved/rotated-here? */ for (cur_fspath = fspath; strlen(cur_fspath) > 1; /* not the root */ cur_fspath = svn_fspath__dirname(cur_fspath, scratch_pool)) { svn_revnum_t created_rev; SVN_ERR(svn_fs_node_created_rev(&created_rev, txn_root, cur_fspath, scratch_pool)); if (!SVN_IS_VALID_REVNUM(created_rev)) { /* The node has no created revision, meaning it is uncommitted. Thus, it was created in this transaction, or it has already been modified in some way (implying it has already passed a modification check. */ /* ### verify the node has been *-here ?? */ return SVN_NO_ERROR; } } return svn_error_createf(SVN_ERR_FS_OUT_OF_DATE, NULL, _("'%s' already exists, so may be out" " of date; try updating"), fspath); } /* This implements svn_editor_cb_add_directory_t */ static svn_error_t * add_directory_cb(void *baton, const char *relpath, const apr_array_header_t *children, apr_hash_t *props, svn_revnum_t replaces_rev, apr_pool_t *scratch_pool) { struct edit_baton *eb = baton; const char *fspath = FSPATH(relpath, scratch_pool); svn_fs_root_t *root; /* Note: we ignore CHILDREN. We have no "incomplete" state to worry about, so we don't need to be aware of what children will be created. */ SVN_ERR(get_root(&root, eb)); if (SVN_IS_VALID_REVNUM(replaces_rev)) { SVN_ERR(can_modify(root, fspath, replaces_rev, scratch_pool)); SVN_ERR(svn_fs_delete(root, fspath, scratch_pool)); } else { SVN_ERR(can_create(root, fspath, scratch_pool)); } SVN_ERR(svn_fs_make_dir(root, fspath, scratch_pool)); SVN_ERR(add_new_props(root, fspath, props, scratch_pool)); return SVN_NO_ERROR; } /* This implements svn_editor_cb_add_file_t */ static svn_error_t * add_file_cb(void *baton, const char *relpath, const svn_checksum_t *checksum, svn_stream_t *contents, apr_hash_t *props, svn_revnum_t replaces_rev, apr_pool_t *scratch_pool) { struct edit_baton *eb = baton; const char *fspath = FSPATH(relpath, scratch_pool); svn_fs_root_t *root; SVN_ERR(get_root(&root, eb)); if (SVN_IS_VALID_REVNUM(replaces_rev)) { SVN_ERR(can_modify(root, fspath, replaces_rev, scratch_pool)); SVN_ERR(svn_fs_delete(root, fspath, scratch_pool)); } else { SVN_ERR(can_create(root, fspath, scratch_pool)); } SVN_ERR(svn_fs_make_file(root, fspath, scratch_pool)); SVN_ERR(set_text(root, fspath, checksum, contents, eb->cancel_func, eb->cancel_baton, scratch_pool)); SVN_ERR(add_new_props(root, fspath, props, scratch_pool)); return SVN_NO_ERROR; } /* This implements svn_editor_cb_add_symlink_t */ static svn_error_t * add_symlink_cb(void *baton, const char *relpath, const char *target, apr_hash_t *props, svn_revnum_t replaces_rev, apr_pool_t *scratch_pool) { struct edit_baton *eb = baton; const char *fspath = FSPATH(relpath, scratch_pool); svn_fs_root_t *root; SVN_ERR(get_root(&root, eb)); if (SVN_IS_VALID_REVNUM(replaces_rev)) { SVN_ERR(can_modify(root, fspath, replaces_rev, scratch_pool)); SVN_ERR(svn_fs_delete(root, fspath, scratch_pool)); } else { SVN_ERR(can_create(root, fspath, scratch_pool)); } /* ### we probably need to construct a file with specific contents ### (until the FS grows some symlink APIs) */ #if 0 SVN_ERR(svn_fs_make_file(root, fspath, scratch_pool)); SVN_ERR(svn_fs_apply_text(&fs_contents, root, fspath, NULL /* result_checksum */, scratch_pool)); /* ### SVN_ERR(svn_stream_printf(fs_contents, ..., scratch_pool)); */ apr_hash_set(props, SVN_PROP_SPECIAL, APR_HASH_KEY_STRING, SVN_PROP_SPECIAL_VALUE); SVN_ERR(add_new_props(root, fspath, props, scratch_pool)); #endif SVN__NOT_IMPLEMENTED(); } /* This implements svn_editor_cb_add_absent_t */ static svn_error_t * add_absent_cb(void *baton, const char *relpath, svn_node_kind_t kind, svn_revnum_t replaces_rev, apr_pool_t *scratch_pool) { /* This is a programming error. Code should not attempt to create these kinds of nodes within the FS. */ /* ### use a custom error code */ return svn_error_create( SVN_ERR_UNSUPPORTED_FEATURE, NULL, _("The filesystem does not support 'absent' nodes")); } /* This implements svn_editor_cb_alter_directory_t */ static svn_error_t * alter_directory_cb(void *baton, const char *relpath, svn_revnum_t revision, const apr_array_header_t *children, apr_hash_t *props, apr_pool_t *scratch_pool) { struct edit_baton *eb = baton; const char *fspath = FSPATH(relpath, scratch_pool); svn_fs_root_t *root; /* Note: we ignore CHILDREN. We have no "incomplete" state to worry about, so we don't need to be aware of what children will be created. */ SVN_ERR(get_root(&root, eb)); SVN_ERR(can_modify(root, fspath, revision, scratch_pool)); if (props) SVN_ERR(alter_props(root, fspath, props, scratch_pool)); return SVN_NO_ERROR; } /* This implements svn_editor_cb_alter_file_t */ static svn_error_t * alter_file_cb(void *baton, const char *relpath, svn_revnum_t revision, apr_hash_t *props, const svn_checksum_t *checksum, svn_stream_t *contents, apr_pool_t *scratch_pool) { struct edit_baton *eb = baton; const char *fspath = FSPATH(relpath, scratch_pool); svn_fs_root_t *root; SVN_ERR(get_root(&root, eb)); SVN_ERR(can_modify(root, fspath, revision, scratch_pool)); if (contents != NULL) { SVN_ERR_ASSERT(checksum != NULL); SVN_ERR(set_text(root, fspath, checksum, contents, eb->cancel_func, eb->cancel_baton, scratch_pool)); } if (props != NULL) { SVN_ERR(alter_props(root, fspath, props, scratch_pool)); } return SVN_NO_ERROR; } /* This implements svn_editor_cb_alter_symlink_t */ static svn_error_t * alter_symlink_cb(void *baton, const char *relpath, svn_revnum_t revision, apr_hash_t *props, const char *target, apr_pool_t *scratch_pool) { struct edit_baton *eb = baton; UNUSED(eb); SVN__NOT_IMPLEMENTED(); } /* This implements svn_editor_cb_delete_t */ static svn_error_t * delete_cb(void *baton, const char *relpath, svn_revnum_t revision, apr_pool_t *scratch_pool) { struct edit_baton *eb = baton; const char *fspath = FSPATH(relpath, scratch_pool); svn_fs_root_t *root; SVN_ERR(get_root(&root, eb)); SVN_ERR(can_modify(root, fspath, revision, scratch_pool)); SVN_ERR(svn_fs_delete(root, fspath, scratch_pool)); return SVN_NO_ERROR; } /* This implements svn_editor_cb_copy_t */ static svn_error_t * copy_cb(void *baton, const char *src_relpath, svn_revnum_t src_revision, const char *dst_relpath, svn_revnum_t replaces_rev, apr_pool_t *scratch_pool) { struct edit_baton *eb = baton; const char *src_fspath = FSPATH(src_relpath, scratch_pool); const char *dst_fspath = FSPATH(dst_relpath, scratch_pool); svn_fs_root_t *root; svn_fs_root_t *src_root; SVN_ERR(get_root(&root, eb)); /* Check if we can we replace the maybe-specified destination (revision). */ if (SVN_IS_VALID_REVNUM(replaces_rev)) { SVN_ERR(can_modify(root, dst_fspath, replaces_rev, scratch_pool)); SVN_ERR(svn_fs_delete(root, dst_fspath, scratch_pool)); } else { SVN_ERR(can_create(root, dst_fspath, scratch_pool)); } SVN_ERR(svn_fs_revision_root(&src_root, svn_fs_root_fs(root), src_revision, scratch_pool)); SVN_ERR(svn_fs_copy(src_root, src_fspath, root, dst_fspath, scratch_pool)); svn_fs_close_root(src_root); return SVN_NO_ERROR; } /* This implements svn_editor_cb_move_t */ static svn_error_t * move_cb(void *baton, const char *src_relpath, svn_revnum_t src_revision, const char *dst_relpath, svn_revnum_t replaces_rev, apr_pool_t *scratch_pool) { struct edit_baton *eb = baton; const char *src_fspath = FSPATH(src_relpath, scratch_pool); const char *dst_fspath = FSPATH(dst_relpath, scratch_pool); svn_fs_root_t *root; svn_fs_root_t *src_root; SVN_ERR(get_root(&root, eb)); /* Check if we delete the specified source (revision), and can we replace the maybe-specified destination (revision). */ SVN_ERR(can_modify(root, src_fspath, src_revision, scratch_pool)); if (SVN_IS_VALID_REVNUM(replaces_rev)) { SVN_ERR(can_modify(root, dst_fspath, replaces_rev, scratch_pool)); SVN_ERR(svn_fs_delete(root, dst_fspath, scratch_pool)); } else { SVN_ERR(can_create(root, dst_fspath, scratch_pool)); } /* ### would be nice to have svn_fs_move() */ /* Copy the src to the dst. */ SVN_ERR(svn_fs_revision_root(&src_root, svn_fs_root_fs(root), src_revision, scratch_pool)); SVN_ERR(svn_fs_copy(src_root, src_fspath, root, dst_fspath, scratch_pool)); svn_fs_close_root(src_root); /* Notice: we're deleting the src repos path from the dst root. */ SVN_ERR(svn_fs_delete(root, src_fspath, scratch_pool)); return SVN_NO_ERROR; } /* This implements svn_editor_cb_rotate_t */ static svn_error_t * rotate_cb(void *baton, const apr_array_header_t *relpaths, const apr_array_header_t *revisions, apr_pool_t *scratch_pool) { struct edit_baton *eb = baton; UNUSED(eb); SVN__NOT_IMPLEMENTED(); } /* This implements svn_editor_cb_complete_t */ static svn_error_t * complete_cb(void *baton, apr_pool_t *scratch_pool) { struct edit_baton *eb = baton; /* Watch out for a following call to svn_fs_editor_commit(). Note that we are likely here because svn_fs_editor_commit() was called, and it invoked svn_editor_complete(). */ eb->completed = TRUE; if (eb->root != NULL) { svn_fs_close_root(eb->root); eb->root = NULL; } return SVN_NO_ERROR; } /* This implements svn_editor_cb_abort_t */ static svn_error_t * abort_cb(void *baton, apr_pool_t *scratch_pool) { struct edit_baton *eb = baton; svn_error_t *err; /* Don't allow a following call to svn_fs_editor_commit(). */ eb->completed = TRUE; if (eb->root != NULL) { svn_fs_close_root(eb->root); eb->root = NULL; } /* ### should we examine the error and attempt svn_fs_purge_txn() ? */ err = svn_fs_abort_txn(eb->txn, scratch_pool); /* For safety, clear the now-useless txn. */ eb->txn = NULL; return svn_error_trace(err); } static svn_error_t * make_editor(svn_editor_t **editor, svn_fs_txn_t *txn, svn_cancel_func_t cancel_func, void *cancel_baton, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { static const svn_editor_cb_many_t editor_cbs = { add_directory_cb, add_file_cb, add_symlink_cb, add_absent_cb, alter_directory_cb, alter_file_cb, alter_symlink_cb, delete_cb, copy_cb, move_cb, rotate_cb, complete_cb, abort_cb }; struct edit_baton *eb = apr_pcalloc(result_pool, sizeof(*eb)); eb->txn = txn; eb->cancel_func = cancel_func; eb->cancel_baton = cancel_baton; eb->txn_pool = result_pool; SVN_ERR(svn_editor_create(editor, eb, cancel_func, cancel_baton, result_pool, scratch_pool)); SVN_ERR(svn_editor_setcb_many(*editor, &editor_cbs, scratch_pool)); return SVN_NO_ERROR; } svn_error_t * svn_fs__editor_create(svn_editor_t **editor, const char **txn_name, svn_fs_t *fs, apr_uint32_t flags, svn_cancel_func_t cancel_func, void *cancel_baton, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { svn_revnum_t revision; svn_fs_txn_t *txn; SVN_ERR(svn_fs_youngest_rev(&revision, fs, scratch_pool)); SVN_ERR(svn_fs_begin_txn2(&txn, fs, revision, flags, result_pool)); SVN_ERR(svn_fs_txn_name(txn_name, txn, result_pool)); return svn_error_trace(make_editor(editor, txn, cancel_func, cancel_baton, result_pool, scratch_pool)); } svn_error_t * svn_fs__editor_create_for(svn_editor_t **editor, svn_fs_t *fs, const char *txn_name, svn_cancel_func_t cancel_func, void *cancel_baton, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { svn_fs_txn_t *txn; SVN_ERR(svn_fs_open_txn(&txn, fs, txn_name, result_pool)); return svn_error_trace(make_editor(editor, txn, cancel_func, cancel_baton, result_pool, scratch_pool)); } svn_error_t * svn_fs__editor_commit(svn_revnum_t *revision, svn_error_t **post_commit_err, const char **conflict_path, svn_editor_t *editor, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { struct edit_baton *eb = svn_editor_get_baton(editor); const char *inner_conflict_path; svn_error_t *err = NULL; /* make sure people are using the correct sequencing. */ if (eb->completed) return svn_error_create(SVN_ERR_FS_INCORRECT_EDITOR_COMPLETION, NULL, NULL); *revision = SVN_INVALID_REVNUM; *post_commit_err = NULL; *conflict_path = NULL; /* Clean up internal resources (eg. eb->root). This also allows the editor infrastructure to know this editor is "complete". */ err = svn_editor_complete(editor); /* Note: docco for svn_fs_commit_txn() states that CONFLICT_PATH will be allocated in the txn's pool. But it lies. Regardless, we want it placed into RESULT_POOL. */ if (!err) err = svn_fs_commit_txn(&inner_conflict_path, revision, eb->txn, scratch_pool); if (SVN_IS_VALID_REVNUM(*revision)) { if (err) { /* Case 3. ERR is a post-commit (cleanup) error. */ /* Pass responsibility via POST_COMMIT_ERR. */ *post_commit_err = err; err = SVN_NO_ERROR; } /* else: Case 1. */ } else { SVN_ERR_ASSERT(err != NULL); if (err->apr_err == SVN_ERR_FS_CONFLICT) { /* Case 2. */ /* Copy this into the correct pool (see note above). */ *conflict_path = apr_pstrdup(result_pool, inner_conflict_path); /* Return sucess. The caller should inspect CONFLICT_PATH to determine this particular case. */ svn_error_clear(err); err = SVN_NO_ERROR; } /* else: Case 4. */ /* Abort the TXN. Nobody wants to use it. */ /* ### should we examine the error and attempt svn_fs_purge_txn() ? */ err = svn_error_compose_create( err, svn_fs_abort_txn(eb->txn, scratch_pool)); } /* For safety, clear the now-useless txn. */ eb->txn = NULL; return svn_error_trace(err); }