This repository has been archived by the owner on Feb 25, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
/
__init__.py
executable file
·298 lines (241 loc) · 10.9 KB
/
__init__.py
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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
"""
git_remote_hg: access hg repositories as git remotes
=====================================================
Are you a git junkie forced to work on projects hosted in mercurial repos?
Are you too lazy, stubborn or maladjusted to learn another VCS tool?
Fear not! This script will let you interact with mercurial repositories as
if they were ordinary git remotes.
Git allows pluggable remote repository protocols via helper scripts. If you
have a script named "git-remote-XXX" then git will use it to interact with
remote repositories whose URLs are of the form XXX::some-url-here. So you
can imagine what a script named git-remote-hg will do.
Yes, this script provides a remote repository implementation that communicates
with mercurial. Install it and you can do::
$ git clone hg::https://hg.example.com/some-mercurial-repo
$ cd some-hg-repo
$ # hackety hackety hack
$ git commit -a
$ git push
Tada! Your commits from git will show up in the remote mercurial repo, and
none of your co-workers will be any the wiser.
All the hard work of interoperating between git and mercurial is done by the
awesome hg-git module. All the hard work of speaking the git-remote-helper
protocol is done by git's own http-protocol handlers. This script just hacks
them together to make it all work a little easier.
For each remote mercurial repository, you actually get *two* additional
repositories hidden inside your local git repo:
* .git/hgremotes/[URL]: a local hg clone of the remote repo
* .git/hgremotes/[URL]/.hg/git: a bare git repo managed by hg-git
When you "git push" from your local git repo into the remote mercurial repo,
here is what git-remote-hg will do for you:
* use git-remote-http to push into .git/hgremotes/[URL]/.hg/git
* call "hg gimport" to import changes into .git/hgremotes/[URL]
* call "hg push" to push them up to the remote repo
Likewise, when you "git pull" from the remote mercurial repo into your local
git repo, here is what happens under the hood:
* call "hg pull" to pull changes from the remote repo
* call "hg gexport" to export them into .git/hgremotes/[URL]/.hg/git
* use git-remote-http to pull them into your local repo
Ugly? Sure. Hacky? You bet. But it seems to work remarkably well.
By the way, there is apparently a native implementation of a git-remote-hg
command in development:
https://plus.google.com/115991361267198418069/posts/Jpzi24bYU91
Since the git-remote-helper protocol is pretty simple, it should be possible
to switch back and forth between that implementation and this one without any
hassle.
WARNINGS:
* Pushing multiple branches into the remote is currently broken.
hg-git seems to map git branches onto mercurial bookmarks, but I'm not
sure of all the details. I don't need it so I haven't tried to make it
work. Don't do it.
"""
__ver_major__ = 0
__ver_minor__ = 1
__ver_patch__ = 0
__ver_sub__ = ""
__version__ = "%d.%d.%d%s" % (__ver_major__,__ver_minor__,__ver_patch__,__ver_sub__)
import sys
import os
import subprocess
import threading
import socket
import time
import urllib
import wsgiref.simple_server
from textwrap import dedent
def main(argv=None, git_dir=None):
"""Main entry-point for the git-remote-hg script.
This function can be called to act as a git-remote-helper script that
will communicate with a remote mercurial repository. It basically does
the following:
* ensure there's a local hg checkout in .git/hgremotes/[URL]
* ensure that it has a matching hg-git repo for import/export
* update the hg-git repo from the remote mercurial repo
* start a background thread running git-http-backend to communicate
with the hg-git repo
* shell out to hg-remote-http to push/pull into the hg-git repo
* send any changes from the hg-git repo back to the remote
Simple, right?
"""
if argv is None:
argv = sys.argv
if git_dir is None:
git_dir = os.environ.get("GIT_DIR", None)
if git_dir is None:
git_dir = os.getcwd()
if os.path.exists(os.path.join(git_dir, ".git")):
git_dir = os.path.join(git_dir, ".git")
# AFAICT, we always get the hg repo url as the second argument.
hg_url = argv[2]
# Grab the local hg-git checkout, creating it if necessary.
hg_checkout = HgGitCheckout(git_dir, hg_url)
# Start git-http-backend to push/pull into the hg-git checkout.
backend = GitHttpBackend(hg_checkout.git_repo_dir)
t = backend.start()
try:
# Wait for the server to come up.
while backend.repo_url is None:
time.sleep(0.1)
# Grab any updates from the remote repo.
# Do it unconditionally for now, so we don't have to interpret
# the incoming hg-remote-helper stream to determine push/pull.
hg_checkout.pull()
# Use git-remote-http to send all commands to the HTTP server.
# This will push any incoming changes into the hg-git checkout.
cmd = ("git", "remote-http", backend.repo_url, )
retcode = subprocess.call(cmd, env=os.environ)
# TODO: what are valid return codes? Seems to be almost always 1.
if retcode not in (0, 1):
msg = "git-remote-http failed with error code %d" % (retcode,)
raise RuntimeError(msg)
# If that worked OK, push any changes up to the remote URL.
# Do it unconditionally for now, so we don't have to interpret
# the incoming hg-remote-helper stream to determine push/pull.
hg_checkout.push()
finally:
# Make sure we tearn down the HTTP server before quitting.
backend.stop()
t.join()
class HgGitCheckout(object):
"""Class managing a local hg-git checkout.
Given the path of a local git repository and the URL of a remote hg
repository, this class manages a hidden hg-git checkout that can be
used to shuffle changes between the two.
"""
def __init__(self, git_dir, hg_url):
self.hg_url = hg_url
self.hg_name = hg_name = urllib.quote(hg_url, safe="")
self.hg_repo_dir = os.path.join(git_dir, "hgremotes", hg_name)
if not os.path.exists(self.hg_repo_dir):
self.initialize_hg_repo()
self.git_repo_dir = os.path.join(self.hg_repo_dir, ".hg", "git")
def _do(self, *cmd, **kwds):
"""Run a hg command, capturing and reporting output."""
silent = kwds.pop("silent", False)
kwds["stdout"] = subprocess.PIPE
kwds["stderr"] = subprocess.STDOUT
p = subprocess.Popen(cmd, **kwds)
output = p.stdout.readline()
while output:
if not silent:
print>>sys.stderr, "hg: " + output.strip()
output = p.stdout.readline()
p.wait()
def pull(self):
"""Grab any changes from the remote repository."""
hg_repo_dir = self.hg_repo_dir
self._do("hg", "pull", cwd=hg_repo_dir)
self._do("hg", "bookmark", "-fr", "default", "master", cwd=hg_repo_dir)
self._do("hg", "gexport", cwd=hg_repo_dir)
def push(self):
"""Push any changes into the remote repository."""
hg_repo_dir = self.hg_repo_dir
self._do("hg", "gimport", cwd=hg_repo_dir)
self._do("hg", "push", cwd=hg_repo_dir)
def initialize_hg_repo(self):
hg_repo_dir = self.hg_repo_dir
if not os.path.isdir(os.path.dirname(hg_repo_dir)):
os.makedirs(os.path.dirname(hg_repo_dir))
self._do("hg", "clone", self.hg_url, hg_repo_dir)
self._do("hg", "update", "null", cwd=hg_repo_dir, silent=True)
with open(os.path.join(hg_repo_dir, "README.txt"), "wt") as f:
f.write(dedent("""
This is a bare mercurial checkout created by git-remote-hg.
Don't mess with it unless you know what you're doing.
"""))
with open(os.path.join(hg_repo_dir, ".hg", "hgrc"), "at") as f:
f.write(dedent("""
[extensions]
hgext.bookmarks =
hggit =
"""))
self._do("hg", "bookmark", "-r", "default", "master", cwd=hg_repo_dir)
self._do("hg", "gexport", cwd=hg_repo_dir)
class SilentWSGIRequestHandler(wsgiref.simple_server.WSGIRequestHandler):
"""WSGIRequestHandler that doesn't print to stderr for each request."""
def log_message(self, format, *args):
pass
class GitHttpBackend(object):
"""Run git-http-backend in a background thread.
This helper class lets us run the git-http-backend server in a background
thread, bound to a local tcp port. The main thread can then interact
with it as needed.
"""
def __init__(self, git_dir):
self.git_dir = os.path.abspath(git_dir)
self.git_project_root = os.path.dirname(self.git_dir)
self.git_project_name = os.path.basename(self.git_dir)
self.server = None
self.server_url = None
self.repo_url = None
def __call__(self, environ, start_response):
"""WSGI handler.
This simply sends all requests out to git-http-backend via
standard CGI protocol. It's nasty and inefficient but good
enough for local use.
"""
cgienv = os.environ.copy()
for (k,v) in environ.iteritems():
if isinstance(v, str):
cgienv[k] = v
cgienv["GIT_PROJECT_ROOT"] = self.git_project_root
cgienv["GIT_HTTP_EXPORT_ALL"] = "ON"
cgienv["REMOTE_USER"] = "rfk"
cmd = ("git", "http-backend", )
p = subprocess.Popen(cmd, env=cgienv, cwd=self.git_dir,
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
if environ.get("CONTENT_LENGTH",None):
data = environ["wsgi.input"].read(int(environ["CONTENT_LENGTH"]))
p.stdin.write(data)
p.stdin.close()
headers = []
header = p.stdout.readline()
while header.strip():
headers.append(header.split(":", 1))
header = p.stdout.readline()
headers = [(k.strip(), v.strip()) for (k,v) in headers]
start_response("200 OK", headers)
return [p.stdout.read()]
def _make_server(self, addr, port):
make = wsgiref.simple_server.make_server
return make(addr, port, self, handler_class=SilentWSGIRequestHandler)
def run(self):
"""Run the git-http-backend server."""
port = 8091
while True:
try:
self.server = self._make_server("localhost", port)
break
except socket.error:
port += 1
self.server_url = "http://localhost:%d/" % (port,)
self.repo_url = self.server_url + self.git_project_name
self.server.serve_forever()
def start(self):
"""Run the git-http-backend server in a new thread."""
t = threading.Thread(target=self.run)
t.start()
return t
def stop(self):
"""Stop the git-http-backend server."""
self.server.shutdown()