summaryrefslogtreecommitdiff
path: root/subversion/svnrdump
diff options
context:
space:
mode:
authorPeter Wemm <peter@FreeBSD.org>2013-06-18 02:07:41 +0000
committerPeter Wemm <peter@FreeBSD.org>2013-06-18 02:07:41 +0000
commit32547653cc5376642e1231fb644db99933ac8db4 (patch)
tree135691142dc0e75a5e5d97b5074d03436435b8e0 /subversion/svnrdump
Notes
Diffstat (limited to 'subversion/svnrdump')
-rw-r--r--subversion/svnrdump/dump_editor.c1280
-rw-r--r--subversion/svnrdump/load_editor.c1211
-rw-r--r--subversion/svnrdump/svnrdump.147
-rw-r--r--subversion/svnrdump/svnrdump.c1185
-rw-r--r--subversion/svnrdump/svnrdump.h129
-rw-r--r--subversion/svnrdump/util.c73
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;
+}