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

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

344 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 

13from subprocess import Popen, PIPE 

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

15import warnings 

16 

17from gitdb import IStream 

18 

19from git.cmd import Git 

20from git.diff import Diffable 

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

22 

23from . import base 

24from .tree import Tree 

25from .util import ( 

26 Serializable, 

27 TraversableIterableObj, 

28 altz_to_utctz_str, 

29 from_timestamp, 

30 parse_actor_and_date, 

31 parse_date, 

32) 

33 

34# typing ------------------------------------------------------------------ 

35 

36from typing import ( 

37 Any, 

38 Dict, 

39 IO, 

40 Iterator, 

41 List, 

42 Literal, 

43 Sequence, 

44 Tuple, 

45 TYPE_CHECKING, 

46 Union, 

47 cast, 

48) 

49 

50from git.types import PathLike 

51 

52if TYPE_CHECKING: 

53 from git.refs import SymbolicReference 

54 from git.repo import Repo 

55 

56# ------------------------------------------------------------------------ 

57 

58_logger = logging.getLogger(__name__) 

59 

60 

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

62 """Wraps a git commit object. 

63 

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

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

66 

67 :note: 

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

69 demand only if it involves calling the git binary. 

70 """ 

71 

72 # ENVIRONMENT VARIABLES 

73 # Read when creating new commits. 

74 env_author_date = "GIT_AUTHOR_DATE" 

75 env_committer_date = "GIT_COMMITTER_DATE" 

76 

77 # CONFIGURATION KEYS 

78 conf_encoding = "i18n.commitencoding" 

79 

80 # INVARIANTS 

81 default_encoding = "UTF-8" 

82 

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

84 unsafe_git_rev_options = [ 

85 "--output", 

86 "-o", 

87 ] 

88 

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

90 

91 __slots__ = ( 

92 "tree", 

93 "author", 

94 "authored_date", 

95 "author_tz_offset", 

96 "committer", 

97 "committed_date", 

98 "committer_tz_offset", 

99 "message", 

100 "parents", 

101 "encoding", 

102 "gpgsig", 

103 ) 

104 

105 _id_attribute_ = "hexsha" 

106 

107 parents: Sequence["Commit"] 

108 

109 def __init__( 

110 self, 

111 repo: "Repo", 

112 binsha: bytes, 

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

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

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

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

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

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

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

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

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

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

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

124 ) -> None: 

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

126 default will be implicitly set on first query. 

127 

128 :param binsha: 

129 20 byte sha1. 

130 

131 :param tree: 

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

133 

134 :param author: 

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

136 

137 :param authored_date: int_seconds_since_epoch 

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

139 different format. 

140 

141 :param author_tz_offset: int_seconds_west_of_utc 

142 The timezone that the `authored_date` is in. 

143 

144 :param committer: 

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

146 

147 :param committed_date: int_seconds_since_epoch 

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

149 different format. 

150 

151 :param committer_tz_offset: int_seconds_west_of_utc 

152 The timezone that the `committed_date` is in. 

153 

154 :param message: string 

155 The commit message. 

156 

157 :param encoding: string 

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

159 

160 :param parents: 

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

162 commit dependency graph. 

163 

164 :return: 

165 :class:`Commit` 

166 

167 :note: 

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

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

170 timezone. 

171 """ 

172 super().__init__(repo, binsha) 

173 self.binsha = binsha 

174 if tree is not None: 

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

176 if tree is not None: 

177 self.tree = tree 

178 if author is not None: 

179 self.author = author 

180 if authored_date is not None: 

181 self.authored_date = authored_date 

182 if author_tz_offset is not None: 

183 self.author_tz_offset = author_tz_offset 

184 if committer is not None: 

185 self.committer = committer 

186 if committed_date is not None: 

187 self.committed_date = committed_date 

188 if committer_tz_offset is not None: 

189 self.committer_tz_offset = committer_tz_offset 

190 if message is not None: 

191 self.message = message 

192 if parents is not None: 

193 self.parents = parents 

194 if encoding is not None: 

195 self.encoding = encoding 

196 if gpgsig is not None: 

197 self.gpgsig = gpgsig 

198 

199 @classmethod 

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

201 return tuple(commit.parents) 

202 

203 @classmethod 

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

205 """Calculate the sha of a commit. 

