GitPython 3.1.45__py3-none-any.whl → 3.1.47__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
git/__init__.py CHANGED
@@ -86,7 +86,7 @@ __all__ = [
86
86
  "to_hex_sha",
87
87
  ]
88
88
 
89
- __version__ = '3.1.45'
89
+ __version__ = '3.1.47'
90
90
 
91
91
  from typing import Any, List, Optional, Sequence, TYPE_CHECKING, Tuple, Union
92
92
 
git/cmd.py CHANGED
@@ -368,8 +368,12 @@ class _AutoInterrupt:
368
368
  status = proc.wait() # Ensure the process goes away.
369
369
 
370
370
  self.status = self._status_code_if_terminate or status
371
- except OSError as ex:
372
- _logger.info("Ignored error after process had died: %r", ex)
371
+ except (OSError, AttributeError) as ex:
372
+ # On interpreter shutdown (notably on Windows), parts of the stdlib used by
373
+ # subprocess can already be torn down (e.g. `subprocess._winapi` becomes None),
374
+ # which can cause AttributeError during terminate(). In that case, we prefer
375
+ # to silently ignore to avoid noisy "Exception ignored in: __del__" messages.
376
+ _logger.info("Ignored error while terminating process: %r", ex)
373
377
  # END exception handling
374
378
 
375
379
  def __del__(self) -> None:
@@ -940,6 +944,21 @@ class Git(metaclass=_GitMeta):
940
944
  f"The `{protocol}::` protocol looks suspicious, use `allow_unsafe_protocols=True` to allow it."
941
945
  )
942
946
 
947
+ @classmethod
948
+ def _canonicalize_option_name(cls, option: str) -> str:
949
+ """Return the option name used for unsafe-option checks.
950
+
951
+ Examples:
952
+ ``"--upload-pack=/tmp/helper"`` -> ``"upload-pack"``
953
+ ``"upload_pack"`` -> ``"upload-pack"``
954
+ ``"--config core.filemode=false"`` -> ``"config"``
955
+ """
956
+ option_name = option.lstrip("-").split("=", 1)[0]
957
+ option_tokens = option_name.split(None, 1)
958
+ if not option_tokens:
959
+ return ""
960
+ return dashify(option_tokens[0])
961
+
943
962
  @classmethod
944
963
  def check_unsafe_options(cls, options: List[str], unsafe_options: List[str]) -> None:
945
964
  """Check for unsafe options.
@@ -947,15 +966,12 @@ class Git(metaclass=_GitMeta):
947
966
  Some options that are passed to ``git <command>`` can be used to execute
948
967
  arbitrary commands. These are blocked by default.
949
968
  """
950
- # Options can be of the form `foo`, `--foo bar`, or `--foo=bar`, so we need to
951
- # check if they start with "--foo" or if they are equal to "foo".
952
- bare_unsafe_options = [option.lstrip("-") for option in unsafe_options]
969
+ # Options can be of the form `foo`, `--foo`, `--foo bar`, or `--foo=bar`.
970
+ canonical_unsafe_options = {cls._canonicalize_option_name(option): option for option in unsafe_options}
953
971
  for option in options:
