Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/git/diff.py: 27%

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

315 statements  

1# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors 

2# 

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

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

5 

6__all__ = ["DiffConstants", "NULL_TREE", "NULL_TREE_SHA", "INDEX", "Diffable", "DiffIndex", "Diff"] 

7 

8import enum 

9import re 

10import warnings 

11 

12from git.cmd import Git, handle_process_output 

13from git.compat import defenc 

14from git.objects.blob import Blob 

15from git.objects.util import mode_str_to_int 

16from git.util import finalize_process, hex_to_bin 

17 

18# typing ------------------------------------------------------------------ 

19 

20from typing import ( 

21 Any, 

22 Iterator, 

23 List, 

24 Match, 

25 Optional, 

26 Sequence, 

27 Tuple, 

28 TYPE_CHECKING, 

29 TypeVar, 

30 Union, 

31 cast, 

32) 

33from git.types import PathLike, Literal 

34 

35if TYPE_CHECKING: 

36 from subprocess import Popen 

37 

38 from git.objects.base import IndexObject 

39 from git.objects.commit import Commit 

40 from git.objects.tree import Tree 

41 from git.repo.base import Repo 

42 

43Lit_change_type = Literal["A", "D", "C", "M", "R", "T", "U"] 

44 

45# ------------------------------------------------------------------------ 

46 

47 

48@enum.unique 

49class DiffConstants(enum.Enum): 

50 """Special objects for :meth:`Diffable.diff`. 

51 

52 See the :meth:`Diffable.diff` method's ``other`` parameter, which accepts various 

53 values including these. 

54 

55 :note: 

56 These constants are also available as attributes of the :mod:`git.diff` module, 

57 the :class:`Diffable` class and its subclasses and instances, and the top-level 

58 :mod:`git` module. 

59 """ 

60 

61 NULL_TREE = enum.auto() 

62 """Stand-in indicating you want to compare against the empty tree in diffs. 

63 

64 Also accessible as :const:`git.NULL_TREE`, :const:`git.diff.NULL_TREE`, and 

65 :const:`Diffable.NULL_TREE`. 

66 """ 

67 

68 INDEX = enum.auto() 

69 """Stand-in indicating you want to diff against the index. 

70 

71 Also accessible as :const:`git.INDEX`, :const:`git.diff.INDEX`, and 

72 :const:`Diffable.INDEX`, as well as :const:`Diffable.Index`. The latter has been 

73 kept for backward compatibility and made an alias of this, so it may still be used. 

74 """ 

75 

76 

77NULL_TREE: Literal[DiffConstants.NULL_TREE] = DiffConstants.NULL_TREE 

78"""Stand-in indicating you want to compare against the empty tree in diffs. 

79 

80See :meth:`Diffable.diff`, which accepts this as a value of its ``other`` parameter. 

81 

82This is an alias of :const:`DiffConstants.NULL_TREE`, which may also be accessed as 

83:const:`git.NULL_TREE` and :const:`Diffable.NULL_TREE`. 

84""" 

85 

86NULL_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" 

87"""SHA of Git's canonical empty tree object.""" 

88 

89INDEX: Literal[DiffConstants.INDEX] = DiffConstants.INDEX 

90"""Stand-in indicating you want to diff against the index. 

91 

92See :meth:`Diffable.diff`, which accepts this as a value of its ``other`` parameter. 

93 

94This is an alias of :const:`DiffConstants.INDEX`, which may also be accessed as 

95:const:`git.INDEX` and :const:`Diffable.INDEX`, as well as :const:`Diffable.Index`. 

96""" 

97 

98 

99def _unquote_path(path: bytes) -> bytes: 

100 result = bytearray() 

101 escapes = { 

102 ord("a"): 7, 

103 ord("b"): 8, 

104 ord("f"): 12, 

105 ord("n"): 10, 

106 ord("r"): 13, 

107 ord("t"): 9, 

108 ord("v"): 11, 

109 } 

110 i = 0 

111 while i < len(path): 

112 if path[i] != ord("\\") or i + 1 == len(path): 

113 result.append(path[i]) 

114 i += 1 

115 continue 

116 if path[i + 1] in b"0123" and i + 3 < len(path) and all(c in b"01234567" for c in path[i + 2 : i + 4]): 

117 result.append(int(path[i + 1 : i + 4], 8)) 

118 i += 4 

119 continue 

120 escaped = path[i + 1] 