206 

207 :param repo: 

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

209 

210 :param commit: 

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

212 """ 

213 

214 stream = BytesIO() 

215 commit._serialize(stream) 

216 streamlen = stream.tell() 

217 stream.seek(0) 

218 

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

220 return istream.binsha 

221 

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

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

224 

225 Any values provided as keyword arguments will replace the corresponding 

226 attribute in the new object. 

227 """ 

228 

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

230 

231 for attrname in kwargs: 

232 if attrname not in self.__slots__: 

233 raise ValueError("invalid attribute name") 

234 

235 attrs.update(kwargs) 

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

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

238 

239 return new_commit 

240 

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

242 if attr in Commit.__slots__: 

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

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

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

246 else: 

247 super()._set_cache_(attr) 

248 # END handle attrs 

249 

250 @property 

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

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

253 

254 @property 

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

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

257 

258 @property 

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

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

261 if isinstance(self.message, str): 

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

263 else: 

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

265 

266 def count( 

267 self, 

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

269 allow_unsafe_options: bool = False, 

270 **kwargs: Any, 

271 ) -> int: 

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

273 

274 :param paths: 

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

276 actually containing the paths. 

277 

278 :param allow_unsafe_options: 

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

280 

281 :param kwargs: 

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

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

284 results. 

285 

286 :return: 

287 An int defining the number of reachable commits 

288 """ 

289 if not allow_unsafe_options: 

290 Git.check_unsafe_options( 

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

292 ) 

293 

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

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

296 if paths: 

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

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

299 

300 @property 

301 def name_rev(self) -> str: 

302 """ 

303 :return: 

304 String describing the commits hex sha based on the closest 

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

306 

307 :note: 

308 Mostly useful for UI purposes. 

309 """ 

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

311 

312 @classmethod 

313 def iter_items( 

314 cls, 

315 repo: "Repo", 

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

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

318 allow_unsafe_options: bool = False, 

319 **kwargs: Any, 

320 ) -> Iterator["Commit"]: 

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

322 

323 :param repo: 

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

325 

326 :param rev: 

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

328 

329 :param paths: 

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

331 include the path or paths will be considered. 

332 

333 :param kwargs: 

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

335 

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

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

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

339 

340 :return: 

341 Iterator yielding :class:`Commit` items. 

342 """ 

343 if "pretty" in kwargs: 

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

345 # END handle pretty 

346 

347 if not allow_unsafe_options: 

348 Git.check_unsafe_options( 

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

350 ) 

351 

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

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

354 

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

356 

357 if paths: 

358 paths_tup: Tuple[PathLike, ...] 

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

360 paths_tup = (paths,) 

361 else: 

362 paths_tup = tuple(paths) 

363 

364 args_list.extend(paths_tup) 

365 # END if paths 

366 

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

368 return cls._iter_from_process_or_stream(repo, proc) 

369 

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

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

372 

373 :param paths: 

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

375 contain at least one of the paths. 

376 

377 :param kwargs: 

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

379 

380 :return: 

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

382 """ 

383 # skip ourselves 

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

385 if skip == 0: # skip ourselves 

386 skip = 1 

387 kwargs["skip"] = skip 

388 

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

390 

391 @property 

392 def stats(self) -> Stats: 

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

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

395 

396 :note: 

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

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

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

400 

401 :return: 

402 :class:`Stats` 

403 """ 

404 

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

406 text = "" 

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

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

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

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

411 return text 

412 

413 if not self.parents: 

414 lines = self.repo.git.diff_tree( 

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

416 ).splitlines()[1:] 

417 text = process_lines(lines) 

418 else: 

419 lines = self.repo.git.diff( 

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

421 ).splitlines() 