954
- for unsafe_option, bare_option in zip(unsafe_options, bare_unsafe_options):
955
- if option.startswith(unsafe_option) or option == bare_option:
956
- raise UnsafeOptionError(
957
- f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it."
958
- )
972
+ unsafe_option = canonical_unsafe_options.get(cls._canonicalize_option_name(option))
973
+ if unsafe_option is not None:
974
+ raise UnsafeOptionError(f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it.")
959
975
 
960
976
  AutoInterrupt: TypeAlias = _AutoInterrupt
961
977
 
@@ -1360,25 +1376,29 @@ class Git(metaclass=_GitMeta):
1360
1376
  if output_stream is None:
1361
1377
  stdout_value, stderr_value = communicate()
1362
1378
  # Strip trailing "\n".
1363
- if stdout_value.endswith(newline) and strip_newline_in_stdout: # type: ignore[arg-type]
1379
+ if stdout_value is not None and stdout_value.endswith(newline) and strip_newline_in_stdout: # type: ignore[arg-type]
1364
1380
  stdout_value = stdout_value[:-1]
1365
- if stderr_value.endswith(newline): # type: ignore[arg-type]
1381
+ if stderr_value is not None and stderr_value.endswith(newline): # type: ignore[arg-type]
1366
1382
  stderr_value = stderr_value[:-1]
1367
1383
 
1368
1384
  status = proc.returncode
1369
1385
  else:
1370
1386
  max_chunk_size = max_chunk_size if max_chunk_size and max_chunk_size > 0 else io.DEFAULT_BUFFER_SIZE
1371
- stream_copy(proc.stdout, output_stream, max_chunk_size)
1372
- stdout_value = proc.stdout.read()
1373
- stderr_value = proc.stderr.read()
1387
+ if proc.stdout is not None:
1388
+ stream_copy(proc.stdout, output_stream, max_chunk_size)
1389
+ stdout_value = proc.stdout.read()
1390
+ if proc.stderr is not None:
1391
+ stderr_value = proc.stderr.read()
1374
1392
  # Strip trailing "\n".
1375
- if stderr_value.endswith(newline): # type: ignore[arg-type]
1393
+ if stderr_value is not None and stderr_value.endswith(newline): # type: ignore[arg-type]
1376
1394
  stderr_value = stderr_value[:-1]
1377
1395
  status = proc.wait()
1378
1396
  # END stdout handling
1379
1397
  finally:
1380
- proc.stdout.close()
1381
- proc.stderr.close()
1398
+ if proc.stdout is not None:
1399
+ proc.stdout.close()
1400
+ if proc.stderr is not None:
1401
+ proc.stderr.close()
1382
1402
 
1383
1403
  if self.GIT_PYTHON_TRACE == "full":
1384
1404
  cmdstr = " ".join(redacted_command)
@@ -1568,7 +1588,7 @@ class Git(metaclass=_GitMeta):
1568
1588
 
1569
1589
  turns into::
1570
1590
 
1571
- git rev-list max-count 10 --header master
1591
+ git rev-list --max-count=10 --header=master
1572
1592
 
1573
1593
  :return:
1574
1594
  Same as :meth:`execute`. If no args are given, used :meth:`execute`'s
git/config.py CHANGED
@@ -66,7 +66,7 @@ _logger = logging.getLogger(__name__)
66
66
  CONFIG_LEVELS: ConfigLevels_Tup = ("system", "user", "global", "repository")
67
67
  """The configuration level of a configuration file."""
68
68
 
69
- CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch):(.+)\"")
69
+ CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch|hasconfig:remote\.\*\.url):(.+)\"")
70
70
  """Section pattern to detect conditional includes.
71
71
 
72
72
  See: https://git-scm.com/docs/git-config#_conditional_includes
@@ -549,11 +549,21 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
549
549
  :return:
550
550
  The list of paths, where each path is a tuple of (option, value).
551
551
  """
552
+
553
+ def _all_items(section: str) -> List[Tuple[str, str]]:
554
+ """Return all (key, value) pairs for a section, including duplicate keys."""
555
+ return [
556
+ (key, value)
557
+ for key, values in self._sections[section].items_all()
558
+ if key != "__name__"
559
+ for value in values
560
+ ]
561
+
552
562
  paths = []
553
563
 
554
564
  for section in self.sections():
555
565
  if section == "include":
556
- paths += self.items(section)
566
+ paths += _all_items(section)
557
567
 
558
568
  match = CONDITIONAL_INCLUDE_REGEXP.search(section)
559
569
  if match is None or self._repo is None:
@@ -574,12 +584,12 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
574
584
  if keyword.endswith("/i"):
575
585
  value = re.sub(
576
586
  r"[a-zA-Z]",
577
- lambda m: "[{}{}]".format(m.group().lower(), m.group().upper()),
587
+ lambda m: f"[{m.group().lower()!r}{m.group().upper()!r}]",
578
588
  value,
579
589
  )
580
590
  if self._repo.git_dir:
