Skip to content

Commit e98f57b

Browse files
authored
Merge pull request #1672 from trail-of-forks/robust-refname-checks
Add more checks for the validity of refnames
2 parents 1774f1e + 46d3d05 commit e98f57b

File tree

2 files changed

+84
-2
lines changed

2 files changed

+84
-2
lines changed

git/refs/symbolic.py

+48-2
Original file line numberDiff line numberDiff line change
@@ -161,15 +161,61 @@ def dereference_recursive(cls, repo: "Repo", ref_path: Union[PathLike, None]) ->
161161
return hexsha
162162
# END recursive dereferencing
163163

164+
@staticmethod
165+
def _check_ref_name_valid(ref_path: PathLike) -> None:
166+
# Based on the rules described in https://git-scm.com/docs/git-check-ref-format/#_description
167+
previous: Union[str, None] = None
168+
one_before_previous: Union[str, None] = None
169+
for c in str(ref_path):
170+
if c in " ~^:?*[\\":
171+
raise ValueError(
172+
f"Invalid reference '{ref_path}': references cannot contain spaces, tildes (~), carets (^),"
173+
f" colons (:), question marks (?), asterisks (*), open brackets ([) or backslashes (\\)"
174+
)
175+
elif c == ".":
176+
if previous is None or previous == "/":
177+
raise ValueError(
178+
f"Invalid reference '{ref_path}': references cannot start with a period (.) or contain '/.'"
179+
)
180+
elif previous == ".":
181+
raise ValueError(f"Invalid reference '{ref_path}': references cannot contain '..'")
182+
elif c == "/":
183+
if previous == "/":
184+
raise ValueError(f"Invalid reference '{ref_path}': references cannot contain '//'")
185+
elif previous is None:
186+
raise ValueError(
187+
f"Invalid reference '{ref_path}': references cannot start with forward slashes '/'"
188+
)
189+
elif c == "{" and previous == "@":
190+
raise ValueError(f"Invalid reference '{ref_path}': references cannot contain '@{{'")
191+
elif ord(c) < 32 or ord(c) == 127:
192+
raise ValueError(f"Invalid reference '{ref_path}': references cannot contain ASCII control characters")
193+
194+
one_before_previous = previous
195+
previous = c
196+
197+
if previous == ".":
198+
raise ValueError(f"Invalid reference '{ref_path}': references cannot end with a period (.)")
199+
elif previous == "/":
200+
raise ValueError(f"Invalid reference '{ref_path}': references cannot end with a forward slash (/)")
201+
elif previous == "@" and one_before_previous is None:
202+
raise ValueError(f"Invalid reference '{ref_path}': references cannot be '@'")
203+
elif any([component.endswith(".lock") for component in str(ref_path).split("/")]):
204+
raise ValueError(
205+
f"Invalid reference '{ref_path}': references cannot have slash-separated components that end with"
206+
f" '.lock'"
207+
)
208+
164209
@classmethod
165210
def _get_ref_info_helper(
166211
cls, repo: "Repo", ref_path: Union[PathLike, None]
167212
) -> Union[Tuple[str, None], Tuple[None, str]]:
168213
"""Return: (str(sha), str(target_ref_path)) if available, the sha the file at
169214
rela_path points to, or None. target_ref_path is the reference we
170215
point to, or None"""
171-
if ".." in str(ref_path):
172-
raise ValueError(f"Invalid reference '{ref_path}'")
216+
if ref_path:
217+
cls._check_ref_name_valid(ref_path)
218+
173219
tokens: Union[None, List[str], Tuple[str, str]] = None
174220
repodir = _git_dir(repo, ref_path)
175221
try:

test/test_refs.py

+36
Original file line numberDiff line numberDiff line change
@@ -631,3 +631,39 @@ def test_refs_outside_repo(self):
631631
ref_file.flush()
632632
ref_file_name = Path(ref_file.name).name
633633
self.assertRaises(BadName, self.rorepo.commit, f"../../{ref_file_name}")
634+
635+
def test_validity_ref_names(self):
636+
check_ref = SymbolicReference._check_ref_name_valid
637+
# Based on the rules specified in https://git-scm.com/docs/git-check-ref-format/#_description
638+
# Rule 1
639+
self.assertRaises(ValueError, check_ref, ".ref/begins/with/dot")
640+
self.assertRaises(ValueError, check_ref, "ref/component/.begins/with/dot")
641+
self.assertRaises(ValueError, check_ref, "ref/ends/with/a.lock")
642+
self.assertRaises(ValueError, check_ref, "ref/component/ends.lock/with/period_lock")
643+
# Rule 2
644+
check_ref("valid_one_level_refname")
645+
# Rule 3
646+
self.assertRaises(ValueError, check_ref, "ref/contains/../double/period")
647+
# Rule 4
648+
for c in " ~^:":
649+
self.assertRaises(ValueError, check_ref, f"ref/contains/invalid{c}/character")
650+
for code in range(0, 32):
651+
self.assertRaises(ValueError, check_ref, f"ref/contains/invalid{chr(code)}/ASCII/control_character")
652+
self.assertRaises(ValueError, check_ref, f"ref/contains/invalid{chr(127)}/ASCII/control_character")
653+
# Rule 5
654+
for c in "*?[":
655+
self.assertRaises(ValueError, check_ref, f"ref/contains/invalid{c}/character")
656+
# Rule 6
657+
self.assertRaises(ValueError, check_ref, "/ref/begins/with/slash")
658+
self.assertRaises(ValueError, check_ref, "ref/ends/with/slash/")
659+
self.assertRaises(ValueError, check_ref, "ref/contains//double/slash/")
660+
# Rule 7
661+
self.assertRaises(ValueError, check_ref, "ref/ends/with/dot.")
662+
# Rule 8
663+
self.assertRaises(ValueError, check_ref, "ref/contains@{/at_brace")
664+
# Rule 9
665+
self.assertRaises(ValueError, check_ref, "@")
666+
# Rule 10
667+
self.assertRaises(ValueError, check_ref, "ref/contain\\s/backslash")
668+
# Valid reference name should not raise
669+
check_ref("valid/ref/name")

0 commit comments

Comments
 (0)