summaryrefslogtreecommitdiff
path: root/subversion/svndumpfilter/svndumpfilter.c
diff options
context:
space:
mode:
Diffstat (limited to 'subversion/svndumpfilter/svndumpfilter.c')
-rw-r--r--subversion/svndumpfilter/svndumpfilter.c1687
1 files changed, 1687 insertions, 0 deletions
diff --git a/subversion/svndumpfilter/svndumpfilter.c b/subversion/svndumpfilter/svndumpfilter.c
new file mode 100644
index 0000000..0e5bbc6
--- /dev/null
+++ b/subversion/svndumpfilter/svndumpfilter.c
@@ -0,0 +1,1687 @@
+/*
+ * svndumpfilter.c: Subversion dump stream filtering tool main file.
+ *
+ * ====================================================================
+ * 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 <stdlib.h>
+
+#include <apr_file_io.h>
+
+#include "svn_private_config.h"
+#include "svn_cmdline.h"
+#include "svn_error.h"
+#include "svn_string.h"
+#include "svn_opt.h"
+#include "svn_utf.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_hash.h"
+#include "svn_repos.h"
+#include "svn_fs.h"
+#include "svn_pools.h"
+#include "svn_sorts.h"
+#include "svn_props.h"
+#include "svn_mergeinfo.h"
+#include "svn_version.h"
+
+#include "private/svn_mergeinfo_private.h"
+#include "private/svn_cmdline_private.h"
+#include "private/svn_subr_private.h"
+
+#ifdef _WIN32
+typedef apr_status_t (__stdcall *open_fn_t)(apr_file_t **, apr_pool_t *);
+#else
+typedef apr_status_t (*open_fn_t)(apr_file_t **, apr_pool_t *);
+#endif
+
+/*** Code. ***/
+
+/* Helper to open stdio streams */
+
+/* NOTE: we used to call svn_stream_from_stdio(), which wraps a stream
+ around a standard stdio.h FILE pointer. The problem is that these
+ pointers operate through C Run Time (CRT) on Win32, which does all
+ sorts of translation on them: LF's become CRLF's, and ctrl-Z's
+ embedded in Word documents are interpreted as premature EOF's.
+
+ So instead, we use apr_file_open_std*, which bypass the CRT and
+ directly wrap the OS's file-handles, which don't know or care about
+ translation. Thus dump/load works correctly on Win32.
+*/
+static svn_error_t *
+create_stdio_stream(svn_stream_t **stream,
+ open_fn_t open_fn,
+ apr_pool_t *pool)
+{
+ apr_file_t *stdio_file;
+ apr_status_t apr_err = open_fn(&stdio_file, pool);
+
+ if (apr_err)
+ return svn_error_wrap_apr(apr_err, _("Can't open stdio file"));
+
+ *stream = svn_stream_from_aprfile2(stdio_file, TRUE, pool);
+ return SVN_NO_ERROR;
+}
+
+
+/* Writes a property in dumpfile format to given stringbuf. */
+static void
+write_prop_to_stringbuf(svn_stringbuf_t *strbuf,
+ const char *name,
+ const svn_string_t *value)
+{
+ int bytes_used;
+ size_t namelen;
+ char buf[SVN_KEYLINE_MAXLEN];
+
+ /* Output name length, then name. */
+ namelen = strlen(name);
+ svn_stringbuf_appendbytes(strbuf, "K ", 2);
+
+ bytes_used = apr_snprintf(buf, sizeof(buf), "%" APR_SIZE_T_FMT, namelen);
+ svn_stringbuf_appendbytes(strbuf, buf, bytes_used);
+ svn_stringbuf_appendbyte(strbuf, '\n');
+
+ svn_stringbuf_appendbytes(strbuf, name, namelen);
+ svn_stringbuf_appendbyte(strbuf, '\n');
+
+ /* Output value length, then value. */
+ svn_stringbuf_appendbytes(strbuf, "V ", 2);
+
+ bytes_used = apr_snprintf(buf, sizeof(buf), "%" APR_SIZE_T_FMT, value->len);
+ svn_stringbuf_appendbytes(strbuf, buf, bytes_used);
+ svn_stringbuf_appendbyte(strbuf, '\n');
+
+ svn_stringbuf_appendbytes(strbuf, value->data, value->len);
+ svn_stringbuf_appendbyte(strbuf, '\n');
+}
+
+
+/* Writes a property deletion in dumpfile format to given stringbuf. */
+static void
+write_propdel_to_stringbuf(svn_stringbuf_t **strbuf,
+ const char *name)
+{
+ int bytes_used;
+ size_t namelen;
+ char buf[SVN_KEYLINE_MAXLEN];
+
+ /* Output name length, then name. */
+ namelen = strlen(name);
+ svn_stringbuf_appendbytes(*strbuf, "D ", 2);
+
+ bytes_used = apr_snprintf(buf, sizeof(buf), "%" APR_SIZE_T_FMT, namelen);
+ svn_stringbuf_appendbytes(*strbuf, buf, bytes_used);
+ svn_stringbuf_appendbyte(*strbuf, '\n');
+
+ svn_stringbuf_appendbytes(*strbuf, name, namelen);
+ svn_stringbuf_appendbyte(*strbuf, '\n');
+}
+
+
+/* Compare the node-path PATH with the (const char *) prefixes in PFXLIST.
+ * Return TRUE if any prefix is a prefix of PATH (matching whole path
+ * components); FALSE otherwise.
+ * PATH starts with a '/', as do the (const char *) paths in PREFIXES. */
+static svn_boolean_t
+ary_prefix_match(const apr_array_header_t *pfxlist, const char *path)
+{
+ int i;
+ size_t path_len = strlen(path);
+
+ for (i = 0; i < pfxlist->nelts; i++)
+ {
+ const char *pfx = APR_ARRAY_IDX(pfxlist, i, const char *);
+ size_t pfx_len = strlen(pfx);
+
+ if (path_len < pfx_len)
+ continue;
+ if (strncmp(path, pfx, pfx_len) == 0
+ && (pfx_len == 1 || path[pfx_len] == '\0' || path[pfx_len] == '/'))
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+
+/* Check whether we need to skip this PATH based on its presence in
+ the PREFIXES list, and the DO_EXCLUDE option.
+ PATH starts with a '/', as do the (const char *) paths in PREFIXES. */
+static APR_INLINE svn_boolean_t
+skip_path(const char *path, const apr_array_header_t *prefixes,
+ svn_boolean_t do_exclude, svn_boolean_t glob)
+{
+ const svn_boolean_t matches =
+ (glob
+ ? svn_cstring_match_glob_list(path, prefixes)
+ : ary_prefix_match(prefixes, path));
+
+ /* NXOR */
+ return (matches ? do_exclude : !do_exclude);
+}
+
+
+
+/* Note: the input stream parser calls us with events.
+ Output of the filtered dump occurs for the most part streamily with the
+ event callbacks, to avoid caching large quantities of data in memory.
+ The exceptions this are:
+ - All revision data (headers and props) must be cached until a non-skipped
+ node within the revision is found, or the revision is closed.
+ - Node headers and props must be cached until all props have been received
+ (to allow the Prop-content-length to be found). This is signalled either
+ by the node text arriving, or the node being closed.
+ The writing_begun members of the associated object batons track the state.
+ output_revision() and output_node() are called to cause this flushing of
+ cached data to occur.
+*/
+
+
+/* Filtering batons */
+
+struct revmap_t
+{
+ svn_revnum_t rev; /* Last non-dropped revision to which this maps. */
+ svn_boolean_t was_dropped; /* Was this revision dropped? */
+};
+
+struct parse_baton_t
+{
+ /* Command-line options values. */
+ svn_boolean_t do_exclude;
+ svn_boolean_t quiet;
+ svn_boolean_t glob;
+ svn_boolean_t drop_empty_revs;
+ svn_boolean_t drop_all_empty_revs;
+ svn_boolean_t do_renumber_revs;
+ svn_boolean_t preserve_revprops;
+ svn_boolean_t skip_missing_merge_sources;
+ svn_boolean_t allow_deltas;
+ apr_array_header_t *prefixes;
+
+ /* Input and output streams. */
+ svn_stream_t *in_stream;
+ svn_stream_t *out_stream;
+
+ /* State for the filtering process. */
+ apr_int32_t rev_drop_count;
+ apr_hash_t *dropped_nodes;
+ apr_hash_t *renumber_history; /* svn_revnum_t -> struct revmap_t */
+ svn_revnum_t last_live_revision;
+ /* The oldest original revision, greater than r0, in the input
+ stream which was not filtered. */
+ svn_revnum_t oldest_original_rev;
+};
+
+struct revision_baton_t
+{
+ /* Reference to the global parse baton. */
+ struct parse_baton_t *pb;
+
+ /* Does this revision have node or prop changes? */
+ svn_boolean_t has_nodes;
+ svn_boolean_t has_props;
+
+ /* Did we drop any nodes? */
+ svn_boolean_t had_dropped_nodes;
+
+ /* Written to output stream? */
+ svn_boolean_t writing_begun;
+
+ /* The original and new (re-mapped) revision numbers. */
+ svn_revnum_t rev_orig;
+ svn_revnum_t rev_actual;
+
+ /* Pointers to dumpfile data. */
+ svn_stringbuf_t *header;
+ apr_hash_t *props;
+};
+
+struct node_baton_t
+{
+ /* Reference to the current revision baton. */
+ struct revision_baton_t *rb;
+
+ /* Are we skipping this node? */
+ svn_boolean_t do_skip;
+
+ /* Have we been instructed to change or remove props on, or change
+ the text of, this node? */
+ svn_boolean_t has_props;
+ svn_boolean_t has_text;
+
+ /* Written to output stream? */
+ svn_boolean_t writing_begun;
+
+ /* The text content length according to the dumpfile headers, because we
+ need the length before we have the actual text. */
+ svn_filesize_t tcl;
+
+ /* Pointers to dumpfile data. */
+ svn_stringbuf_t *header;
+ svn_stringbuf_t *props;
+
+ /* Expect deltas? */
+ svn_boolean_t has_prop_delta;
+ svn_boolean_t has_text_delta;
+
+ /* We might need the node path in a parse error message. */
+ char *node_path;
+};
+
+
+
+/* Filtering vtable members */
+
+/* File-format stamp. */
+static svn_error_t *
+magic_header_record(int version, void *parse_baton, apr_pool_t *pool)
+{
+ struct parse_baton_t *pb = parse_baton;
+
+ if (version >= SVN_REPOS_DUMPFILE_FORMAT_VERSION_DELTAS)
+ pb->allow_deltas = TRUE;
+
+ SVN_ERR(svn_stream_printf(pb->out_stream, pool,
+ SVN_REPOS_DUMPFILE_MAGIC_HEADER ": %d\n\n",
+ version));
+
+ return SVN_NO_ERROR;
+}
+
+
+/* New revision: set up revision_baton, decide if we skip it. */
+static svn_error_t *
+new_revision_record(void **revision_baton,
+ apr_hash_t *headers,
+ void *parse_baton,
+ apr_pool_t *pool)
+{
+ struct revision_baton_t *rb;
+ apr_hash_index_t *hi;
+ const char *rev_orig;
+ svn_stream_t *header_stream;
+
+ *revision_baton = apr_palloc(pool, sizeof(struct revision_baton_t));
+ rb = *revision_baton;
+ rb->pb = parse_baton;
+ rb->has_nodes = FALSE;
+ rb->has_props = FALSE;
+ rb->had_dropped_nodes = FALSE;
+ rb->writing_begun = FALSE;
+ rb->header = svn_stringbuf_create_empty(pool);
+ rb->props = apr_hash_make(pool);
+
+ header_stream = svn_stream_from_stringbuf(rb->header, pool);
+
+ rev_orig = svn_hash_gets(headers, SVN_REPOS_DUMPFILE_REVISION_NUMBER);
+ rb->rev_orig = SVN_STR_TO_REV(rev_orig);
+
+ if (rb->pb->do_renumber_revs)
+ rb->rev_actual = rb->rev_orig - rb->pb->rev_drop_count;
+ else
+ rb->rev_actual = rb->rev_orig;
+
+ SVN_ERR(svn_stream_printf(header_stream, pool,
+ SVN_REPOS_DUMPFILE_REVISION_NUMBER ": %ld\n",
+ rb->rev_actual));
+
+ for (hi = apr_hash_first(pool, headers); hi; hi = apr_hash_next(hi))
+ {
+ const char *key = svn__apr_hash_index_key(hi);
+ const char *val = svn__apr_hash_index_val(hi);
+
+ if ((!strcmp(key, SVN_REPOS_DUMPFILE_CONTENT_LENGTH))
+ || (!strcmp(key, SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH))
+ || (!strcmp(key, SVN_REPOS_DUMPFILE_REVISION_NUMBER)))
+ continue;
+
+ /* passthru: put header into header stringbuf. */
+
+ SVN_ERR(svn_stream_printf(header_stream, pool, "%s: %s\n",
+ key, val));
+ }
+
+ SVN_ERR(svn_stream_close(header_stream));
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Output revision to dumpstream
+ This may be called by new_node_record(), iff rb->has_nodes has been set
+ to TRUE, or by close_revision() otherwise. This must only be called
+ if rb->writing_begun is FALSE. */
+static svn_error_t *
+output_revision(struct revision_baton_t *rb)
+{
+ int bytes_used;
+ char buf[SVN_KEYLINE_MAXLEN];
+ apr_hash_index_t *hi;
+ svn_boolean_t write_out_rev = FALSE;
+ apr_pool_t *hash_pool = apr_hash_pool_get(rb->props);
+ svn_stringbuf_t *props = svn_stringbuf_create_empty(hash_pool);
+ apr_pool_t *subpool = svn_pool_create(hash_pool);
+
+ rb->writing_begun = TRUE;
+
+ /* If this revision has no nodes left because the ones it had were
+ dropped, and we are not dropping empty revisions, and we were not
+ told to preserve revision props, then we want to fixup the
+ revision props to only contain:
+ - the date
+ - a log message that reports that this revision is just stuffing. */
+ if ((! rb->pb->preserve_revprops)
+ && (! rb->has_nodes)
+ && rb->had_dropped_nodes
+ && (! rb->pb->drop_empty_revs)
+ && (! rb->pb->drop_all_empty_revs))
+ {
+ apr_hash_t *old_props = rb->props;
+ rb->has_props = TRUE;
+ rb->props = apr_hash_make(hash_pool);
+ svn_hash_sets(rb->props, SVN_PROP_REVISION_DATE,
+ svn_hash_gets(old_props, SVN_PROP_REVISION_DATE));
+ svn_hash_sets(rb->props, SVN_PROP_REVISION_LOG,
+ svn_string_create(_("This is an empty revision for "
+ "padding."), hash_pool));
+ }
+
+ /* Now, "rasterize" the props to a string, and append the property
+ information to the header string. */
+ if (rb->has_props)
+ {
+ for (hi = apr_hash_first(subpool, rb->props);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *pname = svn__apr_hash_index_key(hi);
+ const svn_string_t *pval = svn__apr_hash_index_val(hi);
+
+ write_prop_to_stringbuf(props, pname, pval);
+ }
+ svn_stringbuf_appendcstr(props, "PROPS-END\n");
+ svn_stringbuf_appendcstr(rb->header,
+ SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH);
+ bytes_used = apr_snprintf(buf, sizeof(buf), ": %" APR_SIZE_T_FMT,
+ props->len);
+ svn_stringbuf_appendbytes(rb->header, buf, bytes_used);
+ svn_stringbuf_appendbyte(rb->header, '\n');
+ }
+
+ svn_stringbuf_appendcstr(rb->header, SVN_REPOS_DUMPFILE_CONTENT_LENGTH);
+ bytes_used = apr_snprintf(buf, sizeof(buf), ": %" APR_SIZE_T_FMT, props->len);
+ svn_stringbuf_appendbytes(rb->header, buf, bytes_used);
+ svn_stringbuf_appendbyte(rb->header, '\n');
+
+ /* put an end to headers */
+ svn_stringbuf_appendbyte(rb->header, '\n');
+
+ /* put an end to revision */
+ svn_stringbuf_appendbyte(props, '\n');
+
+ /* write out the revision */
+ /* Revision is written out in the following cases:
+ 1. If the revision has nodes or
+ it is revision 0 (Special case: To preserve the props on r0).
+ 2. --drop-empty-revs has been supplied,
+ but revision has not all nodes dropped.
+ 3. If no --drop-empty-revs or --drop-all-empty-revs have been supplied,
+ write out the revision which has no nodes to begin with.
+ */
+ if (rb->has_nodes || (rb->rev_orig == 0))
+ write_out_rev = TRUE;
+ else if (rb->pb->drop_empty_revs)
+ write_out_rev = ! rb->had_dropped_nodes;
+ else if (! rb->pb->drop_all_empty_revs)
+ write_out_rev = TRUE;
+
+ if (write_out_rev)
+ {
+ /* This revision is a keeper. */
+ SVN_ERR(svn_stream_write(rb->pb->out_stream,
+ rb->header->data, &(rb->header->len)));
+ SVN_ERR(svn_stream_write(rb->pb->out_stream,
+ props->data, &(props->len)));
+
+ /* Stash the oldest original rev not dropped. */
+ if (rb->rev_orig > 0
+ && !SVN_IS_VALID_REVNUM(rb->pb->oldest_original_rev))
+ rb->pb->oldest_original_rev = rb->rev_orig;
+
+ if (rb->pb->do_renumber_revs)
+ {
+ svn_revnum_t *rr_key;
+ struct revmap_t *rr_val;
+ apr_pool_t *rr_pool = apr_hash_pool_get(rb->pb->renumber_history);
+ rr_key = apr_palloc(rr_pool, sizeof(*rr_key));
+ rr_val = apr_palloc(rr_pool, sizeof(*rr_val));
+ *rr_key = rb->rev_orig;
+ rr_val->rev = rb->rev_actual;
+ rr_val->was_dropped = FALSE;
+ apr_hash_set(rb->pb->renumber_history, rr_key,
+ sizeof(*rr_key), rr_val);
+ rb->pb->last_live_revision = rb->rev_actual;
+ }
+
+ if (! rb->pb->quiet)
+ SVN_ERR(svn_cmdline_fprintf(stderr, subpool,
+ _("Revision %ld committed as %ld.\n"),
+ rb->rev_orig, rb->rev_actual));
+ }
+ else
+ {
+ /* We're dropping this revision. */
+ rb->pb->rev_drop_count++;
+ if (rb->pb->do_renumber_revs)
+ {
+ svn_revnum_t *rr_key;
+ struct revmap_t *rr_val;
+ apr_pool_t *rr_pool = apr_hash_pool_get(rb->pb->renumber_history);
+ rr_key = apr_palloc(rr_pool, sizeof(*rr_key));
+ rr_val = apr_palloc(rr_pool, sizeof(*rr_val));
+ *rr_key = rb->rev_orig;
+ rr_val->rev = rb->pb->last_live_revision;
+ rr_val->was_dropped = TRUE;
+ apr_hash_set(rb->pb->renumber_history, rr_key,
+ sizeof(*rr_key), rr_val);
+ }
+
+ if (! rb->pb->quiet)
+ SVN_ERR(svn_cmdline_fprintf(stderr, subpool,
+ _("Revision %ld skipped.\n"),
+ rb->rev_orig));
+ }
+ svn_pool_destroy(subpool);
+ return SVN_NO_ERROR;
+}
+
+
+/* UUID record here: dump it, as we do not filter them. */
+static svn_error_t *
+uuid_record(const char *uuid, void *parse_baton, apr_pool_t *pool)
+{
+ struct parse_baton_t *pb = parse_baton;
+ SVN_ERR(svn_stream_printf(pb->out_stream, pool,
+ SVN_REPOS_DUMPFILE_UUID ": %s\n\n", uuid));
+ return SVN_NO_ERROR;
+}
+
+
+/* New node here. Set up node_baton by copying headers. */
+static svn_error_t *
+new_node_record(void **node_baton,
+ apr_hash_t *headers,
+ void *rev_baton,
+ apr_pool_t *pool)
+{
+ struct parse_baton_t *pb;
+ struct node_baton_t *nb;
+ char *node_path, *copyfrom_path;
+ apr_hash_index_t *hi;
+ const char *tcl;
+
+ *node_baton = apr_palloc(pool, sizeof(struct node_baton_t));
+ nb = *node_baton;
+ nb->rb = rev_baton;
+ pb = nb->rb->pb;
+
+ node_path = svn_hash_gets(headers, SVN_REPOS_DUMPFILE_NODE_PATH);
+ copyfrom_path = svn_hash_gets(headers, SVN_REPOS_DUMPFILE_NODE_COPYFROM_PATH);
+
+ /* Ensure that paths start with a leading '/'. */
+ if (node_path[0] != '/')
+ node_path = apr_pstrcat(pool, "/", node_path, (char *)NULL);
+ if (copyfrom_path && copyfrom_path[0] != '/')
+ copyfrom_path = apr_pstrcat(pool, "/", copyfrom_path, (char *)NULL);
+
+ nb->do_skip = skip_path(node_path, pb->prefixes,
+ pb->do_exclude, pb->glob);
+
+ /* If we're skipping the node, take note of path, discarding the
+ rest. */
+ if (nb->do_skip)
+ {
+ svn_hash_sets(pb->dropped_nodes,
+ apr_pstrdup(apr_hash_pool_get(pb->dropped_nodes),
+ node_path),
+ (void *)1);
+ nb->rb->had_dropped_nodes = TRUE;
+ }
+ else
+ {
+ const char *kind;
+ const char *action;
+
+ tcl = svn_hash_gets(headers, SVN_REPOS_DUMPFILE_TEXT_CONTENT_LENGTH);
+
+ /* Test if this node was copied from dropped source. */
+ if (copyfrom_path &&
+ skip_path(copyfrom_path, pb->prefixes, pb->do_exclude, pb->glob))
+ {
+ /* This node was copied from a dropped source.
+ We have a problem, since we did not want to drop this node too.
+
+ However, there is one special case we'll handle. If the node is
+ a file, and this was a copy-and-modify operation, then the
+ dumpfile should contain the new contents of the file. In this
+ scenario, we'll just do an add without history using the new
+ contents. */
+ kind = svn_hash_gets(headers, SVN_REPOS_DUMPFILE_NODE_KIND);
+
+ /* If there is a Text-content-length header, and the kind is
+ "file", we just fallback to an add without history. */
+ if (tcl && (strcmp(kind, "file") == 0))
+ {
+ svn_hash_sets(headers, SVN_REPOS_DUMPFILE_NODE_COPYFROM_PATH,
+ NULL);
+ svn_hash_sets(headers, SVN_REPOS_DUMPFILE_NODE_COPYFROM_REV,
+ NULL);
+ copyfrom_path = NULL;
+ }
+ /* Else, this is either a directory or a file whose contents we
+ don't have readily available. */
+ else
+ {
+ return svn_error_createf
+ (SVN_ERR_INCOMPLETE_DATA, 0,
+ _("Invalid copy source path '%s'"), copyfrom_path);
+ }
+ }
+
+ nb->has_props = FALSE;
+ nb->has_text = FALSE;
+ nb->has_prop_delta = FALSE;
+ nb->has_text_delta = FALSE;
+ nb->writing_begun = FALSE;
+ nb->tcl = tcl ? svn__atoui64(tcl) : 0;
+ nb->header = svn_stringbuf_create_empty(pool);
+ nb->props = svn_stringbuf_create_empty(pool);
+ nb->node_path = apr_pstrdup(pool, node_path);
+
+ /* Now we know for sure that we have a node that will not be
+ skipped, flush the revision if it has not already been done. */
+ nb->rb->has_nodes = TRUE;
+ if (! nb->rb->writing_begun)
+ SVN_ERR(output_revision(nb->rb));
+
+ /* A node record is required to begin with 'Node-path', skip the
+ leading '/' to match the form used by 'svnadmin dump'. */
+ SVN_ERR(svn_stream_printf(nb->rb->pb->out_stream,
+ pool, "%s: %s\n",
+ SVN_REPOS_DUMPFILE_NODE_PATH, node_path + 1));
+
+ /* Node-kind is next and is optional. */
+ kind = svn_hash_gets(headers, SVN_REPOS_DUMPFILE_NODE_KIND);
+ if (kind)
+ SVN_ERR(svn_stream_printf(nb->rb->pb->out_stream,
+ pool, "%s: %s\n",
+ SVN_REPOS_DUMPFILE_NODE_KIND, kind));
+
+ /* Node-action is next and required. */
+ action = svn_hash_gets(headers, SVN_REPOS_DUMPFILE_NODE_ACTION);
+ if (action)
+ SVN_ERR(svn_stream_printf(nb->rb->pb->out_stream,
+ pool, "%s: %s\n",
+ SVN_REPOS_DUMPFILE_NODE_ACTION, action));
+ else
+ return svn_error_createf(SVN_ERR_INCOMPLETE_DATA, 0,
+ _("Missing Node-action for path '%s'"),
+ node_path);
+
+ for (hi = apr_hash_first(pool, headers); hi; hi = apr_hash_next(hi))
+ {
+ const char *key = svn__apr_hash_index_key(hi);
+ const char *val = svn__apr_hash_index_val(hi);
+
+ if ((!strcmp(key, SVN_REPOS_DUMPFILE_PROP_DELTA))
+ && (!strcmp(val, "true")))
+ nb->has_prop_delta = TRUE;
+
+ if ((!strcmp(key, SVN_REPOS_DUMPFILE_TEXT_DELTA))
+ && (!strcmp(val, "true")))
+ nb->has_text_delta = TRUE;
+
+ if ((!strcmp(key, SVN_REPOS_DUMPFILE_CONTENT_LENGTH))
+ || (!strcmp(key, SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH))
+ || (!strcmp(key, SVN_REPOS_DUMPFILE_TEXT_CONTENT_LENGTH))
+ || (!strcmp(key, SVN_REPOS_DUMPFILE_NODE_PATH))
+ || (!strcmp(key, SVN_REPOS_DUMPFILE_NODE_KIND))
+ || (!strcmp(key, SVN_REPOS_DUMPFILE_NODE_ACTION)))
+ continue;
+
+ /* Rewrite Node-Copyfrom-Rev if we are renumbering revisions.
+ The number points to some revision in the past. We keep track
+ of revision renumbering in an apr_hash, which maps original
+ revisions to new ones. Dropped revision are mapped to -1.
+ This should never happen here.
+ */
+ if (pb->do_renumber_revs
+ && (!strcmp(key, SVN_REPOS_DUMPFILE_NODE_COPYFROM_REV)))
+ {
+ svn_revnum_t cf_orig_rev;
+ struct revmap_t *cf_renum_val;
+
+ cf_orig_rev = SVN_STR_TO_REV(val);
+ cf_renum_val = apr_hash_get(pb->renumber_history,
+ &cf_orig_rev,
+ sizeof(svn_revnum_t));
+ if (! (cf_renum_val && SVN_IS_VALID_REVNUM(cf_renum_val->rev)))
+ return svn_error_createf
+ (SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
+ _("No valid copyfrom revision in filtered stream"));
+ SVN_ERR(svn_stream_printf
+ (nb->rb->pb->out_stream, pool,
+ SVN_REPOS_DUMPFILE_NODE_COPYFROM_REV ": %ld\n",
+ cf_renum_val->rev));
+ continue;
+ }
+
+ /* passthru: put header straight to output */
+
+ SVN_ERR(svn_stream_printf(nb->rb->pb->out_stream,
+ pool, "%s: %s\n",
+ key, val));
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Output node header and props to dumpstream
+ This will be called by set_fulltext() after setting nb->has_text to TRUE,
+ if the node has any text, or by close_node() otherwise. This must only
+ be called if nb->writing_begun is FALSE. */
+static svn_error_t *
+output_node(struct node_baton_t *nb)
+{
+ int bytes_used;
+ char buf[SVN_KEYLINE_MAXLEN];
+
+ nb->writing_begun = TRUE;
+
+ /* when there are no props nb->props->len would be zero and won't mess up
+ Content-Length. */
+ if (nb->has_props)
+ svn_stringbuf_appendcstr(nb->props, "PROPS-END\n");
+
+ /* 1. recalculate & check text-md5 if present. Passed through right now. */
+
+ /* 2. recalculate and add content-lengths */
+
+ if (nb->has_props)
+ {
+ svn_stringbuf_appendcstr(nb->header,
+ SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH);
+ bytes_used = apr_snprintf(buf, sizeof(buf), ": %" APR_SIZE_T_FMT,
+ nb->props->len);
+ svn_stringbuf_appendbytes(nb->header, buf, bytes_used);
+ svn_stringbuf_appendbyte(nb->header, '\n');
+ }
+ if (nb->has_text)
+ {
+ svn_stringbuf_appendcstr(nb->header,
+ SVN_REPOS_DUMPFILE_TEXT_CONTENT_LENGTH);
+ bytes_used = apr_snprintf(buf, sizeof(buf), ": %" SVN_FILESIZE_T_FMT,
+ nb->tcl);
+ svn_stringbuf_appendbytes(nb->header, buf, bytes_used);
+ svn_stringbuf_appendbyte(nb->header, '\n');
+ }
+ svn_stringbuf_appendcstr(nb->header, SVN_REPOS_DUMPFILE_CONTENT_LENGTH);
+ bytes_used = apr_snprintf(buf, sizeof(buf), ": %" SVN_FILESIZE_T_FMT,
+ (svn_filesize_t) (nb->props->len + nb->tcl));
+ svn_stringbuf_appendbytes(nb->header, buf, bytes_used);
+ svn_stringbuf_appendbyte(nb->header, '\n');
+
+ /* put an end to headers */
+ svn_stringbuf_appendbyte(nb->header, '\n');
+
+ /* 3. output all the stuff */
+
+ SVN_ERR(svn_stream_write(nb->rb->pb->out_stream,
+ nb->header->data , &(nb->header->len)));
+ SVN_ERR(svn_stream_write(nb->rb->pb->out_stream,
+ nb->props->data , &(nb->props->len)));
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Examine the mergeinfo in INITIAL_VAL, omitting missing merge
+ sources or renumbering revisions in rangelists as appropriate, and
+ return the (possibly new) mergeinfo in *FINAL_VAL (allocated from
+ POOL). */
+static svn_error_t *
+adjust_mergeinfo(svn_string_t **final_val, const svn_string_t *initial_val,
+ struct revision_baton_t *rb, apr_pool_t *pool)
+{
+ apr_hash_t *mergeinfo;
+ apr_hash_t *final_mergeinfo = apr_hash_make(pool);
+ apr_hash_index_t *hi;
+ apr_pool_t *subpool = svn_pool_create(pool);
+
+ SVN_ERR(svn_mergeinfo_parse(&mergeinfo, initial_val->data, subpool));
+
+ /* Issue #3020: If we are skipping missing merge sources, then also
+ filter mergeinfo ranges as old or older than the oldest revision in the
+ dump stream. Those older than the oldest obviously refer to history
+ outside of the dump stream. The oldest rev itself is present in the
+ dump, but cannot be a valid merge source revision since it is the
+ start of all history. E.g. if we dump -r100:400 then dumpfilter the
+ result with --skip-missing-merge-sources, any mergeinfo with revision
+ 100 implies a change of -r99:100, but r99 is part of the history we
+ want filtered.
+
+ If the oldest rev is r0 then there is nothing to filter. */
+ if (rb->pb->skip_missing_merge_sources && rb->pb->oldest_original_rev > 0)
+ SVN_ERR(svn_mergeinfo__filter_mergeinfo_by_ranges(
+ &mergeinfo, mergeinfo,
+ rb->pb->oldest_original_rev, 0,
+ FALSE, subpool, subpool));
+
+ for (hi = apr_hash_first(subpool, mergeinfo); hi; hi = apr_hash_next(hi))
+ {
+ const char *merge_source = svn__apr_hash_index_key(hi);
+ svn_rangelist_t *rangelist = svn__apr_hash_index_val(hi);
+ struct parse_baton_t *pb = rb->pb;
+
+ /* Determine whether the merge_source is a part of the prefix. */
+ if (skip_path(merge_source, pb->prefixes, pb->do_exclude, pb->glob))
+ {
+ if (pb->skip_missing_merge_sources)
+ continue;
+ else
+ return svn_error_createf(SVN_ERR_INCOMPLETE_DATA, 0,
+ _("Missing merge source path '%s'; try "
+ "with --skip-missing-merge-sources"),
+ merge_source);
+ }
+
+ /* Possibly renumber revisions in merge source's rangelist. */
+ if (pb->do_renumber_revs)
+ {
+ int i;
+
+ for (i = 0; i < rangelist->nelts; i++)
+ {
+ struct revmap_t *revmap_start;
+ struct revmap_t *revmap_end;
+ svn_merge_range_t *range = APR_ARRAY_IDX(rangelist, i,
+ svn_merge_range_t *);
+
+ revmap_start = apr_hash_get(pb->renumber_history,
+ &range->start, sizeof(svn_revnum_t));
+ if (! (revmap_start && SVN_IS_VALID_REVNUM(revmap_start->rev)))
+ return svn_error_createf
+ (SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
+ _("No valid revision range 'start' in filtered stream"));
+
+ revmap_end = apr_hash_get(pb->renumber_history,
+ &range->end, sizeof(svn_revnum_t));
+ if (! (revmap_end && SVN_IS_VALID_REVNUM(revmap_end->rev)))
+ return svn_error_createf
+ (SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
+ _("No valid revision range 'end' in filtered stream"));
+
+ range->start = revmap_start->rev;
+ range->end = revmap_end->rev;
+ }
+ }
+ svn_hash_sets(final_mergeinfo, merge_source, rangelist);
+ }
+
+ SVN_ERR(svn_mergeinfo__canonicalize_ranges(final_mergeinfo, subpool));
+ SVN_ERR(svn_mergeinfo_to_string(final_val, final_mergeinfo, pool));
+ svn_pool_destroy(subpool);
+
+ return SVN_NO_ERROR;
+}
+
+
+static svn_error_t *
+set_revision_property(void *revision_baton,
+ const char *name,
+ const svn_string_t *value)
+{
+ struct revision_baton_t *rb = revision_baton;
+ apr_pool_t *hash_pool = apr_hash_pool_get(rb->props);
+
+ rb->has_props = TRUE;
+ svn_hash_sets(rb->props,
+ apr_pstrdup(hash_pool, name),
+ svn_string_dup(value, hash_pool));
+ return SVN_NO_ERROR;
+}
+
+
+static svn_error_t *
+set_node_property(void *node_baton,
+ const char *name,
+ const svn_string_t *value)
+{
+ struct node_baton_t *nb = node_baton;
+ struct revision_baton_t *rb = nb->rb;
+
+ if (nb->do_skip)
+ return SVN_NO_ERROR;
+
+ if (! (nb->has_props || nb->has_prop_delta))
+ return svn_error_createf(SVN_ERR_STREAM_MALFORMED_DATA, NULL,
+ _("Delta property block detected, but deltas "
+ "are not enabled for node '%s' in original "
+ "revision %ld"),
+ nb->node_path, rb->rev_orig);
+
+ if (strcmp(name, SVN_PROP_MERGEINFO) == 0)
+ {
+ svn_string_t *filtered_mergeinfo; /* Avoid compiler warning. */
+ apr_pool_t *pool = apr_hash_pool_get(rb->props);
+ SVN_ERR(adjust_mergeinfo(&filtered_mergeinfo, value, rb, pool));
+ value = filtered_mergeinfo;
+ }
+
+ nb->has_props = TRUE;
+ write_prop_to_stringbuf(nb->props, name, value);
+
+ return SVN_NO_ERROR;
+}
+
+
+static svn_error_t *
+delete_node_property(void *node_baton, const char *name)
+{
+ struct node_baton_t *nb = node_baton;
+ struct revision_baton_t *rb = nb->rb;
+
+ if (nb->do_skip)
+ return SVN_NO_ERROR;
+
+ if (!nb->has_prop_delta)
+ return svn_error_createf(SVN_ERR_STREAM_MALFORMED_DATA, NULL,
+ _("Delta property block detected, but deltas "
+ "are not enabled for node '%s' in original "
+ "revision %ld"),
+ nb->node_path, rb->rev_orig);
+
+ nb->has_props = TRUE;
+ write_propdel_to_stringbuf(&(nb->props), name);
+
+ return SVN_NO_ERROR;
+}
+
+
+static svn_error_t *
+remove_node_props(void *node_baton)
+{
+ struct node_baton_t *nb = node_baton;
+
+ /* In this case, not actually indicating that the node *has* props,
+ rather that we know about all the props that it has, since it now
+ has none. */
+ nb->has_props = TRUE;
+
+ return SVN_NO_ERROR;
+}
+
+
+static svn_error_t *
+set_fulltext(svn_stream_t **stream, void *node_baton)
+{
+ struct node_baton_t *nb = node_baton;
+
+ if (!nb->do_skip)
+ {
+ nb->has_text = TRUE;
+ if (! nb->writing_begun)
+ SVN_ERR(output_node(nb));
+ *stream = nb->rb->pb->out_stream;
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Finalize node */
+static svn_error_t *
+close_node(void *node_baton)
+{
+ struct node_baton_t *nb = node_baton;
+ apr_size_t len = 2;
+
+ /* Get out of here if we can. */
+ if (nb->do_skip)
+ return SVN_NO_ERROR;
+
+ /* If the node was not flushed already to output its text, do it now. */
+ if (! nb->writing_begun)
+ SVN_ERR(output_node(nb));
+
+ /* put an end to node. */
+ SVN_ERR(svn_stream_write(nb->rb->pb->out_stream, "\n\n", &len));
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Finalize revision */
+static svn_error_t *
+close_revision(void *revision_baton)
+{
+ struct revision_baton_t *rb = revision_baton;
+
+ /* If no node has yet flushed the revision, do it now. */
+ if (! rb->writing_begun)
+ return output_revision(rb);
+ else
+ return SVN_NO_ERROR;
+}
+
+
+/* Filtering vtable */
+svn_repos_parse_fns3_t filtering_vtable =
+ {
+ magic_header_record,
+ uuid_record,
+ new_revision_record,
+ new_node_record,
+ set_revision_property,
+ set_node_property,
+ delete_node_property,
+ remove_node_props,
+ set_fulltext,
+ NULL,
+ close_node,
+ close_revision
+ };
+
+
+
+/** Subcommands. **/
+
+static svn_opt_subcommand_t
+ subcommand_help,
+ subcommand_exclude,
+ subcommand_include;
+
+enum
+ {
+ svndumpfilter__drop_empty_revs = SVN_OPT_FIRST_LONGOPT_ID,
+ svndumpfilter__drop_all_empty_revs,
+ svndumpfilter__renumber_revs,
+ svndumpfilter__preserve_revprops,
+ svndumpfilter__skip_missing_merge_sources,
+ svndumpfilter__targets,
+ svndumpfilter__quiet,
+ svndumpfilter__glob,
+ svndumpfilter__version
+ };
+
+/* Option codes and descriptions.
+ *
+ * The entire list must be terminated with an entry of nulls.
+ */
+static const apr_getopt_option_t options_table[] =
+ {
+ {"help", 'h', 0,
+ N_("show help on a subcommand")},
+
+ {NULL, '?', 0,
+ N_("show help on a subcommand")},
+
+ {"version", svndumpfilter__version, 0,
+ N_("show program version information") },
+ {"quiet", svndumpfilter__quiet, 0,
+ N_("Do not display filtering statistics.") },
+ {"pattern", svndumpfilter__glob, 0,
+ N_("Treat the path prefixes as file glob patterns.") },
+ {"drop-empty-revs", svndumpfilter__drop_empty_revs, 0,
+ N_("Remove revisions emptied by filtering.")},
+ {"drop-all-empty-revs", svndumpfilter__drop_all_empty_revs, 0,
+ N_("Remove all empty revisions found in dumpstream\n"
+ " except revision 0.")},
+ {"renumber-revs", svndumpfilter__renumber_revs, 0,
+ N_("Renumber revisions left after filtering.") },
+ {"skip-missing-merge-sources",
+ svndumpfilter__skip_missing_merge_sources, 0,
+ N_("Skip missing merge sources.") },
+ {"preserve-revprops", svndumpfilter__preserve_revprops, 0,
+ N_("Don't filter revision properties.") },
+ {"targets", svndumpfilter__targets, 1,
+ N_("Read additional prefixes, one per line, from\n"
+ " file ARG.")},
+ {NULL}
+ };
+
+
+/* Array of available subcommands.
+ * The entire list must be terminated with an entry of nulls.
+ */
+static const svn_opt_subcommand_desc2_t cmd_table[] =
+ {
+ {"exclude", subcommand_exclude, {0},
+ N_("Filter out nodes with given prefixes from dumpstream.\n"
+ "usage: svndumpfilter exclude PATH_PREFIX...\n"),
+ {svndumpfilter__drop_empty_revs, svndumpfilter__drop_all_empty_revs,
+ svndumpfilter__renumber_revs,
+ svndumpfilter__skip_missing_merge_sources, svndumpfilter__targets,
+ svndumpfilter__preserve_revprops, svndumpfilter__quiet,
+ svndumpfilter__glob} },
+
+ {"include", subcommand_include, {0},
+ N_("Filter out nodes without given prefixes from dumpstream.\n"
+ "usage: svndumpfilter include PATH_PREFIX...\n"),
+ {svndumpfilter__drop_empty_revs, svndumpfilter__drop_all_empty_revs,
+ svndumpfilter__renumber_revs,
+ svndumpfilter__skip_missing_merge_sources, svndumpfilter__targets,
+ svndumpfilter__preserve_revprops, svndumpfilter__quiet,
+ svndumpfilter__glob} },
+
+ {"help", subcommand_help, {"?", "h"},
+ N_("Describe the usage of this program or its subcommands.\n"
+ "usage: svndumpfilter help [SUBCOMMAND...]\n"),
+ {0} },
+
+ { NULL, NULL, {0}, NULL, {0} }
+ };
+
+
+/* Baton for passing option/argument state to a subcommand function. */
+struct svndumpfilter_opt_state
+{
+ svn_opt_revision_t start_revision; /* -r X[:Y] is */
+ svn_opt_revision_t end_revision; /* not implemented. */
+ svn_boolean_t quiet; /* --quiet */
+ svn_boolean_t glob; /* --pattern */
+ svn_boolean_t version; /* --version */
+ svn_boolean_t drop_empty_revs; /* --drop-empty-revs */
+ svn_boolean_t drop_all_empty_revs; /* --drop-all-empty-revs */
+ svn_boolean_t help; /* --help or -? */
+ svn_boolean_t renumber_revs; /* --renumber-revs */
+ svn_boolean_t preserve_revprops; /* --preserve-revprops */
+ svn_boolean_t skip_missing_merge_sources;
+ /* --skip-missing-merge-sources */
+ const char *targets_file; /* --targets-file */
+ apr_array_header_t *prefixes; /* mainargs. */
+};
+
+
+static svn_error_t *
+parse_baton_initialize(struct parse_baton_t **pb,
+ struct svndumpfilter_opt_state *opt_state,
+ svn_boolean_t do_exclude,
+ apr_pool_t *pool)
+{
+ struct parse_baton_t *baton = apr_palloc(pool, sizeof(*baton));
+
+ /* Read the stream from STDIN. Users can redirect a file. */
+ SVN_ERR(create_stdio_stream(&(baton->in_stream),
+ apr_file_open_stdin, pool));
+
+ /* Have the parser dump results to STDOUT. Users can redirect a file. */
+ SVN_ERR(create_stdio_stream(&(baton->out_stream),
+ apr_file_open_stdout, pool));
+
+ baton->do_exclude = do_exclude;
+
+ /* Ignore --renumber-revs if there can't possibly be
+ anything to renumber. */
+ baton->do_renumber_revs =
+ (opt_state->renumber_revs && (opt_state->drop_empty_revs
+ || opt_state->drop_all_empty_revs));
+
+ baton->drop_empty_revs = opt_state->drop_empty_revs;
+ baton->drop_all_empty_revs = opt_state->drop_all_empty_revs;
+ baton->preserve_revprops = opt_state->preserve_revprops;
+ baton->quiet = opt_state->quiet;
+ baton->glob = opt_state->glob;
+ baton->prefixes = opt_state->prefixes;
+ baton->skip_missing_merge_sources = opt_state->skip_missing_merge_sources;
+ baton->rev_drop_count = 0; /* used to shift revnums while filtering */
+ baton->dropped_nodes = apr_hash_make(pool);
+ baton->renumber_history = apr_hash_make(pool);
+ baton->last_live_revision = SVN_INVALID_REVNUM;
+ baton->oldest_original_rev = SVN_INVALID_REVNUM;
+ baton->allow_deltas = FALSE;
+
+ *pb = baton;
+ return SVN_NO_ERROR;
+}
+
+/* This implements `help` subcommand. */
+static svn_error_t *
+subcommand_help(apr_getopt_t *os, void *baton, apr_pool_t *pool)
+{
+ struct svndumpfilter_opt_state *opt_state = baton;
+ const char *header =
+ _("general usage: svndumpfilter SUBCOMMAND [ARGS & OPTIONS ...]\n"
+ "Type 'svndumpfilter help <subcommand>' for help on a "
+ "specific subcommand.\n"
+ "Type 'svndumpfilter --version' to see the program version.\n"
+ "\n"
+ "Available subcommands:\n");
+
+ SVN_ERR(svn_opt_print_help4(os, "svndumpfilter",
+ opt_state ? opt_state->version : FALSE,
+ opt_state ? opt_state->quiet : FALSE,
+ /*###opt_state ? opt_state->verbose :*/ FALSE,
+ NULL, header, cmd_table, options_table,
+ NULL, NULL, pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+/* Version compatibility check */
+static svn_error_t *
+check_lib_versions(void)
+{
+ static const svn_version_checklist_t checklist[] =
+ {
+ { "svn_subr", svn_subr_version },
+ { "svn_repos", svn_repos_version },
+ { "svn_delta", svn_delta_version },
+ { NULL, NULL }
+ };
+ SVN_VERSION_DEFINE(my_version);
+
+ return svn_ver_check_list2(&my_version, checklist, svn_ver_equal);
+}
+
+
+/* Do the real work of filtering. */
+static svn_error_t *
+do_filter(apr_getopt_t *os,
+ void *baton,
+ svn_boolean_t do_exclude,
+ apr_pool_t *pool)
+{
+ struct svndumpfilter_opt_state *opt_state = baton;
+ struct parse_baton_t *pb;
+ apr_hash_index_t *hi;
+ apr_array_header_t *keys;
+ int i, num_keys;
+
+ if (! opt_state->quiet)
+ {
+ apr_pool_t *subpool = svn_pool_create(pool);
+
+ if (opt_state->glob)
+ {
+ SVN_ERR(svn_cmdline_fprintf(stderr, subpool,
+ do_exclude
+ ? (opt_state->drop_empty_revs
+ || opt_state->drop_all_empty_revs)
+ ? _("Excluding (and dropping empty "
+ "revisions for) prefix patterns:\n")
+ : _("Excluding prefix patterns:\n")
+ : (opt_state->drop_empty_revs
+ || opt_state->drop_all_empty_revs)
+ ? _("Including (and dropping empty "
+ "revisions for) prefix patterns:\n")
+ : _("Including prefix patterns:\n")));
+ }
+ else
+ {
+ SVN_ERR(svn_cmdline_fprintf(stderr, subpool,
+ do_exclude
+ ? (opt_state->drop_empty_revs
+ || opt_state->drop_all_empty_revs)
+ ? _("Excluding (and dropping empty "
+ "revisions for) prefixes:\n")
+ : _("Excluding prefixes:\n")
+ : (opt_state->drop_empty_revs
+ || opt_state->drop_all_empty_revs)
+ ? _("Including (and dropping empty "
+ "revisions for) prefixes:\n")
+ : _("Including prefixes:\n")));
+ }
+
+ for (i = 0; i < opt_state->prefixes->nelts; i++)
+ {
+ svn_pool_clear(subpool);
+ SVN_ERR(svn_cmdline_fprintf
+ (stderr, subpool, " '%s'\n",
+ APR_ARRAY_IDX(opt_state->prefixes, i, const char *)));
+ }
+
+ SVN_ERR(svn_cmdline_fputs("\n", stderr, subpool));
+ svn_pool_destroy(subpool);
+ }
+
+ SVN_ERR(parse_baton_initialize(&pb, opt_state, do_exclude, pool));
+ SVN_ERR(svn_repos_parse_dumpstream3(pb->in_stream, &filtering_vtable, pb,
+ TRUE, NULL, NULL, pool));
+
+ /* The rest of this is just reporting. If we aren't reporting, get
+ outta here. */
+ if (opt_state->quiet)
+ return SVN_NO_ERROR;
+
+ SVN_ERR(svn_cmdline_fputs("\n", stderr, pool));
+
+ if (pb->rev_drop_count)
+ SVN_ERR(svn_cmdline_fprintf(stderr, pool,
+ Q_("Dropped %d revision.\n\n",
+ "Dropped %d revisions.\n\n",
+ pb->rev_drop_count),
+ pb->rev_drop_count));
+
+ if (pb->do_renumber_revs)
+ {
+ apr_pool_t *subpool = svn_pool_create(pool);
+ SVN_ERR(svn_cmdline_fputs(_("Revisions renumbered as follows:\n"),
+ stderr, subpool));
+
+ /* Get the keys of the hash, sort them, then print the hash keys
+ and values, sorted by keys. */
+ num_keys = apr_hash_count(pb->renumber_history);
+ keys = apr_array_make(pool, num_keys + 1, sizeof(svn_revnum_t));
+ for (hi = apr_hash_first(pool, pb->renumber_history);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const svn_revnum_t *revnum = svn__apr_hash_index_key(hi);
+
+ APR_ARRAY_PUSH(keys, svn_revnum_t) = *revnum;
+ }
+ qsort(keys->elts, keys->nelts,
+ keys->elt_size, svn_sort_compare_revisions);
+ for (i = 0; i < keys->nelts; i++)
+ {
+ svn_revnum_t this_key;
+ struct revmap_t *this_val;
+
+ svn_pool_clear(subpool);
+ this_key = APR_ARRAY_IDX(keys, i, svn_revnum_t);
+ this_val = apr_hash_get(pb->renumber_history, &this_key,
+ sizeof(this_key));
+ if (this_val->was_dropped)
+ SVN_ERR(svn_cmdline_fprintf(stderr, subpool,
+ _(" %ld => (dropped)\n"),
+ this_key));
+ else
+ SVN_ERR(svn_cmdline_fprintf(stderr, subpool,
+ " %ld => %ld\n",
+ this_key, this_val->rev));
+ }
+ SVN_ERR(svn_cmdline_fputs("\n", stderr, subpool));
+ svn_pool_destroy(subpool);
+ }
+
+ if ((num_keys = apr_hash_count(pb->dropped_nodes)))
+ {
+ apr_pool_t *subpool = svn_pool_create(pool);
+ SVN_ERR(svn_cmdline_fprintf(stderr, subpool,
+ Q_("Dropped %d node:\n",
+ "Dropped %d nodes:\n",
+ num_keys),
+ num_keys));
+
+ /* Get the keys of the hash, sort them, then print the hash keys
+ and values, sorted by keys. */
+ keys = apr_array_make(pool, num_keys + 1, sizeof(const char *));
+ for (hi = apr_hash_first(pool, pb->dropped_nodes);
+ hi;
+ hi = apr_hash_next(hi))
+ {
+ const char *path = svn__apr_hash_index_key(hi);
+
+ APR_ARRAY_PUSH(keys, const char *) = path;
+ }
+ qsort(keys->elts, keys->nelts, keys->elt_size, svn_sort_compare_paths);
+ for (i = 0; i < keys->nelts; i++)
+ {
+ svn_pool_clear(subpool);
+ SVN_ERR(svn_cmdline_fprintf
+ (stderr, subpool, " '%s'\n",
+ (const char *)APR_ARRAY_IDX(keys, i, const char *)));
+ }
+ SVN_ERR(svn_cmdline_fputs("\n", stderr, subpool));
+ svn_pool_destroy(subpool);
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* This implements `exclude' subcommand. */
+static svn_error_t *
+subcommand_exclude(apr_getopt_t *os, void *baton, apr_pool_t *pool)
+{
+ return do_filter(os, baton, TRUE, pool);
+}
+
+
+/* This implements `include` subcommand. */
+static svn_error_t *
+subcommand_include(apr_getopt_t *os, void *baton, apr_pool_t *pool)
+{
+ return do_filter(os, baton, FALSE, pool);
+}
+
+
+
+/** Main. **/
+
+int
+main(int argc, const char *argv[])
+{
+ svn_error_t *err;
+ apr_status_t apr_err;
+ apr_pool_t *pool;
+
+ const svn_opt_subcommand_desc2_t *subcommand = NULL;
+ struct svndumpfilter_opt_state opt_state;
+ apr_getopt_t *os;
+ int opt_id;
+ apr_array_header_t *received_opts;
+ int i;
+
+
+ /* Initialize the app. */
+ if (svn_cmdline_init("svndumpfilter", 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));
+
+ /* Check library versions */
+ err = check_lib_versions();
+ if (err)
+ return svn_cmdline_handle_exit_error(err, pool, "svndumpfilter: ");
+
+ received_opts = apr_array_make(pool, SVN_OPT_MAX_OPTIONS, sizeof(int));
+
+ /* Initialize the FS library. */
+ err = svn_fs_initialize(pool);
+ if (err)
+ return svn_cmdline_handle_exit_error(err, pool, "svndumpfilter: ");
+
+ if (argc <= 1)
+ {
+ SVN_INT_ERR(subcommand_help(NULL, NULL, pool));
+ svn_pool_destroy(pool);
+ return EXIT_FAILURE;
+ }
+
+ /* Initialize opt_state. */
+ memset(&opt_state, 0, sizeof(opt_state));
+ opt_state.start_revision.kind = svn_opt_revision_unspecified;
+ opt_state.end_revision.kind = svn_opt_revision_unspecified;
+
+ /* Parse options. */
+ err = svn_cmdline__getopt_init(&os, argc, argv, pool);
+ if (err)
+ return svn_cmdline_handle_exit_error(err, pool, "svndumpfilter: ");
+
+ os->interleave = 1;
+ while (1)
+ {
+ const char *opt_arg;
+
+ /* Parse the next option. */
+ apr_err = apr_getopt_long(os, options_table, &opt_id, &opt_arg);
+ if (APR_STATUS_IS_EOF(apr_err))
+ break;
+ else if (apr_err)
+ {
+ SVN_INT_ERR(subcommand_help(NULL, NULL, pool));
+ svn_pool_destroy(pool);
+ return EXIT_FAILURE;
+ }
+
+ /* Stash the option code in an array before parsing it. */
+ APR_ARRAY_PUSH(received_opts, int) = opt_id;
+
+ switch (opt_id)
+ {
+ case 'h':
+ case '?':
+ opt_state.help = TRUE;
+ break;
+ case svndumpfilter__version:
+ opt_state.version = TRUE;
+ break;
+ case svndumpfilter__quiet:
+ opt_state.quiet = TRUE;
+ break;
+ case svndumpfilter__glob:
+ opt_state.glob = TRUE;
+ break;
+ case svndumpfilter__drop_empty_revs:
+ opt_state.drop_empty_revs = TRUE;
+ break;
+ case svndumpfilter__drop_all_empty_revs:
+ opt_state.drop_all_empty_revs = TRUE;
+ break;
+ case svndumpfilter__renumber_revs:
+ opt_state.renumber_revs = TRUE;
+ break;
+ case svndumpfilter__preserve_revprops:
+ opt_state.preserve_revprops = TRUE;
+ break;
+ case svndumpfilter__skip_missing_merge_sources:
+ opt_state.skip_missing_merge_sources = TRUE;
+ break;
+ case svndumpfilter__targets:
+ opt_state.targets_file = opt_arg;
+ break;
+ default:
+ {
+ SVN_INT_ERR(subcommand_help(NULL, NULL, pool));
+ svn_pool_destroy(pool);
+ return EXIT_FAILURE;
+ }
+ } /* close `switch' */
+ } /* close `while' */
+
+ /* Disallow simultaneous use of both --drop-empty-revs and
+ --drop-all-empty-revs. */
+ if (opt_state.drop_empty_revs && opt_state.drop_all_empty_revs)
+ {
+ err = svn_error_create(SVN_ERR_CL_MUTUALLY_EXCLUSIVE_ARGS, NULL,
+ _("--drop-empty-revs cannot be used with "
+ "--drop-all-empty-revs"));
+ return svn_cmdline_handle_exit_error(err, pool, "svndumpfilter: ");
+ }
+
+ /* If the user asked for help, then the rest of the arguments are
+ the names of subcommands to get help on (if any), or else they're
+ just typos/mistakes. Whatever the case, the subcommand to
+ actually run is subcommand_help(). */
+ if (opt_state.help)
+ subcommand = svn_opt_get_canonical_subcommand2(cmd_table, "help");
+
+ /* If we're not running the `help' subcommand, then look for a
+ subcommand in the first argument. */
+ if (subcommand == NULL)
+ {
+ if (os->ind >= os->argc)
+ {
+ if (opt_state.version)
+ {
+ /* Use the "help" subcommand to handle the "--version" option. */
+ static const svn_opt_subcommand_desc2_t pseudo_cmd =
+ { "--version", subcommand_help, {0}, "",
+ {svndumpfilter__version, /* must accept its own option */
+ svndumpfilter__quiet,
+ } };
+
+ subcommand = &pseudo_cmd;
+ }
+ else
+ {
+ svn_error_clear(svn_cmdline_fprintf
+ (stderr, pool,
+ _("Subcommand argument required\n")));
+ SVN_INT_ERR(subcommand_help(NULL, NULL, pool));
+ svn_pool_destroy(pool);
+ return EXIT_FAILURE;
+ }
+ }
+ else
+ {
+ const char *first_arg = os->argv[os->ind++];
+ subcommand = svn_opt_get_canonical_subcommand2(cmd_table, first_arg);
+ if (subcommand == NULL)
+ {
+ const char* first_arg_utf8;
+ if ((err = svn_utf_cstring_to_utf8(&first_arg_utf8, first_arg,
+ pool)))
+ return svn_cmdline_handle_exit_error(err, pool,
+ "svndumpfilter: ");
+
+ svn_error_clear(
+ svn_cmdline_fprintf(stderr, pool,
+ _("Unknown subcommand: '%s'\n"),
+ first_arg_utf8));
+ SVN_INT_ERR(subcommand_help(NULL, NULL, pool));
+ svn_pool_destroy(pool);
+ return EXIT_FAILURE;
+ }
+ }
+ }
+
+ /* If there's a second argument, it's probably [one of] prefixes.
+ Every subcommand except `help' requires at least one, so we parse
+ them out here and store in opt_state. */
+
+ if (subcommand->cmd_func != subcommand_help)
+ {
+
+ opt_state.prefixes = apr_array_make(pool, os->argc - os->ind,
+ sizeof(const char *));
+ for (i = os->ind ; i< os->argc; i++)
+ {
+ const char *prefix;
+
+ /* Ensure that each prefix is UTF8-encoded, in internal
+ style, and absolute. */
+ SVN_INT_ERR(svn_utf_cstring_to_utf8(&prefix, os->argv[i], pool));
+ prefix = svn_relpath__internal_style(prefix, pool);
+ if (prefix[0] != '/')
+ prefix = apr_pstrcat(pool, "/", prefix, (char *)NULL);
+ APR_ARRAY_PUSH(opt_state.prefixes, const char *) = prefix;
+ }
+
+ if (opt_state.targets_file)
+ {
+ svn_stringbuf_t *buffer, *buffer_utf8;
+ const char *utf8_targets_file;
+ apr_array_header_t *targets = apr_array_make(pool, 0,
+ sizeof(const char *));
+
+ /* We need to convert to UTF-8 now, even before we divide
+ the targets into an array, because otherwise we wouldn't
+ know what delimiter to use for svn_cstring_split(). */
+
+ SVN_INT_ERR(svn_utf_cstring_to_utf8(&utf8_targets_file,
+ opt_state.targets_file, pool));
+
+ SVN_INT_ERR(svn_stringbuf_from_file2(&buffer, utf8_targets_file,
+ pool));
+ SVN_INT_ERR(svn_utf_stringbuf_to_utf8(&buffer_utf8, buffer, pool));
+
+ targets = apr_array_append(pool,
+ svn_cstring_split(buffer_utf8->data, "\n\r",
+ TRUE, pool),
+ targets);
+
+ for (i = 0; i < targets->nelts; i++)
+ {
+ const char *prefix = APR_ARRAY_IDX(targets, i, const char *);
+ if (prefix[0] != '/')
+ prefix = apr_pstrcat(pool, "/", prefix, (char *)NULL);
+ APR_ARRAY_PUSH(opt_state.prefixes, const char *) = prefix;
+ }
+ }
+
+ if (apr_is_empty_array(opt_state.prefixes))
+ {
+ svn_error_clear(svn_cmdline_fprintf
+ (stderr, pool,
+ _("\nError: no prefixes supplied.\n")));
+ svn_pool_destroy(pool);
+ return EXIT_FAILURE;
+ }
+ }
+
+
+ /* Check that the subcommand wasn't passed any inappropriate options. */
+ for (i = 0; i < received_opts->nelts; i++)
+ {
+ 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, options_table, subcommand,
+ pool);
+ svn_opt_format_option(&optstr, badopt, FALSE, pool);
+ if (subcommand->name[0] == '-')
+ SVN_INT_ERR(subcommand_help(NULL, NULL, pool));
+ else
+ svn_error_clear(svn_cmdline_fprintf
+ (stderr, pool,
+ _("Subcommand '%s' doesn't accept option '%s'\n"
+ "Type 'svndumpfilter help %s' for usage.\n"),
+ subcommand->name, optstr, subcommand->name));
+ svn_pool_destroy(pool);
+ return EXIT_FAILURE;
+ }
+ }
+
+ /* Run the subcommand. */
+ err = (*subcommand->cmd_func)(os, &opt_state, pool);
+ if (err)
+ {
+ /* For argument-related problems, suggest using the 'help'
+ subcommand. */
+ if (err->apr_err == SVN_ERR_CL_INSUFFICIENT_ARGS
+ || err->apr_err == SVN_ERR_CL_ARG_PARSING_ERROR)
+ {
+ err = svn_error_quick_wrap(err,
+ _("Try 'svndumpfilter help' for more "
+ "info"));
+ }
+ return svn_cmdline_handle_exit_error(err, pool, "svndumpfilter: ");
+ }
+ else
+ {
+ svn_pool_destroy(pool);
+
+ /* Flush stdout, making sure the user will see any print errors. */
+ SVN_INT_ERR(svn_cmdline_fflush(stdout));
+ return EXIT_SUCCESS;
+ }
+}