581
- if fnmatch.fnmatchcase(str(self._repo.git_dir), value):
582
- paths += self.items(section)
591
+ if fnmatch.fnmatchcase(os.fspath(self._repo.git_dir), value):
592
+ paths += _all_items(section)
583
593
 
584
594
  elif keyword == "onbranch":
585
595
  try:
@@ -589,8 +599,12 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
589
599
  continue
590
600
 
591
601
  if fnmatch.fnmatchcase(branch_name, value):
592
- paths += self.items(section)
593
-
602
+ paths += _all_items(section)
603
+ elif keyword == "hasconfig:remote.*.url":
604
+ for remote in self._repo.remotes:
605
+ if fnmatch.fnmatchcase(remote.url, value):
606
+ paths += _all_items(section)
607
+ break
594
608
  return paths
595
609
 
596
610
  def read(self) -> None: # type: ignore[override]
@@ -629,8 +643,6 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
629
643
  file_path = cast(IO[bytes], file_path)
630
644
  self._read(file_path, file_path.name)
631
645
  else:
632
- # Assume a path if it is not a file-object.
633
- file_path = cast(PathLike, file_path)
634
646
  try:
635
647
  with open(file_path, "rb") as fp:
636
648
  file_ok = True
@@ -764,7 +776,7 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
764
776
  if self.read_only:
765
777
  raise IOError("Cannot execute non-constant method %s.%s" % (self, method_name))
766
778
 
767
- def add_section(self, section: str) -> None:
779
+ def add_section(self, section: "cp._SectionName") -> None:
768
780
  """Assures added options will stay in order."""
769
781
  return super().add_section(section)
770
782
 
git/diff.py CHANGED
@@ -23,13 +23,14 @@ from typing import (
23
23
  List,
24
24
  Match,
25
25
  Optional,
26
+ Sequence,
26
27
  Tuple,
27
28
  TYPE_CHECKING,
28
29
  TypeVar,
29
30
  Union,
30
31
  cast,
31
32
  )
32
- from git.types import Literal, PathLike
33
+ from git.types import PathLike, Literal
33
34
 
34
35
  if TYPE_CHECKING:
35
36
  from subprocess import Popen
@@ -289,7 +290,7 @@ class DiffIndex(List[T_Diff]):
289
290
  The class improves the diff handling convenience.
290
291
  """
291
292
 
292
- change_type = ("A", "C", "D", "R", "M", "T")
293
+ change_type: Sequence[Literal["A", "C", "D", "R", "M", "T"]] = ("A", "C", "D", "R", "M", "T") # noqa: F821
293
294
  """Change type invariant identifying possible ways a blob can have changed:
294
295
 
295
296
  * ``A`` = Added
git/index/base.py CHANGED
@@ -407,7 +407,7 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
407
407
  r = str(self.repo.working_tree_dir)
408
408
  rs = r + os.sep
409
409
  for path in paths:
410
- abs_path = str(path)
410
+ abs_path = os.fspath(path)
411
411
  if not osp.isabs(abs_path):
412
412
  abs_path = osp.join(r, path)
413
413
  # END make absolute path
@@ -656,10 +656,10 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
656
656
  return path
657
657
  if self.repo.bare:
658
658
  raise InvalidGitRepositoryError("require non-bare repository")
659
- if not osp.normpath(str(path)).startswith(str(self.repo.working_tree_dir)):
659
+ if not osp.normpath(path).startswith(str(self.repo.working_tree_dir)):
660
660
  raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir))
661
661
  result = os.path.relpath(path, self.repo.working_tree_dir)
662
- if str(path).endswith(os.sep) and not result.endswith(os.sep):
662
+ if os.fspath(path).endswith(os.sep) and not result.endswith(os.sep):
663
663
  result += os.sep
664
664
  return result
665
665
 
@@ -1036,7 +1036,7 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1036
1036
  args.append("--")
1037
1037
 
1038
1038
  # Preprocess paths.
1039
- paths = self._items_to_rela_paths(items)
1039
+ paths = list(map(os.fspath, self._items_to_rela_paths(items))) # type: ignore[arg-type]
1040
1040
  removed_paths = self.repo.git.rm(args, paths, **kwargs).splitlines()
