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

343 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( 

273 self, 

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

275 allow_unsafe_options: bool = False, 

276 **kwargs: Any, 

277 ) -> int: 

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

279 

280 :param paths: 

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

282 actually containing the paths. 

283 

284 :param allow_unsafe_options: 

285 Allow unsafe options, like ``--output``. 

286 

287 :param kwargs: 

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

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

290 results. 

291 

292 :return: 

293 An int defining the number of reachable commits 

294 """ 

295 if not allow_unsafe_options: 

296 Git.check_unsafe_options( 

297 options=Git._option_candidates([], kwargs), unsafe_options=self.unsafe_git_rev_options 

298 ) 

299 

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

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

302 if paths: 

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

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

305 

306 @property 

307 def name_rev(self) -> str: 

308 """ 

309 :return: 

310 String describing the commits hex sha based on the closest 

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

312 

313 :note: 

314 Mostly useful for UI purposes. 

315 """ 

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

317 

318 @classmethod 

319 def iter_items( 

320 cls, 

321 repo: "Repo", 

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

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

324 allow_unsafe_options: bool = False, 

325 **kwargs: Any, 

326 ) -> Iterator["Commit"]: 

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

328 

329 :param repo: 

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

331 

332 :param rev: 

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

334 

335 :param paths: 

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

337 include the path or paths will be considered. 

338 

339 :param kwargs: 

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

341 

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

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

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

345 

346 :return: 

347 Iterator yielding :class:`Commit` items. 

348 """ 

349 if "pretty" in kwargs: 

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

351 # END handle pretty 

352 

353 if not allow_unsafe_options: 

354 Git.check_unsafe_options( 

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

356 ) 

357 

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

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

360 

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

362 

363 if paths: 

364 paths_tup: Tuple[PathLike, ...] 

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

366 paths_tup = (paths,) 

367 else: 

368 paths_tup = tuple(paths) 

369 

370 args_list.extend(paths_tup) 

371 # END if paths 

372 

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

374 return cls._iter_from_process_or_stream(repo, proc) 

375 

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

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

378 

379 :param paths: 

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

381 contain at least one of the paths. 

382 

383 :param kwargs: 

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

385 

386 :return: 

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

388 """ 

389 # skip ourselves 

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

391 if skip == 0: # skip ourselves 

392 skip = 1 

393 kwargs["skip"] = skip 

394 

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

396 

397 @property 

398 def stats(self) -> Stats: 

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

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

401 

402 :note: 

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

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

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

406 

407 :return: 

408 :class:`Stats` 

409 """ 

410 

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

412 text = "" 

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

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

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

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

417 return text 

418 

419 if not self.parents: 

420 lines = self.repo.git.diff_tree( 

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

422 ).splitlines()[1:] 

423 text = process_lines(lines) 

424 else: 

425 lines = self.repo.git.diff( 

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

427 ).splitlines() 

428 text = process_lines(lines) 

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

430 

431 @property 

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

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

434 

435 :note: 

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

437 :attr:`trailers_dict`. 

438 

439 :return: 

440 Dictionary containing whitespace stripped trailer information. 

441 Only contains the latest instance of each trailer key. 

442 """ 

443 warnings.warn( 

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

445 DeprecationWarning, 

446 stacklevel=2, 

447 ) 

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

449 

450 @property 

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

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

453 

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

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

456 

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

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

459 

460 Valid message with trailer:: 

461 

462 Subject line 

463 

464 some body information 

465 

466 another information 

467 

468 key1: value1.1 

469 key1: value1.2 

470 key2 : value 2 with inner spaces 

471 

472 Returned list will look like this:: 

473 

474 [ 

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

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

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

478 ] 

479 

480 :return: 

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

482 """ 

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

484 

485 if not trailer: 

486 return [] 

487 

488 trailer_list = [] 

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

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

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

492 

493 return trailer_list 

494 

495 @classmethod 

496 def _interpret_trailers( 

497 cls, 

498 repo: "Repo", 

499 message: Union[str, bytes], 

500 trailer_args: Sequence[str], 

501 encoding: str = default_encoding, 

502 ) -> str: 

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

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

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

506 cmd, 

507 as_process=True, 

508 istream=PIPE, 

509 ) 

510 try: 

511 stdout_bytes, _ = proc.communicate(message_bytes) 

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

513 finally: 

514 finalize_process(proc) 

515 

516 @property 

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

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

519 

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

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

522 

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

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

525 trailing whitespaces before they get saved into a dictionary. 

526 

527 Valid message with trailer:: 

528 

529 Subject line 

530 

531 some body information 

532 

533 another information 

534 

535 key1: value1.1 

