Skip to content

Commit

Permalink
add types to git.compat and git.diff
Browse files Browse the repository at this point in the history
  • Loading branch information
Yobmod committed Mar 2, 2021
1 parent 71e28b8 commit 2fd9f6e
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 64 deletions.
16 changes: 10 additions & 6 deletions git/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
import locale
import os
import sys
from typing import AnyStr, Optional, Type


from gitdb.utils.encoding import (
force_bytes, # @UnusedImport
force_text # @UnusedImport
)

from typing import Any, AnyStr, Dict, Optional, Type
from git.types import TBD


is_win = (os.name == 'nt') # type: bool
is_posix = (os.name == 'posix')
Expand Down Expand Up @@ -61,14 +62,17 @@ def win_encode(s: Optional[AnyStr]) -> Optional[bytes]:
return None


def with_metaclass(meta, *bases):
def with_metaclass(meta: Type[Any], *bases: Any) -> 'metaclass': # type: ignore ## mypy cannot understand dynamic class creation
"""copied from https://github.com/Byron/bcore/blob/master/src/python/butility/future.py#L15"""
class metaclass(meta):

class metaclass(meta): # type: ignore
__call__ = type.__call__
__init__ = type.__init__
__init__ = type.__init__ # type: ignore

def __new__(cls, name, nbases, d):
def __new__(cls, name: str, nbases: Optional[int], d: Dict[str, Any]) -> TBD:
if nbases is None:
return type.__new__(cls, name, (), d)
return meta(name, bases, d)

return metaclass(meta.__name__ + 'Helper', None, {})

5 changes: 3 additions & 2 deletions git/db.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Module with our own gitdb implementation - it uses the git command"""
from typing import AnyStr
from git.util import bin_to_hex, hex_to_bin
from gitdb.base import (
OInfo,
Expand All @@ -13,7 +14,7 @@
# typing-------------------------------------------------

from .cmd import Git
from .types import PathLike, TBD
from .types import PathLike

# --------------------------------------------------------

Expand Down Expand Up @@ -48,7 +49,7 @@ def stream(self, sha: bytes) -> OStream:

# { Interface

def partial_to_complete_sha_hex(self, partial_hexsha: str) -> bytes:
def partial_to_complete_sha_hex(self, partial_hexsha: AnyStr) -> bytes:
""":return: Full binary 20 byte sha from the given partial hexsha
:raise AmbiguousObjectName:
:raise BadObject:
Expand Down
114 changes: 66 additions & 48 deletions git/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
#
# This module is part of GitPython and is released under
# the BSD License: http://www.opensource.org/licenses/bsd-license.php
import re

import re
from git.cmd import handle_process_output
from git.compat import defenc
from git.util import finalize_process, hex_to_bin
Expand All @@ -13,22 +13,33 @@
from .objects.util import mode_str_to_int


# typing ------------------------------------------------------------------

from .objects.tree import Tree
from git.repo.base import Repo
from typing_extensions import Final, Literal
from git.types import TBD
from typing import Any, Iterator, List, Match, Optional, Tuple, Type, Union
Lit_change_type = Literal['A', 'D', 'M', 'R', 'T']

# ------------------------------------------------------------------------

__all__ = ('Diffable', 'DiffIndex', 'Diff', 'NULL_TREE')

# Special object to compare against the empty tree in diffs
NULL_TREE = object()
NULL_TREE: Final[object] = object()

_octal_byte_re = re.compile(b'\\\\([0-9]{3})')


def _octal_repl(matchobj):
def _octal_repl(matchobj: Match) -> bytes:
value = matchobj.group(1)
value = int(value, 8)
value = bytes(bytearray((value,)))
return value


def decode_path(path, has_ab_prefix=True):
def decode_path(path: bytes, has_ab_prefix: bool = True) -> Optional[bytes]:
if path == b'/dev/null':
return None

Expand Down Expand Up @@ -60,15 +71,17 @@ class Diffable(object):
class Index(object):
pass

def _process_diff_args(self, args):
def _process_diff_args(self, args: List[Union[str, 'Diffable', object]]) -> List[Union[str, 'Diffable', object]]:
"""
:return:
possibly altered version of the given args list.
Method is called right before git command execution.
Subclasses can use it to alter the behaviour of the superclass"""
return args

