From 2ddebfdc9385d84ff5e545b4ac1da862b1feb28b Mon Sep 17 00:00:00 2001
From: Daniel Baumann <daniel@debian.org>
Date: Thu, 20 Mar 2025 08:17:13 +0100
Subject: [PATCH] Adding upstream version 4.2.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
---
 .github/workflows/languages.yaml |  2 +-
 .github/workflows/main.yml       |  4 +-
 .pre-commit-config.yaml          |  6 +--
 CHANGELOG.md                     | 13 +++++++
 pre_commit/git.py                |  2 +-
 pre_commit/languages/python.py   | 34 +++++++++++-----
 setup.cfg                        |  2 +-
 tests/git_test.py                |  9 +++++
 tests/languages/python_test.py   | 67 ++++++++++++++++++++++++++++++++
 9 files changed, 121 insertions(+), 18 deletions(-)

diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml
index 61293a0..fccf298 100644
--- a/.github/workflows/languages.yaml
+++ b/.github/workflows/languages.yaml
@@ -36,7 +36,7 @@ jobs:
       matrix:
         include: ${{ fromJSON(needs.vars.outputs.languages) }}
     steps:
-    - uses: asottile/workflows/.github/actions/fast-checkout@v1.4.0
+    - uses: asottile/workflows/.github/actions/fast-checkout@v1.8.1
     - uses: actions/setup-python@v4
       with:
         python-version: 3.9
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 2355b66..7fda646 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -12,12 +12,12 @@ concurrency:
 
 jobs:
   main-windows:
-    uses: asottile/workflows/.github/workflows/tox.yml@v1.6.0
+    uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1
     with:
       env: '["py39"]'
       os: windows-latest
   main-linux:
-    uses: asottile/workflows/.github/workflows/tox.yml@v1.6.0
+    uses: asottile/workflows/.github/workflows/tox.yml@v1.8.1
     with:
       env: '["py39", "py310", "py311", "py312"]'
       os: ubuntu-latest
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4a23da2..b216fbd 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -29,15 +29,15 @@ repos:
     -   id: pyupgrade
         args: [--py39-plus]
 -   repo: https://github.com/hhatto/autopep8
-    rev: v2.3.1
+    rev: v2.3.2
     hooks:
     -   id: autopep8
 -   repo: https://github.com/PyCQA/flake8
-    rev: 7.1.1
+    rev: 7.1.2
     hooks:
     -   id: flake8
 -   repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.14.1
+    rev: v1.15.0
     hooks:
     -   id: mypy
         additional_dependencies: [types-pyyaml]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 408afe6..b63f443 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,16 @@
+4.2.0 - 2025-03-18
+==================
+
+### Features
+- For `language: python` first attempt a versioned python executable for
+  the default language version before consulting a potentially unversioned
+  `sys.executable`.
+    - #3430 PR by @asottile.
+
+### Fixes
+- Handle error during conflict detection when a file is named "HEAD"
+    - #3425 PR by @tusharsadhwani.
+
 4.1.0 - 2025-01-20
 ==================
 
diff --git a/pre_commit/git.py b/pre_commit/git.py
index 19aac38..2f424f8 100644
--- a/pre_commit/git.py
+++ b/pre_commit/git.py
@@ -126,7 +126,7 @@ def get_conflicted_files() -> set[str]:
     merge_diff_filenames = zsplit(
         cmd_output(
             'git', 'diff', '--name-only', '--no-ext-diff', '-z',
-            '-m', tree_hash, 'HEAD', 'MERGE_HEAD',
+            '-m', tree_hash, 'HEAD', 'MERGE_HEAD', '--',
         )[1],
     )
     return set(merge_conflict_filenames) | set(merge_diff_filenames)
diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py
index 0c4bb62..88ececc 100644
--- a/pre_commit/languages/python.py
+++ b/pre_commit/languages/python.py
@@ -75,6 +75,13 @@ def _find_by_py_launcher(
     return None
 
 
+def _impl_exe_name() -> str:
+    if sys.implementation.name == 'cpython':  # pragma: cpython cover
+        return 'python'
+    else:  # pragma: cpython no cover
+        return sys.implementation.name  # pypy mostly
+
+
 def _find_by_sys_executable() -> str | None:
     def _norm(path: str) -> str | None:
         _, exe = os.path.split(path.lower())
@@ -100,18 +107,25 @@ def _find_by_sys_executable() -> str | None:
 
 @functools.lru_cache(maxsize=1)
 def get_default_version() -> str:  # pragma: no cover (platform dependent)
-    # First attempt from `sys.executable` (or the realpath)
-    exe = _find_by_sys_executable()
-    if exe:
-        return exe
+    v_major = f'{sys.version_info[0]}'
+    v_minor = f'{sys.version_info[0]}.{sys.version_info[1]}'
 
-    # Next try the `pythonX.X` executable
-    exe = f'python{sys.version_info[0]}.{sys.version_info[1]}'
-    if find_executable(exe):
-        return exe
+    # attempt the likely implementation exe
+    for potential in (v_minor, v_major):
+        exe = f'{_impl_exe_name()}{potential}'
+        if find_executable(exe):
+            return exe
 
-    if _find_by_py_launcher(exe):
-        return exe
+    # next try `sys.executable` (or the realpath)
+    maybe_exe = _find_by_sys_executable()
+    if maybe_exe:
+        return maybe_exe
+
+    # maybe on windows we can find it via py launcher?
+    if sys.platform == 'win32':  # pragma: win32 cover
+        exe = f'python{v_minor}'
+        if _find_by_py_launcher(exe):
+            return exe
 
     # We tried!
     return C.DEFAULT
diff --git a/setup.cfg b/setup.cfg
index 60d9764..af34452 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = pre_commit
-version = 4.1.0
+version = 4.2.0
 description = A framework for managing and maintaining multi-language pre-commit hooks.
 long_description = file: README.md
 long_description_content_type = text/markdown
diff --git a/tests/git_test.py b/tests/git_test.py
index 93f5a1c..02b6ce3 100644
--- a/tests/git_test.py
+++ b/tests/git_test.py
@@ -141,6 +141,15 @@ def test_get_conflicted_files_unstaged_files(in_merge_conflict):
     assert ret == {'conflict_file'}
 
 
+def test_get_conflicted_files_with_file_named_head(in_merge_conflict):
+    resolve_conflict()
+    open('HEAD', 'w').close()
+    cmd_output('git', 'add', 'HEAD')
+
+    ret = set(git.get_conflicted_files())
+    assert ret == {'conflict_file', 'HEAD'}
+
+
 MERGE_MSG = b"Merge branch 'foo' into bar\n\nConflicts:\n\tconflict_file\n"
 OTHER_MERGE_MSG = MERGE_MSG + b'\tother_conflict_file\n'
 
diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py
index ab26e14..565525a 100644
--- a/tests/languages/python_test.py
+++ b/tests/languages/python_test.py
@@ -12,6 +12,7 @@ from pre_commit.languages import python
 from pre_commit.prefix import Prefix
 from pre_commit.util import make_executable
 from pre_commit.util import win_exe
+from testing.auto_namedtuple import auto_namedtuple
 from testing.language_helpers import run_language
 
 
@@ -34,6 +35,72 @@ def test_read_pyvenv_cfg_non_utf8(tmpdir):
     assert python._read_pyvenv_cfg(pyvenv_cfg) == expected
 
 
+def _get_default_version(
+        *,
+        impl: str,
+        exe: str,
+        found: set[str],
+        version: tuple[int, int],
+) -> str:
+    sys_exe = f'/fake/path/{exe}'
+    sys_impl = auto_namedtuple(name=impl)
+    sys_ver = auto_namedtuple(major=version[0], minor=version[1])
+
+    def find_exe(s):
+        if s in found:
+            return f'/fake/path/found/{exe}'
+        else:
+            return None
+
+    with (
+            mock.patch.object(sys, 'implementation', sys_impl),
+            mock.patch.object(sys, 'executable', sys_exe),
+            mock.patch.object(sys, 'version_info', sys_ver),
+            mock.patch.object(python, 'find_executable', find_exe),
+    ):
+        return python.get_default_version.__wrapped__()
+
+
+def test_default_version_sys_executable_found():
+    ret = _get_default_version(
+        impl='cpython',
+        exe='python3.12',
+        found={'python3.12'},
+        version=(3, 12),
+    )
+    assert ret == 'python3.12'
+
+
+def test_default_version_picks_specific_when_found():
+    ret = _get_default_version(
+        impl='cpython',
+        exe='python3',
+        found={'python3', 'python3.12'},
+        version=(3, 12),
+    )
+    assert ret == 'python3.12'
+
+
+def test_default_version_picks_pypy_versioned_exe():
+    ret = _get_default_version(
+        impl='pypy',
+        exe='python',
+        found={'pypy3.12', 'python3'},
+        version=(3, 12),
+    )
+    assert ret == 'pypy3.12'
+
+
+def test_default_version_picks_pypy_unversioned_exe():
+    ret = _get_default_version(
+        impl='pypy',
+        exe='python',
+        found={'pypy3', 'python3'},
+        version=(3, 12),
+    )
+    assert ret == 'pypy3'
+
+
 def test_norm_version_expanduser():
     home = os.path.expanduser('~')
     if sys.platform == 'win32':  # pragma: win32 cover