536 key1: value1.2 

537 key2 : value 2 with inner spaces 

538 

539 Returned dictionary will look like this:: 

540 

541 { 

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

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

544 } 

545 

546 

547 :return: 

548 Dictionary containing whitespace stripped trailer information, mapping 

549 trailer keys to a list of their corresponding values. 

550 """ 

551 d = defaultdict(list) 

552 for key, val in self.trailers_list: 

553 d[key].append(val) 

554 return dict(d) 

555 

556 @classmethod 

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

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

559 

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

561 from our lighting fast object database. 

562 

563 :param proc: 

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

565 

566 :return: 

567 Iterator supplying :class:`Commit` objects 

568 """ 

569 

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

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

572 

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

574 # return hasattr(proc_or_stream, 'readline') 

575 

576 if hasattr(proc_or_stream, "wait"): 

577 proc_or_stream = cast(Popen, proc_or_stream) 

578 stream = proc_or_stream.stdout 

579 if stream is None: 

580 raise ValueError("Process has no stdout stream") 

581 elif hasattr(proc_or_stream, "readline"): 

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

583 stream = proc_or_stream 

584 else: 

585 raise TypeError("Expected a process or stream") 

586 

587 readline = stream.readline 

588 while True: 

589 line = readline() 

590 if not line: 

591 break 

592 hexsha = line.strip() 

593 if len(hexsha) > 40: 

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

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

596 # END handle extra info 

597 

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

599 yield cls(repo, hex_to_bin(hexsha)) 

600 # END for each line in stream 

601 

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

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

604 if hasattr(proc_or_stream, "wait"): 

605 proc_or_stream = cast(Popen, proc_or_stream) 

606 finalize_process(proc_or_stream) 

607 

608 @classmethod 

609 def create_from_tree( 

610 cls, 

611 repo: "Repo", 

612 tree: Union[Tree, str], 

613 message: str, 

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

615 head: bool = False, 

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

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

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

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

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

621 ) -> "Commit": 

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

623 

624 :param repo: 

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

626 

627 :param tree: 

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

629 The tree of the new commit. 

630 

631 :param message: 

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

633 be converted to a string, in any case. 

634 

635 :param parent_commits: 

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

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

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

639 object. 

640 

641 :param head: 

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

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

644 lead to undesired results when diffing files. 

645 

646 :param author: 

647 The name of the author, optional. 

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

649 

650 :param committer: 

651 The name of the committer, optional. 

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

653 

654 :param author_date: 

655 The timestamp for the author field. 

656 

657 :param commit_date: 

658 The timestamp for the committer field. 

659 

660 :param trailers: 

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

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

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

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

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

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

667 

668 :return: 

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

670 

671 :note: 

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

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

674 for more information. 

