def handle_process_output(
    process: "Git.AutoInterrupt" | Popen,
    stdout_handler: Union[
        None,
        Callable[[AnyStr], None],
        Callable[[List[AnyStr]], None],
        Callable[[bytes, "Repo", "DiffIndex"], None],
    ],
    stderr_handler: Union[None, Callable[[AnyStr], None], Callable[[List[AnyStr]], None]],
    finalizer: Union[None, Callable[[Union[Popen, "Git.AutoInterrupt"]], None]] = None,
    decode_streams: bool = True,
    kill_after_timeout: Union[None, float] = None,
) -> None:
    R"""Register for notifications to learn that process output is ready to read, and
    dispatch lines to the respective line handlers.

    This function returns once the finalizer returns.

    :param process:
        :class:`subprocess.Popen` instance.

    :param stdout_handler:
        f(stdout_line_string), or ``None``.

    :param stderr_handler:
        f(stderr_line_string), or ``None``.

    :param finalizer:
        f(proc) - wait for proc to finish.

    :param decode_streams:
        Assume stdout/stderr streams are binary and decode them before pushing their
        contents to handlers.

        This defaults to ``True``. Set it to ``False`` if:

        - ``universal_newlines == True``, as then streams are in text mode, or
        - decoding must happen later, such as for :class:`~git.diff.Diff`\s.

    :param kill_after_timeout:
        :class:`float` or ``None``, Default = ``None``

        To specify a timeout in seconds for the git command, after which the process
        should be killed.
    """

    # Use 2 "pump" threads and wait for both to finish.
    def pump_stream(
        cmdline: List[str],
        name: str,
        stream: Union[BinaryIO, TextIO],
        is_decode: bool,
        handler: Union[None, Callable[[Union[bytes, str]], None]],
    ) -> None:
        try:
            for line in stream:
                if handler:
                    if is_decode:
                        assert isinstance(line, bytes)
                        line_str = line.decode(defenc)
                        handler(line_str)
                    else:
                        handler(line)

        except Exception as ex:
            _logger.error(f"Pumping {name!r} of cmd({remove_password_if_present(cmdline)}) failed due to: {ex!r}")
            if "I/O operation on closed file" not in str(ex):
                # Only reraise if the error was not due to the stream closing.
                raise CommandError([f"<{name}-pump>"] + remove_password_if_present(cmdline), ex) from ex
        finally:
            stream.close()

    if hasattr(process, "proc"):
        process = cast("Git.AutoInterrupt", process)
        cmdline: str | Tuple[str, ...] | List[str] = getattr(process.proc, "args", "")
        p_stdout = process.proc.stdout if process.proc else None
        p_stderr = process.proc.stderr if process.proc else None
    else:
        process = cast(Popen, process)  # type: ignore[redundant-cast]
        cmdline = getattr(process, "args", "")
        p_stdout = process.stdout
        p_stderr = process.stderr

    if not isinstance(cmdline, (tuple, list)):
        cmdline = cmdline.split()

    pumps: List[Tuple[str, IO, Callable[..., None] | None]] = []
    if p_stdout:
        pumps.append(("stdout", p_stdout, stdout_handler))
    if p_stderr:
        pumps.append(("stderr", p_stderr, stderr_handler))

    threads: List[threading.Thread] = []

    for name, stream, handler in pumps:
        t = threading.Thread(target=pump_stream, args=(cmdline, name, stream, decode_streams, handler))
        t.daemon = True
        t.start()
        threads.append(t)

    # FIXME: Why join? Will block if stdin needs feeding...
    for t in threads:
        t.join(timeout=kill_after_timeout)
        if t.is_alive():
            if isinstance(process, Git.AutoInterrupt):
                process._terminate()
            else:  # Don't want to deal with the other case.
                raise RuntimeError(
                    "Thread join() timed out in cmd.handle_process_output()."
                    f" kill_after_timeout={kill_after_timeout} seconds"
                )
            if stderr_handler:
                error_str: Union[str, bytes] = (
                    f"error: process killed because it timed out. kill_after_timeout={kill_after_timeout} seconds"
                )
                if not decode_streams and isinstance(p_stderr, BinaryIO):
                    # Assume stderr_handler needs binary input.
                    error_str = cast(str, error_str)
                    error_str = error_str.encode()
                # We ignore typing on the next line because mypy does not like the way
                # we inferred that stderr takes str or bytes.
                stderr_handler(error_str)  # type: ignore[arg-type]

    if finalizer:
        finalizer(process)
Register for notifications to learn that process output is ready to read, and
    dispatch lines to the respective line handlers.

    This function returns once the finalizer returns.

    :param process:
        :class:`subprocess.Popen` instance.

    :param stdout_handler:
        f(stdout_line_string), or ``None``.

    :param stderr_handler:
        f(stderr_line_string), or ``None``.

    :param finalizer:
        f(proc) - wait for proc to finish.

    :param decode_streams:
        Assume stdout/stderr streams are binary and decode them before pushing their
        contents to handlers.

        This defaults to ``True``. Set it to ``False`` if:

        - ``universal_newlines == True``, as then streams are in text mode, or
        - decoding must happen later, such as for :class:`~git.diff.Diff`\s.

    :param kill_after_timeout:
        :class:`float` or ``None``, Default = ``None``

        To specify a timeout in seconds for the git command, after which the process
        should be killed.
    
git/cmd.py