121 if escaped in escapes or escaped in b'\\"': 

122 result.append(escapes.get(escaped, escaped)) 

123 else: 

124 result.extend(path[i : i + 2]) 

125 i += 2 

126 return bytes(result) 

127 

128 

129def decode_path(path: bytes, has_ab_prefix: bool = True) -> Optional[bytes]: 

130 if path == b"/dev/null": 

131 return None 

132 

133 if path.startswith(b'"') and path.endswith(b'"'): 

134 path = _unquote_path(path[1:-1]) 

135 

136 if has_ab_prefix: 

137 assert path.startswith(b"a/") or path.startswith(b"b/") 

138 path = path[2:] 

139 

140 return path 

141 

142 

143class Diffable: 

144 """Common interface for all objects that can be diffed against another object of 

145 compatible type. 

146 

147 :note: 

148 Subclasses require a :attr:`repo` member, as it is the case for 

149 :class:`~git.objects.base.Object` instances. For practical reasons we do not 

150 derive from :class:`~git.objects.base.Object`. 

151 """ 

152 

153 __slots__ = () 

154 

155 repo: "Repo" 

156 """Repository to operate on. Must be provided by subclass or sibling class.""" 

157 

158 NULL_TREE = NULL_TREE 

159 """Stand-in indicating you want to compare against the empty tree in diffs. 

160 

161 See the :meth:`diff` method, which accepts this as a value of its ``other`` 

162 parameter. 

163 

164 This is the same as :const:`DiffConstants.NULL_TREE`, and may also be accessed as 

165 :const:`git.NULL_TREE` and :const:`git.diff.NULL_TREE`. 

166 """ 

167 

168 INDEX = INDEX 

169 """Stand-in indicating you want to diff against the index. 

170 

171 See the :meth:`diff` method, which accepts this as a value of its ``other`` 

172 parameter. 

173 

174 This is the same as :const:`DiffConstants.INDEX`, and may also be accessed as 

175 :const:`git.INDEX` and :const:`git.diff.INDEX`, as well as :class:`Diffable.INDEX`, 

176 which is kept for backward compatibility (it is now defined an alias of this). 

177 """ 

178 

179 Index = INDEX 

180 """Stand-in indicating you want to diff against the index 

181 (same as :const:`~Diffable.INDEX`). 

182 

183 This is an alias of :const:`~Diffable.INDEX`, for backward compatibility. See 

184 :const:`~Diffable.INDEX` and :meth:`diff` for details. 

185 

186 :note: 

187 Although always meant for use as an opaque constant, this was formerly defined 

188 as a class. Its usage is unchanged, but static type annotations that attempt 

189 to permit only this object must be changed to avoid new mypy errors. This was 

190 previously not possible to do, though ``Type[Diffable.Index]`` approximated it. 

191 It is now possible to do precisely, using ``Literal[DiffConstants.INDEX]``. 

192 """ 

193 

194 def _process_diff_args( 

195 self, 

196 args: List[Union[PathLike, "Diffable"]], 

197 ) -> List[Union[PathLike, "Diffable"]]: 

198 """ 

199 :return: 

200 Possibly altered version of the given args list. 

201 This method is called right before git command execution. 

202 Subclasses can use it to alter the behaviour of the superclass. 

203 """ 

204 return args 

205 

206 def diff( 

207 self, 

208 other: Union[DiffConstants, "Tree", "Commit", str, None] = INDEX, 

209 paths: Union[PathLike, List[PathLike], Tuple[PathLike, ...], None] = None, 

210 create_patch: bool = False, 

211 allow_unsafe_options: bool = False, 

212 **kwargs: Any, 

213 ) -> "DiffIndex[Diff]": 

