Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/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

281 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 Tuple, 

27 TYPE_CHECKING, 

28 TypeVar, 

29 Union, 

30 cast, 

31) 

32from git.types import Literal, PathLike 

33 

34if TYPE_CHECKING: 

35 from subprocess import Popen 

36 

37 from git.cmd import Git 

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 

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

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

88 

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

90 

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

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

93""" 

94 

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

96 

97 

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

99 value = matchobj.group(1) 

100 value = int(value, 8) 

101 value = bytes(bytearray((value,))) 

102 return value 

103 

104 

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

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

107 return None 

108 

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

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

111 

112 path = _octal_byte_re.sub(_octal_repl, path) 

113 

114 if has_ab_prefix: 

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

116 path = path[2:] 

117 

118 return path 

119 

120 

121class Diffable: 

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

123 compatible type. 

124 

125 :note: 

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

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

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

129 """ 

130 

131 __slots__ = () 

132 

133 repo: "Repo" 

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

135 

136 NULL_TREE = NULL_TREE 

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

138 

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

140 parameter. 

141 

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

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

144 """ 

145 

146 INDEX = INDEX 

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

148 

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

150 parameter. 

151 

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

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

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

155 """ 

156 

157 Index = INDEX 

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

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

160 

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

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

163 

164 :note: 

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

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

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

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

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

170 """ 

171 

172 def _process_diff_args( 

173 self, 

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

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

176 """ 

177 :return: 

178 Possibly altered version of the given args list. 

179 This method is called right before git command execution. 

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

181 """ 

182 return args 

183 

184 def diff( 

185 self, 

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

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

188 create_patch: bool = False, 

189 **kwargs: Any, 

190 ) -> "DiffIndex[Diff]": 

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

192 the working tree. Detects renames automatically. 

193 

194 :param other: 

195 This the item to compare us with. 

196 

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

198 

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

200 the respective tree. 

201 

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

203 

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

205 

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

207 method will not by default fail on bare repositories. 

208 

209 :param paths: 

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

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

212 

213 :param create_patch: 

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

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

216 to be read and diffed. 

217 

218 :param kwargs: 

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

220 swap both sides of the diff. 

221 

222 :return: 

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

224 

225 :note: 

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

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

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

229 """ 

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

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

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

233 

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

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

236 args.append("-M") 

237 

238 if create_patch: 

239 args.append("-p") 

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

241 else: 

242 args.append("--raw") 

243 args.append("-z") 

244 

245 # Ensure we never see colored output. 

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

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

248 

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

250 paths = [paths] 

251 

252 diff_cmd = self.repo.git.diff 

253 if other is INDEX: 

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

255 elif other is NULL_TREE: 

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

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

258 diff_cmd = self.repo.git.diff_tree 

259 elif other is not None: 

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

261 args.insert(0, other) 

262 diff_cmd = self.repo.git.diff_tree 

263 

264 args.insert(0, self) 

265 

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

267 if paths: 

268 args.append("--") 

269 args.extend(paths) 

270 # END paths handling 

271 

272 kwargs["as_process"] = True 

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

274 

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

276 index = diff_method(self.repo, proc) 

277 

278 proc.wait() 

279 return index 

280 

281 

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

283 

284 

285class DiffIndex(List[T_Diff]): 

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

287 properties. 

288 

289 The class improves the diff handling convenience. 

290 """ 

291 

292 change_type = ("A", "C", "D", "R", "M", "T") 

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

294 

295 * ``A`` = Added 

296 * ``D`` = Deleted 

297 * ``R`` = Renamed 

298 * ``M`` = Modified 

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

300 """ 

301 

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

303 """ 

304 :return: 

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

306 

307 :param change_type: 

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

309 

310 * 'A' for added paths 

311 * 'D' for deleted paths 

312 * 'R' for renamed paths 

313 * 'M' for paths with modified data 

314 * 'T' for changed in the type paths 

315 """ 

316 if change_type not in self.change_type: 

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

318 

319 for diffidx in self: 

320 if diffidx.change_type == change_type: 

321 yield diffidx 

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

323 yield diffidx 

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

325 yield diffidx 

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

327 yield diffidx 

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

329 yield diffidx 

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

331 yield diffidx 

332 # END for each diff 

333 

334 

335class Diff: 

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

337 

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

