diff options
author | Peter Wemm <peter@FreeBSD.org> | 2013-06-18 02:07:41 +0000 |
---|---|---|
committer | Peter Wemm <peter@FreeBSD.org> | 2013-06-18 02:07:41 +0000 |
commit | 32547653cc5376642e1231fb644db99933ac8db4 (patch) | |
tree | 135691142dc0e75a5e5d97b5074d03436435b8e0 /subversion/svnrdump |
Notes
Diffstat (limited to 'subversion/svnrdump')
-rw-r--r-- | subversion/svnrdump/dump_editor.c | 1280 | ||||
-rw-r--r-- | subversion/svnrdump/load_editor.c | 1211 | ||||
-rw-r--r-- | subversion/svnrdump/svnrdump.1 | 47 | ||||
-rw-r--r-- | subversion/svnrdump/svnrdump.c | 1185 | ||||
-rw-r--r-- | subversion/svnrdump/svnrdump.h | 129 | ||||
-rw-r--r-- | subversion/svnrdump/util.c | 73 |
6 files changed, 3925 insertions, 0 deletions
diff --git a/subversion/svnrdump/dump_editor.c b/subversion/svnrdump/dump_editor.c new file mode 100644 index 0000000000000..faf029f47435b --- /dev/null +++ b/subversion/svnrdump/dump_editor.c @@ -0,0 +1,1280 @@ +/* + * dump_editor.c: The svn_delta_editor_t editor used by svnrdump to + * dump revisions. + * + * ==================================================================== + * 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 "svn_hash.h" +#include "svn_pools.h" +#include "svn_repos.h" +#include "svn_path.h" +#include "svn_props.h" +#include "svn_subst.h" +#include "svn_dirent_uri.h" + +#include "private/svn_subr_private.h" +#include "private/svn_dep_compat.h" +#include "private/svn_editor.h" + +#include "svnrdump.h" +#include <assert.h> + +#define ARE_VALID_COPY_ARGS(p,r) ((p) && SVN_IS_VALID_REVNUM(r)) + +#if 0 +#define LDR_DBG(x) SVN_DBG(x) +#else +#define LDR_DBG(x) while(0) +#endif + +/* A directory baton used by all directory-related callback functions + * in the dump editor. */ +struct dir_baton +{ + struct dump_edit_baton *eb; + struct dir_baton *parent_dir_baton; + + /* Pool for per-directory allocations */ + apr_pool_t *pool; + + /* is this directory a new addition to this revision? */ + svn_boolean_t added; + + /* has this directory been written to the output stream? */ + svn_boolean_t written_out; + + /* the path to this directory */ + const char *repos_relpath; /* a relpath */ + + /* Copyfrom info for the node, if any. */ + const char *copyfrom_path; /* a relpath */ + svn_revnum_t copyfrom_rev; + + /* Properties which were modified during change_dir_prop. */ + apr_hash_t *props; + + /* Properties which were deleted during change_dir_prop. */ + apr_hash_t *deleted_props; + + /* Hash of paths that need to be deleted, though some -might- be + replaced. Maps const char * paths to this dir_baton. Note that + they're full paths, because that's what the editor driver gives + us, although they're all really within this directory. */ + apr_hash_t *deleted_entries; + + /* Flags to trigger dumping props and record termination newlines. */ + svn_boolean_t dump_props; + svn_boolean_t dump_newlines; +}; + +/* A file baton used by all file-related callback functions in the dump + * editor */ +struct file_baton +{ + struct dump_edit_baton *eb; + struct dir_baton *parent_dir_baton; + + /* Pool for per-file allocations */ + apr_pool_t *pool; + + /* the path to this file */ + const char *repos_relpath; /* a relpath */ + + /* Properties which were modified during change_file_prop. */ + apr_hash_t *props; + + /* Properties which were deleted during change_file_prop. */ + apr_hash_t *deleted_props; + + /* The checksum of the file the delta is being applied to */ + const char *base_checksum; + + /* Copy state and source information (if any). */ + svn_boolean_t is_copy; + const char *copyfrom_path; + svn_revnum_t copyfrom_rev; + + /* The action associate with this node. */ + enum svn_node_action action; + + /* Flags to trigger dumping props and text. */ + svn_boolean_t dump_text; + svn_boolean_t dump_props; +}; + +/* A handler baton to be used in window_handler(). */ +struct handler_baton +{ + svn_txdelta_window_handler_t apply_handler; + void *apply_baton; +}; + +/* The baton used by the dump editor. */ +struct dump_edit_baton { + /* The output stream we write the dumpfile to */ + svn_stream_t *stream; + + /* A backdoor ra session to fetch additional information during the edit. */ + svn_ra_session_t *ra_session; + + /* The repository relpath of the anchor of the editor when driven + via the RA update mechanism; NULL otherwise. (When the editor is + driven via the RA "replay" mechanism instead, the editor is + always anchored at the repository, we don't need to prepend an + anchor path to the dumped node paths, and open_root() doesn't + need to manufacture directory additions.) */ + const char *update_anchor_relpath; + + /* Pool for per-revision allocations */ + apr_pool_t *pool; + + /* Temporary file used for textdelta application along with its + absolute path; these two variables should be allocated in the + per-edit-session pool */ + const char *delta_abspath; + apr_file_t *delta_file; + + /* The revision we're currently dumping. */ + svn_revnum_t current_revision; + + /* The kind (file or directory) and baton of the item whose block of + dump stream data has not been fully completed; NULL if there's no + such item. */ + svn_node_kind_t pending_kind; + void *pending_baton; +}; + +/* Make a directory baton to represent the directory at PATH (relative + * to the EDIT_BATON). + * + * COPYFROM_PATH/COPYFROM_REV are the path/revision against which this + * directory should be compared for changes. If the copyfrom + * information is valid, the directory will be compared against its + * copy source. + * + * PB is the directory baton of this directory's parent, or NULL if + * this is the top-level directory of the edit. ADDED indicates if + * this directory is newly added in this revision. Perform all + * allocations in POOL. */ +static struct dir_baton * +make_dir_baton(const char *path, + const char *copyfrom_path, + svn_revnum_t copyfrom_rev, + void *edit_baton, + struct dir_baton *pb, + svn_boolean_t added, + apr_pool_t *pool) +{ + struct dump_edit_baton *eb = edit_baton; + struct dir_baton *new_db = apr_pcalloc(pool, sizeof(*new_db)); + const char *repos_relpath; + + /* Construct the full path of this node. */ + if (pb) + repos_relpath = svn_relpath_canonicalize(path, pool); + else + repos_relpath = ""; + + /* Strip leading slash from copyfrom_path so that the path is + canonical and svn_relpath_join can be used */ + if (copyfrom_path) + copyfrom_path = svn_relpath_canonicalize(copyfrom_path, pool); + + new_db->eb = eb; + new_db->parent_dir_baton = pb; + new_db->pool = pool; + new_db->repos_relpath = repos_relpath; + new_db->copyfrom_path = copyfrom_path + ? svn_relpath_canonicalize(copyfrom_path, pool) + : NULL; + new_db->copyfrom_rev = copyfrom_rev; + new_db->added = added; + new_db->written_out = FALSE; + new_db->props = apr_hash_make(pool); + new_db->deleted_props = apr_hash_make(pool); + new_db->deleted_entries = apr_hash_make(pool); + + return new_db; +} + +/* Make a file baton to represent the directory at PATH (relative to + * PB->eb). PB is the directory baton of this directory's parent, or + * NULL if this is the top-level directory of the edit. Perform all + * allocations in POOL. */ +static struct file_baton * +make_file_baton(const char *path, + struct dir_baton *pb, + apr_pool_t *pool) +{ + struct file_baton *new_fb = apr_pcalloc(pool, sizeof(*new_fb)); + + new_fb->eb = pb->eb; + new_fb->parent_dir_baton = pb; + new_fb->pool = pool; + new_fb->repos_relpath = svn_relpath_canonicalize(path, pool); + new_fb->props = apr_hash_make(pool); + new_fb->deleted_props = apr_hash_make(pool); + new_fb->is_copy = FALSE; + new_fb->copyfrom_path = NULL; + new_fb->copyfrom_rev = SVN_INVALID_REVNUM; + new_fb->action = svn_node_action_change; + + return new_fb; +} + +/* Return in *HEADER and *CONTENT the headers and content for PROPS. */ +static svn_error_t * +get_props_content(svn_stringbuf_t **header, + svn_stringbuf_t **content, + apr_hash_t *props, + apr_hash_t *deleted_props, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_stream_t *content_stream; + apr_hash_t *normal_props; + const char *buf; + + *content = svn_stringbuf_create_empty(result_pool); + *header = svn_stringbuf_create_empty(result_pool); + + content_stream = svn_stream_from_stringbuf(*content, scratch_pool); + + SVN_ERR(svn_rdump__normalize_props(&normal_props, props, scratch_pool)); + SVN_ERR(svn_hash_write_incremental(normal_props, deleted_props, + content_stream, "PROPS-END", + scratch_pool)); + SVN_ERR(svn_stream_close(content_stream)); + + /* Prop-delta: true */ + *header = svn_stringbuf_createf(result_pool, SVN_REPOS_DUMPFILE_PROP_DELTA + ": true\n"); + + /* Prop-content-length: 193 */ + buf = apr_psprintf(scratch_pool, SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH + ": %" APR_SIZE_T_FMT "\n", (*content)->len); + svn_stringbuf_appendcstr(*header, buf); + + return SVN_NO_ERROR; +} + +/* Extract and dump properties stored in PROPS and property deletions + * stored in DELETED_PROPS. If TRIGGER_VAR is not NULL, it is set to + * FALSE. + * + * If PROPSTRING is non-NULL, set *PROPSTRING to a string containing + * the content block of the property changes; otherwise, dump that to + * the stream, too. + */ +static svn_error_t * +do_dump_props(svn_stringbuf_t **propstring, + svn_stream_t *stream, + apr_hash_t *props, + apr_hash_t *deleted_props, + svn_boolean_t *trigger_var, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_stringbuf_t *header; + svn_stringbuf_t *content; + apr_size_t len; + + if (trigger_var && !*trigger_var) + return SVN_NO_ERROR; + + SVN_ERR(get_props_content(&header, &content, props, deleted_props, + result_pool, scratch_pool)); + len = header->len; + SVN_ERR(svn_stream_write(stream, header->data, &len)); + + if (propstring) + { + *propstring = content; + } + else + { + /* Content-length: 14 */ + SVN_ERR(svn_stream_printf(stream, scratch_pool, + SVN_REPOS_DUMPFILE_CONTENT_LENGTH + ": %" APR_SIZE_T_FMT "\n\n", + content->len)); + + len = content->len; + SVN_ERR(svn_stream_write(stream, content->data, &len)); + + /* No text is going to be dumped. Write a couple of newlines and + wait for the next node/ revision. */ + SVN_ERR(svn_stream_puts(stream, "\n\n")); + + /* Cleanup so that data is never dumped twice. */ + apr_hash_clear(props); + apr_hash_clear(deleted_props); + if (trigger_var) + *trigger_var = FALSE; + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +do_dump_newlines(struct dump_edit_baton *eb, + svn_boolean_t *trigger_var, + apr_pool_t *pool) +{ + if (trigger_var && *trigger_var) + { + SVN_ERR(svn_stream_puts(eb->stream, "\n\n")); + *trigger_var = FALSE; + } + return SVN_NO_ERROR; +} + +/* + * Write out a node record for PATH of type KIND under EB->FS_ROOT. + * ACTION describes what is happening to the node (see enum + * svn_node_action). Write record to writable EB->STREAM, using + * EB->BUFFER to write in chunks. + * + * If the node was itself copied, IS_COPY is TRUE and the + * path/revision of the copy source are in COPYFROM_PATH/COPYFROM_REV. + * If IS_COPY is FALSE, yet COPYFROM_PATH/COPYFROM_REV are valid, this + * node is part of a copied subtree. + */ +static svn_error_t * +dump_node(struct dump_edit_baton *eb, + const char *repos_relpath, + struct dir_baton *db, + struct file_baton *fb, + enum svn_node_action action, + svn_boolean_t is_copy, + const char *copyfrom_path, + svn_revnum_t copyfrom_rev, + apr_pool_t *pool) +{ + const char *node_relpath = repos_relpath; + + assert(svn_relpath_is_canonical(repos_relpath)); + assert(!copyfrom_path || svn_relpath_is_canonical(copyfrom_path)); + assert(! (db && fb)); + + /* Add the edit root relpath prefix if necessary. */ + if (eb->update_anchor_relpath) + node_relpath = svn_relpath_join(eb->update_anchor_relpath, + node_relpath, pool); + + /* Node-path: ... */ + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_NODE_PATH ": %s\n", + node_relpath)); + + /* Node-kind: "file" | "dir" */ + if (fb) + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_NODE_KIND ": file\n")); + else if (db) + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_NODE_KIND ": dir\n")); + + + /* Write the appropriate Node-action header */ + switch (action) + { + case svn_node_action_change: + /* We are here after a change_file_prop or change_dir_prop. They + set up whatever dump_props they needed to- nothing to + do here but print node action information. + + Node-action: change. */ + SVN_ERR(svn_stream_puts(eb->stream, + SVN_REPOS_DUMPFILE_NODE_ACTION ": change\n")); + break; + + case svn_node_action_replace: + if (is_copy) + { + /* Delete the original, and then re-add the replacement as a + copy using recursive calls into this function. */ + SVN_ERR(dump_node(eb, repos_relpath, db, fb, svn_node_action_delete, + FALSE, NULL, SVN_INVALID_REVNUM, pool)); + SVN_ERR(dump_node(eb, repos_relpath, db, fb, svn_node_action_add, + is_copy, copyfrom_path, copyfrom_rev, pool)); + } + else + { + /* Node-action: replace */ + SVN_ERR(svn_stream_puts(eb->stream, + SVN_REPOS_DUMPFILE_NODE_ACTION + ": replace\n")); + + /* Wait for a change_*_prop to be called before dumping + anything */ + if (fb) + fb->dump_props = TRUE; + else if (db) + db->dump_props = TRUE; + } + break; + + case svn_node_action_delete: + /* Node-action: delete */ + SVN_ERR(svn_stream_puts(eb->stream, + SVN_REPOS_DUMPFILE_NODE_ACTION ": delete\n")); + + /* We can leave this routine quietly now. Nothing more to do- + print a couple of newlines because we're not dumping props or + text. */ + SVN_ERR(svn_stream_puts(eb->stream, "\n\n")); + + break; + + case svn_node_action_add: + /* Node-action: add */ + SVN_ERR(svn_stream_puts(eb->stream, + SVN_REPOS_DUMPFILE_NODE_ACTION ": add\n")); + + if (is_copy) + { + /* Node-copyfrom-rev / Node-copyfrom-path */ + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_NODE_COPYFROM_REV + ": %ld\n" + SVN_REPOS_DUMPFILE_NODE_COPYFROM_PATH + ": %s\n", + copyfrom_rev, copyfrom_path)); + + /* Ugly hack: If a directory was copied from a previous + revision, nothing like close_file() will be called to write two + blank lines. If change_dir_prop() is called, props are dumped + (along with the necessary PROPS-END\n\n and we're good. So + set DUMP_NEWLINES here to print the newlines unless + change_dir_prop() is called next otherwise the `svnadmin load` + parser will fail. */ + if (db) + db->dump_newlines = TRUE; + } + else + { + /* fb->dump_props (for files) is handled in close_file() + which is called immediately. + + However, directories are not closed until all the work + inside them has been done; db->dump_props (for directories) + is handled (via dump_pending()) in all the functions that + can possibly be called after add_directory(): + + - add_directory() + - open_directory() + - delete_entry() + - close_directory() + - add_file() + - open_file() + + change_dir_prop() is a special case. */ + if (fb) + fb->dump_props = TRUE; + else if (db) + db->dump_props = TRUE; + } + + break; + } + return SVN_NO_ERROR; +} + +static svn_error_t * +dump_mkdir(struct dump_edit_baton *eb, + const char *repos_relpath, + apr_pool_t *pool) +{ + svn_stringbuf_t *prop_header, *prop_content; + apr_size_t len; + const char *buf; + + /* Node-path: ... */ + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_NODE_PATH ": %s\n", + repos_relpath)); + + /* Node-kind: dir */ + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_NODE_KIND ": dir\n")); + + /* Node-action: add */ + SVN_ERR(svn_stream_puts(eb->stream, + SVN_REPOS_DUMPFILE_NODE_ACTION ": add\n")); + + /* Dump the (empty) property block. */ + SVN_ERR(get_props_content(&prop_header, &prop_content, + apr_hash_make(pool), apr_hash_make(pool), + pool, pool)); + len = prop_header->len; + SVN_ERR(svn_stream_write(eb->stream, prop_header->data, &len)); + len = prop_content->len; + buf = apr_psprintf(pool, SVN_REPOS_DUMPFILE_CONTENT_LENGTH + ": %" APR_SIZE_T_FMT "\n", len); + SVN_ERR(svn_stream_puts(eb->stream, buf)); + SVN_ERR(svn_stream_puts(eb->stream, "\n")); + SVN_ERR(svn_stream_write(eb->stream, prop_content->data, &len)); + + /* Newlines to tie it all off. */ + SVN_ERR(svn_stream_puts(eb->stream, "\n\n")); + + return SVN_NO_ERROR; +} + +/* Dump pending items from the specified node, to allow starting the dump + of a child node */ +static svn_error_t * +dump_pending(struct dump_edit_baton *eb, + apr_pool_t *scratch_pool) +{ + if (! eb->pending_baton) + return SVN_NO_ERROR; + + if (eb->pending_kind == svn_node_dir) + { + struct dir_baton *db = eb->pending_baton; + + /* Some pending properties to dump? */ + SVN_ERR(do_dump_props(NULL, eb->stream, db->props, db->deleted_props, + &(db->dump_props), db->pool, scratch_pool)); + + /* Some pending newlines to dump? */ + SVN_ERR(do_dump_newlines(eb, &(db->dump_newlines), scratch_pool)); + } + else if (eb->pending_kind == svn_node_file) + { + struct file_baton *fb = eb->pending_baton; + + /* Some pending properties to dump? */ + SVN_ERR(do_dump_props(NULL, eb->stream, fb->props, fb->deleted_props, + &(fb->dump_props), fb->pool, scratch_pool)); + } + else + abort(); + + /* Anything that was pending is pending no longer. */ + eb->pending_baton = NULL; + eb->pending_kind = svn_node_none; + + return SVN_NO_ERROR; +} + + + +/*** Editor Function Implementations ***/ + +static svn_error_t * +open_root(void *edit_baton, + svn_revnum_t base_revision, + apr_pool_t *pool, + void **root_baton) +{ + struct dump_edit_baton *eb = edit_baton; + struct dir_baton *new_db = NULL; + + /* Clear the per-revision pool after each revision */ + svn_pool_clear(eb->pool); + + LDR_DBG(("open_root %p\n", *root_baton)); + + if (eb->update_anchor_relpath) + { + int i; + const char *parent_path = eb->update_anchor_relpath; + apr_array_header_t *dirs_to_add = + apr_array_make(pool, 4, sizeof(const char *)); + apr_pool_t *iterpool = svn_pool_create(pool); + + while (! svn_path_is_empty(parent_path)) + { + APR_ARRAY_PUSH(dirs_to_add, const char *) = parent_path; + parent_path = svn_relpath_dirname(parent_path, pool); + } + + for (i = dirs_to_add->nelts; i; --i) + { + const char *dir_to_add = + APR_ARRAY_IDX(dirs_to_add, i - 1, const char *); + + svn_pool_clear(iterpool); + + /* For parents of the source directory, we just manufacture + the adds ourselves. */ + if (i > 1) + { + SVN_ERR(dump_mkdir(eb, dir_to_add, iterpool)); + } + else + { + /* ... but for the source directory itself, we'll defer + to letting the typical plumbing handle this task. */ + new_db = make_dir_baton(NULL, NULL, SVN_INVALID_REVNUM, + edit_baton, NULL, TRUE, pool); + SVN_ERR(dump_node(eb, new_db->repos_relpath, new_db, + NULL, svn_node_action_add, FALSE, + NULL, SVN_INVALID_REVNUM, pool)); + + /* Remember that we've started but not yet finished + handling this directory. */ + new_db->written_out = TRUE; + eb->pending_baton = new_db; + eb->pending_kind = svn_node_dir; + } + } + svn_pool_destroy(iterpool); + } + + if (! new_db) + { + new_db = make_dir_baton(NULL, NULL, SVN_INVALID_REVNUM, + edit_baton, NULL, FALSE, pool); + } + + *root_baton = new_db; + return SVN_NO_ERROR; +} + +static svn_error_t * +delete_entry(const char *path, + svn_revnum_t revision, + void *parent_baton, + apr_pool_t *pool) +{ + struct dir_baton *pb = parent_baton; + + LDR_DBG(("delete_entry %s\n", path)); + + SVN_ERR(dump_pending(pb->eb, pool)); + + /* We don't dump this deletion immediate. Rather, we add this path + to the deleted_entries of the parent directory baton. That way, + we can tell (later) an addition from a replacement. All the real + deletions get handled in close_directory(). */ + svn_hash_sets(pb->deleted_entries, apr_pstrdup(pb->eb->pool, path), pb); + + return SVN_NO_ERROR; +} + +static svn_error_t * +add_directory(const char *path, + void *parent_baton, + const char *copyfrom_path, + svn_revnum_t copyfrom_rev, + apr_pool_t *pool, + void **child_baton) +{ + struct dir_baton *pb = parent_baton; + void *val; + struct dir_baton *new_db; + svn_boolean_t is_copy; + + LDR_DBG(("add_directory %s\n", path)); + + SVN_ERR(dump_pending(pb->eb, pool)); + + new_db = make_dir_baton(path, copyfrom_path, copyfrom_rev, pb->eb, + pb, TRUE, pb->eb->pool); + + /* This might be a replacement -- is the path already deleted? */ + val = svn_hash_gets(pb->deleted_entries, path); + + /* Detect an add-with-history */ + is_copy = ARE_VALID_COPY_ARGS(copyfrom_path, copyfrom_rev); + + /* Dump the node */ + SVN_ERR(dump_node(pb->eb, new_db->repos_relpath, new_db, NULL, + val ? svn_node_action_replace : svn_node_action_add, + is_copy, + is_copy ? new_db->copyfrom_path : NULL, + is_copy ? copyfrom_rev : SVN_INVALID_REVNUM, + pool)); + + if (val) + /* Delete the path, it's now been dumped */ + svn_hash_sets(pb->deleted_entries, path, NULL); + + /* Remember that we've started, but not yet finished handling this + directory. */ + new_db->written_out = TRUE; + pb->eb->pending_baton = new_db; + pb->eb->pending_kind = svn_node_dir; + + *child_baton = new_db; + return SVN_NO_ERROR; +} + +static svn_error_t * +open_directory(const char *path, + void *parent_baton, + svn_revnum_t base_revision, + apr_pool_t *pool, + void **child_baton) +{ + struct dir_baton *pb = parent_baton; + struct dir_baton *new_db; + const char *copyfrom_path = NULL; + svn_revnum_t copyfrom_rev = SVN_INVALID_REVNUM; + + LDR_DBG(("open_directory %s\n", path)); + + SVN_ERR(dump_pending(pb->eb, pool)); + + /* If the parent directory has explicit comparison path and rev, + record the same for this one. */ + if (ARE_VALID_COPY_ARGS(pb->copyfrom_path, pb->copyfrom_rev)) + { + copyfrom_path = svn_relpath_join(pb->copyfrom_path, + svn_relpath_basename(path, NULL), + pb->eb->pool); + copyfrom_rev = pb->copyfrom_rev; + } + + new_db = make_dir_baton(path, copyfrom_path, copyfrom_rev, pb->eb, pb, + FALSE, pb->eb->pool); + + *child_baton = new_db; + return SVN_NO_ERROR; +} + +static svn_error_t * +close_directory(void *dir_baton, + apr_pool_t *pool) +{ + struct dir_baton *db = dir_baton; + apr_hash_index_t *hi; + svn_boolean_t this_pending; + + LDR_DBG(("close_directory %p\n", dir_baton)); + + /* Remember if this directory is the one currently pending. */ + this_pending = (db->eb->pending_baton == db); + + SVN_ERR(dump_pending(db->eb, pool)); + + /* If this directory was pending, then dump_pending() should have + taken care of all the props and such. Of course, the only way + that would be the case is if this directory was added/replaced. + + Otherwise, if stuff for this directory has already been written + out (at some point in the past, prior to our handling other + nodes), we might need to generate a second "change" record just + to carry the information we've since learned about the + directory. */ + if ((! this_pending) && (db->dump_props)) + { + SVN_ERR(dump_node(db->eb, db->repos_relpath, db, NULL, + svn_node_action_change, FALSE, + NULL, SVN_INVALID_REVNUM, pool)); + db->eb->pending_baton = db; + db->eb->pending_kind = svn_node_dir; + SVN_ERR(dump_pending(db->eb, pool)); + } + + /* Dump the deleted directory entries */ + for (hi = apr_hash_first(pool, db->deleted_entries); hi; + hi = apr_hash_next(hi)) + { + const char *path = svn__apr_hash_index_key(hi); + + SVN_ERR(dump_node(db->eb, path, NULL, NULL, svn_node_action_delete, + FALSE, NULL, SVN_INVALID_REVNUM, pool)); + } + + /* ### should be unnecessary */ + apr_hash_clear(db->deleted_entries); + + return SVN_NO_ERROR; +} + +static svn_error_t * +add_file(const char *path, + void *parent_baton, + const char *copyfrom_path, + svn_revnum_t copyfrom_rev, + apr_pool_t *pool, + void **file_baton) +{ + struct dir_baton *pb = parent_baton; + struct file_baton *fb; + void *val; + + LDR_DBG(("add_file %s\n", path)); + + SVN_ERR(dump_pending(pb->eb, pool)); + + /* Make the file baton. */ + fb = make_file_baton(path, pb, pool); + + /* This might be a replacement -- is the path already deleted? */ + val = svn_hash_gets(pb->deleted_entries, path); + + /* Detect add-with-history. */ + if (ARE_VALID_COPY_ARGS(copyfrom_path, copyfrom_rev)) + { + fb->copyfrom_path = svn_relpath_canonicalize(copyfrom_path, fb->pool); + fb->copyfrom_rev = copyfrom_rev; + fb->is_copy = TRUE; + } + fb->action = val ? svn_node_action_replace : svn_node_action_add; + + /* Delete the path, it's now been dumped. */ + if (val) + svn_hash_sets(pb->deleted_entries, path, NULL); + + *file_baton = fb; + return SVN_NO_ERROR; +} + +static svn_error_t * +open_file(const char *path, + void *parent_baton, + svn_revnum_t ancestor_revision, + apr_pool_t *pool, + void **file_baton) +{ + struct dir_baton *pb = parent_baton; + struct file_baton *fb; + + LDR_DBG(("open_file %s\n", path)); + + SVN_ERR(dump_pending(pb->eb, pool)); + + /* Make the file baton. */ + fb = make_file_baton(path, pb, pool); + + /* If the parent directory has explicit copyfrom path and rev, + record the same for this one. */ + if (ARE_VALID_COPY_ARGS(pb->copyfrom_path, pb->copyfrom_rev)) + { + fb->copyfrom_path = svn_relpath_join(pb->copyfrom_path, + svn_relpath_basename(path, NULL), + pb->eb->pool); + fb->copyfrom_rev = pb->copyfrom_rev; + } + + *file_baton = fb; + return SVN_NO_ERROR; +} + +static svn_error_t * +change_dir_prop(void *parent_baton, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + struct dir_baton *db = parent_baton; + svn_boolean_t this_pending; + + LDR_DBG(("change_dir_prop %p\n", parent_baton)); + + /* This directory is not pending, but something else is, so handle + the "something else". */ + this_pending = (db->eb->pending_baton == db); + if (! this_pending) + SVN_ERR(dump_pending(db->eb, pool)); + + if (svn_property_kind2(name) != svn_prop_regular_kind) + return SVN_NO_ERROR; + + if (value) + svn_hash_sets(db->props, + apr_pstrdup(db->pool, name), + svn_string_dup(value, db->pool)); + else + svn_hash_sets(db->deleted_props, apr_pstrdup(db->pool, name), ""); + + /* Make sure we eventually output the props, and disable printing + a couple of extra newlines */ + db->dump_newlines = FALSE; + db->dump_props = TRUE; + + return SVN_NO_ERROR; +} + +static svn_error_t * +change_file_prop(void *file_baton, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + struct file_baton *fb = file_baton; + + LDR_DBG(("change_file_prop %p\n", file_baton)); + + if (svn_property_kind2(name) != svn_prop_regular_kind) + return SVN_NO_ERROR; + + if (value) + svn_hash_sets(fb->props, + apr_pstrdup(fb->pool, name), + svn_string_dup(value, fb->pool)); + else + svn_hash_sets(fb->deleted_props, apr_pstrdup(fb->pool, name), ""); + + /* Dump the property headers and wait; close_file might need + to write text headers too depending on whether + apply_textdelta is called */ + fb->dump_props = TRUE; + + return SVN_NO_ERROR; +} + +static svn_error_t * +window_handler(svn_txdelta_window_t *window, void *baton) +{ + struct handler_baton *hb = baton; + static svn_error_t *err; + + err = hb->apply_handler(window, hb->apply_baton); + if (window != NULL && !err) + return SVN_NO_ERROR; + + if (err) + SVN_ERR(err); + + return SVN_NO_ERROR; +} + +static svn_error_t * +apply_textdelta(void *file_baton, const char *base_checksum, + apr_pool_t *pool, + svn_txdelta_window_handler_t *handler, + void **handler_baton) +{ + struct file_baton *fb = file_baton; + struct dump_edit_baton *eb = fb->eb; + struct handler_baton *hb; + svn_stream_t *delta_filestream; + + LDR_DBG(("apply_textdelta %p\n", file_baton)); + + /* This is custom handler_baton, allocated from a separate pool. */ + hb = apr_pcalloc(eb->pool, sizeof(*hb)); + + /* Use a temporary file to measure the Text-content-length */ + delta_filestream = svn_stream_from_aprfile2(eb->delta_file, TRUE, pool); + + /* Prepare to write the delta to the delta_filestream */ + svn_txdelta_to_svndiff3(&(hb->apply_handler), &(hb->apply_baton), + delta_filestream, 0, + SVN_DELTA_COMPRESSION_LEVEL_DEFAULT, pool); + + /* Record that there's text to be dumped, and its base checksum. */ + fb->dump_text = TRUE; + fb->base_checksum = apr_pstrdup(eb->pool, base_checksum); + + /* The actual writing takes place when this function has + finished. Set handler and handler_baton now so for + window_handler() */ + *handler = window_handler; + *handler_baton = hb; + + return SVN_NO_ERROR; +} + +static svn_error_t * +close_file(void *file_baton, + const char *text_checksum, + apr_pool_t *pool) +{ + struct file_baton *fb = file_baton; + struct dump_edit_baton *eb = fb->eb; + apr_finfo_t *info = apr_pcalloc(pool, sizeof(apr_finfo_t)); + svn_stringbuf_t *propstring; + + LDR_DBG(("close_file %p\n", file_baton)); + + SVN_ERR(dump_pending(eb, pool)); + + /* Dump the node. */ + SVN_ERR(dump_node(eb, fb->repos_relpath, NULL, fb, + fb->action, fb->is_copy, fb->copyfrom_path, + fb->copyfrom_rev, pool)); + + /* Some pending properties to dump? We'll dump just the headers for + now, then dump the actual propchange content only after dumping + the text headers too (if present). */ + SVN_ERR(do_dump_props(&propstring, eb->stream, fb->props, fb->deleted_props, + &(fb->dump_props), pool, pool)); + + /* Dump the text headers */ + if (fb->dump_text) + { + apr_status_t err; + + /* Text-delta: true */ + SVN_ERR(svn_stream_puts(eb->stream, + SVN_REPOS_DUMPFILE_TEXT_DELTA + ": true\n")); + + err = apr_file_info_get(info, APR_FINFO_SIZE, eb->delta_file); + if (err) + SVN_ERR(svn_error_wrap_apr(err, NULL)); + + if (fb->base_checksum) + /* Text-delta-base-md5: */ + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_TEXT_DELTA_BASE_MD5 + ": %s\n", + fb->base_checksum)); + + /* Text-content-length: 39 */ + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_TEXT_CONTENT_LENGTH + ": %lu\n", + (unsigned long)info->size)); + + /* Text-content-md5: 82705804337e04dcd0e586bfa2389a7f */ + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_TEXT_CONTENT_MD5 + ": %s\n", + text_checksum)); + } + + /* Content-length: 1549 */ + /* If both text and props are absent, skip this header */ + if (fb->dump_props) + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_CONTENT_LENGTH + ": %ld\n\n", + (unsigned long)info->size + propstring->len)); + else if (fb->dump_text) + SVN_ERR(svn_stream_printf(eb->stream, pool, + SVN_REPOS_DUMPFILE_CONTENT_LENGTH + ": %ld\n\n", + (unsigned long)info->size)); + + /* Dump the props now */ + if (fb->dump_props) + { + SVN_ERR(svn_stream_write(eb->stream, propstring->data, + &(propstring->len))); + + /* Cleanup */ + fb->dump_props = FALSE; + apr_hash_clear(fb->props); + apr_hash_clear(fb->deleted_props); + } + + /* Dump the text */ + if (fb->dump_text) + { + /* Seek to the beginning of the delta file, map it to a stream, + and copy the stream to eb->stream. Then close the stream and + truncate the file so we can reuse it for the next textdelta + application. Note that the file isn't created, opened or + closed here */ + svn_stream_t *delta_filestream; + apr_off_t offset = 0; + + SVN_ERR(svn_io_file_seek(eb->delta_file, APR_SET, &offset, pool)); + delta_filestream = svn_stream_from_aprfile2(eb->delta_file, TRUE, pool); + SVN_ERR(svn_stream_copy3(delta_filestream, eb->stream, NULL, NULL, pool)); + + /* Cleanup */ + SVN_ERR(svn_stream_close(delta_filestream)); + SVN_ERR(svn_io_file_trunc(eb->delta_file, 0, pool)); + } + + /* Write a couple of blank lines for matching output with `svnadmin + dump` */ + SVN_ERR(svn_stream_puts(eb->stream, "\n\n")); + + return SVN_NO_ERROR; +} + +static svn_error_t * +close_edit(void *edit_baton, apr_pool_t *pool) +{ + return SVN_NO_ERROR; +} + +static svn_error_t * +fetch_base_func(const char **filename, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + struct dump_edit_baton *eb = baton; + svn_stream_t *fstream; + svn_error_t *err; + + if (path[0] == '/') + path += 1; + + if (! SVN_IS_VALID_REVNUM(base_revision)) + base_revision = eb->current_revision - 1; + + SVN_ERR(svn_stream_open_unique(&fstream, filename, NULL, + svn_io_file_del_on_pool_cleanup, + result_pool, scratch_pool)); + + err = svn_ra_get_file(eb->ra_session, path, base_revision, + fstream, NULL, NULL, scratch_pool); + if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + SVN_ERR(svn_stream_close(fstream)); + + *filename = NULL; + return SVN_NO_ERROR; + } + else if (err) + return svn_error_trace(err); + + SVN_ERR(svn_stream_close(fstream)); + + return SVN_NO_ERROR; +} + +static svn_error_t * +fetch_props_func(apr_hash_t **props, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + struct dump_edit_baton *eb = baton; + svn_node_kind_t node_kind; + + if (path[0] == '/') + path += 1; + + if (! SVN_IS_VALID_REVNUM(base_revision)) + base_revision = eb->current_revision - 1; + + SVN_ERR(svn_ra_check_path(eb->ra_session, path, base_revision, &node_kind, + scratch_pool)); + + if (node_kind == svn_node_file) + { + SVN_ERR(svn_ra_get_file(eb->ra_session, path, base_revision, + NULL, NULL, props, result_pool)); + } + else if (node_kind == svn_node_dir) + { + apr_array_header_t *tmp_props; + + SVN_ERR(svn_ra_get_dir2(eb->ra_session, NULL, NULL, props, path, + base_revision, 0 /* Dirent fields */, + result_pool)); + tmp_props = svn_prop_hash_to_array(*props, result_pool); + SVN_ERR(svn_categorize_props(tmp_props, NULL, NULL, &tmp_props, + result_pool)); + *props = svn_prop_array_to_hash(tmp_props, result_pool); + } + else + { + *props = apr_hash_make(result_pool); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +fetch_kind_func(svn_node_kind_t *kind, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *scratch_pool) +{ + struct dump_edit_baton *eb = baton; + + if (path[0] == '/') + path += 1; + + if (! SVN_IS_VALID_REVNUM(base_revision)) + base_revision = eb->current_revision - 1; + + SVN_ERR(svn_ra_check_path(eb->ra_session, path, base_revision, kind, + scratch_pool)); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_rdump__get_dump_editor(const svn_delta_editor_t **editor, + void **edit_baton, + svn_revnum_t revision, + svn_stream_t *stream, + svn_ra_session_t *ra_session, + const char *update_anchor_relpath, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + struct dump_edit_baton *eb; + svn_delta_editor_t *de; + svn_delta_shim_callbacks_t *shim_callbacks = + svn_delta_shim_callbacks_default(pool); + + eb = apr_pcalloc(pool, sizeof(struct dump_edit_baton)); + eb->stream = stream; + eb->ra_session = ra_session; + eb->update_anchor_relpath = update_anchor_relpath; + eb->current_revision = revision; + eb->pending_kind = svn_node_none; + + /* Create a special per-revision pool */ + eb->pool = svn_pool_create(pool); + + /* Open a unique temporary file for all textdelta applications in + this edit session. The file is automatically closed and cleaned + up when the edit session is done. */ + SVN_ERR(svn_io_open_unique_file3(&(eb->delta_file), &(eb->delta_abspath), + NULL, svn_io_file_del_on_close, pool, pool)); + + de = svn_delta_default_editor(pool); + de->open_root = open_root; + de->delete_entry = delete_entry; + de->add_directory = add_directory; + de->open_directory = open_directory; + de->close_directory = close_directory; + de->change_dir_prop = change_dir_prop; + de->change_file_prop = change_file_prop; + de->apply_textdelta = apply_textdelta; + de->add_file = add_file; + de->open_file = open_file; + de->close_file = close_file; + de->close_edit = close_edit; + + /* Set the edit_baton and editor. */ + *edit_baton = eb; + *editor = de; + + /* Wrap this editor in a cancellation editor. */ + SVN_ERR(svn_delta_get_cancellation_editor(cancel_func, cancel_baton, + de, eb, editor, edit_baton, pool)); + + shim_callbacks->fetch_base_func = fetch_base_func; + shim_callbacks->fetch_props_func = fetch_props_func; + shim_callbacks->fetch_kind_func = fetch_kind_func; + shim_callbacks->fetch_baton = eb; + + SVN_ERR(svn_editor__insert_shims(editor, edit_baton, *editor, *edit_baton, + NULL, NULL, shim_callbacks, pool, pool)); + + return SVN_NO_ERROR; +} diff --git a/subversion/svnrdump/load_editor.c b/subversion/svnrdump/load_editor.c new file mode 100644 index 0000000000000..1b053f2f0df19 --- /dev/null +++ b/subversion/svnrdump/load_editor.c @@ -0,0 +1,1211 @@ +/* + * load_editor.c: The svn_delta_editor_t editor used by svnrdump to + * load revisions. + * + * ==================================================================== + * 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 "svn_cmdline.h" +#include "svn_pools.h" +#include "svn_delta.h" +#include "svn_repos.h" +#include "svn_props.h" +#include "svn_path.h" +#include "svn_ra.h" +#include "svn_subst.h" +#include "svn_io.h" +#include "svn_private_config.h" +#include "private/svn_repos_private.h" +#include "private/svn_ra_private.h" +#include "private/svn_mergeinfo_private.h" +#include "private/svn_fspath.h" + +#include "svnrdump.h" + +#define SVNRDUMP_PROP_LOCK SVN_PROP_PREFIX "rdump-lock" + +#if 0 +#define LDR_DBG(x) SVN_DBG(x) +#else +#define LDR_DBG(x) while(0) +#endif + + + +/** + * General baton used by the parser functions. + */ +struct parse_baton +{ + /* Commit editor and baton used to transfer loaded revisions to + the target repository. */ + const svn_delta_editor_t *commit_editor; + void *commit_edit_baton; + + /* RA session(s) for committing to the target repository. */ + svn_ra_session_t *session; + svn_ra_session_t *aux_session; + + /* To bleep, or not to bleep? (What kind of question is that?) */ + svn_boolean_t quiet; + + /* UUID found in the dumpstream, if any; NULL otherwise. */ + const char *uuid; + + /* Root URL of the target repository. */ + const char *root_url; + + /* The "parent directory" of the target repository in which to load. + (This is essentially the difference between ROOT_URL and + SESSION's url, and roughly equivalent to the 'svnadmin load + --parent-dir' option.) */ + const char *parent_dir; + + /* A mapping of svn_revnum_t * dump stream revisions to their + corresponding svn_revnum_t * target repository revisions. */ + /* ### See http://subversion.tigris.org/issues/show_bug.cgi?id=3903 + ### for discussion about improving the memory costs of this mapping. */ + apr_hash_t *rev_map; + + /* The most recent (youngest) revision from the dump stream mapped in + REV_MAP, or SVN_INVALID_REVNUM if no revisions have been mapped. */ + svn_revnum_t last_rev_mapped; + + /* The oldest revision loaded from the dump stream, or + SVN_INVALID_REVNUM if none have been loaded. */ + svn_revnum_t oldest_dumpstream_rev; +}; + +/** + * Use to wrap the dir_context_t in commit.c so we can keep track of + * depth, relpath and parent for open_directory and close_directory. + */ +struct directory_baton +{ + void *baton; + const char *relpath; + int depth; + struct directory_baton *parent; +}; + +/** + * Baton used to represent a node; to be used by the parser + * functions. Contains a link to the revision baton. + */ +struct node_baton +{ + const char *path; + svn_node_kind_t kind; + enum svn_node_action action; + + svn_revnum_t copyfrom_rev; + const char *copyfrom_path; + + void *file_baton; + const char *base_checksum; + + struct revision_baton *rb; +}; + +/** + * Baton used to represet a revision; used by the parser + * functions. Contains a link to the parser baton. + */ +struct revision_baton +{ + svn_revnum_t rev; + apr_hash_t *revprop_table; + apr_int32_t rev_offset; + + const svn_string_t *datestamp; + const svn_string_t *author; + + struct parse_baton *pb; + struct directory_baton *db; + apr_pool_t *pool; +}; + + + +/* Record the mapping of FROM_REV to TO_REV in REV_MAP, ensuring that + anything added to the hash is allocated in the hash's pool. */ +static void +set_revision_mapping(apr_hash_t *rev_map, + svn_revnum_t from_rev, + svn_revnum_t to_rev) +{ + svn_revnum_t *mapped_revs = apr_palloc(apr_hash_pool_get(rev_map), + sizeof(svn_revnum_t) * 2); + mapped_revs[0] = from_rev; + mapped_revs[1] = to_rev; + apr_hash_set(rev_map, mapped_revs, + sizeof(svn_revnum_t), mapped_revs + 1); +} + +/* Return the revision to which FROM_REV maps in REV_MAP, or + SVN_INVALID_REVNUM if no such mapping exists. */ +static svn_revnum_t +get_revision_mapping(apr_hash_t *rev_map, + svn_revnum_t from_rev) +{ + svn_revnum_t *to_rev = apr_hash_get(rev_map, &from_rev, + sizeof(from_rev)); + return to_rev ? *to_rev : SVN_INVALID_REVNUM; +} + + +/* Prepend the mergeinfo source paths in MERGEINFO_ORIG with + PARENT_DIR, and return it in *MERGEINFO_VAL. */ +/* ### FIXME: Consider somehow sharing code with + ### libsvn_repos/load-fs-vtable.c:prefix_mergeinfo_paths() */ +static svn_error_t * +prefix_mergeinfo_paths(svn_string_t **mergeinfo_val, + const svn_string_t *mergeinfo_orig, + const char *parent_dir, + apr_pool_t *pool) +{ + apr_hash_t *prefixed_mergeinfo, *mergeinfo; + apr_hash_index_t *hi; + void *rangelist; + + SVN_ERR(svn_mergeinfo_parse(&mergeinfo, mergeinfo_orig->data, pool)); + prefixed_mergeinfo = apr_hash_make(pool); + for (hi = apr_hash_first(pool, mergeinfo); hi; hi = apr_hash_next(hi)) + { + const void *key; + const char *path, *merge_source; + + apr_hash_this(hi, &key, NULL, &rangelist); + merge_source = svn_relpath_canonicalize(key, pool); + + /* The svn:mergeinfo property syntax demands a repos abspath */ + path = svn_fspath__canonicalize(svn_relpath_join(parent_dir, + merge_source, pool), + pool); + svn_hash_sets(prefixed_mergeinfo, path, rangelist); + } + return svn_mergeinfo_to_string(mergeinfo_val, prefixed_mergeinfo, pool); +} + + +/* Examine the mergeinfo in INITIAL_VAL, renumber revisions in rangelists + as appropriate, and return the (possibly new) mergeinfo in *FINAL_VAL + (allocated from POOL). */ +/* ### FIXME: Consider somehow sharing code with + ### libsvn_repos/load-fs-vtable.c:renumber_mergeinfo_revs() */ +static svn_error_t * +renumber_mergeinfo_revs(svn_string_t **final_val, + const svn_string_t *initial_val, + struct revision_baton *rb, + apr_pool_t *pool) +{ + apr_pool_t *subpool = svn_pool_create(pool); + svn_mergeinfo_t mergeinfo, predates_stream_mergeinfo; + svn_mergeinfo_t final_mergeinfo = apr_hash_make(subpool); + apr_hash_index_t *hi; + + SVN_ERR(svn_mergeinfo_parse(&mergeinfo, initial_val->data, subpool)); + + /* Issue #3020 + http://subversion.tigris.org/issues/show_bug.cgi?id=3020#desc16 + Remove mergeinfo older than the oldest revision in the dump stream + and adjust its revisions by the difference between the head rev of + the target repository and the current dump stream rev. */ + if (rb->pb->oldest_dumpstream_rev > 1) + { + SVN_ERR(svn_mergeinfo__filter_mergeinfo_by_ranges( + &predates_stream_mergeinfo, mergeinfo, + rb->pb->oldest_dumpstream_rev - 1, 0, + TRUE, subpool, subpool)); + SVN_ERR(svn_mergeinfo__filter_mergeinfo_by_ranges( + &mergeinfo, mergeinfo, + rb->pb->oldest_dumpstream_rev - 1, 0, + FALSE, subpool, subpool)); + SVN_ERR(svn_mergeinfo__adjust_mergeinfo_rangelists( + &predates_stream_mergeinfo, + predates_stream_mergeinfo, + -rb->rev_offset, subpool, subpool)); + } + else + { + predates_stream_mergeinfo = NULL; + } + + for (hi = apr_hash_first(subpool, mergeinfo); hi; hi = apr_hash_next(hi)) + { + svn_rangelist_t *rangelist; + struct parse_baton *pb = rb->pb; + int i; + const void *path; + apr_ssize_t pathlen; + void *val; + + apr_hash_this(hi, &path, &pathlen, &val); + rangelist = val; + + /* Possibly renumber revisions in merge source's rangelist. */ + for (i = 0; i < rangelist->nelts; i++) + { + svn_revnum_t rev_from_map; + svn_merge_range_t *range = APR_ARRAY_IDX(rangelist, i, + svn_merge_range_t *); + rev_from_map = get_revision_mapping(pb->rev_map, range->start); + if (SVN_IS_VALID_REVNUM(rev_from_map)) + { + range->start = rev_from_map; + } + else if (range->start == pb->oldest_dumpstream_rev - 1) + { + /* Since the start revision of svn_merge_range_t are not + inclusive there is one possible valid start revision that + won't be found in the PB->REV_MAP mapping of load stream + revsions to loaded revisions: The revision immediately + preceeding the oldest revision from the load stream. + This is a valid revision for mergeinfo, but not a valid + copy from revision (which PB->REV_MAP also maps for) so it + will never be in the mapping. + + If that is what we have here, then find the mapping for the + oldest rev from the load stream and subtract 1 to get the + renumbered, non-inclusive, start revision. */ + rev_from_map = get_revision_mapping(pb->rev_map, + pb->oldest_dumpstream_rev); + if (SVN_IS_VALID_REVNUM(rev_from_map)) + range->start = rev_from_map - 1; + } + else + { + /* If we can't remap the start revision then don't even bother + trying to remap the end revision. It's possible we might + actually succeed at the latter, which can result in invalid + mergeinfo with a start rev > end rev. If that gets into the + repository then a world of bustage breaks loose anytime that + bogus mergeinfo is parsed. See + http://subversion.tigris.org/issues/show_bug.cgi?id=3020#desc16. + */ + continue; + } + + rev_from_map = get_revision_mapping(pb->rev_map, range->end); + if (SVN_IS_VALID_REVNUM(rev_from_map)) + range->end = rev_from_map; + } + apr_hash_set(final_mergeinfo, path, pathlen, rangelist); + } + + if (predates_stream_mergeinfo) + { + SVN_ERR(svn_mergeinfo_merge2(final_mergeinfo, predates_stream_mergeinfo, + subpool, subpool)); + } + + SVN_ERR(svn_mergeinfo_sort(final_mergeinfo, subpool)); + + /* Mergeinfo revision sources for r0 and r1 are invalid; you can't merge r0 + or r1. However, svndumpfilter can be abused to produce r1 merge source + revs. So if we encounter any, then strip them out, no need to put them + into the load target. */ + SVN_ERR(svn_mergeinfo__filter_mergeinfo_by_ranges(&final_mergeinfo, + final_mergeinfo, + 1, 0, FALSE, + subpool, subpool)); + + SVN_ERR(svn_mergeinfo_to_string(final_val, final_mergeinfo, pool)); + svn_pool_destroy(subpool); + + return SVN_NO_ERROR; +} + + +static svn_error_t * +commit_callback(const svn_commit_info_t *commit_info, + void *baton, + apr_pool_t *pool) +{ + struct revision_baton *rb = baton; + struct parse_baton *pb = rb->pb; + + /* ### Don't print directly; generate a notification. */ + if (! pb->quiet) + SVN_ERR(svn_cmdline_printf(pool, "* Loaded revision %ld.\n", + commit_info->revision)); + + /* Add the mapping of the dumpstream revision to the committed revision. */ + set_revision_mapping(pb->rev_map, rb->rev, commit_info->revision); + + /* If the incoming dump stream has non-contiguous revisions (e.g. from + using svndumpfilter --drop-empty-revs without --renumber-revs) then + we must account for the missing gaps in PB->REV_MAP. Otherwise we + might not be able to map all mergeinfo source revisions to the correct + revisions in the target repos. */ + if ((pb->last_rev_mapped != SVN_INVALID_REVNUM) + && (rb->rev != pb->last_rev_mapped + 1)) + { + svn_revnum_t i; + + for (i = pb->last_rev_mapped + 1; i < rb->rev; i++) + { + set_revision_mapping(pb->rev_map, i, pb->last_rev_mapped); + } + } + + /* Update our "last revision mapped". */ + pb->last_rev_mapped = rb->rev; + + return SVN_NO_ERROR; +} + +/* Implements `svn_ra__lock_retry_func_t'. */ +static svn_error_t * +lock_retry_func(void *baton, + const svn_string_t *reposlocktoken, + apr_pool_t *pool) +{ + return svn_cmdline_printf(pool, + _("Failed to get lock on destination " + "repos, currently held by '%s'\n"), + reposlocktoken->data); +} + + +static svn_error_t * +fetch_base_func(const char **filename, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + struct revision_baton *rb = baton; + svn_stream_t *fstream; + svn_error_t *err; + + if (! SVN_IS_VALID_REVNUM(base_revision)) + base_revision = rb->rev - 1; + + SVN_ERR(svn_stream_open_unique(&fstream, filename, NULL, + svn_io_file_del_on_pool_cleanup, + result_pool, scratch_pool)); + + err = svn_ra_get_file(rb->pb->aux_session, path, base_revision, + fstream, NULL, NULL, scratch_pool); + if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + SVN_ERR(svn_stream_close(fstream)); + + *filename = NULL; + return SVN_NO_ERROR; + } + else if (err) + return svn_error_trace(err); + + SVN_ERR(svn_stream_close(fstream)); + + return SVN_NO_ERROR; +} + +static svn_error_t * +fetch_props_func(apr_hash_t **props, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + struct revision_baton *rb = baton; + svn_node_kind_t node_kind; + + if (! SVN_IS_VALID_REVNUM(base_revision)) + base_revision = rb->rev - 1; + + SVN_ERR(svn_ra_check_path(rb->pb->aux_session, path, base_revision, + &node_kind, scratch_pool)); + + if (node_kind == svn_node_file) + { + SVN_ERR(svn_ra_get_file(rb->pb->aux_session, path, base_revision, + NULL, NULL, props, result_pool)); + } + else if (node_kind == svn_node_dir) + { + apr_array_header_t *tmp_props; + + SVN_ERR(svn_ra_get_dir2(rb->pb->aux_session, NULL, NULL, props, path, + base_revision, 0 /* Dirent fields */, + result_pool)); + tmp_props = svn_prop_hash_to_array(*props, result_pool); + SVN_ERR(svn_categorize_props(tmp_props, NULL, NULL, &tmp_props, + result_pool)); + *props = svn_prop_array_to_hash(tmp_props, result_pool); + } + else + { + *props = apr_hash_make(result_pool); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +fetch_kind_func(svn_node_kind_t *kind, + void *baton, + const char *path, + svn_revnum_t base_revision, + apr_pool_t *scratch_pool) +{ + struct revision_baton *rb = baton; + + if (! SVN_IS_VALID_REVNUM(base_revision)) + base_revision = rb->rev - 1; + + SVN_ERR(svn_ra_check_path(rb->pb->aux_session, path, base_revision, + kind, scratch_pool)); + + return SVN_NO_ERROR; +} + +static svn_delta_shim_callbacks_t * +get_shim_callbacks(struct revision_baton *rb, + apr_pool_t *pool) +{ + svn_delta_shim_callbacks_t *callbacks = + svn_delta_shim_callbacks_default(pool); + + callbacks->fetch_props_func = fetch_props_func; + callbacks->fetch_kind_func = fetch_kind_func; + callbacks->fetch_base_func = fetch_base_func; + callbacks->fetch_baton = rb; + + return callbacks; +} + +/* Acquire a lock (of sorts) on the repository associated with the + * given RA SESSION. This lock is just a revprop change attempt in a + * time-delay loop. This function is duplicated by svnsync in + * svnsync/svnsync.c + * + * ### TODO: Make this function more generic and + * expose it through a header for use by other Subversion + * applications to avoid duplication. + */ +static svn_error_t * +get_lock(const svn_string_t **lock_string_p, + svn_ra_session_t *session, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + svn_boolean_t be_atomic; + + SVN_ERR(svn_ra_has_capability(session, &be_atomic, + SVN_RA_CAPABILITY_ATOMIC_REVPROPS, + pool)); + if (! be_atomic) + { + /* Pre-1.7 servers can't lock without a race condition. (Issue #3546) */ + svn_error_t *err = + svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL, + _("Target server does not support atomic revision " + "property edits; consider upgrading it to 1.7.")); + svn_handle_warning2(stderr, err, "svnrdump: "); + svn_error_clear(err); + } + + return svn_ra__get_operational_lock(lock_string_p, NULL, session, + SVNRDUMP_PROP_LOCK, FALSE, + 10 /* retries */, lock_retry_func, NULL, + cancel_func, cancel_baton, pool); +} + +static svn_error_t * +new_revision_record(void **revision_baton, + apr_hash_t *headers, + void *parse_baton, + apr_pool_t *pool) +{ + struct revision_baton *rb; + struct parse_baton *pb; + apr_hash_index_t *hi; + svn_revnum_t head_rev; + + rb = apr_pcalloc(pool, sizeof(*rb)); + pb = parse_baton; + rb->pool = svn_pool_create(pool); + rb->pb = pb; + + for (hi = apr_hash_first(pool, headers); hi; hi = apr_hash_next(hi)) + { + const char *hname = svn__apr_hash_index_key(hi); + const char *hval = svn__apr_hash_index_val(hi); + + if (strcmp(hname, SVN_REPOS_DUMPFILE_REVISION_NUMBER) == 0) + rb->rev = atoi(hval); + } + + SVN_ERR(svn_ra_get_latest_revnum(pb->session, &head_rev, pool)); + + /* FIXME: This is a lame fallback loading multiple segments of dump in + several separate operations. It is highly susceptible to race conditions. + Calculate the revision 'offset' for finding copyfrom sources. + It might be positive or negative. */ + rb->rev_offset = (apr_int32_t) ((rb->rev) - (head_rev + 1)); + + /* Stash the oldest (non-zero) dumpstream revision seen. */ + if ((rb->rev > 0) && (!SVN_IS_VALID_REVNUM(pb->oldest_dumpstream_rev))) + pb->oldest_dumpstream_rev = rb->rev; + + /* Set the commit_editor/ commit_edit_baton to NULL and wait for + them to be created in new_node_record */ + rb->pb->commit_editor = NULL; + rb->pb->commit_edit_baton = NULL; + rb->revprop_table = apr_hash_make(rb->pool); + + *revision_baton = rb; + return SVN_NO_ERROR; +} + +static svn_error_t * +magic_header_record(int version, + void *parse_baton, + apr_pool_t *pool) +{ + return SVN_NO_ERROR; +} + +static svn_error_t * +uuid_record(const char *uuid, + void *parse_baton, + apr_pool_t *pool) +{ + struct parse_baton *pb; + pb = parse_baton; + pb->uuid = apr_pstrdup(pool, uuid); + return SVN_NO_ERROR; +} + +static svn_error_t * +new_node_record(void **node_baton, + apr_hash_t *headers, + void *revision_baton, + apr_pool_t *pool) +{ + struct revision_baton *rb = revision_baton; + const struct svn_delta_editor_t *commit_editor = rb->pb->commit_editor; + void *commit_edit_baton = rb->pb->commit_edit_baton; + struct node_baton *nb; + struct directory_baton *child_db; + apr_hash_index_t *hi; + void *child_baton; + char *relpath_compose; + const char *nb_dirname; + + nb = apr_pcalloc(rb->pool, sizeof(*nb)); + nb->rb = rb; + + nb->copyfrom_path = NULL; + nb->copyfrom_rev = SVN_INVALID_REVNUM; + + /* If the creation of commit_editor is pending, create it now and + open_root on it; also create a top-level directory baton. */ + + if (!commit_editor) + { + /* The revprop_table should have been filled in with important + information like svn:log in set_revision_property. We can now + use it all this information to create our commit_editor. But + first, clear revprops that we aren't allowed to set with the + commit_editor. We'll set them separately using the RA API + after closing the editor (see close_revision). */ + + svn_hash_sets(rb->revprop_table, SVN_PROP_REVISION_AUTHOR, NULL); + svn_hash_sets(rb->revprop_table, SVN_PROP_REVISION_DATE, NULL); + + SVN_ERR(svn_ra__register_editor_shim_callbacks(rb->pb->session, + get_shim_callbacks(rb, rb->pool))); + SVN_ERR(svn_ra_get_commit_editor3(rb->pb->session, &commit_editor, + &commit_edit_baton, rb->revprop_table, + commit_callback, revision_baton, + NULL, FALSE, rb->pool)); + + rb->pb->commit_editor = commit_editor; + rb->pb->commit_edit_baton = commit_edit_baton; + + SVN_ERR(commit_editor->open_root(commit_edit_baton, + rb->rev - rb->rev_offset - 1, + rb->pool, &child_baton)); + + LDR_DBG(("Opened root %p\n", child_baton)); + + /* child_db corresponds to the root directory baton here */ + child_db = apr_pcalloc(rb->pool, sizeof(*child_db)); + child_db->baton = child_baton; + child_db->depth = 0; + child_db->relpath = ""; + child_db->parent = NULL; + rb->db = child_db; + } + + for (hi = apr_hash_first(rb->pool, headers); hi; hi = apr_hash_next(hi)) + { + const char *hname = svn__apr_hash_index_key(hi); + const char *hval = svn__apr_hash_index_val(hi); + + /* Parse the different kinds of headers we can encounter and + stuff them into the node_baton for writing later */ + if (strcmp(hname, SVN_REPOS_DUMPFILE_NODE_PATH) == 0) + nb->path = apr_pstrdup(rb->pool, hval); + if (strcmp(hname, SVN_REPOS_DUMPFILE_NODE_KIND) == 0) + nb->kind = strcmp(hval, "file") == 0 ? svn_node_file : svn_node_dir; + if (strcmp(hname, SVN_REPOS_DUMPFILE_NODE_ACTION) == 0) + { + if (strcmp(hval, "add") == 0) + nb->action = svn_node_action_add; + if (strcmp(hval, "change") == 0) + nb->action = svn_node_action_change; + if (strcmp(hval, "delete") == 0) + nb->action = svn_node_action_delete; + if (strcmp(hval, "replace") == 0) + nb->action = svn_node_action_replace; + } + if (strcmp(hname, SVN_REPOS_DUMPFILE_TEXT_DELTA_BASE_MD5) == 0) + nb->base_checksum = apr_pstrdup(rb->pool, hval); + if (strcmp(hname, SVN_REPOS_DUMPFILE_NODE_COPYFROM_REV) == 0) + nb->copyfrom_rev = atoi(hval); + if (strcmp(hname, SVN_REPOS_DUMPFILE_NODE_COPYFROM_PATH) == 0) + nb->copyfrom_path = apr_pstrdup(rb->pool, hval); + } + + nb_dirname = svn_relpath_dirname(nb->path, pool); + if (svn_path_compare_paths(nb_dirname, + rb->db->relpath) != 0) + { + char *ancestor_path; + apr_size_t residual_close_count; + apr_array_header_t *residual_open_path; + int i; + apr_size_t n; + + /* Before attempting to handle the action, call open_directory + for all the path components and set the directory baton + accordingly */ + ancestor_path = + svn_relpath_get_longest_ancestor(nb_dirname, + rb->db->relpath, pool); + residual_close_count = + svn_path_component_count(svn_relpath_skip_ancestor(ancestor_path, + rb->db->relpath)); + residual_open_path = + svn_path_decompose(svn_relpath_skip_ancestor(ancestor_path, + nb_dirname), pool); + + /* First close all as many directories as there are after + skip_ancestor, and then open fresh directories */ + for (n = 0; n < residual_close_count; n ++) + { + /* Don't worry about destroying the actual rb->db object, + since the pool we're using has the lifetime of one + revision anyway */ + LDR_DBG(("Closing dir %p\n", rb->db->baton)); + SVN_ERR(commit_editor->close_directory(rb->db->baton, rb->pool)); + rb->db = rb->db->parent; + } + + for (i = 0; i < residual_open_path->nelts; i ++) + { + relpath_compose = + svn_relpath_join(rb->db->relpath, + APR_ARRAY_IDX(residual_open_path, i, const char *), + rb->pool); + SVN_ERR(commit_editor->open_directory(relpath_compose, + rb->db->baton, + rb->rev - rb->rev_offset - 1, + rb->pool, &child_baton)); + LDR_DBG(("Opened dir %p\n", child_baton)); + child_db = apr_pcalloc(rb->pool, sizeof(*child_db)); + child_db->baton = child_baton; + child_db->depth = rb->db->depth + 1; + child_db->relpath = relpath_compose; + child_db->parent = rb->db; + rb->db = child_db; + } + } + + /* Fix up the copyfrom information in light of mapped revisions and + non-root load targets, and convert copyfrom path into a full + URL. */ + if (nb->copyfrom_path && SVN_IS_VALID_REVNUM(nb->copyfrom_rev)) + { + svn_revnum_t copyfrom_rev; + + /* Try to find the copyfrom revision in the revision map; + failing that, fall back to the revision offset approach. */ + copyfrom_rev = get_revision_mapping(rb->pb->rev_map, nb->copyfrom_rev); + if (! SVN_IS_VALID_REVNUM(copyfrom_rev)) + copyfrom_rev = nb->copyfrom_rev - rb->rev_offset; + + if (! SVN_IS_VALID_REVNUM(copyfrom_rev)) + return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL, + _("Relative source revision %ld is not" + " available in current repository"), + copyfrom_rev); + + nb->copyfrom_rev = copyfrom_rev; + + if (rb->pb->parent_dir) + nb->copyfrom_path = svn_relpath_join(rb->pb->parent_dir, + nb->copyfrom_path, rb->pool); + nb->copyfrom_path = svn_path_url_add_component2(rb->pb->root_url, + nb->copyfrom_path, + rb->pool); + } + + + switch (nb->action) + { + case svn_node_action_delete: + case svn_node_action_replace: + LDR_DBG(("Deleting entry %s in %p\n", nb->path, rb->db->baton)); + SVN_ERR(commit_editor->delete_entry(nb->path, rb->rev - rb->rev_offset, + rb->db->baton, rb->pool)); + if (nb->action == svn_node_action_delete) + break; + else + /* FALL THROUGH */; + case svn_node_action_add: + switch (nb->kind) + { + case svn_node_file: + SVN_ERR(commit_editor->add_file(nb->path, rb->db->baton, + nb->copyfrom_path, + nb->copyfrom_rev, + rb->pool, &(nb->file_baton))); + LDR_DBG(("Added file %s to dir %p as %p\n", + nb->path, rb->db->baton, nb->file_baton)); + break; + case svn_node_dir: + SVN_ERR(commit_editor->add_directory(nb->path, rb->db->baton, + nb->copyfrom_path, + nb->copyfrom_rev, + rb->pool, &child_baton)); + LDR_DBG(("Added dir %s to dir %p as %p\n", + nb->path, rb->db->baton, child_baton)); + child_db = apr_pcalloc(rb->pool, sizeof(*child_db)); + child_db->baton = child_baton; + child_db->depth = rb->db->depth + 1; + child_db->relpath = apr_pstrdup(rb->pool, nb->path); + child_db->parent = rb->db; + rb->db = child_db; + break; + default: + break; + } + break; + case svn_node_action_change: + switch (nb->kind) + { + case svn_node_file: + SVN_ERR(commit_editor->open_file(nb->path, rb->db->baton, + SVN_INVALID_REVNUM, rb->pool, + &(nb->file_baton))); + break; + default: + SVN_ERR(commit_editor->open_directory(nb->path, rb->db->baton, + rb->rev - rb->rev_offset - 1, + rb->pool, &child_baton)); + child_db = apr_pcalloc(rb->pool, sizeof(*child_db)); + child_db->baton = child_baton; + child_db->depth = rb->db->depth + 1; + child_db->relpath = apr_pstrdup(rb->pool, nb->path); + child_db->parent = rb->db; + rb->db = child_db; + break; + } + break; + } + + *node_baton = nb; + return SVN_NO_ERROR; +} + +static svn_error_t * +set_revision_property(void *baton, + const char *name, + const svn_string_t *value) +{ + struct revision_baton *rb = baton; + + SVN_ERR(svn_rdump__normalize_prop(name, &value, rb->pool)); + + SVN_ERR(svn_repos__validate_prop(name, value, rb->pool)); + + if (rb->rev > 0) + { + svn_hash_sets(rb->revprop_table, + apr_pstrdup(rb->pool, name), + svn_string_dup(value, rb->pool)); + } + else if (rb->rev_offset == -1) + { + /* Special case: set revision 0 properties directly (which is + safe because the commit_editor hasn't been created yet), but + only when loading into an 'empty' filesystem. */ + SVN_ERR(svn_ra_change_rev_prop2(rb->pb->session, 0, + name, NULL, value, rb->pool)); + } + + /* Remember any datestamp/ author that passes through (see comment + in close_revision). */ + if (!strcmp(name, SVN_PROP_REVISION_DATE)) + rb->datestamp = svn_string_dup(value, rb->pool); + if (!strcmp(name, SVN_PROP_REVISION_AUTHOR)) + rb->author = svn_string_dup(value, rb->pool); + + return SVN_NO_ERROR; +} + +static svn_error_t * +set_node_property(void *baton, + const char *name, + const svn_string_t *value) +{ + struct node_baton *nb = baton; + const struct svn_delta_editor_t *commit_editor = nb->rb->pb->commit_editor; + apr_pool_t *pool = nb->rb->pool; + + if (value && strcmp(name, SVN_PROP_MERGEINFO) == 0) + { + svn_string_t *renumbered_mergeinfo; + svn_string_t prop_val; + + /* Tolerate mergeinfo with "\r\n" line endings because some + dumpstream sources might contain as much. If so normalize + the line endings to '\n' and make a notification to + PARSE_BATON->FEEDBACK_STREAM that we have made this + correction. */ + if (strstr(value->data, "\r")) + { + const char *prop_eol_normalized; + + SVN_ERR(svn_subst_translate_cstring2(value->data, + &prop_eol_normalized, + "\n", /* translate to LF */ + FALSE, /* no repair */ + NULL, /* no keywords */ + FALSE, /* no expansion */ + pool)); + prop_val.data = prop_eol_normalized; + prop_val.len = strlen(prop_eol_normalized); + value = &prop_val; + + /* ### TODO: notify? */ + } + + /* Renumber mergeinfo as appropriate. */ + SVN_ERR(renumber_mergeinfo_revs(&renumbered_mergeinfo, value, + nb->rb, pool)); + value = renumbered_mergeinfo; + + if (nb->rb->pb->parent_dir) + { + /* Prefix the merge source paths with PB->parent_dir. */ + /* ASSUMPTION: All source paths are included in the dump stream. */ + svn_string_t *mergeinfo_val; + SVN_ERR(prefix_mergeinfo_paths(&mergeinfo_val, value, + nb->rb->pb->parent_dir, pool)); + value = mergeinfo_val; + } + } + + SVN_ERR(svn_rdump__normalize_prop(name, &value, pool)); + + SVN_ERR(svn_repos__validate_prop(name, value, pool)); + + switch (nb->kind) + { + case svn_node_file: + LDR_DBG(("Applying properties on %p\n", nb->file_baton)); + SVN_ERR(commit_editor->change_file_prop(nb->file_baton, name, + value, pool)); + break; + case svn_node_dir: + LDR_DBG(("Applying properties on %p\n", nb->rb->db->baton)); + SVN_ERR(commit_editor->change_dir_prop(nb->rb->db->baton, name, + value, pool)); + break; + default: + break; + } + return SVN_NO_ERROR; +} + +static svn_error_t * +delete_node_property(void *baton, + const char *name) +{ + struct node_baton *nb = baton; + const struct svn_delta_editor_t *commit_editor = nb->rb->pb->commit_editor; + apr_pool_t *pool = nb->rb->pool; + + SVN_ERR(svn_repos__validate_prop(name, NULL, pool)); + + if (nb->kind == svn_node_file) + SVN_ERR(commit_editor->change_file_prop(nb->file_baton, name, + NULL, pool)); + else + SVN_ERR(commit_editor->change_dir_prop(nb->rb->db->baton, name, + NULL, pool)); + + return SVN_NO_ERROR; +} + +static svn_error_t * +remove_node_props(void *baton) +{ + struct node_baton *nb = baton; + apr_pool_t *pool = nb->rb->pool; + apr_hash_index_t *hi; + apr_hash_t *props; + + if ((nb->action == svn_node_action_add + || nb->action == svn_node_action_replace) + && ! SVN_IS_VALID_REVNUM(nb->copyfrom_rev)) + /* Add-without-history; no "old" properties to worry about. */ + return SVN_NO_ERROR; + + if (nb->kind == svn_node_file) + { + SVN_ERR(svn_ra_get_file(nb->rb->pb->aux_session, nb->path, + SVN_INVALID_REVNUM, NULL, NULL, &props, pool)); + } + else /* nb->kind == svn_node_dir */ + { + SVN_ERR(svn_ra_get_dir2(nb->rb->pb->aux_session, NULL, NULL, &props, + nb->path, SVN_INVALID_REVNUM, 0, pool)); + } + + for (hi = apr_hash_first(pool, props); hi; hi = apr_hash_next(hi)) + { + const char *name = svn__apr_hash_index_key(hi); + svn_prop_kind_t kind = svn_property_kind2(name); + + if (kind == svn_prop_regular_kind) + SVN_ERR(set_node_property(nb, name, NULL)); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +set_fulltext(svn_stream_t **stream, + void *node_baton) +{ + struct node_baton *nb = node_baton; + const struct svn_delta_editor_t *commit_editor = nb->rb->pb->commit_editor; + svn_txdelta_window_handler_t handler; + void *handler_baton; + apr_pool_t *pool = nb->rb->pool; + + LDR_DBG(("Setting fulltext for %p\n", nb->file_baton)); + SVN_ERR(commit_editor->apply_textdelta(nb->file_baton, nb->base_checksum, + pool, &handler, &handler_baton)); + *stream = svn_txdelta_target_push(handler, handler_baton, + svn_stream_empty(pool), pool); + return SVN_NO_ERROR; +} + +static svn_error_t * +apply_textdelta(svn_txdelta_window_handler_t *handler, + void **handler_baton, + void *node_baton) +{ + struct node_baton *nb = node_baton; + const struct svn_delta_editor_t *commit_editor = nb->rb->pb->commit_editor; + apr_pool_t *pool = nb->rb->pool; + + LDR_DBG(("Applying textdelta to %p\n", nb->file_baton)); + SVN_ERR(commit_editor->apply_textdelta(nb->file_baton, nb->base_checksum, + pool, handler, handler_baton)); + + return SVN_NO_ERROR; +} + +static svn_error_t * +close_node(void *baton) +{ + struct node_baton *nb = baton; + const struct svn_delta_editor_t *commit_editor = nb->rb->pb->commit_editor; + + /* Pass a file node closure through to the editor *unless* we + deleted the file (which doesn't require us to open it). */ + if ((nb->kind == svn_node_file) && (nb->file_baton)) + { + LDR_DBG(("Closing file %p\n", nb->file_baton)); + SVN_ERR(commit_editor->close_file(nb->file_baton, NULL, nb->rb->pool)); + } + + /* The svn_node_dir case is handled in close_revision */ + + return SVN_NO_ERROR; +} + +static svn_error_t * +close_revision(void *baton) +{ + struct revision_baton *rb = baton; + const svn_delta_editor_t *commit_editor = rb->pb->commit_editor; + void *commit_edit_baton = rb->pb->commit_edit_baton; + svn_revnum_t committed_rev = SVN_INVALID_REVNUM; + + /* Fake revision 0 */ + if (rb->rev == 0) + { + /* ### Don't print directly; generate a notification. */ + if (! rb->pb->quiet) + SVN_ERR(svn_cmdline_printf(rb->pool, "* Loaded revision 0.\n")); + } + else if (commit_editor) + { + /* Close all pending open directories, and then close the edit + session itself */ + while (rb->db && rb->db->parent) + { + LDR_DBG(("Closing dir %p\n", rb->db->baton)); + SVN_ERR(commit_editor->close_directory(rb->db->baton, rb->pool)); + rb->db = rb->db->parent; + } + /* root dir's baton */ + LDR_DBG(("Closing edit on %p\n", commit_edit_baton)); + SVN_ERR(commit_editor->close_directory(rb->db->baton, rb->pool)); + SVN_ERR(commit_editor->close_edit(commit_edit_baton, rb->pool)); + } + else + { + void *child_baton; + + /* Legitimate revision with no node information */ + SVN_ERR(svn_ra_get_commit_editor3(rb->pb->session, &commit_editor, + &commit_edit_baton, rb->revprop_table, + commit_callback, baton, + NULL, FALSE, rb->pool)); + + SVN_ERR(commit_editor->open_root(commit_edit_baton, + rb->rev - rb->rev_offset - 1, + rb->pool, &child_baton)); + + LDR_DBG(("Opened root %p\n", child_baton)); + LDR_DBG(("Closing edit on %p\n", commit_edit_baton)); + SVN_ERR(commit_editor->close_directory(child_baton, rb->pool)); + SVN_ERR(commit_editor->close_edit(commit_edit_baton, rb->pool)); + } + + /* svn_fs_commit_txn() rewrites the datestamp and author properties; + we'll rewrite them again by hand after closing the commit_editor. + The only time we don't do this is for revision 0 when loaded into + a non-empty repository. */ + if (rb->rev > 0) + { + committed_rev = get_revision_mapping(rb->pb->rev_map, rb->rev); + } + else if (rb->rev_offset == -1) + { + committed_rev = 0; + } + + if (SVN_IS_VALID_REVNUM(committed_rev)) + { + SVN_ERR(svn_repos__validate_prop(SVN_PROP_REVISION_DATE, + rb->datestamp, rb->pool)); + SVN_ERR(svn_ra_change_rev_prop2(rb->pb->session, committed_rev, + SVN_PROP_REVISION_DATE, + NULL, rb->datestamp, rb->pool)); + SVN_ERR(svn_repos__validate_prop(SVN_PROP_REVISION_AUTHOR, + rb->author, rb->pool)); + SVN_ERR(svn_ra_change_rev_prop2(rb->pb->session, committed_rev, + SVN_PROP_REVISION_AUTHOR, + NULL, rb->author, rb->pool)); + } + + svn_pool_destroy(rb->pool); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_rdump__load_dumpstream(svn_stream_t *stream, + svn_ra_session_t *session, + svn_ra_session_t *aux_session, + svn_boolean_t quiet, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + svn_repos_parse_fns3_t *parser; + struct parse_baton *parse_baton; + const svn_string_t *lock_string; + svn_boolean_t be_atomic; + svn_error_t *err; + const char *session_url, *root_url, *parent_dir; + + SVN_ERR(svn_ra_has_capability(session, &be_atomic, + SVN_RA_CAPABILITY_ATOMIC_REVPROPS, + pool)); + SVN_ERR(get_lock(&lock_string, session, cancel_func, cancel_baton, pool)); + SVN_ERR(svn_ra_get_repos_root2(session, &root_url, pool)); + SVN_ERR(svn_ra_get_session_url(session, &session_url, pool)); + SVN_ERR(svn_ra_get_path_relative_to_root(session, &parent_dir, + session_url, pool)); + + parser = apr_pcalloc(pool, sizeof(*parser)); + parser->magic_header_record = magic_header_record; + parser->uuid_record = uuid_record; + parser->new_revision_record = new_revision_record; + parser->new_node_record = new_node_record; + parser->set_revision_property = set_revision_property; + parser->set_node_property = set_node_property; + parser->delete_node_property = delete_node_property; + parser->remove_node_props = remove_node_props; + parser->set_fulltext = set_fulltext; + parser->apply_textdelta = apply_textdelta; + parser->close_node = close_node; + parser->close_revision = close_revision; + + parse_baton = apr_pcalloc(pool, sizeof(*parse_baton)); + parse_baton->session = session; + parse_baton->aux_session = aux_session; + parse_baton->quiet = quiet; + parse_baton->root_url = root_url; + parse_baton->parent_dir = parent_dir; + parse_baton->rev_map = apr_hash_make(pool); + parse_baton->last_rev_mapped = SVN_INVALID_REVNUM; + parse_baton->oldest_dumpstream_rev = SVN_INVALID_REVNUM; + + err = svn_repos_parse_dumpstream3(stream, parser, parse_baton, FALSE, + cancel_func, cancel_baton, pool); + + /* If all goes well, or if we're cancelled cleanly, don't leave a + stray lock behind. */ + if ((! err) || (err && (err->apr_err == SVN_ERR_CANCELLED))) + err = svn_error_compose_create( + svn_ra__release_operational_lock(session, SVNRDUMP_PROP_LOCK, + lock_string, pool), + err); + return err; +} diff --git a/subversion/svnrdump/svnrdump.1 b/subversion/svnrdump/svnrdump.1 new file mode 100644 index 0000000000000..99f5210f124a7 --- /dev/null +++ b/subversion/svnrdump/svnrdump.1 @@ -0,0 +1,47 @@ +.\" +.\" +.\" 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. +.\" +.\" +.\" You can view this file with: +.\" nroff -man [filename] +.\" +.TH svnrdump 1 +.SH NAME +svnrdump \- Subversion remote repository dumper and loader +.SH SYNOPSIS +.TP +\fBsvnrdump\fP \fIcommand\fP \fIurl\fP [\fIoptions\fP] [\fIargs\fP] +.SH OVERVIEW +Subversion is a version control system, which allows you to keep old +versions of files and directories (usually source code), keep a log of +who, when, and why changes occurred, etc., like CVS, RCS or SCCS. +\fBSubversion\fP keeps a single copy of the master sources. This copy +is called the source ``repository''; it contains all the information +to permit extracting previous versions of those files at any time. + +For more information about the Subversion project, visit +http://subversion.apache.org. + +Documentation for Subversion and its tools, including detailed usage +explanations of the \fBsvn\fP, \fBsvnadmin\fP, \fBsvnserve\fP and +\fBsvnlook\fP programs, historical background, philosophical +approaches and reasonings, etc., can be found at +http://svnbook.red-bean.com/. + +Run `svnrdump help' to access the built-in tool documentation. diff --git a/subversion/svnrdump/svnrdump.c b/subversion/svnrdump/svnrdump.c new file mode 100644 index 0000000000000..6bf409c7d4498 --- /dev/null +++ b/subversion/svnrdump/svnrdump.c @@ -0,0 +1,1185 @@ +/* + * svnrdump.c: Produce a dumpfile of a local or remote repository + * without touching the filesystem, but for temporary files. + * + * ==================================================================== + * 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 <apr_signal.h> +#include <apr_uri.h> + +#include "svn_pools.h" +#include "svn_cmdline.h" +#include "svn_client.h" +#include "svn_hash.h" +#include "svn_ra.h" +#include "svn_repos.h" +#include "svn_path.h" +#include "svn_utf.h" +#include "svn_private_config.h" +#include "svn_string.h" +#include "svn_props.h" + +#include "svnrdump.h" + +#include "private/svn_cmdline_private.h" +#include "private/svn_ra_private.h" + + + +/*** Cancellation ***/ + +/* A flag to see if we've been cancelled by the client or not. */ +static volatile sig_atomic_t cancelled = FALSE; + +/* A signal handler to support cancellation. */ +static void +signal_handler(int signum) +{ + apr_signal(signum, SIG_IGN); + cancelled = TRUE; +} + +/* Our cancellation callback. */ +static svn_error_t * +check_cancel(void *baton) +{ + if (cancelled) + return svn_error_create(SVN_ERR_CANCELLED, NULL, _("Caught signal")); + else + return SVN_NO_ERROR; +} + + + + +static svn_opt_subcommand_t dump_cmd, load_cmd; + +enum svn_svnrdump__longopt_t + { + opt_config_dir = SVN_OPT_FIRST_LONGOPT_ID, + opt_config_option, + opt_auth_username, + opt_auth_password, + opt_auth_nocache, + opt_non_interactive, + opt_force_interactive, + opt_incremental, + opt_trust_server_cert, + opt_version + }; + +#define SVN_SVNRDUMP__BASE_OPTIONS opt_config_dir, \ + opt_config_option, \ + opt_auth_username, \ + opt_auth_password, \ + opt_auth_nocache, \ + opt_trust_server_cert, \ + opt_non_interactive, \ + opt_force_interactive + +static const svn_opt_subcommand_desc2_t svnrdump__cmd_table[] = +{ + { "dump", dump_cmd, { 0 }, + N_("usage: svnrdump dump URL [-r LOWER[:UPPER]]\n\n" + "Dump revisions LOWER to UPPER of repository at remote URL to stdout\n" + "in a 'dumpfile' portable format. If only LOWER is given, dump that\n" + "one revision.\n"), + { 'r', 'q', opt_incremental, SVN_SVNRDUMP__BASE_OPTIONS } }, + { "load", load_cmd, { 0 }, + N_("usage: svnrdump load URL\n\n" + "Load a 'dumpfile' given on stdin to a repository at remote URL.\n"), + { 'q', SVN_SVNRDUMP__BASE_OPTIONS } }, + { "help", 0, { "?", "h" }, + N_("usage: svnrdump help [SUBCOMMAND...]\n\n" + "Describe the usage of this program or its subcommands.\n"), + { 0 } }, + { NULL, NULL, { 0 }, NULL, { 0 } } +}; + +static const apr_getopt_option_t svnrdump__options[] = + { + {"revision", 'r', 1, + N_("specify revision number ARG (or X:Y range)")}, + {"quiet", 'q', 0, + N_("no progress (only errors) to stderr")}, + {"incremental", opt_incremental, 0, + N_("dump incrementally")}, + {"config-dir", opt_config_dir, 1, + N_("read user configuration files from directory ARG")}, + {"username", opt_auth_username, 1, + N_("specify a username ARG")}, + {"password", opt_auth_password, 1, + N_("specify a password ARG")}, + {"non-interactive", opt_non_interactive, 0, + N_("do no interactive prompting (default is to prompt\n" + " " + "only if standard input is a terminal device)")}, + {"force-interactive", opt_force_interactive, 0, + N_("do interactive prompting even if standard input\n" + " " + "is not a terminal device")}, + {"no-auth-cache", opt_auth_nocache, 0, + N_("do not cache authentication tokens")}, + {"help", 'h', 0, + N_("display this help")}, + {"version", opt_version, 0, + N_("show program version information")}, + {"config-option", opt_config_option, 1, + N_("set user configuration option in the format:\n" + " " + " FILE:SECTION:OPTION=[VALUE]\n" + " " + "For example:\n" + " " + " servers:global:http-library=serf")}, + {"trust-server-cert", opt_trust_server_cert, 0, + N_("accept SSL server certificates from unknown\n" + " " + "certificate authorities without prompting (but only\n" + " " + "with '--non-interactive')") }, + {0, 0, 0, 0} + }; + +/* Baton for the RA replay session. */ +struct replay_baton { + /* A backdoor ra session for fetching information. */ + svn_ra_session_t *extra_ra_session; + + /* The output stream */ + svn_stream_t *stdout_stream; + + /* Whether to be quiet. */ + svn_boolean_t quiet; +}; + +/* Option set */ +typedef struct opt_baton_t { + svn_client_ctx_t *ctx; + svn_ra_session_t *session; + const char *url; + svn_boolean_t help; + svn_boolean_t version; + svn_opt_revision_t start_revision; + svn_opt_revision_t end_revision; + svn_boolean_t quiet; + svn_boolean_t incremental; +} opt_baton_t; + +/* Print dumpstream-formatted information about REVISION. + * Implements the `svn_ra_replay_revstart_callback_t' interface. + */ +static svn_error_t * +replay_revstart(svn_revnum_t revision, + void *replay_baton, + const svn_delta_editor_t **editor, + void **edit_baton, + apr_hash_t *rev_props, + apr_pool_t *pool) +{ + struct replay_baton *rb = replay_baton; + apr_hash_t *normal_props; + svn_stringbuf_t *propstring; + svn_stream_t *stdout_stream; + svn_stream_t *revprop_stream; + + SVN_ERR(svn_stream_for_stdout(&stdout_stream, pool)); + + /* Revision-number: 19 */ + SVN_ERR(svn_stream_printf(stdout_stream, pool, + SVN_REPOS_DUMPFILE_REVISION_NUMBER + ": %ld\n", revision)); + SVN_ERR(svn_rdump__normalize_props(&normal_props, rev_props, pool)); + propstring = svn_stringbuf_create_ensure(0, pool); + revprop_stream = svn_stream_from_stringbuf(propstring, pool); + SVN_ERR(svn_hash_write2(normal_props, revprop_stream, "PROPS-END", pool)); + SVN_ERR(svn_stream_close(revprop_stream)); + + /* Prop-content-length: 13 */ + SVN_ERR(svn_stream_printf(stdout_stream, pool, + SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH + ": %" APR_SIZE_T_FMT "\n", propstring->len)); + + /* Content-length: 29 */ + SVN_ERR(svn_stream_printf(stdout_stream, pool, + SVN_REPOS_DUMPFILE_CONTENT_LENGTH + ": %" APR_SIZE_T_FMT "\n\n", propstring->len)); + + /* Property data. */ + SVN_ERR(svn_stream_write(stdout_stream, propstring->data, + &(propstring->len))); + + SVN_ERR(svn_stream_puts(stdout_stream, "\n")); + SVN_ERR(svn_stream_close(stdout_stream)); + + SVN_ERR(svn_rdump__get_dump_editor(editor, edit_baton, revision, + rb->stdout_stream, rb->extra_ra_session, + NULL, check_cancel, NULL, pool)); + + return SVN_NO_ERROR; +} + +/* Print progress information about the dump of REVISION. + Implements the `svn_ra_replay_revfinish_callback_t' interface. */ +static svn_error_t * +replay_revend(svn_revnum_t revision, + void *replay_baton, + const svn_delta_editor_t *editor, + void *edit_baton, + apr_hash_t *rev_props, + apr_pool_t *pool) +{ + /* No resources left to free. */ + struct replay_baton *rb = replay_baton; + + SVN_ERR(editor->close_edit(edit_baton, pool)); + + if (! rb->quiet) + SVN_ERR(svn_cmdline_fprintf(stderr, pool, "* Dumped revision %lu.\n", + revision)); + return SVN_NO_ERROR; +} + +#ifdef USE_EV2_IMPL +/* Print dumpstream-formatted information about REVISION. + * Implements the `svn_ra_replay_revstart_callback_t' interface. + */ +static svn_error_t * +replay_revstart_v2(svn_revnum_t revision, + void *replay_baton, + svn_editor_t **editor, + apr_hash_t *rev_props, + apr_pool_t *pool) +{ + struct replay_baton *rb = replay_baton; + apr_hash_t *normal_props; + svn_stringbuf_t *propstring; + svn_stream_t *stdout_stream; + svn_stream_t *revprop_stream; + + SVN_ERR(svn_stream_for_stdout(&stdout_stream, pool)); + + /* Revision-number: 19 */ + SVN_ERR(svn_stream_printf(stdout_stream, pool, + SVN_REPOS_DUMPFILE_REVISION_NUMBER + ": %ld\n", revision)); + SVN_ERR(svn_rdump__normalize_props(&normal_props, rev_props, pool)); + propstring = svn_stringbuf_create_ensure(0, pool); + revprop_stream = svn_stream_from_stringbuf(propstring, pool); + SVN_ERR(svn_hash_write2(normal_props, revprop_stream, "PROPS-END", pool)); + SVN_ERR(svn_stream_close(revprop_stream)); + + /* Prop-content-length: 13 */ + SVN_ERR(svn_stream_printf(stdout_stream, pool, + SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH + ": %" APR_SIZE_T_FMT "\n", propstring->len)); + + /* Content-length: 29 */ + SVN_ERR(svn_stream_printf(stdout_stream, pool, + SVN_REPOS_DUMPFILE_CONTENT_LENGTH + ": %" APR_SIZE_T_FMT "\n\n", propstring->len)); + + /* Property data. */ + SVN_ERR(svn_stream_write(stdout_stream, propstring->data, + &(propstring->len))); + + SVN_ERR(svn_stream_puts(stdout_stream, "\n")); + SVN_ERR(svn_stream_close(stdout_stream)); + + SVN_ERR(svn_rdump__get_dump_editor_v2(editor, revision, + rb->stdout_stream, + rb->extra_ra_session, + NULL, check_cancel, NULL, pool, pool)); + + return SVN_NO_ERROR; +} + +/* Print progress information about the dump of REVISION. + Implements the `svn_ra_replay_revfinish_callback_t' interface. */ +static svn_error_t * +replay_revend_v2(svn_revnum_t revision, + void *replay_baton, + svn_editor_t *editor, + apr_hash_t *rev_props, + apr_pool_t *pool) +{ + /* No resources left to free. */ + struct replay_baton *rb = replay_baton; + + SVN_ERR(svn_editor_complete(editor)); + + if (! rb->quiet) + SVN_ERR(svn_cmdline_fprintf(stderr, pool, "* Dumped revision %lu.\n", + revision)); + return SVN_NO_ERROR; +} +#endif + +/* Initialize the RA layer, and set *CTX to a new client context baton + * allocated from POOL. Use CONFIG_DIR and pass USERNAME, PASSWORD, + * CONFIG_DIR and NO_AUTH_CACHE to initialize the authorization baton. + * CONFIG_OPTIONS (if not NULL) is a list of configuration overrides. + * REPOS_URL is used to fiddle with server-specific configuration + * options. + */ +static svn_error_t * +init_client_context(svn_client_ctx_t **ctx_p, + svn_boolean_t non_interactive, + const char *username, + const char *password, + const char *config_dir, + const char *repos_url, + svn_boolean_t no_auth_cache, + svn_boolean_t trust_server_cert, + apr_array_header_t *config_options, + apr_pool_t *pool) +{ + svn_client_ctx_t *ctx = NULL; + svn_config_t *cfg_config, *cfg_servers; + + SVN_ERR(svn_ra_initialize(pool)); + + SVN_ERR(svn_config_ensure(config_dir, pool)); + SVN_ERR(svn_client_create_context2(&ctx, NULL, pool)); + + SVN_ERR(svn_config_get_config(&(ctx->config), config_dir, pool)); + + if (config_options) + SVN_ERR(svn_cmdline__apply_config_options(ctx->config, config_options, + "svnrdump: ", "--config-option")); + + cfg_config = svn_hash_gets(ctx->config, SVN_CONFIG_CATEGORY_CONFIG); + + /* ### FIXME: This is a hack to work around the fact that our dump + ### editor simply can't handle the way ra_serf violates the + ### editor v1 drive ordering requirements. + ### + ### We'll override both the global value and server-specific one + ### for the 'http-bulk-updates' and 'http-max-connections' + ### options in order to get ra_serf to try a bulk-update if the + ### server will allow it, or at least try to limit all its + ### auxiliary GETs/PROPFINDs to happening (well-ordered) on a + ### single server connection. + ### + ### See http://subversion.tigris.org/issues/show_bug.cgi?id=4116. + */ + cfg_servers = svn_hash_gets(ctx->config, SVN_CONFIG_CATEGORY_SERVERS); + svn_config_set_bool(cfg_servers, SVN_CONFIG_SECTION_GLOBAL, + SVN_CONFIG_OPTION_HTTP_BULK_UPDATES, TRUE); + svn_config_set_int64(cfg_servers, SVN_CONFIG_SECTION_GLOBAL, + SVN_CONFIG_OPTION_HTTP_MAX_CONNECTIONS, 2); + if (cfg_servers) + { + apr_status_t status; + apr_uri_t parsed_url; + + status = apr_uri_parse(pool, repos_url, &parsed_url); + if (! status) + { + const char *server_group; + + server_group = svn_config_find_group(cfg_servers, parsed_url.hostname, + SVN_CONFIG_SECTION_GROUPS, pool); + if (server_group) + { + svn_config_set_bool(cfg_servers, server_group, + SVN_CONFIG_OPTION_HTTP_BULK_UPDATES, TRUE); + svn_config_set_int64(cfg_servers, server_group, + SVN_CONFIG_OPTION_HTTP_MAX_CONNECTIONS, 2); + } + } + } + + /* Set up our cancellation support. */ + ctx->cancel_func = check_cancel; + + /* Default authentication providers for non-interactive use */ + SVN_ERR(svn_cmdline_create_auth_baton(&(ctx->auth_baton), non_interactive, + username, password, config_dir, + no_auth_cache, trust_server_cert, + cfg_config, ctx->cancel_func, + ctx->cancel_baton, pool)); + *ctx_p = ctx; + return SVN_NO_ERROR; +} + +/* Print a revision record header for REVISION to STDOUT_STREAM. Use + * SESSION to contact the repository for revision properties and + * such. + */ +static svn_error_t * +dump_revision_header(svn_ra_session_t *session, + svn_stream_t *stdout_stream, + svn_revnum_t revision, + apr_pool_t *pool) +{ + apr_hash_t *prophash; + svn_stringbuf_t *propstring; + svn_stream_t *propstream; + + SVN_ERR(svn_stream_printf(stdout_stream, pool, + SVN_REPOS_DUMPFILE_REVISION_NUMBER + ": %ld\n", revision)); + + prophash = apr_hash_make(pool); + propstring = svn_stringbuf_create_empty(pool); + SVN_ERR(svn_ra_rev_proplist(session, revision, &prophash, pool)); + + propstream = svn_stream_from_stringbuf(propstring, pool); + SVN_ERR(svn_hash_write2(prophash, propstream, "PROPS-END", pool)); + SVN_ERR(svn_stream_close(propstream)); + + /* Property-content-length: 14; Content-length: 14 */ + SVN_ERR(svn_stream_printf(stdout_stream, pool, + SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH + ": %" APR_SIZE_T_FMT "\n", + propstring->len)); + SVN_ERR(svn_stream_printf(stdout_stream, pool, + SVN_REPOS_DUMPFILE_CONTENT_LENGTH + ": %" APR_SIZE_T_FMT "\n\n", + propstring->len)); + /* The properties */ + SVN_ERR(svn_stream_write(stdout_stream, propstring->data, + &(propstring->len))); + SVN_ERR(svn_stream_puts(stdout_stream, "\n")); + + return SVN_NO_ERROR; +} + +static svn_error_t * +dump_initial_full_revision(svn_ra_session_t *session, + svn_ra_session_t *extra_ra_session, + svn_stream_t *stdout_stream, + svn_revnum_t revision, + svn_boolean_t quiet, + apr_pool_t *pool) +{ + const svn_ra_reporter3_t *reporter; + void *report_baton; + const svn_delta_editor_t *dump_editor; + void *dump_baton; + const char *session_url, *source_relpath; + + /* Determine whether we're dumping the repository root URL or some + child thereof. If we're dumping a subtree of the repository + rather than the root, we have to jump through some hoops to make + our update-driven dump generation work the way a replay-driven + one would. + + See http://subversion.tigris.org/issues/show_bug.cgi?id=4101 + */ + SVN_ERR(svn_ra_get_session_url(session, &session_url, pool)); + SVN_ERR(svn_ra_get_path_relative_to_root(session, &source_relpath, + session_url, pool)); + + /* Start with a revision record header. */ + SVN_ERR(dump_revision_header(session, stdout_stream, revision, pool)); + + /* Then, we'll drive the dump editor with what would look like a + full checkout of the repository as it looked in START_REVISION. + We do this by manufacturing a basic 'report' to the update + reporter, telling it that we have nothing to start with. The + delta between nothing and everything-at-REV is, effectively, a + full dump of REV. */ + SVN_ERR(svn_rdump__get_dump_editor(&dump_editor, &dump_baton, revision, + stdout_stream, extra_ra_session, + source_relpath, check_cancel, NULL, pool)); + SVN_ERR(svn_ra_do_update3(session, &reporter, &report_baton, revision, + "", svn_depth_infinity, FALSE, FALSE, + dump_editor, dump_baton, pool, pool)); + SVN_ERR(reporter->set_path(report_baton, "", revision, + svn_depth_infinity, TRUE, NULL, pool)); + SVN_ERR(reporter->finish_report(report_baton, pool)); + + /* All finished with START_REVISION! */ + if (! quiet) + SVN_ERR(svn_cmdline_fprintf(stderr, pool, "* Dumped revision %lu.\n", + revision)); + + return SVN_NO_ERROR; +} + +/* Replay revisions START_REVISION thru END_REVISION (inclusive) of + * the repository URL at which SESSION is rooted, using callbacks + * which generate Subversion repository dumpstreams describing the + * changes made in those revisions. If QUIET is set, don't generate + * progress messages. + */ +static svn_error_t * +replay_revisions(svn_ra_session_t *session, + svn_ra_session_t *extra_ra_session, + svn_revnum_t start_revision, + svn_revnum_t end_revision, + svn_boolean_t quiet, + svn_boolean_t incremental, + apr_pool_t *pool) +{ + struct replay_baton *replay_baton; + const char *uuid; + svn_stream_t *stdout_stream; + + SVN_ERR(svn_stream_for_stdout(&stdout_stream, pool)); + + replay_baton = apr_pcalloc(pool, sizeof(*replay_baton)); + replay_baton->stdout_stream = stdout_stream; + replay_baton->extra_ra_session = extra_ra_session; + replay_baton->quiet = quiet; + + /* Write the magic header and UUID */ + SVN_ERR(svn_stream_printf(stdout_stream, pool, + SVN_REPOS_DUMPFILE_MAGIC_HEADER ": %d\n\n", + SVN_REPOS_DUMPFILE_FORMAT_VERSION)); + SVN_ERR(svn_ra_get_uuid2(session, &uuid, pool)); + SVN_ERR(svn_stream_printf(stdout_stream, pool, + SVN_REPOS_DUMPFILE_UUID ": %s\n\n", uuid)); + + /* Fake revision 0 if necessary */ + if (start_revision == 0) + { + SVN_ERR(dump_revision_header(session, stdout_stream, + start_revision, pool)); + + /* Revision 0 has no tree changes, so we're done. */ + if (! quiet) + SVN_ERR(svn_cmdline_fprintf(stderr, pool, "* Dumped revision %lu.\n", + start_revision)); + start_revision++; + + /* If our first revision is 0, we can treat this as an + incremental dump. */ + incremental = TRUE; + } + + /* If what remains to be dumped is not going to be dumped + incrementally, then dump the first revision in full. */ + if (!incremental) + { + SVN_ERR(dump_initial_full_revision(session, extra_ra_session, + stdout_stream, start_revision, + quiet, pool)); + start_revision++; + } + + /* If there are still revisions left to be dumped, do so. */ + if (start_revision <= end_revision) + { +#ifndef USE_EV2_IMPL + SVN_ERR(svn_ra_replay_range(session, start_revision, end_revision, + 0, TRUE, replay_revstart, replay_revend, + replay_baton, pool)); +#else + SVN_ERR(svn_ra__replay_range_ev2(session, start_revision, end_revision, + 0, TRUE, replay_revstart_v2, + replay_revend_v2, replay_baton, + NULL, NULL, NULL, NULL, pool)); +#endif + } + + SVN_ERR(svn_stream_close(stdout_stream)); + return SVN_NO_ERROR; +} + +/* Read a dumpstream from stdin, and use it to feed a loader capable + * of transmitting that information to the repository located at URL + * (to which SESSION has been opened). AUX_SESSION is a second RA + * session opened to the same URL for performing auxiliary out-of-band + * operations. + */ +static svn_error_t * +load_revisions(svn_ra_session_t *session, + svn_ra_session_t *aux_session, + const char *url, + svn_boolean_t quiet, + apr_pool_t *pool) +{ + apr_file_t *stdin_file; + svn_stream_t *stdin_stream; + + apr_file_open_stdin(&stdin_file, pool); + stdin_stream = svn_stream_from_aprfile2(stdin_file, FALSE, pool); + + SVN_ERR(svn_rdump__load_dumpstream(stdin_stream, session, aux_session, + quiet, check_cancel, NULL, pool)); + + SVN_ERR(svn_stream_close(stdin_stream)); + + return SVN_NO_ERROR; +} + +/* Return a program name for this program, the basename of the path + * represented by PROGNAME if not NULL; use "svnrdump" otherwise. + */ +static const char * +ensure_appname(const char *progname, + apr_pool_t *pool) +{ + if (!progname) + return "svnrdump"; + + return svn_dirent_basename(svn_dirent_internal_style(progname, pool), NULL); +} + +/* Print a simple usage string. */ +static svn_error_t * +usage(const char *progname, + apr_pool_t *pool) +{ + return svn_cmdline_fprintf(stderr, pool, + _("Type '%s help' for usage.\n"), + ensure_appname(progname, pool)); +} + +/* Print information about the version of this program and dependent + * modules. + */ +static svn_error_t * +version(const char *progname, + svn_boolean_t quiet, + apr_pool_t *pool) +{ + svn_stringbuf_t *version_footer = + svn_stringbuf_create(_("The following repository access (RA) modules " + "are available:\n\n"), + pool); + + SVN_ERR(svn_ra_print_modules(version_footer, pool)); + return svn_opt_print_help4(NULL, ensure_appname(progname, pool), + TRUE, quiet, FALSE, version_footer->data, + NULL, NULL, NULL, NULL, NULL, pool); +} + + +/* A statement macro, similar to @c SVN_ERR, but returns an integer. + * Evaluate @a expr. If it yields an error, handle that error and + * return @c EXIT_FAILURE. + */ +#define SVNRDUMP_ERR(expr) \ + do \ + { \ + svn_error_t *svn_err__temp = (expr); \ + if (svn_err__temp) \ + { \ + svn_handle_error2(svn_err__temp, stderr, FALSE, "svnrdump: "); \ + svn_error_clear(svn_err__temp); \ + return EXIT_FAILURE; \ + } \ + } \ + while (0) + +/* Handle the "dump" subcommand. Implements `svn_opt_subcommand_t'. */ +static svn_error_t * +dump_cmd(apr_getopt_t *os, + void *baton, + apr_pool_t *pool) +{ + opt_baton_t *opt_baton = baton; + svn_ra_session_t *extra_ra_session; + const char *repos_root; + + SVN_ERR(svn_client_open_ra_session2(&extra_ra_session, + opt_baton->url, NULL, + opt_baton->ctx, pool, pool)); + SVN_ERR(svn_ra_get_repos_root2(extra_ra_session, &repos_root, pool)); + SVN_ERR(svn_ra_reparent(extra_ra_session, repos_root, pool)); + + return replay_revisions(opt_baton->session, extra_ra_session, + opt_baton->start_revision.value.number, + opt_baton->end_revision.value.number, + opt_baton->quiet, opt_baton->incremental, pool); +} + +/* Handle the "load" subcommand. Implements `svn_opt_subcommand_t'. */ +static svn_error_t * +load_cmd(apr_getopt_t *os, + void *baton, + apr_pool_t *pool) +{ + opt_baton_t *opt_baton = baton; + svn_ra_session_t *aux_session; + + SVN_ERR(svn_client_open_ra_session2(&aux_session, opt_baton->url, NULL, + opt_baton->ctx, pool, pool)); + return load_revisions(opt_baton->session, aux_session, opt_baton->url, + opt_baton->quiet, pool); +} + +/* Handle the "help" subcommand. Implements `svn_opt_subcommand_t'. */ +static svn_error_t * +help_cmd(apr_getopt_t *os, + void *baton, + apr_pool_t *pool) +{ + const char *header = + _("general usage: svnrdump SUBCOMMAND URL [-r LOWER[:UPPER]]\n" + "Type 'svnrdump help <subcommand>' for help on a specific subcommand.\n" + "Type 'svnrdump --version' to see the program version and RA modules.\n" + "\n" + "Available subcommands:\n"); + + return svn_opt_print_help4(os, "svnrdump", FALSE, FALSE, FALSE, NULL, + header, svnrdump__cmd_table, svnrdump__options, + NULL, NULL, pool); +} + +/* Examine the OPT_BATON's 'start_revision' and 'end_revision' + * members, making sure that they make sense (in general, and as + * applied to a repository whose current youngest revision is + * LATEST_REVISION). + */ +static svn_error_t * +validate_and_resolve_revisions(opt_baton_t *opt_baton, + svn_revnum_t latest_revision, + apr_pool_t *pool) +{ + svn_revnum_t provided_start_rev = SVN_INVALID_REVNUM; + + /* Ensure that the start revision is something we can handle. We + want a number >= 0. If unspecified, make it a number (r0) -- + anything else is bogus. */ + if (opt_baton->start_revision.kind == svn_opt_revision_number) + { + provided_start_rev = opt_baton->start_revision.value.number; + } + else if (opt_baton->start_revision.kind == svn_opt_revision_head) + { + opt_baton->start_revision.kind = svn_opt_revision_number; + opt_baton->start_revision.value.number = latest_revision; + } + else if (opt_baton->start_revision.kind == svn_opt_revision_unspecified) + { + opt_baton->start_revision.kind = svn_opt_revision_number; + opt_baton->start_revision.value.number = 0; + } + + if (opt_baton->start_revision.kind != svn_opt_revision_number) + { + return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Unsupported revision specifier used; use " + "only integer values or 'HEAD'")); + } + + if ((opt_baton->start_revision.value.number < 0) || + (opt_baton->start_revision.value.number > latest_revision)) + { + return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Revision '%ld' does not exist"), + opt_baton->start_revision.value.number); + } + + /* Ensure that the end revision is something we can handle. We want + a number <= the youngest, and > the start revision. If + unspecified, make it a number (start_revision + 1 if that was + specified, the youngest revision in the repository otherwise) -- + anything else is bogus. */ + if (opt_baton->end_revision.kind == svn_opt_revision_unspecified) + { + opt_baton->end_revision.kind = svn_opt_revision_number; + if (SVN_IS_VALID_REVNUM(provided_start_rev)) + opt_baton->end_revision.value.number = provided_start_rev; + else + opt_baton->end_revision.value.number = latest_revision; + } + else if (opt_baton->end_revision.kind == svn_opt_revision_head) + { + opt_baton->end_revision.kind = svn_opt_revision_number; + opt_baton->end_revision.value.number = latest_revision; + } + + if (opt_baton->end_revision.kind != svn_opt_revision_number) + { + return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Unsupported revision specifier used; use " + "only integer values or 'HEAD'")); + } + + if ((opt_baton->end_revision.value.number < 0) || + (opt_baton->end_revision.value.number > latest_revision)) + { + return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Revision '%ld' does not exist"), + opt_baton->end_revision.value.number); + } + + /* Finally, make sure that the end revision is younger than the + start revision. We don't do "backwards" 'round here. */ + if (opt_baton->end_revision.value.number < + opt_baton->start_revision.value.number) + { + return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("LOWER revision cannot be greater than " + "UPPER revision; consider reversing your " + "revision range")); + } + return SVN_NO_ERROR; +} + +int +main(int argc, const char **argv) +{ + svn_error_t *err = SVN_NO_ERROR; + const svn_opt_subcommand_desc2_t *subcommand = NULL; + opt_baton_t *opt_baton; + svn_revnum_t latest_revision = SVN_INVALID_REVNUM; + apr_pool_t *pool = NULL; + const char *config_dir = NULL; + const char *username = NULL; + const char *password = NULL; + svn_boolean_t no_auth_cache = FALSE; + svn_boolean_t trust_server_cert = FALSE; + svn_boolean_t non_interactive = FALSE; + svn_boolean_t force_interactive = FALSE; + apr_array_header_t *config_options = NULL; + apr_getopt_t *os; + const char *first_arg; + apr_array_header_t *received_opts; + int i; + + if (svn_cmdline_init ("svnrdump", stderr) != EXIT_SUCCESS) + return EXIT_FAILURE; + + /* Create our top-level pool. Use a separate mutexless allocator, + * given this application is single threaded. + */ + pool = apr_allocator_owner_get(svn_pool_create_allocator(FALSE)); + + opt_baton = apr_pcalloc(pool, sizeof(*opt_baton)); + opt_baton->start_revision.kind = svn_opt_revision_unspecified; + opt_baton->end_revision.kind = svn_opt_revision_unspecified; + opt_baton->url = NULL; + + SVNRDUMP_ERR(svn_cmdline__getopt_init(&os, argc, argv, pool)); + + os->interleave = TRUE; /* Options and arguments can be interleaved */ + + /* Set up our cancellation support. */ + apr_signal(SIGINT, signal_handler); +#ifdef SIGBREAK + /* SIGBREAK is a Win32 specific signal generated by ctrl-break. */ + apr_signal(SIGBREAK, signal_handler); +#endif +#ifdef SIGHUP + apr_signal(SIGHUP, signal_handler); +#endif +#ifdef SIGTERM + apr_signal(SIGTERM, signal_handler); +#endif +#ifdef SIGPIPE + /* Disable SIGPIPE generation for the platforms that have it. */ + apr_signal(SIGPIPE, SIG_IGN); +#endif +#ifdef SIGXFSZ + /* Disable SIGXFSZ generation for the platforms that have it, otherwise + * working with large files when compiled against an APR that doesn't have + * large file support will crash the program, which is uncool. */ + apr_signal(SIGXFSZ, SIG_IGN); +#endif + + received_opts = apr_array_make(pool, SVN_OPT_MAX_OPTIONS, sizeof(int)); + + while (1) + { + int opt; + const char *opt_arg; + apr_status_t status = apr_getopt_long(os, svnrdump__options, &opt, + &opt_arg); + + if (APR_STATUS_IS_EOF(status)) + break; + if (status != APR_SUCCESS) + { + SVNRDUMP_ERR(usage(argv[0], pool)); + exit(EXIT_FAILURE); + } + + /* Stash the option code in an array before parsing it. */ + APR_ARRAY_PUSH(received_opts, int) = opt; + + switch(opt) + { + case 'r': + { + /* Make sure we've not seen -r already. */ + if (opt_baton->start_revision.kind != svn_opt_revision_unspecified) + { + err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Multiple revision arguments " + "encountered; try '-r N:M' instead " + "of '-r N -r M'")); + return svn_cmdline_handle_exit_error(err, pool, "svnrdump: "); + } + /* Parse the -r argument. */ + if (svn_opt_parse_revision(&(opt_baton->start_revision), + &(opt_baton->end_revision), + opt_arg, pool) != 0) + { + const char *utf8_opt_arg; + err = svn_utf_cstring_to_utf8(&utf8_opt_arg, opt_arg, pool); + if (! err) + err = svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("Syntax error in revision " + "argument '%s'"), utf8_opt_arg); + return svn_cmdline_handle_exit_error(err, pool, "svnrdump: "); + } + } + break; + case 'q': + opt_baton->quiet = TRUE; + break; + case opt_config_dir: + config_dir = opt_arg; + break; + case opt_version: + opt_baton->version = TRUE; + break; + case 'h': + opt_baton->help = TRUE; + break; + case opt_auth_username: + SVNRDUMP_ERR(svn_utf_cstring_to_utf8(&username, opt_arg, pool)); + break; + case opt_auth_password: + SVNRDUMP_ERR(svn_utf_cstring_to_utf8(&password, opt_arg, pool)); + break; + case opt_auth_nocache: + no_auth_cache = TRUE; + break; + case opt_non_interactive: + non_interactive = TRUE; + break; + case opt_force_interactive: + force_interactive = TRUE; + break; + case opt_incremental: + opt_baton->incremental = TRUE; + break; + case opt_trust_server_cert: + trust_server_cert = TRUE; + break; + case opt_config_option: + if (!config_options) + config_options = + apr_array_make(pool, 1, + sizeof(svn_cmdline__config_argument_t*)); + + SVNRDUMP_ERR(svn_utf_cstring_to_utf8(&opt_arg, opt_arg, pool)); + SVNRDUMP_ERR(svn_cmdline__parse_config_option(config_options, + opt_arg, pool)); + } + } + + /* The --non-interactive and --force-interactive options are mutually + * exclusive. */ + if (non_interactive && force_interactive) + { + err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("--non-interactive and --force-interactive " + "are mutually exclusive")); + return svn_cmdline_handle_exit_error(err, pool, "svnrdump: "); + } + + if (opt_baton->help) + { + subcommand = svn_opt_get_canonical_subcommand2(svnrdump__cmd_table, + "help"); + } + if (subcommand == NULL) + { + if (os->ind >= os->argc) + { + if (opt_baton->version) + { + /* Use the "help" subcommand to handle the "--version" option. */ + static const svn_opt_subcommand_desc2_t pseudo_cmd = + { "--version", help_cmd, {0}, "", + {opt_version, /* must accept its own option */ + 'q', /* --quiet */ + } }; + subcommand = &pseudo_cmd; + } + + else + { + SVNRDUMP_ERR(help_cmd(NULL, NULL, pool)); + svn_pool_destroy(pool); + exit(EXIT_FAILURE); + } + } + else + { + first_arg = os->argv[os->ind++]; + subcommand = svn_opt_get_canonical_subcommand2(svnrdump__cmd_table, + first_arg); + + if (subcommand == NULL) + { + const char *first_arg_utf8; + err = svn_utf_cstring_to_utf8(&first_arg_utf8, first_arg, pool); + if (err) + return svn_cmdline_handle_exit_error(err, pool, "svnrdump: "); + svn_error_clear( + svn_cmdline_fprintf(stderr, pool, + _("Unknown subcommand: '%s'\n"), + first_arg_utf8)); + SVNRDUMP_ERR(help_cmd(NULL, NULL, pool)); + svn_pool_destroy(pool); + exit(EXIT_FAILURE); + } + } + } + + /* Check that the subcommand wasn't passed any inappropriate options. */ + for (i = 0; i < received_opts->nelts; i++) + { + int opt_id = APR_ARRAY_IDX(received_opts, i, int); + + /* All commands implicitly accept --help, so just skip over this + when we see it. Note that we don't want to include this option + in their "accepted options" list because it would be awfully + redundant to display it in every commands' help text. */ + if (opt_id == 'h' || opt_id == '?') + continue; + + if (! svn_opt_subcommand_takes_option3(subcommand, opt_id, NULL)) + { + const char *optstr; + const apr_getopt_option_t *badopt = + svn_opt_get_option_from_code2(opt_id, svnrdump__options, + subcommand, pool); + svn_opt_format_option(&optstr, badopt, FALSE, pool); + if (subcommand->name[0] == '-') + SVN_INT_ERR(help_cmd(NULL, NULL, pool)); + else + svn_error_clear(svn_cmdline_fprintf( + stderr, pool, + _("Subcommand '%s' doesn't accept option '%s'\n" + "Type 'svnrdump help %s' for usage.\n"), + subcommand->name, optstr, subcommand->name)); + svn_pool_destroy(pool); + return EXIT_FAILURE; + } + } + + if (subcommand && strcmp(subcommand->name, "--version") == 0) + { + SVNRDUMP_ERR(version(argv[0], opt_baton->quiet, pool)); + svn_pool_destroy(pool); + exit(EXIT_SUCCESS); + } + + if (subcommand && strcmp(subcommand->name, "help") == 0) + { + SVNRDUMP_ERR(help_cmd(os, opt_baton, pool)); + svn_pool_destroy(pool); + exit(EXIT_SUCCESS); + } + + /* --trust-server-cert can only be used with --non-interactive */ + if (trust_server_cert && !non_interactive) + { + err = svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL, + _("--trust-server-cert requires " + "--non-interactive")); + return svn_cmdline_handle_exit_error(err, pool, "svnrdump: "); + } + + /* Expect one more non-option argument: the repository URL. */ + if (os->ind != os->argc - 1) + { + SVNRDUMP_ERR(usage(argv[0], pool)); + svn_pool_destroy(pool); + exit(EXIT_FAILURE); + } + else + { + const char *repos_url; + + SVNRDUMP_ERR(svn_utf_cstring_to_utf8(&repos_url, + os->argv[os->ind], pool)); + if (! svn_path_is_url(repos_url)) + { + err = svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, 0, + "Target '%s' is not a URL", + repos_url); + SVNRDUMP_ERR(err); + svn_pool_destroy(pool); + exit(EXIT_FAILURE); + } + opt_baton->url = svn_uri_canonicalize(repos_url, pool); + } + + if (strcmp(subcommand->name, "load") == 0) + { + /* + * By default (no --*-interactive options given), the 'load' subcommand + * is interactive unless username and password were provided on the + * command line. This allows prompting for auth creds to work without + * requiring users to remember to use --force-interactive. + * See issue #3913, "svnrdump load is not working in interactive mode". + */ + if (!non_interactive && !force_interactive) + force_interactive = (username == NULL || password == NULL); + } + + non_interactive = !svn_cmdline__be_interactive(non_interactive, + force_interactive); + + SVNRDUMP_ERR(init_client_context(&(opt_baton->ctx), + non_interactive, + username, + password, + config_dir, + opt_baton->url, + no_auth_cache, + trust_server_cert, + config_options, + pool)); + + err = svn_client_open_ra_session2(&(opt_baton->session), + opt_baton->url, NULL, + opt_baton->ctx, pool, pool); + + /* Have sane opt_baton->start_revision and end_revision defaults if + unspecified. */ + if (!err) + err = svn_ra_get_latest_revnum(opt_baton->session, &latest_revision, pool); + + /* Make sure any provided revisions make sense. */ + if (!err) + err = validate_and_resolve_revisions(opt_baton, latest_revision, pool); + + /* Dispatch the subcommand */ + if (!err) + err = (*subcommand->cmd_func)(os, opt_baton, pool); + + if (err && err->apr_err == SVN_ERR_AUTHN_FAILED && non_interactive) + { + err = svn_error_quick_wrap(err, + _("Authentication failed and interactive" + " prompting is disabled; see the" + " --force-interactive option")); + } + + SVNRDUMP_ERR(err); + + svn_pool_destroy(pool); + + return EXIT_SUCCESS; +} diff --git a/subversion/svnrdump/svnrdump.h b/subversion/svnrdump/svnrdump.h new file mode 100644 index 0000000000000..2a81014c8fcd1 --- /dev/null +++ b/subversion/svnrdump/svnrdump.h @@ -0,0 +1,129 @@ +/* + * svnrdump.h: Internal header file for svnrdump. + * + * ==================================================================== + * 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. + * ==================================================================== + */ + + +#ifndef SVNRDUMP_H +#define SVNRDUMP_H + +/*** Includes. ***/ +#include "svn_error.h" +#include "svn_pools.h" +#include "svn_hash.h" +#include "svn_delta.h" +#include "svn_ra.h" + +#include "private/svn_editor.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + + +/** + * Get a dump editor @a editor along with a @a edit_baton allocated in + * @a pool. The editor will write output to @a stream. + * + * @a update_anchor_relpath is the repository relative path of the + * anchor of the update-style drive which will happen on @a *editor; + * if a replay-style drive will instead be used, it should be passed + * as @c NULL. + * + * Use @a cancel_func and @a cancel_baton to check for user + * cancellation of the operation (for timely-but-safe termination). + */ +svn_error_t * +svn_rdump__get_dump_editor(const svn_delta_editor_t **editor, + void **edit_baton, + svn_revnum_t revision, + svn_stream_t *stream, + svn_ra_session_t *ra_session, + const char *update_anchor_relpath, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool); + +/* Same as above, only returns an Ev2 editor. */ +svn_error_t * +svn_rdump__get_dump_editor_v2(svn_editor_t **editor, + svn_revnum_t revision, + svn_stream_t *stream, + svn_ra_session_t *ra_session, + const char *edit_root_relpath, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *scratch_pool, + apr_pool_t *result_pool); + + +/** + * Load the dumpstream carried in @a stream to the location described + * by @a session. Use @a aux_session (which is opened to the same URL + * as @a session) for any secondary, out-of-band RA communications + * required. If @a quiet is set, suppress notifications. Use @a pool + * for all memory allocations. Use @a cancel_func and @a cancel_baton + * to check for user cancellation of the operation (for + * timely-but-safe termination). + */ +svn_error_t * +svn_rdump__load_dumpstream(svn_stream_t *stream, + svn_ra_session_t *session, + svn_ra_session_t *aux_session, + svn_boolean_t quiet, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool); + + +/* Normalize the line ending style of the values of properties in PROPS + * that "need translation" (according to svn_prop_needs_translation(), + * currently all svn:* props) so that they contain only LF (\n) line endings. + * + * Put the normalized props into NORMAL_PROPS, allocated in RESULT_POOL. + * + * Note: this function does not do a deep copy; it is expected that PROPS has + * a longer lifetime than NORMAL_PROPS. + */ +svn_error_t * +svn_rdump__normalize_props(apr_hash_t **normal_props, + apr_hash_t *props, + apr_pool_t *result_pool); + +/* Normalize the line ending style of a single property that "needs + * translation" (according to svn_prop_needs_translation(), + * currently all svn:* props) so that they contain only LF (\n) line endings. + * "\r" characters found mid-line are replaced with "\n". + * "\r\n" sequences are replaced with "\n" + * + * NAME is used to check that VALUE should be normalized, and if this is the + * case, VALUE is then normalized, allocated from RESULT_POOL + */ +svn_error_t * +svn_rdump__normalize_prop(const char *name, + const svn_string_t **value, + apr_pool_t *result_pool); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* SVNRDUMP_H */ diff --git a/subversion/svnrdump/util.c b/subversion/svnrdump/util.c new file mode 100644 index 0000000000000..91cefb3f0800e --- /dev/null +++ b/subversion/svnrdump/util.c @@ -0,0 +1,73 @@ +/* + * util.c: A few utility functions. + * + * ==================================================================== + * 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 "svn_error.h" +#include "svn_pools.h" +#include "svn_string.h" +#include "svn_props.h" +#include "svn_subst.h" + +#include "svnrdump.h" + + +svn_error_t * +svn_rdump__normalize_prop(const char *name, + const svn_string_t **value, + apr_pool_t *result_pool) +{ + if (svn_prop_needs_translation(name)) + { + const char *cstring; + + SVN_ERR(svn_subst_translate_cstring2((*value)->data, &cstring, + "\n", TRUE, + NULL, FALSE, + result_pool)); + + *value = svn_string_create(cstring, result_pool); + } + return SVN_NO_ERROR; +} + +svn_error_t * +svn_rdump__normalize_props(apr_hash_t **normal_props, + apr_hash_t *props, + apr_pool_t *result_pool) +{ + apr_hash_index_t *hi; + + *normal_props = apr_hash_make(result_pool); + + for (hi = apr_hash_first(result_pool, props); hi; + hi = apr_hash_next(hi)) + { + const char *key = svn__apr_hash_index_key(hi); + const svn_string_t *value = svn__apr_hash_index_val(hi); + + SVN_ERR(svn_rdump__normalize_prop(key, &value, + result_pool)); + + svn_hash_sets(*normal_props, key, value); + } + return SVN_NO_ERROR; +} |