Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/git/repo/fun.py: 44%
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 pass # Default.
305 elif output_type == "tree":
306 try:
307 obj = cast(AnyGitObject, obj)
308 obj = to_commit(obj).tree
309 except (AttributeError, ValueError):
310 pass # Error raised later.
311 # END exception handling
312 elif output_type in ("", "blob"):
313 obj = cast("TagObject", obj)
314 if obj and obj.type == "tag":
315 obj = deref_tag(obj)
316 else:
317 # Cannot do anything for non-tags.
318 pass
319 # END handle tag
320 elif token == "@":
321 # try single int
322 assert ref is not None, "Require Reference to access reflog"
323 revlog_index = None
324 try:
325 # Transform reversed index into the format of our revlog.
326 revlog_index = -(int(output_type) + 1)
327 except ValueError as e:
328 # TODO: Try to parse the other date options, using parse_date maybe.
329 raise NotImplementedError("Support for additional @{...} modes not implemented") from e
330 # END handle revlog index
332 try:
333 entry = ref.log_entry(revlog_index)
334 except IndexError as e:
335 raise IndexError("Invalid revlog index: %i" % revlog_index) from e
336 # END handle index out of bound
338 obj = Object.new_from_sha(repo, hex_to_bin(entry.newhexsha))
340 # Make it pass the following checks.
341 output_type = ""
342 else:
343 raise ValueError("Invalid output type: %s ( in %s )" % (output_type, rev))
344 # END handle output type
346 # Empty output types don't require any specific type, its just about
347 # dereferencing tags.
348 if output_type and obj and obj.type != output_type:
349 raise ValueError("Could not accommodate requested object type %r, got %s" % (output_type, obj.type))
350 # END verify output type
352 start = end + 1 # Skip brace.
353 parsed_to = start
354 continue
355 # END parse type
357 # Try to parse a number.
358 num = 0
359 if token != ":":
360 found_digit = False
361 while start < lr:
362 if rev[start] in digits:
363 num = num * 10 + int(rev[start])
364 start += 1
365 found_digit = True
366 else:
367 break
368 # END handle number
369 # END number parse loop
371 # No explicit number given, 1 is the default. It could be 0 though.
372 if not found_digit:
373 num = 1
374 # END set default num
375 # END number parsing only if non-blob mode
377 parsed_to = start
378 # Handle hierarchy walk.
379 try:
380 obj = cast(AnyGitObject, obj)
381 if token == "~":
382 obj = to_commit(obj)
383 for _ in range(num):
384 obj = obj.parents[0]
385 # END for each history item to walk
386 elif token == "^":
387 obj = to_commit(obj)
388 # Must be n'th parent.
389 if num:
390 obj = obj.parents[num - 1]
391 elif token == ":":
392 if obj.type != "tree":
393 obj = obj.tree
394 # END get tree type
395 obj = obj[rev[start:]]
396 parsed_to = lr
397 else:
398 raise ValueError("Invalid token: %r" % token)
399 # END end handle tag
400 except (IndexError, AttributeError) as e:
401 raise BadName(
402 f"Invalid revision spec '{rev}' - not enough " f"parent commits to reach '{token}{int(num)}'"
403 ) from e
404 # END exception handling
405 # END parse loop
407 # Still no obj? It's probably a simple name.
408 if obj is None:
409 obj = name_to_object(repo, rev)
410 parsed_to = lr
411 # END handle simple name
413 if obj is None:
414 raise ValueError("Revision specifier could not be parsed: %s" % rev)
416 if parsed_to != lr:
417 raise ValueError("Didn't consume complete rev spec %s, consumed part: %s" % (rev, rev[:parsed_to]))
419 return obj