def diff(self, other=Index, paths=None, create_patch=False, **kwargs):
def diff(self, other: Union[Type[Index], Type[Tree], object, None, str] = Index,
paths: Union[str, List[str], Tuple[str, ...], None] = None,
create_patch: bool = False, **kwargs: Any) -> 'DiffIndex':
"""Creates diffs between two items being trees, trees and index or an
index and the working tree. It will detect renames automatically.
Expand Down Expand Up @@ -99,7 +112,7 @@ def diff(self, other=Index, paths=None, create_patch=False, **kwargs):
:note:
On a bare repository, 'other' needs to be provided as Index or as
as Tree/Commit, or a git command error will occur"""
args = []
args = [] # type: List[Union[str, Diffable, object]]
args.append("--abbrev=40") # we need full shas
args.append("--full-index") # get full index paths, not only filenames

Expand All @@ -117,6 +130,9 @@ def diff(self, other=Index, paths=None, create_patch=False, **kwargs):
if paths is not None and not isinstance(paths, (tuple, list)):
paths = [paths]

if hasattr(self, 'repo'): # else raise Error?
self.repo = self.repo # type: 'Repo'

diff_cmd = self.repo.git.diff
if other is self.Index:
args.insert(0, '--cached')
Expand Down Expand Up @@ -163,7 +179,7 @@ class DiffIndex(list):
# T = Changed in the type
change_type = ("A", "C", "D", "R", "M", "T")