339 respectively to indicate that. 

340 

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

342 deletions and new files. 

343 

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

345 

346 New File:: 

347 

348 a_mode is None 

349 a_blob is None 

350 a_path is None 

351 

352 Deleted File:: 

353 

354 b_mode is None 

355 b_blob is None 

356 b_path is None 

357 

358 Working Tree Blobs: 

359 

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

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

362 path will be available, though. 

363 

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

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

366 """ 

367 

368 # Precompiled regex. 

369 re_header = re.compile( 

370 rb""" 

371 ^diff[ ]--git 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

387 """, 

388 re.VERBOSE | re.MULTILINE, 

389 ) 

390 

391 # These can be used for comparisons. 

392 NULL_HEX_SHA = "0" * 40 

393 NULL_BIN_SHA = b"\0" * 20 

394 

395 __slots__ = ( 

396 "a_blob", 

397 "b_blob", 

398 "a_mode", 

399 "b_mode", 

400 "a_rawpath", 

401 "b_rawpath", 

402 "new_file", 

403 "deleted_file", 

404 "copied_file", 

405 "raw_rename_from", 

406 "raw_rename_to", 

407 "diff", 

408 "change_type", 

409 "score", 

410 ) 

411 

412 def __init__( 

413 self, 

414 repo: "Repo", 

415 a_rawpath: Optional[bytes], 

416 b_rawpath: Optional[bytes], 

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

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

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

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

421 new_file: bool, 

422 deleted_file: bool, 

423 copied_file: bool, 

424 raw_rename_from: Optional[bytes], 

425 raw_rename_to: Optional[bytes], 

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

427 change_type: Optional[Lit_change_type], 

428 score: Optional[int], 

429 ) -> None: 

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

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

432 self.a_rawpath = a_rawpath 

433 self.b_rawpath = b_rawpath 

434 

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

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

437 

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

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

440 if repo and a_rawpath: 

441 for submodule in repo.submodules: 

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

443 if submodule.module_exists(): 

444 repo = submodule.module() 

445 break 

446 

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

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

449 self.a_blob = None 

450 else: 

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

452 

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

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

455 self.b_blob = None 

456 else: 

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

458 

459 self.new_file: bool = new_file 

460 self.deleted_file: bool = deleted_file 

461 self.copied_file: bool = copied_file 

462 

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

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

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

466 self.raw_rename_from = raw_rename_from or None 

467 self.raw_rename_to = raw_rename_to or None 

468 

469 self.diff = diff 

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

471 self.score = score 

472 

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

474 for name in self.__slots__: 

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

476 return False 

477 # END for each name 

478 return True 

479 

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

481 return not (self == other) 

482 

483 def __hash__(self) -> int: 

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

485 

486 def __str__(self) -> str: 

487 h = "%s" 

488 if self.a_blob: 

489 h %= self.a_blob.path 

490 elif self.b_blob: 

491 h %= self.b_blob.path 

492 

493 msg = "" 

494 line = None 

495 line_length = 0 

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

497 if b: 

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

499 else: 

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

501 # END if blob is not None 

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

503 msg += line 

504 # END for each blob 

505 

506 # Add headline. 

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

508 

509 if self.deleted_file: 

510 msg += "\nfile deleted in rhs" 

511 if self.new_file: 

512 msg += "\nfile added in rhs" 

513 if self.copied_file: 

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

515 if self.rename_from: 

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

517 if self.rename_to: 

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

519 if self.diff: 

520 msg += "\n---" 

521 try: 

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

523 except UnicodeDecodeError: 

524 msg += "OMITTED BINARY DATA" 

525 # END handle encoding 

526 msg += "\n---" 

527 # END diff info 

528 

529 return h + msg 

530 

531 @property 

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

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

534 

535 @property 

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

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

538 

539 @property 

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

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

542 

543 @property 

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

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

546 

547 @property 

548 def renamed(self) -> bool: 

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

550 

551 :return: 

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

553 

554 :note: 

555 This property is deprecated. 

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

557 """ 

558 warnings.warn( 

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

560 DeprecationWarning, 

561 stacklevel=2, 

562 ) 

563 return self.renamed_file 

564 

565 @property 

566 def renamed_file(self) -> bool: 

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

568 return self.rename_from != self.rename_to 

569 

570 @classmethod 

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

