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

291 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", "INDEX", "Diffable", "DiffIndex", "Diff"] 

7 

8import enum 

9import re 

10import warnings 

11 

12from git.cmd import 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.cmd import Git 

39 from git.objects.base import IndexObject 

40 from git.objects.commit import Commit 

41 from git.objects.tree import Tree 

42 from git.repo.base import Repo 

43 

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

45 

46# ------------------------------------------------------------------------ 

47 

48 

49@enum.unique 

50class DiffConstants(enum.Enum): 

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

52 

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

54 values including these. 

55 

56 :note: 

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

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

59 :mod:`git` module. 

60 """ 

61 

62 NULL_TREE = enum.auto() 

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

64 

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

66 :const:`Diffable.NULL_TREE`. 

67 """ 

68 

69 INDEX = enum.auto() 

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

71 

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

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

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

75 """ 

76 

77 

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

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

80 

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

82 

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

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

85""" 

86 

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

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

89 

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

91 

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

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

94""" 

95 

96_octal_byte_re = re.compile(rb"\\([0-9]{3})") 

97 

98 

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

100 value = matchobj.group(1) 

101 value = int(value, 8) 

102 value = bytes(bytearray((value,))) 

103 return value 

104 

105 

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

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

108 return None 

109 

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

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

112 

113 path = _octal_byte_re.sub(_octal_repl, path) 

114 

115 if has_ab_prefix: 

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

117 path = path[2:] 

118 

119 return path 

120 

121 

122class Diffable: 

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

124 compatible type. 

125 

126 :note: 

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

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

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

130 """ 

131 

132 __slots__ = () 

133 

134 repo: "Repo" 

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

136 

137 NULL_TREE = NULL_TREE 

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

139 

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

141 parameter. 

142 

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

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

145 """ 

146 

147 INDEX = INDEX 

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

149 

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

151 parameter. 

152 

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

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

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

156 """ 

157 

158 Index = INDEX 

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

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

161 

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

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

164 

165 :note: 

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

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

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

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

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

171 """ 

172 

173 def _process_diff_args( 

174 self, 

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

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

177 """ 

178 :return: 

179 Possibly altered version of the given args list. 

180 This method is called right before git command execution. 

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

182 """ 

183 return args 

184 

185 def diff( 

186 self, 

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

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

189 create_patch: bool = False, 

190 **kwargs: Any, 

191 ) -> "DiffIndex[Diff]": 

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

193 the working tree. Detects renames automatically. 

194 

195 :param other: 

196 This the item to compare us with. 

197 

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

199 

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

201 the respective tree. 

202 

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

204 

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

206 

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

208 method will not by default fail on bare repositories. 

209 

210 :param paths: 

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

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

213 

214 :param create_patch: 

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

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

217 to be read and diffed. 

218 

219 :param kwargs: 

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

221 swap both sides of the diff. 

222 

223 :return: 

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

225 

226 :note: 

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

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

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

230 """ 

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

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

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

234 

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

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

237 args.append("-M") 

238 

239 if create_patch: 

240 args.append("-p") 

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

242 else: 

243 args.append("--raw") 

244 args.append("-z") 

245 

246 # Ensure we never see colored output. 

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

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

249 

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

251 paths = [paths] 

252 

253 diff_cmd = self.repo.git.diff 

254 if other is INDEX: 

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

256 elif other is NULL_TREE: 

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

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

259 diff_cmd = self.repo.git.diff_tree 

260 elif other is not None: 

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

262 args.insert(0, other) 

263 diff_cmd = self.repo.git.diff_tree 

264 

265 args.insert(0, self) 

266 

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

268 if paths: 

269 args.append("--") 

270 args.extend(paths) 

271 # END paths handling 

272 

273 kwargs["as_process"] = True 

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

275 

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

277 index = diff_method(self.repo, proc) 

278 

279 proc.wait() 

280 return index 

281 

282 

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

284 

285 

286class DiffIndex(List[T_Diff]): 

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

288 properties. 

289 

290 The class improves the diff handling convenience. 

291 """ 

292 

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

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

295 

296 * ``A`` = Added 

297 * ``D`` = Deleted 

298 * ``R`` = Renamed 

299 * ``M`` = Modified 

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

301 """ 

302 

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

304 """ 

305 :return: 

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

307 

308 :param change_type: 

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

310 

311 * 'A' for added paths 

312 * 'D' for deleted paths 

313 * 'R' for renamed paths 

314 * 'M' for paths with modified data 

315 * 'T' for changed in the type paths 

316 """ 