214 """Create diffs between two items being trees, trees and index or an index and 

215 the working tree. Detects renames automatically. 

216 

217 :param other: 

218 This the item to compare us with. 

219 

220 * If ``None``, we will be compared to the working tree. 

221 

222 * If a :class:`~git.types.Tree_ish` or string, it will be compared against 

223 the respective tree. 

224 

225 * If :const:`INDEX`, it will be compared against the index. 

226 

227 * If :const:`NULL_TREE`, it will compare against the empty tree. 

228 

229 This parameter defaults to :const:`INDEX` (rather than ``None``) so that the 

230 method will not by default fail on bare repositories. 

231 

232 :param paths: 

233 This a list of paths or a single path to limit the diff to. It will only 

234 include at least one of the given path or paths. 

235 

236 :param create_patch: 

237 If ``True``, the returned :class:`Diff` contains a detailed patch that if 

238 applied makes the self to other. Patches are somewhat costly as blobs have 

239 to be read and diffed. 

240 

241 :param allow_unsafe_options: 

242 If ``True``, allow options such as ``--output``, ``--no-index``, and ``-O`` 

243 that can write to or read from arbitrary filesystem paths. 

244 

245 :param kwargs: 

246 Additional arguments passed to :manpage:`git-diff(1)`, such as ``R=True`` to 

247 swap both sides of the diff. 

248 

249 :return: 

250 A :class:`DiffIndex` representing the computed diff. 

251 

252 :note: 

253 On a bare repository, `other` needs to be provided as :const:`INDEX`, or as 

254 an instance of :class:`~git.objects.tree.Tree` or 

255 :class:`~git.objects.commit.Commit`, or a git command error will occur. 

256 """ 

257 if not allow_unsafe_options: 

258 Git.check_unsafe_options( 

259 options=Git._option_candidates([other], kwargs), 

260 unsafe_options=self.repo.unsafe_git_diff_options, 

261 clusterable_short_options="46abceflmnpqrstuvwzBCDMNRW", 

262 ) 

263 

264 args: List[Union[PathLike, Diffable]] = [] 

265 args.append("--abbrev=40") # We need full shas. 

266 args.append("--full-index") # Get full index paths, not only filenames. 

267 

268 # Remove default '-M' arg (check for renames) if user is overriding it. 

269 if not any(x in kwargs for x in ("find_renames", "no_renames", "M")): 

270 args.append("-M") 

271 

272 if create_patch: 

273 args.append("-p") 

274 args.append("--no-ext-diff") 

275 else: 

276 args.append("--raw") 

277 args.append("-z") 

278 

279 # Ensure we never see colored output. 

280 # Fixes: https://github.com/gitpython-developers/GitPython/issues/172 

281 args.append("--no-color") 

282 

283 if paths is not None and not isinstance(paths, (tuple, list)): 

284 paths = [paths] 

285 

286 diff_cmd = self.repo.git.diff 

287 if other is INDEX: 

288 args.insert(0, "--cached") 

289 elif other is NULL_TREE: 

290 args.insert(0, "-r") # Recursive diff-tree. 

291 args.insert(0, "--root") 

292 diff_cmd = self.repo.git.diff_tree 

293 elif other is not None: 

294 args.insert(0, "-r") # Recursive diff-tree. 

295 args.insert(0, other) 

296 diff_cmd = self.repo.git.diff_tree 

297 

298 args.insert(0, self) 

299 

300 # paths is a list or tuple here, or None. 

301 if paths: 

302 args.append("--") 

303 args.extend(paths) 

304 # END paths handling 

305 

306 kwargs["as_process"] = True 

307 args = self._process_diff_args(args) 

308 if create_patch: 

309 self.repo.git(c="diff.mnemonicPrefix=false") 

310 proc = diff_cmd(*args, **kwargs) 

311 

312 diff_method = Diff._index_from_patch_format if create_patch else Diff._index_from_raw_format 

313 index = diff_method(self.repo, proc) 

314 

315 proc.wait() 

316 return index 

317 

318 

319T_Diff = TypeVar("T_Diff", bound="Diff") 

320 

321 

322class DiffIndex(List[T_Diff]): 

323 R"""An index for diffs, allowing a list of :class:`Diff`\s to be queried by the diff 

324 properties. 

325 

326 The class improves the diff handling convenience. 

327 """ 

328 

329 change_type: Sequence[Literal["A", "C", "D", "R", "M", "T"]] = ("A", "C", "D", "R", "M", "T") # noqa: F821 

330 """Change type invariant identifying possible ways a blob can have changed: 

331 

332 * ``A`` = Added 

333 * ``D`` = Deleted 

334 * ``R`` = Renamed 

335 * ``M`` = Modified 

336 * ``T`` = Changed in the type 

337 """ 

338 

339 def iter_change_type(self, change_type: Lit_change_type) -> Iterator[T_Diff]: 

340 """ 

341 :return: 

342 Iterator yielding :class:`Diff` instances that match the given `change_type` 

343 

344 :param change_type: 

345 Member of :attr:`DiffIndex.change_type`, namely: 

346 

347 * 'A' for added paths 

348 * 'D' for deleted paths 

349 * 'R' for renamed paths 

350 * 'M' for paths with modified data 

351 * 'T' for changed in the type paths 

352 """ 

