Skip to content

Commit dbbfc5a

Browse files
authored
Revert "Revert "git: dulwich: implement list_all_commits (#419)" (#424)" (#425)
1 parent c0ffe0c commit dbbfc5a

File tree

3 files changed

+206
-12
lines changed

3 files changed

+206
-12
lines changed

src/scmrepo/git/backend/dulwich/__init__.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
)
1818

1919
from dulwich.config import ConfigFile, StackedConfig
20+
from dulwich.walk import ORDER_DATE
2021
from funcy import cached_property, reraise
2122

2223
from scmrepo.exceptions import AuthError, CloneError, InvalidRemote, RevError, SCMError
@@ -473,7 +474,25 @@ def list_tags(self) -> Iterable[str]:
473474
return sorted(ref[len(base) :] for ref in self.iter_refs(base))
474475

475476
def list_all_commits(self) -> Iterable[str]:
476-
raise NotImplementedError
477+
from dulwich.objects import Tag
478+
479+
repo = self.repo
480+
starting_points: list[bytes] = []
481+
482+
# HEAD
483+
head_rev = self.get_ref("HEAD")
484+
if head_rev:
485+
starting_points.append(head_rev.encode("utf-8"))
486+
487+
# Branches and remotes
488+
for ref in repo.refs:
489+
if ref.startswith((b"refs/heads/", b"refs/remotes/", b"refs/tags/")):
490+
if isinstance(repo.refs[ref], Tag):
491+
ref = self.repo.get_peeled(repo.refs[ref])
492+
starting_points.append(repo.refs[ref])
493+
494+
walker = self.repo.get_walker(include=starting_points, order=ORDER_DATE)
495+
return [e.commit.id.decode() for e in walker]
477496

478497
def get_tree_obj(self, rev: str, **kwargs) -> DulwichObject:
479498
from dulwich.objectspec import parse_tree

src/scmrepo/git/backend/pygit2/__init__.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,32 @@ def list_tags(self) -> Iterable[str]:
452452
return sorted(ref[len(base) :] for ref in self.iter_refs(base))
453453

454454
def list_all_commits(self) -> Iterable[str]:
455-
raise NotImplementedError
455+
import pygit2
456+
from pygit2.enums import SortMode
457+
458+
# Add HEAD
459+
starting_points: list[Union[Oid, str]] = []
460+
if not self.repo.head_is_unborn:
461+
starting_points.append(self.repo.head.target)
462+
463+
# Add all branches, remotes, and tags
464+
for ref in self.repo.references:
465+
if ref.startswith(("refs/heads/", "refs/remotes/")):
466+
oid = self.repo.revparse_single(ref).id
467+
starting_points.append(oid)
468+
elif ref.startswith("refs/tags/"):
469+
tag_obj = self.repo.revparse_single(ref)
470+
if isinstance(tag_obj, pygit2.Tag):
471+
starting_points.append(tag_obj.target)
472+
else:
473+
starting_points.append(tag_obj.id)
474+
475+
# Walk all commits
476+
walker = self.repo.walk(None)
477+
for oid in starting_points:
478+
walker.push(oid)
479+
walker.sort(SortMode.TIME)
480+
return [str(commit.id) for commit in walker]
456481

457482
def get_tree_obj(self, rev: str, **kwargs) -> Pygit2Object:
458483
tree = self.repo[rev].tree

tests/test_git.py

Lines changed: 160 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import shutil
3+
import time
34
from pathlib import Path
45
from typing import Any, Optional
56

@@ -422,21 +423,170 @@ def test_iter_remote_refs(
422423
} == set(git.iter_remote_refs(remote))
423424

424425

425-
@pytest.mark.skip_git_backend("dulwich", "pygit2")
426+
def _gen(scm: Git, s: str, commit_timestamp: Optional[float] = None) -> str:
427+
with open(s, mode="w") as f:
428+
f.write(s)
429+
scm.dulwich.add([s])
430+
scm.dulwich.repo.do_commit(
431+
message=s.encode("utf-8"), commit_timestamp=commit_timestamp
432+
)
433+
return scm.get_rev()
434+
435+
426436
def test_list_all_commits(tmp_dir: TmpDir, scm: Git, git: Git, matcher: type[Matcher]):
427-
def _gen(s):
428-
tmp_dir.gen(s, s)
429-
scm.add_commit(s, message=s)
430-
return scm.get_rev()
437+
assert git.list_all_commits() == []
438+
# https://github.com/libgit2/libgit2/issues/6336
439+
now = time.time()
440+
441+
rev_a = _gen(scm, "a", commit_timestamp=now - 10)
442+
rev_b = _gen(scm, "b", commit_timestamp=now - 8)
443+
rev_c = _gen(scm, "c", commit_timestamp=now - 5)
444+
rev_d = _gen(scm, "d", commit_timestamp=now - 2)
445+
446+
assert git.list_all_commits() == [rev_d, rev_c, rev_b, rev_a]
447+
448+
scm.gitpython.git.reset(rev_b, hard=True)
449+
assert git.list_all_commits() == [rev_b, rev_a]
431450

