Mercurial > hg-git-serve
annotate gitserve.py @ 0:c1dc9d21fa57
First cut at serving hg repos with git.
| author | Paul Fisher <paul@pfish.zone> |
|---|---|
| date | Sat, 14 Feb 2026 05:48:46 -0500 |
| parents | |
| children |
| rev | line source |
|---|---|
|
0
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
1 import email.parser |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
2 import email.policy |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
3 import re |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
4 import threading |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
5 import shutil |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
6 import subprocess |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
7 import typing as t |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
8 |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
9 import mercurial.hgweb.hgweb_mod_inner as web_inner |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
10 import mercurial.hgweb.request as hgreq |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
11 import mercurial.ui as hgui |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
12 from mercurial import extensions |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
13 from mercurial import wireprotoserver |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
14 |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
15 type PermissionCheck = t.Callable[ |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
16 [web_inner.requestcontext, hgreq.parsedrequest, bytes], |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
17 None, |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
18 ] |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
19 |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
20 |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
21 CGI_VAR = re.compile(rb'[A-Z0-9_]+$') |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
22 |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
23 |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
24 def build_git_environ( |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
25 req_ctx: web_inner.requestcontext, |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
26 request: hgreq.parsedrequest, |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
27 ) -> dict[bytes, bytes]: |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
28 """Builds the environment to be sent to Git to serve HTTP.""" |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
29 fixed = { |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
30 k: v for (k, v) in request.rawenv.items() |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
31 if isinstance(v, bytes) and CGI_VAR.match(k) |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
32 } |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
33 fixed[b'GIT_HTTP_EXPORT_ALL'] = b'yes' |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
34 fixed[b'GIT_PROJECT_ROOT'] = req_ctx.repo.path |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
35 fixed[b'PATH_INFO'] = b'/git/' + request.dispatchpath |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
36 return fixed |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
37 |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
38 |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
39 def parse_cgi_response(output: t.IO[bytes]) -> t.Tuple[bytes, dict[bytes, bytes], t.IO[bytes]]: |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
40 parser = email.parser.BytesFeedParser(policy=email.policy.HTTP) |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
41 while line := output.readline(): |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
42 if not line.rstrip(b'\r\n'): |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
43 # We've reached the end of the headers. |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
44 # Leave the rest in the output for later. |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
45 break |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
46 parser.feed(line) |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
47 msg = parser.close() |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
48 status = msg.get('Status', '200 OK I guess').encode('utf-8') |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
49 del msg['Status'] # this won't raise an exception |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
50 byte_headers = {k.encode('utf-8'): v.encode('utf-8') for (k, v) in msg.items()} |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
51 return status, byte_headers, output |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
52 |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
53 |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
54 _feeds = 0 |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
55 _feeder_lock = threading.Lock() |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
56 |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
57 |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
58 def feed_count() -> int: |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
59 global _feeds |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
60 with _feeder_lock: |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
61 _feeds += 1 |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
62 return _feeds |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
63 |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
64 |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
65 def handle_git_protocol( |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
66 original: t.Callable[..., bool], |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
67 req_ctx: web_inner.requestcontext, |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
68 request: hgreq.parsedrequest, |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
69 response: hgreq.wsgiresponse, |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
70 check_permission: PermissionCheck, |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
71 ) -> bool: |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
72 if request.headers.get(b'git-protocol'): |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
73 # If a request is git, we assume we should be the one handling it. |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
74 cgi_env = build_git_environ(req_ctx, request) |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
75 http_backend = req_ctx.repo.ui.configlist( |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
76 b'git-serve', b'http-backend', default=(b'git', b'http-backend')) |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
77 is_post = request.method == b'POST' |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
78 call = subprocess.Popen( |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
79 http_backend, |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
80 close_fds=True, |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
81 stdin=subprocess.PIPE if is_post else None, |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
82 stdout=subprocess.PIPE, |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
83 env=cgi_env, |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
84 text=False, |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
85 ) |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
86 def feed(): |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
87 try: |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
88 with call.stdin as stdin: |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
89 shutil.copyfileobj(request.bodyfh, stdin) |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
90 except (OSError, BrokenPipeError): |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
91 pass # Expected; this just means it's closed. |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
92 if is_post: |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
93 threading.Thread(target=feed, name=f'git-feeder-{feed_count()}').start() |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
94 status, headers, rest = parse_cgi_response(call.stdout) |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
95 response.status = status |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
96 for k, v in headers.items(): |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
97 response.headers[k] = v |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
98 def write_the_rest(): |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
99 with call, rest: |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
100 while more := rest.read(64 * 1024): |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
101 yield more |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
102 response.setbodygen(write_the_rest()) |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
103 response.sendresponse() |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
104 return True |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
105 return original(req_ctx, request, response, check_permission) |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
106 |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
107 def uisetup(_: hgui.ui) -> None: |
|
c1dc9d21fa57
First cut at serving hg repos with git.
Paul Fisher <paul@pfish.zone>
parents:
diff
changeset
|
108 extensions.wrapfunction(wireprotoserver, 'handlewsgirequest', handle_git_protocol) |
