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

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.base import IndexObject 

15from git.objects.blob import Blob 

16from git.objects.util import mode_str_to_int 

17from git.util import finalize_process, hex_to_bin 

18 

19# typing ------------------------------------------------------------------ 

20 

21from typing import ( 

22 Any, 

23 Iterator, 

24 List, 

25 Match, 

26 Optional, 

27 Sequence, 

28 Tuple, 

29 TYPE_CHECKING, 

30 TypeVar, 

31 Union, 

32 cast, 

33) 

34from git.types import PathLike, Literal 

35 

36if TYPE_CHECKING: 

37 from subprocess import Popen 

38 

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 For submodule changes, ``a_blob`` and ``b_blob`` are 

382 :class:`~git.objects.base.IndexObject` instances whose SHAs refer to commits in 

383 the submodule repository. 

384 

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

386 

387 New File:: 

388 

389 a_mode is None 

390 a_blob is None 

391 a_path is None 

392 

393 Deleted File:: 

394 

395 b_mode is None 

396 b_blob is None 

397 b_path is None 

398 

399 Working Tree Blobs: 

400 

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

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

403 path will be available, though. 

404 

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

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

407 """ 

408 

409 # Precompiled regex. 

410 re_header = re.compile( 

411 rb""" 

412 ^diff[ ]--git 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

428 """, 

429 re.VERBOSE | re.MULTILINE, 

430 ) 

431 

432 # These can be used for comparisons. 

433 NULL_HEX_SHA = "0" * 40 

434 NULL_BIN_SHA = b"\0" * 20 

435 

436 __slots__ = ( 

437 "a_blob", 

438 "b_blob", 

439 "a_mode", 

440 "b_mode", 

441 "a_rawpath", 

442 "b_rawpath", 

443 "new_file", 

444 "deleted_file", 

445 "copied_file", 

446 "raw_rename_from", 

447 "raw_rename_to", 

448 "diff", 

449 "change_type", 

450 "score", 

451 ) 

452 

453 def __init__( 

454 self, 

455 repo: "Repo", 

456 a_rawpath: Optional[bytes], 

457 b_rawpath: Optional[bytes], 

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

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

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

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

462 new_file: bool, 

463 deleted_file: bool, 

464 copied_file: bool, 

465 raw_rename_from: Optional[bytes], 

466 raw_rename_to: Optional[bytes], 

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

468 change_type: Optional[Lit_change_type], 

469 score: Optional[int], 

470 ) -> None: 

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

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

473 self.a_rawpath = a_rawpath 

474 self.b_rawpath = b_rawpath 

475 

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

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

478 

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

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

481 if repo and a_rawpath: 

482 for submodule in repo.submodules: 

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

484 if submodule.module_exists(): 

485 repo = submodule.module() 

486 break 

487 

488 # Gitlinks reference commits; generic index objects preserve their path and mode. 

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

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

491 self.a_blob = None 

492 else: 

493 self.a_blob = (IndexObject if self.a_mode == 0o160000 else Blob)( 

494 repo, hex_to_bin(a_blob_id), mode=self.a_mode, path=self.a_path 

495 ) 

496 

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

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

499 self.b_blob = None 

500 else: 

501 self.b_blob = (IndexObject if self.b_mode == 0o160000 else Blob)( 

502 repo, hex_to_bin(b_blob_id), mode=self.b_mode, path=self.b_path 

503 ) 

504 

505 self.new_file: bool = new_file 

506 self.deleted_file: bool = deleted_file 

507 self.copied_file: bool = copied_file 

508 

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

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

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

512 self.raw_rename_from = raw_rename_from or None 

513 self.raw_rename_to = raw_rename_to or None 

514 

515 self.diff = diff 

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

517 self.score = score 

518 

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

520 for name in self.__slots__: 

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

522 return False 

523 # END for each name 

524 return True 

525 

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

527 return not (self == other) 

528 

529 def __hash__(self) -> int: 

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

531 

532 def __str__(self) -> str: 

533 h = "%s" 

534 if self.a_blob: 

535 h %= self.a_blob.path 

536 elif self.b_blob: 

537 h %= self.b_blob.path 

538 

539 msg = "" 

540 line = None 

541 line_length = 0 

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

543 if b: 

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

545 else: 

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

547 # END if blob is not None 

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

549 msg += line 

550 # END for each blob 

551 

552 # Add headline. 

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

554 

555 if self.deleted_file: 

556 msg += "\nfile deleted in rhs" 

557 if self.new_file: 

558 msg += "\nfile added in rhs" 

559 if self.copied_file: 

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

561 if self.rename_from: 

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

563 if self.rename_to: 

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

565 if self.diff: 

566 msg += "\n---" 

567 try: 

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

569 except UnicodeDecodeError: 

570 msg += "OMITTED BINARY DATA" 

571 # END handle encoding 

572 msg += "\n---" 

573 # END diff info 

574 

575 return h + msg 

576 

577 @property 

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

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

580 

581 @property 

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

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

584 

585 @property 

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

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

588 

589 @property 

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

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

592 

593 @property 

594 def renamed(self) -> bool: 

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

596 

597 :return: 

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

599 

600 :note: 

601 This property is deprecated. 

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

603 """ 

