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

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

300 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_octal_byte_re = re.compile(rb"\\([0-9]{3})") 

99 

100 

101def _octal_repl(matchobj: Match) -> bytes: 

102 value = matchobj.group(1) 

103 value = int(value, 8) 

104 value = bytes(bytearray((value,))) 

105 return value 

106 

107 

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

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

110 return None 

111 

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

113 path = path[1:-1].replace(b"\\n", b"\n").replace(b"\\t", b"\t").replace(b'\\"', b'"').replace(b"\\\\", b"\\") 

114 

115 path = _octal_byte_re.sub(_octal_repl, path) 

116 

117 if has_ab_prefix: 

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

119 path = path[2:] 

120 

121 return path 

122 

123 

124class Diffable: 

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

126 compatible type. 

127 

128 :note: 

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

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

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

132 """ 

133 

134 __slots__ = () 

135 

136 repo: "Repo" 

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

138 

139 NULL_TREE = NULL_TREE 

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

141 

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

143 parameter. 

144 

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

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

147 """ 

148 

149 INDEX = INDEX 

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

151 

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

153 parameter. 

154 

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

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

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

158 """ 

159 

160 Index = INDEX 

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

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

163 

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

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

166 

167 :note: 

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

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

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

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

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

173 """ 

174 

175 def _process_diff_args( 

176 self, 

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

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

179 """ 

180 :return: 

181 Possibly altered version of the given args list. 

182 This method is called right before git command execution. 

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

184 """ 

185 return args 

186 

187 def diff( 

188 self, 

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

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

191 create_patch: bool = False, 

192 allow_unsafe_options: bool = False, 

193 **kwargs: Any, 

194 ) -> "DiffIndex[Diff]": 

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

196 the working tree. Detects renames automatically. 

197 

198 :param other: 

199 This the item to compare us with. 

200 

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

202 

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

204 the respective tree. 

205 

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

207 

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

209 

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

211 method will not by default fail on bare repositories. 

212 

213 :param paths: 

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

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

216 

217 :param create_patch: 

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

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

220 to be read and diffed. 

221 

222 :param allow_unsafe_options: 

223 If ``True``, allow options such as ``--output`` that can write to arbitrary 

224 filesystem paths. 

225 

226 :param kwargs: 

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

228 swap both sides of the diff. 

229 

230 :return: 

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

232 

233 :note: 

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

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

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

237 """ 

238 if not allow_unsafe_options: 

239 Git.check_unsafe_options( 

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

241 unsafe_options=self.repo.unsafe_git_revision_options, 

242 ) 

243 

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

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

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

247 

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

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

250 args.append("-M") 

251 

252 if create_patch: 

253 args.append("-p") 

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

255 else: 

256 args.append("--raw") 

257 args.append("-z") 

258 

259 # Ensure we never see colored output. 

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

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

262 

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

264 paths = [paths] 

265 

266 diff_cmd = self.repo.git.diff 

267 if other is INDEX: 

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

269 elif other is NULL_TREE: 

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

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

272 diff_cmd = self.repo.git.diff_tree 

273 elif other is not None: 

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

275 args.insert(0, other) 

276 diff_cmd = self.repo.git.diff_tree 

277 

278 args.insert(0, self) 

279 

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

281 if paths: 

282 args.append("--") 

283 args.extend(paths) 

284 # END paths handling 

285 

286 kwargs["as_process"] = True 

287 proc = diff_cmd(*self._process_diff_args(args), **kwargs) 

288 

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

290 index = diff_method(self.repo, proc) 

291 

292 proc.wait() 

293 return index 

294 

295 

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

297 

298 

299class DiffIndex(List[T_Diff]): 

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

301 properties. 

302 

303 The class improves the diff handling convenience. 

304 """ 

305 

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

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

308 

309 * ``A`` = Added 

310 * ``D`` = Deleted 

311 * ``R`` = Renamed 

312 * ``M`` = Modified 

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

314 """ 

315 

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

317 """ 

318 :return: 

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

320 

321 :param change_type: 

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

323 

324 * 'A' for added paths 

325 * 'D' for deleted paths 

326 * 'R' for renamed paths 

327 * 'M' for paths with modified data 

328 * 'T' for changed in the type paths 

329 """ 

330 if change_type not in self.change_type: 

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

332 

333 for diffidx in self: 

334 if diffidx.change_type == change_type: 

335 yield diffidx 

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

337 yield diffidx 

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

339 yield diffidx 

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

341 yield diffidx 

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

343 yield diffidx 

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

345 yield diffidx 

346 # END for each diff 

347 

348 

349class Diff: 

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

351 

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

353 respectively to indicate that. 

354 

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

356 deletions and new files. 

357 

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

359 

360 New File:: 

361 

362 a_mode is None 

363 a_blob is None 

364 a_path is None 

365 

366 Deleted File:: 

367 

368 b_mode is None 

369 b_blob is None 

370 b_path is None 

371 

372 Working Tree Blobs: 

373 

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

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

376 path will be available, though. 

377 

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

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

380 """ 

381 

382 # Precompiled regex. 

383 re_header = re.compile( 

384 rb""" 

385 ^diff[ ]--git 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

401 """, 

402 re.VERBOSE | re.MULTILINE, 

403 ) 

404 

405 # These can be used for comparisons. 

406 NULL_HEX_SHA = "0" * 40 

407 NULL_BIN_SHA = b"\0" * 20 

408 

409 __slots__ = ( 

410 "a_blob", 

411 "b_blob", 

412 "a_mode", 

413 "b_mode", 

414 "a_rawpath", 

415 "b_rawpath", 

416 "new_file", 

417 "deleted_file", 

418 "copied_file", 

419 "raw_rename_from", 

420 "raw_rename_to", 

421 "diff", 

422 "change_type", 

423 "score", 

424 ) 

425 

426 def __init__( 

427 self, 

428 repo: "Repo", 

429 a_rawpath: Optional[bytes], 

430 b_rawpath: Optional[bytes], 

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

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

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

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

435 new_file: bool, 

436 deleted_file: bool, 

437 copied_file: bool, 

438 raw_rename_from: Optional[bytes], 

439 raw_rename_to: Optional[bytes], 

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

441 change_type: Optional[Lit_change_type], 

442 score: Optional[int], 

443 ) -> None: 

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

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

446 self.a_rawpath = a_rawpath 

447 self.b_rawpath = b_rawpath 

448 

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

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

451 

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

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

454 if repo and a_rawpath: 

455 for submodule in repo.submodules: 

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

457 if submodule.module_exists(): 

458 repo = submodule.module() 

459 break 

460 

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

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

463 self.a_blob = None 

464 else: 

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

466 

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

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

469 self.b_blob = None 

470 else: 

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

472 

473 self.new_file: bool = new_file 

474 self.deleted_file: bool = deleted_file 

475 self.copied_file: bool = copied_file 

476 

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

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

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

480 self.raw_rename_from = raw_rename_from or None 

481 self.raw_rename_to = raw_rename_to or None 

482 

483 self.diff = diff 

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

485 self.score = score 

486 

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

488 for name in self.__slots__: 

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

490 return False 

491 # END for each name 

492 return True 

493 

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

495 return not (self == other) 

496 

497 def __hash__(self) -> int: 

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

499 

500 def __str__(self) -> str: 

501 h = "%s" 

502 if self.a_blob: 

503 h %= self.a_blob.path 

504 elif self.b_blob: 

505 h %= self.b_blob.path 

506 

507 msg = "" 

508 line = None 

509 line_length = 0 

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

511 if b: 

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

513 else: 

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

515 # END if blob is not None 

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

517 msg += line 

518 # END for each blob 

519 

520 # Add headline. 

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

522 

523 if self.deleted_file: 

524 msg += "\nfile deleted in rhs" 

525 if self.new_file: 

526 msg += "\nfile added in rhs" 

527 if self.copied_file: 

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

529 if self.rename_from: 

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

531 if self.rename_to: 

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

533 if self.diff: 

534 msg += "\n---" 

535 try: 

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

537 except UnicodeDecodeError: 

538 msg += "OMITTED BINARY DATA" 

539 # END handle encoding 

540 msg += "\n---" 

541 # END diff info 

542 

543 return h + msg 

544 

545 @property 

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

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

548 

549 @property 

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

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

552 

553 @property 

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

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

556 

557 @property 

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

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

560 

561 @property 

562 def renamed(self) -> bool: 

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

564 

565 :return: 

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

567 

568 :note: 

569 This property is deprecated. 

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

571 """ 

572 warnings.warn( 

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

574 DeprecationWarning, 

575 stacklevel=2, 

576 ) 

577 return self.renamed_file 

578 

579 @property 

580 def renamed_file(self) -> bool: 

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

582 return self.rename_from != self.rename_to 

583 

584 @classmethod 

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

586 if path_match: 

587 return decode_path(path_match) 

588 

589 if rename_match: 

590 return decode_path(rename_match, has_ab_prefix=False) 

591 

592 if path_fallback_match: 

593 return decode_path(path_fallback_match) 

594 

595 return None 

596 

597 @classmethod 

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

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

600 in patch format. 

601 

602 :param repo: 

603 The repository we are operating on. 

604 

605 :param proc: 

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

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

608 

609 :return: 

610 :class:`DiffIndex` 

611 """ 

612 

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

614 text_list: List[bytes] = [] 

615 stderr_list: List[bytes] = [] 

616 

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

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

619 

620 handle_process_output( 

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

622 ) 

623 

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

625 text = b"".join(text_list) 

626 index: "DiffIndex" = DiffIndex() 

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

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

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

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

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

632 ( 

633 a_path_fallback, 

634 b_path_fallback, 

635 old_mode, 

636 new_mode, 

637 rename_from, 

638 rename_to, 

639 new_file_mode, 

640 deleted_file_mode, 

641 copied_file_name, 

642 a_blob_id, 

643 b_blob_id, 

644 b_mode, 

645 a_path, 

646 b_path, 

647 ) = _header.groups() 

648 

649 new_file, deleted_file, copied_file = ( 

650 bool(new_file_mode), 

651 bool(deleted_file_mode), 

652 bool(copied_file_name), 

653 ) 

654 

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

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

657 

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

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

660 if previous_header is not None: 

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

662 # END assign actual diff 

663 

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

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

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

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

668 index.append( 

669 Diff( 

670 repo, 

671 a_path, 

672 b_path, 

673 a_blob_id and a_blob_id.decode(defenc), 

674 b_blob_id and b_blob_id.decode(defenc), 

675 a_mode and a_mode.decode(defenc), 

676 b_mode and b_mode.decode(defenc), 

677 new_file, 

678 deleted_file, 

679 copied_file, 

680 rename_from, 

681 rename_to, 

682 None, 

683 None, 

684 None, 

685 ) 

686 ) 

687 

688 previous_header = _header 

689 header = _header 

690 # END for each header we parse 

691 if index and header: 

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

693 # END assign last diff 

694 

695 return index 

696 

697 @staticmethod 

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

699 lines = lines_bytes.decode(defenc) 

700 

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

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

703 

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

705 if not line: 

706 # The line data is empty, skip. 

707 continue 

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

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

710 a_blob_id: Optional[str] 

711 b_blob_id: Optional[str] 

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

713 # Change type can be R100 

714 # R: status letter 

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

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

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

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

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

720 a_path = path.encode(defenc) 

721 b_path = path.encode(defenc) 

722 deleted_file = False 

723 new_file = False 

724 copied_file = False 

725 rename_from = None 

726 rename_to = None 

727 

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

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

730 if change_type == "D": 

731 b_blob_id = None # Optional[str] 

732 deleted_file = True 

733 elif change_type == "A": 

734 a_blob_id = None 

735 new_file = True 

736 elif change_type == "C": 

737 copied_file = True 

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

739 a_path = a_path_str.encode(defenc) 

740 b_path = b_path_str.encode(defenc) 

741 elif change_type == "R": 

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

743 a_path = a_path_str.encode(defenc) 

744 b_path = b_path_str.encode(defenc) 

745 rename_from, rename_to = a_path, b_path 

746 elif change_type == "T": 

747 # Nothing to do. 

748 pass 

749 # END add/remove handling 

750 

751 diff = Diff( 

752 repo, 

753 a_path, 

754 b_path, 

755 a_blob_id, 

756 b_blob_id, 

757 old_mode, 

758 new_mode, 

759 new_file, 

760 deleted_file, 

761 copied_file, 

762 rename_from, 

763 rename_to, 

764 "", 

765 change_type, 

766 score, 

767 ) 

768 index.append(diff) 

769 

770 @classmethod 

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

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

773 in raw format. 

774 

775 :param repo: 

776 The repository we are operating on. 

777 

778 :param proc: 

779 Process to read output from. 

780 

781 :return: 

782 :class:`DiffIndex` 

783 """ 

784 # handles 

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

786 

787 index: "DiffIndex" = DiffIndex() 

788 stderr_list: List[bytes] = [] 

789 

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

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

792 

793 handle_process_output( 

794 proc, 

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

796 stderr_list.append, 

797 finalize_process_with_stderr, 

798 decode_streams=False, 

799 ) 

800 

801 return index