REPOCUTTER(1)

๐Ÿ’… REPOCUTTER(1) โ€” the repocutter manual, bestie edition ๐ŸŽ€

okay so like. first of all, the vibe: this is the manual page for repocutter, and it's giving surgical-and-filtering-operations-on-Subversion-dump-files energy. we're gonna get through it together, no cap. grab a boba. ๐Ÿ’–


โœจ SYNOPSIS โœจ

repocutter [-q] [-d n] [-i 'filename'] [-r 'selection'] 'subcommand'

like that's the whole entrance exam. slay. ๐Ÿ”ฅ


๐ŸŒธ DESCRIPTION ๐ŸŒธ

okay so repocutter does surgical and filtering operations on Subversion dump files. literally what it says on the tin. it's not as flexible as reposurgeon(1) (bestie, nobody is), BUT it can do Subversion-specific transformations that reposurgeon cannot, and honestly it's so useful for processing Subversion repositories into a form suitable for conversion. also โ€” and this is the iconic part โ€” it supports the version 3 dumpfile format, which reposurgeon does not. ate. ๐Ÿ‘‘

the help subcommand lists help topics. and if you follow it with an argument that's a topic name, it lists the help for that topic. very demure, very mindful. ๐ŸŽ€


๐Ÿฆ‹ the -r / --range situation (iykyk) ๐Ÿฆ‹

in most commands, the -r (or --range) option limits the selection of revisions over which an operation will be performed. usually the other revisions get passed through unaltered โ€” except in the select and deselect commands, where this option controls which revisions get passed through. so like, context matters, bestie. read the room.

a selection consists of one or more comma-separated ranges. a range may be:

  • an integer revision number
  • the special name HEAD for the head revision
  • a colon-separated pair of integers
  • an integer followed by a colon followed by HEAD

if the output stream contains copyfrom references to missing revisions, repocutter silently patches each copysources by stepping it backwards to the most recent previous version that exists. lowkey a lifesaver. ๐Ÿ’€

โš ๏ธ important lore drop: older versions of this tool, before 4.30, treated -r as an implied selection filter rather than passing through unselected revisions unaltered. so if you have old scripts using repocutter, they may need modification. the way that legacy code will come for you... ๐Ÿ˜ญ

๐Ÿ’… the output / progress-spinner situation ๐Ÿ’…

normally, each subcommand produces a progress spinner on standard error; each turn means another revision has been filtered. the -q (or --quiet) option suppresses this. and quiet mode gets set automatically when output is redirected to a file or pipe. it just knows. ๐Ÿ”ฎ


๐Ÿซถ the other global options, in order ๐Ÿซถ

the -d option enables debug messages on standard error. it takes an integer debug level. these messages are probably only of interest to repocutter developers. ngl, unless you're deep in the source, they're giving "not for you." ๐Ÿ™ˆ

the -i option sets the input source to a specified filename. this is primarily useful when running the program under a debugger. when this option is not present, the program expects to read a stream from standard input. classic stdin behavior, we love to see it. โญ

generally, if you need to use this program at all, you will find that you need to pipe your dump file through multiple instances of it doing one kind of operation each. and bestie, this is not as expensive as it sounds โ€” with the exception of the reduce subcommand, the working set of this program is bounded by the size of the largest single blob plus its metadata. it does not need to hold the entire repo metadata in memory. memory-efficient king. ๐Ÿ‘‘

the -f/-fixed option disables regexp compilation of PATTERN arguments, treating them as literal strings. so like, take the regex girlboss out back. ๐ŸŽ€

the -t option sets a tag to be included in error and warning messages. this is gonna be so useful for determining which stage of a multistage repocutter pipeline failed. pipeline girlies rise. ๐ŸŒˆ

there are a few other command-specific options described under individual commands. we'll get to them. patience, bestie. ๐Ÿงš


๐ŸŒธ PATTERN arguments, explained ๐ŸŒธ

in the command descriptions, PATTERN arguments are regular expressions to match pathnames, constrained so that each match must be a path segment or a sequence of path segments. that means:

  • the left end must be either at the start of path or immediately following a /
  • the right end must precede a / or be at end of string

