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

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

338 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 

12# typing ------------------------------------------------------------------ 

13from typing import ( 

14 TYPE_CHECKING, 

15 Any, 

16 Iterator, 

17 List, 

18 Match, 

19 Optional, 

20 Sequence, 

21 Tuple, 

22 TypeVar, 

23 Union, 

24 cast, 

25) 

26 

27from git.cmd import Git, handle_process_output 

28from git.compat import defenc 

29from git.objects.base import IndexObject 

30from git.objects.blob import Blob 

31from git.objects.util import mode_str_to_int 

32from git.types import Literal, PathLike 

33from git.util import finalize_process, hex_to_bin 

34 

35if TYPE_CHECKING: 

36 from subprocess import Popen 

37 

38 from git.objects.commit import Commit 

39 from git.objects.tree import Tree 

40 from git.repo.base import Repo 

41 

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

43 

44# ------------------------------------------------------------------------ 

45 

46 

47@enum.unique 

48class DiffConstants(enum.Enum): 

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

50 

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

52 values including these. 

53 

54 :note: 

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

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

57 :mod:`git` module. 

58 """ 

59 

60 NULL_TREE = enum.auto() 

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

62 

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

64 :const:`Diffable.NULL_TREE`. 

65 """ 

66 

67 INDEX = enum.auto() 

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

69 

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

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

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

73 """ 

74 

75 

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

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

78 

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

80 

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

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

83""" 

84 

85NULL_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" 

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

87 

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

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

90 

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

92 

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

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

95""" 

96 

97 

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

99 result = bytearray() 

100 escapes = { 

101 ord("a"): 7, 

102 ord("b"): 8, 

103 ord("f"): 12, 

104 ord("n"): 10, 

105 ord("r"): 13, 

106 ord("t"): 9, 

107 ord("v"): 11, 

108 } 

109 i = 0 

110 while i < len(path): 

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

112 result.append(path[i]) 

113 i += 1 

114 continue 

115 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]): 

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

117 i += 4 

118 continue 

119 escaped = path[i + 1] 

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

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

122 else: 

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

124 i += 2 

125 return bytes(result) 

126 

127 

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

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

130 return None 

131 

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

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

134 

135 if has_ab_prefix: 

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

137 path = path[2:] 

138 

139 return path 

140 

141 

142class Diffable: 

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

144 compatible type. 

145 

146 :note: 

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

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

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

150 """ 

151 

152 __slots__ = () 

153 

154 repo: "Repo" 

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

156 

157 NULL_TREE = NULL_TREE 

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

159 

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

161 parameter. 

162 

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

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

165 """ 

166 

167 INDEX = INDEX 

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

169 

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

171 parameter. 

172 

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

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

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

176 """ 

177 

178 Index = INDEX 

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

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

181 

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

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

184 

185 :note: 

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

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

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

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

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

191 """ 

192 

193 def _process_diff_args( 

194 self, 

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

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

197 """ 

198 :return: 

199 Possibly altered version of the given args list. 

200 This method is called right before git command execution. 

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

202 """ 

203 return args 

204 

205 def diff( 

206 self, 

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

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

209 create_patch: bool = False, 

210 allow_unsafe_options: bool = False, 

211 **kwargs: Any, 

212 ) -> "DiffIndex[Diff]": 

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

214 the working tree. Detects renames automatically. 

215 

216 :param other: 

217 This the item to compare us with. 

218 

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

220 

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

222 the respective tree. 

223 

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

225 

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

227 

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

229 method will not by default fail on bare repositories. 

230 

231 :param paths: 

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

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

234 

235 :param create_patch: 

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

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

238 to be read and diffed. 

239 

240 :param allow_unsafe_options: 

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

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

243 

244 :param kwargs: 

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

246 swap both sides of the diff. 

247 

248 :return: 

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

250 

251 :note: 

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

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

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

255 """ 

256 if not allow_unsafe_options: 

257 Git.check_unsafe_options( 

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

259 unsafe_options=self.repo.unsafe_git_diff_options, 

260 clusterable_short_options="46abceflmnpqrstuvwzBCDMNRW", 

261 ) 

262 

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

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

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

266 

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

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

269 args.append("-M") 

270 

271 if create_patch: 

272 args.append("-p") 

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

274 else: 

275 args.append("--raw") 

276 args.append("-z") 

277 

278 # Ensure we never see colored output. 

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

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

281 

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

283 paths = [paths] 

284 

285 diff_cmd = self.repo.git.diff 

286 if other is INDEX: 

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

288 elif other is NULL_TREE: 

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

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

291 diff_cmd = self.repo.git.diff_tree 

292 elif other is not None: 

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

294 args.insert(0, other) 

