show unstaged and unmerged files for the entire repository
[yap.git] / plugins / svn.py
blobfdb49337b74aa85e729a152610f675521746ef57
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
10 class RepoBlob(object):
11 def __init__(self, keys):
12 self.keys = keys
14 self.uuid = None
15 self.branches = None
16 self.tags = None
17 self.metadata = {}
19 def add_metadata(self, branch):
20 assert branch not in self.metadata
21 gitdir = get_output("git rev-parse --git-dir")
22 assert gitdir
23 revmap = os.path.join(gitdir[0], "svn", "svn", branch, ".rev_map*")
24 revmap = glob.glob(revmap)
25 if not revmap:
26 return
27 uuid = revmap[0].split('.')[-1]
28 if self.uuid is None:
29 self.uuid = uuid
30 assert self.uuid == uuid
31 rev = get_output("git rev-parse refs/remotes/svn/%s" % branch)
32 data = file(revmap[0]).read()
33 self.metadata[branch] = rev[0], data
35 class SvnPlugin(YapCore):
36 "Allow yap to interoperate with Subversion repositories"
37 def _get_root(self, url):
38 root = get_output("svn info %s 2>/dev/null | gawk '/Repository Root:/{print $3}'" % url)
39 if not root:
40 raise YapError("Not an SVN repo: %s" % url)
41 return root[0]
43 def _configure_repo(self, url, fetch=None):
44 root = self._get_root(url)
45 os.system("git config svn-remote.svn.url %s" % root)
46 if fetch is None:
47 trunk = url.replace(root, '').strip('/')
48 else:
49 trunk = fetch.split(':')[0]
50 os.system("git config svn-remote.svn.fetch %s:refs/remotes/svn/trunk"
51 % trunk)
53 branches = trunk.replace('trunk', 'branches')
54 if branches != trunk:
55 os.system("git config svn-remote.svn.branches %s/*:refs/remotes/svn/*" % branches)
56 tags = trunk.replace('trunk', 'tags')
57 if tags != trunk:
58 os.system("git config svn-remote.svn.tags %s/*:refs/tags/*" % tags)
59 self.cmd_repo("svn", url)
60 os.system("git config yap.svn.enabled 1")
62 def _create_tagged_blob(self):
63 keys = dict()
64 for i in get_output("git config --list | grep ^svn-remote.svn"):
65 k, v = i.split('=')
66 keys[k] = v
67 blob = RepoBlob(keys)
68 for b in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/svn/*'"):
69 b = b.replace('refs/remotes/svn/', '')
70 blob.add_metadata(b)
72 fd_w, fd_r = os.popen2("git hash-object -w --stdin")
73 pickle.dump(blob, fd_w)
74 fd_w.close()
75 hash = fd_r.readline().strip()
76 run_safely("git tag -f yap-svn %s" % hash)
78 def _cleanup_branches(self):
79 for b in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/svn/*@*'"):
80 head = b.replace('refs/remotes/svn/', '')
81 path = os.path.join(".git", "svn", "svn", head)
82 files = os.listdir(path)
83 for f in files:
84 os.unlink(os.path.join(path, f))
85 os.rmdir(path)
87 ref = get_output("git rev-parse %s" % b)
88 if ref:
89 run_safely("git update-ref -d %s %s" % (b, ref[0]))
91 def _clone_svn(self, url, directory=None, **flags):
92 url = url.rstrip('/')
93 if directory is None:
94 directory = url.rsplit('/')[-1]
95 directory = directory.replace('.git', '')
97 try:
98 os.mkdir(directory)
99 except OSError:
100 raise YapError("Directory exists: %s" % directory)
101 os.chdir(directory)
103 self.cmd_init()
104 run_command("git config svn-remote.svn.noMetadata 1")
105 self._configure_repo(url)
106 os.system("git svn fetch -r %s:HEAD" % flags.get('-r', '1'))
108 self._cleanup_branches()
109 self._create_tagged_blob()
111 def _push_svn(self, branch, **flags):
112 if '-d' in flags:
113 raise YapError("Deleting svn branches not supported")
114 print "Verifying branch is up-to-date"
115 run_safely("git svn fetch svn")
117 branch = branch.replace('refs/heads/', '')
118 rev = get_output("git rev-parse --verify refs/remotes/svn/%s" % branch)
120 # Create the branch if requested
121 if not rev:
122 if '-c' not in flags:
123 raise YapError("No matching branch on the repo. Use -c to create a new branch there.")
124 src = get_output("git svn info | gawk '/URL:/{print $2}'")[0]
125 brev = get_output("git svn info | gawk '/Revision:/{print $2}'")[0]
126 root = get_output("git config svn-remote.svn.url")[0]
127 branch_path = get_output("git config svn-remote.svn.branches")[0].split(':')[0]
128 branch_path = branch_path.rstrip('/*')
129 dst = '/'.join((root, branch_path, branch))
131 # Create the branch in svn
132 run_safely("svn cp -r%s %s %s -m 'create branch %s'"
133 % (brev, src, dst, branch))
134 run_safely("git svn fetch svn")
135 rev = get_output("git rev-parse refs/remotes/svn/%s 2>/dev/null" % branch)
136 base = get_output("git svn find-rev r%s" % brev)
138 # Apply our commits to the new branch
139 try:
140 fd, tmpfile = tempfile.mkstemp("yap")
141 os.close(fd)
142 print base[0]
143 os.system("git format-patch -k --stdout '%s' > %s"
144 % (base[0], tmpfile))
145 start = get_output("git rev-parse HEAD")
146 self.cmd_point("refs/remotes/svn/%s"
147 % branch, **{'-f': True})
149 stat = os.stat(tmpfile)
150 size = stat[6]
151 if size > 0:
152 rc = run_command("git am -3 %s" % tmpfile)
153 if (rc):
154 self.cmd_point(start[0], **{'-f': True})
155 raise YapError("Failed to port changes to new svn branch")
156 finally:
157 os.unlink(tmpfile)
159 base = get_output("git merge-base HEAD %s" % rev[0])
160 if base[0] != rev[0]:
161 raise YapError("Branch not up-to-date. Update first.")
162 current = get_output("git symbolic-ref HEAD")
163 if not current:
164 raise YapError("Not on a branch!")
165 current = current[0].replace('refs/heads/', '')
166 self._confirm_push(current, branch, "svn")
167 if run_command("git update-index --refresh"):
168 raise YapError("Can't push with uncommitted changes")
170 master = get_output("git rev-parse --verify refs/heads/master 2>/dev/null")
171 os.system("git svn dcommit")
172 run_safely("git svn rebase")
173 if not master:
174 master = get_output("git rev-parse --verify refs/heads/master 2>/dev/null")
175 if master:
176 run_safely("git update-ref -d refs/heads/master %s" % master[0])
178 def _fetch_svn(self):
179 os.system("git svn fetch svn")
180 self._create_tagged_blob()
181 self._cleanup_branches()
183 def _enabled(self):
184 enabled = get_output("git config yap.svn.enabled")
185 return bool(enabled)
187 def _applicable(self, args):
188 if not self._enabled():
189 return False
191 if args and args[0] == 'svn':
192 return True
194 if not args:
195 current = get_output("git symbolic-ref HEAD")
196 if not current:
197 raise YapError("Not on a branch!")
199 current = current[0].replace('refs/heads/', '')
200 remote, merge = self._get_tracking(current)
201 if remote == "svn":
202 return True
204 return False
206 # Ensure users don't accidentally kill our "svn" repo
207 def cmd_repo(self, *args, **flags):
208 if self._enabled():
209 if '-d' in flags and args and args[0] == "svn":
210 raise YapError("Refusing to delete special svn repository")
211 super(SvnPlugin, self).cmd_repo(*args, **flags)
213 @takes_options("r:")
214 def cmd_clone(self, *args, **flags):
215 handled = True
216 if not args:
217 handled = False
218 if (handled and not args[0].startswith("http")
219 and not args[0].startswith("svn")):
220 handled = False
221 if handled and run_command("svn info %s" % args[0]):
222 handled = False
224 if handled:
225 self._clone_svn(*args, **flags)
226 else:
227 super(SvnPlugin, self).cmd_clone(*args, **flags)
229 if self._enabled():
230 # nothing to do
231 return
233 run_safely("git fetch origin --tags")
234 hash = get_output("git rev-parse --verify refs/tags/yap-svn 2>/dev/null")
235 if not hash:
236 return
238 fd = os.popen("git cat-file blob %s" % hash[0])
239 blob = pickle.load(fd)
240 for k, v in blob.keys.items():
241 run_safely("git config %s %s" % (k, v))
243 self.cmd_repo("svn", blob.keys['svn-remote.svn.url'])
244 os.system("git config yap.svn.enabled 1")
245 run_safely("git fetch origin 'refs/remotes/svn/*:refs/remotes/svn/*'")
247 for b in blob.metadata.keys():
248 branch = os.path.join(".git", "svn", "svn", b)
249 os.makedirs(branch)
250 fd = file(os.path.join(branch, ".rev_map.%s" % blob.uuid), "w")
252 rev, metadata = blob.metadata[b]
253 fd.write(metadata)
254 run_command("git update-ref refs/remotes/svn/%s %s" % (b, rev))
256 def cmd_fetch(self, *args, **flags):
257 if self._applicable(args):
258 self._fetch_svn()
259 return
261 super(SvnPlugin, self).cmd_fetch(*args, **flags)
263 def cmd_push(self, *args, **flags):
264 if self._applicable(args):
265 if len (args) >= 2:
266 merge = args[1]
267 else:
268 current = get_output("git symbolic-ref HEAD")
269 if not current:
270 raise YapError("Not on a branch!")
272 current = current[0].replace('refs/heads/', '')
273 remote, merge = self._get_tracking(current)
274 if remote != "svn":
275 raise YapError("Need a branch name")
276 self._push_svn(merge, **flags)
277 return
278 super(SvnPlugin, self).cmd_push(*args, **flags)
280 @short_help("change options for the svn plugin")
281 def cmd_svn(self, subcmd):
282 "enable"
284 if subcmd not in ["enable"]:
285 raise TypeError
287 if "svn" in [x[0] for x in self._list_remotes()]:
288 raise YapError("A remote named 'svn' already exists")
291 if not run_command("git config svn-remote.svn.branches"):
292 raise YapError("Cannot currently enable in a repository with svn branches")
294 url = get_output("git config svn-remote.svn.url")
295 if not url:
296 raise YapError("Not a git-svn repository?")
297 fetch = get_output("git config svn-remote.svn.fetch")
298 assert fetch
299 lhs, rhs = fetch[0].split(':')
302 rev = get_output("git rev-parse %s" % rhs)
303 assert rev
304 run_safely("git update-ref refs/remotes/svn/trunk %s" % rev[0])
306 url = '/'.join((url[0], lhs))
307 self._configure_repo(url)
308 run_safely("git update-ref -d %s %s" % (rhs, rev[0]))