353 if change_type not in self.change_type: 

354 raise ValueError("Invalid change type: %s" % change_type) 

355 

356 for diffidx in self: 

357 if diffidx.change_type == change_type: 

358 yield diffidx 

359 elif change_type == "A" and diffidx.new_file: 

360 yield diffidx 

361 elif change_type == "D" and diffidx.deleted_file: 

362 yield diffidx 

363 elif change_type == "C" and diffidx.copied_file: 

364 yield diffidx 

365 elif change_type == "R" and diffidx.renamed_file: 

366 yield diffidx 

367 elif change_type == "M" and diffidx.a_blob and diffidx.b_blob and diffidx.a_blob != diffidx.b_blob: 

368 yield diffidx 

369 # END for each diff 

370 

371 

372class Diff: 

373 """A Diff contains diff information between two Trees. 

374 

375 It contains two sides a and b of the diff. Members are prefixed with "a" and "b" 

376 respectively to indicate that. 

377 

378 Diffs keep information about the changed blob objects, the file mode, renames, 

379 deletions and new files. 

380 

381 There are a few cases where ``None`` has to be expected as member variable value: 

382 

383 New File:: 

384 

385 a_mode is None 

386 a_blob is None 

387 a_path is None 

388 

389 Deleted File:: 

390 

391 b_mode is None 

392 b_blob is None 

393 b_path is None 

394 

395 Working Tree Blobs: 

396 

397 When comparing to working trees, the working tree blob will have a null hexsha 

398 as a corresponding object does not yet exist. The mode will be null as well. The 

399 path will be available, though. 

400 

401 If it is listed in a diff, the working tree version of the file must differ from 

402 the version in the index or tree, and hence has been modified. 

403 """ 

404 

405 # Precompiled regex. 

406 re_header = re.compile( 

407 rb""" 

408 ^diff[ ]--git 

409 [ ](?P<a_path_fallback>"?[ab]/.+?"?)[ ](?P<b_path_fallback>"?[ab]/.+?"?)\n 

410 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n 

411 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))? 

412 (?:^similarity[ ]index[ ]\d+%\n 

413 ^rename[ ]from[ ](?P<rename_from>.*)\n 

414 ^rename[ ]to[ ](?P<rename_to>.*)(?:\n|$))? 

415 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))? 

416 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))? 

417 (?:^similarity[ ]index[ ]\d+%\n 

418 ^copy[ ]from[ ].*\n 

419 ^copy[ ]to[ ](?P<copied_file_name>.*)(?:\n|$))? 

420 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+) 

421 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))? 

422 (?:^---[ ](?P<a_path>[^\t\n\r\f\v]*)[\t\r\f\v]*(?:\n|$))? 

423 (?:^\+\+\+[ ](?P<b_path>[^\t\n\r\f\v]*)[\t\r\f\v]*(?:\n|$))? 

424 """, 

425 re.VERBOSE | re.MULTILINE, 

426 ) 

427 

428 # These can be used for comparisons. 

429 NULL_HEX_SHA = "0" * 40 

430 NULL_BIN_SHA = b"\0" * 20 

431 

432 __slots__ = ( 

433 "a_blob", 

434 "b_blob", 

435 "a_mode", 

436 "b_mode", 

437 "a_rawpath", 

438 "b_rawpath", 

439 "new_file", 

440 "deleted_file", 

441 "copied_file", 

442 "raw_rename_from", 

443 "raw_rename_to", 

444 "diff", 

445 "change_type", 

446 "score", 

447 ) 

448 

449 def __init__( 

450 self, 

451 repo: "Repo", 

452 a_rawpath: Optional[bytes], 

453 b_rawpath: Optional[bytes], 

454 a_blob_id: Union[str, bytes, None], 

455 b_blob_id: Union[str, bytes, None], 

456 a_mode: Union[bytes, str, None], 

457 b_mode: Union[bytes, str, None], 

458 new_file: bool, 

459 deleted_file: bool, 

460 copied_file: bool, 

461 raw_rename_from: Optional[bytes], 

462 raw_rename_to: Optional[bytes], 

463 diff: Union[str, bytes, None], 

464 change_type: Optional[Lit_change_type], 

465 score: Optional[int], 

466 ) -> None: 

