GitPython 3.1.56__py3-none-any.whl → 3.1.58__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.56'
89
+ __version__ = '3.1.58'
90
90
 
91
91
  from typing import Any, List, Optional, Sequence, TYPE_CHECKING, Tuple, Union
92
92
 
git/cmd.py CHANGED
@@ -654,6 +654,12 @@ class Git(metaclass=_GitMeta):
654
654
  "--upload-pack",
655
655
  ]
656
656
 
657
+ unsafe_git_pathspec_from_file_options = [
658
+ # Reads pathspecs from a caller-controlled file. Some commands include an
659
+ # unmatched pathspec in their error output, which can disclose the file.
660
+ "--pathspec-from-file",
661
+ ]
662
+
657
663
  def __getstate__(self) -> Dict[str, Any]:
658
664
  return slots_to_dict(self, exclude=self._excluded_)
659
665
 
@@ -1044,13 +1050,21 @@ class Git(metaclass=_GitMeta):
1044
1050
  values = value if isinstance(value, (list, tuple)) else (value,)
1045
1051
  if any(value is True or (value is not False and value is not None) for value in values):
1046
1052
  key = str(key)
1047
- options.append(f"-{key}" if len(key) == 1 else f"--{dashify(key)}")
1048
- if len(key) == 1 and split_single_char_options:
1053
+ if len(key) != 1:
1054
+ options.append(f"--{dashify(key)}")
1055
+ elif split_single_char_options:
1056
+ options.append(f"-{key}")
1049
1057
  options.extend(
1050
1058
  str(value)
1051
1059
  for value in values
1052
1060
  if value is not True and value not in (False, None) and str(value).startswith("-")
1053
1061
  )
1062
+ else:
1063
+ options.extend(
1064
+ f"-{key}" if value is True else f"-{key}{value}"
1065
+ for value in values
1066
+ if value is True or (value is not False and value is not None)
1067
+ )
1054
1068
  return options
1055
1069
 
1056
1070
  AutoInterrupt: TypeAlias = _AutoInterrupt
@@ -1435,6 +1449,10 @@ class Git(metaclass=_GitMeta):
1435
1449
  if as_process:
1436
1450
  return self.AutoInterrupt(proc, command)
1437
1451
 
1452
+ watchdog: Optional[threading.Timer] = None
1453
+ kill_check: Optional[threading.Event] = None
1454
+ timeout_error: Optional[Callable[[], Union[str, bytes]]] = None
1455
+
1438
1456
  if sys.platform != "win32" and kill_after_timeout is not None:
1439
1457
  # Help mypy figure out this is not None even when used inside communicate().
1440
1458
  timeout = kill_after_timeout
@@ -1460,6 +1478,7 @@ class Git(metaclass=_GitMeta):
1460
1478
  except OSError:
1461
1479
  pass
1462
1480
  # Tell the main routine that the process was killed.
1481
+ assert kill_check is not None
1463
1482
  kill_check.set()
1464
1483
  except OSError:
1465
1484
  # It is possible that the process gets completed in the duration
@@ -1467,23 +1486,25 @@ class Git(metaclass=_GitMeta):
1467
1486
  pass
1468
1487
  return
1469
1488
 
1489
+ def make_timeout_error() -> Union[str, bytes]:
1490
+ err = f'Timeout: the command "{" ".join(redacted_command)}" did not complete in {timeout:g} secs.'
1491
+ return err if universal_newlines else err.encode(defenc)
1492
+
1470
1493
  def communicate() -> Tuple[AnyStr, AnyStr]:
1494
+ assert watchdog is not None
1495
+ assert kill_check is not None
1471
1496
  watchdog.start()
1472
1497
  out, err = proc.communicate()
1473
1498
  watchdog.cancel()
1474
1499
  if kill_check.is_set():
1475
- err = 'Timeout: the command "%s" did not complete in %d secs.' % (
1476
- " ".join(redacted_command),
1477
- timeout,
1478
- )
1479
- if not universal_newlines:
1480
- err = err.encode(defenc)
1500
+ err = make_timeout_error()
1481
1501
  return out, err
1482
1502
 
1483
1503
  # END helpers
1484
1504
 
1485
1505
  kill_check = threading.Event()
1486
1506
  watchdog = threading.Timer(timeout, kill_process, args=(proc.pid,))
1507
+ timeout_error = make_timeout_error
1487
1508
  else:
1488
1509
  communicate = proc.communicate
1489
1510
 
@@ -1504,15 +1525,24 @@ class Git(metaclass=_GitMeta):
1504
1525
  status = proc.returncode
1505
1526
  else:
1506
1527
  max_chunk_size = max_chunk_size if max_chunk_size and max_chunk_size > 0 else io.DEFAULT_BUFFER_SIZE
1507
- if proc.stdout is not None:
1508
- stream_copy(proc.stdout, output_stream, max_chunk_size)
1509
- stdout_value = proc.stdout.read()
1510
- if proc.stderr is not None:
1511
- stderr_value = proc.stderr.read()
1528
+ if watchdog is not None:
1529
+ watchdog.start()
1530
+ try:
1531
+ if proc.stdout is not None:
1532
+ stream_copy(proc.stdout, output_stream, max_chunk_size)
1533
+ stdout_value = proc.stdout.read()
1534
+ if proc.stderr is not None:
1535
+ stderr_value = proc.stderr.read()
1536
+ status = proc.wait()
1537
+ finally:
1538
+ if watchdog is not None:
1539
+ watchdog.cancel()
1512
1540
  # Strip trailing "\n".
1513
1541
  if stderr_value is not None and stderr_value.endswith(newline): # type: ignore[arg-type]
1514
1542
  stderr_value = stderr_value[:-1]
1515
- status = proc.wait()
1543
+ if kill_check is not None and kill_check.is_set():
1544
+ assert timeout_error is not None
1545
+ stderr_value = timeout_error()
1516
1546
  # END stdout handling
1517
1547
  finally:
1518
1548
  if proc.stdout is not None:
git/config.py CHANGED
@@ -75,6 +75,9 @@ See: https://git-scm.com/docs/git-config#_conditional_includes
75
75
  UNSAFE_CONFIG_CHARS_RE = re.compile(r"[\r\n\x00]")
76
76
  """Characters that cannot be safely written in config names or values."""
77
77
 
78
+ VALID_CONFIG_OPTION_NAME_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
79
+ """Pattern for option names that can be written without changing config syntax."""
80
+
78
81
 
79
82
  class MetaParserBuilder(abc.ABCMeta): # noqa: B024
80
83
  """Utility class wrapping base-class methods into decorators that assure read-only
@@ -577,8 +580,12 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
577
580
 
578
581
  if keyword in ["gitdir", "gitdir/i"]:
579
582
  value = osp.expanduser(value)
583
+ git_dir = os.fspath(self._repo.git_dir) if self._repo.git_dir else None
584
+ if sys.platform == "win32":
585
+ git_dir = git_dir.replace("\\", "/") if git_dir else None
580
586
 
581
- if not any(value.startswith(s) for s in ["./", "/"]):
587
+ drive, _tail = osp.splitdrive(value)
588
+ if not drive and not any(value.startswith(s) for s in ["./", "/"]):
582
589
  value = "**/" + value
583
590
  if value.endswith("/"):
584
591
  value += "**"
@@ -590,9 +597,8 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
590
597
  lambda m: f"[{m.group().lower()!r}{m.group().upper()!r}]",
591
598
  value,
592
599
  )
593
- if self._repo.git_dir:
594
- if fnmatch.fnmatchcase(os.fspath(self._repo.git_dir), value):
595
- paths += _all_items(section)
600
+ if git_dir and fnmatch.fnmatchcase(git_dir, value):
601
+ paths += _all_items(section)
596
602
 
597
603
  elif keyword == "onbranch":
598
604
  try:
@@ -897,6 +903,8 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
897
903
  def _assure_config_name_safe(self, name: "cp._SectionName", label: str) -> None:
898
904
  if isinstance(name, str) and UNSAFE_CONFIG_CHARS_RE.search(name):
899
905
  raise ValueError("Git config %s names must not contain CR, LF, or NUL" % label)
906
+ if label == "option" and isinstance(name, str) and not VALID_CONFIG_OPTION_NAME_RE.fullmatch(name):
907
+ raise ValueError("Git config option names may contain only letters, digits, '-', '_', or '.'")
900
908
  if label == "section" and isinstance(name, str):
901
909
  in_quotes = False
902
910
  escaped = False
git/diff.py CHANGED
@@ -284,7 +284,10 @@ class Diffable:
284
284
  # END paths handling
285
285
 
286
286
  kwargs["as_process"] = True
287
- proc = diff_cmd(*self._process_diff_args(args), **kwargs)
287
+ args = self._process_diff_args(args)
288
+ if create_patch:
289
+ self.repo.git(c="diff.mnemonicPrefix=false")
290
+ proc = diff_cmd(*args, **kwargs)
288
291
 
289
292
  diff_method = Diff._index_from_patch_format if create_patch else Diff._index_from_raw_format
290
293
  index = diff_method(self.repo, proc)
git/index/base.py CHANGED
@@ -34,6 +34,8 @@ from git.util import (
34
34
  LockedFD,
35
35
  join_path_native,
36
36
  file_contents_ro,
37
+ _is_path_rooted,
38
+ _to_relative_path,
37
39
  to_native_path_linux,
38
40
  unbare_repo,
39
41
  to_bin_sha,
@@ -58,6 +60,7 @@ from typing import (
58
60
  Any,
59
61
  BinaryIO,
60
62
  Callable,
63
+ cast,
61
64
  Dict,
62
65
  Generator,
63
66
  IO,
@@ -130,6 +133,9 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
130
133
  index directly before operating on it using the git command.
131
134
  """
