Skip to content

Commit 6cf7ac3

Browse files
codexByron
authored andcommitted
Address rev-parse review feedback
1 parent bdbdf4b commit 6cf7ac3

2 files changed

Lines changed: 73 additions & 18 deletions

File tree

git/repo/fun.py

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636
# Typing ----------------------------------------------------------------------
3737

38-
from typing import Optional, TYPE_CHECKING, Tuple, Union, cast, overload
38+
from typing import Iterator, Optional, TYPE_CHECKING, Tuple, Union, cast, overload
3939

4040
from git.types import AnyGitObject, Literal, PathLike
4141

@@ -190,10 +190,6 @@ def name_to_object(repo: "Repo", name: str, return_ref: bool = False) -> Union[A
190190
# END handle short shas
191191
# END find sha if it matches
192192

193-
if hexsha is None:
194-
hexsha = _describe_to_long(repo, name)
195-
# END handle describe output
196-
197193
# If we couldn't find an object for what seemed to be a short hexsha, try to find it
198194
# as reference anyway, it could be named 'aaa' for instance.
199195
if hexsha is None:
@@ -216,6 +212,10 @@ def name_to_object(repo: "Repo", name: str, return_ref: bool = False) -> Union[A
216212
# END for each base
217213
# END handle hexsha
218214

215+
if hexsha is None:
216+
hexsha = _describe_to_long(repo, name)
217+
# END handle describe output
218+
219219
# Didn't find any ref, this is an error.
220220
if return_ref:
221221
raise BadObject("Couldn't find reference named %r" % name)
@@ -363,6 +363,8 @@ def _tracking_branch_object(repo: "Repo", ref: Optional[SymbolicReference]) -> A
363363
raise BadName("@{upstream}") from e
364364
elif isinstance(ref, Head):
365365
head = ref
366+
elif os.fspath(ref.path).startswith("refs/heads/"):
367+
head = Head(repo, ref.path)
366368
else:
367369
raise BadName("%s@{upstream}" % ref.name)
368370
# END handle head
@@ -479,11 +481,15 @@ def _find_commit_by_message(
479481
repo: "Repo", rev: Optional[AnyGitObject], pattern: str, braced: bool = False
480482
) -> AnyGitObject:
481483
pattern, negated = _parse_search(_unescape_braced_regex(pattern) if braced else pattern)
482-
regex = re.compile(pattern)
484+
try:
485+
regex = re.compile(pattern)
486+
except re.error as e:
487+
raise ValueError("Invalid commit message regex %r" % pattern) from e
488+
# END handle invalid regex
483489
if rev is None:
484-
commits = repo.iter_commits("--all")
490+
commits = _all_ref_commits(repo)
485491
else:
486-
commits = repo.iter_commits(to_commit(cast(Object, rev)).hexsha)
492+
commits = _reachable_commits([to_commit(cast(Object, rev))])
487493
# END handle starting point
488494

489495
for commit in commits:
@@ -499,6 +505,38 @@ def _find_commit_by_message(
499505
raise BadName("No commit found matching message pattern %r" % pattern)
500506

501507

508+
def _all_ref_commits(repo: "Repo") -> Iterator["Commit"]:
509+
starts = []
510+
for ref in repo.references:
511+
try:
512+
starts.append(to_commit(cast(Object, ref.object)))
513+
except (BadName, ValueError):
514+
pass
515+
# END skip refs that do not point to commits
516+
# END for each ref
517+
try:
518+
starts.append(repo.head.commit)
519+
except ValueError:
520+
pass
521+
# END handle unborn head
522+
return _reachable_commits(starts)
523+
524+
525+
def _reachable_commits(starts: list["Commit"]) -> Iterator["Commit"]:
526+
seen = set()
527+
pending = starts[:]
528+
while pending:
529+
pending.sort(key=lambda commit: commit.committed_date, reverse=True)
530+
commit = pending.pop(0)
531+
if commit.binsha in seen:
532+
continue
533+
# END skip seen commit
534+
seen.add(commit.binsha)
535+
yield commit
536+
pending.extend(commit.parents)
537+
# END while commits remain
538+
539+
502540
def _index_lookup(repo: "Repo", spec: str) -> AnyGitObject:
503541
if not spec:
504542
raise ValueError("':' must be followed by a path")
@@ -527,8 +565,6 @@ def _tree_lookup(obj: AnyGitObject, path: str) -> AnyGitObject:
527565

528566

529567
def _peel(obj: AnyGitObject, output_type: str, repo: "Repo", rev: str) -> AnyGitObject:
530-
if output_type == "/":
531-
return obj
532568
if output_type.startswith("/"):
533569
return _find_commit_by_message(repo, obj, output_type[1:], braced=True)
534570
if output_type == "":

test/test_rev_parse.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
# Copyright (C) 2026 Michael Trier (mtrier@gmail.com) and contributors
2+
#
3+
# This module is part of GitPython and is released under the
4+
# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
5+
16
from pathlib import Path
27

38
import pytest
49

510
from git import Repo
11+
from git.refs import RemoteReference
12+
from git.refs import SymbolicReference
613
from gitdb.exc import BadName
714

815

@@ -31,14 +38,12 @@ def rev_parse_repo(tmp_path):
3138
repo.create_tag("v1.0", ref=release)
3239
main = repo.active_branch
3340

34-
side = repo.create_head("side", root)
35-
side.checkout()
3641
_write(repo, "side.txt", "side\n")
37-
side_commit = repo.index.commit("side branch")
42+
side_commit = repo.index.commit("side branch", parent_commits=[root], head=False, skip_hooks=True)
43+
repo.create_head("side", side_commit)
3844

39-
main.checkout()
40-
repo.git.merge("--no-ff", "side", "-m", "merge side")
41-
merge = repo.head.commit
45+
merge = repo.index.commit("merge side", parent_commits=[release, side_commit], skip_hooks=True)
46+
repo.head.log_append(side_commit.binsha, "checkout: moving from side to main", merge.binsha)
4247

4348
repo.create_head("aaaaaaaa", merge)
4449
repo.create_tag("@foo", ref=merge)
@@ -55,16 +60,21 @@ def rev_parse_repo(tmp_path):
5560

5661
def test_rev_parse_names_hex_and_describe_forms(rev_parse_repo):
5762
repo = rev_parse_repo["repo"]
63+
release = rev_parse_repo["release"]
5864
merge = rev_parse_repo["merge"]
5965

6066
assert repo.rev_parse("@") == merge
6167
assert repo.rev_parse("@foo") == merge
6268
assert repo.rev_parse("aaaaaaaa") == merge
6369
assert repo.rev_parse(merge.hexsha[:7]) == merge
70+
describe_name = "anything-9-g%s" % merge.hexsha[:7]
6471
assert repo.rev_parse("v1.0-1-g%s" % merge.hexsha[:7]) == merge
65-
assert repo.rev_parse("anything-9-g%s" % merge.hexsha[:7]) == merge
72+
assert repo.rev_parse(describe_name) == merge
6673
assert repo.rev_parse("%s-dirty" % merge.hexsha[:7]) == merge
6774

75+
repo.create_tag(describe_name, ref=release)
76+
assert repo.rev_parse(describe_name) == release
77+
6878

6979
def test_rev_parse_navigation_and_peeling(rev_parse_repo):
7080
repo = rev_parse_repo["repo"]
@@ -87,7 +97,8 @@ def test_rev_parse_navigation_and_peeling(rev_parse_repo):
8797
assert repo.rev_parse("ann^{}") == root
8898
assert repo.rev_parse("ann^{commit}") == root
8999
assert repo.rev_parse("HEAD^{tree}") == merge.tree
90-
assert repo.rev_parse("HEAD^{/}") == merge
100+
with pytest.raises(ValueError):
101+
repo.rev_parse("HEAD^{/}")
91102

92103

93104
def test_rev_parse_tree_and_index_paths(rev_parse_repo):
@@ -114,6 +125,10 @@ def test_rev_parse_reflog_selectors(rev_parse_repo):
114125
assert repo.rev_parse("%s@{0}" % main.name) == merge
115126
assert repo.rev_parse("@{-1}") == side
116127

128+
SymbolicReference.create(repo, "refs/remotes/origin/%s" % main.name, merge)
129+
main.set_tracking_branch(RemoteReference(repo, "refs/remotes/origin/%s" % main.name))
130+
assert repo.rev_parse("%s@{upstream}" % main.name) == merge
131+
117132

118133
def test_rev_parse_commit_message_search(rev_parse_repo):
119134
repo = rev_parse_repo["repo"]
@@ -132,6 +147,10 @@ def test_rev_parse_rejects_invalid_object_specs(rev_parse_repo):
132147
repo.rev_parse(":")
133148
with pytest.raises(ValueError):
134149
repo.rev_parse(":/")
150+
with pytest.raises(ValueError):
151+
repo.rev_parse(":/[")
152+
with pytest.raises(ValueError):
153+
repo.rev_parse("HEAD^{/[}")
135154
with pytest.raises(ValueError):
136155
repo.rev_parse("@{-0}")
137156
with pytest.raises(ValueError):

0 commit comments

Comments
 (0)