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

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

331 

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 

337 

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

339 

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 

345 

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 

351 

352 start = end + 1 # Skip brace. 

353 parsed_to = start 

354 continue 

355 # END parse type 

356 

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 

370 

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 

376 

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 

406 

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 

412 

413 if obj is None: 

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

415 

416 if parsed_to != lr: 

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

418 

419 return obj