422 text = process_lines(lines) 

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

424 

425 @property 

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

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

428 

429 :note: 

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

431 :attr:`trailers_dict`. 

432 

433 :return: 

434 Dictionary containing whitespace stripped trailer information. 

435 Only contains the latest instance of each trailer key. 

436 """ 

437 warnings.warn( 

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

439 DeprecationWarning, 

440 stacklevel=2, 

441 ) 

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

443 

444 @property 

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

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

447 

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

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

450 

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

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

453 

454 Valid message with trailer:: 

455 

456 Subject line 

457 

458 some body information 

459 

460 another information 

461 

462 key1: value1.1 

463 key1: value1.2 

464 key2 : value 2 with inner spaces 

465 

466 Returned list will look like this:: 

467 

468 [ 

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

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

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

472 ] 

473 

474 :return: 

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

476 """ 

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

478 

479 if not trailer: 

480 return [] 

481 

482 trailer_list = [] 

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

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

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

486 

487 return trailer_list 

488 

489 @classmethod 

490 def _interpret_trailers( 

491 cls, 

492 repo: "Repo", 

493 message: Union[str, bytes], 

494 trailer_args: Sequence[str], 

495 encoding: str = default_encoding, 

496 ) -> str: 

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

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

499 proc: Git.AutoInterrupt = repo.git.execute( 

500 cmd, 

501 as_process=True, 

502 istream=PIPE, 

503 ) 

504 try: 

505 stdout_bytes, _ = proc.communicate(message_bytes) 

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

507 finally: 

508 finalize_process(proc) 

509 

510 @property 

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

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

513 

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

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

516 

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

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

519 trailing whitespaces before they get saved into a dictionary. 

520 

521 Valid message with trailer:: 

522 

523 Subject line 

524 

525 some body information 

526 

527 another information 

528 

529 key1: value1.1 

530 key1: value1.2 

531 key2 : value 2 with inner spaces 

532 

533 Returned dictionary will look like this:: 

534 

535 { 

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

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

538 } 

539 

540 

541 :return: 

542 Dictionary containing whitespace stripped trailer information, mapping 

543 trailer keys to a list of their corresponding values. 

544 """ 

545 d = defaultdict(list) 

546 for key, val in self.trailers_list: 

547 d[key].append(val) 

548 return dict(d) 

549 

550 @classmethod 

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

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

553 

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

555 from our lighting fast object database. 

556 

557 :param proc: 

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

559 

560 :return: 

561 Iterator supplying :class:`Commit` objects 

562 """ 

563 

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

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

566 

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

568 # return hasattr(proc_or_stream, 'readline') 

569 

570 if hasattr(proc_or_stream, "wait"): 

571 proc_or_stream = cast(Popen, proc_or_stream) 

572 stream = proc_or_stream.stdout 

573 if stream is None: 

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

575 elif hasattr(proc_or_stream, "readline"): 

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

577 stream = proc_or_stream 

578 else: 

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

580 

581 readline = stream.readline 

582 while True: 

583 line = readline() 

584 if not line: 

585 break 

586 hexsha = line.strip() 

587 if len(hexsha) > 40: 

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

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

590 # END handle extra info 

591 

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

593 yield cls(repo, hex_to_bin(hexsha)) 

594 # END for each line in stream 

595 

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

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

598 if hasattr(proc_or_stream, "wait"): 

599 proc_or_stream = cast(Popen, proc_or_stream) 

600 finalize_process(proc_or_stream) 

601 

602 @classmethod 

603 def create_from_tree( 

604 cls, 

605 repo: "Repo", 

606 tree: Union[Tree, str], 

607 message: str, 

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

609 head: bool = False, 

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

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

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

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

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

615 ) -> "Commit": 

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

617 

618 :param repo: 

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

620 

621 :param tree: 

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

623 The tree of the new commit. 

624 

625 :param message: 

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

627 be converted to a string, in any case. 

628 

629 :param parent_commits: 

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

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

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

633 object. 