467 assert a_rawpath is None or isinstance(a_rawpath, bytes) 

468 assert b_rawpath is None or isinstance(b_rawpath, bytes) 

469 self.a_rawpath = a_rawpath 

470 self.b_rawpath = b_rawpath 

471 

472 self.a_mode = mode_str_to_int(a_mode) if a_mode else None 

473 self.b_mode = mode_str_to_int(b_mode) if b_mode else None 

474 

475 # Determine whether this diff references a submodule. If it does then 

476 # we need to overwrite "repo" to the corresponding submodule's repo instead. 

477 if repo and a_rawpath: 

478 for submodule in repo.submodules: 

479 if submodule.path == a_rawpath.decode(defenc, "replace"): 

480 if submodule.module_exists(): 

481 repo = submodule.module() 

482 break 

483 

484 self.a_blob: Union["IndexObject", None] 

485 if a_blob_id is None or a_blob_id == self.NULL_HEX_SHA: 

486 self.a_blob = None 

487 else: 

488 self.a_blob = Blob(repo, hex_to_bin(a_blob_id), mode=self.a_mode, path=self.a_path) 

489 

490 self.b_blob: Union["IndexObject", None] 

491 if b_blob_id is None or b_blob_id == self.NULL_HEX_SHA: 

492 self.b_blob = None 

493 else: 

494 self.b_blob = Blob(repo, hex_to_bin(b_blob_id), mode=self.b_mode, path=self.b_path) 

495 

496 self.new_file: bool = new_file 

497 self.deleted_file: bool = deleted_file 

498 self.copied_file: bool = copied_file 

499 

500 # Be clear and use None instead of empty strings. 

501 assert raw_rename_from is None or isinstance(raw_rename_from, bytes) 

502 assert raw_rename_to is None or isinstance(raw_rename_to, bytes) 

503 self.raw_rename_from = raw_rename_from or None 

504 self.raw_rename_to = raw_rename_to or None 

505 

506 self.diff = diff 

507 self.change_type: Union[Lit_change_type, None] = change_type 

508 self.score = score 

509 

510 def __eq__(self, other: object) -> bool: 

511 for name in self.__slots__: 

512 if getattr(self, name) != getattr(other, name): 

513 return False 

514 # END for each name 

515 return True 

516 

517 def __ne__(self, other: object) -> bool: 

518 return not (self == other) 

519 

520 def __hash__(self) -> int: 

521 return hash(tuple(getattr(self, n) for n in self.__slots__)) 

522 

523 def __str__(self) -> str: 

524 h = "%s" 

525 if self.a_blob: 

526 h %= self.a_blob.path 

527 elif self.b_blob: 

528 h %= self.b_blob.path 

529 

530 msg = "" 

531 line = None 

532 line_length = 0 

533 for b, n in zip((self.a_blob, self.b_blob), ("lhs", "rhs")): 

534 if b: 

535 line = "\n%s: %o | %s" % (n, b.mode, b.hexsha) 

536 else: 

537 line = "\n%s: None" % n 

538 # END if blob is not None 

539 line_length = max(len(line), line_length) 

540 msg += line 

541 # END for each blob 

542 

543 # Add headline. 

544 h += "\n" + "=" * line_length 

545 

546 if self.deleted_file: 

547 msg += "\nfile deleted in rhs" 

548 if self.new_file: 

549 msg += "\nfile added in rhs" 

550 if self.copied_file: 

551 msg += "\nfile %r copied from %r" % (self.b_path, self.a_path) 

552 if self.rename_from: 

553 msg += "\nfile renamed from %r" % self.rename_from 

554 if self.rename_to: 

555 msg += "\nfile renamed to %r" % self.rename_to 

556 if self.diff: 

557 msg += "\n---" 

558 try: 

559 msg += self.diff.decode(defenc) if isinstance(self.diff, bytes) else self.diff 

560 except UnicodeDecodeError: 

561 msg += "OMITTED BINARY DATA" 

562 # END handle encoding 

563 msg += "\n---" 

564 # END diff info 

565 

566 return h + msg 

567 

568 @property 

569 def a_path(self) -> Optional[str]: 

570 return self.a_rawpath.decode(defenc, "replace") if self.a_rawpath else None 

571 

572 @property 

573 def b_path(self) -> Optional[str]: 

574 return self.b_rawpath.decode(defenc, "replace") if self.b_rawpath else None 

575 

576 @property 

577 def rename_from(self) -> Optional[str]: 

