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
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
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)