with a leading ^ the match is constrained to be a leading sequence of the pathname; with a trailing $, a trailing one. it's giving anchored but make it path-aware. ๐Ÿ’œ


๐Ÿ‘‘ THE SUBCOMMANDS ๐Ÿ‘‘

the following subcommands are available. buckle up, we're going through all of them, no skipping. periodt. ๐Ÿ’…


๐ŸŽ€ select ๐ŸŽ€

the select subcommand selects a range and permits only revisions and nodes in that range to pass to standard output. a range beginning with 0 includes the dumpfile header. mergeinfo properties in all revisions are updated so they no longer refer to omitted revisions.

โš ๏ธ Warning: this can produce a dump that is not valid dump that can be read by reposurgeon. in particular, it may delete a revision that is referenced in a later copy-from operation, which will crash reposurgeon. bestie. be careful. ๐Ÿ˜ญ

๐Ÿฆ‹ deselect ๐Ÿฆ‹

the deselect subcommand selects a range and permits only revisions and nodes NOT in that range to pass to standard output. any mergeinfo properties in other revisions are updated so they no longer refer to dropped revisions.

โš ๏ธ Warning: same deal โ€” can produce a dump that is not valid dump that can be read by reposurgeon. in particular, it may delete a revision that is referenced in a later copy-from operation, which will crash reposurgeon. ๐Ÿ’€

๐Ÿ“ squash ๐Ÿ“

the squash subcommand merges adjacent commits that have the same author and log text and were made within 5 minutes of each other. consolidation era. โœจ

with the -m (or --mapfile) option, squash emits a map to the named file showing how old revision numbers map into new ones. very cute, very helpful.

with the -e (or --excise) option, the specified set of revisions is unconditionally removed. the tool will exit with an error if an excised revision is about to be squashed with another revision. note that repocutter does not perform any checks on whether the repository history is afterwards valid โ€” if you delete a node using this option, you may not discover a problem until you attempt to load the resulting dumpfile. so like... consequences, bestie. ๐Ÿ™ˆ

repocutter attempts to fix up references to Subversion revisions in log entries so they will still be correct after squashing. it's doing its best. ๐Ÿซถ


๐ŸŒˆ see ๐ŸŒˆ

render a very condensed report on the repository node structure, mainly useful for examining strange and pathological repositories. file content is ignored. you get one line per repository operation, reporting:

  • the revision
  • operation type
  • file path
  • the copy source (if any)

directory paths are distinguished by a trailing slash. the 'copy' operation is really an 'add' with a directory source and target; the display name is changed to make them easier to see. very aesthetic. this report can be restricted by a selection set. ๐Ÿ’–


โญ renumber โญ

renumber all revisions, patching Node-copyfrom headers as required. any selection option is ignored. takes no arguments. the -b option can be used to set the base to renumber from, defaulting to 0. clean slate energy. ๐ŸŽ€


๐Ÿ”ฅ count ๐Ÿ”ฅ

the count subcommand lists the last revision number in the input stream. this is normally the revision count, but may not be if the stream has omitted revisions. nuance, bestie. nuance. ๐Ÿ’…


๐ŸŒธ log ๐ŸŒธ

generate a log report, same format as the output of svn log on a repository, to standard output. classic. iconic. no notes. โญ


๐Ÿงš setlog ๐Ÿงš

replace the log entries in the input dumpfile with the corresponding entries in the LOGFILE, which should be in the format of an svn log output. replacements may be restricted to a specified range. it's giving log glow-up. ๐Ÿ’œ


๐ŸŽ€ propdel ๐ŸŽ€

delete the property PROPNAME. may be restricted by a revision selection. you may specify multiple properties to be deleted. bye bestie. ๐Ÿ’…


๐Ÿฆ‹ proprename ๐Ÿฆ‹

rename the property OLDNAME to NEWNAME. may be restricted by a revision selection. you may specify multiple properties to be renamed. rebranding era. โœจ


๐Ÿ“ propset ๐Ÿ“

set the property PROPNAME to PROPVAL. may be restricted by a revision selection. note that specifying only a revision will cause the property to be set on the revision properties and on all nodes in the revision โ€” you'll probably want to specify a node index. so like, be specific or it does the most. ๐Ÿ˜ญ

