Repository: seeraven/gitcache

Issue:
Ctrl-C the `less` process in `git diff` etc. leads to terminal being left in raw mode
As in the title.

1. Do a `git diff` or `git show @` when pager is less, and make sure it's long enough for less to be invoked
2. ctrl-C
3. see terminal is left in raw mode, typing anything does not show up anything in the terminal

Discussion:
@seeraven: Good catch. `git` itself simply ignores `SIGINT` resp. CTRL-C when using `less`, but python handles it. But since it was not catched within the wrapper, the process running git is killed which then leaves the terminal in the raw mode. I will release a fix soon.@user202729: I was thinking about using `os.exec*` instead, but I suppose this works too. (as long as there isn't anything Python need to do after the subprocess exits.)@seeraven: Yes exactly, there are multiple calls where the same function is used to get the return code and continue with the processing, so `os.exec*` wasn't an option. Besides, I knew the `subprocess` stuff works also on Windows (which is always somewhat picky) and I didn't want to do a full manual testing on Windows with a new function. ;-)

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e456a7f..a44e66f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## Upcoming Version
 
+## v1.0.28
+
+- Bugfix: Capture CTRL-C and forward it to the `git` subprocess.
+
 ## v1.0.27
 
 - Bugfix: Fix `git remote add origin` command when not using the `-C` option.
diff --git a/README.md b/README.md
index cc1d7bd..c8d510e 100644
--- a/README.md
+++ b/README.md
@@ -77,8 +77,8 @@ gitcache is distributed as a single executable packaged using [pyInstaller].
 So all you have to do is to download the latest executable and copy it to a
 location of your choice, for example `~/bin`:
 
-    wget https://github.com/seeraven/gitcache/releases/download/v1.0.27/gitcache_v1.0.27_Ubuntu22.04_x86_64
-    mv gitcache_v1.0.27_Ubuntu22.04_x86_64 ~/bin/gitcache
+    wget https://github.com/seeraven/gitcache/releases/download/v1.0.28/gitcache_v1.0.28_Ubuntu22.04_x86_64
+    mv gitcache_v1.0.28_Ubuntu22.04_x86_64 ~/bin/gitcache
     chmod +x ~/bin/gitcache
 
 gitcache can be used as a stand-alone command, but it is much easier to use
@@ -115,9 +115,9 @@ gitcache is distributed as a tar-ball (`*.tgz` file). Download the archive and
 extract it at your desired target location (the archive contains a subfolder):
 
     cd /my/target/destination
-    tar xfz gitcache_v1.0.27_Darwin_arm64.tgz
-    xattr -cr gitcache_v1.0.27_Darwin_arm64
-    ls gitcache_v1.0.27_Darwin_arm64
+    tar xfz gitcache_v1.0.28_Darwin_arm64.tgz
+    xattr -cr gitcache_v1.0.28_Darwin_arm64
+    ls gitcache_v1.0.28_Darwin_arm64
 
 To use the `gitcache` command, the final installation directory should be put
 into your `PATH` variable. To use it as a wrapper to the `git` command, you
@@ -136,7 +136,7 @@ before opening a bug ticket.
 The latest wheel is found on the release page
 https://github.com/seeraven/gitcache/releases and is installed using pip:
 
-    pip install gitcache-1.0.27-py3-none-any.whl
+    pip install gitcache-1.0.28-py3-none-any.whl
 
 As with the other installation methods, you have to make sure to create a
 symlink named `git` to the location of the `gitcache` script and ensure it
@@ -359,7 +359,7 @@ is pushed to the repository. This changes the release process a little bit:
     `src/git_cache/git_cache_command.py` and `doc/source/installation.rst` and
     replace the version number:
 
-        sed -i 's/1.0.27/1.0.28/g' Makefile pyproject.toml src/git_cache/git_cache_command.py doc/source/installation.rst
+        sed -i 's/1.0.28/1.0.29/g' Makefile pyproject.toml src/git_cache/git_cache_command.py doc/source/installation.rst
 
 
 ## Notes on Building a Pip-Package
diff --git a/doc/source/installation.rst b/doc/source/installation.rst
index da48106..092eebe 100644
--- a/doc/source/installation.rst
+++ b/doc/source/installation.rst
@@ -14,8 +14,8 @@ So all you have to do is to download the latest executable from the release page
 |release_url| and copy it to a location of your choice, for example
 :code:`/usr/local/bin`.
 
-    $ wget https://github.com/seeraven/gitcache/releases/download/v1.0.28/gitcache_v1.0.28_Ubuntu22.04_amd64
-    $ mv gitcache_v1.0.28_Ubuntu22.04_amd64 /usr/local/bin/gitcache
+    $ wget https://github.com/seeraven/gitcache/releases/download/v1.0.29/gitcache_v1.0.29_Ubuntu22.04_amd64
+    $ mv gitcache_v1.0.29_Ubuntu22.04_amd64 /usr/local/bin/gitcache
     $ chmod +x /usr/local/bin/gitcache
     $ ln -s /usr/local/bin/gitcache /usr/local/bin/git
 
diff --git a/pip_deps/dev_requirements-linux-py3.10.12.txt b/pip_deps/dev_requirements-linux-py3.10.12.txt
index 587d6f3..47c5203 100644
--- a/pip_deps/dev_requirements-linux-py3.10.12.txt
+++ b/pip_deps/dev_requirements-linux-py3.10.12.txt
@@ -14,7 +14,7 @@ black==25.1.0
     # via
     #   flake8-black
     #   gitcache (pyproject.toml)
-certifi==2025.1.31
+certifi==2025.4.26
     # via requests
 charset-normalizer==3.4.1
     # via requests
@@ -22,13 +22,13 @@ click==8.1.8
     # via black
 coloredlogs==15.0.1
     # via gitcache (pyproject.toml)
-coverage[toml]==7.7.0
+coverage[toml]==7.8.0
     # via
     #   gitcache (pyproject.toml)
     #   pytest-cov
 decorator==5.2.1
     # via ipython
-dill==0.3.9
+dill==0.4.0
     # via pylint
 docutils==0.21.2
     # via
@@ -46,7 +46,7 @@ exceptiongroup==1.2.2
     #   pytest
 executing==2.2.0
     # via stack-data
-flake8==7.1.2
+flake8==7.2.0
     # via
     #   flake8-black
     #   flake8-bugbear
@@ -73,7 +73,7 @@ imagesize==1.4.1
     # via sphinx
 iniconfig==2.1.0
     # via pytest
-ipython==8.34.0
+ipython==8.36.0
     # via gitcache (pyproject.toml)
 isort==6.0.1
     # via
@@ -96,13 +96,13 @@ mock==5.2.0
     # via gitcache (pyproject.toml)
 mypy==1.15.0
     # via gitcache (pyproject.toml)
-mypy-extensions==1.0.0
+mypy-extensions==1.1.0
     # via
     #   black
     #   mypy
 natsort==8.4.0
     # via domdf-python-tools
-packaging==24.2
+packaging==25.0
     # via
     #   black
     #   pyinstaller
@@ -125,25 +125,25 @@ pluggy==1.5.0
     # via pytest
 portalocker==3.1.1
     # via gitcache (pyproject.toml)
-prompt-toolkit==3.0.50
+prompt-toolkit==3.0.51
     # via ipython
 ptyprocess==0.7.0
     # via pexpect
 pure-eval==0.2.3
     # via stack-data
-pycodestyle==2.12.1
+pycodestyle==2.13.0
     # via
     #   flake8
     #   gitcache (pyproject.toml)
-pyflakes==3.2.0
+pyflakes==3.3.2
     # via flake8
 pygments==2.19.1
     # via
     #   ipython
     #   sphinx
-pyinstaller==6.12.0
+pyinstaller==6.13.0
     # via gitcache (pyproject.toml)
-pyinstaller-hooks-contrib==2025.1
+pyinstaller-hooks-contrib==2025.3
     # via pyinstaller
 pylint==3.3.6
     # via gitcache (pyproject.toml)
@@ -152,7 +152,7 @@ pytest==8.3.5
     #   gitcache (pyproject.toml)
     #   pytest-cov
     #   pytest-timeout
-pytest-cov==6.0.0
+pytest-cov==6.1.1
     # via gitcache (pyproject.toml)
 pytest-timeout==2.3.1
     # via gitcache (pyproject.toml)
@@ -206,14 +206,14 @@ traitlets==5.14.3
     #   matplotlib-inline
 types-mock==5.2.0.20250306
     # via gitcache (pyproject.toml)
-typing-extensions==4.12.2
+typing-extensions==4.13.2
     # via
     #   astroid
     #   black
     #   domdf-python-tools
     #   ipython
     #   mypy
-urllib3==2.3.0
+urllib3==2.4.0
     # via requests
 wcwidth==0.2.13
     # via prompt-toolkit
diff --git a/pip_deps/dev_requirements-linux-py3.12.3.txt b/pip_deps/dev_requirements-linux-py3.12.3.txt
index 570c409..d4429c6 100644
--- a/pip_deps/dev_requirements-linux-py3.12.3.txt
+++ b/pip_deps/dev_requirements-linux-py3.12.3.txt
@@ -14,7 +14,7 @@ black==25.1.0
     # via
     #   flake8-black
     #   gitcache (pyproject.toml)
-certifi==2025.1.31
+certifi==2025.4.26
     # via requests
 charset-normalizer==3.4.1
     # via requests
@@ -22,13 +22,13 @@ click==8.1.8
     # via black
 coloredlogs==15.0.1
     # via gitcache (pyproject.toml)
-coverage[toml]==7.7.0
+coverage[toml]==7.8.0
     # via
     #   gitcache (pyproject.toml)
     #   pytest-cov
 decorator==5.2.1
     # via ipython
-dill==0.3.9
+dill==0.4.0
     # via pylint
 docutils==0.21.2
     # via
@@ -42,7 +42,7 @@ domdf-python-tools==3.10.0
     #   sphinx-pyproject
 executing==2.2.0
     # via stack-data
-flake8==7.1.2
+flake8==7.2.0
     # via
     #   flake8-black
     #   flake8-bugbear
@@ -69,7 +69,7 @@ imagesize==1.4.1
     # via sphinx
 iniconfig==2.1.0
     # via pytest
-ipython==9.0.2
+ipython==9.2.0
     # via gitcache (pyproject.toml)
 ipython-pygments-lexers==1.1.1
     # via ipython
@@ -94,13 +94,13 @@ mock==5.2.0
     # via gitcache (pyproject.toml)
 mypy==1.15.0
     # via gitcache (pyproject.toml)
-mypy-extensions==1.0.0
+mypy-extensions==1.1.0
     # via
     #   black
     #   mypy
 natsort==8.4.0
     # via domdf-python-tools
-packaging==24.2
+packaging==25.0
     # via
     #   black
     #   pyinstaller
@@ -123,26 +123,26 @@ pluggy==1.5.0
     # via pytest
 portalocker==3.1.1
     # via gitcache (pyproject.toml)
-prompt-toolkit==3.0.50
+prompt-toolkit==3.0.51
     # via ipython
 ptyprocess==0.7.0
     # via pexpect
 pure-eval==0.2.3
     # via stack-data
-pycodestyle==2.12.1
+pycodestyle==2.13.0
     # via
     #   flake8
     #   gitcache (pyproject.toml)
-pyflakes==3.2.0
+pyflakes==3.3.2
     # via flake8
 pygments==2.19.1
     # via
     #   ipython
     #   ipython-pygments-lexers
     #   sphinx
-pyinstaller==6.12.0
+pyinstaller==6.13.0
     # via gitcache (pyproject.toml)
-pyinstaller-hooks-contrib==2025.1
+pyinstaller-hooks-contrib==2025.3
     # via pyinstaller
 pylint==3.3.6
     # via gitcache (pyproject.toml)
@@ -151,7 +151,7 @@ pytest==8.3.5
     #   gitcache (pyproject.toml)
     #   pytest-cov
     #   pytest-timeout
-pytest-cov==6.0.0
+pytest-cov==6.1.1
     # via gitcache (pyproject.toml)
 pytest-timeout==2.3.1
     # via gitcache (pyproject.toml)
@@ -198,11 +198,11 @@ traitlets==5.14.3
     #   matplotlib-inline
 types-mock==5.2.0.20250306
     # via gitcache (pyproject.toml)
-typing-extensions==4.12.2
+typing-extensions==4.13.2
     # via
     #   domdf-python-tools
     #   mypy
-urllib3==2.3.0
+urllib3==2.4.0
     # via requests
 wcwidth==0.2.13
     # via prompt-toolkit
diff --git a/pip_deps/dev_requirements-linux-py3.8.10.txt b/pip_deps/dev_requirements-linux-py3.8.10.txt
index 36218ca..c86e710 100644
--- a/pip_deps/dev_requirements-linux-py3.8.10.txt
+++ b/pip_deps/dev_requirements-linux-py3.8.10.txt
@@ -16,7 +16,7 @@ black==24.8.0
     # via
     #   flake8-black
     #   gitcache (pyproject.toml)
-certifi==2025.1.31
+certifi==2025.4.26
     # via requests
 charset-normalizer==3.4.1
     # via requests
@@ -30,7 +30,7 @@ coverage[toml]==7.6.1
     #   pytest-cov
 decorator==5.2.1
     # via ipython
-dill==0.3.9
+dill==0.4.0
     # via pylint
 docutils==0.20.1
     # via
@@ -104,13 +104,13 @@ mock==5.2.0
     # via gitcache (pyproject.toml)
 mypy==1.14.1
     # via gitcache (pyproject.toml)
-mypy-extensions==1.0.0
+mypy-extensions==1.1.0
     # via
     #   black
     #   mypy
 natsort==8.4.0
     # via domdf-python-tools
-packaging==24.2
+packaging==25.0
     # via
     #   black
     #   pyinstaller
@@ -135,7 +135,7 @@ pluggy==1.5.0
     # via pytest
 portalocker==3.0.0
     # via gitcache (pyproject.toml)
-prompt-toolkit==3.0.50
+prompt-toolkit==3.0.51
     # via ipython
 ptyprocess==0.7.0
     # via pexpect
@@ -151,9 +151,9 @@ pygments==2.19.1
     # via
     #   ipython
     #   sphinx
-pyinstaller==6.12.0
+pyinstaller==6.13.0
     # via gitcache (pyproject.toml)
-pyinstaller-hooks-contrib==2025.1
+pyinstaller-hooks-contrib==2025.3
     # via pyinstaller
 pylint==3.2.7
     # via gitcache (pyproject.toml)
@@ -168,7 +168,7 @@ pytest-timeout==2.3.1
     # via gitcache (pyproject.toml)
 pytimeparse==1.1.8
     # via gitcache (pyproject.toml)
-pytz==2025.1
+pytz==2025.2
     # via babel
 requests==2.32.3
     # via sphinx
@@ -217,7 +217,7 @@ traitlets==5.14.3
     #   matplotlib-inline
 types-mock==5.1.0.20240425
     # via gitcache (pyproject.toml)
-typing-extensions==4.12.2
+typing-extensions==4.13.2
     # via
     #   astroid
     #   black
diff --git a/pyproject.toml b/pyproject.toml
index c007f86..0c5a6a1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,7 +8,7 @@ build-backend = 'setuptools.build_meta'
 
 [project]
 name = "gitcache"
-version = "1.0.28"
+version = "1.0.29"
 authors = [{name = "Clemens Rabe", email = "clemens.rabe@clemensrabe.de"}]
 description = "Local cache for git repositories to speed up working with large repositories and multiple clones."
 readme = "README.md"
diff --git a/src/git_cache/command_execution.py b/src/git_cache/command_execution.py
index 59fe9d9..7f92503 100644
--- a/src/git_cache/command_execution.py
+++ b/src/git_cache/command_execution.py
@@ -18,6 +18,7 @@ Copyright:
 # -----------------------------------------------------------------------------
 import logging
 import platform
+import signal
 import subprocess
 import time
 
@@ -191,12 +192,17 @@ def simple_call_command(cmd, shell=False, cwd=None):
         Returns the return code of the command.
     """
     try:
-        # pylint: disable=subprocess-run-check
-        result = subprocess.run(cmd, shell=shell, cwd=cwd)
+        # pylint: disable=consider-using-with
+        process = subprocess.Popen(cmd, shell=shell, cwd=cwd)
     except FileNotFoundError:
         return 127
 
-    return result.returncode
+    while process.poll() is None:
+        try:
+            process.wait()
+        except KeyboardInterrupt:
+            process.send_signal(signal.SIGINT)
+    return process.poll()
 
 
 def getstatusoutput(cmd, shell=False, cwd=None):
diff --git a/src/git_cache/git_cache_command.py b/src/git_cache/git_cache_command.py
index 7fedbc2..f3e3ac6 100644
--- a/src/git_cache/git_cache_command.py
+++ b/src/git_cache/git_cache_command.py
@@ -53,7 +53,7 @@ If called with the first argument 'git' or when called as 'git' using a symlink,
 it acts as a wrapper for the git command and intercepts the commands required
 for the mirror handling.
 """
-GITCACHE_VERSION = "1.0.28"
+GITCACHE_VERSION = "1.0.29"
 
 
 # -----------------------------------------------------------------------------


Test patch:
diff --git a/test/unittests/test_git_cache_command_execution_call_command_retry.py b/test/unittests/test_git_cache_command_execution_call_command_retry.py
index e78359f..2084cac 100644
--- a/test/unittests/test_git_cache_command_execution_call_command_retry.py
+++ b/test/unittests/test_git_cache_command_execution_call_command_retry.py
@@ -88,9 +88,9 @@ class GitCacheCallCommandRetryTest(TestCase):
         else:
             cmd = ["pwd"]
             cwd = "/var/log"
-        return_code, stdout_buffer, _ = call_command_retry(cmd, 3, cwd=cwd)
+        return_code, stdout_buffer, stderr_buffer = call_command_retry(cmd, 3, cwd=cwd)
         self.assertEqual(0, return_code)
-        self.assertIn(cwd, stdout_buffer.decode().strip())
+        self.assertTrue(cwd in stdout_buffer.decode().strip() or cwd in stderr_buffer.decode().strip())
 
     def test_shell_cwd(self):
         """git_cache.command_execution.call_command_retry(): Support of cwd using shell."""