Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/git/objects/commit.py: 55%

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

339 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__ = ["Commit"] 

7 

8from collections import defaultdict 

9import datetime 

10from io import BytesIO 

11import logging 

12import os 

13import re 

14from subprocess import Popen, PIPE 

15import sys 

16from time import altzone, daylight, localtime, time, timezone 

17import warnings 

18 

19from gitdb import IStream 

20 

21from git.cmd import Git 

22from git.diff import Diffable 

23from git.util import Actor, Stats, finalize_process, hex_to_bin 

24 

25from . import base 

26from .tree import Tree 

27from .util import ( 

28 Serializable, 

29 TraversableIterableObj, 

30 altz_to_utctz_str, 

31 from_timestamp, 

32 parse_actor_and_date, 

33 parse_date, 

34) 

35 

36# typing ------------------------------------------------------------------ 

37 

38from typing import ( 

39 Any, 

40 Dict, 

41 IO, 

42 Iterator, 

43 List, 

44 Sequence, 

45 Tuple, 

46 TYPE_CHECKING, 

47 Union, 

48 cast, 

49) 

50 

51if sys.version_info >= (3, 8): 

52 from typing import Literal 

53else: 

54 from typing_extensions import Literal 

55 

56from git.types import PathLike 

57 

58if TYPE_CHECKING: 

59 from git.refs import SymbolicReference 

60 from git.repo import Repo 

61 

62# ------------------------------------------------------------------------ 

63 

64_logger = logging.getLogger(__name__) 

65 

66 

67class Commit(base.Object, TraversableIterableObj, Diffable, Serializable): 

68 """Wraps a git commit object. 

69 

70 See :manpage:`gitglossary(7)` on "commit object": 

71 https://git-scm.com/docs/gitglossary#def_commit_object 

72 

73 :note: 

74 This class will act lazily on some of its attributes and will query the value on 

75 demand only if it involves calling the git binary. 

76 """ 

77 

78 # ENVIRONMENT VARIABLES 

79 # Read when creating new commits. 

80 env_author_date = "GIT_AUTHOR_DATE" 

81 env_committer_date = "GIT_COMMITTER_DATE" 

82 

83 # CONFIGURATION KEYS 

84 conf_encoding = "i18n.commitencoding" 

85 

86 # INVARIANTS 

87 default_encoding = "UTF-8" 

88 

89 # Options to :manpage:`git-rev-list(1)` that can overwrite files. 

90 unsafe_git_rev_options = [ 

91 "--output", 

92 "-o", 

93 ] 

94 

95 type: Literal["commit"] = "commit" 

96 

97 __slots__ = ( 

98 "tree", 

99 "author", 

100 "authored_date", 

101 "author_tz_offset", 

102 "committer", 

103 "committed_date", 

104 "committer_tz_offset", 

105 "message", 

106 "parents", 

107 "encoding", 

108 "gpgsig", 

109 ) 

110 

111 _id_attribute_ = "hexsha" 

112 

113 parents: Sequence["Commit"] 

114 

115 def __init__( 

116 self, 

117 repo: "Repo", 

118 binsha: bytes, 

119 tree: Union[Tree, None] = None, 

120 author: Union[Actor, None] = None, 

121 authored_date: Union[int, None] = None, 

122 author_tz_offset: Union[None, float] = None, 

123 committer: Union[Actor, None] = None, 

124 committed_date: Union[int, None] = None, 

125 committer_tz_offset: Union[None, float] = None, 

126 message: Union[str, bytes, None] = None, 

127 parents: Union[Sequence["Commit"], None] = None, 

128 encoding: Union[str, None] = None, 

129 gpgsig: Union[str, None] = None, 

130 ) -> None: 