1041
1041
 
1042
1042
  # Process output to gain proper paths.
@@ -1133,6 +1133,7 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1133
1133
  author_date: Union[datetime.datetime, str, None] = None,
1134
1134
  commit_date: Union[datetime.datetime, str, None] = None,
1135
1135
  skip_hooks: bool = False,
1136
+ trailers: Union[None, "Dict[str, str]", "List[Tuple[str, str]]"] = None,
1136
1137
  ) -> Commit:
1137
1138
  """Commit the current default index file, creating a
1138
1139
  :class:`~git.objects.commit.Commit` object.
@@ -1169,6 +1170,7 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1169
1170
  committer=committer,
1170
1171
  author_date=author_date,
1171
1172
  commit_date=commit_date,
1173
+ trailers=trailers,
1172
1174
  )
1173
1175
  if not skip_hooks:
1174
1176
  run_commit_hook("post-commit", self)
@@ -1359,11 +1361,11 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1359
1361
  try:
1360
1362
  self.entries[(co_path, 0)]
1361
1363
  except KeyError:
1362
- folder = str(co_path)
1364
+ folder = co_path
1363
1365
  if not folder.endswith("/"):
1364
1366
  folder += "/"
1365
1367
  for entry in self.entries.values():
1366
- if str(entry.path).startswith(folder):
1368
+ if os.fspath(entry.path).startswith(folder):
1367
1369
  p = entry.path
1368
1370
  self._write_path_to_stdin(proc, p, p, make_exc, fprogress, read_from_stdout=False)
1369
1371
  checked_out_files.append(p)
git/index/fun.py CHANGED
@@ -36,7 +36,7 @@ from git.objects.fun import (
36
36
  )
37
37
  from git.util import IndexFileSHA1Writer, finalize_process
38
38
 
39
- from .typ import BaseIndexEntry, IndexEntry, CE_NAMEMASK, CE_STAGESHIFT
39
+ from .typ import CE_EXTENDED, BaseIndexEntry, IndexEntry, CE_NAMEMASK, CE_STAGESHIFT
40
40
  from .util import pack, unpack
41
41
 
42
42
  # typing -----------------------------------------------------------------------------
@@ -87,7 +87,7 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
87
87
  return
88
88
 
89
89
  env = os.environ.copy()
90
- env["GIT_INDEX_FILE"] = safe_decode(str(index.path))
90
+ env["GIT_INDEX_FILE"] = safe_decode(os.fspath(index.path))
91
91
  env["GIT_EDITOR"] = ":"
92
92
  cmd = [hp]
93
93
  try:
@@ -158,7 +158,7 @@ def write_cache(
158
158
  write = stream_sha.write
159
159
 
160
160
  # Header
161
- version = 2
161
+ version = 3 if any(entry.extended_flags for entry in entries) else 2
162
162
  write(b"DIRC")
163
163
  write(pack(">LL", version, len(entries)))
164
164
 
@@ -172,6 +172,8 @@ def write_cache(
172
172
  plen = len(path) & CE_NAMEMASK # Path length
173
173
  assert plen == len(path), "Path %s too long to fit into index" % entry.path
174
174
  flags = plen | (entry.flags & CE_NAMEMASK_INV) # Clear possible previous values.
175
+ if entry.extended_flags:
176
+ flags |= CE_EXTENDED
175
177
  write(
176
178
  pack(
177
179
  ">LLLLLL20sH",
@@ -185,6 +187,8 @@ def write_cache(
185
187
  flags,
186
188
  )
187
189
  )
190
+ if entry.extended_flags:
191
+ write(pack(">H", entry.extended_flags))
188
192
  write(path)
189
193
  real_size = (tell() - beginoffset + 8) & ~7
190
194
  write(b"\0" * ((beginoffset + real_size) - tell()))
@@ -206,8 +210,7 @@ def read_header(stream: IO[bytes]) -> Tuple[int, int]:
206
210
  unpacked = cast(Tuple[int, int], unpack(">LL", stream.read(4 * 2)))
207
211
  version, num_entries = unpacked
208
212
 
209
- # TODO: Handle version 3: extended data, see read-cache.c.
210
- assert version in (1, 2), "Unsupported git index version %i, only 1 and 2 are supported" % version
213
+ assert version in (1, 2, 3), "Unsupported git index version %i, only 1, 2, and 3 are supported" % version
211
214
  return version, num_entries
212
215
 
213
216
 
@@ -260,12 +263,15 @@ def read_cache(
260
263
  ctime = unpack(">8s", read(8))[0]
261
264
  mtime = unpack(">8s", read(8))[0]
262
265
  (dev, ino, mode, uid, gid, size, sha, flags) = unpack(">LLLLLL20sH", read(20 + 4 * 6 + 2))
266
+ extended_flags = 0
267
+ if flags & CE_EXTENDED:
268
+ extended_flags = unpack(">H", read(2))[0]
263
269
  path_size = flags & CE_NAMEMASK
264
270
  path = read(path_size).decode(defenc)
265
271
 
266
272
  real_size = (tell() - beginoffset + 8) & ~7
267
273
  read((beginoffset + real_size) - tell())
268
- entry = IndexEntry((mode, sha, flags, path, ctime, mtime, dev, ino, uid, gid, size))
274
+ entry = IndexEntry((mode, sha, flags, path, ctime, mtime, dev, ino, uid, gid, size, extended_flags))
269
275
  # entry_key would be the method to use, but we save the effort.
270
276
  entries[(path, entry.stage)] = entry
271
277
  count += 1
git/index/typ.py CHANGED
@@ -32,6 +32,9 @@ CE_EXTENDED = 0x4000
32
32
  CE_VALID = 0x8000
33
33
  CE_STAGESHIFT = 12
34
34
 
35
+ CE_EXT_SKIP_WORKTREE = 0x4000
36
+ CE_EXT_INTENT_TO_ADD = 0x2000
37
+
35
38
  # } END invariants
36
39
 
37
40
 
@@ -87,6 +90,8 @@ class BaseIndexEntryHelper(NamedTuple):
87
90
  uid: int = 0
88
91
  gid: int = 0
89
92
  size: int = 0
93
+ # version 3 extended flags, only when (flags & CE_EXTENDED) is set
94
+ extended_flags: int = 0
90
95
 
91
96
 
92
97
  class BaseIndexEntry(BaseIndexEntryHelper):
@@ -102,7 +107,7 @@ class BaseIndexEntry(BaseIndexEntryHelper):
102
107
  cls,
103
108
  inp_tuple: Union[
104
109
  Tuple[int, bytes, int, PathLike],
105
- Tuple[int, bytes, int, PathLike, bytes, bytes, int, int, int, int, int],
110
+ Tuple[int, bytes, int, PathLike, bytes, bytes, int, int, int, int, int, int],
106
111
  ],
107
112
  ) -> "BaseIndexEntry":
108
113
  """Override ``__new__`` to allow construction from a tuple for backwards