317 if change_type not in self.change_type: 

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

319 

320 for diffidx in self: 

321 if diffidx.change_type == change_type: 

322 yield diffidx 

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

324 yield diffidx 

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

326 yield diffidx 

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

328 yield diffidx 

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

330 yield diffidx 

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

332 yield diffidx 

333 # END for each diff 

334 

335 

336class Diff: 

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

338 

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

340 respectively to indicate that. 

341 

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

343 deletions and new files. 

344 

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

346 

347 New File:: 

348 

349 a_mode is None 

350 a_blob is None 

351 a_path is None 

352 

353 Deleted File:: 

354 

355 b_mode is None 

356 b_blob is None 

357 b_path is None 

358 

359 Working Tree Blobs: 

360 

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

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

363 path will be available, though. 

364 

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

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

367 """ 

368 

369 # Precompiled regex. 

370 re_header = re.compile( 

371 rb""" 

372 ^diff[ ]--git 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

388 """, 

389 re.VERBOSE | re.MULTILINE, 

390 ) 

391 

392 # These can be used for comparisons. 

393 NULL_HEX_SHA = "0" * 40 

394 NULL_BIN_SHA = b"\0" * 20 

395 

396 __slots__ = ( 

397 "a_blob", 

398 "b_blob", 

399 "a_mode", 

400 "b_mode", 

401 "a_rawpath", 

402 "b_rawpath", 

403 "new_file", 

404 "deleted_file", 

405 "copied_file", 

406 "raw_rename_from", 

407 "raw_rename_to", 

408 "diff", 

409 "change_type", 

410 "score", 

411 ) 

412 

413 def __init__( 

414 self, 

415 repo: "Repo", 

416 a_rawpath: Optional[bytes], 

417 b_rawpath: Optional[bytes], 

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

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

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

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

422 new_file: bool, 

423 deleted_file: bool, 

424 copied_file: bool, 

425 raw_rename_from: Optional[bytes], 

426 raw_rename_to: Optional[bytes], 

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

428 change_type: Optional[Lit_change_type], 

429 score: Optional[int], 

430 ) -> None: 

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

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

433 self.a_rawpath = a_rawpath 

434 self.b_rawpath = b_rawpath 

435 

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

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

438 

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

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

441 if repo and a_rawpath: 

442 for submodule in repo.submodules: 

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

444 if submodule.module_exists(): 

445 repo = submodule.module() 

446 break 

447 

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

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

450 self.a_blob = None 

451 else: 

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

453 

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

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

456 self.b_blob = None 

457 else: 

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

459 

460 self.new_file: bool = new_file 

461 self.deleted_file: bool = deleted_file 

462 self.copied_file: bool = copied_file 

463 

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

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

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

467 self.raw_rename_from = raw_rename_from or None 

468 self.raw_rename_to = raw_rename_to or None 

469 

470 self.diff = diff 

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

472 self.score = score 

473 

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

475 for name in self.__slots__: 

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

477 return False 

478 # END for each name 

479 return True 

480 

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

482 return not (self == other) 

483 

484 def __hash__(self) -> int: 

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

486 

487 def __str__(self) -> str: 

488 h = "%s" 

489 if self.a_blob: 

490 h %= self.a_blob.path 

491 elif self.b_blob: 

492 h %= self.b_blob.path 

493 

494 msg = "" 

495 line = None 

496 line_length = 0 

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

498 if b: 

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

500 else: 

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

502 # END if blob is not None 

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

504 msg += line 

505 # END for each blob 

506 

507 # Add headline. 

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

509 

510 if self.deleted_file: 

511 msg += "\nfile deleted in rhs" 

512 if self.new_file: 

513 msg += "\nfile added in rhs" 

514 if self.copied_file: 

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

516 if self.rename_from: 

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

518 if self.rename_to: 

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

520 if self.diff: 

521 msg += "\n---" 

522 try: 

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

524 except UnicodeDecodeError: 

525 msg += "OMITTED BINARY DATA" 

526 # END handle encoding 

527 msg += "\n---" 

528 # END diff info 

529 

530 return h + msg 

531 

532 @property 

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

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

535 

536 @property 

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

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

539 

540 @property 

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

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

543 

544 @property 

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

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

547 

548 @property 

549 def renamed(self) -> bool: 

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

551 

552 :return: 

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

554 

555 :note: 

556 This property is deprecated. 

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

558 """ 

559 warnings.warn( 

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

561 DeprecationWarning, 

562 stacklevel=2, 

563 ) 

564 return self.renamed_file 

