Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions pre_commit/commands/hook_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@
Z40 = '0' * 40


def _is_zero_oid(oid: str) -> bool:
# git's pre-push protocol uses an all-zero object id to indicate
# branch creation (old) or deletion (new). the length depends on the
# repository's object format: 40 for sha1 (default) and 64 for sha256
# (`git init --object-format=sha256`).
return len(oid) in (40, 64) and set(oid) == {'0'}


def _run_legacy(
hook_type: str,
hook_dir: str | None,
Expand Down Expand Up @@ -128,9 +136,9 @@ def _pre_push_ns(
for line in stdin.decode().splitlines():
parts = line.rsplit(maxsplit=3)
local_branch, local_sha, remote_branch, remote_sha = parts
if local_sha == Z40:
if _is_zero_oid(local_sha):
continue
elif remote_sha != Z40 and _rev_exists(remote_sha):
elif not _is_zero_oid(remote_sha) and _rev_exists(remote_sha):
return _ns(
'pre-push', color,
from_ref=remote_sha, to_ref=local_sha,
Expand Down
81 changes: 81 additions & 0 deletions tests/commands/hook_impl_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pre_commit import git
from pre_commit.commands import hook_impl
from pre_commit.envcontext import envcontext
from pre_commit.util import CalledProcessError
from pre_commit.util import cmd_output
from pre_commit.util import make_executable
from testing.fixtures import git_dir
Expand Down Expand Up @@ -347,6 +348,86 @@ def test_run_ns_pre_push_deleting_branch(push_example):
assert ns is None


def test_is_zero_oid_sha1():
assert hook_impl._is_zero_oid('0' * 40) is True


def test_is_zero_oid_sha256():
assert hook_impl._is_zero_oid('0' * 64) is True


@pytest.mark.parametrize(
'oid',
(
'',
'0' * 39,
'0' * 41,
'0' * 63,
'0' * 65,
'0' * 80,
'1' * 40,
'1' * 64,
'0' * 39 + '1',
'a' * 40,
'deadbeef' * 5,
'deadbeef' * 8,
),
)
def test_is_zero_oid_rejects_non_zero(oid):
assert hook_impl._is_zero_oid(oid) is False


@pytest.fixture
def sha256_push_example(tempdir_factory):
src = tempdir_factory.get()
try:
cmd_output('git', 'init', '--object-format=sha256', '--quiet', src)
except CalledProcessError:
pytest.skip('git does not support --object-format=sha256')
git_commit(cwd=src)
src_head = git.head_rev(src)

clone = tempdir_factory.get()
cmd_output('git', 'clone', src, clone)
git_commit(cwd=clone)
clone_head = git.head_rev(clone)
return (src, src_head, clone, clone_head)


def test_run_ns_pre_push_sha256_new_branch(sha256_push_example):
# regression test for #3664: the hardcoded 40-char Z40 sentinel did not
# match the 64-char zero oid emitted by git on sha256 repos, causing
# branch creation to fall through to a `git diff <real>..<64-zeros>`
# which produced `fatal: Invalid revision range`.
src, src_head, clone, clone_head = sha256_push_example
zero64 = '0' * 64
assert len(src_head) == 64

with cwd(clone):
args = ('origin', src)
stdin = f'HEAD {clone_head} refs/heads/b {zero64}\n'.encode()
ns = hook_impl._run_ns('pre-push', False, args, stdin)

assert ns is not None
assert ns.from_ref == src_head
assert ns.to_ref == clone_head


def test_run_ns_pre_push_sha256_deleting_branch(sha256_push_example):
# regression test for #3664: `git push origin --delete <branch>` on a
# sha256 repo emits a 64-char zero oid for local_sha, which previously
# did not match Z40 and caused a CalledProcessError exit.
src, src_head, clone, _ = sha256_push_example
zero64 = '0' * 64

with cwd(clone):
args = ('origin', src)
stdin = f'(delete) {zero64} refs/heads/b {src_head}'.encode()
ns = hook_impl._run_ns('pre-push', False, args, stdin)

assert ns is None


def test_hook_impl_main_noop_pre_push(cap_out, store, push_example):
src, src_head, clone, _ = push_example

Expand Down
Loading