]>
Commit | Line | Data |
---|---|---|
7aeaa2fc SR |
1 | #!/usr/bin/env python |
2 | ||
9609dc9d MM |
3 | # This command is a simple remote-helper, that is used both as a |
4 | # testcase for the remote-helper functionality, and as an example to | |
5 | # show remote-helper authors one possible implementation. | |
6 | # | |
7 | # This is a Git <-> Git importer/exporter, that simply uses git | |
8 | # fast-import and git fast-export to consume and produce fast-import | |
9 | # streams. | |
10 | # | |
11 | # To understand better the way things work, one can activate debug | |
12 | # traces by setting (to any value) the environment variables | |
13 | # GIT_TRANSPORT_HELPER_DEBUG and GIT_DEBUG_TESTGIT, to see messages | |
14 | # from the transport-helper side, or from this example remote-helper. | |
15 | ||
23b093ee BC |
16 | # hashlib is only available in python >= 2.5 |
17 | try: | |
18 | import hashlib | |
19 | _digest = hashlib.sha1 | |
20 | except ImportError: | |
21 | import sha | |
22 | _digest = sha.new | |
7aeaa2fc | 23 | import sys |
f733f6a0 | 24 | import os |
7fb8e163 | 25 | import time |
f733f6a0 | 26 | sys.path.insert(0, os.getenv("GITPYTHONLIB",".")) |
7aeaa2fc SR |
27 | |
28 | from git_remote_helpers.util import die, debug, warn | |
29 | from git_remote_helpers.git.repo import GitRepo | |
30 | from git_remote_helpers.git.exporter import GitExporter | |
31 | from git_remote_helpers.git.importer import GitImporter | |
32 | from git_remote_helpers.git.non_local import NonLocalGit | |
33 | ||
0846b0c9 JK |
34 | if sys.hexversion < 0x02000000: |
35 | # string.encode() is the limiter | |
36 | sys.stderr.write("git-remote-testgit: requires Python 2.0 or later.\n") | |
a33faf28 ER |
37 | sys.exit(1) |
38 | ||
3ac221a7 JK |
39 | |
40 | def encode_filepath(path): | |
41 | """Encodes a Unicode file path to a byte string. | |
42 | ||
43 | On Python 2 this is a no-op; on Python 3 we encode the string as | |
44 | suggested by [1] which allows an exact round-trip from the command line | |
45 | to the filesystem. | |
46 | ||
47 | [1] http://docs.python.org/3/c-api/unicode.html#file-system-encoding | |
48 | ||
49 | """ | |
50 | if sys.hexversion < 0x03000000: | |
51 | return path | |
52 | return path.encode(sys.getfilesystemencoding(), 'surrogateescape') | |
53 | ||
54 | ||
7aeaa2fc SR |
55 | def get_repo(alias, url): |
56 | """Returns a git repository object initialized for usage. | |
57 | """ | |
58 | ||
59 | repo = GitRepo(url) | |
60 | repo.get_revs() | |
61 | repo.get_head() | |
62 | ||
23b093ee | 63 | hasher = _digest() |
3ac221a7 | 64 | hasher.update(encode_filepath(repo.path)) |
7aeaa2fc SR |
65 | repo.hash = hasher.hexdigest() |
66 | ||
67 | repo.get_base_path = lambda base: os.path.join( | |
68 | base, 'info', 'fast-import', repo.hash) | |
69 | ||
70 | prefix = 'refs/testgit/%s/' % alias | |
71 | debug("prefix: '%s'", prefix) | |
72 | ||
e1735872 | 73 | repo.gitdir = os.environ["GIT_DIR"] |
7aeaa2fc SR |
74 | repo.alias = alias |
75 | repo.prefix = prefix | |
76 | ||
77 | repo.exporter = GitExporter(repo) | |
78 | repo.importer = GitImporter(repo) | |
79 | repo.non_local = NonLocalGit(repo) | |
80 | ||
81 | return repo | |
82 | ||
83 | ||
84 | def local_repo(repo, path): | |
85 | """Returns a git repository object initalized for usage. | |
86 | """ | |
87 | ||
88 | local = GitRepo(path) | |
89 | ||
90 | local.non_local = None | |
91 | local.gitdir = repo.gitdir | |
92 | local.alias = repo.alias | |
93 | local.prefix = repo.prefix | |
94 | local.hash = repo.hash | |
95 | local.get_base_path = repo.get_base_path | |
96 | local.exporter = GitExporter(local) | |
97 | local.importer = GitImporter(local) | |
98 | ||
99 | return local | |
100 | ||
101 | ||
102 | def do_capabilities(repo, args): | |
103 | """Prints the supported capabilities. | |
104 | """ | |
105 | ||
f9640ac2 JK |
106 | print("import") |
107 | print("export") | |
108 | print("refspec refs/heads/*:%s*" % repo.prefix) | |
7aeaa2fc | 109 | |
a515ebe9 SR |
110 | dirname = repo.get_base_path(repo.gitdir) |
111 | ||
112 | if not os.path.exists(dirname): | |
113 | os.makedirs(dirname) | |
114 | ||
3b705268 | 115 | path = os.path.join(dirname, 'git.marks') |
a515ebe9 | 116 | |
f9640ac2 | 117 | print("*export-marks %s" % path) |
a515ebe9 | 118 | if os.path.exists(path): |
f9640ac2 | 119 | print("*import-marks %s" % path) |
a515ebe9 | 120 | |
f9640ac2 | 121 | print('') # end capabilities |
7aeaa2fc SR |
122 | |
123 | ||
124 | def do_list(repo, args): | |
125 | """Lists all known references. | |
126 | ||
127 | Bug: This will always set the remote head to master for non-local | |
128 | repositories, since we have no way of determining what the remote | |
129 | head is at clone time. | |
130 | """ | |
131 | ||
132 | for ref in repo.revs: | |
133 | debug("? refs/heads/%s", ref) | |
f9640ac2 | 134 | print("? refs/heads/%s" % ref) |
7aeaa2fc SR |
135 | |
136 | if repo.head: | |
137 | debug("@refs/heads/%s HEAD" % repo.head) | |
f9640ac2 | 138 | print("@refs/heads/%s HEAD" % repo.head) |
7aeaa2fc SR |
139 | else: |
140 | debug("@refs/heads/master HEAD") | |
f9640ac2 | 141 | print("@refs/heads/master HEAD") |
7aeaa2fc | 142 | |
f9640ac2 | 143 | print('') # end list |
7aeaa2fc SR |
144 | |
145 | ||
146 | def update_local_repo(repo): | |
147 | """Updates (or clones) a local repo. | |
148 | """ | |
149 | ||
150 | if repo.local: | |
151 | return repo | |
152 | ||
153 | path = repo.non_local.clone(repo.gitdir) | |
154 | repo.non_local.update(repo.gitdir) | |
155 | repo = local_repo(repo, path) | |
156 | return repo | |
157 | ||
158 | ||
159 | def do_import(repo, args): | |
160 | """Exports a fast-import stream from testgit for git to import. | |
161 | """ | |
162 | ||
163 | if len(args) != 1: | |
164 | die("Import needs exactly one ref") | |
165 | ||
166 | if not repo.gitdir: | |
167 | die("Need gitdir to import") | |
168 | ||
9504bc9d SR |
169 | ref = args[0] |
170 | refs = [ref] | |
171 | ||
172 | while True: | |
d04c94a2 | 173 | line = sys.stdin.readline().decode() |
9504bc9d SR |
174 | if line == '\n': |
175 | break | |
176 | if not line.startswith('import '): | |
177 | die("Expected import line.") | |
178 | ||
179 | # strip of leading 'import ' | |
180 | ref = line[7:].strip() | |
181 | refs.append(ref) | |
182 | ||
f9640ac2 | 183 | print("feature done") |
6c323322 FC |
184 | |
185 | if os.environ.get("GIT_REMOTE_TESTGIT_FAILURE"): | |
186 | die('Told to fail') | |
187 | ||
7aeaa2fc | 188 | repo = update_local_repo(repo) |
9504bc9d | 189 | repo.exporter.export_repo(repo.gitdir, refs) |
7aeaa2fc | 190 | |
f9640ac2 | 191 | print("done") |
1f25c504 | 192 | |
7aeaa2fc SR |
193 | |
194 | def do_export(repo, args): | |
195 | """Imports a fast-import stream from git to testgit. | |
196 | """ | |
197 | ||
198 | if not repo.gitdir: | |
199 | die("Need gitdir to export") | |
200 | ||
6c323322 FC |
201 | if os.environ.get("GIT_REMOTE_TESTGIT_FAILURE"): |
202 | die('Told to fail') | |
203 | ||
7aeaa2fc | 204 | update_local_repo(repo) |
6c8151a3 | 205 | changed = repo.importer.do_import(repo.gitdir) |
0fb56ce7 SR |
206 | |
207 | if not repo.local: | |
208 | repo.non_local.push(repo.gitdir) | |
7aeaa2fc | 209 | |
6c8151a3 | 210 | for ref in changed: |
f9640ac2 JK |
211 | print("ok %s" % ref) |
212 | print('') | |
6c8151a3 | 213 | |
7aeaa2fc | 214 | |
7aeaa2fc SR |
215 | COMMANDS = { |
216 | 'capabilities': do_capabilities, | |
217 | 'list': do_list, | |
218 | 'import': do_import, | |
219 | 'export': do_export, | |
7aeaa2fc SR |
220 | } |
221 | ||
222 | ||
223 | def sanitize(value): | |
224 | """Cleans up the url. | |
225 | """ | |
226 | ||
227 | if value.startswith('testgit::'): | |
228 | value = value[9:] | |
229 | ||
230 | return value | |
231 | ||
232 | ||
233 | def read_one_line(repo): | |
234 | """Reads and processes one command. | |
235 | """ | |
236 | ||
7fb8e163 PW |
237 | sleepy = os.environ.get("GIT_REMOTE_TESTGIT_SLEEPY") |
238 | if sleepy: | |
239 | debug("Sleeping %d sec before readline" % int(sleepy)) | |
240 | time.sleep(int(sleepy)) | |
241 | ||
7aeaa2fc SR |
242 | line = sys.stdin.readline() |
243 | ||
d04c94a2 | 244 | cmdline = line.decode() |
7aeaa2fc SR |
245 | |
246 | if not cmdline: | |
247 | warn("Unexpected EOF") | |
248 | return False | |
249 | ||
250 | cmdline = cmdline.strip().split() | |
251 | if not cmdline: | |
252 | # Blank line means we're about to quit | |
253 | return False | |
254 | ||
255 | cmd = cmdline.pop(0) | |
256 | debug("Got command '%s' with args '%s'", cmd, ' '.join(cmdline)) | |
257 | ||
258 | if cmd not in COMMANDS: | |
259 | die("Unknown command, %s", cmd) | |
260 | ||
261 | func = COMMANDS[cmd] | |
262 | func(repo, cmdline) | |
263 | sys.stdout.flush() | |
264 | ||
265 | return True | |
266 | ||
267 | ||
268 | def main(args): | |
269 | """Starts a new remote helper for the specified repository. | |
270 | """ | |
271 | ||
272 | if len(args) != 3: | |
273 | die("Expecting exactly three arguments.") | |
274 | sys.exit(1) | |
275 | ||
276 | if os.getenv("GIT_DEBUG_TESTGIT"): | |
277 | import git_remote_helpers.util | |
278 | git_remote_helpers.util.DEBUG = True | |
279 | ||
280 | alias = sanitize(args[1]) | |
281 | url = sanitize(args[2]) | |
282 | ||
283 | if not alias.isalnum(): | |
284 | warn("non-alnum alias '%s'", alias) | |
285 | alias = "tmp" | |
286 | ||
287 | args[1] = alias | |
288 | args[2] = url | |
289 | ||
290 | repo = get_repo(alias, url) | |
291 | ||
292 | debug("Got arguments %s", args[1:]) | |
293 | ||
294 | more = True | |
295 | ||
d04c94a2 JK |
296 | # Use binary mode since Python 3 does not permit unbuffered I/O in text |
297 | # mode. Unbuffered I/O is required to avoid data that should be going | |
298 | # to git-fast-import after an "export" command getting caught in our | |
299 | # stdin buffer instead. | |
300 | sys.stdin = os.fdopen(sys.stdin.fileno(), 'rb', 0) | |
7aeaa2fc SR |
301 | while (more): |
302 | more = read_one_line(repo) | |
303 | ||
304 | if __name__ == '__main__': | |
305 | sys.exit(main(sys.argv)) |