578 return self.raw_rename_from.decode(defenc, "replace") if self.raw_rename_from else None 

579 

580 @property 

581 def rename_to(self) -> Optional[str]: 

582 return self.raw_rename_to.decode(defenc, "replace") if self.raw_rename_to else None 

583 

584 @property 

585 def renamed(self) -> bool: 

586 """Deprecated, use :attr:`renamed_file` instead. 

587 

588 :return: 

589 ``True`` if the blob of our diff has been renamed 

590 

591 :note: 

592 This property is deprecated. 

593 Please use the :attr:`renamed_file` property instead. 

594 """ 

595 warnings.warn( 

596 "Diff.renamed is deprecated, use Diff.renamed_file instead", 

597 DeprecationWarning, 

598 stacklevel=2, 

599 ) 

600 return self.renamed_file 

601 

602 @property 

603 def renamed_file(self) -> bool: 

604 """:return: ``True`` if the blob of our diff has been renamed""" 

605 return self.rename_from != self.rename_to 

606 

607 @classmethod 

608 def _pick_best_path(cls, path_match: bytes, rename_match: bytes, path_fallback_match: bytes) -> Optional[bytes]: 

609 if path_match: 

610 return decode_path(path_match) 

611 

612 if rename_match: 

613 return decode_path(rename_match, has_ab_prefix=False) 

614 

615 if path_fallback_match: 

616 return decode_path(path_fallback_match) 

617 

618 return None 

619 

620 @classmethod 

621 def _index_from_patch_format(cls, repo: "Repo", proc: Union["Popen", "Git.AutoInterrupt"]) -> DiffIndex["Diff"]: 

622 """Create a new :class:`DiffIndex` from the given process output which must be 

623 in patch format. 

624 

625 :param repo: 

626 The repository we are operating on. 

627 

628 :param proc: 

629 :manpage:`git-diff(1)` process to read from 

630 (supports :class:`Git.AutoInterrupt <git.cmd.Git.AutoInterrupt>` wrapper). 

631 

632 :return: 

633 :class:`DiffIndex` 

634 """ 

635 

636 # FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise. 

637 text_list: List[bytes] = [] 

638 stderr_list: List[bytes] = [] 

639 

640 def finalize_process_with_stderr(proc: Union["Popen", "Git.AutoInterrupt"]) -> None: 

641 finalize_process(proc, stderr=b"".join(stderr_list)) 

642 

643 handle_process_output( 

644 proc, text_list.append, stderr_list.append, finalize_process_with_stderr, decode_streams=False 

645 ) 

646 

647 # For now, we have to bake the stream. 

648 text = b"".join(text_list) 

649 index: "DiffIndex" = DiffIndex() 

650 previous_header: Union[Match[bytes], None] = None 

651 header: Union[Match[bytes], None] = None 

652 a_path, b_path = None, None # For mypy. 

653 a_mode, b_mode = None, None # For mypy. 

654 for _header in cls.re_header.finditer(text): 

655 ( 

656 a_path_fallback, 

657 b_path_fallback, 

658 old_mode, 

659 new_mode, 

660 rename_from, 

661 rename_to, 

662 new_file_mode, 

663 deleted_file_mode, 

664 copied_file_name, 

665 a_blob_id, 

666 b_blob_id, 

667 b_mode, 

668 a_path, 

669 b_path, 

670 ) = _header.groups() 

671 

672 new_file, deleted_file, copied_file = ( 

673 bool(new_file_mode), 

674 bool(deleted_file_mode), 

675 bool(copied_file_name), 

676 ) 

677 

678 a_path = cls._pick_best_path(a_path, rename_from, a_path_fallback) 

679 b_path = cls._pick_best_path(b_path, rename_to, b_path_fallback) 

680 

681 # Our only means to find the actual text is to see what has not been matched 

682 # by our regex, and then retro-actively assign it to our index. 

683 if previous_header is not None: 

684 index[-1].diff = text[previous_header.end() : _header.start()] 

685 # END assign actual diff 

686 

687 # Make sure the mode is set if the path is set. Otherwise the resulting blob 

688 # is invalid. We just use the one mode we should have parsed. 

689 a_mode = old_mode or deleted_file_mode or (a_path and (b_mode or new_mode or new_file_mode)) 

690 b_mode = b_mode or new_mode or new_file_mode or (b_path and a_mode) 