131 """Instantiate a new :class:`Commit`. All keyword arguments taking ``None`` as 

132 default will be implicitly set on first query. 

133 

134 :param binsha: 

135 20 byte sha1. 

136 

137 :param tree: 

138 A :class:`~git.objects.tree.Tree` object. 

139 

140 :param author: 

141 The author :class:`~git.util.Actor` object. 

142 

143 :param authored_date: int_seconds_since_epoch 

144 The authored DateTime - use :func:`time.gmtime` to convert it into a 

145 different format. 

146 

147 :param author_tz_offset: int_seconds_west_of_utc 

148 The timezone that the `authored_date` is in. 

149 

150 :param committer: 

151 The committer string, as an :class:`~git.util.Actor` object. 

152 

153 :param committed_date: int_seconds_since_epoch 

154 The committed DateTime - use :func:`time.gmtime` to convert it into a 

155 different format. 

156 

157 :param committer_tz_offset: int_seconds_west_of_utc 

158 The timezone that the `committed_date` is in. 

159 

160 :param message: string 

161 The commit message. 

162 

163 :param encoding: string 

164 Encoding of the message, defaults to UTF-8. 

165 

166 :param parents: 

167 List or tuple of :class:`Commit` objects which are our parent(s) in the 

168 commit dependency graph. 

169 

170 :return: 

171 :class:`Commit` 

172 

173 :note: 

174 Timezone information is in the same format and in the same sign as what 

175 :func:`time.altzone` returns. The sign is inverted compared to git's UTC 

176 timezone. 

177 """ 

178 super().__init__(repo, binsha) 

179 self.binsha = binsha 

180 if tree is not None: 

181 assert isinstance(tree, Tree), "Tree needs to be a Tree instance, was %s" % type(tree) 

182 if tree is not None: 

183 self.tree = tree 

184 if author is not None: 

185 self.author = author 

186 if authored_date is not None: 

187 self.authored_date = authored_date 

188 if author_tz_offset is not None: 

189 self.author_tz_offset = author_tz_offset 

190 if committer is not None: 

191 self.committer = committer 

192 if committed_date is not None: 

193 self.committed_date = committed_date 

194 if committer_tz_offset is not None: 

195 self.committer_tz_offset = committer_tz_offset 

196 if message is not None: 

197 self.message = message 

198 if parents is not None: 

199 self.parents = parents 

200 if encoding is not None: 

201 self.encoding = encoding 

202 if gpgsig is not None: 

203 self.gpgsig = gpgsig 

204 

205 @classmethod 

206 def _get_intermediate_items(cls, commit: "Commit") -> Tuple["Commit", ...]: 

207 return tuple(commit.parents) 

208 

209 @classmethod 

210 def _calculate_sha_(cls, repo: "Repo", commit: "Commit") -> bytes: 

211 """Calculate the sha of a commit. 

212 

213 :param repo: 

214 :class:`~git.repo.base.Repo` object the commit should be part of. 

215 

216 :param commit: 

217 :class:`Commit` object for which to generate the sha. 

218 """ 

219 

220 stream = BytesIO() 

221 commit._serialize(stream) 

222 streamlen = stream.tell() 

223 stream.seek(0) 

224 

225 istream = repo.odb.store(IStream(cls.type, streamlen, stream)) 

226 return istream.binsha 

227 

228 def replace(self, **kwargs: Any) -> "Commit": 

229 """Create new commit object from an existing commit object. 

230 

231 Any values provided as keyword arguments will replace the corresponding 

232 attribute in the new object. 

233 """ 

234 

235 attrs = {k: getattr(self, k) for k in self.__slots__} 

236 

237 for attrname in kwargs: 

238 if attrname not in self.__slots__: 

239 raise ValueError("invalid attribute name") 

240 

241 attrs.update(kwargs) 

242 new_commit = self.__class__(self.repo, self.NULL_BIN_SHA, **attrs) 

243 new_commit.binsha = self._calculate_sha_(self.repo, new_commit) 

244 

245 return new_commit 

246 

247 def _set_cache_(self, attr: str) -> None: 

248 if attr in Commit.__slots__: 

249 # Read the data in a chunk, its faster - then provide a file wrapper. 

250 _binsha, _typename, self.size, stream = self.repo.odb.stream(self.binsha) 

251 self._deserialize(BytesIO(stream.read())) 

252 else: 

253 super()._set_cache_(attr) 

254 # END handle attrs 

255 

256 @property 

257 def authored_datetime(self) -> datetime.datetime: 

258 return from_timestamp(self.authored_date, self.author_tz_offset) 

259 

260 @property 

261 def committed_datetime(self) -> datetime.datetime: 

