Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/git/repo/fun.py: 51%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This module is part of GitPython and is released under the
2# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/
4"""General repository-related functions."""
6from __future__ import annotations
8__all__ = [
9 "rev_parse",
10 "is_git_dir",
11 "touch",
12 "find_submodule_git_dir",
13 "name_to_object",
14 "short_to_long",
15 "deref_tag",
16 "to_commit",
17 "find_worktree_git_dir",
18]
20import os
21import os.path as osp
22from pathlib import Path
23import stat
24from string import digits
26from gitdb.exc import BadName, BadObject
28from git.cmd import Git
29from git.exc import WorkTreeRepositoryUnsupported
30from git.objects import Object
31from git.refs import SymbolicReference
32from git.util import cygpath, bin_to_hex, hex_to_bin
34# Typing ----------------------------------------------------------------------
36from typing import Optional, TYPE_CHECKING, Union, cast, overload
38from git.types import AnyGitObject, Literal, PathLike
40if TYPE_CHECKING:
41 from git.db import GitCmdObjectDB
42 from git.objects import Commit, TagObject
43 from git.refs.reference import Reference
44 from git.refs.tag import Tag
46 from .base import Repo
48# ----------------------------------------------------------------------------
51def touch(filename: str) -> str:
52 with open(filename, "ab"):
53 pass
54 return filename
57def is_git_dir(d: PathLike) -> bool:
58 """This is taken from the git setup.c:is_git_directory function.
60 :raise git.exc.WorkTreeRepositoryUnsupported:
61 If it sees a worktree directory. It's quite hacky to do that here, but at least
62 clearly indicates that we don't support it. There is the unlikely danger to
63 throw if we see directories which just look like a worktree dir, but are none.
64 """
65 if osp.isdir(d):
66 if (osp.isdir(osp.join(d, "objects")) or "GIT_OBJECT_DIRECTORY" in os.environ) and osp.isdir(
67 osp.join(d, "refs")
68 ):
69 headref = osp.join(d, "HEAD")
70 return osp.isfile(headref) or (osp.islink(headref) and os.readlink(headref).startswith("refs"))
71 elif (
72 osp.isfile(osp.join(d, "gitdir"))
73 and osp.isfile(osp.join(d, "commondir"))
74 and osp.isfile(osp.join(d, "gitfile"))
75 ):
76 raise WorkTreeRepositoryUnsupported(d)
77 return False
80def find_worktree_git_dir(dotgit: PathLike) -> Optional[str]:
81 """Search for a gitdir for this worktree."""
82 try:
83 statbuf = os.stat(dotgit)
84 except OSError:
85 return None
86 if not stat.S_ISREG(statbuf.st_mode):
87 return None
89 try:
90 lines = Path(dotgit).read_text().splitlines()
91 for key, value in [line.strip().split(": ") for line in lines]:
92 if key == "gitdir":
93 return value
94 except ValueError:
95 pass
96 return None
99def find_submodule_git_dir(d: PathLike) -> Optional[PathLike]:
100 """Search for a submodule repo."""
101 if is_git_dir(d):
102 return d
104 try:
105 with open(d) as fp:
106 content = fp.read().rstrip()
107 except IOError:
108 # It's probably not a file.
109 pass
110 else:
111 if content.startswith("gitdir: "):
112 path = content[8:]
114 if Git.is_cygwin():
115 # Cygwin creates submodules prefixed with `/cygdrive/...`.
116 # Cygwin git understands Cygwin paths much better than Windows ones.
117 # Also the Cygwin tests are assuming Cygwin paths.
118 path = cygpath(path)
119 if not osp.isabs(path):
120 path = osp.normpath(osp.join(osp.dirname(d), path))
121 return find_submodule_git_dir(path)
122 # END handle exception
123 return None
126def short_to_long(odb: "GitCmdObjectDB", hexsha: str) -> Optional[bytes]:
127 """
128 :return:
129 Long hexadecimal sha1 from the given less than 40 byte hexsha, or ``None`` if no
130 candidate could be found.
132 :param hexsha:
133 hexsha with less than 40 bytes.
134 """
135 try:
136 return bin_to_hex(odb.partial_to_complete_sha_hex(hexsha))
137 except BadObject:
138 return None
139 # END exception handling
142@overload
143def name_to_object(repo: "Repo", name: str, return_ref: Literal[False] = ...) -> AnyGitObject: ...
146@overload
147def name_to_object(repo: "Repo", name: str, return_ref: Literal[True]) -> Union[AnyGitObject, SymbolicReference]: ...
150def name_to_object(repo: "Repo", name: str, return_ref: bool = False) -> Union[AnyGitObject, SymbolicReference]:
151 """
152 :return:
153 Object specified by the given name - hexshas (short and long) as well as
154 references are supported.
156 :param return_ref:
157 If ``True``, and name specifies a reference, we will return the reference
158 instead of the object. Otherwise it will raise :exc:`~gitdb.exc.BadObject` or
159 :exc:`~gitdb.exc.BadName`.
160 """
161 hexsha: Union[None, str, bytes] = None
163 # Is it a hexsha? Try the most common ones, which is 7 to 40.
164 if repo.re_hexsha_shortened.match(name):
165 if len(name) != 40:
166 # Find long sha for short sha.
167 hexsha = short_to_long(repo.odb, name)
168 else:
169 hexsha = name
170 # END handle short shas
171 # END find sha if it matches
173 # If we couldn't find an object for what seemed to be a short hexsha, try to find it
174 # as reference anyway, it could be named 'aaa' for instance.
175 if hexsha is None:
176 for base in (
177 "%s",
178 "refs/%s",
179 "refs/tags/%s",
180 "refs/heads/%s",
181 "refs/remotes/%s",
182 "refs/remotes/%s/HEAD",
183 ):
184 try:
185 hexsha = SymbolicReference.dereference_recursive(repo, base % name)
186 if return_ref:
187 return SymbolicReference(repo, base % name)
188 # END handle symbolic ref
189 break
190 except ValueError:
191 pass
192 # END for each base
193 # END handle hexsha
195 # Didn't find any ref, this is an error.
196 if return_ref:
197 raise BadObject("Couldn't find reference named %r" % name)
198 # END handle return ref
200 # Tried everything ? fail.
201 if hexsha is None:
202 raise BadName(name)
203 # END assert hexsha was found
205 return Object.new_from_sha(repo, hex_to_bin(hexsha))
208def deref_tag(tag: "Tag") -> AnyGitObject:
209 """Recursively dereference a tag and return the resulting object."""
210 while True:
211 try:
212 tag = tag.object
213 except AttributeError:
214 break
215 # END dereference tag
216 return tag
219def to_commit(obj: Object) -> "Commit":
220 """Convert the given object to a commit if possible and return it."""
221 if obj.type == "tag":
222 obj = deref_tag(obj)
224 if obj.type != "commit":
225 raise ValueError("Cannot convert object %r to type commit" % obj)
226 # END verify type
227 return obj
230def rev_parse(repo: "Repo", rev: str) -> AnyGitObject:
231 """Parse a revision string. Like :manpage:`git-rev-parse(1)`.
233 :return:
234 `~git.objects.base.Object` at the given revision.
236 This may be any type of git object:
238 * :class:`Commit <git.objects.commit.Commit>`
239 * :class:`TagObject <git.objects.tag.TagObject>`
240 * :class:`Tree <git.objects.tree.Tree>`
241 * :class:`Blob <git.objects.blob.Blob>`
243 :param rev:
244 :manpage:`git-rev-parse(1)`-compatible revision specification as string.
245 Please see :manpage:`git-rev-parse(1)` for details.
247 :raise gitdb.exc.BadObject:
248 If the given revision could not be found.
250 :raise ValueError:
251 If `rev` couldn't be parsed.
253 :raise IndexError:
254 If an invalid reflog index is specified.
255 """
256 # Are we in colon search mode?
257 if rev.startswith(":/"):
258 # Colon search mode
259 raise NotImplementedError("commit by message search (regex)")
260 # END handle search
262 obj: Optional[AnyGitObject] = None
263 ref = None
264 output_type = "commit"
265 start = 0
266 parsed_to = 0
267 lr = len(rev)
268 while start < lr:
269 if rev[start] not in "^~:@":
270 start += 1
271 continue
272 # END handle start
274 token = rev[start]
276 if obj is None:
277 # token is a rev name.
278 if start == 0:
279 ref = repo.head.ref
280 else:
281 if token == "@":
282 ref = cast("Reference", name_to_object(repo, rev[:start], return_ref=True))
283 else:
284 obj = name_to_object(repo, rev[:start])
285 # END handle token
286 # END handle refname
287 else:
288 if ref is not None:
289 obj = cast("Commit", ref.commit)
290 # END handle ref
291 # END initialize obj on first token
293 start += 1
295 # Try to parse {type}.
296 if start < lr and rev[start] == "{":
297 end = rev.find("}", start)
298 if end == -1:
299 raise ValueError("Missing closing brace to define type in %s" % rev)
300 output_type = rev[start + 1 : end] # Exclude brace.
302 # Handle type.
303 if output_type == "commit":
304 obj = cast("TagObject", obj)
305 if obj and obj.type == "tag":
306 obj = deref_tag(obj)
307 else:
308 # Cannot do anything for non-tags.
309 pass
310 # END handle tag
311 elif output_type == "tree":
312 try:
313 obj = cast(AnyGitObject, obj)
314 obj = to_commit(obj).tree
315 except (AttributeError, ValueError):
316 pass # Error raised later.
317 # END exception handling
318 elif output_type in ("", "blob"):
319 obj = cast("TagObject", obj)
320 if obj and obj.type == "tag":
321 obj = deref_tag(obj)
322 else:
323 # Cannot do anything for non-tags.
324 pass
325 # END handle tag
326 elif token == "@":
327 # try single int
328 assert ref is not None, "Require Reference to access reflog"
329 revlog_index = None
330 try:
331 # Transform reversed index into the format of our revlog.
332 revlog_index = -(int(output_type) + 1)
333 except ValueError as e:
334 # TODO: Try to parse the other date options, using parse_date maybe.
335 raise NotImplementedError("Support for additional @{...} modes not implemented") from e
336 # END handle revlog index
338 try:
339 entry = ref.log_entry(revlog_index)
340 except IndexError as e:
341 raise IndexError("Invalid revlog index: %i" % revlog_index) from e
342 # END handle index out of bound
344 obj = Object.new_from_sha(repo, hex_to_bin(entry.newhexsha))
346 # Make it pass the following checks.
347 output_type = ""
348 else:
349 raise ValueError("Invalid output type: %s ( in %s )" % (output_type, rev))
350 # END handle output type
352 # Empty output types don't require any specific type, its just about
353 # dereferencing tags.
354 if output_type and obj and obj.type != output_type:
355 raise ValueError("Could not accommodate requested object type %r, got %s" % (output_type, obj.type))
356 # END verify output type
358 start = end + 1 # Skip brace.
359 parsed_to = start
360 continue
361 # END parse type
363 # Try to parse a number.
364 num = 0
365 if token != ":":
366 found_digit = False
367 while start < lr:
368 if rev[start] in digits:
369 num = num * 10 + int(rev[start])
370 start += 1
371 found_digit = True
372 else:
373 break
374 # END handle number
375 # END number parse loop
377 # No explicit number given, 1 is the default. It could be 0 though.
378 if not found_digit:
379 num = 1
380 # END set default num
381 # END number parsing only if non-blob mode
383 parsed_to = start
384 # Handle hierarchy walk.
385 try:
386 obj = cast(AnyGitObject, obj)
387 if token == "~":
388 obj = to_commit(obj)
389 for _ in range(num):
390 obj = obj.parents[0]
391 # END for each history item to walk
392 elif token == "^":
393 obj = to_commit(obj)
394 # Must be n'th parent.
395 if num:
396 obj = obj.parents[num - 1]
397 elif token == ":":
398 if obj.type != "tree":
399 obj = obj.tree
400 # END get tree type
401 obj = obj[rev[start:]]
402 parsed_to = lr
403 else:
404 raise ValueError("Invalid token: %r" % token)
405 # END end handle tag
406 except (IndexError, AttributeError) as e:
407 raise BadName(
408 f"Invalid revision spec '{rev}' - not enough parent commits to reach '{token}{int(num)}'"
409 ) from e
410 # END exception handling
411 # END parse loop
413 # Still no obj? It's probably a simple name.
414 if obj is None:
415 obj = name_to_object(repo, rev)
416 parsed_to = lr
417 # END handle simple name
419 if obj is None:
420 raise ValueError("Revision specifier could not be parsed: %s" % rev)
422 if parsed_to != lr:
423 raise ValueError("Didn't consume complete rev spec %s, consumed part: %s" % (rev, rev[:parsed_to]))
425 return obj