691 index.append( 

692 Diff( 

693 repo, 

694 a_path, 

695 b_path, 

696 a_blob_id and a_blob_id.decode(defenc), 

697 b_blob_id and b_blob_id.decode(defenc), 

698 a_mode and a_mode.decode(defenc), 

699 b_mode and b_mode.decode(defenc), 

700 new_file, 

701 deleted_file, 

702 copied_file, 

703 rename_from, 

704 rename_to, 

705 None, 

706 None, 

707 None, 

708 ) 

709 ) 

710 

711 previous_header = _header 

712 header = _header 

713 # END for each header we parse 

714 if index and header: 

715 index[-1].diff = text[header.end() :] 

716 # END assign last diff 

717 

718 return index 

719 

720 @staticmethod 

721 def _handle_diff_line(lines_bytes: bytes, repo: "Repo", index: DiffIndex["Diff"]) -> None: 

722 lines = lines_bytes.decode(defenc) 

723 

724 # Discard everything before the first colon, and the colon itself. 

725 _, _, lines = lines.partition(":") 

726 

727 for line in lines.split("\x00:"): 

728 if not line: 

729 # The line data is empty, skip. 

730 continue 

731 meta, _, path = line.partition("\x00") 

732 path = path.rstrip("\x00") 

733 a_blob_id: Optional[str] 

734 b_blob_id: Optional[str] 

735 old_mode, new_mode, a_blob_id, b_blob_id, _change_type = meta.split(None, 4) 

736 # Change type can be R100 

737 # R: status letter 

738 # 100: score (in case of copy and rename) 

739 change_type: Lit_change_type = cast(Lit_change_type, _change_type[0]) 

740 score_str = "".join(_change_type[1:]) 

741 score = int(score_str) if score_str.isdigit() else None 

742 path = path.strip("\n") 

743 a_path = path.encode(defenc) 

744 b_path = path.encode(defenc) 

745 deleted_file = False 

746 new_file = False 

747 copied_file = False 

748 rename_from = None 

749 rename_to = None 

750 

751 # NOTE: We cannot conclude from the existence of a blob to change type, 

752 # as diffs with the working do not have blobs yet. 

753 if change_type == "D": 

754 b_blob_id = None # Optional[str] 

755 deleted_file = True 

756 elif change_type == "A": 

757 a_blob_id = None 

758 new_file = True 

759 elif change_type == "C": 

760 copied_file = True 

761 a_path_str, b_path_str = path.split("\x00", 1) 

762 a_path = a_path_str.encode(defenc) 

763 b_path = b_path_str.encode(defenc) 

764 elif change_type == "R": 

765 a_path_str, b_path_str = path.split("\x00", 1) 

766 a_path = a_path_str.encode(defenc) 

767 b_path = b_path_str.encode(defenc) 

768 rename_from, rename_to = a_path, b_path 

769 elif change_type == "T": 

770 # Nothing to do. 

771 pass 

772 # END add/remove handling 

773 

774 diff = Diff( 

775 repo, 

776 a_path, 

777 b_path, 

778 a_blob_id, 

779 b_blob_id, 

780 old_mode, 

781 new_mode, 

782 new_file, 

783 deleted_file, 

784 copied_file, 

785 rename_from, 

786 rename_to, 

787 "", 

788 change_type, 

789 score, 

790 ) 

791 index.append(diff) 

792 

793 @classmethod 

794 def _index_from_raw_format(cls, repo: "Repo", proc: "Popen") -> "DiffIndex[Diff]": 

795 """Create a new :class:`DiffIndex` from the given process output which must be 

796 in raw format. 

797 

798 :param repo: 

799 The repository we are operating on. 

800 

801 :param proc: 

802 Process to read output from. 

803 

804 :return: 

805 :class:`DiffIndex` 

806 """ 

807 # handles 

808 # :100644 100644 687099101... 37c5e30c8... M .gitignore 

809 

810 index: "DiffIndex" = DiffIndex() 

811 stderr_list: List[bytes] = [] 

812 

813 def finalize_process_with_stderr(proc: Union["Popen", "Git.AutoInterrupt"]) -> None: 

814 finalize_process(proc, stderr=b"".join(stderr_list)) 

815 

816 handle_process_output( 

817 proc, 

818 lambda byt: cls._handle_diff_line(byt, repo, index), 

819 stderr_list.append, 

820 finalize_process_with_stderr, 

821 decode_streams=False, 

822 ) 

823 

824 return index