/usr/bin/subvertpy3-fast-export is in python3-subvertpy 0.10.1-1build1.
This file is owned by root:root, with mode 0o755.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | #!/usr/bin/python3
#
# svn-fast-export.py
# ----------
# Walk through each revision of a local Subversion repository and export it
# in a stream that git-fast-import can consume.
#
# Author: Chris Lee <clee@kde.org>
# License: MIT <http://www.opensource.org/licenses/mit-license.php>
#
# Adapted for subvertpy by Jelmer Vernooij <jelmer@samba.org>
from io import BytesIO
import sys
import os.path
from optparse import OptionParser
import stat
from time import mktime, strptime
from subvertpy.repos import PATH_CHANGE_DELETE, Repository
trunk_path = '/trunk/'
branches_path = '/branches/'
tags_path = '/tags/'
address = 'localhost'
ct_short = ['M', 'A', 'D', 'R', 'X']
stdout = getattr(sys.stdout, 'buffer', sys.stdout)
def dump_file_blob(root, stream, stream_length):
stdout.write(("data %s\n" % stream_length).encode("ascii"))
stdout.flush()
stdout.write(stream.read())
stdout.write(b"\n")
class Matcher(object):
branch = None
def __init__(self, trunk_path):
self.trunk_path = trunk_path
def branchname(self):
return self.branch
def __str__(self):
return super(Matcher, self).__str__() + ":" + self.trunk_path
@staticmethod
def getMatcher(trunk_path):
if trunk_path.startswith("regex:"):
return RegexStringMatcher(trunk_path)
else:
return StaticStringMatcher(trunk_path)
class StaticStringMatcher(Matcher):
branch = "master"
def __init__(self, trunk_path):
if not trunk_path.startswith("/"):
raise ValueError("Trunk path does not start with a slash (/)")
Matcher.__init__(self, trunk_path)
def matches(self, path):
return path.startswith(self.trunk_path)
def replace(self, path):
return path.replace(self.trunk_path, '')
class RegexStringMatcher(Matcher):
def __init__(self, trunk_path):
super(RegexStringMatcher, self).__init__(trunk_path)
import re
self.matcher = re.compile(self.trunk_path[len("regex:"):])
def matches(self, path):
match = self.matcher.match(path)
if match:
self.branch = match.group(1)
return True
else:
return False
def replace(self, path):
return self.matcher.sub("\g<2>", path)
MATCHER = None
def export_revision(rev, fs):
sys.stderr.write("Exporting revision %s... " % rev)
# Open a root object representing the youngest (HEAD) revision.
root = fs.revision_root(rev)
# And the list of what changed in this revision.
changes = root.paths_changed()
i = 1
marks = {}
file_changes = []
for path, (node_id, change_type, text_changed, prop_changed) in changes.items():
if root.is_dir(path):
continue
if not MATCHER.matches(path):
# We don't handle branches. Or tags. Yet.
pass
else:
if change_type == PATH_CHANGE_DELETE:
file_changes.append("D %s" % MATCHER.replace(path).lstrip("/"))
else:
props = root.proplist(path)
marks[i] = MATCHER.replace(path)
if props.get("svn:special", ""):
contents = root.file_content(path).read()
if not contents.startswith(b"link "):
sys.stderr.write("special file '%s' is not a symlink, ignoring...\n" % path)
continue
mode = stat.S_IFLNK
stream = BytesIO(contents[len(b"link "):])
stream_length = len(stream.getvalue())
else:
if props.get("svn:executable", ""):
mode = 0o755
else:
mode = 0o644
stream_length = root.file_length(path)
stream = root.file_content(path)
file_changes.append("M %o :%s %s" % (
mode, i, marks[i].lstrip("/")))
stdout.write(("blob\nmark :%s\n" % i).encode("ascii"))
dump_file_blob(root, stream, stream_length)
stream.close()
i += 1
# Get the commit author and message
props = fs.revision_proplist(rev)
# Do the recursive crawl.
if 'svn:author' in props:
author = "%s <%s@%s>" % (props['svn:author'], props['svn:author'], address)
else:
author = 'nobody <nobody@localhost>'
if len(file_changes) == 0:
sys.stderr.write("skipping.\n")
return
svndate = props['svn:date'][0:-8]
commit_time = mktime(strptime(svndate, '%Y-%m-%dT%H:%M:%S'))
line = "commit refs/heads/%s\n" % MATCHER.branchname()
stdout.write(line.encode("utf-8"))
line = "committer %s %s -0000\n" % (author, int(commit_time))
stdout.write(line.encode("utf-8"))
stdout.write(("data %s\n" % len(props['svn:log'])).encode("ascii"))
stdout.write(props['svn:log'].encode("utf-8"))
stdout.write(b"\n")
stdout.write(b'\n'.join(c.encode("utf-8") for c in file_changes))
stdout.write(b"\n\n")
sys.stderr.write("done!\n")
def crawl_revisions(repos_path, first_rev=None, final_rev=None):
"""Open the repository at REPOS_PATH, and recursively crawl all its
revisions."""
# Open the repository at REPOS_PATH, and get a reference to its
# versioning filesystem.
fs_obj = Repository(repos_path).fs()
# Query the current youngest revision.
if first_rev is None:
first_rev = 1
if final_rev is None:
final_rev = fs_obj.youngest_revision()
for rev in range(first_rev, final_rev + 1):
export_revision(rev, fs_obj)
if __name__ == '__main__':
usage = '%prog [options] REPOS_PATH'
parser = OptionParser()
parser.set_usage(usage)
parser.add_option('-f', '--final-rev', help='Final revision to import',
dest='final_rev', metavar='FINAL_REV', type='int')
parser.add_option('-r', '--first-rev', help='First revision to import',
dest='first_rev', metavar='FIRST_REV', type='int')
parser.add_option(
'-t', '--trunk-path',
help=(
"Path in repo to /trunk, may be `regex:/cvs/(trunk)/proj1/(.*)`\n"
"First group is used as branchname, second to match files"),
dest='trunk_path', metavar='TRUNK_PATH')
parser.add_option(
'-b', '--branches-path', help='Path in repo to /branches',
dest='branches_path', metavar='BRANCHES_PATH')
parser.add_option(
'-T', '--tags-path', help='Path in repo to /tags',
dest='tags_path', metavar='TAGS_PATH')
parser.add_option(
'-a', '--address',
help='Domain to put on users for their mail address',
dest='address', metavar='hostname', type='string')
parser.add_option(
"--version", help="Print version and exit",
action="store_true")
(options, args) = parser.parse_args()
if options.version:
import subvertpy
print(".".join(str(x) for x in subvertpy.__version__))
sys.exit(0)
if options.trunk_path is not None:
trunk_path = options.trunk_path
if options.branches_path is not None:
branches_path = options.branches_path
if options.tags_path is not None:
tags_path = options.tags_path
if options.address is not None:
address = options.address
MATCHER = Matcher.getMatcher(trunk_path)
sys.stderr.write("%s\n" % MATCHER)
if len(args) != 1:
parser.print_help()
sys.exit(2)
# Canonicalize (enough for Subversion, at least) the repository path.
repos_path = os.path.normpath(args[0])
if repos_path == '.':
repos_path = ''
crawl_revisions(repos_path, first_rev=options.first_rev,
final_rev=options.final_rev)
|