634 

635 :param head: 

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

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

638 lead to undesired results when diffing files. 

639 

640 :param author: 

641 The name of the author, optional. 

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

643 

644 :param committer: 

645 The name of the committer, optional. 

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

647 

648 :param author_date: 

649 The timestamp for the author field. 

650 

651 :param commit_date: 

652 The timestamp for the committer field. 

653 

654 :param trailers: 

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

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

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

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

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

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

661 

662 :return: 

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

664 

665 :note: 

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

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

668 for more information. 

669 """ 

670 if parent_commits is None: 

671 try: 

672 parent_commits = [repo.head.commit] 

673 except ValueError: 

674 # Empty repositories have no head commit. 

675 parent_commits = [] 

676 # END handle parent commits 

677 else: 

678 for p in parent_commits: 

679 if not isinstance(p, cls): 

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

681 # END check parent commit types 

682 # END if parent commits are unset 

683 

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

685 # Generally: 

686 # * Environment variables override configuration values. 

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

688 

689 # COMMITTER AND AUTHOR INFO 

690 cr = repo.config_reader() 

691 env = os.environ 

692 

693 committer = committer or Actor.committer(cr) 

694 author = author or Actor.author(cr) 

695 

696 # PARSE THE DATES 

697 unix_time = int(time()) 

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

699 offset = altzone if is_dst else timezone 

700 

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

702 if author_date: 

703 author_time, author_offset = parse_date(author_date) 

704 elif author_date_str: 

705 author_time, author_offset = parse_date(author_date_str) 

706 else: 

707 author_time, author_offset = unix_time, offset 

708 # END set author time 

709 

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

711 if commit_date: 

712 committer_time, committer_offset = parse_date(commit_date) 

713 elif committer_date_str: 

714 committer_time, committer_offset = parse_date(committer_date_str) 

715 else: 

716 committer_time, committer_offset = unix_time, offset 

717 # END set committer time 

718 

719 # Assume UTF-8 encoding. 

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

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

722 if not isinstance(conf_encoding, str): 

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

724 

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

726 # commit object is invalid. 

727 if isinstance(tree, str): 

728 tree = repo.tree(tree) 

729 # END tree conversion 

730 

731 # APPLY TRAILERS 

732 if trailers: 

733 trailer_args: List[str] = [] 

734 if isinstance(trailers, dict): 

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

736 trailer_args.append("--trailer") 

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

738 else: 

739 for key, val in trailers: 

740 trailer_args.append("--trailer") 

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

742 

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

744 # END apply trailers 

745 

746 # CREATE NEW COMMIT 

747 new_commit = cls( 

748 repo, 

749 cls.NULL_BIN_SHA, 

750 tree, 

751 author, 

752 author_time, 

753 author_offset, 

754 committer, 

755 committer_time, 

756 committer_offset, 

757 message, 

758 parent_commits, 

759 conf_encoding, 

760 ) 

761 

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

763 

764 if head: 

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

766 # well... 

767 import git.refs 

768 

769 try: 

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

771 except ValueError: 

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

773 # Happens on first commit. 

774 master = git.refs.Head.create( 

775 repo, 

776 repo.head.ref, 

777 new_commit, 

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

779 ) 

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

781 # END handle empty repositories 

782 # END advance head handling 

783 

784 return new_commit 

785 

786 # { Serializable Implementation 

787 

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

789 write = stream.write 

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

791 for p in self.parents: 

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

793 

794 a = self.author 

795 aname = a.name 

796 c = self.committer 

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

798 write( 

799 ( 

800 fmt 

801 % ( 

802 "author", 

803 aname, 

804 a.email, 

805 self.authored_date, 

806 altz_to_utctz_str(self.author_tz_offset), 

807 ) 

808 ).encode(self.encoding) 

809 ) 

810 

811 # Encode committer. 

812 aname = c.name 

813 write( 

814 ( 

815 fmt 

816 % ( 

817 "committer", 

818 aname, 

819 c.email, 

820 self.committed_date, 

821 altz_to_utctz_str(self.committer_tz_offset), 

822 ) 

823 ).encode(self.encoding) 

824 ) 

825 

826 if self.encoding != self.default_encoding: 

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

828 

829 try: 

830 if self.__getattribute__("gpgsig"): 

831 write(b"gpgsig") 

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

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

834 except AttributeError: 

835 pass 

836 

837 write(b"\n") 

838 

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

840 if isinstance(self.message, str): 

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

842 else: 

843 write(self.message) 

844 # END handle encoding 

845 return self 

846 

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

848 readline = stream.readline 

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

850 

851 self.parents = [] 

852 next_line = None 

853 while True: 

854 parent_line = readline() 

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

856 next_line = parent_line 

857 break 

858 # END abort reading parents 

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

860 # END for each parent line 

861 self.parents = tuple(self.parents) 

862 

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

864 # lines around. 

865 author_line = next_line 

866 committer_line = readline() 

867 

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

869 next_line = readline() 

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

871 next_line = readline() 

872 while next_line.startswith(b" "): 

873 next_line = readline() 

874 # END skip mergetags 

875 

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

877 # message. 

878 self.encoding = self.default_encoding 

879 self.gpgsig = "" 

880 

881 # Read headers. 

882 enc = next_line 

883 buf = enc.strip() 

884 while buf: 

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

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

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

888 sig_lines = [buf[buf.find(b" ") + 1 :] + b"\n"] 

889 is_next_header = False 

890 while True: 

891 sigbuf = readline() 

892 if not sigbuf: 

893 break 

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

895 buf = sigbuf.strip() 

896 is_next_header = True 

897 break 

898 sig_lines.append(sigbuf[1:]) 

899 # END read all signature 

900 self.gpgsig = b"".join(sig_lines).rstrip(b"\n").decode(self.encoding, "ignore") 

901 if is_next_header: 

902 continue 

903 buf = readline().strip() 

904 

905 # Decode the author's name. 

906 try: 

907 ( 

908 self.author, 

909 self.authored_date, 

910 self.author_tz_offset, 

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

912 except UnicodeDecodeError: 

913 _logger.error( 

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

915 author_line, 

916 self.encoding, 

917 exc_info=True, 

918 ) 

919 

920 try: 

921 ( 

922 self.committer, 

923 self.committed_date, 

924 self.committer_tz_offset, 

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

926 except UnicodeDecodeError: 

927 _logger.error( 

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

929 committer_line, 

930 self.encoding, 

931 exc_info=True, 

932 ) 

933 # END handle author's encoding 

934 

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

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

937 self.message = stream.read() 

938 try: 

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

940 except UnicodeDecodeError: 

941 _logger.error( 

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

943 self.message, 

944 self.encoding, 

945 exc_info=True, 

946 ) 

947 # END exception handling 

948 

949 return self 

950 

951 # } END serializable implementation 

952 

953 @property 

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

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

956 

957 Details on co-authors: 

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

959 

960 :return: 

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

962 """ 

963 co_authors = [] 

964 

965 if self.message: 

966 # Scan line by line instead of matching `(.*) <(.*?)>` across the whole 

967 # message. On a single trailer line that repeats " <" without ever closing 

968 # a ">", greedy backtracking over each " <" made the regex run in O(n^2) 

969 # time, so a large (fully attacker-controlled) commit message could stall 

970 # any caller of this property. A trailer is "Co-authored-by: <name> <email>" 

971 # with the email in the final angle brackets, so the name ends at the last 

972 # " <" and the line ends at ">". 

973 prefix = "Co-authored-by: " 

974 for line in str(self.message).split("\n"): 

975 if not line.startswith(prefix) or not line.endswith(">"): 

976 continue 

977 identity = line[len(prefix) :] 

978 separator = identity.rfind(" <") 

979 if separator == -1: 

980 continue 

981 co_authors.append(Actor(identity[:separator], identity[separator + 2 : -1])) 

982 

983 return co_authors