2 files changed
@@ -35,7 +35,7 @@ | |||
| 35 | 35 | ||
| 36 | 36 | # Typing ---------------------------------------------------------------------- | |
| 37 | 37 | ||
| 38 | - from typing import Optional, TYPE_CHECKING, Tuple, Union, cast, overload | ||
| 38 | + from typing import Iterator, Optional, TYPE_CHECKING, Tuple, Union, cast, overload | ||
| 39 | 39 | ||
| 40 | 40 | from git.types import AnyGitObject, Literal, PathLike | |
| 41 | 41 | ||
@@ -190,10 +190,6 @@ def name_to_object(repo: "Repo", name: str, return_ref: bool = False) -> Union[A | |||
| 190 | 190 | # END handle short shas | |
| 191 | 191 | # END find sha if it matches | |
| 192 | 192 | ||
| 193 | - if hexsha is None: | ||
| 194 | - hexsha = _describe_to_long(repo, name) | ||
| 195 | - # END handle describe output | ||
| 196 | - | ||
| 197 | 193 | # If we couldn't find an object for what seemed to be a short hexsha, try to find it | |
| 198 | 194 | # as reference anyway, it could be named 'aaa' for instance. | |
| 199 | 195 | if hexsha is None: | |
@@ -216,6 +212,10 @@ def name_to_object(repo: "Repo", name: str, return_ref: bool = False) -> Union[A | |||
| 216 | 212 | # END for each base | |
| 217 | 213 | # END handle hexsha | |
| 218 | 214 | ||
| 215 | + if hexsha is None: | ||
| 216 | + hexsha = _describe_to_long(repo, name) | ||
| 217 | + # END handle describe output | ||
| 218 | + | ||
| 219 | 219 | # Didn't find any ref, this is an error. | |
| 220 | 220 | if return_ref: | |
| 221 | 221 | raise BadObject("Couldn't find reference named %r" % name) | |
@@ -363,6 +363,8 @@ def _tracking_branch_object(repo: "Repo", ref: Optional[SymbolicReference]) -> A | |||
| 363 | 363 | raise BadName("@{upstream}") from e | |
| 364 | 364 | elif isinstance(ref, Head): | |
| 365 | 365 | head = ref | |
| 366 | + elif os.fspath(ref.path).startswith("refs/heads/"): | ||
| 367 | + head = Head(repo, ref.path) | ||
| 366 | 368 | else: | |
| 367 | 369 | raise BadName("%s@{upstream}" % ref.name) | |
| 368 | 370 | # END handle head | |
@@ -479,11 +481,15 @@ def _find_commit_by_message( | |||
| 479 | 481 | repo: "Repo", rev: Optional[AnyGitObject], pattern: str, braced: bool = False | |
| 480 | 482 | ) -> AnyGitObject: | |
| 481 | 483 | 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 | ||
| 483 | 489 | if rev is None: | |
| 484 | - commits = repo.iter_commits("--all") | ||
| 490 | + commits = _all_ref_commits(repo) | ||
| 485 | 491 | else: | |
| 486 | - commits = repo.iter_commits(to_commit(cast(Object, rev)).hexsha) | ||
| 492 | + commits = _reachable_commits([to_commit(cast(Object, rev))]) | ||
| 487 | 493 | # END handle starting point | |
| 488 | 494 | ||
| 489 | 495 | for commit in commits: | |
@@ -499,6 +505,38 @@ def _find_commit_by_message( | |||
| 499 | 505 | raise BadName("No commit found matching message pattern %r" % pattern) | |
| 500 | 506 | ||
| 501 | 507 | ||
| 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 | + | ||
| 502 | 540 | def _index_lookup(repo: "Repo", spec: str) -> AnyGitObject: | |
| 503 | 541 | if not spec: | |
| 504 | 542 | raise ValueError("':' must be followed by a path") | |
@@ -527,8 +565,6 @@ def _tree_lookup(obj: AnyGitObject, path: str) -> AnyGitObject: | |||
| 527 | 565 | ||
| 528 | 566 | ||
| 529 | 567 | def _peel(obj: AnyGitObject, output_type: str, repo: "Repo", rev: str) -> AnyGitObject: | |
| 530 | - if output_type == "/": | ||
| 531 | - return obj | ||
| 532 | 568 | if output_type.startswith("/"): | |
| 533 | 569 | return _find_commit_by_message(repo, obj, output_type[1:], braced=True) | |
| 534 | 570 | if output_type == "": | |
@@ -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 | + | ||
| 1 | 6 | from pathlib import Path | |
| 2 | 7 | ||
| 3 | 8 | import pytest | |
| 4 | 9 | ||
| 5 | 10 | from git import Repo | |
| 11 | + from git.refs import RemoteReference | ||
| 12 | + from git.refs import SymbolicReference | ||
| 6 | 13 | from gitdb.exc import BadName | |
| 7 | 14 | ||
| 8 | 15 | ||
@@ -31,14 +38,12 @@ def rev_parse_repo(tmp_path): | |||
| 31 | 38 | repo.create_tag("v1.0", ref=release) | |
| 32 | 39 | main = repo.active_branch | |
| 33 | 40 | ||
| 34 | - side = repo.create_head("side", root) | ||
| 35 | - side.checkout() | ||
| 36 | 41 | _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) | ||
| 38 | 44 | ||
| 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) | ||
| 42 | 47 | ||
| 43 | 48 | repo.create_head("aaaaaaaa", merge) | |
| 44 | 49 | repo.create_tag("@foo", ref=merge) | |
@@ -55,16 +60,21 @@ def rev_parse_repo(tmp_path): | |||
| 55 | 60 | ||
| 56 | 61 | def test_rev_parse_names_hex_and_describe_forms(rev_parse_repo): | |
| 57 | 62 | repo = rev_parse_repo["repo"] | |
| 63 | + release = rev_parse_repo["release"] | ||
| 58 | 64 | merge = rev_parse_repo["merge"] | |
| 59 | 65 | ||
| 60 | 66 | assert repo.rev_parse("@") == merge | |
| 61 | 67 | assert repo.rev_parse("@foo") == merge | |
| 62 | 68 | assert repo.rev_parse("aaaaaaaa") == merge | |
| 63 | 69 | assert repo.rev_parse(merge.hexsha[:7]) == merge | |
| 70 | + describe_name = "anything-9-g%s" % merge.hexsha[:7] | ||
| 64 | 71 | 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 | ||
| 66 | 73 | assert repo.rev_parse("%s-dirty" % merge.hexsha[:7]) == merge | |
| 67 | 74 | ||
| 75 | + repo.create_tag(describe_name, ref=release) | ||
| 76 | + assert repo.rev_parse(describe_name) == release | ||
| 77 | + | ||
| 68 | 78 | ||
| 69 | 79 | def test_rev_parse_navigation_and_peeling(rev_parse_repo): | |
| 70 | 80 | repo = rev_parse_repo["repo"] | |
@@ -87,7 +97,8 @@ def test_rev_parse_navigation_and_peeling(rev_parse_repo): | |||
| 87 | 97 | assert repo.rev_parse("ann^{}") == root | |
| 88 | 98 | assert repo.rev_parse("ann^{commit}") == root | |
| 89 | 99 | 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^{/}") | ||
| 91 | 102 | ||
| 92 | 103 | ||
| 93 | 104 | def test_rev_parse_tree_and_index_paths(rev_parse_repo): | |
@@ -114,6 +125,10 @@ def test_rev_parse_reflog_selectors(rev_parse_repo): | |||
| 114 | 125 | assert repo.rev_parse("%s@{0}" % main.name) == merge | |
| 115 | 126 | assert repo.rev_parse("@{-1}") == side | |
| 116 | 127 | ||
| 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 | + | ||
| 117 | 132 | ||
| 118 | 133 | def test_rev_parse_commit_message_search(rev_parse_repo): | |
| 119 | 134 | repo = rev_parse_repo["repo"] | |
@@ -132,6 +147,10 @@ def test_rev_parse_rejects_invalid_object_specs(rev_parse_repo): | |||
| 132 | 147 | repo.rev_parse(":") | |
| 133 | 148 | with pytest.raises(ValueError): | |
| 134 | 149 | repo.rev_parse(":/") | |
| 150 | + with pytest.raises(ValueError): | ||
| 151 | + repo.rev_parse(":/[") | ||
| 152 | + with pytest.raises(ValueError): | ||
| 153 | + repo.rev_parse("HEAD^{/[}") | ||
| 135 | 154 | with pytest.raises(ValueError): | |
| 136 | 155 | repo.rev_parse("@{-0}") | |
| 137 | 156 | with pytest.raises(ValueError): | |
0 commit comments