262 return from_timestamp(self.committed_date, self.committer_tz_offset) 

263 

264 @property 

265 def summary(self) -> Union[str, bytes]: 

266 """:return: First line of the commit message""" 

267 if isinstance(self.message, str): 

268 return self.message.split("\n", 1)[0] 

269 else: 

270 return self.message.split(b"\n", 1)[0] 

271 

272 def count(self, paths: Union[PathLike, Sequence[PathLike]] = "", **kwargs: Any) -> int: 

273 """Count the number of commits reachable from this commit. 

274 

275 :param paths: 

276 An optional path or a list of paths restricting the return value to commits 

277 actually containing the paths. 

278 

279 :param kwargs: 

280 Additional options to be passed to :manpage:`git-rev-list(1)`. They must not 

281 alter the output style of the command, or parsing will yield incorrect 

282 results. 

283 

284 :return: 

285 An int defining the number of reachable commits 

286 """ 

287 # Yes, it makes a difference whether empty paths are given or not in our case as 

288 # the empty paths version will ignore merge commits for some reason. 

289 if paths: 

290 return len(self.repo.git.rev_list(self.hexsha, "--", paths, **kwargs).splitlines()) 

291 return len(self.repo.git.rev_list(self.hexsha, **kwargs).splitlines()) 

292 

293 @property 

294 def name_rev(self) -> str: 

295 """ 

296 :return: 

297 String describing the commits hex sha based on the closest 

298 :class:`~git.refs.reference.Reference`. 

299 

300 :note: 

301 Mostly useful for UI purposes. 

302 """ 

303 return self.repo.git.name_rev(self) 

304 

305 @classmethod 

306 def iter_items( 

307 cls, 

308 repo: "Repo", 

309 rev: Union[str, "Commit", "SymbolicReference"], 

310 paths: Union[PathLike, Sequence[PathLike]] = "", 

311 allow_unsafe_options: bool = False, 

312 **kwargs: Any, 

313 ) -> Iterator["Commit"]: 

314 R"""Find all commits matching the given criteria. 

315 

316 :param repo: 

317 The :class:`~git.repo.base.Repo`. 

318 

319 :param rev: 

320 Revision specifier. See :manpage:`git-rev-parse(1)` for viable options. 

321 

322 :param paths: 

323 An optional path or list of paths. If set only :class:`Commit`\s that 

324 include the path or paths will be considered. 

325 

326 :param kwargs: 

327 Optional keyword arguments to :manpage:`git-rev-list(1)` where: 

328 

329 * ``max_count`` is the maximum number of commits to fetch. 

330 * ``skip`` is the number of commits to skip. 

331 * ``since`` selects all commits since some date, e.g. ``"1970-01-01"``. 

332 

333 :return: 

334 Iterator yielding :class:`Commit` items. 

335 """ 

336 if "pretty" in kwargs: 

337 raise ValueError("--pretty cannot be used as parsing expects single sha's only") 

338 # END handle pretty 

339 

340 if not allow_unsafe_options: 

341 Git.check_unsafe_options( 

342 options=Git._option_candidates([rev], kwargs), unsafe_options=cls.unsafe_git_rev_options 

343 ) 

344 

345 # Use -- in all cases, to prevent possibility of ambiguous arguments. 

346 # See https://github.com/gitpython-developers/GitPython/issues/264. 

347 

348 args_list: List[PathLike] = ["--"] 

349 

350 if paths: 

351 paths_tup: Tuple[PathLike, ...] 

352 if isinstance(paths, (str, os.PathLike)): 

353 paths_tup = (paths,) 

354 else: 

355 paths_tup = tuple(paths) 

356 

357 args_list.extend(paths_tup) 

358 # END if paths 

359 

360 proc = repo.git.rev_list(rev, args_list, as_process=True, **kwargs) 

361 return cls._iter_from_process_or_stream(repo, proc) 

362 

363 def iter_parents(self, paths: Union[PathLike, Sequence[PathLike]] = "", **kwargs: Any) -> Iterator["Commit"]: 