@@ -134,6 +139,14 @@ class BaseIndexEntry(BaseIndexEntryHelper):
134
139
  """
135
140
  return (self.flags & CE_STAGEMASK) >> CE_STAGESHIFT
136
141
 
142
+ @property
143
+ def skip_worktree(self) -> bool:
144
+ return (self.extended_flags & CE_EXT_SKIP_WORKTREE) > 0
145
+
146
+ @property
147
+ def intent_to_add(self) -> bool:
148
+ return (self.extended_flags & CE_EXT_INTENT_TO_ADD) > 0
149
+
137
150
  @classmethod
138
151
  def from_blob(cls, blob: Blob, stage: int = 0) -> "BaseIndexEntry":
139
152
  """:return: Fully equipped BaseIndexEntry at the given stage"""
@@ -179,7 +192,7 @@ class IndexEntry(BaseIndexEntry):
179
192
  Instance of type :class:`BaseIndexEntry`.
180
193
  """
181
194
  time = pack(">LL", 0, 0)
182
- return IndexEntry((base.mode, base.binsha, base.flags, base.path, time, time, 0, 0, 0, 0, 0))
195
+ return IndexEntry((base.mode, base.binsha, base.flags, base.path, time, time, 0, 0, 0, 0, 0)) # type: ignore[arg-type]
183
196
 