132
135
 
136
+ unsafe_git_checkout_index_options = ["--prefix"]
137
+ unsafe_git_read_tree_options = ["--index-output"]
138
+
133
139
  __slots__ = ("repo", "version", "entries", "_extension_data", "_file_path")
134
140
 
135
141
  _VERSION = 2
@@ -254,7 +260,12 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
254
260
 
255
261
  @post_clear_cache
256
262
  @default_index
257
- def merge_tree(self, rhs: Treeish, base: Union[None, Treeish] = None) -> "IndexFile":
263
+ def merge_tree(
264
+ self,
265
+ rhs: Treeish,
266
+ base: Union[None, Treeish] = None,
267
+ allow_unsafe_options: bool = False,
268
+ ) -> "IndexFile":
258
269
  """Merge the given `rhs` treeish into the current index, possibly taking
259
270
  a common base treeish into account.
260
271
 
@@ -268,6 +279,9 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
268
279
  Optional treeish reference pointing to the common base of `rhs` and this
269
280
  index which equals lhs.
270
281
 
282
+ :param allow_unsafe_options:
283
+ Allow options that may write to arbitrary paths.
284
+
271
285
  :return:
272
286
  self (containing the merge and possibly unmerged entries in case of
273
287
  conflicts)
@@ -278,6 +292,12 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
278
292
  yourself, you have to commit the changed index (or make a valid tree from
279
293
  it) and retry with a three-way :meth:`index.from_tree <from_tree>` call.
280
294
  """
295
+ if not allow_unsafe_options:
296
+ Git.check_unsafe_options(
297
+ options=Git._option_candidates([base, rhs]),
298
+ unsafe_options=self.unsafe_git_read_tree_options,
299
+ )
300
+
281
301
  # -i : ignore working tree status
282
302
  # --aggressive : handle more merge cases
283
303
  # -m : do an actual merge
@@ -322,7 +342,13 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
322
342
  return inst
323
343
 
324
344
  @classmethod
325
- def from_tree(cls, repo: "Repo", *treeish: Treeish, **kwargs: Any) -> "IndexFile":
345
+ def from_tree(
346
+ cls,
347
+ repo: "Repo",
348
+ *treeish: Treeish,
349
+ allow_unsafe_options: bool = False,
350
+ **kwargs: Any,
351
+ ) -> "IndexFile":
326
352
  R"""Merge the given treeish revisions into a new index which is returned.
327
353
  The original index will remain unaltered.
328
354
 
@@ -346,6 +372,9 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
346
372
  :param kwargs:
347
373
  Additional arguments passed to :manpage:`git-read-tree(1)`.
348
374
 
375
+ :param allow_unsafe_options:
376
+ Allow options that may write to arbitrary paths.
377
+
349
378
  :return:
350
379
  New :class:`IndexFile` instance. It will point to a temporary index location
351
380
  which does not exist anymore. If you intend to write such a merged Index,
@@ -363,6 +392,12 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
363
392
  if len(treeish) == 0 or len(treeish) > 3:
364
393
  raise ValueError("Please specify between 1 and 3 treeish, got %i" % len(treeish))
365
394
 
395
+ if not allow_unsafe_options:
396
+ Git.check_unsafe_options(
397
+ options=Git._option_candidates(treeish, kwargs),
398
+ unsafe_options=cls.unsafe_git_read_tree_options,
399
+ )
400
+
366
401
  arg_list: List[Union[Treeish, str]] = []
367
402
  # Ignore that the working tree and index possibly are out of date.
368
403
  if len(treeish) > 1:
@@ -653,16 +688,12 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
653
688
 
654
689
  :raise ValueError:
655
690
  """
656
- if not osp.isabs(path):
657
- return path
658
691
  if self.repo.bare:
659
- raise InvalidGitRepositoryError("require non-bare repository")
660
- if not osp.normpath(path).startswith(str(self.repo.working_tree_dir)):
661
- raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir))
662
- result = os.path.relpath(path, self.repo.working_tree_dir)
663
- if os.fspath(path).endswith(os.sep) and not result.endswith(os.sep):
664
- result += os.sep
665
- return result
692
+ drive, _tail = osp.splitdrive(os.fspath(path))
693
+ if drive or _is_path_rooted(path):
694
+ raise InvalidGitRepositoryError("paths with a drive or root require a non-bare repository")
695
+ return path
696
+ return _to_relative_path(cast(PathLike, self.repo.working_tree_dir), path)
666
697
 
667
698
  def _preprocess_add_items(
668
699
  self, items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]]
@@ -729,14 +760,17 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
729
760
  ) -> List[BaseIndexEntry]:
730
761
  entries_added: List[BaseIndexEntry] = []
731
762
  if path_rewriter:
763
+ working_tree_dir = self.repo.working_tree_dir
764
+ if working_tree_dir is None:
765
+ raise InvalidGitRepositoryError("Cannot rewrite paths without a working tree")
766
+ working_tree_dir = str(working_tree_dir)
732
767
  for path in paths:
733
768
  if osp.isabs(path):
734
769
  abspath = path
735
- gitrelative_path = path[len(str(self.repo.working_tree_dir)) + 1 :]
770
+ gitrelative_path = path[len(working_tree_dir) + 1 :]
736
771
  else:
737
772
  gitrelative_path = path
738
- if self.repo.working_tree_dir:
739
- abspath = osp.join(self.repo.working_tree_dir, gitrelative_path)
773
+ abspath = osp.join(working_tree_dir, gitrelative_path)
740
774
  # END obtain relative and absolute paths
741
775
 
742
776
  blob = Blob(
@@ -990,6 +1024,7 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
990
1024
  self,
991
1025
  items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]],
992
1026
  working_tree: bool = False,
1027
+ allow_unsafe_options: bool = False,
993
1028
  **kwargs: Any,
994
1029
  ) -> List[str]:
995
1030
  R"""Remove the given items from the index and optionally from the working tree
@@ -1020,6 +1055,10 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1020
1055
  physically removing the respective file. This may fail if there are
1021
1056
  uncommitted changes in it.
1022
1057
 
1058
+ :param allow_unsafe_options:
1059
+ Allow unsafe options such as ``--pathspec-from-file`` to be passed to
1060
+ :manpage:`git-rm(1)`.
1061
+
1023
1062
  :param kwargs:
1024
1063
  Additional keyword arguments to be passed to :manpage:`git-rm(1)`, such as
1025
1064
  ``r`` to allow recursive removal.
@@ -1031,6 +1070,11 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1031
1070
  This is interesting to know in case you have provided a directory or globs.
1032
1071
  Paths are relative to the repository.
1033
1072
  """
1073
+ if not allow_unsafe_options:
1074
+ Git.check_unsafe_options(
1075
+ options=Git._option_candidates([], kwargs),
1076
+ unsafe_options=Git.unsafe_git_pathspec_from_file_options,
1077
+ )
1034
1078
  args = []
1035
1079
  if not working_tree:
1036
1080
  args.append("--cached")
@@ -1212,6 +1256,7 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1212
1256
  paths: Union[None, Iterable[PathLike]] = None,
1213
1257
  force: bool = False,
1214
1258
  fprogress: Callable = lambda *args: None,
1259
+ allow_unsafe_options: bool = False,
1215
1260
  **kwargs: Any,
1216
1261
  ) -> Union[None, Iterator[PathLike], Sequence[PathLike]]:
