2 * main.c: Subversion dump stream filtering tool.
4 * ====================================================================
5 * Copyright (c) 2000-2006 CollabNet. All rights reserved.
7 * This software is licensed as described in the file COPYING, which
8 * you should have received as part of this distribution. The terms
9 * are also available at http://subversion.tigris.org/license-1.html.
10 * If newer versions of this license are posted there, you may use a
11 * newer version instead, at your option.
13 * This software consists of voluntary contributions made by many
14 * individuals. For exact contribution history, see the revision
15 * history and logs, available at http://subversion.tigris.org/.
16 * ====================================================================
22 #include <apr_file_io.h>
24 #include "svn_private_config.h"
25 #include "svn_cmdline.h"
26 #include "svn_error.h"
27 #include "svn_string.h"
32 #include "svn_repos.h"
34 #include "svn_pools.h"
35 #include "svn_sorts.h"
36 #include "svn_props.h"
37 #include "svn_mergeinfo.h"
39 #include "private/svn_mergeinfo_private.h"
44 /* Helper to open stdio streams */
46 /* NOTE: we used to call svn_stream_from_stdio(), which wraps a stream
47 around a standard stdio.h FILE pointer. The problem is that these
48 pointers operate through C Run Time (CRT) on Win32, which does all
49 sorts of translation on them: LF's become CRLF's, and ctrl-Z's
50 embedded in Word documents are interpreted as premature EOF's.
52 So instead, we use apr_file_open_std*, which bypass the CRT and
53 directly wrap the OS's file-handles, which don't know or care about
54 translation. Thus dump/load works correctly on Win32.
57 create_stdio_stream(svn_stream_t
**stream
,
58 APR_DECLARE(apr_status_t
) open_fn(apr_file_t
**,
62 apr_file_t
*stdio_file
;
63 apr_status_t apr_err
= open_fn(&stdio_file
, pool
);
66 return svn_error_wrap_apr(apr_err
, _("Can't open stdio file"));
68 *stream
= svn_stream_from_aprfile(stdio_file
, pool
);
73 /* Writes a property in dumpfile format to given stringbuf. */
75 write_prop_to_stringbuf(svn_stringbuf_t
**strbuf
,
77 const svn_string_t
*value
)
79 int bytes_used
, namelen
;
80 char buf
[SVN_KEYLINE_MAXLEN
];
82 /* Output name length, then name. */
83 namelen
= strlen(name
);
84 svn_stringbuf_appendbytes(*strbuf
, "K ", 2);
86 bytes_used
= sprintf(buf
, "%d", namelen
);
87 svn_stringbuf_appendbytes(*strbuf
, buf
, bytes_used
);
88 svn_stringbuf_appendbytes(*strbuf
, "\n", 1);
90 svn_stringbuf_appendbytes(*strbuf
, name
, namelen
);
91 svn_stringbuf_appendbytes(*strbuf
, "\n", 1);
93 /* Output value length, then value. */
94 svn_stringbuf_appendbytes(*strbuf
, "V ", 2);
96 bytes_used
= sprintf(buf
, "%" APR_SIZE_T_FMT
, value
->len
);
97 svn_stringbuf_appendbytes(*strbuf
, buf
, bytes_used
);
98 svn_stringbuf_appendbytes(*strbuf
, "\n", 1);
100 svn_stringbuf_appendbytes(*strbuf
, value
->data
, value
->len
);
101 svn_stringbuf_appendbytes(*strbuf
, "\n", 1);
105 /* Prefix matching function to compare node-path with set of prefixes. */
107 ary_prefix_match(apr_array_header_t
*pfxlist
, const char *path
)
109 int i
, pfx_len
, path_len
= strlen(path
);
112 for (i
= 0; i
< pfxlist
->nelts
; i
++)
114 pfx
= APR_ARRAY_IDX(pfxlist
, i
, const char *);
115 pfx_len
= strlen(pfx
);
116 if (path_len
< pfx_len
)
118 if (strncmp(path
, pfx
, pfx_len
) == 0)
126 /* Check whether we need to skip this PATH based on its presence in
127 the PREFIXES list, and the DO_EXCLUDE option. */
128 static APR_INLINE svn_boolean_t
129 skip_path(const char *path
, apr_array_header_t
*prefixes
,
130 svn_boolean_t do_exclude
)
133 return (ary_prefix_match(prefixes
, path
) ? do_exclude
: !do_exclude
);
138 /* Note: the input stream parser calls us with events.
139 Output of the filtered dump occurs for the most part streamily with the
140 event callbacks, to avoid caching large quantities of data in memory.
141 The exceptions this are:
142 - All revision data (headers and props) must be cached until a non-skipped
143 node within the revision is found, or the revision is closed.
144 - Node headers and props must be cached until all props have been received
145 (to allow the Prop-content-length to be found). This is signalled either
146 by the node text arriving, or the node being closed.
147 The writing_begun members of the associated object batons track the state.
148 output_revision() and output_node() are called to cause this flushing of
149 cached data to occur.
153 /* Filtering batons */
157 svn_revnum_t rev
; /* Last non-dropped revision to which this maps. */
158 svn_boolean_t was_dropped
; /* Was this revision dropped? */
163 /* Command-line options values. */
164 svn_boolean_t do_exclude
;
166 svn_boolean_t drop_empty_revs
;
167 svn_boolean_t do_renumber_revs
;
168 svn_boolean_t preserve_revprops
;
169 svn_boolean_t skip_missing_merge_sources
;
170 apr_array_header_t
*prefixes
;
172 /* Input and output streams. */
173 svn_stream_t
*in_stream
;
174 svn_stream_t
*out_stream
;
176 /* State for the filtering process. */
177 apr_int32_t rev_drop_count
;
178 apr_hash_t
*dropped_nodes
;
179 apr_hash_t
*renumber_history
; /* svn_revnum_t -> struct revmap_t */
180 svn_revnum_t last_live_revision
;
183 struct revision_baton_t
185 /* Reference to the global parse baton. */
186 struct parse_baton_t
*pb
;
188 /* Does this revision have node or prop changes? */
189 svn_boolean_t has_nodes
;
190 svn_boolean_t has_props
;
192 /* Did we drop any nodes? */
193 svn_boolean_t had_dropped_nodes
;
195 /* Written to output stream? */
196 svn_boolean_t writing_begun
;
198 /* The original and new (re-mapped) revision numbers. */
199 svn_revnum_t rev_orig
;
200 svn_revnum_t rev_actual
;
202 /* Pointers to dumpfile data. */
203 svn_stringbuf_t
*header
;
209 /* Reference to the current revision baton. */
210 struct revision_baton_t
*rb
;
212 /* Are we skipping this node? */
213 svn_boolean_t do_skip
;
215 /* Have we been instructed to change or remove props on, or change
216 the text of, this node? */
217 svn_boolean_t has_props
;
218 svn_boolean_t has_text
;
220 /* Written to output stream? */
221 svn_boolean_t writing_begun
;
223 /* The text content length according to the dumpfile headers, because we
224 need the length before we have the actual text. */
227 /* Pointers to dumpfile data. */
228 svn_stringbuf_t
*header
;
229 svn_stringbuf_t
*props
;
234 /* Filtering vtable members */
236 /* New revision: set up revision_baton, decide if we skip it. */
238 new_revision_record(void **revision_baton
,
243 struct revision_baton_t
*rb
;
244 apr_hash_index_t
*hi
;
247 svn_stream_t
*header_stream
;
249 *revision_baton
= apr_palloc(pool
, sizeof(struct revision_baton_t
));
250 rb
= *revision_baton
;
251 rb
->pb
= parse_baton
;
252 rb
->has_nodes
= FALSE
;
253 rb
->has_props
= FALSE
;
254 rb
->had_dropped_nodes
= FALSE
;
255 rb
->writing_begun
= FALSE
;
256 rb
->header
= svn_stringbuf_create("", pool
);
257 rb
->props
= apr_hash_make(pool
);
259 header_stream
= svn_stream_from_stringbuf(rb
->header
, pool
);
261 val
= apr_hash_get(headers
, SVN_REPOS_DUMPFILE_REVISION_NUMBER
,
262 APR_HASH_KEY_STRING
);
263 rb
->rev_orig
= SVN_STR_TO_REV(val
);
265 if (rb
->pb
->do_renumber_revs
)
266 rb
->rev_actual
= rb
->rev_orig
- rb
->pb
->rev_drop_count
;
268 rb
->rev_actual
= rb
->rev_orig
;
270 SVN_ERR(svn_stream_printf(header_stream
, pool
,
271 SVN_REPOS_DUMPFILE_REVISION_NUMBER
": %ld\n",
274 for (hi
= apr_hash_first(pool
, headers
); hi
; hi
= apr_hash_next(hi
))
276 apr_hash_this(hi
, &key
, NULL
, &val
);
277 if ((!strcmp(key
, SVN_REPOS_DUMPFILE_CONTENT_LENGTH
))
278 || (!strcmp(key
, SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH
))
279 || (!strcmp(key
, SVN_REPOS_DUMPFILE_REVISION_NUMBER
)))
282 /* passthru: put header into header stringbuf. */
284 SVN_ERR(svn_stream_printf(header_stream
, pool
, "%s: %s\n",
289 SVN_ERR(svn_stream_close(header_stream
));
295 /* Output revision to dumpstream
296 This may be called by new_node_record(), iff rb->has_nodes has been set
297 to TRUE, or by close_revision() otherwise. This must only be called
298 if rb->writing_begun is FALSE. */
300 output_revision(struct revision_baton_t
*rb
)
303 char buf
[SVN_KEYLINE_MAXLEN
];
304 apr_hash_index_t
*hi
;
305 apr_pool_t
*hash_pool
= apr_hash_pool_get(rb
->props
);
306 svn_stringbuf_t
*props
= svn_stringbuf_create("", hash_pool
);
307 apr_pool_t
*subpool
= svn_pool_create(hash_pool
);
309 rb
->writing_begun
= TRUE
;
311 /* If this revision has no nodes left because the ones it had were
312 dropped, and we are not dropping empty revisions, and we were not
313 told to preserve revision props, then we want to fixup the
314 revision props to only contain:
316 - a log message that reports that this revision is just stuffing. */
317 if ((! rb
->pb
->preserve_revprops
)
319 && rb
->had_dropped_nodes
320 && (! rb
->pb
->drop_empty_revs
))
322 apr_hash_t
*old_props
= rb
->props
;
323 rb
->has_props
= TRUE
;
324 rb
->props
= apr_hash_make(hash_pool
);
325 apr_hash_set(rb
->props
, SVN_PROP_REVISION_DATE
, APR_HASH_KEY_STRING
,
326 apr_hash_get(old_props
, SVN_PROP_REVISION_DATE
,
327 APR_HASH_KEY_STRING
));
328 apr_hash_set(rb
->props
, SVN_PROP_REVISION_LOG
, APR_HASH_KEY_STRING
,
329 svn_string_create(_("This is an empty revision for "
330 "padding."), hash_pool
));
333 /* Now, "rasterize" the props to a string, and append the property
334 information to the header string. */
337 for (hi
= apr_hash_first(subpool
, rb
->props
);
339 hi
= apr_hash_next(hi
))
343 apr_hash_this(hi
, &key
, NULL
, &val
);
344 write_prop_to_stringbuf(&props
, key
, val
);
346 svn_stringbuf_appendcstr(props
, "PROPS-END\n");
347 svn_stringbuf_appendcstr(rb
->header
,
348 SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH
);
349 bytes_used
= sprintf(buf
, ": %" APR_SIZE_T_FMT
, props
->len
);
350 svn_stringbuf_appendbytes(rb
->header
, buf
, bytes_used
);
351 svn_stringbuf_appendbytes(rb
->header
, "\n", 1);
354 svn_stringbuf_appendcstr(rb
->header
, SVN_REPOS_DUMPFILE_CONTENT_LENGTH
);
355 bytes_used
= sprintf(buf
, ": %" APR_SIZE_T_FMT
, props
->len
);
356 svn_stringbuf_appendbytes(rb
->header
, buf
, bytes_used
);
357 svn_stringbuf_appendbytes(rb
->header
, "\n", 1);
359 /* put an end to headers */
360 svn_stringbuf_appendbytes(rb
->header
, "\n", 1);
362 /* put an end to revision */
363 svn_stringbuf_appendbytes(props
, "\n", 1);
365 /* write out the revision */
366 /* Revision is written out in the following cases:
367 1. No --drop-empty-revs has been supplied.
368 2. --drop-empty-revs has been supplied,
369 but revision has not all nodes dropped
370 3. Revision had no nodes to begin with.
373 || (! rb
->pb
->drop_empty_revs
)
374 || (! rb
->had_dropped_nodes
))
376 /* This revision is a keeper. */
377 SVN_ERR(svn_stream_write(rb
->pb
->out_stream
,
378 rb
->header
->data
, &(rb
->header
->len
)));
379 SVN_ERR(svn_stream_write(rb
->pb
->out_stream
,
380 props
->data
, &(props
->len
)));
382 if (rb
->pb
->do_renumber_revs
)
384 svn_revnum_t
*rr_key
;
385 struct revmap_t
*rr_val
;
386 apr_pool_t
*rr_pool
= apr_hash_pool_get(rb
->pb
->renumber_history
);
387 rr_key
= apr_palloc(rr_pool
, sizeof(*rr_key
));
388 rr_val
= apr_palloc(rr_pool
, sizeof(*rr_val
));
389 *rr_key
= rb
->rev_orig
;
390 rr_val
->rev
= rb
->rev_actual
;
391 rr_val
->was_dropped
= FALSE
;
392 apr_hash_set(rb
->pb
->renumber_history
, rr_key
,
393 sizeof(*rr_key
), rr_val
);
394 rb
->pb
->last_live_revision
= rb
->rev_actual
;
398 SVN_ERR(svn_cmdline_fprintf(stderr
, subpool
,
399 _("Revision %ld committed as %ld.\n"),
400 rb
->rev_orig
, rb
->rev_actual
));
404 /* We're dropping this revision. */
405 rb
->pb
->rev_drop_count
++;
406 if (rb
->pb
->do_renumber_revs
)
408 svn_revnum_t
*rr_key
;
409 struct revmap_t
*rr_val
;
410 apr_pool_t
*rr_pool
= apr_hash_pool_get(rb
->pb
->renumber_history
);
411 rr_key
= apr_palloc(rr_pool
, sizeof(*rr_key
));
412 rr_val
= apr_palloc(rr_pool
, sizeof(*rr_val
));
413 *rr_key
= rb
->rev_orig
;
414 rr_val
->rev
= rb
->pb
->last_live_revision
;
415 rr_val
->was_dropped
= TRUE
;
416 apr_hash_set(rb
->pb
->renumber_history
, rr_key
,
417 sizeof(*rr_key
), rr_val
);
421 SVN_ERR(svn_cmdline_fprintf(stderr
, subpool
,
422 _("Revision %ld skipped.\n"),
425 svn_pool_destroy(subpool
);
430 /* UUID record here: dump it, as we do not filter them. */
432 uuid_record(const char *uuid
, void *parse_baton
, apr_pool_t
*pool
)
434 struct parse_baton_t
*pb
= parse_baton
;
435 SVN_ERR(svn_stream_printf(pb
->out_stream
, pool
,
436 SVN_REPOS_DUMPFILE_UUID
": %s\n\n", uuid
));
441 /* New node here. Set up node_baton by copying headers. */
443 new_node_record(void **node_baton
,
448 struct parse_baton_t
*pb
;
449 struct node_baton_t
*nb
;
450 char *node_path
, *copyfrom_path
;
451 apr_hash_index_t
*hi
;
456 *node_baton
= apr_palloc(pool
, sizeof(struct node_baton_t
));
461 node_path
= apr_hash_get(headers
, SVN_REPOS_DUMPFILE_NODE_PATH
,
462 APR_HASH_KEY_STRING
);
463 copyfrom_path
= apr_hash_get(headers
,
464 SVN_REPOS_DUMPFILE_NODE_COPYFROM_PATH
,
465 APR_HASH_KEY_STRING
);
467 /* Ensure that paths start with a leading '/'. */
468 node_path
= svn_path_join("/", node_path
, pool
);
470 copyfrom_path
= svn_path_join("/", copyfrom_path
, pool
);
472 nb
->do_skip
= skip_path(node_path
, pb
->prefixes
, pb
->do_exclude
);
474 /* If we're skipping the node, take note of path, discarding the
478 apr_hash_set(pb
->dropped_nodes
,
479 apr_pstrdup(apr_hash_pool_get(pb
->dropped_nodes
),
481 APR_HASH_KEY_STRING
, (void *)1);
482 nb
->rb
->had_dropped_nodes
= TRUE
;
486 tcl
= apr_hash_get(headers
, SVN_REPOS_DUMPFILE_TEXT_CONTENT_LENGTH
,
487 APR_HASH_KEY_STRING
);
489 /* Test if this node was copied from dropped source. */
491 skip_path(copyfrom_path
, pb
->prefixes
, pb
->do_exclude
))
493 /* This node was copied from a dropped source.
494 We have a problem, since we did not want to drop this node too.
496 However, there is one special case we'll handle. If the node is
497 a file, and this was a copy-and-modify operation, then the
498 dumpfile should contain the new contents of the file. In this
499 scenario, we'll just do an add without history using the new
502 kind
= apr_hash_get(headers
, SVN_REPOS_DUMPFILE_NODE_KIND
,
503 APR_HASH_KEY_STRING
);
505 /* If there is a Text-content-length header, and the kind is
506 "file", we just fallback to an add without history. */
507 if (tcl
&& (strcmp(kind
, "file") == 0))
509 apr_hash_set(headers
, SVN_REPOS_DUMPFILE_NODE_COPYFROM_PATH
,
510 APR_HASH_KEY_STRING
, NULL
);
511 apr_hash_set(headers
, SVN_REPOS_DUMPFILE_NODE_COPYFROM_REV
,
512 APR_HASH_KEY_STRING
, NULL
);
513 copyfrom_path
= NULL
;
515 /* Else, this is either a directory or a file whose contents we
516 don't have readily available. */
519 return svn_error_createf
520 (SVN_ERR_INCOMPLETE_DATA
, 0,
521 _("Invalid copy source path '%s'"), copyfrom_path
);
525 nb
->has_props
= FALSE
;
526 nb
->has_text
= FALSE
;
527 nb
->writing_begun
= FALSE
;
528 nb
->tcl
= tcl
? svn__atoui64(tcl
) : 0;
529 nb
->header
= svn_stringbuf_create("", pool
);
530 nb
->props
= svn_stringbuf_create("", pool
);
532 /* Now we know for sure that we have a node that will not be
533 skipped, flush the revision if it has not already been done. */
534 nb
->rb
->has_nodes
= TRUE
;
535 if (! nb
->rb
->writing_begun
)
536 SVN_ERR(output_revision(nb
->rb
));
538 for (hi
= apr_hash_first(pool
, headers
); hi
; hi
= apr_hash_next(hi
))
540 apr_hash_this(hi
, (const void **) &key
, NULL
, &val
);
541 if ((!strcmp(key
, SVN_REPOS_DUMPFILE_CONTENT_LENGTH
))
542 || (!strcmp(key
, SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH
))
543 || (!strcmp(key
, SVN_REPOS_DUMPFILE_TEXT_CONTENT_LENGTH
)))
546 /* Rewrite Node-Copyfrom-Rev if we are renumbering revisions.
547 The number points to some revision in the past. We keep track
548 of revision renumbering in an apr_hash, which maps original
549 revisions to new ones. Dropped revision are mapped to -1.
550 This should never happen here.
552 if (pb
->do_renumber_revs
553 && (!strcmp(key
, SVN_REPOS_DUMPFILE_NODE_COPYFROM_REV
)))
555 svn_revnum_t cf_orig_rev
;
556 struct revmap_t
*cf_renum_val
;
558 cf_orig_rev
= SVN_STR_TO_REV(val
);
559 cf_renum_val
= apr_hash_get(pb
->renumber_history
,
561 sizeof(svn_revnum_t
));
562 if (! (cf_renum_val
&& SVN_IS_VALID_REVNUM(cf_renum_val
->rev
)))
563 return svn_error_createf
564 (SVN_ERR_NODE_UNEXPECTED_KIND
, NULL
,
565 _("No valid copyfrom revision in filtered stream"));
566 SVN_ERR(svn_stream_printf
567 (nb
->rb
->pb
->out_stream
, pool
,
568 SVN_REPOS_DUMPFILE_NODE_COPYFROM_REV
": %ld\n",
573 /* passthru: put header straight to output */
575 SVN_ERR(svn_stream_printf(nb
->rb
->pb
->out_stream
,
586 /* Output node header and props to dumpstream
587 This will be called by set_fulltext() after setting nb->has_text to TRUE,
588 if the node has any text, or by close_node() otherwise. This must only
589 be called if nb->writing_begun is FALSE. */
591 output_node(struct node_baton_t
*nb
)
594 char buf
[SVN_KEYLINE_MAXLEN
];
596 nb
->writing_begun
= TRUE
;
598 /* when there are no props nb->props->len would be zero and won't mess up
601 svn_stringbuf_appendcstr(nb
->props
, "PROPS-END\n");
603 /* 1. recalculate & check text-md5 if present. Passed through right now. */
605 /* 2. recalculate and add content-lengths */
609 svn_stringbuf_appendcstr(nb
->header
,
610 SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH
);
611 bytes_used
= sprintf(buf
, ": %" APR_SIZE_T_FMT
, nb
->props
->len
);
612 svn_stringbuf_appendbytes(nb
->header
, buf
, bytes_used
);
613 svn_stringbuf_appendbytes(nb
->header
, "\n", 1);
617 svn_stringbuf_appendcstr(nb
->header
,
618 SVN_REPOS_DUMPFILE_TEXT_CONTENT_LENGTH
);
619 bytes_used
= sprintf(buf
, ": %" SVN_FILESIZE_T_FMT
, nb
->tcl
);
620 svn_stringbuf_appendbytes(nb
->header
, buf
, bytes_used
);
621 svn_stringbuf_appendbytes(nb
->header
, "\n", 1);
623 svn_stringbuf_appendcstr(nb
->header
, SVN_REPOS_DUMPFILE_CONTENT_LENGTH
);
624 bytes_used
= sprintf(buf
, ": %" SVN_FILESIZE_T_FMT
,
625 (svn_filesize_t
) (nb
->props
->len
+ nb
->tcl
));
626 svn_stringbuf_appendbytes(nb
->header
, buf
, bytes_used
);
627 svn_stringbuf_appendbytes(nb
->header
, "\n", 1);
629 /* put an end to headers */
630 svn_stringbuf_appendbytes(nb
->header
, "\n", 1);
632 /* 3. output all the stuff */
634 SVN_ERR(svn_stream_write(nb
->rb
->pb
->out_stream
,
635 nb
->header
->data
, &(nb
->header
->len
)));
636 SVN_ERR(svn_stream_write(nb
->rb
->pb
->out_stream
,
637 nb
->props
->data
, &(nb
->props
->len
)));
643 /* Examine the mergeinfo in INITIAL_VAL, omitting missing merge
644 sources or renumbering revisions in rangelists as appropriate, and
645 return the (possibly new) mergeinfo in *FINAL_VAL (allocated from
648 adjust_mergeinfo(svn_string_t
**final_val
, const svn_string_t
*initial_val
,
649 struct revision_baton_t
*rb
, apr_pool_t
*pool
)
651 apr_hash_t
*mergeinfo
;
652 apr_hash_t
*final_mergeinfo
= apr_hash_make(pool
);
653 apr_hash_index_t
*hi
;
654 apr_pool_t
*subpool
= svn_pool_create(pool
);
656 SVN_ERR(svn_mergeinfo_parse(&mergeinfo
, initial_val
->data
, subpool
));
657 for (hi
= apr_hash_first(NULL
, mergeinfo
); hi
; hi
= apr_hash_next(hi
))
659 const char *merge_source
;
660 apr_array_header_t
*rangelist
;
661 struct parse_baton_t
*pb
= rb
->pb
;
666 apr_hash_this(hi
, &key
, NULL
, &val
);
667 merge_source
= (const char *) key
;
668 rangelist
= (apr_array_header_t
*) val
;
670 /* Determine whether the merge_source is a part of the prefix. */
671 if (skip_path(merge_source
, pb
->prefixes
, pb
->do_exclude
))
673 if (pb
->skip_missing_merge_sources
)
676 return svn_error_createf(SVN_ERR_INCOMPLETE_DATA
, 0,
677 _("Missing merge source path '%s'; try "
678 "with --skip-missing-merge-sources"),
682 /* Possibly renumber revisions in merge source's rangelist. */
683 if (pb
->do_renumber_revs
)
685 for (i
= 0; i
< rangelist
->nelts
; i
++)
687 struct revmap_t
*revmap_start
;
688 struct revmap_t
*revmap_end
;
689 svn_merge_range_t
*range
= APR_ARRAY_IDX(rangelist
, i
,
690 svn_merge_range_t
*);
692 revmap_start
= apr_hash_get(pb
->renumber_history
,
693 &range
->start
, sizeof(svn_revnum_t
));
694 if (! (revmap_start
&& SVN_IS_VALID_REVNUM(revmap_start
->rev
)))
695 return svn_error_createf
696 (SVN_ERR_NODE_UNEXPECTED_KIND
, NULL
,
697 _("No valid revision range 'start' in filtered stream"));
699 revmap_end
= apr_hash_get(pb
->renumber_history
,
700 &range
->end
, sizeof(svn_revnum_t
));
701 if (! (revmap_end
&& SVN_IS_VALID_REVNUM(revmap_end
->rev
)))
702 return svn_error_createf
703 (SVN_ERR_NODE_UNEXPECTED_KIND
, NULL
,
704 _("No valid revision range 'end' in filtered stream"));
706 range
->start
= revmap_start
->rev
;
707 range
->end
= revmap_end
->rev
;
709 apr_hash_set(final_mergeinfo
, merge_source
,
710 APR_HASH_KEY_STRING
, rangelist
);
714 SVN_ERR(svn_mergeinfo_sort(final_mergeinfo
, subpool
));
715 SVN_ERR(svn_mergeinfo_to_string(final_val
, final_mergeinfo
, pool
));
716 svn_pool_destroy(subpool
);
723 set_revision_property(void *revision_baton
,
725 const svn_string_t
*value
)
727 struct revision_baton_t
*rb
= revision_baton
;
728 apr_pool_t
*hash_pool
= apr_hash_pool_get(rb
->props
);
730 rb
->has_props
= TRUE
;
731 apr_hash_set(rb
->props
, apr_pstrdup(hash_pool
, name
),
732 APR_HASH_KEY_STRING
, svn_string_dup(value
, hash_pool
));
738 set_node_property(void *node_baton
,
740 const svn_string_t
*value
)
742 struct node_baton_t
*nb
= node_baton
;
743 struct revision_baton_t
*rb
= nb
->rb
;
749 return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE
, NULL
,
750 _("Delta property block detected - "
751 "not supported by svndumpfilter"));
753 if (strcmp(name
, SVN_PROP_MERGEINFO
) == 0)
755 svn_string_t
*filtered_mergeinfo
; /* Avoid compiler warning. */
756 apr_pool_t
*pool
= apr_hash_pool_get(rb
->props
);
757 SVN_ERR(adjust_mergeinfo(&filtered_mergeinfo
, value
, rb
, pool
));
758 value
= filtered_mergeinfo
;
761 write_prop_to_stringbuf(&(nb
->props
), name
, value
);
768 remove_node_props(void *node_baton
)
770 struct node_baton_t
*nb
= node_baton
;
772 /* In this case, not actually indicating that the node *has* props,
773 rather that we know about all the props that it has, since it now
775 nb
->has_props
= TRUE
;
782 set_fulltext(svn_stream_t
**stream
, void *node_baton
)
784 struct node_baton_t
*nb
= node_baton
;
789 if (! nb
->writing_begun
)
790 SVN_ERR(output_node(nb
));
791 *stream
= nb
->rb
->pb
->out_stream
;
800 close_node(void *node_baton
)
802 struct node_baton_t
*nb
= node_baton
;
805 /* Get out of here if we can. */
809 /* If the node was not flushed already to output its text, do it now. */
810 if (! nb
->writing_begun
)
811 SVN_ERR(output_node(nb
));
813 /* put an end to node. */
814 SVN_ERR(svn_stream_write(nb
->rb
->pb
->out_stream
, "\n\n", &len
));
820 /* Finalize revision */
822 close_revision(void *revision_baton
)
824 struct revision_baton_t
*rb
= revision_baton
;
826 /* If no node has yet flushed the revision, do it now. */
827 if (! rb
->writing_begun
)
828 return output_revision(rb
);
834 /* Filtering vtable */
835 svn_repos_parser_fns2_t filtering_vtable
=
840 set_revision_property
,
854 static svn_opt_subcommand_t
861 svndumpfilter__drop_empty_revs
= SVN_OPT_FIRST_LONGOPT_ID
,
862 svndumpfilter__renumber_revs
,
863 svndumpfilter__preserve_revprops
,
864 svndumpfilter__skip_missing_merge_sources
,
865 svndumpfilter__quiet
,
866 svndumpfilter__version
869 /* Option codes and descriptions.
871 * The entire list must be terminated with an entry of nulls.
873 static const apr_getopt_option_t options_table
[] =
876 N_("show help on a subcommand")},
879 N_("show help on a subcommand")},
881 {"version", svndumpfilter__version
, 0,
882 N_("show program version information") },
883 {"quiet", svndumpfilter__quiet
, 0,
884 N_("Do not display filtering statistics.") },
885 {"drop-empty-revs", svndumpfilter__drop_empty_revs
, 0,
886 N_("Remove revisions emptied by filtering.")},
887 {"renumber-revs", svndumpfilter__renumber_revs
, 0,
888 N_("Renumber revisions left after filtering.") },
889 {"skip-missing-merge-sources",
890 svndumpfilter__skip_missing_merge_sources
, 0,
891 N_("Skip missing merge sources.") },
892 {"preserve-revprops", svndumpfilter__preserve_revprops
, 0,
893 N_("Don't filter revision properties.") },
898 /* Array of available subcommands.
899 * The entire list must be terminated with an entry of nulls.
901 static const svn_opt_subcommand_desc_t cmd_table
[] =
903 {"exclude", subcommand_exclude
, {0},
904 N_("Filter out nodes with given prefixes from dumpstream.\n"
905 "usage: svndumpfilter exclude PATH_PREFIX...\n"),
906 {svndumpfilter__drop_empty_revs
, svndumpfilter__renumber_revs
,
907 svndumpfilter__skip_missing_merge_sources
,
908 svndumpfilter__preserve_revprops
, svndumpfilter__quiet
} },
910 {"include", subcommand_include
, {0},
911 N_("Filter out nodes without given prefixes from dumpstream.\n"
912 "usage: svndumpfilter include PATH_PREFIX...\n"),
913 {svndumpfilter__drop_empty_revs
, svndumpfilter__renumber_revs
,
914 svndumpfilter__skip_missing_merge_sources
,
915 svndumpfilter__preserve_revprops
, svndumpfilter__quiet
} },
917 {"help", subcommand_help
, {"?", "h"},
918 N_("Describe the usage of this program or its subcommands.\n"
919 "usage: svndumpfilter help [SUBCOMMAND...]\n"),
922 { NULL
, NULL
, {0}, NULL
, {0} }
926 /* Baton for passing option/argument state to a subcommand function. */
927 struct svndumpfilter_opt_state
929 svn_opt_revision_t start_revision
; /* -r X[:Y] is */
930 svn_opt_revision_t end_revision
; /* not implemented. */
931 svn_boolean_t quiet
; /* --quiet */
932 svn_boolean_t version
; /* --version */
933 svn_boolean_t drop_empty_revs
; /* --drop-empty-revs */
934 svn_boolean_t help
; /* --help or -? */
935 svn_boolean_t renumber_revs
; /* --renumber-revs */
936 svn_boolean_t preserve_revprops
; /* --preserve-revprops */
937 svn_boolean_t skip_missing_merge_sources
;
938 /* --skip-missing-merge-sources */
939 apr_array_header_t
*prefixes
; /* mainargs. */
944 parse_baton_initialize(struct parse_baton_t
**pb
,
945 struct svndumpfilter_opt_state
*opt_state
,
946 svn_boolean_t do_exclude
,
949 struct parse_baton_t
*baton
= apr_palloc(pool
, sizeof(*baton
));
951 /* Read the stream from STDIN. Users can redirect a file. */
952 SVN_ERR(create_stdio_stream(&(baton
->in_stream
),
953 apr_file_open_stdin
, pool
));
955 /* Have the parser dump results to STDOUT. Users can redirect a file. */
956 SVN_ERR(create_stdio_stream(&(baton
->out_stream
),
957 apr_file_open_stdout
, pool
));
959 baton
->do_exclude
= do_exclude
;
960 baton
->do_renumber_revs
= opt_state
->renumber_revs
;
961 baton
->drop_empty_revs
= opt_state
->drop_empty_revs
;
962 baton
->preserve_revprops
= opt_state
->preserve_revprops
;
963 baton
->quiet
= opt_state
->quiet
;
964 baton
->prefixes
= opt_state
->prefixes
;
965 baton
->skip_missing_merge_sources
= opt_state
->skip_missing_merge_sources
;
966 baton
->rev_drop_count
= 0; /* used to shift revnums while filtering */
967 baton
->dropped_nodes
= apr_hash_make(pool
);
968 baton
->renumber_history
= apr_hash_make(pool
);
969 baton
->last_live_revision
= SVN_INVALID_REVNUM
;
971 /* This is non-ideal: We should pass through the version of the
972 * input dumpstream. However, our API currently doesn't allow that.
973 * Hardcoding version 2 is acceptable because:
974 * - We currently do not accept version 3 or greater.
975 * - Dumpstream version 1 is so ancient as to be ignorable
976 * (0.17.x and earlier)
978 SVN_ERR(svn_stream_printf(baton
->out_stream
, pool
,
979 SVN_REPOS_DUMPFILE_MAGIC_HEADER
": %d\n\n",
986 /* This implements `help` subcommand. */
988 subcommand_help(apr_getopt_t
*os
, void *baton
, apr_pool_t
*pool
)
990 struct svndumpfilter_opt_state
*opt_state
= baton
;
992 _("general usage: svndumpfilter SUBCOMMAND [ARGS & OPTIONS ...]\n"
993 "Type 'svndumpfilter help <subcommand>' for help on a "
994 "specific subcommand.\n"
995 "Type 'svndumpfilter --version' to see the program version.\n"
997 "Available subcommands:\n");
999 SVN_ERR(svn_opt_print_help(os
, "svndumpfilter",
1000 opt_state
? opt_state
->version
: FALSE
,
1002 header
, cmd_table
, options_table
, NULL
,
1005 return SVN_NO_ERROR
;
1009 /* Version compatibility check */
1010 static svn_error_t
*
1011 check_lib_versions(void)
1013 static const svn_version_checklist_t checklist
[] =
1015 { "svn_subr", svn_subr_version
},
1016 { "svn_repos", svn_repos_version
},
1017 { "svn_delta", svn_delta_version
},
1021 SVN_VERSION_DEFINE(my_version
);
1022 return svn_ver_check_list(&my_version
, checklist
);
1026 /* Do the real work of filtering. */
1027 static svn_error_t
*
1028 do_filter(apr_getopt_t
*os
,
1030 svn_boolean_t do_exclude
,
1033 struct svndumpfilter_opt_state
*opt_state
= baton
;
1034 struct parse_baton_t
*pb
;
1035 apr_hash_index_t
*hi
;
1036 apr_array_header_t
*keys
;
1040 if (! opt_state
->quiet
)
1042 apr_pool_t
*subpool
= svn_pool_create(pool
);
1044 SVN_ERR(svn_cmdline_fprintf(stderr
, subpool
,
1046 ? opt_state
->drop_empty_revs
1047 ? _("Excluding (and dropping empty "
1048 "revisions for) prefixes:\n")
1049 : _("Excluding prefixes:\n")
1050 : opt_state
->drop_empty_revs
1051 ? _("Including (and dropping empty "
1052 "revisions for) prefixes:\n")
1053 : _("Including prefixes:\n")));
1055 for (i
= 0; i
< opt_state
->prefixes
->nelts
; i
++)
1057 svn_pool_clear(subpool
);
1058 SVN_ERR(svn_cmdline_fprintf
1059 (stderr
, subpool
, " '%s'\n",
1060 APR_ARRAY_IDX(opt_state
->prefixes
, i
, const char *)));
1063 SVN_ERR(svn_cmdline_fputs("\n", stderr
, subpool
));
1064 svn_pool_destroy(subpool
);
1067 SVN_ERR(parse_baton_initialize(&pb
, opt_state
, do_exclude
, pool
));
1068 SVN_ERR(svn_repos_parse_dumpstream2(pb
->in_stream
, &filtering_vtable
, pb
,
1071 /* The rest of this is just reporting. If we aren't reporting, get
1073 if (opt_state
->quiet
)
1074 return SVN_NO_ERROR
;
1076 SVN_ERR(svn_cmdline_fputs("\n", stderr
, pool
));
1078 if (pb
->rev_drop_count
)
1079 SVN_ERR(svn_cmdline_fprintf(stderr
, pool
,
1080 _("Dropped %d revision(s).\n\n"),
1081 pb
->rev_drop_count
));
1083 if (pb
->do_renumber_revs
)
1085 apr_pool_t
*subpool
= svn_pool_create(pool
);
1086 SVN_ERR(svn_cmdline_fputs(_("Revisions renumbered as follows:\n"),
1089 /* Get the keys of the hash, sort them, then print the hash keys
1090 and values, sorted by keys. */
1091 num_keys
= apr_hash_count(pb
->renumber_history
);
1092 keys
= apr_array_make(pool
, num_keys
+ 1, sizeof(svn_revnum_t
));
1093 for (hi
= apr_hash_first(pool
, pb
->renumber_history
);
1095 hi
= apr_hash_next(hi
))
1097 apr_hash_this(hi
, &key
, NULL
, NULL
);
1098 APR_ARRAY_PUSH(keys
, svn_revnum_t
) = *((const svn_revnum_t
*) key
);
1100 qsort(keys
->elts
, keys
->nelts
,
1101 keys
->elt_size
, svn_sort_compare_revisions
);
1102 for (i
= 0; i
< keys
->nelts
; i
++)
1104 svn_revnum_t this_key
;
1105 struct revmap_t
*this_val
;
1107 svn_pool_clear(subpool
);
1108 this_key
= APR_ARRAY_IDX(keys
, i
, svn_revnum_t
);
1109 this_val
= apr_hash_get(pb
->renumber_history
, &this_key
,
1111 if (this_val
->was_dropped
)
1112 SVN_ERR(svn_cmdline_fprintf(stderr
, subpool
,
1113 _(" %ld => (dropped)\n"),
1116 SVN_ERR(svn_cmdline_fprintf(stderr
, subpool
,
1118 this_key
, this_val
->rev
));
1120 SVN_ERR(svn_cmdline_fputs("\n", stderr
, subpool
));
1121 svn_pool_destroy(subpool
);
1124 if (apr_hash_count(pb
->dropped_nodes
))
1126 apr_pool_t
*subpool
= svn_pool_create(pool
);
1127 SVN_ERR(svn_cmdline_fprintf(stderr
, subpool
,
1128 _("Dropped %d node(s):\n"),
1129 apr_hash_count(pb
->dropped_nodes
)));
1131 /* Get the keys of the hash, sort them, then print the hash keys
1132 and values, sorted by keys. */
1133 num_keys
= apr_hash_count(pb
->dropped_nodes
);
1134 keys
= apr_array_make(pool
, num_keys
+ 1, sizeof(const char *));
1135 for (hi
= apr_hash_first(pool
, pb
->dropped_nodes
);
1137 hi
= apr_hash_next(hi
))
1139 apr_hash_this(hi
, &key
, NULL
, NULL
);
1140 APR_ARRAY_PUSH(keys
, const char *) = key
;
1142 qsort(keys
->elts
, keys
->nelts
, keys
->elt_size
, svn_sort_compare_paths
);
1143 for (i
= 0; i
< keys
->nelts
; i
++)
1145 svn_pool_clear(subpool
);
1146 SVN_ERR(svn_cmdline_fprintf
1147 (stderr
, subpool
, " '%s'\n",
1148 (const char *)APR_ARRAY_IDX(keys
, i
, const char *)));
1150 SVN_ERR(svn_cmdline_fputs("\n", stderr
, subpool
));
1151 svn_pool_destroy(subpool
);
1154 return SVN_NO_ERROR
;
1157 /* This implements `exclude' subcommand. */
1158 static svn_error_t
*
1159 subcommand_exclude(apr_getopt_t
*os
, void *baton
, apr_pool_t
*pool
)
1161 return do_filter(os
, baton
, TRUE
, pool
);
1165 /* This implements `include` subcommand. */
1166 static svn_error_t
*
1167 subcommand_include(apr_getopt_t
*os
, void *baton
, apr_pool_t
*pool
)
1169 return do_filter(os
, baton
, FALSE
, pool
);
1177 main(int argc
, const char *argv
[])
1180 apr_status_t apr_err
;
1181 apr_allocator_t
*allocator
;
1184 const svn_opt_subcommand_desc_t
*subcommand
= NULL
;
1185 struct svndumpfilter_opt_state opt_state
;
1188 apr_array_header_t
*received_opts
;
1192 /* Initialize the app. */
1193 if (svn_cmdline_init("svndumpfilter", stderr
) != EXIT_SUCCESS
)
1194 return EXIT_FAILURE
;
1196 /* Create our top-level pool. Use a seperate mutexless allocator,
1197 * given this application is single threaded.
1199 if (apr_allocator_create(&allocator
))
1200 return EXIT_FAILURE
;
1202 apr_allocator_max_free_set(allocator
, SVN_ALLOCATOR_RECOMMENDED_MAX_FREE
);
1204 pool
= svn_pool_create_ex(NULL
, allocator
);
1205 apr_allocator_owner_set(allocator
, pool
);
1207 /* Check library versions */
1208 err
= check_lib_versions();
1210 return svn_cmdline_handle_exit_error(err
, pool
, "svndumpfilter: ");
1212 received_opts
= apr_array_make(pool
, SVN_OPT_MAX_OPTIONS
, sizeof(int));
1214 /* Initialize the FS library. */
1215 err
= svn_fs_initialize(pool
);
1217 return svn_cmdline_handle_exit_error(err
, pool
, "svndumpfilter: ");
1221 subcommand_help(NULL
, NULL
, pool
);
1222 svn_pool_destroy(pool
);
1223 return EXIT_FAILURE
;
1226 /* Initialize opt_state. */
1227 memset(&opt_state
, 0, sizeof(opt_state
));
1228 opt_state
.start_revision
.kind
= svn_opt_revision_unspecified
;
1229 opt_state
.end_revision
.kind
= svn_opt_revision_unspecified
;
1231 /* Parse options. */
1232 err
= svn_cmdline__getopt_init(&os
, argc
, argv
, pool
);
1234 return svn_cmdline_handle_exit_error(err
, pool
, "svndumpfilter: ");
1239 const char *opt_arg
;
1241 /* Parse the next option. */
1242 apr_err
= apr_getopt_long(os
, options_table
, &opt_id
, &opt_arg
);
1243 if (APR_STATUS_IS_EOF(apr_err
))
1247 subcommand_help(NULL
, NULL
, pool
);
1248 svn_pool_destroy(pool
);
1249 return EXIT_FAILURE
;
1252 /* Stash the option code in an array before parsing it. */
1253 APR_ARRAY_PUSH(received_opts
, int) = opt_id
;
1259 opt_state
.help
= TRUE
;
1261 case svndumpfilter__version
:
1262 opt_state
.version
= TRUE
;
1263 case svndumpfilter__quiet
:
1264 opt_state
.quiet
= TRUE
;
1266 case svndumpfilter__drop_empty_revs
:
1267 opt_state
.drop_empty_revs
= TRUE
;
1269 case svndumpfilter__renumber_revs
:
1270 opt_state
.renumber_revs
= TRUE
;
1272 case svndumpfilter__preserve_revprops
:
1273 opt_state
.preserve_revprops
= TRUE
;
1275 case svndumpfilter__skip_missing_merge_sources
:
1276 opt_state
.skip_missing_merge_sources
= TRUE
;
1280 subcommand_help(NULL
, NULL
, pool
);
1281 svn_pool_destroy(pool
);
1282 return EXIT_FAILURE
;
1284 } /* close `switch' */
1285 } /* close `while' */
1287 /* If the user asked for help, then the rest of the arguments are
1288 the names of subcommands to get help on (if any), or else they're
1289 just typos/mistakes. Whatever the case, the subcommand to
1290 actually run is subcommand_help(). */
1292 subcommand
= svn_opt_get_canonical_subcommand(cmd_table
, "help");
1294 /* If we're not running the `help' subcommand, then look for a
1295 subcommand in the first argument. */
1296 if (subcommand
== NULL
)
1298 if (os
->ind
>= os
->argc
)
1300 if (opt_state
.version
)
1302 /* Use the "help" subcommand to handle the "--version" option. */
1303 static const svn_opt_subcommand_desc_t pseudo_cmd
=
1304 { "--version", subcommand_help
, {0}, "",
1305 {svndumpfilter__version
, /* must accept its own option */
1308 subcommand
= &pseudo_cmd
;
1312 svn_error_clear(svn_cmdline_fprintf
1314 _("Subcommand argument required\n")));
1315 subcommand_help(NULL
, NULL
, pool
);
1316 svn_pool_destroy(pool
);
1317 return EXIT_FAILURE
;
1322 const char *first_arg
= os
->argv
[os
->ind
++];
1323 subcommand
= svn_opt_get_canonical_subcommand(cmd_table
, first_arg
);
1324 if (subcommand
== NULL
)
1326 const char* first_arg_utf8
;
1327 if ((err
= svn_utf_cstring_to_utf8(&first_arg_utf8
, first_arg
,
1329 return svn_cmdline_handle_exit_error(err
, pool
,
1332 svn_error_clear(svn_cmdline_fprintf(stderr
, pool
,
1333 _("Unknown command: '%s'\n"),
1335 subcommand_help(NULL
, NULL
, pool
);
1336 svn_pool_destroy(pool
);
1337 return EXIT_FAILURE
;
1342 /* If there's a second argument, it's probably [one of] prefixes.
1343 Every subcommand except `help' requires at least one, so we parse
1344 them out here and store in opt_state. */
1346 if (subcommand
->cmd_func
!= subcommand_help
)
1348 if (os
->ind
>= os
->argc
)
1350 svn_error_clear(svn_cmdline_fprintf
1352 _("\nError: no prefixes supplied.\n")));
1353 svn_pool_destroy(pool
);
1354 return EXIT_FAILURE
;
1357 opt_state
.prefixes
= apr_array_make(pool
, os
->argc
- os
->ind
,
1358 sizeof(const char *));
1359 for (i
= os
->ind
; i
< os
->argc
; i
++)
1363 /* Ensure that each prefix is UTF8-encoded, in internal
1364 style, and absolute. */
1365 SVN_INT_ERR(svn_utf_cstring_to_utf8(&prefix
, os
->argv
[i
], pool
));
1366 prefix
= svn_path_internal_style(prefix
, pool
);
1367 prefix
= svn_path_join("/", prefix
, pool
);
1368 APR_ARRAY_PUSH(opt_state
.prefixes
, const char *) = prefix
;
1373 /* Check that the subcommand wasn't passed any inappropriate options. */
1374 for (i
= 0; i
< received_opts
->nelts
; i
++)
1376 opt_id
= APR_ARRAY_IDX(received_opts
, i
, int);
1378 /* All commands implicitly accept --help, so just skip over this
1379 when we see it. Note that we don't want to include this option
1380 in their "accepted options" list because it would be awfully
1381 redundant to display it in every commands' help text. */
1382 if (opt_id
== 'h' || opt_id
== '?')
1385 if (! svn_opt_subcommand_takes_option(subcommand
, opt_id
))
1388 const apr_getopt_option_t
*badopt
=
1389 svn_opt_get_option_from_code(opt_id
, options_table
);
1390 svn_opt_format_option(&optstr
, badopt
, FALSE
, pool
);
1391 if (subcommand
->name
[0] == '-')
1392 subcommand_help(NULL
, NULL
, pool
);
1394 svn_error_clear(svn_cmdline_fprintf
1396 _("Subcommand '%s' doesn't accept option '%s'\n"
1397 "Type 'svndumpfilter help %s' for usage.\n"),
1398 subcommand
->name
, optstr
, subcommand
->name
));
1399 svn_pool_destroy(pool
);
1400 return EXIT_FAILURE
;
1404 /* Run the subcommand. */
1405 err
= (*subcommand
->cmd_func
)(os
, &opt_state
, pool
);
1408 /* For argument-related problems, suggest using the 'help'
1410 if (err
->apr_err
== SVN_ERR_CL_INSUFFICIENT_ARGS
1411 || err
->apr_err
== SVN_ERR_CL_ARG_PARSING_ERROR
)
1413 err
= svn_error_quick_wrap(err
,
1414 _("Try 'svndumpfilter help' for more "
1417 return svn_cmdline_handle_exit_error(err
, pool
, "svndumpfilter: ");
1421 svn_pool_destroy(pool
);
1423 /* Flush stdout, making sure the user will see any print errors. */
1424 SVN_INT_ERR(svn_cmdline_fflush(stdout
));
1425 return EXIT_SUCCESS
;