Skip to content

Commit cd55773

Browse files
codexByron
authored andcommitted
feat(gix): count reachable commits natively
Add a closable native history helper and use it for `Commit.count`, including skip, limit and first-parent selection. Counting does not depend on the known general walk-order difference. Keep path filters and other revision options on Git; iteration errors preserve the public Git command error type. The staged version passes Ruff and Python syntax checks. Its SHA-1/SHA-256 native smoke and focused pytest selection pass: 5 passed in 1.01s.
1 parent 7a90cc1 commit cd55773

2 files changed

Lines changed: 59 additions & 1 deletion

File tree

‎git/_backend.py‎

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
from collections import Counter
99
from importlib import import_module
1010
import io
11+
from itertools import islice
1112
import logging
1213
import os
1314
import re
1415
from threading import Lock
15-
from typing import Any, Callable, Dict, List, Sequence, Tuple
16+
from typing import Any, Callable, Dict, Iterator, List, Sequence, Tuple
1617

1718
from git.compat import safe_decode
1819
from git.exc import GitCommandError
@@ -261,6 +262,59 @@ def _merge_base(repo: Any, args: List[str], kwargs: Dict[str, Any]) -> bytes:
261262
return b"" if ancestor else b"".join(str(oid).encode("ascii") + b"\n" for oid in ids)
262263

263264

265+
def _walk(repo: Any, rev: str, options: Dict[str, Any]) -> Iterator[str]:
266+
if not hasattr(repo, "rev_walk"):
267+
raise _Unsupported("revision feature disabled")
268+
if options.keys() - {"max_count", "skip", "first_parent"}:
269+
raise _Unsupported("history options or ordering (GIX-8)")
270+
if options.get("first_parent") not in (None, True, False):
271+
raise _Unsupported("history options")
272+
start, count = options.get("skip", 0), options.get("max_count")
273+
if type(start) is not int or start < 0 or (count is not None and (type(count) is not int or count < 0)):
274+
raise _Unsupported("history limits")
275+
tip = repo.find_object(_oid(repo, rev)).peel_to_commit().id
276+
platform = repo.rev_walk([tip])
277+
if options.get("first_parent"):
278+
platform = platform.first_parent_only()
279+
cursor = platform.all()
280+
281+
def iterate() -> Iterator[str]:
282+
try:
283+
with cursor:
284+
for item in islice(cursor, start, None if count is None else start + count):
285+
yield str(item.id)
286+
except gix.Error as exc:
287+
raise GitCommandError(["gix", "rev-list", rev], 128, str(exc)) from exc
288+
289+
return iterate()
290+
291+
292+
def history(command: Any, rev: str, paths: Any, options: Dict[str, Any], *, count: bool = False) -> Any:
293+
"""Count reachable commits, or lazily walk the first-parent chain."""
294+
if gix is None:
295+
return NotImplemented
296+
method = "Commit.count" if count else "Commit.iter_items"
297+
command._require_version()
298+
try:
299+
if paths:
300+
raise _Unsupported("history path filtering")
301+
if (
302+
not count
303+
and not options.get("first_parent")
304+
and not (options.get("max_count") == 1 and not options.get("skip"))
305+
):
306+
raise _Unsupported("Git history ordering (GIX-8)")
307+
iterator = _walk(_repository(command, {}), rev, options)
308+
result = sum(1 for _ in iterator) if count else iterator
309+
except _Unsupported as exc:
310+
return _fallback(method, str(exc))
311+
except gix.Error as exc:
312+
_logger.debug("%s native preparation: %s", method, exc)
313+
return _fallback(method, "native preparation diagnostics")
314+
record(method, "native")
315+
return result
316+
317+
264318
_HANDLERS: Dict[str, Callable[[Any, List[str], Dict[str, Any]], bytes]] = {
265319
"rev_parse": _rev_parse,
266320
"ls_tree": _ls_tree,

‎git/objects/commit.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import tempfile
1717

1818

19+
from git import _backend
1920
from git.cmd import Git
2021
from git.exc import GitCommandError, UnsafeOptionError
2122
from git.diff import Diffable
@@ -321,6 +322,9 @@ def count(
321322

322323
# Yes, it makes a difference whether empty paths are given or not in our case as
323324
# the empty paths version will ignore merge commits for some reason.
325+
native = _backend.history(self.repo.git, self.hexsha, paths, kwargs, count=True)
326+
if native is not NotImplemented:
327+
return native
324328
if paths:
325329
return len(self.repo.git._call_process_safe("rev_list", self.hexsha, "--", paths, **kwargs).splitlines())
326330
return len(self.repo.git._call_process_safe("rev_list", self.hexsha, **kwargs).splitlines())

0 commit comments

Comments
 (0)