Mercurial > hg-git-serve
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')
