Repository navigation
Remote.fetch(kill_after_timeout=...) does not stop git when it writes nothing more to stderr #2276
Description
Question
The documentation for Remote.fetch describes kill_after_timeout as "a timeout in seconds for the git command, after which the process should be killed". When git writes nothing more to stderr after the deadline, which is what happens when a remote stalls, the call never returns and the git fetch process keeps running. Is this a known limitation, or a bug?
Context
We use kill_after_timeout to put an upper bound on how long a fetch can run.
Reproduction
import socket
import tempfile
import threading
import time
from pathlib import Path
from git import Repo
from git.exc import GitCommandError
# A remote that accepts the connection and never answers.
server = socket.socket()
server.bind(("127.0.0.1", 0))
server.listen()
port = server.getsockname()[1]
held = []
threading.Thread(target=lambda: [held.append(server.accept()[0]) for _ in iter(int, 1)], daemon=True).start()
repo = Repo.init(Path(tempfile.mkdtemp()))
repo.create_remote("origin", f"git://127.0.0.1:{port}/stalled.git")
started = time.monotonic()
try:
repo.remotes.origin.fetch(kill_after_timeout=3)
print("returned")
except GitCommandError as exc:
print(f"GitCommandError after {time.monotonic() - started:.1f}s: {exc}")Expected: a GitCommandError after about 3 seconds.
Actual: the call never returns, and git fetch -v -- origin keeps running until it is killed by hand.
Reproduced with:
- GitPython 3.2.0, git 2.47.3, Python 3.13, Linux (the
python:3.13Docker image) - GitPython 3.1.61 and 3.2.0, git 2.52.0, Python 3.14.7, macOS
What I think happens
Remote.fetch does not pass kill_after_timeout to the git command. It passes it to _get_fetch_info_from_stderr, and from there to handle_process_output. When the stderr pump thread is still alive after the timeout, handle_process_output calls AutoInterrupt._terminate(). That method closes stdin, stdout and stderr before it calls proc.terminate().
The pump thread is blocked in read() on that same stderr stream, so it holds the buffered reader's lock, and stderr.close() waits for that lock indefinitely. The process is never signalled. A native stack sample of the stuck process (on macOS) shows exactly that:
- main thread:
_io_TextIOWrapper_close→_io__Buffered_close→_enter_buffered_busy→PyThread_acquire_lock_timed - pump thread:
_textiowrapper_readline→_io__Buffered_read1→read
So the timeout takes effect only if git writes to stderr after the deadline, which releases the pump thread's read.
The command-level timeout works for the same remote: repo.git.fetch("origin", kill_after_timeout=3) goes through Git.execute, whose watchdog sends SIGKILL. It raised GitCommandError with status -9 after 3.0 seconds, on both platforms.
A possible fix
Signal the process before closing its streams, either by reordering _terminate() or by killing the process in handle_process_output before calling _terminate(). Once git exits, the write end of the pipe closes, read() returns EOF, and the close no longer blocks.
One caveat, which I have not tested: for https:// and ssh remotes, git fetch starts a helper (git-remote-https or ssh) that inherits its stderr. If only git fetch is signalled, a helper stuck on the network may keep the pipe open, and the read may still block. A complete fix might need to signal the children too, as the Git.execute watchdog does, or the whole process group.
Two smaller points, in case they are useful:
- The loop joins each pump thread one after another with the full timeout, so the worst-case wait is the timeout multiplied by the number of pumped streams.
_terminate()sendsSIGTERM, while theGit.executewatchdog sendsSIGKILL.
Remote.pull and Remote.push appear to use the same path, but I have only tested fetch.
Activity
fix: stop stalled remote commands when their timeout expires (#2276)
fix: stop stalled remote commands when their timeout expires (#2276)
fix: stop stalled remote commands when their timeout expires (#2276)
fix: stop stalled remote commands when their timeout expires (#2276)
fix: stop stalled remote commands when their timeout expires (#2276)
fix: stop stalled remote commands when their timeout expires (#2276)
fix: stop stalled remote commands when their timeout expires (#2276)
fix: stop stalled remote commands when their timeout expires (#2276)
fix: stop stalled remote commands when their timeout expires (#2276)
ogenstad commentedon Oct 8, 2026
@Byron, thank you for the quick fix!