604 warnings.warn( 

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

606 DeprecationWarning, 

607 stacklevel=2, 

608 ) 

609 return self.renamed_file 

610 

611 @property 

612 def renamed_file(self) -> bool: 

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

614 return self.rename_from != self.rename_to 

615 

616 @classmethod 

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

618 if path_match: 

619 return decode_path(path_match) 

620 

621 if rename_match: 

622 return decode_path(rename_match, has_ab_prefix=False) 

623 

624 if path_fallback_match: 

625 return decode_path(path_fallback_match) 

626 

627 return None 

628 

629 @classmethod 

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

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

632 in patch format. 

633 

634 :param repo: 

635 The repository we are operating on. 

636 

637 :param proc: 

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

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

640 

641 :return: 

642 :class:`DiffIndex` 

643 """ 

644 

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

646 text_list: List[bytes] = [] 

647 stderr_list: List[bytes] = [] 

648 

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

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

651 

652 handle_process_output( 

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

654 ) 

655 

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

657 text = b"".join(text_list) 

658 index: "DiffIndex" = DiffIndex() 

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

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

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

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

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

664 ( 

665 a_path_fallback, 

666 b_path_fallback, 

667 old_mode, 

668 new_mode, 

669 rename_from, 

670 rename_to, 

671 new_file_mode, 

672 deleted_file_mode, 

673 copied_file_name, 

674 a_blob_id, 

675 b_blob_id, 

676 b_mode, 

677 a_path, 

678 b_path, 

679 ) = _header.groups() 

680 

681 new_file, deleted_file, copied_file = ( 

682 bool(new_file_mode), 

683 bool(deleted_file_mode), 

684 bool(copied_file_name), 

685 ) 

686 

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

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

689 

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

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

692 if previous_header is not None: 

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

694 # END assign actual diff 

695 

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

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

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

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

700 index.append( 

701 Diff( 

702 repo, 

703 a_path, 

704 b_path, 

705 a_blob_id and a_blob_id.decode(defenc), 

706 b_blob_id and b_blob_id.decode(defenc), 

707 a_mode and a_mode.decode(defenc), 

708 b_mode and b_mode.decode(defenc), 

709 new_file, 

710 deleted_file, 

711 copied_file, 

712 rename_from, 

713 rename_to, 

714 None, 

715 None, 

716 None, 

717 ) 

718 ) 

719 

720 previous_header = _header 

721 header = _header 

722 # END for each header we parse 

723 if index and header: 

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

725 # END assign last diff 

726 

727 return index 

728 

729 @staticmethod 

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

731 lines = lines_bytes.decode(defenc) 

732 

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

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

735 

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

737 if not line: 

738 # The line data is empty, skip. 

739 continue 

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

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

742 a_blob_id: Optional[str] 

743 b_blob_id: Optional[str] 

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

745 # Change type can be R100 

746 # R: status letter 

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

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

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

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

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

752 a_path = path.encode(defenc) 

753 b_path = path.encode(defenc) 

754 deleted_file = False 

755 new_file = False 

756 copied_file = False 

757 rename_from = None 

758 rename_to = None 

759 

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

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

762 if change_type == "D": 

763 b_blob_id = None # Optional[str] 

764 deleted_file = True 

765 elif change_type == "A": 

766 a_blob_id = None 

767 new_file = True 

768 elif change_type == "C": 

769 copied_file = True 

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

771 a_path = a_path_str.encode(defenc) 

772 b_path = b_path_str.encode(defenc) 

773 elif change_type == "R": 

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

775 a_path = a_path_str.encode(defenc) 

776 b_path = b_path_str.encode(defenc) 

777 rename_from, rename_to = a_path, b_path 

778 elif change_type == "T": 

779 # Nothing to do. 

780 pass 

781 # END add/remove handling 

782 

783 diff = Diff( 

784 repo, 

785 a_path, 

786 b_path, 

787 a_blob_id, 

788 b_blob_id, 

789 old_mode, 

790 new_mode, 

791 new_file, 

792 deleted_file, 

793 copied_file, 

794 rename_from, 

795 rename_to, 

796 "", 

797 change_type, 

798 score, 

799 ) 

800 index.append(diff) 

801 

802 @classmethod 

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

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

805 in raw format. 

806 

807 :param repo: 

808 The repository we are operating on. 

809 

810 :param proc: 

811 Process to read output from. 

812 

813 :return: 

814 :class:`DiffIndex` 

815 """ 

816 # handles 

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

818 

819 index: "DiffIndex" = DiffIndex() 

820 stderr_list: List[bytes] = [] 

821 

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

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

824 

825 handle_process_output( 

826 proc, 

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

828 stderr_list.append, 

829 finalize_process_with_stderr, 

830 decode_streams=False, 

831 ) 

832 

833 return index