184
197
  @classmethod
185
198
  def from_blob(cls, blob: Blob, stage: int = 0) -> "IndexEntry":
@@ -198,5 +211,5 @@ class IndexEntry(BaseIndexEntry):
198
211
  0,
199
212
  0,
200
213
  blob.size,
201
- )
214
+ ) # type: ignore[arg-type]
202
215
  )
git/index/util.py CHANGED
@@ -15,7 +15,7 @@ from types import TracebackType
15
15
 
16
16
  # typing ----------------------------------------------------------------------
17
17
 
18
- from typing import Any, Callable, TYPE_CHECKING, Optional, Type
18
+ from typing import Any, Callable, TYPE_CHECKING, Optional, Type, cast
19
19
 
20
20
  from git.types import Literal, PathLike, _T
21
21
 
@@ -106,7 +106,7 @@ def git_working_dir(func: Callable[..., _T]) -> Callable[..., _T]:
106
106
  @wraps(func)
107
107
  def set_git_working_dir(self: "IndexFile", *args: Any, **kwargs: Any) -> _T:
108
108
  cur_wd = os.getcwd()
109
- os.chdir(str(self.repo.working_tree_dir))
109
+ os.chdir(cast(PathLike, self.repo.working_tree_dir))
110
110
  try:
111
111
  return func(self, *args, **kwargs)
112
112
  finally:
git/objects/blob.py CHANGED
@@ -6,6 +6,7 @@
6
6
  __all__ = ["Blob"]
7
7
 
8
8
  from mimetypes import guess_type
9
+ import os
9
10
  import sys
10
11
 
11
12
  if sys.version_info >= (3, 8):
@@ -44,5 +45,5 @@ class Blob(base.IndexObject):
44
45
  """
45
46
  guesses = None
46
47
  if self.path:
47
- guesses = guess_type(str(self.path))
48
+ guesses = guess_type(os.fspath(self.path))
48
49
  return guesses and guesses[0] or self.DEFAULT_MIME_TYPE
git/objects/commit.py CHANGED
@@ -450,14 +450,7 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
450
450
  :return:
451
451
  List containing key-value tuples of whitespace stripped trailer information.
452
452
  """
453
- cmd = ["git", "interpret-trailers", "--parse"]
454
- proc: Git.AutoInterrupt = self.repo.git.execute( # type: ignore[call-overload]
455
- cmd,
456
- as_process=True,
457
- istream=PIPE,
458
- )
459
- trailer: str = proc.communicate(str(self.message).encode())[0].decode("utf8")
460
- trailer = trailer.strip()
453
+ trailer = self._interpret_trailers(self.repo, self.message, ["--parse"], encoding=self.encoding).strip()
461
454
 
462
455
  if not trailer:
463
456
  return []
@@ -469,6 +462,27 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
469
462
 
470
463
  return trailer_list
471
464
 
465
+ @classmethod
466
+ def _interpret_trailers(
467
+ cls,
468
+ repo: "Repo",
469
+ message: Union[str, bytes],
470
+ trailer_args: Sequence[str],
471
+ encoding: str = default_encoding,
472
+ ) -> str:
473
+ message_bytes = message if isinstance(message, bytes) else message.encode(encoding, errors="strict")
474
+ cmd = [repo.git.GIT_PYTHON_GIT_EXECUTABLE, "interpret-trailers", *trailer_args]
475
+ proc: Git.AutoInterrupt = repo.git.execute( # type: ignore[call-overload]
476
+ cmd,
477
+ as_process=True,
478
+ istream=PIPE,
479
+ )
480
+ try:
481
+ stdout_bytes, _ = proc.communicate(message_bytes)
482
+ return stdout_bytes.decode(encoding, errors="strict")
483
+ finally:
484
+ finalize_process(proc)
485
+
472
486
  @property
473
487
  def trailers_dict(self) -> Dict[str, List[str]]:
474
488
  """Get the trailers of the message as a dictionary.
@@ -570,6 +584,7 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
570
584
  committer: Union[None, Actor] = None,
571
585
  author_date: Union[None, str, datetime.datetime] = None,
572
586
  commit_date: Union[None, str, datetime.datetime] = None,
587
+ trailers: Union[None, Dict[str, str], List[Tuple[str, str]]] = None,
573
588
  ) -> "Commit":
574
589
  """Commit the given tree, creating a :class:`Commit` object.
575
590
 
@@ -609,6 +624,14 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
609
624
  :param commit_date:
610
625
  The timestamp for the committer field.
611
626
 
627
+ :param trailers:
628
+ Optional trailer key-value pairs to append to the commit message.
629
+ Can be a dictionary mapping trailer keys to values, or a list of
630
+ ``(key, value)`` tuples (useful when the same key appears multiple
631
+ times, e.g. multiple ``Signed-off-by`` trailers). Trailers are
632
+ appended using ``git interpret-trailers``.
633
+ See :manpage:`git-interpret-trailers(1)`.
634
+
612
635
  :return:
613
636
  :class:`Commit` object representing the new commit.
614
637
 
@@ -678,6 +701,21 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
678
701
  tree = repo.tree(tree)
679
702
  # END tree conversion
680
703
 
704
+ # APPLY TRAILERS
705
+ if trailers:
706
+ trailer_args: List[str] = []
707
+ if isinstance(trailers, dict):
708
+ for key, val in trailers.items():
709
+ trailer_args.append("--trailer")
710
+ trailer_args.append(f"{key}: {val}")
711
+ else:
712
+ for key, val in trailers:
713
+ trailer_args.append("--trailer")
714
+ trailer_args.append(f"{key}: {val}")
715
+
716
+ message = cls._interpret_trailers(repo, str(message), trailer_args)
717
+ # END apply trailers
718
+
681
719
  # CREATE NEW COMMIT