you may specify multiple property settings. stack 'em. ๐Ÿ”ฅ


๐ŸŒˆ propclean ๐ŸŒˆ

every path with a suffix matching one of SUFFIXES gets a property turned off. the default property is svn:: โ€” wait, bestie, that's literally what the page says: the default property is svn::Another property may be set with the -p option. yes, the source text is a little... mystical there. we're reporting what it says. ๐Ÿ™ˆ


โญ expunge โญ

delete all operations with Node-path or Node-copyfrom-path headers matching specified Golang regular expressions (opposite of 'sift'). any revision left with no Node records after this filtering has its Revision record dropped as well. mergeinfo properties in all revisions are updated so they no longer refer to dropped revisions.

โš ๏ธ Warning: can produce a dump that is not valid dump that can be read by reposurgeon. in particular, it may delete a revision that is referenced in a later copy-from operation, which will crash reposurgeon. ๐Ÿ’€

๐ŸŽ€ sift ๐ŸŽ€

delete all operations with either Node-path or Node-copyfrom-path headers not matching specified Golang regular expressions (opposite of 'expunge'). any revision left with no Node records after this filtering has its Revision record removed as well. mergeinfo properties in all revisions are updated so they no longer refer to dropped revisions. this transform can be restricted by a selection set. ๐Ÿงš

โš ๏ธ Warning: same as above โ€” can produce a dump that is not valid dump that can be read by reposurgeon. in particular, it may delete a revision that is referenced in a later copy-from operation, which will crash reposurgeon. ๐Ÿ’€

๐Ÿฆ‹ closure ๐Ÿฆ‹

the closure subcommand computes the transitive closure of a path set under the relation 'copies from' โ€” that is, with the smallest set of additional paths such that every copy-from source is in the set. math girlie moment. ๐Ÿ”ฅ


๐ŸŒธ pathlist ๐ŸŒธ

list all distinct node-paths in the stream, once each, in the order first encountered. no dupes, no chaos. just vibes. ๐Ÿ’œ


๐Ÿ“ pathrename ๐Ÿ“

modify Node-path headers, Node-copyfrom-path headers, and svn::expression FROM; replace with TO. TO may contain Golang-style backreferences (${1}, ${2} etc โ€” curly brackets not optional) to parenthesized portions of FROM. bestie, do not forget the curly brackets. i'm serious. ๐Ÿ˜ค

matches are constrained so that each match must be a path segment or a sequence of path segments; that is, the left end must be either at the start of path or immediately following a /, and the right end must precede a / or be at end of string. with a leading ^ the match is constrained to be a leading sequence of the pathname; with a trailing $, a trailing one.

multiple FROM/TO pairs may be specified and are applied in order. this transform can be restricted by a selection set. all mergeinfo properties are updated in accordance with the path renames. (yes, that trailing comma is in the original. we respect the source. ๐Ÿ’…)


๐ŸŽ€ setpath ๐ŸŽ€

in the specified revisions, replace the Node-path with the specified PATH. does not alter mergeinfo properties as a side effect. clean and literal. โœจ


๐Ÿฆ‹ setcopyfrom ๐Ÿฆ‹

in the specified revisions, replace the Node-copyfrom-path with the specified PATH. does not alter mergeinfo properties as a side effect. terminates with error if any selected node is not a copy. no cap, it will simply end you. ๐Ÿ’€


๐ŸŒˆ pop ๐ŸŒˆ

pop initial segment off each path matching PATTERN โ€” by default, all paths. ๐Ÿซถ

may be useful after a sift command to turn a dump from a subproject stripped from a dump for a multiple-project repository into the normal form with trunk/tags/branches at the top level. chef's kiss. ๐Ÿ‘‘


okay that's part 1 bestie, and there's literally more where that came from โ€” don't go anywhere, we're not done. ๐Ÿ’–

This transform cannot be restricted by a selection set, because there is literally no way to guarantee that copyfrom paths and mergeinfo properties will be modified consistently if you slap that kind of restriction on it. ๐Ÿ˜ค

Mergeinfo properties in all revisions get updated, plus the path and copyfrom parts. Periodt.