1217
1262
  """Check out the given paths or all files from the version known to the index
@@ -1238,6 +1283,9 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1238
1283
  no explicit paths are given. Otherwise progress information will be send
1239
1284
  prior and after a file has been checked out.
1240
1285
 
1286
+ :param allow_unsafe_options:
1287
+ Allow unsafe options, such as ``--prefix``.
1288
+
1241
1289
  :param kwargs:
1242
1290
  Additional arguments to be passed to :manpage:`git-checkout-index(1)`.
1243
1291
 
@@ -1261,6 +1309,12 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1261
1309
  i.e. if you want :manpage:`git-checkout(1)`-like behaviour, use
1262
1310
  ``head.checkout`` instead of ``index.checkout``.
1263
1311
  """
1312
+ if not allow_unsafe_options:
1313
+ Git.check_unsafe_options(
1314
+ options=Git._option_candidates([], kwargs),
1315
+ unsafe_options=self.unsafe_git_checkout_index_options,
1316
+ )
1317
+
1264
1318
  args = ["--index"]
1265
1319
  if force:
1266
1320
  args.append("--force")
@@ -1402,6 +1456,7 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1402
1456
  working_tree: bool = False,
1403
1457
  paths: Union[None, Iterable[PathLike]] = None,
1404
1458
  head: bool = False,
1459
+ allow_unsafe_options: bool = False,
1405
1460
  **kwargs: Any,
1406
1461
  ) -> "IndexFile":
1407
1462
  """Reset the index to reflect the tree at the given commit. This will not adjust
@@ -1433,6 +1488,9 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1433
1488
  The paths need to exist at the commit, otherwise an exception will be
1434
1489
  raised.
1435
1490
 
1491
+ :param allow_unsafe_options:
1492
+ Allow options that may write to arbitrary paths.
1493
+
1436
1494
  :param kwargs:
1437
1495
  Additional keyword arguments passed to :manpage:`git-reset(1)`.
1438
1496
 
@@ -1449,15 +1507,15 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1449
1507
  """
1450
1508
  # What we actually want to do is to merge the tree into our existing index,
1451
1509
  # which is what git-read-tree does.
1452
- new_inst = type(self).from_tree(self.repo, commit)
1510
+ new_inst = type(self).from_tree(self.repo, commit, allow_unsafe_options=allow_unsafe_options)
1453
1511
  if not paths:
1454
1512
  self.entries = new_inst.entries
1455
1513
  else:
1456
1514
  nie = new_inst.entries
1457
1515
  for path in paths:
1458
1516
  path = self._to_relative_path(path)
1517
+ key = entry_key(path, 0)
1459
1518
  try:
1460
- key = entry_key(path, 0)
1461
1519
  self.entries[key] = nie[key]
1462
1520
  except KeyError:
1463
1521
  # If key is not in theirs, it mustn't be in ours.
@@ -1547,6 +1605,8 @@ class IndexFile(LazyMixin, git_diff.Diffable, Serializable):
1547
1605
  args.extend(paths)
1548
1606
 
1549
1607
  kwargs["as_process"] = True
1608
+ if create_patch:
1609
+ self.repo.git(c="diff.mnemonicPrefix=false")
1550
1610
  proc = self.repo.git.diff(*args, **kwargs)
1551
1611
 
