Skip to content

Commit 6c9b01f

Browse files
committed
1 parent 8e6f72a commit 6c9b01f

File tree

2 files changed

+104
-0
lines changed

2 files changed

+104
-0
lines changed

python/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@ These Python scripts can be run directly from their URLs using `uv run`.
44

55
Their source code is [available on GitHub](https://github.com/simonw/tools/tree/main/python).
66

7+
## git_read_only_http.py
8+
9+
Serve a local Git repository over HTTP in read-only mode.
10+
11+
```bash
12+
uv run https://tools.simonwillison.net/python/git_read_only_http.py \
13+
/path/to/repo
14+
```
15+
Defaults to serving on `localhost:8000`. Use `-H/--host` and `-p/--port` to change that.
16+
17+
You can then clone the repo like this:
18+
```bash
19+
git clone http://localhost:8000/ directory_name
20+
```
21+
22+
[Claude transcript](https://claude.ai/share/95001fd2-24ea-427e-91f0-6a76f97206e7).
23+
724
## claude_code_to_gist.py
825

926
```bash

python/git_read_only_http.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/env python3
2+
"""Minimal HTTP server for git repositories using git-http-backend."""
3+
4+
import argparse
5+
import io
6+
import os
7+
import subprocess
8+
from http.client import parse_headers
9+
from socketserver import ThreadingMixIn
10+
from wsgiref.simple_server import make_server, WSGIServer
11+
12+
13+
class ThreadedWSGIServer(ThreadingMixIn, WSGIServer):
14+
daemon_threads = True
15+
16+
17+
def git_http_app(repo_path):
18+
"""Create a WSGI app that serves a git repo via git-http-backend."""
19+
repo_path = os.path.abspath(repo_path)
20+
repo_name = os.path.basename(repo_path)
21+
project_root = os.path.dirname(repo_path)
22+
23+
# Find git-http-backend
24+
exec_path = subprocess.check_output(["git", "--exec-path"], text=True).strip()
25+
backend = os.path.join(exec_path, "git-http-backend")
26+
27+
def app(environ, start_response):
28+
path_info = "/" + repo_name + environ.get("PATH_INFO", "")
29+
30+
env = {
31+
"GIT_PROJECT_ROOT": project_root,
32+
"GIT_HTTP_EXPORT_ALL": "1",
33+
"PATH_INFO": path_info,
34+
"REQUEST_METHOD": environ.get("REQUEST_METHOD", "GET"),
35+
"QUERY_STRING": environ.get("QUERY_STRING", ""),
36+
"CONTENT_TYPE": environ.get("CONTENT_TYPE", ""),
37+
"CONTENT_LENGTH": environ.get("CONTENT_LENGTH", ""),
38+
**{k: v for k, v in environ.items() if k.startswith("HTTP_")},
39+
}
40+
41+
# Read request body
42+
body_input = b""
43+
if env["CONTENT_LENGTH"]:
44+
try:
45+
body_input = environ["wsgi.input"].read(int(env["CONTENT_LENGTH"]))
46+
except (ValueError, TypeError):
47+
pass
48+
49+
proc = subprocess.run(
50+
[backend], env={**os.environ, **env}, input=body_input, capture_output=True
51+
)
52+
53+
# Parse CGI response: headers\n\nbody
54+
header_end = proc.stdout.find(b"\r\n\r\n")
55+
sep_len = 4
56+
if header_end == -1:
57+
header_end = proc.stdout.find(b"\n\n")
58+
sep_len = 2
59+
60+
if header_end == -1:
61+
start_response("200 OK", [("Content-Type", "application/octet-stream")])
62+
return [proc.stdout]
63+
64+
headers = parse_headers(io.BytesIO(proc.stdout[:header_end] + b"\r\n\r\n"))
65+
body = proc.stdout[header_end + sep_len :]
66+
67+
status = headers.get("Status", "200 OK")
68+
header_list = [(k, v) for k, v in headers.items() if k.lower() != "status"]
69+
70+
start_response(status, header_list)
71+
return [body]
72+
73+
return app
74+
75+
76+
if __name__ == "__main__":
77+
p = argparse.ArgumentParser(description="Serve a git repo over HTTP (read-only)")
78+
p.add_argument("repo", help="Path to git repository")
79+
p.add_argument("-p", "--port", type=int, default=8000)
80+
p.add_argument("-H", "--host", default="localhost")
81+
args = p.parse_args()
82+
83+
app = git_http_app(args.repo)
84+
server = make_server(args.host, args.port, app, server_class=ThreadedWSGIServer)
85+
print(f"Serving {args.repo} at http://{args.host}:{args.port}/")
86+
print(f"Clone: git clone http://{args.host}:{args.port}/ directory_name")
87+
server.serve_forever()

0 commit comments

Comments
 (0)