๐Ÿซถ push

Push an initial segment onto each matching path. Normally this is the vibe for adding a "trunk" prefix to every path in a flat repository โ€” you know, giving it a little main-character introduction. The -s option lets you set a different initial segment if trunk isn't your thing.

Same energy as above: this transform cannot be restricted by a selection set, because there's no way to guarantee copyfrom paths and mergeinfo properties stay consistent under that kind of restriction. ๐Ÿ™…โ€โ™€๏ธ

Mergeinfo properties in all revisions are updated to refer to the new pathnames. Slay.

๐Ÿ“ filecopy

For each node in the revision range, it stashes the current version of the node-path's content. Then, for each later file copy operation with that source, it replaces the file copy with an explicit add/change using the stashed content. Cute, right?

You can use this to sever links from obsolete branches or non-conformable directories in a multiproject repository, so the unwanted content can be expunged without changing the content of later revisions. It's giving "cut ties respectfully". โœ‚๏ธ

If a PATTERN argument is provided, only replace copies with an explicit add/change when the source node path matches PATTERN.

With the -n flag, only the basename is required to match PATTERN if it is provided. Otherwise, with -n and no PATTERN, require a match of source to target on basename only rather than the full path. This may be required in order to extract file copies from branches. iykyk.

Restricting the range holds down the memory requirement of this tool, which in the worst (and default) 1:$ case will keep a copy of every blob in the repository until it's done processing the stream. That's a lot of blobs bestie, plan accordingly. ๐Ÿ˜ญ

๐Ÿงš skipcopy

Replace the source revision and path of a copy at the upper end of the selection with the source revisions and path of a copy at the lower end. Fails unless both revisions are copies โ€” no cap, it just dies. Used to remove an unwanted intermediate copy or copies, cleaning up the history. Like clearing out your camera roll, but for revisions. ๐ŸŒธ

๐ŸŒˆ count

Delete Node-copyfrom-rev and Node-copyfrom-path node headers from nodes in the specified revision range; the nodes become add operations and are otherwise unchanged. That's it, that's the whole thing.

๐Ÿ’… swap

Swap the top two elements of each pathname in every revision in the selection set. Super useful following a sift operation for straightening out a common form of multi-project repository. If a PATTERN argument is given, only paths matching it are swapped.

๐Ÿ‘‘ swapsvn

Like swap, but it's aware of Subversion structure. Used for transforming multiproject repositories into a standard layout with trunk, tags, and branches at the top level. It's giving glow-up. โœจ

It fires when the second component of a matching path is "trunk", "branches", or "tags", or when the path consists of a single segment that is a top-level project directory; it passes through all paths for which this is not so, unaltered.

Top-level project directories with properties or comments make this command die (return status 1) with an error message on stderr; otherwise these directories are silently discarded. Rest in peace, little directories. ๐Ÿ’€

Otherwise, it swaps "trunk" and the top-level (project) directory straight up. For tags and branches, the following two components are swapped to the top โ€” thus, "foo/branches/release23" becomes "branches/release23/foo", putting the project directory beneath the branch. The way that just works.

Also fires when an entire project directory is copied; this is transformed into a copy of trunk and copies of each subbranch and tag that exists.

After the swap, there are attempts to recognize spans of copies into branch directories, and copies into tag subdirectories that are parallel in all top-level (project) directories. These get coalesced into single copies in the inverted structure. No attempts are made to coalesce deletes; you, the user, must manually trim unneeded branches. Do your own chores bestie. ๐Ÿซถ

Accordingly, copies with three-segment sources and three-segment targets are transformed; for tags/ and branches/ paths the last segment (the subdirectory below the branch name) is dropped. Following copies are skipped.

This has two minor negative consequences, and we're being honest about them:

  • Metadata belonging to all deletes or copies after the first one in a coalesced span is lost. ๐Ÿซ 
  • Branches and tags local to individual project directories are promoted to global branches and tags across the entire transformed repository; no content is lost this way.

Parallel rename sequences are also coalesced. Iconic.

If a PATTERN argument is given, only paths matching the pattern are swapped.