1552
1612
  diff_method = (
git/index/fun.py CHANGED
@@ -26,7 +26,7 @@ import sys
26
26
  from gitdb.base import IStream
27
27
  from gitdb.typ import str_tree_type
28
28
 
29
- from git.cmd import handle_process_output, safer_popen
29
+ from git.cmd import Git, handle_process_output, safer_popen
30
30
  from git.compat import defenc, force_bytes, force_text, safe_decode
31
31
  from git.exc import HookExecutionError, UnmergedEntriesError
32
32
  from git.objects.fun import (
@@ -79,6 +79,99 @@ def _has_file_extension(path: str) -> str:
79
79
  return osp.splitext(path)[1]
80
80
 
81
81
 
82
+ def _is_in_windows_system_root(path: str) -> bool:
83
+ """Return whether ``path`` is inside the Windows installation directory."""
84
+ system_root = os.environ.get("SystemRoot")
85
+ if not system_root:
86
+ return False
87
+
88
+ system_root = osp.normcase(osp.realpath(system_root))
89
+ path = osp.normcase(osp.realpath(path))
90
+ try:
91
+ return osp.commonpath((system_root, path)) == system_root
92
+ except ValueError:
93
+ # Paths on different drives have no common path on Windows.
94
+ return False
95
+
96
+
97
+ def _which_from_path(command: str) -> Union[str, None]:
98
+ """Resolve ``command`` from PATH, excluding the Windows installation."""
99
+ for directory in os.get_exec_path():
100
+ # Unlike POSIX, Windows does not define an empty PATH entry as the current
101
+ # directory. Skip it rather than letting abspath() turn it into one.
102
+ if not directory:
103
+ continue
104
+ directory = osp.abspath(directory)
105
+ candidate = osp.join(directory, command)
106
+ # SystemRoot contains the WSL launcher stubs. They are valid executables but
107
+ # not suitable for running a Windows Git hook: the hook path and environment
108
+ # were prepared for Git for Windows, and WSL may have no distribution at all.
109
+ if _is_in_windows_system_root(candidate):
110
+ continue
111
+ if osp.isfile(candidate) and os.access(candidate, os.X_OK):
112
+ return candidate
113
+ return None
114
+
115
+
116
+ _GIT_FOR_WINDOWS_PREFIXES = ("mingw64", "mingw32", "clangarm64", "clang64", "clang32", "ucrt64")
117
+
118
+
119
+ def _git_for_windows_root() -> Union[str, None]:
120
+ """Infer a standard Git for Windows root from GitPython's selected executable."""
121
+ git_executable = os.fspath(Git.GIT_PYTHON_GIT_EXECUTABLE or Git.git_exec_name)
122
+ if osp.dirname(git_executable):
123
+ # CreateProcess resolves a relative executable path containing a directory
124
+ # from the parent process cwd, even when Popen supplies a different child cwd.
125
+ git_executable = osp.abspath(git_executable)
126
+ else:
127
+ # GitPython deliberately retains a bare executable name so later PATH changes
128
+ # affect Git commands. Resolve it with the same PATH snapshot used for Bash.
129
+ names = (git_executable,) if _has_file_extension(git_executable) else (git_executable, f"{git_executable}.exe")
130
+ for name in names:
131
+ resolved = _which_from_path(name)
132
+ if resolved is not None:
133
+ git_executable = resolved
134
+ break
135
+ else:
136
+ git_executable = ""
137
+ if not git_executable:
138
+ return None
139
+ if osp.basename(git_executable).lower() not in ("git", "git.exe"):
140
+ return None
141
+
142
+ executable_dir = osp.dirname(git_executable)
143
+ directory_name = osp.basename(executable_dir).lower()
144
+ if directory_name == "cmd":
145
+ # The normal system-wide PATH entry is <git-root>/cmd.
146
+ return osp.dirname(executable_dir)
147
+ if directory_name == "bin":
148
+ prefix = osp.dirname(executable_dir)
149
+ if osp.basename(prefix).lower() in _GIT_FOR_WINDOWS_PREFIXES:
150
+ # Git Bash commonly exposes <git-root>/<platform>/bin/git.exe.
151
+ return osp.dirname(prefix)
152
+ if osp.basename(prefix).lower() != "usr":
153
+ # An explicitly configured Git may be the root-level bin/git.exe. Do
154
+ # not make the same inference from usr/bin: unlike the recognized
155
+ # platform prefixes, "usr" has no reliably bounded parent layout.
156
+ return prefix
157
+ return None
158
+
159
+
160
+ def _git_for_windows_bash() -> Union[str, None]:
161
+ """Return Bash from the Git for Windows installation selected by GitPython."""
162
+ git_root = _git_for_windows_root()
163
+ if git_root is None:
164
+ return None
165
+
166
+ # Match gix-path's precedence: prefer the lightweight bin shim, then the
167
+ # underlying usr/bin executable. Both belong to the same installation as Git.
168
+ for relative_path in ("bin/bash.exe", "usr/bin/bash.exe"):
169
+ candidate = osp.join(git_root, *relative_path.split("/"))
170
+ if osp.isfile(candidate) and os.access(candidate, os.X_OK):
171
+ return candidate
172
+ return None
173
+
174
+
82
175
  def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
83
176
  """Run the commit hook of the given name. Silently ignore hooks that do not exist.
84
177
 
@@ -112,7 +205,12 @@ def run_commit_hook(name: str, index: "IndexFile", *args: str) -> None:
112
205
  # an absolute path in this form, although a relative path is preferable
113
206
  # because it also works with the Windows Subsystem for Linux wrapper.
114
207
  bash_hp = hp
115
- cmd = ["bash.exe", Path(bash_hp).as_posix()]
208
+ # Prefer Bash associated with GitPython's selected Git installation. If
209
+ # that layout is not recognized, use an explicitly configured non-system
210
+ # PATH entry. Preserve the bare fallback for installations that previously
211
+ # relied on WSL or another CreateProcess-resolved Bash.
212
+ bash_executable = _git_for_windows_bash() or _which_from_path("bash.exe") or "bash.exe"
213
+ cmd = [bash_executable, Path(bash_hp).as_posix()]
116
214
 
117
215
  process = safer_popen(
118
216
  cmd + list(args),
git/objects/commit.py CHANGED
@@ -575,11 +575,14 @@ class Commit(base.Object, TraversableIterableObj, Diffable, Serializable):
575
575
 
576
576
  if hasattr(proc_or_stream, "wait"):
577
577
  proc_or_stream = cast(Popen, proc_or_stream)
578
- if proc_or_stream.stdout is not None:
579
- stream = proc_or_stream.stdout
578
+ stream = proc_or_stream.stdout
579
+ if stream is None:
580
+ raise ValueError("Process has no stdout stream")
580
581
  elif hasattr(proc_or_stream, "readline"):
581
582
  proc_or_stream = cast(IO, proc_or_stream) # type: ignore[redundant-cast]
582
583
  stream = proc_or_stream
584
+ else:
585
+ raise TypeError("Expected a process or stream")
583
586
 
584
587
  readline = stream.readline
585
588
  while True:
git/objects/fun.py CHANGED
@@ -115,11 +115,11 @@ def tree_entries_from_data(data: bytes) -> List[EntryTup]:
115
115
  # Default encoding for strings in git is UTF-8.
116
116
  # Only use the respective unicode object if the byte stream was encoded.
117
117
  name_bytes = data[ns:i]
118
- name = safe_decode(name_bytes)
118
+ name = safe_decode(bytes(name_bytes))
119
119
 
120
120
  # Byte is NULL, get next 20.
121
121
  i += 1
122
- sha = data[i : i + 20]
122
+ sha = bytes(data[i : i + 20])
123
123
  i = i + 20
124
124
  out.append((sha, mode, name))
125
125
  # END for each byte in data stream
@@ -6,6 +6,7 @@ __all__ = ["Submodule", "UpdateProgress"]
6
6
  import gc
7
7
  from io import BytesIO
8
8
  import logging
9
+ import ntpath
9
10
  import os
10
11
  import os.path as osp
11
12
  import stat
@@ -28,6 +29,7 @@ from git.objects.util import TraversableIterableObj
28
29
  from git.util import (
29
30
  IterableList,
30
31
  RemoteProgress,
32
+ _to_relative_path,
31
33
  join_path_native,
32
34
  rmtree,
33
35
  to_native_path_linux,
@@ -302,8 +304,20 @@ class Submodule(IndexObject, TraversableIterableObj):
302
304
  parser.set_submodule(self)
303
305
  return SectionConstraint(parser, sm_section(self.name))
304
306
 
307
+ @classmethod
308
+ def _validated_name(cls, name: str) -> str:
309
+ if (
310
+ not name
311
+ or name.startswith(("/", "\\"))
312
+ or ntpath.splitdrive(name)[0]
313
+ or ".." in name.replace("\\", "/").split("/")
314
+ ):
315
+ raise ValueError("Invalid submodule name %r" % name)
316
+ return name
317
+
305
318
  @classmethod
306
319
  def _module_abspath(cls, parent_repo: "Repo", path: PathLike, name: str) -> PathLike:
320
+ name = cls._validated_name(name)
307
321
  if cls._need_gitfile_submodules(parent_repo.git):
308
322
  return osp.join(parent_repo.git_dir, "modules", name)
309
323
  if parent_repo.working_tree_dir:
@@ -379,23 +393,14 @@ class Submodule(IndexObject, TraversableIterableObj):
379
393
  :raise ValueError:
380
394
  If path is not contained in the parent repository's working tree.
381
395
  """
382
- path = to_native_path_linux(path)
396
+ if parent_repo.working_tree_dir:
397
+ path = _to_relative_path(parent_repo.working_tree_dir, path)
398
+ else:
399
+ path = to_native_path_linux(path)
383
400
  if path.endswith("/"):
384
401
  path = path[:-1]
385
- # END handle trailing slash
386
-
387
- if osp.isabs(path) and parent_repo.working_tree_dir:
388
- working_tree_linux = to_native_path_linux(parent_repo.working_tree_dir)
389
- if not path.startswith(working_tree_linux):
390
- raise ValueError(
391
- "Submodule checkout path '%s' needs to be within the parents repository at '%s'"
392
- % (working_tree_linux, path)
393
- )
394
- path = path[len(working_tree_linux.rstrip("/")) + 1 :]
395
- if not path:
396
- raise ValueError("Absolute submodule path '%s' didn't yield a valid relative path" % path)
397
- # END verify converted relative path makes sense
398
- # END convert to a relative path
402
+ if not path or path == ".":
403
+ raise ValueError("Submodule checkout path must not be the repository root")
399
404
 
400
405
  return path
401
406
 
@@ -523,6 +528,7 @@ class Submodule(IndexObject, TraversableIterableObj):
523
528
  raise InvalidGitRepositoryError("Cannot add submodules to bare repositories")
524
529
  # END handle bare repos
525
530
 
531
+ cls._validated_name(name)
526
532
  path = cls._to_relative_path(repo, path)
527
533
 
528
534
  # Ensure we never put backslashes into the URL, as might happen on Windows.
@@ -734,9 +740,7 @@ class Submodule(IndexObject, TraversableIterableObj):
734
740
  prefix = "DRY-RUN: "
735
741
  # END handle prefix
736
742
 
737
- # To keep things plausible in dry-run mode.
738
- if dry_run:
739
- mrepo = None
743
+ mrepo = None
740
744
  # END init mrepo
741
745
 
742
746
  def fetch_remotes(module_repo: "Repo") -> None:
@@ -771,6 +775,8 @@ class Submodule(IndexObject, TraversableIterableObj):
771
775
  # END fetch new data
772
776
 
773
777
  try:
778
+ self._validated_name(self.name)
779
+
774
780
  # ENSURE REPO IS PRESENT AND UP-TO-DATE
775
781
  #######################################
776
782
  try:
@@ -871,6 +877,7 @@ class Submodule(IndexObject, TraversableIterableObj):
871
877
  #############################
872
878
  binsha = self.binsha
873
879
  hexsha = self.hexsha
880
+ is_detached = False
874
881
  if mrepo is not None:
875
882
  # mrepo is only set if we are not in dry-run mode or if the module
876
883
  # existed.
@@ -1020,6 +1027,7 @@ class Submodule(IndexObject, TraversableIterableObj):
1020
1027
  raise ValueError("You must specify to move at least the module or the configuration of the submodule")
1021
1028
  # END handle input
1022
1029
 
1030
+ self._validated_name(self.name)
1023
1031
  module_checkout_path = self._to_relative_path(self.repo, module_path)
1024
1032
 
1025
1033
  # VERIFY DESTINATION
@@ -1160,6 +1168,7 @@ class Submodule(IndexObject, TraversableIterableObj):
1160
1168
  raise ValueError("Need to specify to delete at least the module, or the configuration")
1161
1169
  # END handle parameters
1162
1170
 
1171
+ self._validated_name(self.name)
1163
1172
  # Recursively remove children of this submodule.
1164
1173
  nc = 0
1165
1174
  for csm in self.children():
@@ -1213,6 +1222,7 @@ class Submodule(IndexObject, TraversableIterableObj):
1213
1222
  for remote in mod.remotes:
1214
1223
  num_branches_with_new_commits = 0
1215
1224
  rrefs = remote.refs
1225
+ rref = None
1216
1226
  for rref in rrefs:
1217
1227
  num_branches_with_new_commits += len(mod.git.cherry(rref)) != 0
1218
1228
  # END for each remote ref
@@ -1416,6 +1426,9 @@ class Submodule(IndexObject, TraversableIterableObj):
1416
1426
  if self.name == new_name:
1417
1427
  return self
1418
1428
 
1429
+ self._validated_name(self.name)
1430
+ self._validated_name(new_name)
1431
+
1419
1432
  # .git/config
1420
1433
  with self.repo.config_writer() as pw:
1421
1434
  # As we ourselves didn't write anything about submodules into the parent
@@ -1463,6 +1476,7 @@ class Submodule(IndexObject, TraversableIterableObj):
1463
1476
  If a repository was not available.
1464
1477
  This could also mean that it was not yet initialized.
1465
1478
  """
1479
+ self._validated_name(self.name)
1466
1480
  module_checkout_abspath = self.abspath
1467
1481
  try:
1468
1482
  repo = git.Repo(module_checkout_abspath)
@@ -7,6 +7,7 @@ import logging
7
7
 
8
8
  import git
9
9
  from git.exc import InvalidGitRepositoryError
10
+ from git.util import IterableList
10
11
 
11
12
  from .base import Submodule, UpdateProgress
12
13
  from .util import find_first_remote_branch
@@ -19,7 +20,6 @@ from git.types import Commit_ish
19
20
 
20
21
  if TYPE_CHECKING:
21
22
  from git.repo import Repo
22
- from git.util import IterableList
23
23
 
24
24
  # ----------------------------------------------------------------------------
25
25
 
@@ -162,6 +162,7 @@ class RootModule(Submodule):
162
162
  prefix = "DRY-RUN: "
163
163
 
164
164
  repo = self.repo
165
+ sms: "IterableList[Submodule]" = IterableList("name")
165
166
 
166
167
  try:
167
168
  # SETUP BASE COMMIT
@@ -182,7 +183,7 @@ class RootModule(Submodule):
182
183
  # END handle previous commit
183
184
 
184
185
  psms: "IterableList[Submodule]" = self.list_items(repo, parent_commit=previous_commit)
185
- sms: "IterableList[Submodule]" = self.list_items(repo)
186
+ sms = self.list_items(repo)
186
187
  spsms = set(psms)
187
188
  ssms = set(sms)
188
189
 
git/refs/head.py CHANGED
@@ -19,6 +19,7 @@ from .symbolic import SymbolicReference
19
19
 
20
20
  from typing import Any, Sequence, TYPE_CHECKING, Union
21
21
 
22
+ from git.cmd import Git
22
23
  from git.types import Commit_ish, PathLike
23
24
 
24
25
  if TYPE_CHECKING:
@@ -62,6 +63,7 @@ class HEAD(SymbolicReference):
62
63
  index: bool = True,
63
64
  working_tree: bool = False,
64
65
  paths: Union[PathLike, Sequence[PathLike], None] = None,
66
+ allow_unsafe_options: bool = False,
65
67
  **kwargs: Any,
66
68
  ) -> "HEAD":
67
69
  """Reset our HEAD to the given commit optionally synchronizing the index and
@@ -84,12 +86,21 @@ class HEAD(SymbolicReference):
84
86
  Single path or list of paths relative to the git root directory
85
87
  that are to be reset. This allows to partially reset individual files.
86
88
 
89
+ :param allow_unsafe_options:
90
+ Allow unsafe options such as ``--pathspec-from-file`` to be passed to
91
+ :manpage:`git-reset(1)`.
92
+
87
93
  :param kwargs:
88
94
  Additional arguments passed to :manpage:`git-reset(1)`.
89
95
 
90
96
  :return:
91
97
  self
92
98
  """
99
+ if not allow_unsafe_options:
100
+ Git.check_unsafe_options(
101
+ options=Git._option_candidates([commit], kwargs),
102
+ unsafe_options=Git.unsafe_git_pathspec_from_file_options,
103
+ )
93
104
  mode: Union[str, None]
94
105
  mode = "--soft"
95
106
  if index:
@@ -234,7 +245,12 @@ class Head(Reference):
234
245
  self.path = "%s/%s" % (self._common_path_default, new_path)
235
246
  return self
236
247
 
237
- def checkout(self, force: bool = False, **kwargs: Any) -> Union["HEAD", "Head"]:
248
+ def checkout(
249
+ self,
250
+ force: bool = False,
251
+ allow_unsafe_options: bool = False,
252
+ **kwargs: Any,
253
+ ) -> Union["HEAD", "Head"]:
238
254
  """Check out this head by setting the HEAD to this reference, by updating the
239
255
  index to reflect the tree we point to and by updating the working tree to
240
256
  reflect the latest index.
@@ -246,6 +262,10 @@ class Head(Reference):
246
262
  If ``False``, :exc:`~git.exc.GitCommandError` will be raised in that
247
263
  situation.
248
264
 
265
+ :param allow_unsafe_options:
266
+ Allow unsafe options such as ``--pathspec-from-file`` to be passed to
267
+ :manpage:`git-checkout(1)`.
268
+
249
269
  :param kwargs:
250
270
  Additional keyword arguments to be passed to git checkout, e.g.
251
271
  ``b="new_branch"`` to create a new branch at the given spot.
@@ -261,6 +281,11 @@ class Head(Reference):
261
281
  the HEAD detached which is allowed and possible, but remains a special state
262
282
  that some tools might not be able to handle.
263
283
  """
284
+ if not allow_unsafe_options:
285
+ Git.check_unsafe_options(
286
+ options=Git._option_candidates([], kwargs),
287
+ unsafe_options=Git.unsafe_git_pathspec_from_file_options,
288
+ )
264
289
  kwargs["f"] = force
265
290
  if kwargs["f"] is False:
266
291
  kwargs.pop("f")
git/refs/log.py CHANGED
@@ -269,6 +269,7 @@ class RefLog(List[RefLogEntry], Serializable):
269
269
  return RefLogEntry.from_line(fp.readlines()[index].strip())
270
270
  # Read until index is reached.
271
271
 
272
+ line = b""
272
273
  for i in range(index + 1):
273
274
  line = fp.readline()
274
275
  if not line:
git/refs/symbolic.py CHANGED
@@ -119,7 +119,7 @@ class SymbolicReference:
119
119
  common_path = os.path.commonpath([base_path, abs_path])
120
120
  except ValueError as e:
121
121
  raise ValueError("Reference path %r escapes the repository" % path) from e
122
- if os.path.normcase(common_path) != os.path.normcase(base_path):
122
+ if common_path != base_path:
123
123
  raise ValueError("Reference path %r escapes the repository" % path)
124
124
  return abs_path
125
125
 
git/refs/tag.py CHANGED
@@ -16,6 +16,7 @@ from .reference import Reference
16
16
 
17
17
  from typing import Any, TYPE_CHECKING, Type, Union
18
18
 
19
+ from git.cmd import Git
19
20
  from git.types import AnyGitObject, PathLike
20
21
 
21
22
  if TYPE_CHECKING:
@@ -42,6 +43,8 @@ class TagReference(Reference):
42
43
 
43
44
  __slots__ = ()
44
45
 
46
+ unsafe_git_tag_options = ["--file", "-F"]
47
+
45
48
  _common_default = "tags"
46
49
  _common_path_default = Reference._common_path_default + "/" + _common_default
47
50
 
@@ -92,6 +95,7 @@ class TagReference(Reference):
92
95
  reference: Union[str, "SymbolicReference"] = "HEAD",
93
96
  logmsg: Union[str, None] = None,
94
97
  force: bool = False,
98
+ allow_unsafe_options: bool = False,
95
99
  **kwargs: Any,
96
100
  ) -> "TagReference":
97
101
  """Create a new tag reference.
@@ -121,12 +125,21 @@ class TagReference(Reference):
121
125
  :param force:
122
126
  If ``True``, force creation of a tag even though that tag already exists.
123
127
 
128
+ :param allow_unsafe_options:
129
+ Allow unsafe options, such as ``--file``.
130
+
124
131
  :param kwargs:
125
132
  Additional keyword arguments to be passed to :manpage:`git-tag(1)`.
126
133
 
127
134
  :return:
128
135
  A new :class:`TagReference`.
129
136
  """
137
+ if not allow_unsafe_options:
138
+ Git.check_unsafe_options(
139
+ options=Git._option_candidates([], kwargs),
140
+ unsafe_options=cls.unsafe_git_tag_options,
141
+ )
142
+
130
143
  if "ref" in kwargs and kwargs["ref"]:
131
144
  reference = kwargs["ref"]
132
145
 
git/repo/base.py CHANGED
@@ -142,6 +142,14 @@ class Repo:
142
142
  re_author_committer_start = re.compile(r"^(author|committer)")
143
143
  re_tab_full_line = re.compile(r"^\t(.*)$")
144
144
 
145
+ unsafe_git_init_options = [
146
+ # Can install hooks that execute during later Git commands:
147
+ "--template",
148
+ # Redirects the repository metadata to a caller-controlled path:
149
+ "--separate-git-dir",
150
+ ]
151
+ """Options to :manpage:`git-init(1)` that permit unsafe code execution or I/O."""
152
+
145
153
  unsafe_git_clone_options = [
146
154
  # Executes arbitrary commands:
147
155
  "--upload-pack",
@@ -151,8 +159,10 @@ class Repo:
151
159
  "-c",
152
160
  # Can install hooks that execute during clone:
153
161
  "--template",
162
+ # Fetches from an additional caller-controlled URI:
163
+ "--bundle-uri",
154
164
  ]
155
- """Options to :manpage:`git-clone(1)` that allow arbitrary commands to be executed.
165
+ """Options to :manpage:`git-clone(1)` that permit unsafe command execution or I/O.
156
166
 
157
167
  The ``--upload-pack``/``-u`` option allows users to execute arbitrary commands
158
168
  directly:
@@ -164,6 +174,11 @@ class Repo:
164
174
 
165
175
  The ``--template`` option can install hooks that execute during clone:
166
176
  https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---templatetemplate-directory
177
+
178
+ The ``--bundle-uri`` option fetches from an additional URI before fetching from the
179
+ clone URL. An untrusted value can therefore make Git access local files or
180
+ unintended network resources:
181
+ https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---bundle-uriuri
167
182
  """
168
183
 
169
184
  unsafe_git_archive_options = [
@@ -172,6 +187,10 @@ class Repo:
172
187
  # Writes output to a caller-controlled filesystem path.
173
188
  "--output",
174
189
  "-o",
190
+ # Reads from a caller-controlled filesystem path:
191
+ "--add-file",
192
+ # Injects a caller-controlled path and contents:
193
+ "--add-virtual-file",
175
194
  ]
176
195
 
177
196
  unsafe_git_revision_options = [
@@ -922,13 +941,17 @@ class Repo:
922
941
  return False
923
942
 
924
943
  def _get_daemon_export(self) -> bool:
925
- if self.git_dir:
926
- filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE)
944
+ git_dir = getattr(self, "git_dir", None)
945
+ if git_dir is None:
946
+ return False
947
+ filename = osp.join(git_dir, self.DAEMON_EXPORT_FILE)
927
948
  return osp.exists(filename)
928
949
 
929
950
  def _set_daemon_export(self, value: object) -> None:
930
- if self.git_dir:
931
- filename = osp.join(self.git_dir, self.DAEMON_EXPORT_FILE)
951
+ git_dir = getattr(self, "git_dir", None)
952
+ if git_dir is None:
953
+ return
954
+ filename = osp.join(git_dir, self.DAEMON_EXPORT_FILE)
932
955
  fileexists = osp.exists(filename)
933
956
  if value and not fileexists:
934
957
  touch(filename)
@@ -950,8 +973,7 @@ class Repo:
950
973
  :return:
951
974
  List of strings being pathnames of alternates
952
975
  """
953
- if self.git_dir:
954
- alternates_path = osp.join(self.git_dir, "objects", "info", "alternates")
976
+ alternates_path = osp.join(self.common_dir, "objects", "info", "alternates")
955
977
 
956
978
  if osp.exists(alternates_path):
957
979
  with open(alternates_path, "rb") as f:
@@ -1269,6 +1291,7 @@ class Repo:
1269
1291
 
1270
1292
  keepends = True
1271
1293
  for line_bytes in data.splitlines(keepends):
1294
+ line_str = ""
1272
1295
  try:
1273
1296
  line_str = line_bytes.rstrip().decode(defenc)
1274
1297
  except UnicodeDecodeError:
@@ -1383,6 +1406,7 @@ class Repo:
1383
1406
  mkdir: bool = True,
1384
1407
  odbt: Type[GitCmdObjectDB] = GitCmdObjectDB,
1385
1408
  expand_vars: bool = True,
1409
+ allow_unsafe_options: bool = False,
1386
1410
  **kwargs: Any,
1387
1411
  ) -> "Repo":
1388
1412
  """Initialize a git repository at the given path if specified.
@@ -1407,6 +1431,10 @@ class Repo:
1407
1431
  information disclosure, allowing attackers to access the contents of
1408
1432
  environment variables.
1409
1433
 
1434
+ :param allow_unsafe_options:
1435
+ Allow unsafe options to be used, such as ``--template`` and
1436
+ ``--separate-git-dir``.
1437
+
1410
1438
  :param kwargs:
1411
1439
  Keyword arguments serving as additional options to the
1412
1440
  :manpage:`git-init(1)` command.
@@ -1414,6 +1442,11 @@ class Repo:
1414
1442
  :return:
1415
1443
  :class:`Repo` (the newly created repo)
1416
1444
  """
1445
+ if not allow_unsafe_options:
1446
+ Git.check_unsafe_options(
1447
+ options=Git._option_candidates([], kwargs),
1448
+ unsafe_options=cls.unsafe_git_init_options,
1449
+ )
1417
1450
  if path:
1418
1451
  path = expand_path(path, expand_vars)
1419
1452
  if mkdir and path and not osp.exists(path):
@@ -1727,8 +1760,9 @@ class Repo:
1727
1760
 
1728
1761
  ``None`` if we are not currently rebasing.
1729
1762
  """
1730
- if self.git_dir:
1731
- rebase_head_file = osp.join(self.git_dir, "REBASE_HEAD")
1763
+ if not self.git_dir:
1764
+ return None
1765
+ rebase_head_file = osp.join(self.git_dir, "REBASE_HEAD")
1732
1766
  if not osp.isfile(rebase_head_file):
1733
1767
  return None
1734
1768
  with open(rebase_head_file, "rt") as f:
git/util.py CHANGED
@@ -315,6 +315,68 @@ def join_path_native(a: PathLike, *p: PathLike) -> PathLike:
315
315
  return to_native_path(join_path(a, *p))
316
316
 
317
317
 
318
+ def _is_path_rooted(path: PathLike) -> bool:
319
+ r"""Whether ``path`` has a root, including one encoded in a UNC drive.
320
+
321
+ On Windows, ``\directory`` is rooted on the current drive without being
322
+ absolute, while ``C:\directory`` has both a drive and a root. In contrast,
323
+ ``directory`` and the drive-relative ``C:directory`` have no root.
324
+ UNC paths are rooted: ``\\server\share`` stores the share in the drive
325
+ returned by :func:`os.path.splitdrive`, while ``\\server\share\directory``
326
+ additionally has a rooted tail.
327
+ On POSIX, which has no drive concept, this simply distinguishes absolute
328
+ paths such as ``/directory`` from relative paths such as ``directory``.
329
+ """
330
+ drive, tail = osp.splitdrive(os.fspath(path))
331
+ separators = (os.sep,) if os.altsep is None else (os.sep, os.altsep)
332
+ return tail.startswith(separators) or drive.startswith(separators)
333
+
334
+
335
+ def _to_relative_path(root: PathLike, path: PathLike) -> str:
336
+ r"""Return a normalized Git-style path confined to ``root``.
337
+
338
+ A Windows path such as ``\directory`` is rooted but not absolute. Resolve it
339
+ against the drive of ``root`` rather than treating it as relative to ``root``.
340
+ Drive-relative paths such as ``C:directory`` are rejected because their meaning
341
+ depends on process-global per-drive state.
342
+
343
+ For example, with ``root`` set to ``C:\repo`` on Windows:
344
+
345
+ * ``directory\file`` -> ``directory/file``
346
+ * ``directory\`` -> ``directory/``
347
+ * ``C:\repo\directory\file`` -> ``directory/file``
348
+ * ``\repo\directory\file`` -> ``directory/file``
349
+ * ``C:directory\file`` -> :exc:`ValueError`
350
+ * ``C:\other\file`` -> :exc:`ValueError`
351
+
352
+ On POSIX, ``/repo/directory/file`` under ``/repo`` similarly becomes
353
+ ``directory/file``. A trailing separator is preserved as a Git-style ``/``.
354
+ """
355
+ path_str = os.fspath(path)
356
+ if not path_str:
357
+ return path_str
358
+
359
+ drive, _tail = osp.splitdrive(path_str)
360
+ rooted = _is_path_rooted(path_str)
361
+ if drive and not rooted:
362
+ raise ValueError("Drive-relative path %r is not supported" % path_str)
363
+
364
+ root_abs = osp.abspath(os.fspath(root))
365
+ path_abs = osp.abspath(osp.join(root_abs, path_str))
366
+ try:
367
+ common_path = osp.commonpath([root_abs, path_abs])
368
+ except ValueError as e:
369
+ raise ValueError("Path %r is not in repository at %r" % (path_str, root_abs)) from e
370
+ if common_path != root_abs:
371
+ raise ValueError("Path %r is not in repository at %r" % (path_str, root_abs))
372
+
373
+ relative_path = to_native_path_linux(osp.relpath(path_abs, root_abs))
374
+ separators = (os.sep,) if os.altsep is None else (os.sep, os.altsep)
375
+ if path_str.endswith(separators) and relative_path != "." and not relative_path.endswith("/"):
376
+ relative_path += "/"
377
+ return relative_path
378
+
379
+
318
380
  def assure_directory_exists(path: PathLike, is_file: bool = False) -> bool:
319
381
  """Make sure that the directory pointed to by path exists.
320
382
 
@@ -540,8 +602,7 @@ def expand_path(p: Union[None, PathLike], expand_vars: bool = True) -> Optional[
540
602
 
541
603
 
542
604
  def remove_password_if_present(cmdline: Sequence[str]) -> List[str]:
543
- """Parse any command line argument and if one of the elements is an URL with a
544
- username and/or password, replace them by stars (in-place).
605
+ """Redact credentials in URLs and HTTP Authorization extra headers in a command line.
545
606
 
546
607
  If nothing is found, this just returns the command line as-is.
547
608
 
@@ -551,6 +612,16 @@ def remove_password_if_present(cmdline: Sequence[str]) -> List[str]:
551
612
  new_cmdline = []
552
613
  for index, to_parse in enumerate(cmdline):
553
614
  new_cmdline.append(to_parse)
615
+ config_key, separator, header = to_parse.partition("=")
616
+ header_name, colon, _ = header.partition(":")
617
+ if (
618
+ separator
619
+ and colon
620
+ and config_key.lower().endswith(".extraheader")
621
+ and header_name.strip().lower() == "authorization"
622
+ ):
623
+ new_cmdline[index] = "%s%s%s%s *****" % (config_key, separator, header_name, colon)
624
+ continue
554
625
  try:
555
626
  url = urlsplit(to_parse)
556
627
  # Remove password from the URL if present.
@@ -611,13 +682,17 @@ class RemoteProgress:
611
682
  self.error_lines: List[str] = []
612
683
  self.other_lines: List[str] = []
613
684
 
614
- def _parse_progress_line(self, line: AnyStr) -> None:
685
+ def _parse_progress_line(self, line: AnyStr) -> Optional[object]:
615
686
  """Parse progress information from the given line as retrieved by
616
687
  :manpage:`git-push(1)` or :manpage:`git-fetch(1)`.
617
688
 
618
689
  - Lines that do not contain progress info are stored in :attr:`other_lines`.
619
690
  - Lines that seem to contain an error (i.e. start with ``error:`` or ``fatal:``)
620
691
  are stored in :attr:`error_lines`.
692
+
693
+ The base implementation returns ``None``. Subclasses may return another
694
+ value for compatibility with existing overrides, but callers should treat
695
+ the return value as unspecified.
621
696
  """
622
697
  # handle
623
698
  # Counting objects: 4, done.
@@ -632,7 +707,7 @@ class RemoteProgress:
632
707
 
633
708
  if self._cur_line.startswith(("error:", "fatal:")):
634
709
  self.error_lines.append(self._cur_line)
635
- return
710
+ return None
636
711
 
637
712
  cur_count, max_count = None, None
638
713
  match = self.re_op_relative.match(line_str)
@@ -642,7 +717,7 @@ class RemoteProgress:
642
717
  if not match:
643
718
  self.line_dropped(line_str)
644
719
  self.other_lines.append(line_str)
645
- return
720
+ return None
646
721
  # END could not get match
647
722
 
648
723
  op_code = 0
@@ -673,7 +748,7 @@ class RemoteProgress:
673
748
  self.line_dropped(line_str)
674
749
  # Note: Don't add this line to the other lines, as we have to silently
675
750
  # drop it.
676
- return
751
+ return None
677
752
  # END handle op code
678
753
 
679
754
  # Figure out stage.
@@ -699,6 +774,7 @@ class RemoteProgress:
699
774
  max_count and float(max_count),
700
775
  message,
701
776
  )
777
+ return None
702
778
 
703
779
  def new_message_handler(self) -> Callable[[str], None]:
704
780
  """
@@ -708,7 +784,7 @@ class RemoteProgress:
708
784
  """
709
785
 
710
786
  def handler(line: AnyStr) -> None:
711
- return self._parse_progress_line(line.rstrip())
787
+ self._parse_progress_line(line.rstrip())
712
788
 
713
789
  # END handler
714
790
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GitPython
3
- Version: 3.1.56
3
+ Version: 3.1.58
4
4
  Summary: GitPython is a Python library used to interact with Git repositories
5
5
  Home-page: https://github.com/gitpython-developers/GitPython
6
6
  Author: Sebastian Thiel, Michael Trier
@@ -33,6 +33,7 @@ Requires-Dist: gitdb<5,>=4.0.1
33
33
  Requires-Dist: typing-extensions>=3.10.0.2; python_version < "3.10"
34
34
  Provides-Extra: test
35
35
  Requires-Dist: coverage[toml]; extra == "test"
36
+ Requires-Dist: basedpyright==1.39.9; (python_version >= "3.9" and sys_platform != "cygwin") and extra == "test"
36
37
  Requires-Dist: ddt!=1.4.3,>=1.1.1; extra == "test"
37
38
  Requires-Dist: mock; python_version < "3.8" and extra == "test"
38
39
  Requires-Dist: mypy==1.18.2; python_version >= "3.9" and extra == "test"
@@ -163,15 +164,13 @@ In the less common case that you do not want to install test dependencies, `pip
163
164
 
164
165
  #### With editable *dependencies* (not preferred, and rarely needed)
165
166
 
166
- In rare cases, you may want to work on GitPython and one or both of its [gitdb](https://github.com/gitpython-developers/gitdb) and [smmap](https://github.com/gitpython-developers/smmap) dependencies at the same time, with changes in your local working copy of gitdb or smmap immediately reflected in the behavior of your local working copy of GitPython. This can be done by making editable installations of those dependencies in the same virtual environment where you install GitPython.
167
-
168
- If you want to do that *and* you want the versions in GitPython's git submodules to be used, then pass `-e git/ext/gitdb` and/or `-e git/ext/gitdb/gitdb/ext/smmap` to `pip install`. This can be done in any order, and in separate `pip install` commands or the same one, so long as `-e` appears before *each* path. For example, you can install GitPython, gitdb, and smmap editably in the currently active virtual environment this way:
167
+ GitPython, [gitdb](gitdb), and [smmap](smmap) live in this repository as independently packaged projects. To work on all three at once, install each one editably in the same virtual environment:
169
168
 
170
169
  ```sh
171
- pip install -e ".[test]" -e git/ext/gitdb -e git/ext/gitdb/gitdb/ext/smmap
170
+ pip install -e smmap -e gitdb -e ".[test]"
172
171
  ```
173
172
 
174
- The submodules must have been cloned for that to work, but that will already be the case if you have run `./init-tests-after-clone.sh`. You can use `pip list` to check which packages are installed editably and which are installed normally.
173
+ You can use `pip list` to check which packages are installed editably and which are installed normally.
175
174
 
176
175
  To reiterate, this approach should only rarely be used. For most development it is preferable to allow the gitdb and smmap dependencices to be retrieved automatically from PyPI in their latest stable packaged versions.
177
176
 
@@ -1,44 +1,44 @@
1
- git/__init__.py,sha256=rqhgyk2pLpsRiDm8ixgf4vM0CT2P0RNotTLMYiTWt14,8899
2
- git/cmd.py,sha256=SfzSWj4dxcXDHdpHQwIfyG3ISknzU8lnoYSi6fFBLbo,75648
1
+ git/__init__.py,sha256=LzalIqZf4OxqrTAKsm4RVdP-VtO02P4bfFImzWRshdM,8899
2
+ git/cmd.py,sha256=leCmxJSwz5Y_ddQmgIN1Cm8fEPxpPTzE0C5wIVkcqVE,77023
3
3
  git/compat.py,sha256=y1E6y6O2q5r8clSlr8ZNmuIWG9nmHuehQEsVsmBffs8,4526
4
- git/config.py,sha256=aLDSuDiiB67W-OLktJpYVjSLwgIqWJtPYbRllNhQ7Gw,38406
4
+ git/config.py,sha256=ZYg-s7KEcHXJbOYL839HOUqPhHsCbGoj6e2khvYiW8U,38988
5
5
  git/db.py,sha256=vIW9uWSbqu99zbuU2ZDmOhVOv1UPTmxrnqiCtRHCfjE,2368
6
- git/diff.py,sha256=oMhc8IyMFfN24A7agcnarbFYjsz1vqskYzjh0s7MULw,28169
6
+ git/diff.py,sha256=k7iYB8gBzqrGDNn_XfZPXpS6LFxq02-JnyLo4bdwvVA,28271
7
7
  git/exc.py,sha256=Gc7g1pHpn8OmTse30NHmJVsBJ2CYH8LxaR8y8UA3lIM,7119
8
8
  git/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  git/remote.py,sha256=d68K7XmkYXZUlIoLBDRpRom_df6mbkhWMrcPAP5s11c,46900
10
10
  git/types.py,sha256=d3oDduUGjfoqffcUL_GOD-3qroF8m3r9ai6Xi9_FraM,10279
11
- git/util.py,sha256=27MIkq6AhpjyAHF9r7UEA1pGYRAkxRMiZNo5IJGdJhU,44712
11
+ git/util.py,sha256=TJWZ8zeqpsp2NoIr5u_tcUB7yM3OB_RHoYLy4b95TUQ,48135
12
12
  git/index/__init__.py,sha256=i-Nqb8Lufp9aFbmxpQBORmmQnjEVVM1Pn58fsQkyGgQ,406
13
- git/index/base.py,sha256=yG2hJuFBa15l1sAXji8IPE-llXEjYZHnzTMs5ELvb20,62734
14
- git/index/fun.py,sha256=Cfrpq6GmhLXGVDDjYiXTN4f4bb35kvw6-cOGOBuJeGc,17862
13
+ git/index/base.py,sha256=jFITcQoUhQacJhDiurNaG2izyseH9vaNciRoVy4acQE,64707
14
+ git/index/fun.py,sha256=AIW3T6Gr6h5E22z_2wKHevzh5K0HhjjEO9kXAK3nHOM,22419
15
15
  git/index/typ.py,sha256=ZjRWp5J79OwFQor1XXN4C6JmHTDclD9H-tKZtAeqyTI,7019
16
16
  git/index/util.py,sha256=f0BfE2JPkdypsB2Q_HJ8KU7MC45RRHywL_GWwxa_5hw,3676
17
17
  git/objects/__init__.py,sha256=O6ZL_olX7e5-8iIbKviRPkVSJxN37WA-EC0q9d48U5Y,637
18
18
  git/objects/base.py,sha256=5-p9uSGWvPxX9hidvkGH9tQOJoH2eaRggUbMr9VJiL0,10285
19
19
  git/objects/blob.py,sha256=G9PZMNc5CFRQFNnlquBRAM5-jZg8581lRcxkvARobSk,1231
20
- git/objects/commit.py,sha256=aJYxpbkcmAskmnc6qv6Podswhshu1-GlY306UqNWzvs,33175
21
- git/objects/fun.py,sha256=B4jCqhAjm6Hl79GK58FPzW1H9K6Wc7Tx0rssyWmAcEE,8935
20
+ git/objects/commit.py,sha256=YbF2cjl6aziTq2HRgZkxr5TgfU8keIVEEkoBB0DPLKc,33291
21
+ git/objects/fun.py,sha256=bcofjsxrF1NniYWa1r8oAy8uYT_q2rUpLFnYj1aULGU,8949
22
22
  git/objects/tag.py,sha256=jAGESnpmTEv-dLakPzheT5ILZFFArcItnXYqfxfDrgc,4441
23
23
  git/objects/tree.py,sha256=MmExcS59Iamb2cBqUvoHPdzgixabyvv5voHabossrl8,13898
24
24
  git/objects/util.py,sha256=Nlza4zLgdPmr_Yasyvvs6c1rKtW_wMxI6wDmQpQ3ufw,23846
25
25
  git/objects/submodule/__init__.py,sha256=6xySp767LVz3UylWgUalntS_nGXRuVzXxDuFAv_Wc2c,303
26
- git/objects/submodule/base.py,sha256=1w39BpgNnpEmEacCLdCGLH-aeIaAMKU0oWUtgVcyVW0,66347
27
- git/objects/submodule/root.py,sha256=5eTtYNHasqdPq6q0oDCPr7IaO6uAHL3b4DxMoiO2LhE,20246
26
+ git/objects/submodule/base.py,sha256=jf8PYfhgVthsle_7qICs_YSqWyU3jhPGShNxuJtMTqs,66547
27
+ git/objects/submodule/root.py,sha256=767GIhBwCwn2xvXeMHm2uEO3kZlRyaIaFXnSqcfdZxU,20277
28
28
  git/objects/submodule/util.py,sha256=sQqAYaiSJdFkZa9NlAuK_wTsMNiS-kkQnQjvIoJtc_o,3509
29
29
  git/refs/__init__.py,sha256=DWlJNnsx-4jM_E-VycbP-FZUdn6iWhjnH_uZ_pZXBro,509
30
- git/refs/head.py,sha256=59MUMVu9phmE_ImkmNw0sDDSCc75s7TxkQj_cKzRhbo,10397
31
- git/refs/log.py,sha256=X88JTqjwX1ShvHr-NHNAZj0qLiHbVoF-0v3VymZDwoM,12612
30
+ git/refs/head.py,sha256=Q3VZt5MmEUwltBpBHJmcAZM9gYzCekAKgMpJlPrbttk,11309
31
+ git/refs/log.py,sha256=G2e9GR1owwdmDuKmwGGozt5b1y8qIccUoXiuk2q1E1o,12635
32
32
  git/refs/reference.py,sha256=uzrvNTZ5fg7A0Aq4VC3KLSMbksgYUf4KvmHau0tFYIM,5646
33
33
  git/refs/remote.py,sha256=WTlkJtpu895jJxbknDNOwL31tivQY_AHQrvroUoFBX4,2902
34
- git/refs/symbolic.py,sha256=7ScmZHwlqPDKX9bxVC5queSZXUgqEY7_J_Si6gDBSLw,36139
35
- git/refs/tag.py,sha256=AKrFrMhkNzX8e9x5WmDRtHovbN9zYQghuKoneVW-TJg,5002
34
+ git/refs/symbolic.py,sha256=MPi5UN0ujdE2GMOrzfW2zt-6HlVreuY5W4P7tnBg4V8,36103
35
+ git/refs/tag.py,sha256=27fZPVVkAPvaRoUt6_IX-tXKLleCYwoKA0GAk453w9E,5418
36
36
  git/repo/__init__.py,sha256=CILSVH36fX_WxVFSjD9o1WF5LgsNedPiJvSngKZqfVU,210
37
- git/repo/base.py,sha256=DZhyKpL1CkfPoXkFRgia6p9d2x7FNuogEhILlT68Xa4,64030
37
+ git/repo/base.py,sha256=kEnMAyQA0tyPd_W8BT2Sj5ssjvMtcbrhETMTLgmBe5o,65436
38
38
  git/repo/fun.py,sha256=PDFNkuTSGGoQc0EEEXjxHw2_KaKYvVjBZ9rrkwH5Q6Y,23462
39
- gitpython-3.1.56.dist-info/licenses/AUTHORS,sha256=Iucx_CH3w7_4084yjM6C5BvKfLA8cVfYHWW2pVmpxSk,2410
40
- gitpython-3.1.56.dist-info/licenses/LICENSE,sha256=hvyUwyGpr7wRUUcTURuv3tIl8lEA3MD3NQ6CvCMbi-s,1503
41
- gitpython-3.1.56.dist-info/METADATA,sha256=2GkcUbrC-AoVKsGEiIRrcIrMu4dgl-kg9iXzo3k4C-Q,14340
42
- gitpython-3.1.56.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
43
- gitpython-3.1.56.dist-info/top_level.txt,sha256=0hzDuIp8obv624V3GmbqsagBWkk8ohtGU-Bc1PmTT0o,4
44
- gitpython-3.1.56.dist-info/RECORD,,
39
+ gitpython-3.1.58.dist-info/licenses/AUTHORS,sha256=Iucx_CH3w7_4084yjM6C5BvKfLA8cVfYHWW2pVmpxSk,2410
40
+ gitpython-3.1.58.dist-info/licenses/LICENSE,sha256=hvyUwyGpr7wRUUcTURuv3tIl8lEA3MD3NQ6CvCMbi-s,1503
41
+ gitpython-3.1.58.dist-info/METADATA,sha256=lEyUxCGa5z5oNHvXTNT-6ooCbqoXjaL4DrYdNPAnO9s,13558
42
+ gitpython-3.1.58.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
43
+ gitpython-3.1.58.dist-info/top_level.txt,sha256=0hzDuIp8obv624V3GmbqsagBWkk8ohtGU-Bc1PmTT0o,4
44
+ gitpython-3.1.58.dist-info/RECORD,,