blob: f1e065f8bfb2a86e33e1070184e983a0dbc2c158 [file] [log] [blame]
/*
* main.c: Subversion dump stream filtering tool.
*
* ====================================================================
* Copyright (c) 2000-2003 CollabNet. All rights reserved.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at http://subversion.tigris.org/license-1.html.
* If newer versions of this license are posted there, you may use a
* newer version instead, at your option.
*
* This software consists of voluntary contributions made by many
* individuals. For exact contribution history, see the revision
* history and logs, available at http://subversion.tigris.org/.
* ====================================================================
*/
#include <apr_file_io.h>
#include "svn_cmdline.h"
#include "svn_error.h"
#include "svn_opt.h"
#include "svn_utf.h"
#include "svn_subst.h"
#include "svn_path.h"
#include "svn_config.h"
#include "svn_hash.h"
#include "svn_repos.h"
#include "svn_pools.h"
/* a reasonable guess at number of nodes that would be dropped */
#define REASONABLE_GUESS 42
/*** 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,
APR_DECLARE(apr_status_t) open_fn (apr_file_t **,
apr_pool_t *),
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_create (apr_err, NULL,
"error opening stdio file");
*stream = svn_stream_from_aprfile (stdio_file, pool);
return SVN_NO_ERROR;
}
#if 0
static void
lx_dump_2cstring_ht (apr_pool_t *p, apr_hash_t *ht)
{
apr_hash_index_t *hi;
void *key, *val;
for (hi = apr_hash_first (p, ht); hi; hi = apr_hash_next (hi))
{
apr_hash_this (hi, &key, NULL, &val);
fprintf (stderr, "\n '%s' => '%s'",
(const char *)key, (const char *)val);
}
}
#endif
static void
lx_dump_2revnum_ht (apr_pool_t *p, apr_hash_t *ht)
{
apr_hash_index_t *hi;
const void *key;
void *val;
for (hi = apr_hash_first (p, ht); hi; hi = apr_hash_next (hi))
{
apr_hash_this (hi, &key, NULL, &val);
fprintf (stderr, "\n '%" SVN_REVNUM_T_FMT "' => '%" SVN_REVNUM_T_FMT
"'", *((svn_revnum_t *)key), *((svn_revnum_t *)val));
}
}
static void
lx_dump_cstring_ary (apr_array_header_t *array)
{
int i;
const char *elt;
for (i= 0; i < array->nelts; i++)
{
elt = APR_ARRAY_IDX (array , i, const char *);
fprintf (stderr,"%s\n", elt);
}
}
/* 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, namelen;
char buf[SVN_KEYLINE_MAXLEN];
/* Output name length, then name. */
namelen = strlen (name);
svn_stringbuf_appendbytes (*strbuf, "K ", 2);
sprintf (buf, "%d%n", namelen, &bytes_used);
svn_stringbuf_appendbytes (*strbuf, buf, bytes_used);
svn_stringbuf_appendbytes (*strbuf, "\n", 1);
svn_stringbuf_appendbytes (*strbuf, name, namelen);
svn_stringbuf_appendbytes (*strbuf, "\n", 1);
/* Output value length, then value. */
svn_stringbuf_appendbytes (*strbuf, "V ", 2);
sprintf (buf, "%" APR_SIZE_T_FMT "%n", value->len, &bytes_used);
svn_stringbuf_appendbytes (*strbuf, buf, bytes_used);
svn_stringbuf_appendbytes (*strbuf, "\n", 1);
svn_stringbuf_appendbytes (*strbuf, value->data, value->len);
svn_stringbuf_appendbytes (*strbuf, "\n", 1);
}
/* Prefix matching function to compare node-path with set of prefixes. */
static svn_boolean_t
ary_prefix_match (apr_array_header_t *pfxlist, const char *path)
{
int i, path_len, pfx_len;
const char *pfx;
svn_boolean_t rval = FALSE;
path_len = strlen (path);
for (i= 0; i < pfxlist->nelts; i++)
{
pfx = APR_ARRAY_IDX (pfxlist , i, const char *);
pfx_len = strlen (pfx);
if (path_len < pfx_len)
continue;
rval = (0 == strncmp (path, pfx, pfx_len));
if (rval)
break;
}
return rval;
}
/* EXact matching function to compare node-path with set of prefixes. */
static svn_boolean_t
ary_exact_match (apr_array_header_t *estlist, const char *path)
{
int i, path_len, est_len;
const char *est;
svn_boolean_t rval = FALSE;
path_len = strlen (path);
for (i= 0; i < estlist->nelts; i++)
{
est = APR_ARRAY_IDX (estlist , i, const char *);
est_len = strlen (est);
if (path_len != est_len)
continue;
rval = (0 == strncmp (path, est, est_len));
if (rval)
break;
}
return rval;
}
/* Note: the input stream parser calls us up with events.
Output of filtered dump should take place at the close-events.
Before that we just save supplied data in corresponding batons.
*/
/* Filtering batons */
struct parse_baton_t {
svn_boolean_t do_exclude;
svn_boolean_t drop_empty_revs;
svn_boolean_t do_renumber_revs;
svn_boolean_t preserve_revprops;
svn_stream_t *in_stream;
svn_stream_t *out_stream;
apr_int32_t rev_drop_count;
apr_int32_t node_drop_count;
apr_array_header_t *prefixes;
apr_array_header_t *dropped_nodes;
apr_hash_t *renumber_history;
};
struct revision_baton_t {
struct parse_baton_t *pb;
svn_boolean_t has_nodes;
svn_boolean_t has_props;
svn_boolean_t had_dropped_nodes;
svn_revnum_t rev_orig;
svn_revnum_t rev_actual;
svn_stringbuf_t *header;
apr_hash_t *props;
svn_stringbuf_t *body;
svn_stream_t *body_stream;
};
struct node_baton_t {
struct revision_baton_t *rb;
svn_boolean_t do_skip;
svn_boolean_t has_props;
svn_boolean_t has_text;
svn_boolean_t remove_props;
svn_stringbuf_t *header;
svn_stringbuf_t *props;
svn_stringbuf_t *body;
svn_stringbuf_t *node_path;
svn_stringbuf_t *copyfrom_path;
svn_stream_t *body_stream;
};
/* Filtering vtable members */
/* 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 void *key;
void *val;
svn_stream_t *header_stream;
apr_pool_t *revhistory_pool;
svn_revnum_t *rr_key, *rr_value;
*revision_baton = apr_palloc (pool, sizeof (struct revision_baton_t));
rb = *revision_baton;
rb->has_nodes = FALSE;
rb->has_props = FALSE;
rb->had_dropped_nodes = FALSE;
rb->pb = parse_baton;
rb->header = svn_stringbuf_create ("", pool);
rb->body = svn_stringbuf_create ("", pool);
rb->props = apr_hash_make (pool);
rb->body_stream = svn_stream_from_stringbuf (rb->body, pool);
header_stream = svn_stream_from_stringbuf (rb->header, pool);
val = apr_hash_get (headers, SVN_REPOS_DUMPFILE_REVISION_NUMBER,
APR_HASH_KEY_STRING);
rb->rev_orig = SVN_STR_TO_REV(val);
if (rb->pb->do_renumber_revs)
{
rb->rev_actual = rb->rev_orig - rb->pb->rev_drop_count;
revhistory_pool = apr_hash_pool_get (rb->pb->renumber_history);
rr_key = apr_palloc (revhistory_pool, sizeof (svn_revnum_t));
rr_value = apr_palloc (revhistory_pool, sizeof (svn_revnum_t));
*rr_key = rb->rev_orig;
*rr_value = rb->rev_actual;
apr_hash_set (rb->pb->renumber_history, rr_key,
sizeof (svn_revnum_t), rr_value);
}
else
{
rb->rev_actual = rb->rev_orig;
}
SVN_ERR (svn_stream_printf (header_stream, pool,
SVN_REPOS_DUMPFILE_REVISION_NUMBER ": %"
SVN_REVNUM_T_FMT "\n", rb->rev_actual));
for (hi = apr_hash_first (pool, headers); hi; hi = apr_hash_next (hi))
{
apr_hash_this (hi, &key, NULL, &val);
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",
(const char *)key,
(const char *)val));
}
SVN_ERR (svn_stream_close (header_stream));
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;
svn_revnum_t cf_orig_rev, *cf_renum_rev;
apr_hash_index_t *hi;
const void *key;
void *val;
svn_stream_t *header_stream;
*node_baton = apr_palloc (pool, sizeof (struct node_baton_t));
nb = *node_baton;
nb->rb = rev_baton;
pb = nb->rb->pb;
node_path = apr_hash_get (headers, SVN_REPOS_DUMPFILE_NODE_PATH,
APR_HASH_KEY_STRING);
/* shame, shame, shame ... this is NXOR. */
nb->do_skip = (ary_prefix_match (pb->prefixes, node_path)
? pb->do_exclude : (!pb->do_exclude));
copyfrom_path = apr_hash_get (headers,
SVN_REPOS_DUMPFILE_NODE_COPYFROM_PATH,
APR_HASH_KEY_STRING);
/* See if this node was copied from dropped source. If it was,
we have 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. */
if ((copyfrom_path != NULL) && (! nb->do_skip))
{
const char *kind, *tcl;
kind = apr_hash_get (headers, SVN_REPOS_DUMPFILE_NODE_KIND,
APR_HASH_KEY_STRING);
tcl = apr_hash_get (headers, SVN_REPOS_DUMPFILE_TEXT_CONTENT_LENGTH,
APR_HASH_KEY_STRING);
/* 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))
{
apr_hash_set (headers, SVN_REPOS_DUMPFILE_NODE_COPYFROM_PATH,
APR_HASH_KEY_STRING, NULL);
apr_hash_set (headers, SVN_REPOS_DUMPFILE_NODE_COPYFROM_REV,
APR_HASH_KEY_STRING, NULL);
copyfrom_path = NULL;
}
else
{
nb->do_skip = ary_exact_match (pb->dropped_nodes, copyfrom_path);
}
}
/* if we're skipping the node, take note of path, discarding the rest
This is ugly :-(
*/
if (nb->do_skip)
{
*((const char **) apr_array_push (pb->dropped_nodes))
= apr_pstrdup (pb->dropped_nodes->pool, node_path);
nb->rb->had_dropped_nodes = TRUE;
pb->node_drop_count ++;
}
else
{
nb->has_props = FALSE;
nb->has_text = FALSE;
nb->remove_props = FALSE;
nb->header = svn_stringbuf_create ("", pool);
nb->props = svn_stringbuf_create ("", pool);
nb->body = svn_stringbuf_create ("", pool);
nb->body_stream = svn_stream_from_stringbuf (nb->body, pool);
header_stream = svn_stream_from_stringbuf (nb->header, pool);
for (hi = apr_hash_first (pool, headers); hi; hi = apr_hash_next (hi))
{
apr_hash_this (hi, (const void **) &key, NULL, &val);
if ((!strcmp (key, SVN_REPOS_DUMPFILE_CONTENT_LENGTH))
|| (!strcmp (key, SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH))
|| (!strcmp (key, SVN_REPOS_DUMPFILE_TEXT_CONTENT_LENGTH)))
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 a 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)))
{
cf_orig_rev = SVN_STR_TO_REV(val);
cf_renum_rev = apr_hash_get (pb->renumber_history,
&cf_orig_rev,
sizeof (svn_revnum_t));
if ((cf_renum_rev == NULL) || (*cf_renum_rev == -1))
{
/* bail out with an error */
return svn_error_createf
(SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
"Node with dropped parent sneaked in.");
}
SVN_ERR (svn_stream_printf
(header_stream, pool,
SVN_REPOS_DUMPFILE_NODE_COPYFROM_REV ": %"
SVN_REVNUM_T_FMT "\n", *cf_renum_rev));
continue;
}
/* passthru: put header into header stringbuf */
SVN_ERR (svn_stream_printf (header_stream, pool, "%s: %s\n",
(const char *)key,
(const char *)val));
}
SVN_ERR (svn_stream_close (header_stream));
}
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;
apr_hash_set (rb->props, apr_pstrdup (hash_pool, name),
APR_HASH_KEY_STRING, 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;
if (!nb->do_skip)
{
write_prop_to_stringbuf (&(nb->props), name, value);
nb->has_props = TRUE;
}
return SVN_NO_ERROR;
}
static svn_error_t *
remove_node_props (void *node_baton)
{
struct node_baton_t *nb = node_baton;
nb->remove_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)
{
*stream = nb->body_stream;
nb->has_text = TRUE;
}
return SVN_NO_ERROR;
}
/* Finalize node */
static svn_error_t *
close_node (void *node_baton)
{
struct node_baton_t *nb = node_baton;
int bytes_used;
char buf[SVN_KEYLINE_MAXLEN];
/* Get out of here if we can. */
if (nb->do_skip)
{
return SVN_NO_ERROR;
}
/* 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);
sprintf (buf, ": %" APR_SIZE_T_FMT "%n", nb->props->len, &bytes_used);
svn_stringbuf_appendbytes (nb->header, buf, bytes_used);
svn_stringbuf_appendbytes (nb->header, "\n", 1);
}
if (nb->has_text)
{
svn_stringbuf_appendcstr (nb->header,
SVN_REPOS_DUMPFILE_TEXT_CONTENT_LENGTH);
sprintf (buf, ": %" APR_SIZE_T_FMT "%n", nb->body->len, &bytes_used);
svn_stringbuf_appendbytes (nb->header, buf, bytes_used);
svn_stringbuf_appendbytes (nb->header, "\n", 1);
}
svn_stringbuf_appendcstr (nb->header, SVN_REPOS_DUMPFILE_CONTENT_LENGTH);
sprintf (buf, ": %" APR_SIZE_T_FMT "%n", (nb->props->len + nb->body->len),
&bytes_used);
svn_stringbuf_appendbytes (nb->header, buf, bytes_used);
svn_stringbuf_appendbytes (nb->header, "\n", 1);
/* put an end to headers */
svn_stringbuf_appendbytes (nb->header, "\n", 1);
/* put an end to node. */
svn_stringbuf_appendbytes (nb->body, "\n\n", 2);
/* 3. add all stuff to the parent revision */
svn_stringbuf_appendstr (nb->rb->body, nb->header);
svn_stringbuf_appendstr (nb->rb->body, nb->props);
svn_stringbuf_appendstr (nb->rb->body, nb->body);
nb->rb->has_nodes = TRUE;
return SVN_NO_ERROR;
}
/* Finalize revision */
static svn_error_t *
close_revision (void *revision_baton)
{
struct revision_baton_t *rb = revision_baton;
int bytes_used;
char buf[SVN_KEYLINE_MAXLEN];
apr_hash_index_t *hi;
apr_pool_t *hash_pool = apr_hash_pool_get (rb->props);
svn_stringbuf_t *props = svn_stringbuf_create ("", hash_pool);
/* 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))
{
apr_hash_t *old_props = rb->props;
rb->has_props = TRUE;
rb->props = apr_hash_make (hash_pool);
apr_hash_set (rb->props, SVN_PROP_REVISION_DATE, APR_HASH_KEY_STRING,
apr_hash_get (old_props, SVN_PROP_REVISION_DATE,
APR_HASH_KEY_STRING));
apr_hash_set (rb->props, SVN_PROP_REVISION_LOG, APR_HASH_KEY_STRING,
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 (hash_pool, rb->props);
hi;
hi = apr_hash_next (hi))
{
const void *key;
void *val;
apr_hash_this (hi, &key, NULL, &val);
write_prop_to_stringbuf (&props, key, val);
}
svn_stringbuf_appendcstr (props, "PROPS-END\n");
svn_stringbuf_appendcstr (rb->header,
SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH);
sprintf (buf, ": %" APR_SIZE_T_FMT "%n", props->len, &bytes_used);
svn_stringbuf_appendbytes (rb->header, buf, bytes_used);
svn_stringbuf_appendbytes (rb->header, "\n", 1);
}
svn_stringbuf_appendcstr (rb->header, SVN_REPOS_DUMPFILE_CONTENT_LENGTH);
sprintf (buf, ": %" APR_SIZE_T_FMT "%n", props->len, &bytes_used);
svn_stringbuf_appendbytes (rb->header, buf, bytes_used);
svn_stringbuf_appendbytes (rb->header, "\n", 1);
/* put an end to headers */
svn_stringbuf_appendbytes (rb->header, "\n", 1);
/* put an end to revision */
svn_stringbuf_appendbytes (props, "\n", 1);
/* write out the revision */
/* Revision is written out in the following cases:
1. No --drop-empty-revs has been supplied.
2. --drop-empty-revs has been supplied,
but revision has not all nodes dropped
3. Revision had no nodes to begin with.
*/
if (rb->has_nodes
|| (! rb->pb->drop_empty_revs)
|| (! rb->had_dropped_nodes))
{
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)));
SVN_ERR (svn_stream_write (rb->pb->out_stream,
rb->body->data , &(rb->body->len)));
fprintf (stderr, "Revision %" SVN_REVNUM_T_FMT " committed as %"
SVN_REVNUM_T_FMT ".\n", rb->rev_orig, rb->rev_actual);
}
else
{
rb->pb->rev_drop_count ++;
fprintf (stderr, "Revision %" SVN_REVNUM_T_FMT " skipped.\n",
rb->rev_orig);
}
return SVN_NO_ERROR;
}
/* Filtering vtable */
svn_repos_parser_fns_t filtering_vtable =
{
new_revision_record,
uuid_record,
new_node_record,
set_revision_property,
set_node_property,
remove_node_props,
set_fulltext,
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__renumber_revs,
svndumpfilter__preserve_revprops
};
/* Option codes and descriptions.
*
* This must not have more than SVN_OPT_MAX_OPTIONS entries; if you
* need more, increase that limit first.
*
* The entire list must be terminated with an entry of nulls.
*/
static const apr_getopt_option_t options_table[] =
{
{"help", 'h', 0,
"show help on a subcommand"},
{NULL, '?', 0,
"show help on a subcommand"},
{"drop-empty-revs", svndumpfilter__drop_empty_revs, 0,
"Remove revisions emptied by filtering."},
{"renumber-revs", svndumpfilter__renumber_revs, 0,
"Renumber revisions left after filtering." },
{"preserve-revprops", svndumpfilter__preserve_revprops, 0,
"Don't filter revision properties." },
{NULL}
};
/* Array of available subcommands.
* The entire list must be terminated with an entry of nulls.
*/
static const svn_opt_subcommand_desc_t cmd_table[] =
{
{"exclude", subcommand_exclude, {0},
"usage: svndumpfilter exclude PATH_PREFIX...\n\n"
"Filter out nodes with given prefixes from dumpstream.\n",
{svndumpfilter__drop_empty_revs, svndumpfilter__renumber_revs,
svndumpfilter__preserve_revprops} },
{"include", subcommand_include, {0},
"usage: svndumpfilter include PATH_PREFIX...\n\n"
"Filter out nodes without given prefixes from dumpstream.\n",
{svndumpfilter__drop_empty_revs, svndumpfilter__renumber_revs,
svndumpfilter__preserve_revprops} },
{"help", subcommand_help, {"?", "h"},
"usage: svndumpfilter help [SUBCOMMAND...]\n\n"
"Describe the usage of this program or its subcommands.\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 drop_empty_revs; /* --drop-empty-revs */
svn_boolean_t help; /* --help or -? */
svn_boolean_t renumber_revs; /* --renumber-revs */
svn_boolean_t preserve_revprops; /* --preserve-revprops */
apr_array_header_t *prefixes; /* mainargs. */
};
static void
dump_opt_state (struct svndumpfilter_opt_state *opt_state,
svn_boolean_t do_exclude)
{
int i;
fprintf (stderr,"%s %s drop called for prefixes:\n",
do_exclude ? "Exclude" : "Include",
opt_state->drop_empty_revs ? "with" : "without");
for (i= 0; i < opt_state->prefixes->nelts; i++)
{
fprintf (stderr,"'%s'\n",
APR_ARRAY_IDX (opt_state->prefixes , i, const char *));
}
}
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)
{
dump_opt_state (opt_state, do_exclude);
*pb = apr_palloc (pool, sizeof (struct parse_baton_t));
/* Read the stream from STDIN. Users can redirect a file. */
SVN_ERR (create_stdio_stream (&((*pb)->in_stream),
apr_file_open_stdin, pool));
/* Have the parser dump results to STDOUT. Users can redirect a file. */
SVN_ERR (create_stdio_stream (&((*pb)->out_stream),
apr_file_open_stdout, pool));
(*pb)->do_exclude = do_exclude;
(*pb)->do_renumber_revs = opt_state->renumber_revs;
(*pb)->drop_empty_revs = opt_state->drop_empty_revs;
(*pb)->preserve_revprops = opt_state->preserve_revprops;
(*pb)->prefixes = opt_state->prefixes;
/* this may be used to shift revision numbers while filtering */
(*pb)->rev_drop_count = 0;
(*pb)->node_drop_count = 0;
(*pb)->dropped_nodes = apr_array_make (pool, REASONABLE_GUESS,
sizeof (const char *));
(*pb)->renumber_history = apr_hash_make (pool);
SVN_ERR (svn_stream_printf ((*pb)->out_stream, pool,
SVN_REPOS_DUMPFILE_MAGIC_HEADER ": %d\n\n",
SVN_REPOS_DUMPFILE_FORMAT_VERSION));
return SVN_NO_ERROR;
}
/* This implements `help` subcommand. */
static svn_error_t *
subcommand_help (apr_getopt_t *os, void *baton, apr_pool_t *pool)
{
const char *header =
"general usage: svndumpfilter SUBCOMMAND [ARGS & OPTIONS ...]\n"
"Type \"svndumpfilter help <subcommand>\" for help on a "
"specific subcommand.\n"
"\n"
"Available subcommands:\n";
SVN_ERR (svn_opt_print_help (os, "svndumpfilter", FALSE, FALSE, NULL,
header, cmd_table, options_table, NULL,
pool));
return SVN_NO_ERROR;
}
/* This implements `exclude' subcommand. */
static svn_error_t *
subcommand_exclude (apr_getopt_t *os, void *baton, apr_pool_t *pool)
{
struct svndumpfilter_opt_state *opt_state = baton;
struct parse_baton_t *pb;
SVN_ERR (parse_baton_initialize (&pb, opt_state, TRUE, pool));
SVN_ERR (svn_repos_parse_dumpstream (pb->in_stream,
&filtering_vtable, pb, pool));
fprintf (stderr, "\n Dropped %d revisions, %d nodes\n",
pb->rev_drop_count, pb->node_drop_count);
if (pb->do_renumber_revs)
{
fprintf (stderr, "\nRenumber history:\n");
lx_dump_2revnum_ht (pool, pb->renumber_history);
}
if (pb->node_drop_count > 0)
{
fprintf (stderr, "\n\nDropped nodes list:\n");
lx_dump_cstring_ary (pb->dropped_nodes);
}
else
{
fprintf (stderr, "\n\nNo nodes dropped.\n");
}
return SVN_NO_ERROR;
}
/* This implements `include` subcommand. */
static svn_error_t *
subcommand_include (apr_getopt_t *os, void *baton, apr_pool_t *pool)
{
struct svndumpfilter_opt_state *opt_state = baton;
struct parse_baton_t *pb;
SVN_ERR (parse_baton_initialize (&pb, opt_state, FALSE, pool));
SVN_ERR (svn_repos_parse_dumpstream (pb->in_stream,
&filtering_vtable, pb, pool));
fprintf (stderr, "\n Dropped %d revisions, %d nodes\n",
pb->rev_drop_count, pb->node_drop_count);
if (pb->do_renumber_revs)
{
fprintf (stderr, "\nRenumber history:\n");
lx_dump_2revnum_ht (pool, pb->renumber_history);
}
if (pb->node_drop_count > 0)
{
fprintf (stderr, "\n\nDropped nodes list:\n");
lx_dump_cstring_ary (pb->dropped_nodes);
}
else
{
fprintf (stderr, "\n\nNo nodes dropped.\n");
}
return SVN_NO_ERROR;
}
/** Main. **/
int
main (int argc, const char * const *argv)
{
svn_error_t *err;
apr_status_t apr_err;
apr_allocator_t *allocator;
apr_pool_t *pool;
const svn_opt_subcommand_desc_t *subcommand = NULL;
struct svndumpfilter_opt_state opt_state;
apr_getopt_t *os;
int opt_id;
int received_opts[SVN_OPT_MAX_OPTIONS];
int i, num_opts = 0;
/* Initialize the app. */
if (svn_cmdline_init ("svndumpfilter", stderr) != EXIT_SUCCESS)
return EXIT_FAILURE;
/* Create our top-level pool. Use a seperate mutexless allocator,
* given this application is single threaded.
*/
if (apr_allocator_create (&allocator))
return EXIT_FAILURE;
apr_allocator_max_free_set (allocator, SVN_ALLOCATOR_RECOMMENDED_MAX_FREE);
pool = svn_pool_create_ex (NULL, allocator);
apr_allocator_owner_set (allocator, pool);
if (argc <= 1)
{
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. */
apr_getopt_init (&os, pool, argc, argv);
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)
{
subcommand_help (NULL, NULL, pool);
svn_pool_destroy (pool);
return EXIT_FAILURE;
}
/* Stash the option code in an array before parsing it. */
received_opts[num_opts] = opt_id;
num_opts++;
switch (opt_id)
{
case 'h':
case '?':
opt_state.help = TRUE;
break;
case svndumpfilter__drop_empty_revs:
opt_state.drop_empty_revs = TRUE;
break;
case svndumpfilter__renumber_revs:
opt_state.renumber_revs = TRUE;
break;
case svndumpfilter__preserve_revprops:
opt_state.preserve_revprops = TRUE;
break;
default:
{
subcommand_help (NULL, NULL, pool);
svn_pool_destroy (pool);
return EXIT_FAILURE;
}
} /* close `switch' */
} /* close `while' */
/* 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_subcommand (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)
{
fprintf (stderr, "subcommand argument required\n");
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_subcommand (cmd_table, first_arg);
if (subcommand == NULL)
{
fprintf (stderr, "unknown command: '%s'\n", first_arg);
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)
{
if (os->ind < os->argc)
{
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_utf8;
SVN_INT_ERR (svn_utf_cstring_to_utf8 (&prefix_utf8,
os->argv[i],
pool));
*(const char **)apr_array_push (opt_state.prefixes)
= svn_path_internal_style (prefix_utf8, pool);
}
}
else
{
fprintf (stderr,
"\nError: no prefixes supplied.\n");
svn_opt_subcommand_help (subcommand->name,
cmd_table,
options_table,
pool);
svn_pool_destroy (pool);
return EXIT_FAILURE;
}
}
/* Check that the subcommand wasn't passed any inappropriate options. */
for (i = 0; i < num_opts; i++)
{
opt_id = received_opts[i];
/* 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_option (subcommand, opt_id))
{
const char *optstr;
const apr_getopt_option_t *badopt =
svn_opt_get_option_from_code (opt_id, options_table);
svn_opt_format_option (&optstr, badopt, FALSE, pool);
fprintf (stderr,
"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)
{
if (err->apr_err == SVN_ERR_CL_ARG_PARSING_ERROR)
{
svn_handle_error (err, stderr, 0);
svn_opt_subcommand_help (subcommand->name, cmd_table,
options_table, pool);
}
else
svn_handle_error (err, stderr, 0);
svn_pool_destroy (pool);
return EXIT_FAILURE;
}
else
{
svn_pool_destroy (pool);
return EXIT_SUCCESS;
}
}