changeset 4:5ad58438318a

Fix issue where sometimes reads would be cut off. Also format and fix other stuff.
author Paul Fisher <paul@pfish.zone>
date Sat, 14 Feb 2026 21:20:36 -0500
parents 189f4a0bc653
children c43ce246240b
files src/git_serve/__init__.py
diffstat 1 files changed, 30 insertions(+), 46 deletions(-) [+]
line wrap: on
line diff
--- a/src/git_serve/__init__.py	Sat Feb 14 18:53:30 2026 -0500
+++ b/src/git_serve/__init__.py	Sat Feb 14 21:20:36 2026 -0500
@@ -7,7 +7,6 @@
 import re
 import shutil
 import subprocess
-import threading
 import typing as t
 
 import dulwich.refs
@@ -33,6 +32,11 @@
     GitPrelude = t.Sequence[bytes | str | os.PathLike]
 
 
+def _is_gitty(repo: hgrepo.IRepo) -> t.TypeGuard[GittyRepo]:
+    """Ensures that we have hg-git installed and active."""
+    return hasattr(repo, 'githandler')
+
+
 _CGI_VAR = re.compile(rb'[A-Z0-9_]+$')
 """Environment variables that we need to pass to git-as-cgi."""
 
@@ -56,6 +60,7 @@
 def _parse_cgi_response(
     output: t.IO[bytes],
 ) -> tuple[bytes, dict[bytes, bytes], t.IO[bytes]]:
+    """Parses a CGI response into a status, headers, and everyhting else."""
     parser = email.parser.BytesFeedParser(policy=email.policy.HTTP)
     while line := output.readline():
         if not line.rstrip(b'\r\n'):
@@ -72,30 +77,16 @@
     return status, byte_headers, output
 
 
-_feeds = 0
-_feeder_lock = threading.Lock()
-
-
-def feed_count() -> int:
-    global _feeds
-    with _feeder_lock:
-        _feeds += 1
-        return _feeds
-
-
-def git_binary(ui: hgui.ui) -> bytes:
-    return ui.config(b'git-serve', b'git', default=b'git')
-
-
-def handle_git_protocol(
+def _handle_git_protocol(
     original: t.Callable[..., bool],
     req_ctx: web_inner.requestcontext,
     request: hgreq.parsedrequest,
     response: hgreq.wsgiresponse,
     check_permission: PermissionCheck,
 ) -> bool:
+    """Intercepts requests from Git, if needed."""
     repo = req_ctx.repo
-    if not is_gitty(repo) or b'git-protocol' not in request.headers:
+    if not _is_gitty(repo) or b'git-protocol' not in request.headers:
         # We only handle Git requests; everything else is normal.
         return original(req_ctx, request, response, check_permission)
     check_permission(req_ctx, request, b'pull')
@@ -104,27 +95,21 @@
     http_backend = req_ctx.repo.ui.configlist(
         b'git-serve', b'http-backend', default=(b'git', b'http-backend')
     )
-    is_post = request.method == b'POST'
     call = subprocess.Popen(
         http_backend,
         close_fds=True,
-        stdin=subprocess.PIPE if is_post else None,
+        stdin=subprocess.PIPE,
         stdout=subprocess.PIPE,
         stderr=subprocess.DEVNULL,
         env=cgi_env,
         text=False,
     )
     assert call.stdout
+    assert call.stdin
+    # Git will not start writing output until stdin is fully closed.
+    with call.stdin:
+        shutil.copyfileobj(request.bodyfh, call.stdin)
 
-    def feed():
-        try:
-            with call.stdin as stdin:
-                shutil.copyfileobj(request.bodyfh, stdin)
-        except (OSError, BrokenPipeError):
-            pass  # Expected; this just means it's closed.
-
-    if is_post:
-        threading.Thread(target=feed, name=f'git-feeder-{feed_count()}').start()
     status, headers, rest = _parse_cgi_response(call.stdout)
     response.status = status
     for k, v in headers.items():
@@ -132,7 +117,7 @@
 
     def write_the_rest():
         with call, rest:
-            while more := rest.read(64 * 1024):
+            while more := rest.read(1024 * 1024):
                 yield more
 
     response.setbodygen(write_the_rest())
@@ -140,12 +125,13 @@
     return True
 
 
-def clean_all_refs(refs: dulwich.refs.RefsContainer) -> None:
+def _clean_all_refs(refs: dulwich.refs.RefsContainer) -> None:
+    """Removes all refs from the Git repository."""
     for ref in refs.allkeys():
         refs.remove_if_equals(ref, None)
 
 
-def set_head(repo: GittyRepo) -> None:
+def _set_head(repo: GittyRepo) -> None:
     """Creates a HEAD reference in Git referring to the current HEAD."""
     # By default, we use '@', since that's what will be auto checked out.
     current = b'@'
@@ -185,28 +171,26 @@
     refs.set_symbolic_ref(b'HEAD', git_branch)
 
 
-def export_hook(ui: hgui.ui, repo: GittyRepo, **__: object) -> None:
+def _export_hook(ui: hgui.ui, repo: GittyRepo, **__: object) -> None:
+    """Exports to Git and sets up for serving."""
     never_export = ui.configbool(b'git-serve', b'never-export')
     if never_export:
         return
     always_export = ui.configbool(b'git-serve', b'always-export', False)
     if always_export or os.path.isdir(repo.githandler.gitdir):
-        export_repo(repo)
+        _export_repo(repo)
 
 
-def export_repo(repo: GittyRepo) -> None:
-    clean_all_refs(repo.githandler.git.refs)
+def _export_repo(repo: GittyRepo) -> None:
+    """Do the actual exporting."""
+    _clean_all_refs(repo.githandler.git.refs)
     repo.githandler.export_commits()
-    set_head(repo)
-
-
-def is_gitty(repo: hgrepo.IRepo) -> t.TypeGuard[GittyRepo]:
-    return hasattr(repo, 'githandler')
+    _set_head(repo)
 
 
 # Interfacing with Mercurial
 
-__version__ = '0.1.1'
+__version__ = '0.1.2'
 
 cmdtable: dict[bytes, object] = {}
 
@@ -215,19 +199,19 @@
 
 @command(b'git-serve-export')
 def git_serve_export(_: hgui.ui, repo: hgrepo.IRepo, **__: object) -> None:
-    if not is_gitty(repo):
+    if not _is_gitty(repo):
         raise hgerr.Abort(b'this extension depends on the `hggit` extension')
-    export_repo(repo)
+    _export_repo(repo)
 
 
 def uisetup(_: hgui.ui) -> None:
     extensions.wrapfunction(
-        wireprotoserver, 'handlewsgirequest', handle_git_protocol
+        wireprotoserver, 'handlewsgirequest', _handle_git_protocol
     )
 
 
 def reposetup(ui: hgui.ui, _: hgrepo.IRepo) -> None:
-    ui.setconfig(b'hooks', b'txnclose.__gitserve_internal__', export_hook)
+    ui.setconfig(b'hooks', b'txnclose.__gitserve_internal__', _export_hook)
 
 
 __all__ = ('__version__', 'cmdtable', 'command', 'uisetup', 'reposetup')