565 

566 @property 

567 def renamed_file(self) -> bool: 

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

569 return self.rename_from != self.rename_to 

570 

571 @classmethod 

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

573 if path_match: 

574 return decode_path(path_match) 

575 

576 if rename_match: 

577 return decode_path(rename_match, has_ab_prefix=False) 

578 

579 if path_fallback_match: 

580 return decode_path(path_fallback_match) 

581 

582 return None 

583 

584 @classmethod 

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

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

587 in patch format. 

588 

589 :param repo: 

590 The repository we are operating on. 

591 

592 :param proc: 

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

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

595 

596 :return: 

597 :class:`DiffIndex` 

598 """ 

599 

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

601 text_list: List[bytes] = [] 

602 handle_process_output(proc, text_list.append, None, finalize_process, decode_streams=False) 

603 

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

605 text = b"".join(text_list) 

606 index: "DiffIndex" = DiffIndex() 

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

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

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

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

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

612 ( 

613 a_path_fallback, 

614 b_path_fallback, 

615 old_mode, 

616 new_mode, 

617 rename_from, 

618 rename_to, 

619 new_file_mode, 

620 deleted_file_mode, 

621 copied_file_name, 

622 a_blob_id, 

623 b_blob_id, 

624 b_mode, 

625 a_path, 

626 b_path, 

627 ) = _header.groups() 

628 

629 new_file, deleted_file, copied_file = ( 

630 bool(new_file_mode), 

631 bool(deleted_file_mode), 

632 bool(copied_file_name), 

633 ) 

634 

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

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

637 

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

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

640 if previous_header is not None: 

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

642 # END assign actual diff 

643 

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

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

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

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

648 index.append( 

649 Diff( 

650 repo, 

651 a_path, 

652 b_path, 

653 a_blob_id and a_blob_id.decode(defenc), 

654 b_blob_id and b_blob_id.decode(defenc), 

655 a_mode and a_mode.decode(defenc), 

656 b_mode and b_mode.decode(defenc), 

657 new_file, 

658 deleted_file, 

659 copied_file, 

660 rename_from, 

661 rename_to, 

662 None, 

663 None, 

664 None, 

665 ) 

666 ) 

667 

668 previous_header = _header 

669 header = _header 

670 # END for each header we parse 

671 if index and header: 

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

673 # END assign last diff 

674 

675 return index 

676 

677 @staticmethod 

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

679 lines = lines_bytes.decode(defenc) 

680 

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

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

683 

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

685 if not line: 

686 # The line data is empty, skip. 

687 continue 

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

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

690 a_blob_id: Optional[str] 

691 b_blob_id: Optional[str] 

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

693 # Change type can be R100 

694 # R: status letter 

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

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

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

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

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

700 a_path = path.encode(defenc) 

701 b_path = path.encode(defenc) 

702 deleted_file = False 

703 new_file = False 

704 copied_file = False 

705 rename_from = None 

706 rename_to = None 

707 

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

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

710 if change_type == "D": 

711 b_blob_id = None # Optional[str] 

712 deleted_file = True 

713 elif change_type == "A": 

714 a_blob_id = None 

715 new_file = True 

716 elif change_type == "C": 

717 copied_file = True 

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

719 a_path = a_path_str.encode(defenc) 

720 b_path = b_path_str.encode(defenc) 

721 elif change_type == "R": 

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

723 a_path = a_path_str.encode(defenc) 

724 b_path = b_path_str.encode(defenc) 

725 rename_from, rename_to = a_path, b_path 

726 elif change_type == "T": 

727 # Nothing to do. 

728 pass 

729 # END add/remove handling 

730 

731 diff = Diff( 

732 repo, 

733 a_path, 

734 b_path, 

735 a_blob_id, 

736 b_blob_id, 

737 old_mode, 

738 new_mode, 

739 new_file, 

740 deleted_file, 

741 copied_file, 

742 rename_from, 

743 rename_to, 

744 "", 

745 change_type, 

746 score, 

747 ) 

748 index.append(diff) 

749 

750 @classmethod 

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

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

753 in raw format. 

754 

755 :param repo: 

756 The repository we are operating on. 

757 

758 :param proc: 

759 Process to read output from. 

760 

761 :return: 

762 :class:`DiffIndex` 

763 """ 

764 # handles 

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

766 

767 index: "DiffIndex" = DiffIndex() 

768 handle_process_output( 

769 proc, 

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

771 None, 

772 finalize_process, 

773 decode_streams=False, 

774 ) 

775 

776 return index