295 diff_cmd = self.repo.git.diff_tree 

296 

297 args.insert(0, self) 

298 

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

300 if paths: 

301 args.append("--") 

302 args.extend(paths) 

303 # END paths handling 

304 

305 kwargs["as_process"] = True 

306 args = self._process_diff_args(args) 

307 if create_patch: 

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

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

310 

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

312 index = diff_method(self.repo, proc) 

313 

314 proc.wait() 

315 return index 

316 

317 

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

319 

320 

321class DiffIndex(List[T_Diff]): 

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

323 properties. 

324 

325 The class improves the diff handling convenience. 

326 """ 

327 

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

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

330 

331 * ``A`` = Added 

332 * ``D`` = Deleted 

333 * ``R`` = Renamed 

334 * ``M`` = Modified 

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

336 """ 

337 

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

339 """ 

340 :return: 

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

342 

343 :param change_type: 

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

345 

346 * 'A' for added paths 

347 * 'D' for deleted paths 

348 * 'R' for renamed paths 

349 * 'M' for paths with modified data 

350 * 'T' for changed in the type paths 

351 """ 

352 if change_type not in self.change_type: 

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

354 

355 for diffidx in self: 

356 if diffidx.change_type == change_type: 

357 yield diffidx 

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

359 yield diffidx 

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

361 yield diffidx 

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

363 yield diffidx 

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

365 yield diffidx 

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

367 yield diffidx 

368 # END for each diff 

369 

370 

371class Diff: 

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

373 

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

375 respectively to indicate that. 

376 

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

378 deletions and new files. 

379 

380 For submodule changes, ``a_blob`` and ``b_blob`` are 

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

382 the submodule repository. 

383 

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

385 

386 New File:: 

387 

388 a_mode is None 

389 a_blob is None 

390 a_path is None 

391 

392 Deleted File:: 

393 

394 b_mode is None 

395 b_blob is None 

396 b_path is None 

397 

398 Working Tree Blobs: 

399 

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

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

402 path will be available, though. 

403 

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

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

406 """ 

407 

408 # Precompiled regex. 

409 re_header = re.compile( 

410 rb""" 

411 ^diff[ ]--git 