682
720
  new_commit = cls(
683
721
  repo,
@@ -900,7 +938,7 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
900
938
  if self.message:
901
939
  results = re.findall(
902
940
  r"^Co-authored-by: (.*) <(.*?)>$",
903
- self.message,
941
+ str(self.message),
904
942
  re.MULTILINE,
905
943
  )
906
944
  for author in results:
@@ -66,7 +66,7 @@ from git.types import Commit_ish, PathLike, TBD
66
66
  if TYPE_CHECKING:
67
67
  from git.index import IndexFile
68
68
  from git.objects.commit import Commit
69
- from git.refs import Head
69
+ from git.refs import Head, RemoteReference
70
70
  from git.repo import Repo
71
71
 
72
72
  # -----------------------------------------------------------------------------
@@ -352,10 +352,10 @@ class Submodule(IndexObject, TraversableIterableObj):
352
352
  module_abspath_dir = osp.dirname(module_abspath)
353
353
  if not osp.isdir(module_abspath_dir):
354
354
  os.makedirs(module_abspath_dir)
355
- module_checkout_path = osp.join(str(repo.working_tree_dir), path)
355
+ module_checkout_path = osp.join(repo.working_tree_dir, path) # type: ignore[arg-type]
356
356
 
357
357
  if url.startswith("../"):
358
- remote_name = repo.active_branch.tracking_branch().remote_name
358
+ remote_name = cast("RemoteReference", repo.active_branch.tracking_branch()).remote_name
359
359
  repo_remote_url = repo.remote(remote_name).url
360
360
  url = os.path.join(repo_remote_url, url)
361
361
 
@@ -541,7 +541,7 @@ class Submodule(IndexObject, TraversableIterableObj):
541
541
  if sm.exists():
542
542
  # Reretrieve submodule from tree.
543
543
  try:
544
- sm = repo.head.commit.tree[str(path)]
544
+ sm = repo.head.commit.tree[os.fspath(path)]
545
545
  sm._name = name
546
546
  return sm
547
547
  except KeyError:
git/objects/tree.py CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  __all__ = ["TreeModifier", "Tree"]
7
7
 
8
+ import os
8
9
  import sys
9
10
 
10
11
  import git.diff as git_diff
@@ -230,7 +231,7 @@ class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
230
231
  raise TypeError("Unknown mode %o found in tree data for path '%s'" % (mode, path)) from e
231
232
  # END for each item
232
233
 
233
- def join(self, file: str) -> IndexObjUnion:
234
+ def join(self, file: PathLike) -> IndexObjUnion:
234
235
  """Find the named object in this tree's contents.
235
236
 
236
237
  :return:
@@ -241,6 +242,7 @@ class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
241
242
  If the given file or tree does not exist in this tree.
242
243
  """
243
244
  msg = "Blob or Tree named %r not found"
245
+ file = os.fspath(file)
244
246
  if "/" in file:
245
247
  tree = self
246
248
  item = self
@@ -269,7 +271,7 @@ class Tree(IndexObject, git_diff.Diffable, util.Traversable, util.Serializable):
269
271
  raise KeyError(msg % file)
270
272
  # END handle long paths
271
273
 
272
- def __truediv__(self, file: str) -> IndexObjUnion:
274
+ def __truediv__(self, file: PathLike) -> IndexObjUnion:
273
275
  """The ``/`` operator is another syntax for joining.
274
276
 
275
277
  See :meth:`join` for details.
git/refs/head.py CHANGED
@@ -22,7 +22,6 @@ from typing import Any, Sequence, TYPE_CHECKING, Union
22
22
  from git.types import Commit_ish, PathLike
23
23
 
24
24
  if TYPE_CHECKING:
25
- from git.objects import Commit
26
25
  from git.refs import RemoteReference
27
26
  from git.repo import Repo
28
27
 
@@ -44,9 +43,6 @@ class HEAD(SymbolicReference):
44
43
 
45
44
  __slots__ = ()
46
45
 
47
- # TODO: This can be removed once SymbolicReference.commit has static type hints.
48
- commit: "Commit"
49
-
50
46
  def __init__(self, repo: "Repo", path: PathLike = _HEAD_NAME) -> None:
51
47
  if path != self._HEAD_NAME:
52
48
  raise ValueError("HEAD instance must point to %r, got %r" % (self._HEAD_NAME, path))
@@ -149,7 +145,7 @@ class Head(Reference):
149
145
  k_config_remote_ref = "merge" # Branch to merge from remote.
150
146
 
151
147
  @classmethod
152
- def delete(cls, repo: "Repo", *heads: "Union[Head, str]", force: bool = False, **kwargs: Any) -> None:
148
+ def delete(cls, repo: "Repo", *heads: "Union[Head, str]", force: bool = False, **kwargs: Any) -> None: # type: ignore[override]
153
149
  """Delete the given heads.
154
150
 
155
151
  :param force:
git/refs/log.py CHANGED
@@ -145,7 +145,7 @@ class RefLogEntry(Tuple[str, str, Actor, Tuple[int, int], str]):
145
145
  actor = Actor._from_string(info[82 : email_end + 1])
146
146
  time, tz_offset = parse_date(info[email_end + 2 :]) # skipcq: PYL-W0621
147
147
 
148
- return RefLogEntry((oldhexsha, newhexsha, actor, (time, tz_offset), msg))
148
+ return RefLogEntry((oldhexsha, newhexsha, actor, (time, tz_offset), msg)) # type: ignore [arg-type]
149
149
 
150
150
 
151
151
  class RefLog(List[RefLogEntry], Serializable):
git/refs/reference.py CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  __all__ = ["Reference"]
5
5
 
6
+ import os
6
7
  from git.util import IterableObj, LazyMixin
7
8
 
8
9
  from .symbolic import SymbolicReference, T_References
@@ -65,7 +66,7 @@ class Reference(SymbolicReference, LazyMixin, IterableObj):
65
66
  If ``False``, you can provide any path.
66
67
  Otherwise the path must start with the default path prefix of this type.
67
68
  """
68
- if check_path and not str(path).startswith(self._common_path_default + "/"):
69
+ if check_path and not os.fspath(path).startswith(self._common_path_default + "/"):
69
70
  raise ValueError(f"Cannot instantiate {self.__class__.__name__!r} from path {path}")
70
71
  self.path: str # SymbolicReference converts to string at the moment.
71
72
  super().__init__(repo, path)