572 if path_match: 

573 return decode_path(path_match) 

574 

575 if rename_match: 

576 return decode_path(rename_match, has_ab_prefix=False) 

577 

578 if path_fallback_match: 

579 return decode_path(path_fallback_match) 

580 

581 return None 

582 

583 @classmethod 

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

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

586 in patch format. 

587 

588 :param repo: 

589 The repository we are operating on. 

590 

591 :param proc: 

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

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

594 

595 :return: 

596 :class:`DiffIndex` 

597 """ 

598 

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

600 text_list: List[bytes] = [] 

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

602 

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

604 text = b"".join(text_list) 

605 index: "DiffIndex" = DiffIndex() 

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

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

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

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

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

611 ( 

612 a_path_fallback, 

613 b_path_fallback, 

614 old_mode, 

615 new_mode, 

616 rename_from, 

617 rename_to, 

618 new_file_mode, 

619 deleted_file_mode, 

620 copied_file_name, 

621 a_blob_id, 

622 b_blob_id, 

623 b_mode, 

624 a_path, 

625 b_path, 

626 ) = _header.groups() 

627 

628 new_file, deleted_file, copied_file = ( 

629 bool(new_file_mode), 

630 bool(deleted_file_mode), 

631 bool(copied_file_name), 

632 ) 

633 

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

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

636 

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

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

639 if previous_header is not None: 

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

641 # END assign actual diff 

642 

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

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

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

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

647 index.append( 

648 Diff( 

649 repo, 

650 a_path, 

651 b_path, 

652 a_blob_id and a_blob_id.decode(defenc), 

653 b_blob_id and b_blob_id.decode(defenc), 

654 a_mode and a_mode.decode(defenc), 

655 b_mode and b_mode.decode(defenc), 

656 new_file, 

657 deleted_file, 

658 copied_file, 

659 rename_from, 

660 rename_to, 

661 None, 

662 None, 

663 None, 

664 ) 

665 ) 

666 

667 previous_header = _header 

668 header = _header 

669 # END for each header we parse 

670 if index and header: 

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

672 # END assign last diff 

673 

674 return index 

675 

676 @staticmethod 

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

678 lines = lines_bytes.decode(defenc) 

679 

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

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

682 

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

684 if not line: 

685 # The line data is empty, skip. 

686 continue 

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

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

689 a_blob_id: Optional[str] 

690 b_blob_id: Optional[str] 

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

692 # Change type can be R100 

693 # R: status letter 

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

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

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

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

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

699 a_path = path.encode(defenc) 

700 b_path = path.encode(defenc) 

701 deleted_file = False 

702 new_file = False 

703 copied_file = False 

704 rename_from = None 

705 rename_to = None 

706 

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

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

709 if change_type == "D": 

710 b_blob_id = None # Optional[str] 

711 deleted_file = True 

712 elif change_type == "A": 

713 a_blob_id = None 

714 new_file = True 

715 elif change_type == "C": 

716 copied_file = True 

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

718 a_path = a_path_str.encode(defenc) 

719 b_path = b_path_str.encode(defenc) 

720 elif change_type == "R": 

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

722 a_path = a_path_str.encode(defenc) 

723 b_path = b_path_str.encode(defenc) 

724 rename_from, rename_to = a_path, b_path 

725 elif change_type == "T": 

726 # Nothing to do. 

727 pass 

728 # END add/remove handling 

729 

730 diff = Diff( 

731 repo, 

732 a_path, 

733 b_path, 

734 a_blob_id, 

735 b_blob_id, 

736 old_mode, 

737 new_mode, 

738 new_file, 

739 deleted_file, 

740 copied_file, 

741 rename_from, 

742 rename_to, 

743 "", 

744 change_type, 

745 score, 

746 ) 

747 index.append(diff) 

748 

749 @classmethod 

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

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

752 in raw format. 

753 

754 :param repo: 

755 The repository we are operating on. 

756 

757 :param proc: 

758 Process to read output from. 

759 

760 :return: 

761 :class:`DiffIndex` 

762 """ 

763 # handles 

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

765 

766 index: "DiffIndex" = DiffIndex() 

767 handle_process_output( 

768 proc, 

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

770 None, 

771 finalize_process, 

772 decode_streams=False, 

773 ) 

774 

775 return index