Note that the result of swapping does not have initial trunk/branches/tags directory creations and can thus not be fed directly to svnload. reposurgeon copes with this, but Subversion will not. Fr fr, don't do it. ๐Ÿ™ˆ

Mergeinfo properties are updated to use the swapped path names.

This transform can be restricted by a selection set. Finally, one that listens. ๐Ÿ’œ

๐Ÿ”ฅ swapcheck

Lists directory prefixes of anomalous paths that would confuse swapsvn. This includes:

  • any single-segment path other than trunk/tags/branches or a project copy operation,
  • any path with two or more segments in which the second is not trunk/tags/branches,
  • and any path in which trunk/tags/branches occurs more than one segment down from the root.

Each report line has two fields: the first is the earliest revision containing a path with the prefix given, and the second is the prefix. Once a particular path prefix has been recognized and reported as anomalous, later paths with that prefix are not reported. Efficiency slay. โญ

If feeding a Subversion dump to this subcommand doesn't produce an empty report, you can expect swapsvn to produce an invalid dump that will confuse and possibly crash reposurgeon. ๐Ÿ˜ฑ The remedy for this is a set of pathrename and/or deselections that yields paths conformable to being swapped into a regular Subversion structure.

๐ŸŽ€ replace

Perform a regular expression search/replace on blob content. The first character of the argument (normally /) is treated as the end delimiter for the regular-expression and replacement parts. Sneaky but iconic. This transform can be restricted by a selection set. ๐Ÿฆ‹

๐ŸงŠ strip

Replace content with unique generated cookies on all node paths matching the specified regular expressions; if no expressions are given, match all paths. Cookie-cutter content, literally. ๐Ÿช

This command is useful for reducing the bulk of a stream without touching its metadata, so you can do test conversions more quickly. Speedrun vibes.

๐Ÿ’ซ hash

Replace content with hash on all node paths matching the specified regular expressions; if no expressions are given, match all paths.

๐Ÿ•ถ๏ธ obscure

Replace path segments and committer IDs with arbitrary but consistent names in order to obscure them. The replacement algorithm is tuned to make the replacements readily distinguishable by eyeball โ€” so you can tell what's what without squinting. ๐Ÿ‘€ This transform can be restricted by a selection set.

๐ŸŒธ reduce

Strip revisions out of a dump so the only parts left are those likely to be relevant to a conversion problem. This is done by dropping every node that consists of a change on a file and has no property settings. Mergeinfo properties in all revisions are updated so they no longer refer to dropped revisions. KonMari method but for dumps. ๐Ÿงน

๐Ÿงš testify

Replace commit timestamps with a monotonically increasing clock tick starting at the Unix epoch and advancing by 10 seconds per commit. Replace all attributions with 'fred'. Discard the repository UUID. Use this to neutralize procedurally-generated streams so they can be compared.

Yes, everything is fred now. That's the point. ๐Ÿ˜Œ This transform can be restricted by a selection set.

๐Ÿ” count

Set the debug level to the specified value on the selected revisions. Setting debugging enables diagnostics to standard error, and suppresses the progress baton for the entire run in order not to step on any diagnostics that might be emitted.

For the meaning of the debug levels, see the source code. This option is probably only of interest to repocutter developers. Ngl if you're reading this you're either very lost or extremely built different. ๐Ÿ’€

๐Ÿท๏ธ version

Report major and minor repocutter version.


๐Ÿ“œ HISTORY ๐Ÿ“œ

Under the name "svncutter", an ancestor of this program traveled in the 'contrib/' directory of the Subversion distribution. It had functional overlap with reposurgeon(1) because it was directly ancestral to that code. It was moved to the reposurgeon(1) distribution in January 2016. This program was ported from Python to Go in August 2018, at which time the obsolete "squash" command was retired. The syntax of regular expressions in the pathrename command changed at that time. Character development arc fr. ๐ŸŒฑ

The reason for the partial functional overlap between repocutter and reposurgeon is that repocutter was first written earlier and became a testbed for some of the design concepts in reposurgeon. After reposurgeon was written, the author learned that it could not naturally support some useful operations very specific to Subversion, and enhanced repocutter to do those. Underdog origin story, we love to see it. ๐Ÿซถ