412 [ ](?P<header_names>(?P<a_path_fallback>"?[ab]/.+?"?) 

413 [ ](?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[ ](?P<copied_from_name>.*)\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 @staticmethod 

630 def _same_path_in_header(names: bytes) -> Optional[bytes]: 

631 """Return the path that a ``diff --git a/<path> b/<path>`` line names twice. 

632 

633 git separates the two names with a space but does not quote a path for 

634 containing one, so once the path contains ``" a/"`` or ``" b/"`` the line alone 

635 does not show where the first name ends. When no ``---``, ``+++``, rename or 

636 copy line names the file, both names are the same path, and the split is 

637 accepted only where the two halves agree, as ``git_header_name`` in git's 

638 ``apply.c`` does. A quoted first name ends at its closing quote. 

639 

640 :return: 

641 The path without its ``a/`` prefix, or ``None`` if no split agrees. 

642 """ 

643 if names.startswith(b'"'): 

644 end = 1 

645 while end < len(names) and names[end : end + 1] != b'"': 

646 end += 2 if names[end : end + 1] == b"\\" else 1 

647 split = end + 1 

648 else: 

649 # Equal unquoted paths have equal-length names, so only the midpoint 

650 # can separate them. Trying every space would copy the header quadratically. 

651 if len(names) % 2 == 0: 

652 return None 

653 split = len(names) // 2 

654 if names[split : split + 1] != b" ": 

655 return None 

656 a_name, b_name = names[:split], names[split + 1 :] 

657 if a_name.startswith(b'"'): 

658 if not (a_name.endswith(b'"') and b_name.startswith(b'"') and b_name.endswith(b'"')): 

659 return None 

660 a_name, b_name = _unquote_path(a_name[1:-1]), _unquote_path(b_name[1:-1]) 

661 if a_name.startswith(b"a/") and b_name.startswith(b"b/") and a_name[2:] == b_name[2:]: 

662 return a_name[2:] 

663 return None 

664 

665 @classmethod 

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

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

668 in patch format. 

669 

670 :param repo: 

671 The repository we are operating on. 

672 

673 :param proc: 

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

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

676 

677 :return: 

678 :class:`DiffIndex` 

679 """ 

680 

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

682 text_list: List[bytes] = [] 

683 stderr_list: List[bytes] = [] 

684 

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

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

687 

688 handle_process_output( 

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

690 ) 

691 

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

693 text = b"".join(text_list) 

694 index: "DiffIndex" = DiffIndex() 

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

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

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

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

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

700 ( 

701 header_names, 

702 a_path_fallback, 

703 b_path_fallback, 

704 old_mode, 

705 new_mode, 

706 rename_from, 

707 rename_to, 

708 new_file_mode, 

709 deleted_file_mode, 

710 copied_from_name, 

711 copied_file_name, 

712 a_blob_id, 

713 b_blob_id, 

714 b_mode, 

715 a_path, 

716 b_path, 

717 ) = _header.groups() 

718 

719 new_file, deleted_file, copied_file = ( 

720 bool(new_file_mode), 

721 bool(deleted_file_mode), 

722 bool(copied_file_name), 

723 ) 

724 

725 # Mode changes, binary files and empty files have no `---` and `+++` lines, 

726 # so only the `diff --git` line names them, and it names the same path twice. 

727 named_elsewhere = any((a_path, b_path, rename_from, rename_to, copied_from_name, copied_file_name)) 

728 same_path = None if named_elsewhere else cls._same_path_in_header(header_names) 

729 if same_path is None: 

730 a_path = cls._pick_best_path(a_path, rename_from or copied_from_name, a_path_fallback) 

731 b_path = cls._pick_best_path(b_path, rename_to or copied_file_name, b_path_fallback) 

732 else: 

733 a_path = b_path = same_path 

734 

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

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

737 if previous_header is not None: 

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

739 # END assign actual diff 

740 

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

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

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

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

745 index.append( 

746 Diff( 

747 repo, 

748 a_path, 

749 b_path, 

750 a_blob_id and a_blob_id.decode(defenc), 

751 b_blob_id and b_blob_id.decode(defenc), 

752 a_mode and a_mode.decode(defenc), 

753 b_mode and b_mode.decode(defenc), 

754 new_file, 

755 deleted_file, 

756 copied_file, 

757 rename_from, 

758 rename_to, 

759 None, 

760 None, 

761 None, 

762 ) 

763 ) 

764 

765 previous_header = _header 

766 header = _header 

767 # END for each header we parse 

768 if index and header: 

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

770 # END assign last diff 

771 

772 return index 

773 

774 @staticmethod 

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

776 lines = lines_bytes.decode(defenc) 

777 

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

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

780 

781 # Each record is a metadata field starting with a colon, followed by one path, 

782 # or two for copies and renames, each terminated by NUL. A path may itself 

783 # start with a colon or contain a line feed, so read the fields in order 

784 # instead of splitting at colons. 

785 fields = iter(lines.rstrip("\n").split("\x00")) 

786 for meta in fields: 

787 if not meta: 

788 # The line data is empty, skip. 

789 continue 

790 if meta.startswith(":"): 

791 meta = meta[1:] 

792 a_blob_id: Optional[str] 

793 b_blob_id: Optional[str] 

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

795 # Change type can be R100 

796 # R: status letter 

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

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

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

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

801 path = next(fields, "") 

802 a_path = path.encode(defenc) 

803 b_path = path.encode(defenc) 

804 deleted_file = False 

805 new_file = False 

806 copied_file = False 

807 rename_from = None 

808 rename_to = None 

809 

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

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

812 if change_type == "D": 

813 b_blob_id = None # Optional[str] 

814 deleted_file = True 

815 elif change_type == "A": 

816 a_blob_id = None 

817 new_file = True 

818 elif change_type == "C": 

819 copied_file = True 

820 b_path = next(fields, "").encode(defenc) 

821 elif change_type == "R": 

822 b_path = next(fields, "").encode(defenc) 

823 rename_from, rename_to = a_path, b_path 

824 elif change_type == "T": 

825 # Nothing to do. 

826 pass 

827 # END add/remove handling 

828 

829 diff = Diff( 

830 repo, 

831 a_path, 

832 b_path, 

833 a_blob_id, 

834 b_blob_id, 

835 old_mode, 

836 new_mode, 

837 new_file, 

838 deleted_file, 

839 copied_file, 

840 rename_from, 

841 rename_to, 

842 "", 

843 change_type, 

844 score, 

845 ) 

846 index.append(diff) 

847 

848 @classmethod 

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

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

851 in raw format. 

852 

853 :param repo: 

854 The repository we are operating on. 

855 

856 :param proc: 

857 Process to read output from. 

858 

859 :return: 

860 :class:`DiffIndex` 

861 """ 

862 # handles 

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

864 

865 index: "DiffIndex" = DiffIndex() 

866 text_list: List[bytes] = [] 

867 stderr_list: List[bytes] = [] 

868 

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

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

871 

872 # Output is read line by line, but paths may contain line feeds, so parse 

873 # the records only once all of it has been read. 

874 handle_process_output( 

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

876 ) 

877 cls._handle_diff_line(b"".join(text_list), repo, index) 

878 

879 return index