364 R"""Iterate *all* parents of this commit. 

365 

366 :param paths: 

367 Optional path or list of paths limiting the :class:`Commit`\s to those that 

368 contain at least one of the paths. 

369 

370 :param kwargs: 

371 All arguments allowed by :manpage:`git-rev-list(1)`. 

372 

373 :return: 

374 Iterator yielding :class:`Commit` objects which are parents of ``self`` 

375 """ 

376 # skip ourselves 

377 skip = kwargs.get("skip", 1) 

378 if skip == 0: # skip ourselves 

379 skip = 1 

380 kwargs["skip"] = skip 

381 

382 return self.iter_items(self.repo, self, paths, **kwargs) 

383 

384 @property 

385 def stats(self) -> Stats: 

386 """Create a git stat from changes between this commit and its first parent 

387 or from all changes done if this is the very first commit. 

388 

389 :note: 

390 If this commit is at the boundary of a shallow clone, this will 

391 raise :exc:`~git.exc.GitCommandError`, since the parent object 

392 was never fetched and only exists as a reference on this commit. 

393 

394 :return: 

395 :class:`Stats` 

396 """ 

397 

398 def process_lines(lines: List[str]) -> str: 

399 text = "" 

400 for file_info, line in zip(lines, lines[len(lines) // 2 :]): 

401 change_type = file_info.split("\t")[0][-1] 

402 (insertions, deletions, filename) = line.split("\t") 

403 text += "%s\t%s\t%s\t%s\n" % (change_type, insertions, deletions, filename) 

404 return text 

405 

406 if not self.parents: 

407 lines = self.repo.git.diff_tree( 

408 self.hexsha, "--", numstat=True, no_renames=True, root=True, raw=True 

409 ).splitlines()[1:] 

410 text = process_lines(lines) 

411 else: 

412 lines = self.repo.git.diff( 

413 self.parents[0].hexsha, self.hexsha, "--", numstat=True, no_renames=True, raw=True 

414 ).splitlines() 

415 text = process_lines(lines) 

416 return Stats._list_from_string(self.repo, text) 

417 

418 @property 

419 def trailers(self) -> Dict[str, str]: 

420 """Deprecated. Get the trailers of the message as a dictionary. 

421 

422 :note: 

423 This property is deprecated, please use either :attr:`trailers_list` or 

424 :attr:`trailers_dict`. 

425 

426 :return: 

427 Dictionary containing whitespace stripped trailer information. 

428 Only contains the latest instance of each trailer key. 

429 """ 

430 warnings.warn( 

431 "Commit.trailers is deprecated, use Commit.trailers_list or Commit.trailers_dict instead", 

432 DeprecationWarning, 

433 stacklevel=2, 

434 ) 

435 return {k: v[0] for k, v in self.trailers_dict.items()} 

436 

437 @property 

438 def trailers_list(self) -> List[Tuple[str, str]]: 

439 """Get the trailers of the message as a list. 

440 

441 Git messages can contain trailer information that are similar to :rfc:`822` 

442 e-mail headers. See :manpage:`git-interpret-trailers(1)`. 

443 

444 This function calls ``git interpret-trailers --parse`` onto the message to 

445 extract the trailer information, returns the raw trailer data as a list. 

446 

447 Valid message with trailer:: 

448 

449 Subject line 

450 

451 some body information 

452 

453 another information 

454 

455 key1: value1.1 

456 key1: value1.2 

457 key2 : value 2 with inner spaces 

458 

459 Returned list will look like this:: 

460 

461 [ 

462 ("key1", "value1.1"), 

463 ("key1", "value1.2"), 

464 ("key2", "value 2 with inner spaces"), 

465 ] 

466 

467 :return: 

468 List containing key-value tuples of whitespace stripped trailer information. 

469 """ 

470 trailer = self._interpret_trailers(self.repo, self.message, ["--parse"], encoding=self.encoding).strip() 

471 

472 if not trailer: 

473 return [] 

474 

475 trailer_list = [] 

476 for t in trailer.split("\n"): 

477 key, val = t.split(":", 1) 

478 trailer_list.append((key.strip(), val.strip())) 

479 

480 return trailer_list 

481 

482 @classmethod 

483 def _interpret_trailers( 

484 cls, 

485 repo: "Repo", 

486 message: Union[str, bytes], 

487 trailer_args: Sequence[str], 

488 encoding: str = default_encoding, 

489 ) -> str: 

490 message_bytes = message if isinstance(message, bytes) else message.encode(encoding, errors="strict") 

491 cmd = [repo.git.GIT_PYTHON_GIT_EXECUTABLE, "interpret-trailers", *trailer_args] 

492 proc: Git.AutoInterrupt = repo.git.execute( # type: ignore[call-overload] 

493 cmd, 

494 as_process=True, 

495 istream=PIPE, 

496 ) 

497 try: 

498 stdout_bytes, _ = proc.communicate(message_bytes) 

499 return stdout_bytes.decode(encoding, errors="strict") 

500 finally: 

501 finalize_process(proc) 

502 

503 @property 

504 def trailers_dict(self) -> Dict[str, List[str]]: 

505 """Get the trailers of the message as a dictionary. 

506 

507 Git messages can contain trailer information that are similar to :rfc:`822` 

508 e-mail headers. See :manpage:`git-interpret-trailers(1)`. 

509 

510 This function calls ``git interpret-trailers --parse`` onto the message to 

511 extract the trailer information. The key value pairs are stripped of leading and 

512 trailing whitespaces before they get saved into a dictionary. 

513 

514 Valid message with trailer:: 

515 

516 Subject line 

517 

518 some body information 

519 

520 another information 

521 

522 key1: value1.1 

523 key1: value1.2 

524 key2 : value 2 with inner spaces 

525 

526 Returned dictionary will look like this:: 

527 

528 { 

529 "key1": ["value1.1", "value1.2"], 

530 "key2": ["value 2 with inner spaces"], 

531 } 

532 

533 

534 :return: 

535 Dictionary containing whitespace stripped trailer information, mapping 

536 trailer keys to a list of their corresponding values. 

537 """ 

538 d = defaultdict(list) 

539 for key, val in self.trailers_list: 

540 d[key].append(val) 

541 return dict(d) 

542 

543 @classmethod 

544 def _iter_from_process_or_stream(cls, repo: "Repo", proc_or_stream: Union[Popen, IO]) -> Iterator["Commit"]: 

545 """Parse out commit information into a list of :class:`Commit` objects. 

546 

547 We expect one line per commit, and parse the actual commit information directly 

548 from our lighting fast object database. 

549 

550 :param proc: 

551 :manpage:`git-rev-list(1)` process instance - one sha per line. 

552 

553 :return: 

554 Iterator supplying :class:`Commit` objects 

555 """ 

556 

557 # def is_proc(inp) -> TypeGuard[Popen]: 

558 # return hasattr(proc_or_stream, 'wait') and not hasattr(proc_or_stream, 'readline') 

559 

560 # def is_stream(inp) -> TypeGuard[IO]: 

561 # return hasattr(proc_or_stream, 'readline') 

562 

563 if hasattr(proc_or_stream, "wait"): 

564 proc_or_stream = cast(Popen, proc_or_stream) 

565 if proc_or_stream.stdout is not None: 

566 stream = proc_or_stream.stdout 

567 elif hasattr(proc_or_stream, "readline"): 

568 proc_or_stream = cast(IO, proc_or_stream) # type: ignore[redundant-cast] 

569 stream = proc_or_stream 

570 

571 readline = stream.readline 

572 while True: 

573 line = readline() 

574 if not line: 

575 break 

576 hexsha = line.strip() 

577 if len(hexsha) > 40: 

578 # Split additional information, as returned by bisect for instance. 

579 hexsha, _ = line.split(None, 1) 

580 # END handle extra info 

581 

582 assert len(hexsha) == 40, "Invalid line: %s" % hexsha 

583 yield cls(repo, hex_to_bin(hexsha)) 

584 # END for each line in stream 

585 

586 # TODO: Review this - it seems process handling got a bit out of control due to 

587 # many developers trying to fix the open file handles issue. 

588 if hasattr(proc_or_stream, "wait"): 

589 proc_or_stream = cast(Popen, proc_or_stream) 

590 finalize_process(proc_or_stream) 

591 

592 @classmethod 

593 def create_from_tree( 

594 cls, 

595 repo: "Repo", 

596 tree: Union[Tree, str], 

597 message: str, 

598 parent_commits: Union[None, List["Commit"]] = None, 

599 head: bool = False, 

600 author: Union[None, Actor] = None, 

601 committer: Union[None, Actor] = None, 

602 author_date: Union[None, str, datetime.datetime] = None, 

603 commit_date: Union[None, str, datetime.datetime] = None, 

604 trailers: Union[None, Dict[str, str], List[Tuple[str, str]]] = None, 

605 ) -> "Commit": 

606 """Commit the given tree, creating a :class:`Commit` object. 

607 

608 :param repo: 

609 :class:`~git.repo.base.Repo` object the commit should be part of. 

610 

611 :param tree: 

612 :class:`~git.objects.tree.Tree` object or hex or bin sha. 

613 The tree of the new commit. 

614 

615 :param message: 

616 Commit message. It may be an empty string if no message is provided. It will 

617 be converted to a string, in any case. 

618 

619 :param parent_commits: 

620 Optional :class:`Commit` objects to use as parents for the new commit. If 

621 empty list, the commit will have no parents at all and become a root commit. 

622 If ``None``, the current head commit will be the parent of the new commit 

623 object. 

624 

625 :param head: 

626 If ``True``, the HEAD will be advanced to the new commit automatically. 

627 Otherwise the HEAD will remain pointing on the previous commit. This could 

628 lead to undesired results when diffing files. 

629 

630 :param author: 

631 The name of the author, optional. 

632 If unset, the repository configuration is used to obtain this value. 

633 

634 :param committer: 

635 The name of the committer, optional. 

636 If unset, the repository configuration is used to obtain this value. 

637 

638 :param author_date: 

639 The timestamp for the author field. 

640 

641 :param commit_date: 

642 The timestamp for the committer field. 

643 

644 :param trailers: 

645 Optional trailer key-value pairs to append to the commit message. 

646 Can be a dictionary mapping trailer keys to values, or a list of 

647 ``(key, value)`` tuples (useful when the same key appears multiple 

648 times, e.g. multiple ``Signed-off-by`` trailers). Trailers are 

649 appended using ``git interpret-trailers``. 

650 See :manpage:`git-interpret-trailers(1)`. 

651 

652 :return: 

653 :class:`Commit` object representing the new commit. 

654 

655 :note: 

656 Additional information about the committer and author are taken from the 

657 environment or from the git configuration. See :manpage:`git-commit-tree(1)` 

658 for more information. 

659 """ 

660 if parent_commits is None: 

661 try: 

662 parent_commits = [repo.head.commit] 

663 except ValueError: 

664 # Empty repositories have no head commit. 

665 parent_commits = [] 

666 # END handle parent commits 

667 else: 

668 for p in parent_commits: 

669 if not isinstance(p, cls): 

670 raise ValueError(f"Parent commit '{p!r}' must be of type {cls}") 

671 # END check parent commit types 

672 # END if parent commits are unset 

673 

674 # Retrieve all additional information, create a commit object, and serialize it. 

675 # Generally: 

676 # * Environment variables override configuration values. 

677 # * Sensible defaults are set according to the git documentation. 

678 

679 # COMMITTER AND AUTHOR INFO 

680 cr = repo.config_reader() 

681 env = os.environ 

682 

683 committer = committer or Actor.committer(cr) 

684 author = author or Actor.author(cr) 

685 

686 # PARSE THE DATES 

687 unix_time = int(time()) 

688 is_dst = daylight and localtime().tm_isdst > 0 

689 offset = altzone if is_dst else timezone 

690 

691 author_date_str = env.get(cls.env_author_date, "") 

692 if author_date: 

693 author_time, author_offset = parse_date(author_date) 

694 elif author_date_str: 

695 author_time, author_offset = parse_date(author_date_str) 

696 else: 

697 author_time, author_offset = unix_time, offset 

698 # END set author time 

699 

700 committer_date_str = env.get(cls.env_committer_date, "") 

701 if commit_date: 

702 committer_time, committer_offset = parse_date(commit_date) 

703 elif committer_date_str: 

704 committer_time, committer_offset = parse_date(committer_date_str) 

705 else: 

706 committer_time, committer_offset = unix_time, offset 

707 # END set committer time 

708 

709 # Assume UTF-8 encoding. 

710 enc_section, enc_option = cls.conf_encoding.split(".") 

711 conf_encoding = cr.get_value(enc_section, enc_option, cls.default_encoding) 

712 if not isinstance(conf_encoding, str): 

713 raise TypeError("conf_encoding could not be coerced to str") 

714 

715 # If the tree is no object, make sure we create one - otherwise the created 

716 # commit object is invalid. 

717 if isinstance(tree, str): 

718 tree = repo.tree(tree) 

719 # END tree conversion 

720 

721 # APPLY TRAILERS 

722 if trailers: 

723 trailer_args: List[str] = [] 

724 if isinstance(trailers, dict): 

725 for key, val in trailers.items(): 

726 trailer_args.append("--trailer") 

727 trailer_args.append(f"{key}: {val}") 

728 else: 

729 for key, val in trailers: 

730 trailer_args.append("--trailer") 

731 trailer_args.append(f"{key}: {val}") 

732 

733 message = cls._interpret_trailers(repo, str(message), trailer_args) 

734 # END apply trailers 

735 

736 # CREATE NEW COMMIT 

737 new_commit = cls( 

738 repo, 

739 cls.NULL_BIN_SHA, 

740 tree, 

741 author, 

742 author_time, 

743 author_offset, 

744 committer, 

745 committer_time, 

746 committer_offset, 

747 message, 

748 parent_commits, 

749 conf_encoding, 

750 ) 

751 

752 new_commit.binsha = cls._calculate_sha_(repo, new_commit) 

753 

754 if head: 

755 # Need late import here, importing git at the very beginning throws as 

756 # well... 

757 import git.refs 

758 

759 try: 

760 repo.head.set_commit(new_commit, logmsg=message) 

761 except ValueError: 

762 # head is not yet set to the ref our HEAD points to. 

763 # Happens on first commit. 

764 master = git.refs.Head.create( 

765 repo, 

766 repo.head.ref, 

767 new_commit, 

768 logmsg="commit (initial): %s" % message, 

769 ) 

770 repo.head.set_reference(master, logmsg="commit: Switching to %s" % master) 

771 # END handle empty repositories 

772 # END advance head handling 

773 

774 return new_commit 

775 

776 # { Serializable Implementation 

777 

778 def _serialize(self, stream: BytesIO) -> "Commit": 

779 write = stream.write 

780 write(("tree %s\n" % self.tree).encode("ascii")) 

781 for p in self.parents: 

782 write(("parent %s\n" % p).encode("ascii")) 

783 

784 a = self.author 

785 aname = a.name 

786 c = self.committer 

787 fmt = "%s %s <%s> %s %s\n" 

788 write( 

789 ( 

790 fmt 

791 % ( 

792 "author", 

793 aname, 

794 a.email, 

795 self.authored_date, 

796 altz_to_utctz_str(self.author_tz_offset), 

797 ) 

798 ).encode(self.encoding) 

799 ) 

800 

801 # Encode committer. 

802 aname = c.name 

803 write( 

804 ( 

805 fmt 

806 % ( 

807 "committer", 

808 aname, 

809 c.email, 

810 self.committed_date, 

811 altz_to_utctz_str(self.committer_tz_offset), 

812 ) 

813 ).encode(self.encoding) 

814 ) 

815 

816 if self.encoding != self.default_encoding: 

817 write(("encoding %s\n" % self.encoding).encode("ascii")) 

818 

819 try: 

820 if self.__getattribute__("gpgsig"): 

821 write(b"gpgsig") 

822 for sigline in self.gpgsig.rstrip("\n").split("\n"): 

823 write((" " + sigline + "\n").encode("ascii")) 

824 except AttributeError: 

825 pass 

826 

827 write(b"\n") 

828 

829 # Write plain bytes, be sure its encoded according to our encoding. 

830 if isinstance(self.message, str): 

831 write(self.message.encode(self.encoding)) 

832 else: 

833 write(self.message) 

834 # END handle encoding 

835 return self 

836 

837 def _deserialize(self, stream: BytesIO) -> "Commit": 

838 readline = stream.readline 

839 self.tree = Tree(self.repo, hex_to_bin(readline().split()[1]), Tree.tree_id << 12, "") 

840 

841 self.parents = [] 

842 next_line = None 

843 while True: 

844 parent_line = readline() 

845 if not parent_line.startswith(b"parent"): 

846 next_line = parent_line 

847 break 

848 # END abort reading parents 

849 self.parents.append(type(self)(self.repo, hex_to_bin(parent_line.split()[-1].decode("ascii")))) 

850 # END for each parent line 

851 self.parents = tuple(self.parents) 

852 

853 # We don't know actual author encoding before we have parsed it, so keep the 

854 # lines around. 

855 author_line = next_line 

856 committer_line = readline() 

857 

858 # We might run into one or more mergetag blocks, skip those for now. 

859 next_line = readline() 

860 while next_line.startswith(b"mergetag "): 

861 next_line = readline() 

862 while next_line.startswith(b" "): 

863 next_line = readline() 

864 # END skip mergetags 

865 

866 # Now we can have the encoding line, or an empty line followed by the optional 

867 # message. 

868 self.encoding = self.default_encoding 

869 self.gpgsig = "" 

870 

871 # Read headers. 

872 enc = next_line 

873 buf = enc.strip() 

874 while buf: 

875 if buf[0:10] == b"encoding ": 

876 self.encoding = buf[buf.find(b" ") + 1 :].decode(self.encoding, "ignore") 

877 elif buf[0:7] == b"gpgsig ": 

878 sig = buf[buf.find(b" ") + 1 :] + b"\n" 

879 is_next_header = False 

880 while True: 

881 sigbuf = readline() 

882 if not sigbuf: 

883 break 

884 if sigbuf[0:1] != b" ": 

885 buf = sigbuf.strip() 

886 is_next_header = True 

887 break 

888 sig += sigbuf[1:] 

889 # END read all signature 

890 self.gpgsig = sig.rstrip(b"\n").decode(self.encoding, "ignore") 

891 if is_next_header: 

892 continue 

893 buf = readline().strip() 

894 

895 # Decode the author's name. 

896 try: 

897 ( 

898 self.author, 

899 self.authored_date, 

900 self.author_tz_offset, 

901 ) = parse_actor_and_date(author_line.decode(self.encoding, "replace")) 

902 except UnicodeDecodeError: 

903 _logger.error( 

904 "Failed to decode author line '%s' using encoding %s", 

905 author_line, 

906 self.encoding, 

907 exc_info=True, 

908 ) 

909 

910 try: 

911 ( 

912 self.committer, 

913 self.committed_date, 

914 self.committer_tz_offset, 

915 ) = parse_actor_and_date(committer_line.decode(self.encoding, "replace")) 

916 except UnicodeDecodeError: 

917 _logger.error( 

918 "Failed to decode committer line '%s' using encoding %s", 

919 committer_line, 

920 self.encoding, 

921 exc_info=True, 

922 ) 

923 # END handle author's encoding 

924 

925 # A stream from our data simply gives us the plain message. 

926 # The end of our message stream is marked with a newline that we strip. 

927 self.message = stream.read() 

928 try: 

929 self.message = self.message.decode(self.encoding, "replace") 

930 except UnicodeDecodeError: 

931 _logger.error( 

932 "Failed to decode message '%s' using encoding %s", 

933 self.message, 

934 self.encoding, 

935 exc_info=True, 

936 ) 

937 # END exception handling 

938 

939 return self 

940 

941 # } END serializable implementation 

942 

943 @property 

944 def co_authors(self) -> List[Actor]: 

945 """Search the commit message for any co-authors of this commit. 

946 

947 Details on co-authors: 

948 https://github.blog/2018-01-29-commit-together-with-co-authors/ 

949 

950 :return: 

951 List of co-authors for this commit (as :class:`~git.util.Actor` objects). 

952 """ 

953 co_authors = [] 

954 

955 if self.message: 

956 results = re.findall( 

957 r"^Co-authored-by: (.*) <(.*?)>$", 

958 str(self.message), 

959 re.MULTILINE, 

960 ) 

961 for author in results: 

962 co_authors.append(Actor(*author)) 

963 

964 return co_authors