๐ŸŽฏ RETURN VALUES ๐ŸŽฏ

Normally 0. Can be 1 if repocutter sees an ill-formed dump, or if the output stream contains any copyfrom references to missing revisions. ๐Ÿšจ


๐Ÿ’€ BUGS ๐Ÿ’€

There is one regression since the Python version: repocutter no longer recognizes Macintosh-style line endings consisting of a carriage return only. This may be addressed in a future version. Pour one out for the carriage returns. ๐Ÿ˜ญ


๐Ÿ‘€ SEE ALSO ๐Ÿ‘€

reposurgeon(1). Go read that one too bestie, it's the whole extended universe. ๐Ÿ’–

๐Ÿ’… EXAMPLE ๐Ÿ’…

Okay bestie, picture this: you've got a Subversion repo and it's semi-pathological. Like not fully cursed, but definitely giving "who did this" energy. The structure looks a little something like:

Directory1/ (with unrelated content)
Directory2/ (with unrelated content)
TheDirIWantToMigrate/
                branches/
                               crazy-feature/
                                              UnrelatedApp1/
                                              TheAppIWantToMigrate/
                tags/
                               v1.001/
                                              UnrelatedApp1/
                                              UnrelatedApp2/
                                              TheAppIWantToMigrate/
                trunk/
                               UnrelatedApp1/
                               UnrelatedApp2/
                               TheAppIWantToMigrate/

The dream? Transform that dump file so TheAppIWantToMigrate can be subject to a regular branchy lift โœจ. And the way to dissect out the code of interest is literally just applying this series of filters, in order, like a little pipeline glow-up:

repocutter expunge '^Directory1' '^Directory2'
repocutter pathrename '^TheDirIWantToMigrate/' ''
repocutter expunge '^branches/crazy-feature/UnrelatedApp1/
repocutter pathrename 'branches/crazy-feature/TheAppIWantToMigrate/' 'branches/crazy-feature/'
repocutter expunge '^tags/v1.001/UnrelatedApp1/'
repocutter expunge '^tags/v1.001/UnrelatedApp2/'
repocutter pathrename '^tags/v1.001/TheAppIWantToMigrate/' 'tags/v1.001/'
repocutter expunge '^trunk/UnrelatedApp1/'
repocutter expunge '^trunk/UnrelatedApp2/'
repocutter pathrename '^trunk/TheAppIWantToMigrate/' 'trunk/'
๐Ÿซถ not me leaving that third line exactly as the source gives it โ€” the quote situation on that one is a whole situation, and I do not edit the sacred text. iykyk.

๐Ÿ’€ LIMITATIONS ๐Ÿ’€

Okay so this is the part where I hold your hand, because it's real: the sift and expunge operations can produce output dumps that are invalid. ๐Ÿ˜ญ Not a vibe, but facts are facts.

The problem is copyfrom operations โ€” you know, Subversion branch and tag creations. If an included revision includes a copyfrom reference to an excluded one, the reference target won't be in the emitted dump. It won't load correctly in Subversion. And yeah, reposurgeon has fallback logic that backs down to the latest existing revision before the kissing one, but that expedient is fragile โ€” like, cute-but-don't-trust-it fragile.

The revision number in a copyfrom header pointing to a missing revision will be zero. And nope, attempts to be clever about this won't work โ€” the problem is inherent in the data model of Subversion. It's giving structural, not fixable-with-a-trick. Periodt. ๐Ÿ”ฅ


๐Ÿ‘‘ AUTHOR ๐Ÿ‘‘

Eric S. Raymond esr@thyrsus.com โ€” icon behavior, honestly. This tool is distributed with reposurgeon; see the project page at http://www.catb.org/~esr/reposurgeon ๐Ÿฆ‹


and that's the whole page bestie!! ๐ŸŽ€ this one came to you as REPOCUTTER(1), dated 2026-09-14, straight from the reposurgeon corner of the universe ๐ŸŒˆ go forth and cut some repos, mwah ๐Ÿ’œโœจ

๐Ÿ“ฆ where this page comes from
  • epel 10: reposurgeon 5.11-1.el10_3 ยท Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND ISC AND MIT
  • ๐Ÿ’– girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms