git-remote-hg: improve sanitation of local repo urls
[git] / git-remote-hg.py
1 #!/usr/bin/env python
2
3 import hashlib
4 import sys
5 import os
6 sys.path.insert(0, os.getenv("GITPYTHONLIB","."))
7
8 from git_remote_helpers.util import die, debug, warn
9 from git_remote_helpers.hg import util
10 from git_remote_helpers.hg.hg import GitHg
11 from git_remote_helpers.hg.exporter import GitExporter
12 from git_remote_helpers.hg.importer import GitImporter
13 from git_remote_helpers.hg.non_local import NonLocalHg
14
15
16 def get_repo(alias, url):
17     """Returns a hg.repository object initialized for usage.
18     """
19
20     try:
21         from mercurial import hg, ui
22     except ImportError:
23         die("Mercurial python libraries not installed")
24
25     remote = False
26
27     if url.startswith("remote://"):
28         remote = True
29         url = "file://%s" % url[9:]
30
31     ui = ui.ui()
32     source, revs, checkout = util.parseurl(ui.expandpath(url), ['default'])
33     repo = hg.repository(ui, source)
34     if repo.capable('branchmap'):
35         revs += repo.branchmap().keys()
36         revs = set(revs)
37
38     hasher = hashlib.sha1()
39     hasher.update(repo.path)
40     repo.hash = hasher.hexdigest()
41
42     repo.get_base_path = lambda base: os.path.join(
43         base, 'info', 'fast-import', repo.hash)
44
45     prefix = 'refs/hg/%s/' % alias
46     debug("prefix: '%s'", prefix)
47
48     repo.hg = hg
49     repo.gitdir = ""
50     repo.alias = alias
51     repo.prefix = prefix
52     repo.revs = revs
53
54     repo.git_hg = GitHg(warn)
55     repo.exporter = GitExporter(repo)
56     repo.importer = GitImporter(repo)
57     repo.non_local = NonLocalHg(repo)
58
59     repo.is_local = not remote and repo.local()
60
61     return repo
62
63
64 def local_repo(repo, path):
65     """Returns a hg.repository object initalized for usage.
66     """
67
68     local = repo.hg.repository(repo.ui, path)
69
70     local.git_hg = repo.git_hg
71     local.non_local = None
72     local.hg = repo.hg
73     local.gitdir = repo.gitdir
74     local.alias = repo.alias
75     local.prefix = repo.prefix
76     local.revs = repo.revs
77     local.hash = repo.hash
78     local.get_base_path = repo.get_base_path
79     local.exporter = GitExporter(local)
80     local.importer = GitImporter(local)
81     local.is_local = repo.is_local
82
83     return local
84
85
86 def do_capabilities(repo, args):
87     """Prints the supported capabilities.
88     """
89
90     print "import"
91     print "export"
92     print "*gitdir"
93
94     sys.stdout.flush()
95     if not read_one_line(repo):
96         die("Expected gitdir, got empty line")
97
98     print "*refspec refs/heads/*:%s*" % repo.prefix
99
100     dirname = repo.get_base_path(repo.gitdir)
101
102     if not os.path.exists(dirname):
103         os.makedirs(dirname)
104
105     path = os.path.join(dirname, 'git.marks')
106
107     print "*export-marks %s" % path
108     if os.path.exists(path):
109         print "*import-marks %s" % path
110
111     print # end capabilities
112
113
114 def do_list(repo, args):
115     """Lists all known references.
116     """
117
118     for ref in repo.revs:
119         debug("? refs/heads/%s", ref)
120         print "? refs/heads/%s" % ref
121
122     debug("@refs/heads/default HEAD")
123     print "@refs/heads/default HEAD"
124
125     print # end list
126
127
128 def update_local_repo(repo):
129     """Updates (or clones) a local repo.
130     """
131
132     if repo.is_local:
133         return repo
134
135     path = repo.non_local.clone(repo.gitdir)
136     repo.non_local.update(repo.gitdir)
137     repo = local_repo(repo, path)
138     return repo
139
140
141 def do_import(repo, args):
142     """Exports a fast-import stream from hg for git to import.
143     """
144
145     if args:
146         die("Import expects its ref seperately")
147
148     if not repo.gitdir:
149         die("Need gitdir to import")
150
151     refs = []
152
153     while True:
154         line = sys.stdin.readline()
155         if line == '\n':
156             break
157         refs.append(line.strip())
158
159     repo = update_local_repo(repo)
160     repo.exporter.setup(True, repo.gitdir, True, True)
161
162     repo.exporter.export_repo()
163     repo.exporter.export_branch('default', 'default')
164
165     repo.exporter.write_marks(repo.gitdir)
166
167     print "done"
168
169
170 def do_export(repo, args):
171     """Imports a fast-import stream from git to hg.
172     """
173
174     if not repo.gitdir:
175         die("Need gitdir to export")
176
177     refs = []
178
179     while True:
180         line = sys.stdin.readline()
181         if line == '\n':
182             break
183         refs.append(line.strip())
184
185     local_repo = update_local_repo(repo)
186     local_repo.importer.do_import(local_repo.gitdir)
187
188     if not repo.is_local:
189         repo.non_local.push(repo.gitdir)
190
191     for ref in refs:
192         print "ok %s" % ref
193
194     print
195
196
197 def do_gitdir(repo, args):
198     """Stores the location of the gitdir.
199     """
200
201     if not args:
202         die("gitdir needs an argument")
203
204     repo.gitdir = ' '.join(args)
205
206
207 COMMANDS = {
208     'capabilities': do_capabilities,
209     'list': do_list,
210     'import': do_import,
211     'export': do_export,
212     'gitdir': do_gitdir,
213 }
214
215
216 def sanitize(value):
217     """Cleans up the url.
218     """
219
220     if value.startswith('hg::'):
221         value = value[4:]
222
223     # for local URLs (no protocol), remove a terminal
224     # /.hg and make sure the path is absolute
225     if value.find("://") == -1:
226         if value.endswith("/.hg"):
227             value = value[:-4]
228         value = os.path.abspath(value)
229
230     return value
231
232
233 def read_one_line(repo):
234     """Reads and processes one command.
235     """
236
237     line = sys.stdin.readline()
238
239     cmdline = line
240
241     if not cmdline:
242         warn("Unexpected EOF")
243         return False
244
245     cmdline = cmdline.strip().split()
246     if not cmdline:
247         debug("Got empty line, quitting")
248         # Blank line means we're about to quit
249         return False
250
251     cmd = cmdline.pop(0)
252     debug("Got command '%s' with args '%s'", cmd, ' '.join(cmdline))
253
254     if cmd not in COMMANDS:
255         die("Unknown command, %s", cmd)
256
257     func = COMMANDS[cmd]
258     func(repo, cmdline)
259
260     try:
261         sys.stdout.flush()
262     except IOError, e:
263         warn("while flushing '%s' with args '%s'", str(cmd), str(cmdline))
264         die(str(e))
265
266     return True
267
268
269 def main(args):
270     """Starts a new remote helper for the specified repository.
271     """
272
273     if len(args) != 3:
274         die("Expecting exactly three arguments.")
275         sys.exit(1)
276
277     if os.getenv("GIT_DEBUG_HG"):
278         import git_remote_helpers.util
279         git_remote_helpers.util.DEBUG = True
280
281     alias = args[1]
282     url = sanitize(args[2])
283
284     if not alias.isalnum():
285         warn("non-alnum alias '%s'", alias)
286         alias = "tmp"
287
288     args[1] = alias
289     args[2] = url
290
291     repo = get_repo(alias, url)
292
293     debug("Got arguments %s", args[1:])
294
295     more = True
296
297     while (more):
298         more = read_one_line(repo)
299
300 if __name__ == '__main__':
301     sys.exit(main(sys.argv))