675 """ 

676 if parent_commits is None: 

677 try: 

678 parent_commits = [repo.head.commit] 

679 except ValueError: 

680 # Empty repositories have no head commit. 

681 parent_commits = [] 

682 # END handle parent commits 

683 else: 

684 for p in parent_commits: 

685 if not isinstance(p, cls): 

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

687 # END check parent commit types 

688 # END if parent commits are unset 

689 

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

691 # Generally: 

692 # * Environment variables override configuration values. 

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

694 

695 # COMMITTER AND AUTHOR INFO 

696 cr = repo.config_reader() 

697 env = os.environ 

698 

699 committer = committer or Actor.committer(cr) 

700 author = author or Actor.author(cr) 

701 

702 # PARSE THE DATES 

703 unix_time = int(time()) 

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

705 offset = altzone if is_dst else timezone 

706 

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

708 if author_date: 

709 author_time, author_offset = parse_date(author_date) 

710 elif author_date_str: 

711 author_time, author_offset = parse_date(author_date_str) 

712 else: 

713 author_time, author_offset = unix_time, offset 

714 # END set author time 

715 

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

717 if commit_date: 

718 committer_time, committer_offset = parse_date(commit_date) 

719 elif committer_date_str: 

720 committer_time, committer_offset = parse_date(committer_date_str) 

721 else: 

722 committer_time, committer_offset = unix_time, offset 

723 # END set committer time 

724 

725 # Assume UTF-8 encoding. 

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

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

728 if not isinstance(conf_encoding, str): 

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

730 

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

732 # commit object is invalid. 

733 if isinstance(tree, str): 

734 tree = repo.tree(tree) 

735 # END tree conversion 

736 

737 # APPLY TRAILERS 

738 if trailers: 

739 trailer_args: List[str] = [] 

740 if isinstance(trailers, dict): 

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

742 trailer_args.append("--trailer") 

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

744 else: 

745 for key, val in trailers: 

746 trailer_args.append("--trailer") 

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

748 

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

750 # END apply trailers 

751 

752 # CREATE NEW COMMIT 

753 new_commit = cls( 

754 repo, 

755 cls.NULL_BIN_SHA, 

756 tree, 

757 author, 

758 author_time, 

759 author_offset, 

760 committer, 

761 committer_time, 

762 committer_offset, 

763 message, 

764 parent_commits, 

765 conf_encoding, 

766 ) 

767 

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

769 

770 if head: 

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

772 # well... 

773 import git.refs 

774 

775 try: 

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

777 except ValueError: 

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

779 # Happens on first commit. 

780 master = git.refs.Head.create( 

781 repo, 

782 repo.head.ref, 

783 new_commit, 

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

785 ) 

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

787 # END handle empty repositories 

788 # END advance head handling 

789 

790 return new_commit 

791 

792 # { Serializable Implementation 

793 

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

795 write = stream.write 

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

797 for p in self.parents: 

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

799 

800 a = self.author 

801 aname = a.name 

802 c = self.committer 

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

804 write( 

805 ( 

806 fmt 

807 % ( 

808 "author", 

809 aname, 

810 a.email, 

811 self.authored_date, 

812 altz_to_utctz_str(self.author_tz_offset), 

813 ) 

814 ).encode(self.encoding) 

815 ) 

816 

817 # Encode committer. 

818 aname = c.name 

819 write( 

820 ( 

821 fmt 

822 % ( 

823 "committer", 

824 aname, 

825 c.email, 

826 self.committed_date, 

827 altz_to_utctz_str(self.committer_tz_offset), 

828 ) 

829 ).encode(self.encoding) 

830 ) 

831 

832 if self.encoding != self.default_encoding: 

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

834 

835 try: 

836 if self.__getattribute__("gpgsig"): 

837 write(b"gpgsig") 

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

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

840 except AttributeError: 

841 pass 

842 

843 write(b"\n") 

844 

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

846 if isinstance(self.message, str): 

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

848 else: 

849 write(self.message) 

850 # END handle encoding 

851 return self 

852 

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

854 readline = stream.readline 

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

856 

857 self.parents = [] 

858 next_line = None 

859 while True: 

860 parent_line = readline() 

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

862 next_line = parent_line 

863 break 

864 # END abort reading parents 

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

866 # END for each parent line 

867 self.parents = tuple(self.parents) 

868 

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

870 # lines around. 

871 author_line = next_line 

872 committer_line = readline() 

873 

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

875 next_line = readline() 

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

877 next_line = readline() 

878 while next_line.startswith(b" "): 

879 next_line = readline() 

880 # END skip mergetags 

881 

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

883 # message. 

884 self.encoding = self.default_encoding 

885 self.gpgsig = "" 

886 

887 # Read headers. 

888 enc = next_line 

889 buf = enc.strip() 

890 while buf: 

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

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

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

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

895 is_next_header = False 

896 while True: 

897 sigbuf = readline() 

898 if not sigbuf: 

899 break 

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

901 buf = sigbuf.strip() 

902 is_next_header = True 

903 break 

904 sig += sigbuf[1:] 

905 # END read all signature 

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

907 if is_next_header: 

908 continue 

909 buf = readline().strip() 

910 

911 # Decode the author's name. 

912 try: 

913 ( 

914 self.author, 

915 self.authored_date, 

916 self.author_tz_offset, 

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

918 except UnicodeDecodeError: 

919 _logger.error( 

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

921 author_line, 

922 self.encoding, 

923 exc_info=True, 

924 ) 

925 

926 try: 

927 ( 

928 self.committer, 

929 self.committed_date, 

930 self.committer_tz_offset, 

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

932 except UnicodeDecodeError: 

933 _logger.error( 

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

935 committer_line, 

936 self.encoding, 

937 exc_info=True, 

938 ) 

939 # END handle author's encoding 

940 

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

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

943 self.message = stream.read() 

944 try: 

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

946 except UnicodeDecodeError: 

947 _logger.error( 

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

949 self.message, 

950 self.encoding, 

951 exc_info=True, 

952 ) 

953 # END exception handling 

954 

955 return self 

956 

957 # } END serializable implementation 

958 

959 @property 

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

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

962 

963 Details on co-authors: 

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

965 

966 :return: 

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

968 """ 

969 co_authors = [] 

970 

971 if self.message: 

972 results = re.findall( 

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

974 str(self.message), 

975 re.MULTILINE, 

976 ) 

977 for author in results: 

978 co_authors.append(Actor(*author)) 

979 

980 return co_authors