Merge branch 'svn-rev'
[yap.git] / plugins / svn.py
blob99d500dd3ec35dd217bc274f3a6f61373336569f
2 from yap.yap import YapCore, YapError
3 from yap.util import get_output, takes_options, run_command, run_safely, short_help
5 import os
6 import tempfile
7 import glob
8 import pickle
9 import re
10 import bisect
11 import struct
13 class RepoBlob(object):
14 def __init__(self, keys):
15 self.keys = keys
17 self.uuid = None
18 self.branches = None
19 self.tags = None
20 self.metadata = {}
22 def add_metadata(self, branch):
23 assert branch not in self.metadata
24 gitdir = get_output("git rev-parse --git-dir")
25 assert gitdir
26 revmap = os.path.join(gitdir[0], "svn", "svn", branch, ".rev_map*")
27 revmap = glob.glob(revmap)
28 if not revmap:
29 return
30 uuid = revmap[0].split('.')[-1]
31 if self.uuid is None:
32 self.uuid = uuid
33 assert self.uuid == uuid
34 rev = get_output("git rev-parse refs/remotes/svn/%s" % branch)
35 data = file(revmap[0]).read()
36 self.metadata[branch] = rev[0], data
38 # Helper class for dealing with SVN metadata
39 class SVNRevMap(object):
40 RECORD_SZ = 24
41 def __init__(self, filename):
42 self.fd = file(filename, "rb")
43 size = os.stat(filename)[6]
44 self.nrecords = size / self.RECORD_SZ
46 def __getitem__(self, index):
47 if index >= self.nrecords:
48 raise IndexError
49 return self.get_record(index)[0]
51 def __len__(self):
52 return self.nrecords
54 def get_record(self, index):
55 self.fd.seek(index * self.RECORD_SZ)
56 record = self.fd.read(self.RECORD_SZ)
57 return self.parse_record(record)
59 def parse_record(self, record):
60 record = struct.unpack("!I20B", record)
61 rev = record[0]
62 hash = map(lambda x: "%02x" % x, record[1:])
63 hash = ''.join(hash)
64 return rev, hash
66 class SvnPlugin(YapCore):
67 "Allow yap to interoperate with Subversion repositories"
69 revpat = re.compile('^r(\d+)$')
71 def _get_root(self, url):
72 root = get_output("svn info %s 2>/dev/null | gawk '/Repository Root:/{print $3}'" % url)
73 if not root:
74 raise YapError("Not an SVN repo: %s" % url)
75 return root[0]
77 def _configure_repo(self, url, fetch=None):
78 root = self._get_root(url)
79 os.system("git config svn-remote.svn.url %s" % root)
80 if fetch is None:
81 trunk = url.replace(root, '').strip('/')
82 else:
83 trunk = fetch.split(':')[0]
84 os.system("git config svn-remote.svn.fetch %s:refs/remotes/svn/trunk"
85 % trunk)
87 branches = trunk.replace('trunk', 'branches')
88 if branches != trunk:
89 os.system("git config svn-remote.svn.branches %s/*:refs/remotes/svn/*" % branches)
90 tags = trunk.replace('trunk', 'tags')
91 if tags != trunk:
92 os.system("git config svn-remote.svn.tags %s/*:refs/tags/*" % tags)
93 self.cmd_repo("svn", url)
94 os.system("git config yap.svn.enabled 1")
96 def _create_tagged_blob(self):
97 keys = dict()
98 for i in get_output("git config --list | grep ^svn-remote.svn"):
99 k, v = i.split('=')
100 keys[k] = v
101 blob = RepoBlob(keys)
102 for b in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/svn/*'"):
103 b = b.replace('refs/remotes/svn/', '')
104 blob.add_metadata(b)
106 fd_w, fd_r = os.popen2("git hash-object -w --stdin")
107 pickle.dump(blob, fd_w)
108 fd_w.close()
109 hash = fd_r.readline().strip()
110 run_safely("git tag -f yap-svn %s" % hash)
112 def _cleanup_branches(self):
113 for b in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/svn/*@*'"):
114 head = b.replace('refs/remotes/svn/', '')
115 path = os.path.join(".git", "svn", "svn", head)
116 files = os.listdir(path)
117 for f in files:
118 os.unlink(os.path.join(path, f))
119 os.rmdir(path)
121 ref = get_output("git rev-parse %s" % b)
122 if ref:
123 run_safely("git update-ref -d %s %s" % (b, ref[0]))
125 def _clone_svn(self, url, directory=None, **flags):
126 url = url.rstrip('/')
127 if directory is None:
128 directory = url.rsplit('/')[-1]
129 directory = directory.replace('.git', '')
131 try:
132 os.mkdir(directory)
133 except OSError:
134 raise YapError("Directory exists: %s" % directory)
135 os.chdir(directory)
137 self.cmd_init()
138 run_command("git config svn-remote.svn.noMetadata 1")
139 self._configure_repo(url)
140 os.system("git svn fetch -r %s:HEAD" % flags.get('-r', '1'))
142 self._cleanup_branches()
143 self._create_tagged_blob()
145 def _push_svn(self, branch, **flags):
146 if '-d' in flags:
147 raise YapError("Deleting svn branches not supported")
148 print "Verifying branch is up-to-date"
149 run_safely("git svn fetch svn")
151 branch = branch.replace('refs/heads/', '')
152 rev = get_output("git rev-parse --verify refs/remotes/svn/%s" % branch)
154 # Create the branch if requested
155 if not rev:
156 if '-c' not in flags:
157 raise YapError("No matching branch on the repo. Use -c to create a new branch there.")
158 src = get_output("git svn info | gawk '/URL:/{print $2}'")[0]
159 brev = get_output("git svn info | gawk '/Revision:/{print $2}'")[0]
160 root = get_output("git config svn-remote.svn.url")[0]
161 branch_path = get_output("git config svn-remote.svn.branches")[0].split(':')[0]
162 branch_path = branch_path.rstrip('/*')
163 dst = '/'.join((root, branch_path, branch))
165 # Create the branch in svn
166 run_safely("svn cp -r%s %s %s -m 'create branch %s'"
167 % (brev, src, dst, branch))
168 run_safely("git svn fetch svn")
169 rev = get_output("git rev-parse refs/remotes/svn/%s 2>/dev/null" % branch)
170 base = get_output("git svn find-rev r%s" % brev)
172 # Apply our commits to the new branch
173 try:
174 fd, tmpfile = tempfile.mkstemp("yap")
175 os.close(fd)
176 print base[0]
177 os.system("git format-patch -k --stdout '%s' > %s"
178 % (base[0], tmpfile))
179 start = get_output("git rev-parse HEAD")
180 self.cmd_point("refs/remotes/svn/%s"
181 % branch, **{'-f': True})
183 stat = os.stat(tmpfile)
184 size = stat[6]
185 if size > 0:
186 rc = run_command("git am -3 %s" % tmpfile)
187 if (rc):
188 self.cmd_point(start[0], **{'-f': True})
189 raise YapError("Failed to port changes to new svn branch")
190 finally:
191 os.unlink(tmpfile)
193 base = get_output("git merge-base HEAD %s" % rev[0])
194 if base[0] != rev[0]:
195 raise YapError("Branch not up-to-date. Update first.")
196 current = get_output("git symbolic-ref HEAD")
197 if not current:
198 raise YapError("Not on a branch!")
199 current = current[0].replace('refs/heads/', '')
200 self._confirm_push(current, branch, "svn")
201 if run_command("git update-index --refresh"):
202 raise YapError("Can't push with uncommitted changes")
204 master = get_output("git rev-parse --verify refs/heads/master 2>/dev/null")
205 os.system("git svn dcommit")
206 run_safely("git svn rebase")
207 if not master:
208 master = get_output("git rev-parse --verify refs/heads/master 2>/dev/null")
209 if master:
210 run_safely("git update-ref -d refs/heads/master %s" % master[0])
212 def _fetch_svn(self):
213 os.system("git svn fetch svn")
214 self._create_tagged_blob()
215 self._cleanup_branches()
217 def _enabled(self):
218 enabled = get_output("git config yap.svn.enabled")
219 return bool(enabled)
221 def _applicable(self, args):
222 if not self._enabled():
223 return False
225 if args and args[0] == 'svn':
226 return True
228 if not args:
229 current = get_output("git symbolic-ref HEAD")
230 if not current:
231 raise YapError("Not on a branch!")
233 current = current[0].replace('refs/heads/', '')
234 remote, merge = self._get_tracking(current)
235 if remote == "svn":
236 return True
238 return False
240 # Ensure users don't accidentally kill our "svn" repo
241 def cmd_repo(self, *args, **flags):
242 if self._enabled():
243 if '-d' in flags and args and args[0] == "svn":
244 raise YapError("Refusing to delete special svn repository")
245 super(SvnPlugin, self).cmd_repo(*args, **flags)
247 @takes_options("r:")
248 def cmd_clone(self, *args, **flags):
249 handled = True
250 if not args:
251 handled = False
252 if (handled and not args[0].startswith("http")
253 and not args[0].startswith("svn")):
254 handled = False
255 if handled and run_command("svn info %s" % args[0]):
256 handled = False
258 if handled:
259 self._clone_svn(*args, **flags)
260 else:
261 super(SvnPlugin, self).cmd_clone(*args, **flags)
263 if self._enabled():
264 # nothing to do
265 return
267 run_safely("git fetch origin --tags")
268 hash = get_output("git rev-parse --verify refs/tags/yap-svn 2>/dev/null")
269 if not hash:
270 return
272 fd = os.popen("git cat-file blob %s" % hash[0])
273 blob = pickle.load(fd)
274 for k, v in blob.keys.items():
275 run_safely("git config %s %s" % (k, v))
277 self.cmd_repo("svn", blob.keys['svn-remote.svn.url'])
278 os.system("git config yap.svn.enabled 1")
279 run_safely("git fetch origin 'refs/remotes/svn/*:refs/remotes/svn/*'")
281 for b in blob.metadata.keys():
282 branch = os.path.join(".git", "svn", "svn", b)
283 os.makedirs(branch)
284 fd = file(os.path.join(branch, ".rev_map.%s" % blob.uuid), "w")
286 rev, metadata = blob.metadata[b]
287 fd.write(metadata)
288 run_command("git update-ref refs/remotes/svn/%s %s" % (b, rev))
290 def cmd_fetch(self, *args, **flags):
291 if self._applicable(args):
292 self._fetch_svn()
293 return
295 super(SvnPlugin, self).cmd_fetch(*args, **flags)
297 def cmd_push(self, *args, **flags):
298 if self._applicable(args):
299 if len (args) >= 2:
300 merge = args[1]
301 else:
302 current = get_output("git symbolic-ref HEAD")
303 if not current:
304 raise YapError("Not on a branch!")
306 current = current[0].replace('refs/heads/', '')
307 remote, merge = self._get_tracking(current)
308 if remote != "svn":
309 raise YapError("Need a branch name")
310 self._push_svn(merge, **flags)
311 return
312 super(SvnPlugin, self).cmd_push(*args, **flags)
314 @short_help("change options for the svn plugin")
315 def cmd_svn(self, subcmd):
316 "enable"
318 if subcmd not in ["enable"]:
319 raise TypeError
321 if "svn" in [x[0] for x in self._list_remotes()]:
322 raise YapError("A remote named 'svn' already exists")
325 if not run_command("git config svn-remote.svn.branches"):
326 raise YapError("Cannot currently enable in a repository with svn branches")
328 url = get_output("git config svn-remote.svn.url")
329 if not url:
330 raise YapError("Not a git-svn repository?")
331 fetch = get_output("git config svn-remote.svn.fetch")
332 assert fetch
333 lhs, rhs = fetch[0].split(':')
336 rev = get_output("git rev-parse %s" % rhs)
337 assert rev
338 run_safely("git update-ref refs/remotes/svn/trunk %s" % rev[0])
340 url = '/'.join((url[0], lhs))
341 self._configure_repo(url)
342 run_safely("git update-ref -d %s %s" % (rhs, rev[0]))
344 # We are intentionally overriding a yap utility function
345 def _resolve_rev(self, *args, **flags):
346 rev = None
347 if args:
348 m = self.revpat.match(args[0])
349 if m is not None:
350 revnum = int(m.group(1))
351 rev = get_output("git svn find-rev r%d 2>/dev/null" % revnum)
353 if not rev:
354 gitdir = get_output("git rev-parse --git-dir")
355 assert gitdir
356 revmaps = os.path.join(gitdir[0], "svn", "svn",
357 "*", ".rev_map*")
358 print revmaps
359 revmaps = glob.glob(revmaps)
361 for f in revmaps:
362 rm = SVNRevMap(f)
363 idx = bisect.bisect_left(rm, revnum)
364 if idx >= len(rm) or rm[idx] != revnum:
365 continue
367 revnum, rev = rm.get_record(idx)
368 if hash == "0" * 40:
369 continue
370 break
371 pass
373 if not rev:
374 rev = super(SvnPlugin, self)._resolve_rev(*args, **flags)
375 return rev