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

209 statements  

1# This module is part of GitPython and is released under the 

2# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 

3 

4"""General repository-related functions.""" 

5 

6from __future__ import annotations 

7 

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] 

19 

20import os 

21import os.path as osp 

22from pathlib import Path 

23import stat 

24from string import digits 

25 

26from gitdb.exc import BadName, BadObject 

27 

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 

33 

34# Typing ---------------------------------------------------------------------- 

35 

36from typing import Optional, TYPE_CHECKING, Union, cast, overload 

37 

38from git.types import AnyGitObject, Literal, PathLike 

39 

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 

45 

46 from .base import Repo 

47 

48# ---------------------------------------------------------------------------- 

49 

50 

51def touch(filename: str) -> str: 

52 with open(filename, "ab"): 

53 pass 

54 return filename 

55 

56 

57def is_git_dir(d: PathLike) -> bool: 

58 """This is taken from the git setup.c:is_git_directory function. 

59 

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 

78 

79 

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 

88 

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 

97 

98 

99def find_submodule_git_dir(d: PathLike) -> Optional[PathLike]: 

100 """Search for a submodule repo.""" 

101 if is_git_dir(d): 

102 return d 

103 

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:] 

113 

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 

124 

125 

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. 

131 

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 

140 

141 

142@overload 

143def name_to_object(repo: "Repo", name: str, return_ref: Literal[False] = ...) -> AnyGitObject: ... 

144 

145 

146@overload 

147def name_to_object(repo: "Repo", name: str, return_ref: Literal[True]) -> Union[AnyGitObject, SymbolicReference]: ... 

148 

149 

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. 

155 

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 

162 

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 

172 

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 

194 

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 

199 

200 # Tried everything ? fail. 

201 if hexsha is None: 

202 raise BadName(name) 

203 # END assert hexsha was found 

204 

205 return Object.new_from_sha(repo, hex_to_bin(hexsha)) 

206 

207 

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 

217 

218 

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) 

223 

224 if obj.type != "commit": 

225 raise ValueError("Cannot convert object %r to type commit" % obj) 

226 # END verify type 

227 return obj 

228 

229 

230def rev_parse(repo: "Repo", rev: str) -> AnyGitObject: 

231 """Parse a revision string. Like :manpage:`git-rev-parse(1)`. 

232 

233 :return: 

234 `~git.objects.base.Object` at the given revision. 

235 

236 This may be any type of git object: 

237 

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>` 

242 

243 :param rev: 

244 :manpage:`git-rev-parse(1)`-compatible revision specification as string. 

245 Please see :manpage:`git-rev-parse(1)` for details. 

246 

247 :raise gitdb.exc.BadObject: 

248 If the given revision could not be found. 

249 

250 :raise ValueError: 

251 If `rev` couldn't be parsed. 

252 

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 

261 

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 

273 

274 token = rev[start] 

275 

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 

292 

293 start += 1 

294 

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. 

301 

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 

337 

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 

343 

344 obj = Object.new_from_sha(repo, hex_to_bin(entry.newhexsha)) 

345 

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 

351 

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 

357 

358 start = end + 1 # Skip brace. 

359 parsed_to = start 

360 continue 

361 # END parse type 

362 

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 

376 

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 

382 

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 

412 

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 

418 

419 if obj is None: 

420 raise ValueError("Revision specifier could not be parsed: %s" % rev) 

421 

422 if parsed_to != lr: 

423 raise ValueError("Didn't consume complete rev spec %s, consumed part: %s" % (rev, rev[:parsed_to])) 

424 

425 return obj