432-
rev_a = _gen("a")
433-
rev_b = _gen("b")
451+
452+
def test_list_all_commits_branch(
453+
tmp_dir: TmpDir, scm: Git, git: Git, matcher: type[Matcher]
454+
):
455+
revs = {}
456+
now = time.time()
457+
458+
revs["1"] = _gen(scm, "a", commit_timestamp=now - 10)
459+
460+
scm.checkout("branch", create_new=True)
461+
revs["3"] = _gen(scm, "c", commit_timestamp=now - 9)
462+
463+
scm.checkout("master")
464+
revs["2"] = _gen(scm, "b", commit_timestamp=now - 7)
465+
466+
scm.checkout("branch")
467+
revs["5"] = _gen(scm, "e", commit_timestamp=now - 6)
468+
469+
scm.checkout("master")
470+
revs["4"] = _gen(scm, "d", commit_timestamp=now - 5)
471+
472+
scm.checkout("branch")
473+
revs["6"] = _gen(scm, "f", commit_timestamp=now - 4)
474+
475+
scm.checkout("master")
476+
revs["7"] = _gen(scm, "g", commit_timestamp=now - 3)
477+
revs["8"] = scm.merge("branch", msg="merge branch")
478+
479+
inv_map = {v: k for k, v in revs.items()}
480+
assert [inv_map[k] for k in git.list_all_commits()] == [
481+
"8",
482+
"7",
483+
"6",
484+
"4",
485+
"5",
486+
"2",
487+
"3",
488+
"1",
489+
]
490+
491+
492+
def test_list_all_tags(tmp_dir: TmpDir, scm: Git, git: Git, matcher: type[Matcher]):
493+
rev_a = _gen(scm, "a")
434494
scm.tag("tag")
435-
rev_c = _gen("c")
495+
rev_b = _gen(scm, "b")
496+
scm.tag("annotated", annotated=True, message="Annotated Tag")
497+
rev_c = _gen(scm, "c")
498+
rev_d = _gen(scm, "d")
499+
assert git.list_all_commits() == matcher.unordered(rev_d, rev_c, rev_b, rev_a)
500+
501+
rev_e = _gen(scm, "e")
502+
scm.tag(
503+
"annotated2",
504+
target="refs/tags/annotated",
505+
annotated=True,
506+
message="Annotated Tag",
507+
)
508+
assert git.list_all_commits() == matcher.unordered(
509+
rev_e, rev_d, rev_c, rev_b, rev_a
510+
)
511+
512+
rev_f = _gen(scm, "f")
513+
scm.tag(
514+
"annotated3",
515+
target="refs/tags/annotated2",
516+
annotated=True,
517+
message="Annotated Tag 3",
518+
)
519+
assert git.list_all_commits() == matcher.unordered(
520+
rev_f, rev_e, rev_d, rev_c, rev_b, rev_a
521+
)
522+
523+
scm.gitpython.git.reset(rev_a, hard=True)
524+
assert git.list_all_commits() == matcher.unordered(rev_b, rev_a)
525+
526+
527+
def test_list_all_commits_dangling_annotated_tag(tmp_dir: TmpDir, scm: Git, git: Git):
528+
rev_a = _gen(scm, "a")
529+
scm.tag("annotated", annotated=True, message="Annotated Tag")
530+
531+
_gen(scm, "b")
532+
533+
# Delete branch pointing to rev_a
534+
scm.checkout(rev_a)
535+
scm.gitpython.repo.delete_head("master", force=True)
536+
537+
assert git.list_all_commits() == [rev_a] # Only reachable via the tag
538+
539+
540+
def test_list_all_commits_orphan(
541+
tmp_dir: TmpDir, scm: Git, git: Git, matcher: type[Matcher]
542+
):
543+
rev_a = _gen(scm, "a")
544+
545+
# Make an orphan branch
546+
scm.gitpython.git.checkout("--orphan", "orphan-branch")
547+
rev_orphan = _gen(scm, "orphanfile")
548+
549+
assert rev_orphan != rev_a
550+
assert git.list_all_commits() == matcher.unordered(rev_orphan, rev_a)
551+
552+
553+
def test_list_all_commits_refs(
554+
tmp_dir: TmpDir, scm: Git, git: Git, matcher: type[Matcher]
555+
):
556+
assert git.list_all_commits() == []
557+
558+
rev_a = _gen(scm, "a")
559+
560+
assert git.list_all_commits() == [rev_a]
561+
rev_b = _gen(scm, "b")
562+
scm.set_ref("refs/remotes/origin/feature", rev_b)
563+
assert git.list_all_commits() == matcher.unordered(rev_b, rev_a)
564+
565+
# also add refs/exps/foo/bar
566+
rev_c = _gen(scm, "c")
567+
scm.set_ref("refs/exps/foo/bar", rev_c)
568+
assert git.list_all_commits() == matcher.unordered(rev_c, rev_b, rev_a)
569+
570+
# Dangling/broken ref ---
571+
scm.set_ref("refs/heads/bad-ref", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
572+
with pytest.raises(Exception): # noqa: B017, PT011
573+
git.list_all_commits()
574+
scm.remove_ref("refs/heads/bad-ref")
575+
436576
scm.gitpython.git.reset(rev_a, hard=True)
437-
scm.set_ref("refs/foo/bar", rev_c)
577+
assert git.list_all_commits() == matcher.unordered(rev_b, rev_a)
578+
579+
580+
def test_list_all_commits_detached_head(
581+
tmp_dir: TmpDir, scm: Git, git: Git, matcher: type[Matcher]
582+
):
583+
rev_a = _gen(scm, "a")
584+
rev_b = _gen(scm, "b")
585+
rev_c = _gen(scm, "c")
586+
scm.checkout(rev_b)
438587

439-
assert git.list_all_commits() == matcher.unordered(rev_a, rev_b)
588+
assert scm.pygit2.repo.head_is_detached
589+
assert git.list_all_commits() == matcher.unordered(rev_c, rev_b, rev_a)
440590

441591

442592
@pytest.mark.skip_git_backend("pygit2")

0 commit comments

Comments
 (0)