def iter_change_type(self, change_type):
def iter_change_type(self, change_type: Lit_change_type) -> Iterator['Diff']:
"""
:return:
iterator yielding Diff instances that match the given change_type
Expand All @@ -180,7 +196,7 @@ def iter_change_type(self, change_type):
if change_type not in self.change_type:
raise ValueError("Invalid change type: %s" % change_type)

for diff in self:
for diff in self: # type: 'Diff'
if diff.change_type == change_type:
yield diff
elif change_type == "A" and diff.new_file:
Expand Down Expand Up @@ -255,22 +271,21 @@ class Diff(object):
"new_file", "deleted_file", "copied_file", "raw_rename_from",
"raw_rename_to", "diff", "change_type", "score")

def __init__(self, repo, a_rawpath, b_rawpath, a_blob_id, b_blob_id, a_mode,
b_mode, new_file, deleted_file, copied_file, raw_rename_from,
raw_rename_to, diff, change_type, score):

self.a_mode = a_mode
self.b_mode = b_mode
def __init__(self, repo: Repo,
a_rawpath: Optional[bytes], b_rawpath: Optional[bytes],
a_blob_id: Union[str, bytes, None], b_blob_id: Union[str, bytes, None],
a_mode: Union[bytes, str, None], b_mode: Union[bytes, str, None],
new_file: bool, deleted_file: bool, copied_file: bool,
raw_rename_from: Optional[bytes], raw_rename_to: Optional[bytes],
diff: Union[str, bytes, None], change_type: Optional[str], score: Optional[int]) -> None:

assert a_rawpath is None or isinstance(a_rawpath, bytes)
assert b_rawpath is None or isinstance(b_rawpath, bytes)
self.a_rawpath = a_rawpath
self.b_rawpath = b_rawpath

if self.a_mode:
self.a_mode = mode_str_to_int(self.a_mode)
if self.b_mode:
self.b_mode = mode_str_to_int(self.b_mode)
self.a_mode = mode_str_to_int(a_mode) if a_mode else None
self.b_mode = mode_str_to_int(b_mode) if b_mode else None

# Determine whether this diff references a submodule, if it does then
# we need to overwrite "repo" to the corresponding submodule's repo instead
Expand Down Expand Up @@ -305,27 +320,27 @@ def __init__(self, repo, a_rawpath, b_rawpath, a_blob_id, b_blob_id, a_mode,
self.change_type = change_type
self.score = score

def __eq__(self, other):
def __eq__(self, other: object) -> bool:
for name in self.__slots__:
if getattr(self, name) != getattr(other, name):
return False
# END for each name
return True

def __ne__(self, other):
def __ne__(self, other: object) -> bool:
return not (self == other)

def __hash__(self):
def __hash__(self) -> int:
return hash(tuple(getattr(self, n) for n in self.__slots__))

def __str__(self):
h = "%s"
def __str__(self) -> str:
h = "%s" # type: str
if self.a_blob:
h %= self.a_blob.path
elif self.b_blob:
h %= self.b_blob.path

msg = ''
msg = '' # type: str
line = None # temp line
line_length = 0 # line length
for b, n in zip((self.a_blob, self.b_blob), ('lhs', 'rhs')):
Expand Down Expand Up @@ -354,7 +369,7 @@ def __str__(self):
if self.diff:
msg += '\n---'
try:
msg += self.diff.decode(defenc)
msg += self.diff.decode(defenc) if isinstance(self.diff, bytes) else self.diff
except UnicodeDecodeError:
msg += 'OMITTED BINARY DATA'
# end handle encoding
Expand All @@ -368,36 +383,36 @@ def __str__(self):
return res

@property
def a_path(self):
def a_path(self) -> Optional[str]:
return self.a_rawpath.decode(defenc, 'replace') if self.a_rawpath else None

@property
def b_path(self):
def b_path(self) -> Optional[str]:
return self.b_rawpath.decode(defenc, 'replace') if self.b_rawpath else None

@property
def rename_from(self):
def rename_from(self) -> Optional[str]:
return self.raw_rename_from.decode(defenc, 'replace') if self.raw_rename_from else None

@property
def rename_to(self):
def rename_to(self) -> Optional[str]:
return self.raw_rename_to.decode(defenc, 'replace') if self.raw_rename_to else None

@property
def renamed(self):
def renamed(self) -> bool:
""":returns: True if the blob of our diff has been renamed
:note: This property is deprecated, please use ``renamed_file`` instead.
"""
return self.renamed_file

@property
def renamed_file(self):
def renamed_file(self) -> bool:
""":returns: True if the blob of our diff has been renamed
"""
return self.rename_from != self.rename_to

@classmethod
def _pick_best_path(cls, path_match, rename_match, path_fallback_match):
def _pick_best_path(cls, path_match: bytes, rename_match: bytes, path_fallback_match: bytes) -> Optional[bytes]:
if path_match:
return decode_path(path_match)

Expand All @@ -410,21 +425,23 @@ def _pick_best_path(cls, path_match, rename_match, path_fallback_match):
return None

@classmethod
def _index_from_patch_format(cls, repo, proc):
def _index_from_patch_format(cls, repo: Repo, proc: TBD) -> DiffIndex:
"""Create a new DiffIndex from the given text which must be in patch format
:param repo: is the repository we are operating on - it is required
:param stream: result of 'git diff' as a stream (supporting file protocol)
:return: git.DiffIndex """

## FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise.
text = []
handle_process_output(proc, text.append, None, finalize_process, decode_streams=False)
text_list = [] # type: List[bytes]
handle_process_output(proc, text_list.append, None, finalize_process, decode_streams=False)

# for now, we have to bake the stream
text = b''.join(text)
text = b''.join(text_list)
index = DiffIndex()
previous_header = None
header = None
a_path, b_path = None, None # for mypy
a_mode, b_mode = None, None # for mypy
for _header in cls.re_header.finditer(text):
a_path_fallback, b_path_fallback, \
old_mode, new_mode, \
Expand Down Expand Up @@ -464,27 +481,28 @@ def _index_from_patch_format(cls, repo, proc):
previous_header = _header
header = _header
# end for each header we parse
if index:
if index and header:
index[-1].diff = text[header.end():]
# end assign last diff

return index

@classmethod
def _index_from_raw_format(cls, repo, proc):
def _index_from_raw_format(cls, repo: 'Repo', proc: TBD) -> DiffIndex:
"""Create a new DiffIndex from the given stream which must be in raw format.
:return: git.DiffIndex"""
# handles
# :100644 100644 687099101... 37c5e30c8... M .gitignore

index = DiffIndex()

def handle_diff_line(lines):
lines = lines.decode(defenc)
def handle_diff_line(lines_bytes: bytes) -> None:
lines = lines_bytes.decode(defenc)

for line in lines.split(':')[1:]:
meta, _, path = line.partition('\x00')
path = path.rstrip('\x00')
a_blob_id, b_blob_id = None, None # Type: Optional[str]
old_mode, new_mode, a_blob_id, b_blob_id, _change_type = meta.split(None, 4)
# Change type can be R100
# R: status letter
Expand All @@ -504,20 +522,20 @@ def handle_diff_line(lines):
# NOTE: We cannot conclude from the existence of a blob to change type
# as diffs with the working do not have blobs yet
if change_type == 'D':
b_blob_id = None
b_blob_id = None # Optional[str]
deleted_file = True
elif change_type == 'A':
a_blob_id = None
new_file = True
elif change_type == 'C':
copied_file = True
a_path, b_path = path.split('\x00', 1)
a_path = a_path.encode(defenc)
b_path = b_path.encode(defenc)
a_path_str, b_path_str = path.split('\x00', 1)
a_path = a_path_str.encode(defenc)
b_path = b_path_str.encode(defenc)
elif change_type == 'R':
a_path, b_path = path.split('\x00', 1)
a_path = a_path.encode(defenc)
b_path = b_path.encode(defenc)
a_path_str, b_path_str = path.split('\x00', 1)
a_path = a_path_str.encode(defenc)
b_path = b_path_str.encode(defenc)
rename_from, rename_to = a_path, b_path
elif change_type == 'T':
# Nothing to do
Expand Down
Loading

0 comments on commit 2fd9f6e

Please sign in to comment.