From 763fe9048481c936e9c8e27326bc206eff0d0d77 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Fri, 1 Sep 2023 21:22:59 +0200 Subject: [PATCH 001/119] Add Python requirements file --- requirements.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8a16487 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +setuptools==67.8.0 +wheel==0.40.0 +Sphinx==7.0.1 +sphinx-rtd-theme==1.2.0 +behave==1.2.6 \ No newline at end of file From cbc1b9d5b5c5e82f55e5e343554797f9fbf75f68 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Fri, 1 Sep 2023 22:14:43 +0200 Subject: [PATCH 002/119] Add Sphinx configuration --- docs/Makefile | 20 ++++ docs/_documentation_templates/conf.py_t | 116 ++++++++++++++++++++++++ docs/conf.py | 42 +++++++++ docs/index.rst | 20 ++++ docs/modules.rst | 7 ++ docs/sources.rst | 21 +++++ 6 files changed, 226 insertions(+) create mode 100644 docs/Makefile create mode 100644 docs/_documentation_templates/conf.py_t create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/modules.rst create mode 100644 docs/sources.rst diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_documentation_templates/conf.py_t b/docs/_documentation_templates/conf.py_t new file mode 100644 index 0000000..0a022d6 --- /dev/null +++ b/docs/_documentation_templates/conf.py_t @@ -0,0 +1,116 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +{% if append_syspath -%} +import os +import sys +sys.path.insert(0, {{ module_path | repr }}) +{% else -%} +# import os +# import sys +{% if module_path -%} +# sys.path.insert(0, {{ module_path | repr }}) +{% else -%} +# sys.path.insert(0, os.path.abspath('.')) +{% endif -%} +{% endif %} + +# -- Project information ----------------------------------------------------- + +project = {{ project | repr }} +copyright = {{ copyright | repr }} +author = {{ author | repr }} + +{%- if version %} + +# The short X.Y version +version = {{ version | repr }} +{%- endif %} +{%- if release %} + +# The full version, including alpha/beta/rc tags +release = {{ release | repr }} +{%- endif %} + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ +{%- for ext in extensions %} + '{{ ext }}', +{%- endfor %} +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['{{ dot }}templates'] + +{% if suffix != '.rst' -%} +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = {{ suffix | repr }} + +{% endif -%} +{% if master != 'index' -%} +# The master toctree document. +master_doc = {{ master | repr }} + +{% endif -%} +{% if language -%} +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = {{ language | repr }} + +{% endif -%} +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [{{ exclude_patterns }}] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['{{ dot }}static'] +{%- if extensions %} + + +# -- Extension configuration ------------------------------------------------- +{%- endif %} +{%- if 'sphinx.ext.intersphinx' in extensions %} + +# -- Options for intersphinx extension --------------------------------------- + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/3/': None} +{%- endif %} +{%- if 'sphinx.ext.todo' in extensions %} + +# -- Options for todo extension ---------------------------------------------- + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True +{%- endif %} diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..d2a4eed --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,42 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'py-git' +copyright = '2023, Dmytro Leshchenko' +author = 'Dmytro Leshchenko' + +version = '0.0.1' +release = '0.0.1' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', +] + +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'alabaster' +html_static_path = ['_static'] + +# -- Options for intersphinx extension --------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration + +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), +} diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..18a2cff --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,20 @@ +.. py-git documentation master file, created by + sphinx-quickstart on Sat May 20 20:17:21 2023. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to py-git's documentation! +================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/modules.rst b/docs/modules.rst new file mode 100644 index 0000000..aa45929 --- /dev/null +++ b/docs/modules.rst @@ -0,0 +1,7 @@ +sources +======= + +.. toctree:: + :maxdepth: 4 + + sources diff --git a/docs/sources.rst b/docs/sources.rst new file mode 100644 index 0000000..e327104 --- /dev/null +++ b/docs/sources.rst @@ -0,0 +1,21 @@ +sources package +=============== + +Submodules +---------- + +sources.git module +------------------ + +.. automodule:: sources.git + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: sources + :members: + :undoc-members: + :show-inheritance: From a5a408d3838a8e7904a332a97caf2239ce008add Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Fri, 1 Sep 2023 22:15:54 +0200 Subject: [PATCH 003/119] Add sources folder --- sources/__init__.py | 0 sources/exceptions.py | 81 ++++ sources/git.py | 803 ++++++++++++++++++++++++++++++++ sources/options/__init__.py | 0 sources/options/options.py | 122 +++++ sources/options/pull_options.py | 34 ++ sources/utils/__init__.py | 0 sources/utils/path_util.py | 16 + 8 files changed, 1056 insertions(+) create mode 100644 sources/__init__.py create mode 100644 sources/exceptions.py create mode 100644 sources/git.py create mode 100644 sources/options/__init__.py create mode 100644 sources/options/options.py create mode 100644 sources/options/pull_options.py create mode 100644 sources/utils/__init__.py create mode 100644 sources/utils/path_util.py diff --git a/sources/__init__.py b/sources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sources/exceptions.py b/sources/exceptions.py new file mode 100644 index 0000000..3535dba --- /dev/null +++ b/sources/exceptions.py @@ -0,0 +1,81 @@ +""" +Module with Git-specific exceptions classes (derived from Exception base class). +""" + + +class GitException(Exception): + """ + Generic Git Exception class. + """ + + +class GitMissingDefinitionException(GitException): + """ + Exception thrown if no definition has been found for the git option. + """ + + +class GitIncorrectOptionValueException(GitException): + """ + Exception thrown if git option value is not on choices list, where choices list must not be 'None'. + """ + + +class GitIncorrectPositionalOptionDefinitionException(GitException): + """ + Exception thrown if positional option has incorrect definition. + """ + + +class GitMissingRequiredOptionsException(GitException): + """ + Exception thrown if some of the required options are missing. + """ + + +class GitRepositoryNotFoundException(GitException): + """ + Exception thrown if repository doesn't exist. + """ + + +class NotGitRepositoryException(GitException): + """ + Exception thrown if provided path is not a git repository. + """ + + +class GitCommandException(GitException): + """ + Generic Exception class for Git commands. + """ + + +class GitAddException(GitCommandException): + """ + Exception thrown when 'add' operation has failed. + """ + + +class GitMvException(GitCommandException): + """ + Exception thrown when 'mv' operation has failed. + """ + + +class GitRmException(GitCommandException): + """ + Exception thrown when 'rm' operation has failed. + """ + + +class GitPullException(GitCommandException): + """ + Exception thrown when 'pull' operation has failed. + """ + + +class GitPushException(GitCommandException): + """ + Exception thrown when 'push' operation has failed. + """ diff --git a/sources/git.py b/sources/git.py new file mode 100644 index 0000000..4c27d2a --- /dev/null +++ b/sources/git.py @@ -0,0 +1,803 @@ +import fnmatch +import hashlib +import logging +import os +import re +from configparser import ConfigParser +from dataclasses import dataclass, field +from datetime import datetime +from pathlib import Path +from subprocess import Popen, PIPE +from typing import Union, List, Optional, Dict, ClassVar, NoReturn + +from sources.exceptions import GitException, GitPullException, GitRepositoryNotFoundException, \ + NotGitRepositoryException, GitPushException, GitAddException, GitRmException, GitMvException +from sources.options.options import GitOption +from sources.options.pull_options import PullOptionsDefinitions +from sources.utils.path_util import PathUtil + + +class GitCommand: + def __init__(self): + self.__command = 'git' + self.__working_directory = Path() + + @property + def working_directory(self): + return self.__working_directory + + @working_directory.setter + def working_directory(self, working_directory: Union[str, Path]): + class_name = self.__class__.__name__ + if isinstance(working_directory, str): + working_directory = Path(working_directory) + logging.debug(f'Switching {class_name} working directory to "{working_directory.absolute()}"') + self.__working_directory = working_directory + + def __generate_command(self, command: List[Union[str, int]]) -> List[Union[str, int]]: + if isinstance(command, list): + command.insert(0, self.__command) + return command + + def execute(self, command: List[Union[str, int]]): + command = self.__generate_command(command) + logging.debug(command) + with Popen(command, shell=False, stderr=PIPE, stdout=PIPE, cwd=self.__working_directory) as process: + stdout, stderr = process.communicate() + if process.returncode != 0: + raise GitException(stderr.decode('UTF-8')) + return stdout.decode('UTF-8') + + +@dataclass +class Remote: + name: str + url: str + + +class Remotes: + __remotes: List[Remote] + + def __init__(self): + self.__remotes = list() + + @classmethod + def create_from_commits_list(cls, remotes: List[Remote]): + instance = cls() + instance.__remotes = remotes + return instance + + def __getitem__(self, item: Union[int, str]): + if type(item) == int: + return self.__remotes[item] + elif type(item) == str: + for remote in self.__remotes: + if remote.name == item: + return remote + return None + + def __iter__(self): + self.current_index = 0 + return self + + def __next__(self): + if self.current_index < len(self.__remotes): + element = self.__remotes[self.current_index] + self.current_index += 1 + return element + raise StopIteration + + +class GitConfig: + def __init__(self, configuration_path: Union[str, Path]): + configuration_path = PathUtil.convert_to_path(configuration_path) + self.__data = self.__read_configuration(configuration_path) + + @staticmethod + def __read_configuration(path: Path) -> ConfigParser: + parser = ConfigParser() + parser.read(path) + return parser + + def get(self, section: str, name: str): + return self.__data.get(section, name) + + def set(self, section: str, name: str, value: str): + self.__data.set(section, name, value) + + @property + def remotes(self): + regex = re.compile(r'^remote\s*\"(?P[^"]*)\"$') + sections = self.__data.sections() + remotes = [] + for section in sections: + match = regex.match(section) + if match: + remote = Remote(name=match.group('remote'), url=self.__data[section]['url']) + remotes.append(remote) + return remotes + + +class Git: + __configuration_folder: Path + + def __init__(self, git_config_folder: Union[str, Path]): + git_config_folder = PathUtil.convert_to_path(git_config_folder) + self.__configuration_folder = git_config_folder + + def __read_configuration(self, configuration_folder: Path): + configuration = GitConfig(configuration_folder) + logging.info(configuration.get('remote')) + + +@dataclass +class Reference: + path: str + + +class Refspec: + DELIMITER: str = ':' + __source: Reference + __destination: Reference + + def __init__(self): + self.__source = None + self.__destination = None + + @classmethod + def create_from_string(cls, refspec: str): + source, destination = refspec.split(cls.DELIMITER) + instance = cls() + instance.__source = source + instance.__destination = destination + return instance + + @property + def source(self) -> Reference: + return self.__source + + @source.setter + def source(self, source: Reference): + self.__source = source + + @property + def destination(self) -> Reference: + return self.__destination + + @destination.setter + def destination(self, destination: Reference): + self.__destination = destination + + @property + def raw(self) -> str: + return self.DELIMITER.join([self.__source.path, self.__destination.path]) + + +class Refspecs: + def __init__(self): + self.__refspecs = list() + + +@dataclass +class Author: + name: str + email: str + + +@dataclass +class Commit: + FORMAT_DELIMITER: ClassVar[str] = '%n' + FORMAT: ClassVar[str] = FORMAT_DELIMITER.join(['%H', '%an', '%ae', '%ad', '%s']) + DATE_FORMAT: ClassVar[str] = '%Y-%m-%d %H:%M:%S' + message: str + author: 'Author' + date: datetime + commit_hash: str = field(default_factory=str) + parents: List['Commit'] = field(default_factory=list, repr=False) + tags: List['Tag'] = field(default_factory=list) + + def add_tag(self, tag: 'Tag'): + self.tags.append(tag) + + +class Commits: + __commits: List[Commit] + + def __init__(self): + self.__commits = list() + + @classmethod + def create_from_commits_list(cls, commits: List[Commit]): + instance = cls() + instance.__commits = commits + return instance + + def __getitem__(self, item: Union[int, str]): + if type(item) == int: + return self.__commits[item] + elif type(item) == str: + for commit in self.__commits: + if commit.commit_hash == item: + return commit + return None + + def __iter__(self): + self.current_index = 0 + return self + + def __next__(self): + if self.current_index < len(self.__commits): + element = self.__commits[self.current_index] + self.current_index += 1 + return element + raise StopIteration + + +class GitIgnore: + def __init__(self, path: Path): + self.__path = path + if not self.__path.exists(): + raise FileNotFoundError(self.__path) + self.__exclude_patterns = self.__read_file(self.__path) + + @classmethod + def create_from_content(cls, path: Path, content: str): + instance = cls(path) + instance.__exclude_patterns = GitIgnore.__read_content(content.split('\n')) + return instance + + @staticmethod + def __read_file(path: Path): + with path.open('r') as file: + exclude_patterns = GitIgnore.__read_content(file.readlines()) + return exclude_patterns + + @staticmethod + def __read_content(content: List[str]): + exclude_patterns = list() + for line in content: + line = line.strip() + if line and not line.startswith('#'): + exclude_patterns.append(line) + return exclude_patterns + + def refresh(self): + self.__exclude_patterns = self.__read_file(self.__path) + + @property + def patterns(self) -> List[str]: + return self.__exclude_patterns + + +class FilesChangesHandler: + START = 'start' + END = 'end' + + ADDED = 'added' + MODIFIED = 'modified' + REMOVED = 'removed' + EXCLUDED = 'excluded' + + def __init__(self, repository: 'GitRepository'): + self.__repository = repository + self.__files_hashes = {self.START: {}, self.END: {}} + + def __enter__(self): + self.__update_files_hash(self.__repository.path.absolute(), self.__files_hashes[self.START]) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + # TODO(dl1998): Optimize the code. Sort "start" and "end", so it will be faster. Instead of checking using + # "in", walk through lists and classify file by file. Consider to use shared input. + result = self.files_status + logging.debug(result) + + @property + def files_status(self): + self.__update_files_hash(self.__repository.path.absolute(), self.__files_hashes[self.END]) + result = { + self.ADDED: self.__get_added(self.__files_hashes[self.START], self.__files_hashes[self.END]), + self.MODIFIED: self.__get_modified(self.__files_hashes[self.START], self.__files_hashes[self.END]), + self.REMOVED: self.__get_removed(self.__files_hashes[self.START], self.__files_hashes[self.END]) + } + if self.__repository.gitignore: + result[self.EXCLUDED] = self.__get_excluded(self.__files_hashes[self.END], + self.__repository.gitignore.patterns) + return result + + @staticmethod + def __update_files_hash(parent: Path, files_list: Dict[str, str]): + for root, folders, files in os.walk(parent.absolute()): + for file in files: + absolute_path = Path(root, file) + with absolute_path.open('rb') as binary_file: + file_hash = hashlib.md5(binary_file.read()).hexdigest() + files_list[str(absolute_path)] = file_hash + + @staticmethod + def __get_added(start: Dict, end: Dict): + result = [] + for key, value in end.items(): + if key not in start.keys(): + result.append(key) + return result + + @staticmethod + def __get_modified(start: Dict, end: Dict): + result = [] + for key, value in start.items(): + if key in end.keys() and value != end[key]: + result.append(key) + return result + + @staticmethod + def __get_removed(start: Dict, end: Dict): + result = [] + for key, value in start.items(): + if key not in end.keys(): + result.append(key) + return result + + @staticmethod + def __get_excluded(files: List, excludes: List): + result = [] + for key, value in files.items(): + for exclude in excludes: + match = fnmatch.fnmatch(key, exclude) + if match: + result.append(key) + break + return result + + +class CheckoutHandler: + __new_branch: str + + def __init__(self, new_branch: str, git_command: GitCommand, old_branch: Optional[str] = None): + self.__new_branch = new_branch + self.__old_branch = old_branch + self.__git_command = git_command + self.checkout(self.__new_branch) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.checkout(self.__old_branch) + + def checkout(self, branch: Optional[str] = None): + if branch is None: + branch = self.__new_branch + logging.info(f'Switching to "{branch}" branch.') + self.__git_command.execute(['checkout', branch]) + + +@dataclass +class Tag(Reference): + name: str + commit: Commit + author: Author + + def __post_init__(self): + self.path = '/'.join(['refs', 'tags', self.name]) + self.commit.add_tag(self) + + +@dataclass +class LightweightTag(Tag): + pass + + +@dataclass +class AnnotatedTag(Tag): + tagger: Author + message: str + + +@dataclass +class Branch(Reference): + name: str + commit: Commit + + def __init__(self, name: str, commit: Union[str, Commit]): + self.name = name + self.commit = commit + self.path = '/'.join(['refs', 'heads', self.name]) + + @property + def commit(self): + return self.commit + + @commit.setter + def commit(self, commit: Union[str, Commit]): + if isinstance(commit, str): + commit = Commit(commit_hash=commit, message='', author=None, date=None) + self.commit = commit + + +@dataclass +class RemoteBranch(Branch): + pass + + +class Branches: + __branches: List[Branch] + + def __init__(self): + self.__branches = list() + + @classmethod + def create_from_branches_list(cls, branches: List[Branch]): + instance = cls() + instance.__branches = branches + return instance + + def __getitem__(self, item: Union[int, str]): + if type(item) == int: + return self.__branches[item] + elif type(item) == str: + for branch in self.__branches: + if branch.name == item: + return branch + return None + + def __iter__(self): + self.current_index = 0 + return self + + def __next__(self): + if self.current_index < len(self.__branches): + element = self.__branches[self.current_index] + self.current_index += 1 + return element + raise StopIteration + + +class PathsMapping: + DELIMITER: str = ':' + + __source: Path + __destination: Path + __root_path: Path + + def __init__(self, source: Union[str, Path], destination: Union[str, Path], root_path: Union[str, Path]): + self.root_path = root_path + self.__source = source + self.__destination = destination + + @classmethod + def create_from_text(cls, mapping: str, root_path: Union[str, Path] = None): + if root_path is None: + root_path = Path() + source, destination = mapping.strip().split(cls.DELIMITER) + source = source.strip() + destination = destination.strip() + instance = cls(source, destination, root_path) + return instance + + def __normalize_path(self, path: Union[str, Path]) -> Path: + if isinstance(path, str) and str(path).startswith(str(self.__root_path)): + normalized_path = Path(path) + elif isinstance(path, str): + normalized_path = self.__root_path.joinpath(path) + else: + normalized_path = path + return normalized_path + + @property + def source(self) -> Path: + return self.__normalize_path(self.__source) + + @property + def destination(self) -> Path: + return self.__normalize_path(self.__destination) + + @property + def root_path(self) -> Path: + return self.__root_path + + @root_path.setter + def root_path(self, root_path: Union[str, Path]) -> NoReturn: + self.__root_path = root_path + + +class GitRepository: + __git_command: GitCommand + __git_directory: Path + __git_config: GitConfig + __gitignore: GitIgnore + __active_branch: Branch + __remotes: Remotes + __branches: Branches + __commits: Commits + + def __init__(self, path: Union[str, Path]): + self.__git_command = GitCommand() + self.__git_command.working_directory = path + self.__repository_path = PathUtil.convert_to_path(path) + if not self.__repository_path.exists(): + raise GitRepositoryNotFoundException(f'Git repository: {self.__repository_path} not exists.') + self.__git_directory = self.__repository_path.joinpath('.git') + if not self.__git_directory.exists(): + raise NotGitRepositoryException('Provided path is not a git repository, .git directory was not found.') + git_ignore_path = self.__repository_path.joinpath('.gitignore') + self.__gitignore = self.__read_git_ignore(git_ignore_path, self.__git_command) + self.__git_config = GitConfig(self.__git_directory.joinpath('config')) + self.__default_author = self.__get_default_author() + self.refresh_repository(refresh_active_branch=True, refresh_branches=True, refresh_commits=True, + refresh_tags=True, refresh_remotes=True) + + @property + def git_command(self) -> GitCommand: + return self.__git_command + + @property + def path(self): + return self.__repository_path + + @property + def gitignore(self): + return self.__gitignore + + @property + def current_branch(self): + return self.__active_branch + + @property + def remotes(self): + return self.__remotes + + @property + def branches(self): + return self.__branches + + @property + def commits(self): + return self.__commits + + def refresh_repository(self, refresh_active_branch: bool, refresh_branches: bool, refresh_commits: bool, + refresh_tags: bool, refresh_remotes: bool): + branch_name, branch_last_commit = self.__read_active_branch_raw() + if refresh_commits: + try: + self.__commits = self.__get_commits(Branch(name=branch_name, commit=branch_last_commit)) + except GitException: + self.__commits = Commits.create_from_commits_list([]) + if refresh_remotes: + self.__remotes = self.__read_remotes() + if refresh_active_branch: + self.__set_active_branch(branch_name, branch_last_commit) + if refresh_branches: + self.__branches = self.__read_branches() + + def __get_default_author(self): + name = self.__git_command.execute(['config', 'user.name']).strip() + email = self.__git_command.execute(['config', 'user.email']).strip() + return Author(name=name, email=email) + + @staticmethod + def __read_git_ignore(path: Path, git_command: GitCommand) -> Optional[GitIgnore]: + gitignore = None + if path.exists(): + commit_content = git_command.execute(['show', f'HEAD:{path.name}']) + with path.open('r') as file: + content = file.read() + if content != commit_content: + gitignore = GitIgnore.create_from_content(commit_content) + if not gitignore: + gitignore = GitIgnore(path) + return gitignore + + def __read_remotes(self): + return Remotes.create_from_commits_list(self.__git_config.remotes) + + def __set_active_branch(self, active_branch, active_branch_commit): + for commit in self.__commits: + if commit.commit_hash == active_branch_commit: + active_branch_commit = commit + break + self.__active_branch = Branch(name=active_branch, commit=active_branch_commit) + + def __read_active_branch_raw(self): + head_file = self.__git_directory.joinpath('HEAD') + with head_file.open('r') as file: + content = file.read().strip() + match = re.match(r'ref:\s*(?Prefs/heads/(?P.*))', content, + flags=re.MULTILINE) + if match: + active_branch = match.group('active_branch') + active_branch_path = match.group('active_branch_path') + active_branch_path = self.__git_directory.joinpath(active_branch_path) + if not active_branch_path.exists(): + return active_branch, None + with active_branch_path.open('r') as branch_file: + commit_hash = branch_file.read().strip() + return active_branch, commit_hash + + def __read_branches(self): + branches = [] + branches.extend(self.__read_local_branches()) + branches.extend(self.__read_packed_branches()) + return Branches.create_from_branches_list(branches) + + def __read_local_branches(self): + branches_path = self.__git_directory.joinpath('refs', 'heads') + branches = [] + for file_path in branches_path.iterdir(): + if file_path.name == '.DS_Store': + continue + with file_path.open('r') as file: + commit = file.read().strip() + branches.append(Branch(name=file_path.name, commit=commit)) + return branches + + def __read_packed_branches(self): + packed_refs_path = self.__git_directory.joinpath('packed-refs') + packed_branch_pattern = re.compile( + r'^\s*(?P[0-9a-fA-F]+)\s+refs/(?Pheads|remotes/[A-Za-z0-9._-]+)/(?P.+)$') + branches = [] + if packed_refs_path.exists(): + with packed_refs_path.open('r') as file: + for line in file.readlines(): + match = packed_branch_pattern.match(line) + if match: + branches.append(Branch(name=match.group('name'), commit=match.group('commit'))) + return branches + + def __get_commits(self, branch: Branch): + commit_attributes = len(Commit.FORMAT.split(Commit.FORMAT_DELIMITER)) + output = self.__git_command.execute( + ['log', f'--pretty=format:{Commit.FORMAT}', f'--date=format:{Commit.DATE_FORMAT}', branch.name]) + lines = output.split('\n') + raw_commits = [lines[row_index:row_index + 5] for row_index in + range(len(lines) - commit_attributes, -1, commit_attributes * -1)] + parents = [] + for raw_commit in raw_commits: + commit_hash = raw_commit[0] + author_name = raw_commit[1] + author_email = raw_commit[2] + commit_date = datetime.strptime(raw_commit[3], Commit.DATE_FORMAT) + commit_message = raw_commit[4] + author = Author(name=author_name, email=author_email) + commit = Commit(commit_hash=commit_hash, message=commit_message, author=author, date=commit_date, + parents=parents.copy()) + parents.append(commit) + commits = list(reversed(parents)) + return Commits.create_from_commits_list(commits) + + def checkout(self, branch: str): + return CheckoutHandler(branch, self.__git_command, self.__active_branch.name) + + def create_commit(self, message: str, author: Optional[Author] = None, date: Optional[datetime] = None, + commit_hash: str = None): + if author is None: + author = self.__default_author + if date is None: + date = datetime.now() + if commit_hash is None: + commit_hash = '' + return Commit(message=message, author=author, date=date, commit_hash=commit_hash) + + @classmethod + def init(cls, path: Union[str, Path]): + if isinstance(path, str): + path = Path(path) + git_command = GitCommand() + command = ['init', str(path.absolute())] + output = git_command.execute(command) + logging.info(output.strip()) + return cls(path) + + @classmethod + def clone(cls, repository: Union[str, Remote], path: Union[str, Path] = None): + if isinstance(path, str): + path = Path(path) + command = ['clone'] + if isinstance(repository, Remote): + repository = repository.url + command.append(repository) + if path is not None: + command.append(str(path.absolute())) + git_command = GitCommand() + output = git_command.execute(command) + logging.info(output) + return cls(path) + + def add(self, files: Union[str, Path, List[Union[str, Path]]], force: bool = False, update: bool = False): + outputs = [] + additional_options = [] + if force: + additional_options.append('-f') + if update: + additional_options.append('-u') + if isinstance(files, str) or isinstance(files, Path): + files = [files] + for index, file_path in enumerate(files): + if isinstance(file_path, Path): + file_path = str(file_path.absolute()) + try: + command = ['add', *additional_options, file_path] + output = self.__git_command.execute(command) + outputs.append(output.strip()) + except GitException as exception: + raise GitAddException(exception.args[0]) from None + return '\n'.join(outputs) + + def mv(self, mappings: Union[PathsMapping, List[PathsMapping]], force: bool = False): + outputs = [] + additional_options = [] + if force: + additional_options.append('-f') + if isinstance(mappings, PathsMapping): + mappings = [mappings] + for index, mapping in enumerate(mappings): + try: + mapping.root_path = self.__repository_path + command = ['mv', *additional_options, str(mapping.source.absolute()), + str(mapping.destination.absolute())] + output = self.__git_command.execute(command) + outputs.append(output.strip()) + except GitException as exception: + raise GitMvException(exception.args[0]) from None + return '\n'.join(outputs) + + def rm(self, files: Union[str, Path, List[Union[str, Path]]], recursive: bool = False, force: bool = False): + outputs = [] + additional_options = [] + if recursive: + additional_options.append('-r') + if force: + additional_options.append('-f') + if isinstance(files, str) or isinstance(files, Path): + files = [files] + for index, file_path in enumerate(files): + raw_repository_path = str(self.__repository_path.absolute()) + if isinstance(file_path, str) and file_path.startswith(raw_repository_path): + file_path = Path(file_path) + else: + file_path = self.__repository_path.joinpath(file_path) + file_path = str(file_path.absolute()) + try: + command = ['rm', *additional_options, file_path] + output = self.__git_command.execute(command) + outputs.append(output.strip()) + except GitException as exception: + raise GitRmException(exception.args[0]) from None + return '\n'.join(outputs) + + @staticmethod + def __get_refspec(reference: Optional[Union[Reference, Refspec]]): + refspec = None + if isinstance(reference, Reference): + refspec = reference.path + elif isinstance(reference, Refspec): + refspec = reference.raw + return refspec + + def pull(self, remote: Remote, reference: Optional[Union[Reference, Refspec]] = None, *options: GitOption): + # TODO(dl1998): check branch parameter, according to the documentation the last parameter shall be refspec. + refspec = self.__get_refspec(reference) + try: + options = list(options) + options.append(GitOption(name=PullOptionsDefinitions.Options.REPOSITORY.value, value=remote.name)) + options.append(GitOption(name=PullOptionsDefinitions.Options.REFSPEC.value, value=refspec)) + pull_options_definitions = PullOptionsDefinitions() + pull_options = pull_options_definitions.transform_to_command(options) + command = ['pull', *pull_options] + if None in command: + command.remove(None) + output = self.__git_command.execute(command) + return output + except GitException as exception: + raise GitPullException(exception.args[0]) from None + + def push(self, remote: Remote, reference: Optional[Union[Reference, Refspec]] = None): + refspec = self.__get_refspec(reference) + try: + command = ['push', remote.name, refspec] + if None in command: + command.remove(None) + output = self.__git_command.execute(command) + return output + except GitException as exception: + raise GitPushException(exception.args[0]) from None diff --git a/sources/options/__init__.py b/sources/options/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sources/options/options.py b/sources/options/options.py new file mode 100644 index 0000000..e714092 --- /dev/null +++ b/sources/options/options.py @@ -0,0 +1,122 @@ +from dataclasses import dataclass, field +from enum import Enum +from typing import Dict, List, Any, Union, Tuple, Optional, Type + +from sources.exceptions import GitMissingDefinitionException, GitIncorrectPositionalOptionDefinitionException, \ + GitMissingRequiredOptionsException, GitIncorrectOptionValueException + + +@dataclass +class GitOption: + name: str + value: Any + + +@dataclass +class GitOptionDefinition: + name: str + type: type + short_name: str = field(default=None) + required: bool = field(default=False) + positional: bool = field(default=False) + position: int = field(default=None) + choices: Union[list, Type[Enum]] = field(default=None) + separator: str = field(default=' ') + + def compare_with_option(self, other: GitOption): + is_the_same = True + if self.name != other.name: + is_the_same = False + if type(other.value) != self.type: + is_the_same = False + return is_the_same + + +class GitOptions: + definitions: List[GitOptionDefinition] + + def __init__(self): + self.definitions = list() + + def get_definition(self, option: GitOption): + found_definition = None + for definition in self.definitions: + if definition.compare_with_option(option): + found_definition = definition + break + return found_definition + + def validate(self, options: Union[GitOption, List[GitOption]]): + if isinstance(options, GitOption): + options = [options] + for index, option in enumerate(options): + definition = self.get_definition(option) + if not definition: + raise GitMissingDefinitionException( + f'definition for "{option.name}" of type "{type(option.value).__name__}" was not found') + if not self.validate_choices(option, definition): + raise GitIncorrectOptionValueException( + f'value "{option.value}" is not on choices list {definition.choices}') + all_positional_list_options_correct, incorrect_option_name = self.validate_positional_list() + if not all_positional_list_options_correct: + raise GitIncorrectPositionalOptionDefinitionException( + f'positional option "{incorrect_option_name}" of type "list" only can be defined as the last option') + all_required_present, missing_required = self.validate_required(options) + if not all_required_present: + raise GitMissingRequiredOptionsException(f'some required options are missing {missing_required}') + + def validate_positional_list(self) -> Tuple[bool, Optional[str]]: + positions = [definition.position for definition in self.definitions if definition.positional] + positions = sorted(positions) + last_position = positions[-1] + for definition in self.definitions: + if definition.type == list and definition.position != last_position: + return False, definition.name + return True, None + + def validate_required(self, options: Union[GitOption, List[GitOption]]) -> Tuple[bool, List[str]]: + required_definitions = [definition.name for definition in self.definitions if definition.required] + for option in options: + if option.name in required_definitions: + required_definitions.remove(option.name) + return len(required_definitions) == 0, required_definitions + + def validate_choices(self, option: GitOption, definition: GitOptionDefinition = None): + if definition is None: + definition = self.get_definition(option) + if definition.choices is None: + return True + return option.value in [choice.value if isinstance(choice, Enum) else choice for choice in definition.choices] + + def __transform_positional_options_to_command(self, positional_options: List[GitOption]): + command = [] + positional_order = [definition for definition in self.definitions if definition.positional] + positional_order = sorted(positional_order, key=lambda positional: positional.position) + positional_order = [positional.name for positional in positional_order] + for positional_name in positional_order: + for positional_option in positional_options: + if positional_name == positional_option.name: + command.append(positional_option.value) + positional_options.remove(positional_option) + break + return command + + def transform_to_command(self, options: Union[GitOption, List[GitOption]]): + positional_options = [] + command = [] + for option in options: + definition = self.get_definition(option) + if definition.positional: + positional_options.append(option) + continue + if definition.short_name is not None: + command.append(f'-{definition.short_name}') + else: + command.append(f'--{definition.name}') + if definition.type is not bool: + if definition.separator == ' ': + command.append(option.value) + else: + command[-1] = f'{command[-1]}{definition.separator}{option.value}' + command.extend(self.__transform_positional_options_to_command(positional_options)) + return command diff --git a/sources/options/pull_options.py b/sources/options/pull_options.py new file mode 100644 index 0000000..35a09c3 --- /dev/null +++ b/sources/options/pull_options.py @@ -0,0 +1,34 @@ +from enum import Enum + +from sources.options.options import GitOptions, GitOptionDefinition + + +class PullOptionsDefinitions(GitOptions): + class Options(Enum): + QUIET = 'quiet' + VERBOSE = 'verbose' + RECURSE_SUBMODULES = 'recurse-submodules' + REPOSITORY = 'repository' + REFSPEC = 'refspec' + + class RecurseSubmodulesChoices(Enum): + YES = 'yes' + ON_DEMAND = 'on-demand' + NO = 'no' + + @classmethod + def create_from_value(cls, value: str): + for element in cls: + if element.value == value: + return element + + def __init__(self): + super().__init__() + self.definitions = [ + GitOptionDefinition(name=self.Options.QUIET.value, type=bool, short_name='q'), + GitOptionDefinition(name=self.Options.VERBOSE.value, type=bool, short_name='v'), + GitOptionDefinition(name=self.Options.RECURSE_SUBMODULES.value, type=str, + choices=self.RecurseSubmodulesChoices, separator='='), + GitOptionDefinition(name=self.Options.REPOSITORY.value, type=str, positional=True, position=0), + GitOptionDefinition(name=self.Options.REFSPEC.value, type=str, positional=True, position=1), + ] diff --git a/sources/utils/__init__.py b/sources/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sources/utils/path_util.py b/sources/utils/path_util.py new file mode 100644 index 0000000..78944d0 --- /dev/null +++ b/sources/utils/path_util.py @@ -0,0 +1,16 @@ +from pathlib import Path +from typing import Union + + +class PathUtil: + @staticmethod + def convert_to_path(path: Union[str, Path]) -> Path: + if isinstance(path, str): + path = Path(path) + return path + + @staticmethod + def convert_to_string(path: Union[str, Path]) -> str: + if isinstance(path, Path): + path = str(path.absolute()) + return path From 9c95e7791921cae99f52e2f632993cba396e345e Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Fri, 1 Sep 2023 22:16:29 +0200 Subject: [PATCH 004/119] Add tests folder --- tests/__init__.py | 0 tests/acceptance_tests/__init__.py | 0 tests/acceptance_tests/features/__init__.py | 0 .../features/basic_operations.feature | 14 ++++ .../acceptance_tests/features/environment.py | 11 +++ .../features/steps/__init__.py | 0 .../features/steps/basic_operations.py | 72 +++++++++++++++++++ 7 files changed, 97 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/acceptance_tests/__init__.py create mode 100644 tests/acceptance_tests/features/__init__.py create mode 100644 tests/acceptance_tests/features/basic_operations.feature create mode 100644 tests/acceptance_tests/features/environment.py create mode 100644 tests/acceptance_tests/features/steps/__init__.py create mode 100644 tests/acceptance_tests/features/steps/basic_operations.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/acceptance_tests/__init__.py b/tests/acceptance_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/acceptance_tests/features/__init__.py b/tests/acceptance_tests/features/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/acceptance_tests/features/basic_operations.feature b/tests/acceptance_tests/features/basic_operations.feature new file mode 100644 index 0000000..5755f9d --- /dev/null +++ b/tests/acceptance_tests/features/basic_operations.feature @@ -0,0 +1,14 @@ +Feature: git basic operations + Scenario: Add a new file + Given new git repository + And new file has been created + When executing 'git add' on file + Then new file will be added to the git tracking + + Scenario: Remove a file + Given new git repository + And new file has been created + And new file has been added to tracking + And commit changes + When executing 'git rm' on file + Then the file will be removed from the git tracking \ No newline at end of file diff --git a/tests/acceptance_tests/features/environment.py b/tests/acceptance_tests/features/environment.py new file mode 100644 index 0000000..ebaa69a --- /dev/null +++ b/tests/acceptance_tests/features/environment.py @@ -0,0 +1,11 @@ +import shutil + +from behave.model import Scenario +from behave.runner import Context + +from sources.git import GitRepository + + +def after_scenario(context: Context, scenario: Scenario): + repository: GitRepository = context.repository + shutil.rmtree(repository.path, ignore_errors=True) diff --git a/tests/acceptance_tests/features/steps/__init__.py b/tests/acceptance_tests/features/steps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/acceptance_tests/features/steps/basic_operations.py b/tests/acceptance_tests/features/steps/basic_operations.py new file mode 100644 index 0000000..4680a7e --- /dev/null +++ b/tests/acceptance_tests/features/steps/basic_operations.py @@ -0,0 +1,72 @@ +import logging +import platform +import uuid +from pathlib import Path + +from behave import given, when, then +from behave.runner import Context + +from sources.exceptions import GitException +from sources.git import GitRepository, Commit + + +@given("new git repository") +def step_impl(context: Context): + hash_name = str(uuid.uuid4()) + if platform.system().lower() == 'windows': + path = Path(r'') + else: + path = Path(fr'/tmp/git-repository-{hash_name}') + logging.info(f'Git Repository: {path}') + path.mkdir(parents=True, exist_ok=True) + repository = GitRepository.init(path=path) + context.repository = repository + + +@given("new file has been created") +def step_impl(context: Context): + repository: GitRepository = context.repository + new_file = repository.path.joinpath('add_file.txt') + logging.info(f'Create a new file: {new_file}') + with new_file.open('w') as file: + file.write(f'Add new file for testing "git add" command.\r\n') + context.new_file = new_file + + +@when("executing 'git add' on file") +@given("new file has been added to tracking") +def step_impl(context: Context): + repository: GitRepository = context.repository + repository.add(context.new_file) + + +@given("commit changes") +def step_impl(context: Context): + repository: GitRepository = context.repository + repository.git_command.execute(['commit', '-m', 'Add new file']) + + +@then("new file will be added to the git tracking") +def step_impl(context: Context): + new_file: Path = context.new_file + repository: GitRepository = context.repository + output = repository.git_command.execute(['ls-files', '--error-unmatch', new_file.name]) + assert new_file.name in output + + +@when("executing 'git rm' on file") +def step_impl(context: Context): + new_file: Path = context.new_file + repository: GitRepository = context.repository + repository.rm(new_file) + + +@then("the file will be removed from the git tracking") +def step_impl(context: Context): + new_file: Path = context.new_file + repository: GitRepository = context.repository + try: + repository.git_command.execute(['ls-files', '--error-unmatch', new_file.name]) + assert False + except GitException: + assert True From d414600c72ddf97fe203820f906cd52d2303f657 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Fri, 1 Sep 2023 22:19:23 +0200 Subject: [PATCH 005/119] Update commit variable in Branch class --- sources/git.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sources/git.py b/sources/git.py index 4c27d2a..56bc6c0 100644 --- a/sources/git.py +++ b/sources/git.py @@ -406,13 +406,13 @@ def __init__(self, name: str, commit: Union[str, Commit]): @property def commit(self): - return self.commit + return self.__commit @commit.setter def commit(self, commit: Union[str, Commit]): if isinstance(commit, str): commit = Commit(commit_hash=commit, message='', author=None, date=None) - self.commit = commit + self.__commit = commit @dataclass From eae61046912166ad90c93055b3770cd606d3dfd8 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Fri, 1 Sep 2023 22:19:48 +0200 Subject: [PATCH 006/119] Add PyLinter --- .pylintrc | 628 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 628 insertions(+) create mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..a3788c3 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,628 @@ +[MAIN] + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. +ignore-paths= + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.10 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=exception, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +#typealias-rgx= + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + asyncSetUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=120 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work.. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io From e5643f5a29c5c794a4ea9c8496b0b2d564a147ee Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Fri, 1 Sep 2023 22:20:19 +0200 Subject: [PATCH 007/119] Add setup.py --- setup.py | 435 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 435 insertions(+) create mode 100644 setup.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d86258c --- /dev/null +++ b/setup.py @@ -0,0 +1,435 @@ +#!/usr/bin/env python3 + +import io +import os +import subprocess +import sys +from pathlib import Path +from typing import NoReturn, Union, Set, List + +from setuptools import find_packages, setup, Command + +# In that example author and maintainer is the same person. +NAME = 'py-git' +DESCRIPTION = 'Python Git CLI Wrapper' +URL = 'https://github.com/dl1998/PyGit' +EMAIL = 'dima.leschenko1998@gmail.com' +AUTHOR = 'Dmytro Leshchenko' +REQUIRES_PYTHON = '>=3.6.0' +VERSION = '0.0.1' +RELEASE = VERSION + +REQUIRED = [ + +] + +EXTRAS = [ + +] + +here = os.path.abspath(os.path.dirname(__file__)) + +long_description = '' + +try: + with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as file: + long_description = file.read() +except FileNotFoundError: + long_description = DESCRIPTION + +about = {} + +used_python = 'python' +try: + used_python = os.environ['USED_PYTHON'] +except KeyError: + if 'prepare_venv' in sys.argv: + print('USED_PYTHON not found in environment variables, default python version will be used.') + +if not VERSION: + project_slug = NAME.lower().replace("-", "_").replace(" ", "_") + with open(os.path.join(here, 'sources', project_slug, '__version__.py')) as file: + exec(file.read(), about) +else: + about['__version__'] = VERSION + + +def check_status_code(status_code, successful_message, error_message) -> NoReturn: + """ + Check status code and based on status code print one of two messages. + + :raises CommandExecutionException: If status code different from 0. + :param status_code: Checked status code, returned by command. + :type status_code: int + :param successful_message: Message that will be printed, if status code equals to 0. + :type successful_message: str + :param error_message: Message that will be printed, if status code is different from 0. + :type error_message: str + """ + if status_code == 0: + print(successful_message) + else: + raise CommandExecutionException(error_message) + + +class UnknownOSException(Exception): + """ + Unknown operating system exception. + """ + pass + + +class CommandExecutionException(Exception): + """ + Exception that will be used, if error occurred in command execution. + """ + pass + + +class PrepareVirtualEnvironmentCommand(Command): + """ + Support setup.py create venv and install requirements. + """ + + description = 'Create venv and install requirements from requirements.txt file.' + user_options = [] + + @staticmethod + def status(text: str) -> NoReturn: + """ + Method will be used for printing information to console. + + :param text: Parameter that will be printed to console. + :type text: str + """ + print(text) + + def initialize_options(self) -> NoReturn: + """ + If command receive arguments, then method can be used to set default values. + """ + pass + + def finalize_options(self) -> NoReturn: + """ + If command receive arguments, then method can be used to set final values for arguments. + """ + pass + + def __create_venv(self, venv_path: str) -> NoReturn: + """ + Create python virtual environment. + + :raises CommandExecutionException: If error occurred during the command execution. + :param venv_path: Path to place where located virtual environment. + :type venv_path: str + """ + self.status('Create venv') + status_code = os.system(f'{used_python} -m venv {venv_path}') + successful_message = 'Venv successfully created' + error_message = 'Error occurred during venv creation, installation will not be executed' + check_status_code(status_code, successful_message, error_message) + + def __install_requirements(self, venv_path: str) -> NoReturn: + """ + Install requirements from requirements.txt file. + + :raises CommandExecutionException: If error occurred during the command execution. + :path venv_path: Virtual environment for which install requirements. + :type venv_path: str + """ + requirements_file_path = os.path.join(here, 'requirements.txt') + venv_pip = None + if sys.platform == 'linux': + venv_pip = os.path.join(venv_path, 'bin', 'pip3') + elif sys.platform == 'win32': + venv_pip = os.path.join(venv_path, 'Scripts', 'pip3') + else: + raise UnknownOSException(f'Unknown operation system: {sys.platform}.') + self.status('Check requirements.txt') + requirements_exists = os.path.exists(requirements_file_path) + if requirements_exists: + self.status('Install requirements from requirements.txt') + status_code = os.system(f'{venv_pip} install --upgrade --requirement requirements.txt') + successful_message = 'Installation completed successfully' + error_message = 'Error occurred during the installation' + check_status_code(status_code, successful_message, error_message) + else: + self.status('Requirements file not found') + + def run(self) -> NoReturn: + """ + Create virtual environment and install all needed requirements. + """ + venv_path = os.path.join(here, 'venv') + try: + self.__create_venv(venv_path) + self.__install_requirements(venv_path) + except CommandExecutionException as execution_exception: + print(execution_exception) + except UnknownOSException as unknown_os_exception: + print(unknown_os_exception) + + +class BuildDocsCommand(Command): + """ + Custom command to build documentation with Sphinx + """ + doc_dir: Path + + user_options = [ + ('doc-dir=', None, 'The documentation output directory'), + ] + + def initialize_options(self): + self.doc_dir = None + + def finalize_options(self) -> None: + self.doc_dir = Path(self.doc_dir) if self.doc_dir else Path(__file__).parent.joinpath('docs') + + def run(self): + build_command = [ + 'sphinx-build', + '-b', 'html', + '-d', str(self.doc_dir.joinpath('_build/doctrees')), + '-j', 'auto', + str(self.doc_dir), + str(self.doc_dir.joinpath('_build/html')) + ] + + try: + subprocess.check_call(build_command) + except subprocess.CalledProcessError as e: + print(f"Error: Failed to build documentation with Sphinx: {e}") + raise + + +class SphinxGenerate(Command): + """ + Class responsible for Sphinx project generation. + """ + user_options = [ + ('name', None, 'The project name'), + ('author', None, 'The project author'), + ('version', None, 'The project version'), + ('release', None, 'The project release'), + ] + + def initialize_options(self) -> NoReturn: + """ + If command receive arguments, then method can be used to set default values. + """ + self.name = None + self.author = None + self.version = None + self.release = None + + def finalize_options(self) -> NoReturn: + """ + If command receive arguments, then method can be used to set final values for arguments. + """ + pass + + def run(self) -> None: + generate_command = [ + 'sphinx-build', + '-b', 'html', + '-d', str(self.doc_dir.joinpath('_build/doctrees')), + '-j', 'auto', + str(self.doc_dir), + str(self.doc_dir.joinpath('_build/html')) + ] + + try: + subprocess.check_call(generate_command) + except subprocess.CalledProcessError as e: + print(f"Error: Failed to generate documentation with Sphinx: {e}") + raise + + +class SphinxAutoDoc(Command): + """ + Class responsible for configuration of the Sphinx project and documentation generation. + """ + + description = 'Generate documentation for modules.' + user_options = [ + ('with-magic-methods', None, 'Include magic packages to documentation.'), + ('with-private-methods', None, 'Include private methods to documentation'), + ('generate-project', None, 'Generate Sphinx project based on configuration.'), + ('separate', None, 'Separate modules from sub-modules.'), + ('automodules=', None, 'Coma-separated list of auto-modules.') + ] + + with_magic_methods: bool + with_private_methods: bool + generate_project: bool + separate: bool + automodules: str + + @staticmethod + def __get_boolean_value(variable: Union[int, bool]) -> bool: + """ + Get boolean value from status code. + + :param variable: Checked variable. + :type variable: Union[int, bool] + :return: True or False for status code and default value for None. + :rtype: bool + """ + if variable == 1: + return True + elif variable == 0: + return False + else: + return variable + + def initialize_options(self) -> NoReturn: + """ + If command receive arguments, then method can be used to set default values. + """ + self.with_magic_methods = False + self.with_private_methods = False + self.generate_project = False + self.separate = False + self.automodules = "" + + def finalize_options(self) -> NoReturn: + """ + If command receive arguments, then method can be used to set final values for arguments. + """ + self.with_magic_methods = self.__get_boolean_value(self.with_magic_methods) + self.with_private_methods = self.__get_boolean_value(self.with_private_methods) + self.generate_project = self.__get_boolean_value(self.generate_project) + self.separate = self.__get_boolean_value(self.separate) + + @staticmethod + def __get_magic_packages(sources_path: str) -> Set[str]: + """ + Get all magic packages from sources. + + :param sources_path: Path to sources. + :type sources_path: str + :return: Set of magic methods. + :rtype: Set[str] + """ + magic_packages = set() + for root, folders, files in os.walk(sources_path): + for file in files: + if file.startswith('__') and file.endswith('__.py'): + magic_packages.add(file) + return magic_packages + + def __get_magic_excludes(self, sources_path: str) -> List[str]: + """ + Get list of fnmatch excludes. + + :param sources_path: Path to sources folder. + :type sources_path: str + :return: Fnmatch excludes list. + :rtype: List[str] + """ + excludes = [] + magic_packages = self.__get_magic_packages(sources_path) + for exclude in magic_packages: + excludes.append(f'*{exclude}') + if '*__init__.py' in excludes: + excludes.remove('*__init__.py') + return excludes + + def __print_configuration(self) -> NoReturn: + """ + Print configuration used to generate documentation. + """ + print('Generate documentation configuration:') + print(f'\tGenerate project - {self.generate_project}') + print(f'\tSeparate modules from sub-modules - {self.separate}') + print(f'\tAdd private methods - {self.with_private_methods}') + print(f'\tAdd magic methods - {self.with_magic_methods}') + print(f'\tSelected auto-modules - {self.automodules}') + + def run(self) -> NoReturn: + """ + Generate Sphinx project and documentation. + """ + docs_path = os.path.join(here, 'docs') + templates = os.path.join(docs_path, '_documentation_templates') + sources_path = os.path.join(here, 'sources') + rel_sources_path = os.path.relpath(sources_path, here) + self.__print_configuration() + cmd = ['sphinx-apidoc', '--templatedir', templates, '--force', '-o', docs_path] + if self.generate_project: + cmd.extend(['--full', '-a', '-H', NAME, '-A', AUTHOR, '-V', VERSION, '-R', RELEASE]) + if self.with_private_methods: + if self.automodules: + os.environ['SPHINX_APIDOC_OPTIONS'] = self.automodules + cmd.append('--private') + if self.separate: + cmd.append('--separate') + cmd.append(rel_sources_path) + if not self.with_magic_methods: + exclude_packages = ' '.join(self.__get_magic_excludes(sources_path)) + cmd.append(exclude_packages) + print('Generate documentation for modules.') + process = subprocess.Popen(cmd) + process.communicate() + successful_message = 'Documentation for modules was successfully generated.' + error_message = 'Error occurred during the documentation generation.' + check_status_code(process.returncode, successful_message, error_message) + + +cmd_class = { + 'prepare-venv': PrepareVirtualEnvironmentCommand, + 'sphinx-generate-project': SphinxGenerate, + 'sphinx-update-modules': SphinxAutoDoc, + 'sphinx-build': BuildDocsCommand +} + +command_options = { + +} + + +def parse_requirements(file_path): + with open(file_path, 'r') as f: + requirements = f.read().splitlines() + return requirements + + +# Path to the requirements.txt file +requirements_path = 'requirements.txt' + +# Parse the requirements from requirements.txt +requirements = parse_requirements(requirements_path) + +setup( + name=NAME, + version=about['__version__'], + description=DESCRIPTION, + long_description=long_description, + author=AUTHOR, + author_email=EMAIL, + maintainer=AUTHOR, + maintainer_email=EMAIL, + python_requires=REQUIRES_PYTHON, + install_requires=requirements, + url=URL, + platforms=['any'], + packages=find_packages(exclude=['tests', '*.tests', '*.tests.*', 'tests.*']), + include_package_data=True, + license='MIT', + classifiers=[ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + ], + keywords=['git', 'version control', 'sources control', 'Git wrapper', 'Git CLI', 'Git commands', 'Git automation', + 'Git interface', 'Git integration', 'Git management', 'Git utility', 'Git interaction', 'Git convenience', + 'Git operations', 'Git workflow'], + cmdclass=cmd_class, + command_options=command_options, +) From fcd669d44a951f549d94a7faaa9ae78669220514 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Fri, 1 Sep 2023 22:20:43 +0200 Subject: [PATCH 008/119] Add changelog file --- CHANGELOG.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..224807d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,42 @@ +# Change Log +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +## [Unreleased] - 2023-09-01 + +### Added +- Basic functionality + - General + - Read Git configuration + - Update Git configuration + - Execute raw Git command + - Repository + - Init repository + - Clone repository + - Use existing repository + - Change management + - Add files/folders + - Remove files/folders + - Move files/folders + - Create a new commit + - Create a new tag + - Create a new branch + - Switch between branches + - Pull changes + - Push changes + - Access information about repository + - List of branches + - List of tags + - List of commits + - Active branch +- README.md +- LICENSE.md +- CHANGELOG.md +- requirements.txt (Python Dependencies) +- .pylintrc (PyLinter Configuration) +- Sphinx Documentation Configuration +- Basic Tests + +[unreleased]: https://github.com/dl1998/PyGit From 51f58fc4ce4a6fb057ec663fada5696ffbe71db6 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 21:45:39 +0200 Subject: [PATCH 009/119] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 98c994a..18c5b2b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # PyGit -Python Git CLI Wrapper + +Python Git CLI wrapper, provides various classes that simplifies interaction with the Git using Python. + +It provides basic Git functionality that could be accessed using pre-defined models. From 90f8b96d17baddd0c5548b9d9d4adc55aa4b2f4b Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 21:46:17 +0200 Subject: [PATCH 010/119] Update PyLinter configuration --- .pylintrc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pylintrc b/.pylintrc index a3788c3..50350fb 100644 --- a/.pylintrc +++ b/.pylintrc @@ -67,7 +67,7 @@ ignored-modules= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). -#init-hook= +init-hook='import sys; sys.path.append("sources")' # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the # number of processors available to use, and will cap the count on Windows to @@ -209,7 +209,7 @@ method-naming-style=snake_case # Regular expression matching correct method names. Overrides method-naming- # style. If left empty, method names will be checked with the set naming style. -#method-rgx= +method-rgx=([^\\W\\dA-Z][^\\WA-Z]{1,}|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$ # Naming style matching correct module names. module-naming-style=snake_case @@ -275,7 +275,7 @@ valid-metaclass-classmethod-first-arg=mcs # List of regular expressions of class ancestor names to ignore when counting # public methods (see R0903) -exclude-too-few-public-methods= +exclude-too-few-public-methods=Enum # List of qualified class names to ignore when counting class parents (see # R0901) From 979e8868c36840ee6d8c923567bbc44f05cf0b5b Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 21:47:39 +0200 Subject: [PATCH 011/119] Update base class for GitCommand --- sources/options/options.py | 181 +++++++++++++++++++++++++++++++------ 1 file changed, 154 insertions(+), 27 deletions(-) diff --git a/sources/options/options.py b/sources/options/options.py index e714092..ff00742 100644 --- a/sources/options/options.py +++ b/sources/options/options.py @@ -1,6 +1,9 @@ +""" +Module contains base classes for git command options. +""" from dataclasses import dataclass, field from enum import Enum -from typing import Dict, List, Any, Union, Tuple, Optional, Type +from typing import List, Any, Union, Tuple, Optional, Type, NoReturn from sources.exceptions import GitMissingDefinitionException, GitIncorrectPositionalOptionDefinitionException, \ GitMissingRequiredOptionsException, GitIncorrectOptionValueException @@ -8,14 +11,21 @@ @dataclass class GitOption: + """ + Class represents one git command option, it consists from the option name and the value. + """ name: str value: Any @dataclass class GitOptionDefinition: - name: str - type: type + """ + Class defines git option for the git command. It describes option behaviour and some attributes applicable for this + option. + """ + name: Union[str, 'CommandOptions'] + type: Union[Type, Tuple] short_name: str = field(default=None) required: bool = field(default=False) positional: bool = field(default=False) @@ -23,22 +33,51 @@ class GitOptionDefinition: choices: Union[list, Type[Enum]] = field(default=None) separator: str = field(default=' ') - def compare_with_option(self, other: GitOption): + def __post_init__(self): + if isinstance(self.name, CommandOptions): + self.name = self.name.value + + def compare_with_option(self, other: GitOption) -> bool: + """ + Method compares option definition with option itself. It validates the value of the option and checks that it is + the same as the one that is defined in the option definition. All options are matched by their name. + + :param other: Git option that should be compared with current definition. + :type other: GitOption + :return: True, if option is matching the definition, otherwise False. + """ is_the_same = True + if isinstance(self.type, tuple): + types = self.type + else: + types = tuple([self.type]) if self.name != other.name: is_the_same = False - if type(other.value) != self.type: + if type(other.value) not in types: is_the_same = False return is_the_same -class GitOptions: +class GitCommand: + """ + Class defines a git command and stores all options definitions for this command. + """ + command_name: str definitions: List[GitOptionDefinition] - def __init__(self): - self.definitions = list() + def __init__(self, subcommand_name: str): + self.command_name = subcommand_name + self.definitions = [] + + def get_definition(self, option: GitOption) -> Optional[GitOptionDefinition]: + """ + Method receives git option and searches through all command definitions for the definition that is matching this + option. - def get_definition(self, option: GitOption): + :param option: Git option for which it shall return its definition. + :type option: GitOption + :return: Git option definition, if it was found, otherwise None. + """ found_definition = None for definition in self.definitions: if definition.compare_with_option(option): @@ -46,10 +85,22 @@ def get_definition(self, option: GitOption): break return found_definition - def validate(self, options: Union[GitOption, List[GitOption]]): + def validate(self, options: Union[GitOption, List[GitOption]]) -> NoReturn: + """ + Validate git options with their definitions. + + :param options: List of options for this command that will be validated. + :type options: Union[GitOption, List[GitOption]] + :raises GitMissingDefinitionException: If option doesn't have definition. + :raises GitIncorrectOptionValueException: If option has choices, then check that the value is present on the + choices list. + :raises GitIncorrectPositionalOptionDefinitionException: If there is a positional option of the list type that + is not defined on the last position. + :raises GitMissingRequiredOptionsException: If not all required options are present. + """ if isinstance(options, GitOption): options = [options] - for index, option in enumerate(options): + for option in options: definition = self.get_definition(option) if not definition: raise GitMissingDefinitionException( @@ -66,6 +117,13 @@ def validate(self, options: Union[GitOption, List[GitOption]]): raise GitMissingRequiredOptionsException(f'some required options are missing {missing_required}') def validate_positional_list(self) -> Tuple[bool, Optional[str]]: + """ + Method checks that there are no positional options of the list type that are defined not on the last position. + + :return: Tuple with boolean and optional string, boolean contains True if everything is correct and False if + there is an incorrect option. Optional string contains name of the definition which failed the check, where if + all positional options passed the check, then None will be returned. + """ positions = [definition.position for definition in self.definitions if definition.positional] positions = sorted(positions) last_position = positions[-1] @@ -75,48 +133,117 @@ def validate_positional_list(self) -> Tuple[bool, Optional[str]]: return True, None def validate_required(self, options: Union[GitOption, List[GitOption]]) -> Tuple[bool, List[str]]: + """ + Method checks that all required options are present. + + :param options: List of provided options for the command. + :type options: Union[GitOption, List[GitOption]] + :return: Tuple with boolean and list of strings. Boolean is set as True, if all required options are present, + otherwise it is set as False. List of string contains name of the options that are required, but they are + missing. + """ required_definitions = [definition.name for definition in self.definitions if definition.required] for option in options: if option.name in required_definitions: required_definitions.remove(option.name) return len(required_definitions) == 0, required_definitions - def validate_choices(self, option: GitOption, definition: GitOptionDefinition = None): + def validate_choices(self, option: GitOption, definition: Optional[GitOptionDefinition] = None) -> bool: + """ + Method checks that option value is one of the values on the choices list, where if choices list is None, then + it can be any value. + + :param option: An option that will be validated. + :type option: GitOption + :param definition: Definition for the provided option. + :type definition: Optional[GitOptionDefinition] + :return: True, if definition choices are None or option value is on the choices list, otherwise return False. + """ if definition is None: definition = self.get_definition(option) if definition.choices is None: return True return option.value in [choice.value if isinstance(choice, Enum) else choice for choice in definition.choices] - def __transform_positional_options_to_command(self, positional_options: List[GitOption]): + def __transform_positional_options_to_command(self, positional_options: List[GitOption]) -> List[Union[str, int]]: + """ + Method transforms positional options into commands list. + + :param positional_options: List of positional options. + :type positional_options: List[GitOption] + :return: Transformed list with positional options. + """ command = [] positional_order = [definition for definition in self.definitions if definition.positional] positional_order = sorted(positional_order, key=lambda positional: positional.position) positional_order = [positional.name for positional in positional_order] for positional_name in positional_order: for positional_option in positional_options: - if positional_name == positional_option.name: - command.append(positional_option.value) - positional_options.remove(positional_option) - break + if positional_name != positional_option.name: + continue + if isinstance(positional_option.value, list): + values = positional_option.value + else: + values = [positional_option.value] + for value in values: + command.append(value) + positional_options.remove(positional_option) + break return command - def transform_to_command(self, options: Union[GitOption, List[GitOption]]): + def transform_to_command(self, options: Union[GitOption, List[GitOption]]) -> List[Union[str, int]]: + """ + Transform git option objects into list of string and integer options. + + :param options: List of git option objects. + :type options: Union[GitOption, List[GitOption]] + :return: Transformed git command, that consists from command and its options. + """ positional_options = [] - command = [] + command = [self.command_name] for option in options: definition = self.get_definition(option) if definition.positional: positional_options.append(option) continue - if definition.short_name is not None: - command.append(f'-{definition.short_name}') - else: - command.append(f'--{definition.name}') - if definition.type is not bool: - if definition.separator == ' ': - command.append(option.value) + if not isinstance(option.value, bool) or option.value is not False: + if definition.short_name is not None: + command.append(f'-{definition.short_name}') else: - command[-1] = f'{command[-1]}{definition.separator}{option.value}' + command.append(f'--{definition.name}') + if not isinstance(option.value, bool) and definition.separator == ' ': + command.append(option.value) + elif not isinstance(option.value, bool): + command[-1] = f'{command[-1]}{definition.separator}{option.value}' command.extend(self.__transform_positional_options_to_command(positional_options)) return command + + +class CommandOptions(Enum): + """ + Base class for git options enum. + """ + + @classmethod + def create_from_value(cls, value: str) -> Optional['CommandOptions']: + """ + Create class instance based on the value. + + :param value: Enum value from the class. + :type value: str + :return: Optional[CommandOptions] + """ + for element in cls: + if element.value == value: + return element + return None + + def create_option(self, value: Any) -> GitOption: + """ + Create GitOption with provided value from the option class instance. + + :param value: A value for GitOption. + :type value: Any + :return: A new GitOption object for this option with the provided value. + """ + return GitOption(name=self.value, value=value) From c20c70337e90da71724200fbec0251da43083aa2 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 21:48:53 +0200 Subject: [PATCH 012/119] Update PullCommandDefinitions class to be compliant with the base class --- sources/options/pull_options.py | 50 ++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/sources/options/pull_options.py b/sources/options/pull_options.py index 35a09c3..8f4ae2b 100644 --- a/sources/options/pull_options.py +++ b/sources/options/pull_options.py @@ -1,34 +1,50 @@ -from enum import Enum +""" +Module contains classes that defines options that could be configured for 'git pull' command. +Reference: https://git-scm.com/docs/git-pull +""" +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions -from sources.options.options import GitOptions, GitOptionDefinition +class PullCommandDefinitions(GitCommand): + """ + Options definitions class for 'git pull' command, it contains definitions of the options for 'git pull'. + """ -class PullOptionsDefinitions(GitOptions): - class Options(Enum): + class Options(CommandOptions): + """ + Options class for 'git pull' command, it contains options that can be configured. + """ QUIET = 'quiet' VERBOSE = 'verbose' RECURSE_SUBMODULES = 'recurse-submodules' + COMMIT = 'commit' + FAST_FORWARD_ONLY = 'ff-only' + FAST_FORWARD = 'ff' + ALL = 'all' + FORCE = 'force' REPOSITORY = 'repository' REFSPEC = 'refspec' - class RecurseSubmodulesChoices(Enum): + class RecurseSubmodulesChoices(CommandOptions): + """ + Class represents choices enum for recurse-submodule option. + """ YES = 'yes' ON_DEMAND = 'on-demand' NO = 'no' - @classmethod - def create_from_value(cls, value: str): - for element in cls: - if element.value == value: - return element - def __init__(self): - super().__init__() + super().__init__('pull') self.definitions = [ - GitOptionDefinition(name=self.Options.QUIET.value, type=bool, short_name='q'), - GitOptionDefinition(name=self.Options.VERBOSE.value, type=bool, short_name='v'), - GitOptionDefinition(name=self.Options.RECURSE_SUBMODULES.value, type=str, + GitOptionDefinition(name=self.Options.QUIET, type=bool, short_name='q'), + GitOptionDefinition(name=self.Options.VERBOSE, type=bool, short_name='v'), + GitOptionDefinition(name=self.Options.RECURSE_SUBMODULES, type=str, choices=self.RecurseSubmodulesChoices, separator='='), - GitOptionDefinition(name=self.Options.REPOSITORY.value, type=str, positional=True, position=0), - GitOptionDefinition(name=self.Options.REFSPEC.value, type=str, positional=True, position=1), + GitOptionDefinition(name=self.Options.COMMIT, type=bool), + GitOptionDefinition(name=self.Options.FAST_FORWARD_ONLY, type=bool), + GitOptionDefinition(name=self.Options.FAST_FORWARD, type=bool), + GitOptionDefinition(name=self.Options.ALL, type=bool), + GitOptionDefinition(name=self.Options.FORCE, type=bool, short_name='f'), + GitOptionDefinition(name=self.Options.REPOSITORY, type=str, positional=True, position=0), + GitOptionDefinition(name=self.Options.REFSPEC, type=str, positional=True, position=1), ] From 4ceb98e1a1dfd62fc3f6718eecd1f00a61aca216 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 21:49:45 +0200 Subject: [PATCH 013/119] Add a new class with definition of the 'git add' command --- sources/options/add_options.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 sources/options/add_options.py diff --git a/sources/options/add_options.py b/sources/options/add_options.py new file mode 100644 index 0000000..c5fb11c --- /dev/null +++ b/sources/options/add_options.py @@ -0,0 +1,28 @@ +""" +Module contains classes that defines options that could be configured for 'git add' command. +Reference: https://git-scm.com/docs/git-add +""" +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions + + +class AddCommandDefinitions(GitCommand): + """ + Options definitions class for 'git add' command, it contains definitions of the options for 'git add'. + """ + class Options(CommandOptions): + """ + Options class for 'git add' command, it contains options that can be configured. + """ + VERBOSE = 'verbose' + FORCE = 'force' + UPDATE = 'update' + PATHSPEC = 'pathspec' + + def __init__(self): + super().__init__('add') + self.definitions = [ + GitOptionDefinition(name=self.Options.VERBOSE, type=bool, short_name='v'), + GitOptionDefinition(name=self.Options.FORCE, type=bool, short_name='f'), + GitOptionDefinition(name=self.Options.UPDATE, type=bool, short_name='u'), + GitOptionDefinition(name=self.Options.PATHSPEC, type=list, positional=True, position=0), + ] From 8dc2a58b68356e1da586a9f7155f549078ea5e31 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 21:50:03 +0200 Subject: [PATCH 014/119] Add a new class with definition of the 'git clone' command --- sources/options/clone_options.py | 44 ++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 sources/options/clone_options.py diff --git a/sources/options/clone_options.py b/sources/options/clone_options.py new file mode 100644 index 0000000..65adcd3 --- /dev/null +++ b/sources/options/clone_options.py @@ -0,0 +1,44 @@ +""" +Module contains classes that defines options that could be configured for 'git clone' command. +Reference: https://git-scm.com/docs/git-clone +""" +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions + + +class CloneCommandDefinitions(GitCommand): + """ + Options definitions class for 'git clone' command, it contains definitions of the options for 'git clone'. + """ + class Options(CommandOptions): + """ + Options class for 'git clone' command, it contains options that can be configured. + """ + VERBOSE = 'verbose' + QUIET = 'quiet' + LOCAL = 'local' + NO_HARDLINKS = 'no-hardlinks' + SHARED = 'shared' + BARE = 'bare' + ORIGIN = 'origin' + BRANCH = 'branch' + NO_TAGS = 'no-tags' + RECURSE_SUBMODULES = 'recurse-submodules' + REPOSITORY = 'repository' + DIRECTORY = 'directory' + + def __init__(self): + super().__init__('clone') + self.definitions = [ + GitOptionDefinition(name=self.Options.VERBOSE, type=bool, short_name='v'), + GitOptionDefinition(name=self.Options.QUIET, type=bool, short_name='q'), + GitOptionDefinition(name=self.Options.LOCAL, type=bool, short_name='l'), + GitOptionDefinition(name=self.Options.NO_HARDLINKS, type=bool), + GitOptionDefinition(name=self.Options.SHARED, type=bool, short_name='s'), + GitOptionDefinition(name=self.Options.BARE, type=bool), + GitOptionDefinition(name=self.Options.ORIGIN, type=bool, short_name='o'), + GitOptionDefinition(name=self.Options.BRANCH, type=bool, short_name='b'), + GitOptionDefinition(name=self.Options.NO_TAGS, type=bool), + GitOptionDefinition(name=self.Options.RECURSE_SUBMODULES, type=(bool, str)), + GitOptionDefinition(name=self.Options.REPOSITORY, type=str, positional=True, position=0, required=True), + GitOptionDefinition(name=self.Options.DIRECTORY, type=str, positional=True, position=1), + ] From abf26f1406839b72967bb4e40aad2bf7b7549f75 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 21:50:26 +0200 Subject: [PATCH 015/119] Add a new class with definition of the 'git for-each-ref' command --- sources/options/for_each_ref_options.py | 45 +++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 sources/options/for_each_ref_options.py diff --git a/sources/options/for_each_ref_options.py b/sources/options/for_each_ref_options.py new file mode 100644 index 0000000..0d0e831 --- /dev/null +++ b/sources/options/for_each_ref_options.py @@ -0,0 +1,45 @@ +""" +Module contains classes that defines options that could be configured for 'git for-each-ref' command. +Reference: https://git-scm.com/docs/git-for-each-ref +""" +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions + + +class ForEachRefCommandDefinitions(GitCommand): + """ + Options definitions class for 'git for-each-ref' command, it contains definitions of the options for + 'git for-each-ref'. + """ + class Options(CommandOptions): + """ + Options class for 'git for-each-ref' command, it contains options that can be configured. + """ + COUNT = 'count' + SORT = 'sort' + FORMAT = 'format' + POINTS_AT = 'points-at' + MERGED = 'merged' + NO_MERGED = 'no-merged' + CONTAINS = 'contains' + NO_CONTAINS = 'no-contains' + IGNORE_CASE = 'ignore-case' + OMIT_EMPTY = 'omit-empty' + EXCLUDE = 'exclude' + PATTERN = 'pattern' + + def __init__(self): + super().__init__('for-each-ref') + self.definitions = [ + GitOptionDefinition(name=self.Options.COUNT, type=str, separator='='), + GitOptionDefinition(name=self.Options.SORT, type=str, separator='='), + GitOptionDefinition(name=self.Options.FORMAT, type=str, separator='='), + GitOptionDefinition(name=self.Options.POINTS_AT, type=str, separator='='), + GitOptionDefinition(name=self.Options.MERGED, type=(bool, str), separator='='), + GitOptionDefinition(name=self.Options.NO_MERGED, type=(bool, str), separator='='), + GitOptionDefinition(name=self.Options.CONTAINS, type=(bool, str), separator='='), + GitOptionDefinition(name=self.Options.NO_CONTAINS, type=(bool, str), separator='='), + GitOptionDefinition(name=self.Options.IGNORE_CASE, type=bool), + GitOptionDefinition(name=self.Options.OMIT_EMPTY, type=bool), + GitOptionDefinition(name=self.Options.EXCLUDE, type=str, separator='='), + GitOptionDefinition(name=self.Options.PATTERN, type=str, positional=True, position=0), + ] From f274075dec9172681e91d7199b422f6e9a340f76 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 21:51:02 +0200 Subject: [PATCH 016/119] Add a new class with definition of the 'git init' command --- sources/options/init_options.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 sources/options/init_options.py diff --git a/sources/options/init_options.py b/sources/options/init_options.py new file mode 100644 index 0000000..bd4d44c --- /dev/null +++ b/sources/options/init_options.py @@ -0,0 +1,28 @@ +""" +Module contains classes that defines options that could be configured for 'git init' command. +Reference: https://git-scm.com/docs/git-init +""" +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions + + +class InitCommandDefinitions(GitCommand): + """ + Options definitions class for 'git init' command, it contains definitions of the options for 'git init'. + """ + class Options(CommandOptions): + """ + Options class for 'git init' command, it contains options that can be configured. + """ + QUIET = 'quiet' + BARE = 'bare' + INITIAL_BRANCH = 'initial-branch' + DIRECTORY = 'directory' + + def __init__(self): + super().__init__('init') + self.definitions = [ + GitOptionDefinition(name=self.Options.QUIET, type=bool, short_name='q'), + GitOptionDefinition(name=self.Options.BARE, type=bool), + GitOptionDefinition(name=self.Options.INITIAL_BRANCH, type=bool, short_name='b'), + GitOptionDefinition(name=self.Options.DIRECTORY, type=str, positional=True, position=0), + ] From f2d8ca74f54da3f76cabfc1cfacb3ffa00199801 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 21:51:41 +0200 Subject: [PATCH 017/119] Add a new class with definition of the 'git log' command --- sources/options/log_options.py | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 sources/options/log_options.py diff --git a/sources/options/log_options.py b/sources/options/log_options.py new file mode 100644 index 0000000..059f09b --- /dev/null +++ b/sources/options/log_options.py @@ -0,0 +1,38 @@ +""" +Module contains classes that defines options that could be configured for 'git log' command. +Reference: https://git-scm.com/docs/git-log +""" +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions + + +class LogCommandDefinitions(GitCommand): + """ + Options definitions class for 'git log' command, it contains definitions of the options for 'git log'. + """ + class Options(CommandOptions): + """ + Options class for 'git log' command, it contains options that can be configured. + """ + MAX_COUNT = 'max-count' + SKIP = 'skip' + BRANCHES = 'branches' + ALL = 'all' + FORMAT = 'format' + PRETTY = 'pretty' + DATE = 'date' + REVISION_RANGE = 'revision-range' + PATH = 'path' + + def __init__(self): + super().__init__('log') + self.definitions = [ + GitOptionDefinition(name=self.Options.MAX_COUNT, type=int, short_name='n'), + GitOptionDefinition(name=self.Options.SKIP, type=int), + GitOptionDefinition(name=self.Options.BRANCHES, type=(bool, str)), + GitOptionDefinition(name=self.Options.ALL, type=bool), + GitOptionDefinition(name=self.Options.FORMAT, type=str, separator='='), + GitOptionDefinition(name=self.Options.PRETTY, type=str, separator='='), + GitOptionDefinition(name=self.Options.DATE, type=str, separator='='), + GitOptionDefinition(name=self.Options.REVISION_RANGE, type=str, positional=True, position=0), + GitOptionDefinition(name=self.Options.PATH, type=str, positional=True, position=1), + ] From 8a15aa7c09bbb6ca5085e0e202c1bd3085de95aa Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 21:51:56 +0200 Subject: [PATCH 018/119] Add a new class with definition of the 'git mv' command --- sources/options/mv_options.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 sources/options/mv_options.py diff --git a/sources/options/mv_options.py b/sources/options/mv_options.py new file mode 100644 index 0000000..b029418 --- /dev/null +++ b/sources/options/mv_options.py @@ -0,0 +1,28 @@ +""" +Module contains classes that defines options that could be configured for 'git mv' command. +Reference: https://git-scm.com/docs/git-mv +""" +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions + + +class MvCommandDefinitions(GitCommand): + """ + Options definitions class for 'git mv' command, it contains definitions of the options for 'git mv'. + """ + class Options(CommandOptions): + """ + Options class for 'git mv' command, it contains options that can be configured. + """ + FORCE = 'force' + VERBOSE = 'verbose' + SOURCE = 'source' + DESTINATION = 'destination' + + def __init__(self): + super().__init__('mv') + self.definitions = [ + GitOptionDefinition(name=self.Options.FORCE, type=bool, short_name='f'), + GitOptionDefinition(name=self.Options.VERBOSE, type=bool, short_name='v'), + GitOptionDefinition(name=self.Options.SOURCE, type=str, positional=True, position=0), + GitOptionDefinition(name=self.Options.DESTINATION, type=str, positional=True, position=1), + ] From c8494fe805938040c7ed4707f7a3f070ae577163 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 21:52:19 +0200 Subject: [PATCH 019/119] Add a new class with definition of the 'git push' command --- sources/options/push_options.py | 49 +++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 sources/options/push_options.py diff --git a/sources/options/push_options.py b/sources/options/push_options.py new file mode 100644 index 0000000..92b9b5d --- /dev/null +++ b/sources/options/push_options.py @@ -0,0 +1,49 @@ +""" +Module contains classes that defines options that could be configured for 'git push' command. +Reference: https://git-scm.com/docs/git-push +""" +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions + + +class PushCommandDefinitions(GitCommand): + """ + Options definitions class for 'git push' command, it contains definitions of the options for 'git push'. + """ + + class Options(CommandOptions): + """ + Options class for 'git push' command, it contains options that can be configured. + """ + VERBOSE = 'verbose' + RECURSE_SUBMODULES = 'recurse-submodules' + ALL = 'all' + BRANCHES = 'branches' + PRUNE = 'prune' + DELETE = 'delete' + TAGS = 'tags' + REPOSITORY = 'repository' + REFSPEC = 'refspec' + + class RecurseSubmodulesChoices(CommandOptions): + """ + Class represents choices enum for recurse-submodule option. + """ + CHECK = 'check' + ON_DEMAND = 'on-demand' + ONLY = 'only' + NO = 'no' + + def __init__(self): + super().__init__('push') + self.definitions = [ + GitOptionDefinition(name=self.Options.VERBOSE, type=bool, short_name='v'), + GitOptionDefinition(name=self.Options.RECURSE_SUBMODULES, type=str, choices=self.RecurseSubmodulesChoices, + separator='='), + GitOptionDefinition(name=self.Options.ALL, type=bool), + GitOptionDefinition(name=self.Options.BRANCHES, type=bool), + GitOptionDefinition(name=self.Options.PRUNE, type=bool), + GitOptionDefinition(name=self.Options.DELETE, type=bool), + GitOptionDefinition(name=self.Options.TAGS, type=bool), + GitOptionDefinition(name=self.Options.REPOSITORY, type=str, positional=True, position=0), + GitOptionDefinition(name=self.Options.REFSPEC, type=str, positional=True, position=1), + ] From cf11c51d6198f62e5f984c4cd556219e513afadf Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 21:52:49 +0200 Subject: [PATCH 020/119] Add a new class with definition of the 'git rm' command --- sources/options/rm_options.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 sources/options/rm_options.py diff --git a/sources/options/rm_options.py b/sources/options/rm_options.py new file mode 100644 index 0000000..3c3ab4e --- /dev/null +++ b/sources/options/rm_options.py @@ -0,0 +1,28 @@ +""" +Module contains classes that defines options that could be configured for 'git rm' command. +Reference: https://git-scm.com/docs/git-rm +""" +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions + + +class RmCommandDefinitions(GitCommand): + """ + Options definitions class for 'git rm' command, it contains definitions of the options for 'git rm'. + """ + class Options(CommandOptions): + """ + Options class for 'git rm' command, it contains options that can be configured. + """ + QUIET = 'quiet' + FORCE = 'force' + RECURSIVE = 'r' + PATHSPEC = 'pathspec' + + def __init__(self): + super().__init__('rm') + self.definitions = [ + GitOptionDefinition(name=self.Options.QUIET, type=bool, short_name='q'), + GitOptionDefinition(name=self.Options.FORCE, type=bool, short_name='f'), + GitOptionDefinition(name=self.Options.RECURSIVE, type=bool, short_name='r'), + GitOptionDefinition(name=self.Options.PATHSPEC, type=list, positional=True, position=0), + ] From 47a54e40c796586fdbed49b7aa0685470596a99d Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 21:53:03 +0200 Subject: [PATCH 021/119] Add a new class with definition of the 'git show' command --- sources/options/show_options.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 sources/options/show_options.py diff --git a/sources/options/show_options.py b/sources/options/show_options.py new file mode 100644 index 0000000..029877e --- /dev/null +++ b/sources/options/show_options.py @@ -0,0 +1,26 @@ +""" +Module contains classes that defines options that could be configured for 'git show' command. +Reference: https://git-scm.com/docs/git-show +""" +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions + + +class ShowCommandDefinitions(GitCommand): + """ + Options definitions class for 'git show' command, it contains definitions of the options for 'git show'. + """ + class Options(CommandOptions): + """ + Options class for 'git show' command, it contains options that can be configured. + """ + QUIET = 'quiet' + FORMAT = 'format' + OBJECTS = 'objects' + + def __init__(self): + super().__init__('show') + self.definitions = [ + GitOptionDefinition(name=self.Options.QUIET, type=bool, short_name='q'), + GitOptionDefinition(name=self.Options.FORMAT, type=str, separator='='), + GitOptionDefinition(name=self.Options.OBJECTS, type=list, positional=True, position=0), + ] From 8c4bb52571736ccd5fd801986e9aafc3637031e1 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 21:54:41 +0200 Subject: [PATCH 022/119] Add .DS_Store file to the .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 68bc17f..9eabc31 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,6 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# MacOS DB file +.DS_Store \ No newline at end of file From e76275c950c0ab920b8ab58e0864916b196ee29a Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 21:56:13 +0200 Subject: [PATCH 023/119] Add a new class for manipulations with the paths --- sources/utils/path_util.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/sources/utils/path_util.py b/sources/utils/path_util.py index 78944d0..33ad0e1 100644 --- a/sources/utils/path_util.py +++ b/sources/utils/path_util.py @@ -1,16 +1,36 @@ +""" +Module contains class for manipulations with a path. +""" from pathlib import Path from typing import Union class PathUtil: + """ + Class that allows to manipulate with a path. + """ @staticmethod def convert_to_path(path: Union[str, Path]) -> Path: + """ + Method converts string to the Path, or returns the object, if it is of the type Path. + + :param path: Path that will be converted. + :type path: Union[str, Path] + :return: Converted to Path object. + """ if isinstance(path, str): path = Path(path) return path @staticmethod def convert_to_string(path: Union[str, Path]) -> str: + """ + Method converts Path to the string, or returns the object, if it is of the type string. + + :param path: String with a path that will be converted. + :type path: Union[str, Path] + :return: Converted to a string path object. + """ if isinstance(path, Path): path = str(path.absolute()) return path From c43811efeec17c8819ad52ec3ccfc0d82e055e74 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 21:57:39 +0200 Subject: [PATCH 024/119] Uncomment .idea exclude --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9eabc31..355f053 100644 --- a/.gitignore +++ b/.gitignore @@ -157,7 +157,7 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ # MacOS DB file .DS_Store \ No newline at end of file From 465a6afb151a0e2b2612361881d9b83b8f95bc29 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 21:59:20 +0200 Subject: [PATCH 025/119] Add submodule for git object models --- sources/models/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 sources/models/__init__.py diff --git a/sources/models/__init__.py b/sources/models/__init__.py new file mode 100644 index 0000000..e69de29 From b177b3c7233b5272e19e204fa6859f420d043818 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 22:00:25 +0200 Subject: [PATCH 026/119] Add common model classes --- sources/models/base_classes.py | 84 ++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 sources/models/base_classes.py diff --git a/sources/models/base_classes.py b/sources/models/base_classes.py new file mode 100644 index 0000000..a90d073 --- /dev/null +++ b/sources/models/base_classes.py @@ -0,0 +1,84 @@ +""" +Base Git objects classes, that are used by multiple objects. +""" +from dataclasses import dataclass, field +from typing import Union + + +@dataclass +class Author: + """ + Class represents an author in the git. + """ + __slots__ = ('name', 'email') + name: str + email: str + + +@dataclass +class Reference: + """ + Class represents git reference. + """ + path: str = field(init=False) + + +class Refspec: + """ + Class represents git refspec. + """ + DELIMITER: str = ':' + __source: Reference + __destination: Reference + + @classmethod + def create_from_string(cls, refspec: str) -> 'Refspec': + """ + Create an instance of the Refspec class from the string of format "source:destination". + + :param refspec: Refspec string. + :type refspec: str + :return: Refspec class instance. + """ + source, destination = refspec.split(cls.DELIMITER) + instance = cls() + instance.source = source + instance.destination = destination + return instance + + @property + def source(self) -> Reference: + """ + Refspec source. + """ + return self.__source + + @source.setter + def source(self, source: Union[str, Reference]): + if isinstance(source, str): + refspec = Refspec() + refspec.path = source + source = refspec + self.__source = source + + @property + def destination(self) -> Reference: + """ + Refspec destination. + """ + return self.__destination + + @destination.setter + def destination(self, destination: Union[str, Reference]): + if isinstance(destination, str): + refspec = Refspec() + refspec.path = destination + destination = refspec + self.__destination = destination + + @property + def raw(self) -> str: + """ + Raw refspec string in the format "source:destination". + """ + return self.DELIMITER.join([self.__source.path, self.__destination.path]) From c3532e6b5d0e52453a537b907e65d9c5f4ed43d1 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 22:01:13 +0200 Subject: [PATCH 027/119] Add models for the git branches --- sources/models/branches.py | 61 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 sources/models/branches.py diff --git a/sources/models/branches.py b/sources/models/branches.py new file mode 100644 index 0000000..b3d8de8 --- /dev/null +++ b/sources/models/branches.py @@ -0,0 +1,61 @@ +""" +Module contains models for git branches. +""" +from dataclasses import dataclass +from typing import List, Union, Optional + +from sources.models.base_classes import Reference +from sources.models.commits import Commit + + +@dataclass +class Branch(Reference): + """ + Class represents git branch. + """ + name: str + commit: Optional[Commit] = None + + def __post_init__(self): + self.path = '/'.join(['refs', 'heads', self.name]) + + +@dataclass +class RemoteBranch(Branch): + """ + Class represents remote git branch. + """ + + +class Branches: + """ + Class contains list of branches. + """ + __branches: List[Branch] + __current_index: int + + def __init__(self, branches: Optional[List[Branch]] = None): + if branches is None: + self.__branches = [] + else: + self.__branches = branches + + def __getitem__(self, item: Union[int, str]) -> Optional[Branch]: + if isinstance(item, int): + return self.__branches[item] + if isinstance(item, str): + for branch in self.__branches: + if branch.name == item: + return branch + return None + + def __iter__(self): + self.__current_index = 0 + return self + + def __next__(self): + if self.__current_index < len(self.__branches): + element = self.__branches[self.__current_index] + self.__current_index += 1 + return element + raise StopIteration From 34726c605504f0f8fb3a52ce72e75bc82f166ece Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 22:01:25 +0200 Subject: [PATCH 028/119] Add models for the git commits --- sources/models/commits.py | 62 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 sources/models/commits.py diff --git a/sources/models/commits.py b/sources/models/commits.py new file mode 100644 index 0000000..4dce2d5 --- /dev/null +++ b/sources/models/commits.py @@ -0,0 +1,62 @@ +""" +Module contains models for git commits. +""" +from dataclasses import field, dataclass +from datetime import datetime +from typing import List, Optional, Dict, NoReturn + + +@dataclass +class Commit: + """ + Class represents git commit. + """ + message: str + author: 'Author' + date: datetime + parent: 'Commit' = field(repr=False) + commit_hash: str = field(default_factory=str) + tags: List['Tag'] = field(default_factory=list) + + def add_tag(self, tag: 'Tag') -> NoReturn: + """ + Method adds a new tag to the current commit. + + :param tag: A new tag that will be added. + :type tag: Tag + """ + self.tags.append(tag) + + +class Commits: + """ + Class contains list of commits. + """ + __commits: Dict[str, Commit] + __current_index: int + __keys: List[str] + + def __init__(self, commits: Optional[Dict[str, Commit]] = None): + if commits is None: + self.__commits = {} + else: + self.__commits = commits + + def __add__(self, other: Commit): + self.__commits[other.commit_hash] = other + return self + + def __getitem__(self, item: str): + return self.__commits.get(item, None) + + def __iter__(self): + self.__current_index = 0 + self.__keys = list(self.__commits.keys()) + return self + + def __next__(self): + if self.__current_index < len(self.__keys): + element = self.__commits[self.__keys[self.__current_index]] + self.__current_index += 1 + return element + raise StopIteration From a751e625fe0ca06d57e71dbb8e7b32600a729261 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 22:01:43 +0200 Subject: [PATCH 029/119] Add models for the git remotes --- sources/models/remotes.py | 48 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 sources/models/remotes.py diff --git a/sources/models/remotes.py b/sources/models/remotes.py new file mode 100644 index 0000000..fc57c9d --- /dev/null +++ b/sources/models/remotes.py @@ -0,0 +1,48 @@ +""" +Module contains models for git remotes. +""" +from dataclasses import dataclass +from typing import List, Union, Optional + + +@dataclass +class Remote: + """ + Class represents git remote. + """ + name: str + url: str + + +class Remotes: + """ + Class contains list of remotes. + """ + __remotes: List[Remote] + __current_index: int + + def __init__(self, remotes: Optional[List[Remote]] = None): + if remotes is None: + self.__remotes = [] + else: + self.__remotes = remotes + + def __getitem__(self, item: Union[int, str]): + if isinstance(item, int): + return self.__remotes[item] + if isinstance(item, str): + for remote in self.__remotes: + if remote.name == item: + return remote + return None + + def __iter__(self): + self.__current_index = 0 + return self + + def __next__(self): + if self.__current_index < len(self.__remotes): + element = self.__remotes[self.__current_index] + self.__current_index += 1 + return element + raise StopIteration From 4e8a93c0623d8ae26daafe78d904af8ff75f8a67 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 22:02:44 +0200 Subject: [PATCH 030/119] Add model for the git repository paths --- sources/models/repository_information.py | 38 ++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 sources/models/repository_information.py diff --git a/sources/models/repository_information.py b/sources/models/repository_information.py new file mode 100644 index 0000000..49233fe --- /dev/null +++ b/sources/models/repository_information.py @@ -0,0 +1,38 @@ +""" +Module contains class with git repository paths. +""" +from pathlib import Path +from typing import Union + +from sources.utils.path_util import PathUtil + + +class GitRepositoryPaths: + """ + Class that stores git repository paths. + """ + def __init__(self, path: Union[str, Path]): + self.__repository_path = PathUtil.convert_to_path(path) + self.__git_directory = self.__repository_path.joinpath('.git') + self.__git_ignore_path = self.__repository_path.joinpath('.gitignore') + + @property + def path(self): + """ + Path to the git repository. + """ + return self.__repository_path + + @property + def git_directory(self): + """ + Path to the git directory within the repository. + """ + return self.__git_directory + + @property + def git_ignore_path(self): + """ + Path to the git ignore file within repository. + """ + return self.__git_ignore_path From b91cf4f337aeddea167cceaff55bdda6700fa404 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 22:02:54 +0200 Subject: [PATCH 031/119] Add models for the git tags --- sources/models/tags.py | 74 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 sources/models/tags.py diff --git a/sources/models/tags.py b/sources/models/tags.py new file mode 100644 index 0000000..025a048 --- /dev/null +++ b/sources/models/tags.py @@ -0,0 +1,74 @@ +""" +Module contains models for git tags. +""" +from dataclasses import dataclass +from typing import List, ClassVar, Union, Optional + +from sources.models.base_classes import Author, Reference +from sources.models.commits import Commit + + +@dataclass +class Tag(Reference): + """ + Base class that represents git tag. + """ + DELIMITER: ClassVar[str] = '%n' + FORMAT: ClassVar[str] = DELIMITER.join(['%aN', '%aE', '%H']) + name: str + commit: Commit + + def __post_init__(self): + self.path = '/'.join(['refs', 'tags', self.name]) + if self.commit and isinstance(self.commit, Commit): + self.commit.add_tag(self) + + +@dataclass +class LightweightTag(Tag): + """ + Class that represents lightweight git tag. + """ + + +@dataclass +class AnnotatedTag(Tag): + """ + Class that represents annotated git tag. + """ + tagger: Author + message: str + + +class Tags: + """ + Class contains list of tags. + """ + __tags: List[Tag] + __current_index: int + + def __init__(self, tags: Optional[List[Tag]] = None): + if tags is None: + self.__tags = [] + else: + self.__tags = tags + + def __getitem__(self, item: Union[int, str]) -> Optional[Tag]: + if isinstance(item, int): + return self.__tags[item] + if isinstance(item, str): + for tag in self.__tags: + if tag.name == item: + return tag + return None + + def __iter__(self): + self.__current_index = 0 + return self + + def __next__(self): + if self.__current_index < len(self.__tags): + element = self.__tags[self.__current_index] + self.__current_index += 1 + return element + raise StopIteration From 78c439afd9afe3e788c91efb0985e86ece810c8b Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 22:04:58 +0200 Subject: [PATCH 032/119] Add submodule for git objects parsers --- sources/parsers/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 sources/parsers/__init__.py diff --git a/sources/parsers/__init__.py b/sources/parsers/__init__.py new file mode 100644 index 0000000..e69de29 From ceba40cca3b02c262af6b1bc869b99e09affc7ad Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 22:05:22 +0200 Subject: [PATCH 033/119] Add a new parser for the git branches --- sources/parsers/branches_parser.py | 105 +++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 sources/parsers/branches_parser.py diff --git a/sources/parsers/branches_parser.py b/sources/parsers/branches_parser.py new file mode 100644 index 0000000..de41d7c --- /dev/null +++ b/sources/parsers/branches_parser.py @@ -0,0 +1,105 @@ +""" +Module contains parser for the git branches. +""" +import re +from typing import Optional, NoReturn + +from sources.models.branches import Branches, Branch +from sources.models.commits import Commits +from sources.models.repository_information import GitRepositoryPaths + + +class BranchesParser: + """ + Class parses git branches. + """ + ACTIVE_BRANCH_PATTERN: str = r'ref:\s*(?Prefs/heads/(?P.*))' + PACKED_BRANCH_PATTERN: str = (r'^\s*(?P[0-9a-fA-F]+)\s+' + r'refs/(?Pheads|remotes/[A-Za-z0-9._-]+)/(?P.+)$') + __active_branch_name: Optional[str] + __active_branch_commit_hash: Optional[str] + + def __init__(self, repository_information: GitRepositoryPaths, commits: Commits): + self.__repository_information = repository_information + self.__active_branch_name = None + self.__active_branch_commit_hash = None + self.__active_branch_regex = re.compile(self.ACTIVE_BRANCH_PATTERN, flags=re.MULTILINE) + self.__packed_branch_regex = re.compile(self.PACKED_BRANCH_PATTERN) + self.__commits = commits + + @property + def active_branch_name(self) -> Optional[str]: + """ + Active branch name, it is the branch to which the repository is currently configured. + """ + return self.__active_branch_name + + @property + def active_branch_commit_hash(self) -> Optional[str]: + """ + Active branch commit hash, it is the commit hash of the last commit on the branch to which the repository is + currently configured. + """ + return self.__active_branch_commit_hash + + def refresh_active_branch(self) -> NoReturn: + """ + Method refreshes values of the active branch name and active branch commit hash. + """ + head_file = self.__repository_information.git_directory.joinpath('HEAD') + with head_file.open('r') as file: + content = file.read().strip() + match = self.__active_branch_regex.match(content) + if match: + active_branch = match.group('active_branch') + active_branch_path = match.group('active_branch_path') + active_branch_path = self.__repository_information.git_directory.joinpath(active_branch_path) + if not active_branch_path.exists(): + self.__active_branch_name = active_branch + self.__active_branch_commit_hash = None + else: + with active_branch_path.open('r') as branch_file: + self.__active_branch_name = active_branch + self.__active_branch_commit_hash = branch_file.read().strip() + + @property + def branches(self): + """ + List of all branches of the local repository, includes: local branches and packed branches. + """ + branches = [] + branches.extend(self.local_branches) + for packed_branch in self.packed_branches: + if packed_branch not in branches: + branches.append(packed_branch) + return Branches(branches) + + @property + def local_branches(self): + """ + List of all local branches, except of packed branches. + """ + branches_path = self.__repository_information.git_directory.joinpath('refs', 'heads') + branches = [] + for file_path in branches_path.iterdir(): + if file_path.name == '.DS_Store': + continue + with file_path.open('r') as file: + commit = file.read().strip() + branches.append(Branch(name=file_path.name, commit=self.__commits[commit])) + return branches + + @property + def packed_branches(self): + """ + List of all packed branches in the local repository. + """ + packed_refs_path = self.__repository_information.git_directory.joinpath('packed-refs') + branches = [] + if packed_refs_path.exists(): + with packed_refs_path.open('r') as file: + for line in file.readlines(): + match = self.__packed_branch_regex.match(line) + if match: + branches.append(Branch(name=match.group('name'), commit=self.__commits[match.group('commit')])) + return branches From 5d554f35f224247e5b36135a9a7b9a62ef9565bf Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 22:05:36 +0200 Subject: [PATCH 034/119] Add a new parser for the git commits --- sources/parsers/commits_parser.py | 97 +++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 sources/parsers/commits_parser.py diff --git a/sources/parsers/commits_parser.py b/sources/parsers/commits_parser.py new file mode 100644 index 0000000..453ec26 --- /dev/null +++ b/sources/parsers/commits_parser.py @@ -0,0 +1,97 @@ +""" +Module contains parser for the git commits. +""" +from datetime import datetime +from enum import Enum +from typing import List, Optional + +from sources.command import GitCommandRunner +from sources.models.base_classes import Author, Reference +from sources.models.commits import Commits, Commit +from sources.options.log_options import LogCommandDefinitions + + +class CommitsParser: + """ + Class parses git commits. + """ + class FormatOptions(Enum): + """ + Class contains options that could be used for commits formatting. + """ + AUTHOR_NAME = '%aN' + AUTHOR_EMAIL = '%aE' + AUTHOR_DATE = '%ad' + COMMIT_HASH = '%H' + PARENT_HASH = '%P' + SUMMARY = '%s' + + DELIMITER: str = '%n' + FORMAT: List[str] = [ + FormatOptions.AUTHOR_NAME.value, + FormatOptions.AUTHOR_EMAIL.value, + FormatOptions.AUTHOR_DATE.value, + FormatOptions.PARENT_HASH.value, + FormatOptions.COMMIT_HASH.value, + FormatOptions.SUMMARY.value + ] + FORMAT_RAW: str = DELIMITER.join(FORMAT) + + DATE_FORMAT: str = '%Y-%m-%d %H:%M:%S' + + TAGGER_LINE_INDEX = 1 + + def __init__(self, git_command: GitCommandRunner): + self.__git_command = git_command + + @property + def commits(self) -> Commits: + """ + All commits in the repository. + """ + return self.get_commits() + + def parse_commits(self, raw_commits: List[str]) -> Commits: + """ + Parse list of the raw stings with commits returned by 'git log' to Commits object. + + :param raw_commits: List of raw strings with commits information. + :type raw_commits: List[str] + :return: Commits object that contains list of commits. + """ + commits = Commits() + for raw_commit in raw_commits: + commit_hash = raw_commit[self.FORMAT.index(self.FormatOptions.COMMIT_HASH.value)] + parent_hash = raw_commit[self.FORMAT.index(self.FormatOptions.PARENT_HASH.value)] + author_name = raw_commit[self.FORMAT.index(self.FormatOptions.AUTHOR_NAME.value)] + author_email = raw_commit[self.FORMAT.index(self.FormatOptions.AUTHOR_EMAIL.value)] + commit_date = datetime.strptime(raw_commit[self.FORMAT.index(self.FormatOptions.AUTHOR_DATE.value)], + self.DATE_FORMAT) + commit_message = raw_commit[self.FORMAT.index(self.FormatOptions.SUMMARY.value)] + author = Author(name=author_name, email=author_email) + commit = Commit(commit_hash=commit_hash, message=commit_message, author=author, date=commit_date, + parent=commits[parent_hash]) + commits += commit + return commits + + def get_commits(self, reference: Optional[Reference] = None) -> Commits: + """ + Extract all commits for the provided reference from the repository and parse them to Commits object. + + :param reference: Reference for which the commits will be collected. + :type reference: Optional[Reference] + :return: Commits object with list of the commits for the reference. + """ + options = [] + if reference is None: + options.append(LogCommandDefinitions.Options.ALL.create_option(True)) + else: + options.append(LogCommandDefinitions.Options.PATH.create_option(reference.path)) + options.append(LogCommandDefinitions.Options.PRETTY.create_option(f'format:{self.FORMAT_RAW}')) + options.append(LogCommandDefinitions.Options.DATE.create_option(f'format:{self.DATE_FORMAT}')) + commit_attributes = len(self.FORMAT) + output = self.__git_command.execute(options, LogCommandDefinitions) + lines = output.strip().split('\n') + raw_commits = [lines[row_index:row_index + commit_attributes] for row_index in + range(len(lines) - commit_attributes, -1, commit_attributes * -1)] + return self.parse_commits(raw_commits) From bd86163e7615c67732d403b5c475b2eccb36ac03 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 22:05:48 +0200 Subject: [PATCH 035/119] Add a new parser for the git commits --- sources/parsers/tags_parser.py | 100 +++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 sources/parsers/tags_parser.py diff --git a/sources/parsers/tags_parser.py b/sources/parsers/tags_parser.py new file mode 100644 index 0000000..40e99b9 --- /dev/null +++ b/sources/parsers/tags_parser.py @@ -0,0 +1,100 @@ +""" +Module contains parser for the git tags. +""" +from enum import Enum +from typing import List, Union + +from sources.command import GitCommandRunner +from sources.models.base_classes import Author +from sources.models.commits import Commits +from sources.models.tags import Tags, LightweightTag, AnnotatedTag +from sources.options.for_each_ref_options import ForEachRefCommandDefinitions + + +class TagsParser: + """ + Class parses git tags. + """ + class FormatOptions(Enum): + """ + Class contains options that could be used for tags formatting. + """ + OBJECT_TYPE = '%(objecttype)' + OBJECT_HASH = '%(if)%(object)%(then)%(object)%(else)%(objectname)%(end)' + TAG_NAME = '%(refname:short)' + AUTHOR_NAME = '%(taggername)' + AUTHOR_EMAIL = '%(taggeremail)' + SUBJECT = '%(subject)' + + DELIMITER: str = '%0a' + FORMAT: List[str] = [ + FormatOptions.OBJECT_TYPE.value, + FormatOptions.OBJECT_HASH.value, + FormatOptions.TAG_NAME.value, + FormatOptions.AUTHOR_NAME.value, + FormatOptions.AUTHOR_EMAIL.value, + FormatOptions.SUBJECT.value + ] + + RAW_FORMAT: str = DELIMITER.join(FORMAT) + + PATTERN = 'refs/tags' + + TAGGER_LINE_INDEX = 1 + + def __init__(self, git_command: GitCommandRunner, commits: Commits): + self.__git_command = git_command + self.__commits = commits + + @property + def tags(self) -> Tags: + """ + All tags in the repository. + """ + tags = [] + tags.extend(self.get_tags()) + return Tags(tags) + + def parse_tags(self, raw_tags: List[str]) -> List[Union[LightweightTag, AnnotatedTag]]: + """ + Parse list of the raw strings with tags returned by 'git for-each-ref' to list of tags objects. + + :param raw_tags: List of raw strings with tags information. + :type raw_tags: List[str] + :return: List of tags objects. + """ + tags = [] + for tag in raw_tags: + tag_type = tag[self.FORMAT.index(self.FormatOptions.OBJECT_TYPE.value)] + object_hash = tag[self.FORMAT.index(self.FormatOptions.OBJECT_HASH.value)] + name = tag[self.FORMAT.index(self.FormatOptions.TAG_NAME.value)] + if tag_type == 'commit': + tags.append(LightweightTag(name=name, commit=self.__commits[object_hash])) + else: + subject = tag[self.FORMAT.index(self.FormatOptions.SUBJECT.value)] + author = tag[self.FORMAT.index(self.FormatOptions.AUTHOR_NAME.value)] + email = tag[self.FORMAT.index(self.FormatOptions.AUTHOR_EMAIL.value)].removeprefix('<').removesuffix( + '>') + tagger = Author(name=author, email=email) + tags.append( + AnnotatedTag(tagger=tagger, message=subject, name=name, commit=self.__commits[object_hash])) + return tags + + def get_tags(self) -> List[Union[LightweightTag, AnnotatedTag]]: + """ + Extract all tags from the repository and parse them to tags objects. + + :return: List of parsed tags objects. + """ + options = [ + ForEachRefCommandDefinitions.Options.FORMAT.create_option(self.RAW_FORMAT), + ForEachRefCommandDefinitions.Options.PATTERN.create_option(self.PATTERN), + ] + output = self.__git_command.execute(options, ForEachRefCommandDefinitions).strip() + if not output: + return [] + lines = output.split('\n') + arguments = len(self.FORMAT) + raw_tags = [lines[row_index:row_index + arguments] for row_index in + range(0, len(lines), arguments)] + return self.parse_tags(raw_tags) From 7d436eecb119be0caf6b84c87b38e4f4b49088ea Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 22:06:42 +0200 Subject: [PATCH 036/119] Extend git command runner functionality --- sources/command.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 sources/command.py diff --git a/sources/command.py b/sources/command.py new file mode 100644 index 0000000..8133be2 --- /dev/null +++ b/sources/command.py @@ -0,0 +1,85 @@ +""" +Module defines base class for git command execution. +""" +import logging +from pathlib import Path +from subprocess import PIPE, Popen +from typing import Union, List, Optional, Type + +from sources.exceptions import GitCommandException +from sources.options.options import GitCommand, GitOption + + +class GitCommandRunner: + """ + Base class that wraps git command execution. + """ + ENCODING = 'UTF-8' + ERRORS = 'ignore' + + def __init__(self): + self.__command = 'git' + self.__working_directory = Path() + self.__logging = logging.getLogger('git-command') + + @property + def working_directory(self): + """ + Working directory (git repository) for the git command. + """ + return self.__working_directory + + @working_directory.setter + def working_directory(self, working_directory: Union[str, Path]): + class_name = self.__class__.__name__ + if isinstance(working_directory, str): + working_directory = Path(working_directory) + self.__logging.debug('Switching %s working directory to "%s"', class_name, working_directory.absolute()) + self.__working_directory = working_directory + + def __generate_command(self, command: List[Union[str, int]]) -> List[Union[str, int]]: + """ + A method that generates git command, it adds 'git' to the command options list. + + :param command: List of options for the git command. + :type command: List[Union[str, int]] + :return: Command options list with 'git' as the first argument. + """ + if isinstance(command, list): + command.insert(0, self.__command) + return command + + def execute(self, commands: List[Union[str, int, GitOption, None]], + definitions_class: Optional[Type[GitCommand]] = None, log_output: bool = False): + """ + Method executes git command with options provided as an input. It validates options based on the provided + definition. If definitions class has not been provided, then no validation happens and standard list of commands + is expected, otherwise it expects GitOption list. + + :param commands: List of options for the git command. + :type commands: List[Union[str, int, GitOption, None]] + :param definitions_class: Definition class that describes options for the git command, shall be derived from + GitCommand class. + :type definitions_class: Optional[Type[GitCommand]] + :param log_output: If this option is True, then stdout of the command will be logged, + :type log_output: bool + :raises GitCommandException: If command execution has been finished with non-zero exit code. + :return: Output from the command. + """ + if None in commands: + commands.remove(None) + if definitions_class is not None: + definitions = definitions_class() + definitions.validate(commands) + commands = definitions.transform_to_command(commands) + commands = self.__generate_command(commands) + self.__logging.debug(commands) + with Popen(commands, shell=False, stderr=PIPE, stdout=PIPE, cwd=self.__working_directory) as process: + while log_output and process.poll() is None: + self.__logging.info(process.stdout.readline().decode(self.ENCODING, errors=self.ERRORS).strip()) + stdout, stderr = process.communicate() + stdout = stdout.decode(self.ENCODING, errors=self.ERRORS) + stderr = stderr.decode(self.ENCODING, errors=self.ERRORS) + if process.returncode != 0: + raise GitCommandException(stderr) + return stdout From 3c312ac92449f914e41450df2a1d8798400bcbae Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 22:07:21 +0200 Subject: [PATCH 037/119] Update main classes to work with recent changes --- sources/git.py | 573 +++++++++++-------------------------------------- 1 file changed, 131 insertions(+), 442 deletions(-) diff --git a/sources/git.py b/sources/git.py index 56bc6c0..7f74555 100644 --- a/sources/git.py +++ b/sources/git.py @@ -1,93 +1,40 @@ +""" +Main module that contains entry points classes for the manipulations with the git repository. +""" import fnmatch import hashlib import logging import os import re from configparser import ConfigParser -from dataclasses import dataclass, field from datetime import datetime from pathlib import Path -from subprocess import Popen, PIPE -from typing import Union, List, Optional, Dict, ClassVar, NoReturn +from typing import Union, List, Optional, Dict, NoReturn +from sources.command import GitCommandRunner from sources.exceptions import GitException, GitPullException, GitRepositoryNotFoundException, \ NotGitRepositoryException, GitPushException, GitAddException, GitRmException, GitMvException +from sources.models.base_classes import Reference, Author, Refspec +from sources.models.branches import Branch, Branches +from sources.models.commits import Commits, Commit +from sources.models.remotes import Remotes, Remote +from sources.models.repository_information import GitRepositoryPaths +from sources.models.tags import Tags +from sources.options.add_options import AddCommandDefinitions +from sources.options.clone_options import CloneCommandDefinitions +from sources.options.init_options import InitCommandDefinitions +from sources.options.mv_options import MvCommandDefinitions from sources.options.options import GitOption -from sources.options.pull_options import PullOptionsDefinitions +from sources.options.pull_options import PullCommandDefinitions +from sources.options.push_options import PushCommandDefinitions +from sources.options.rm_options import RmCommandDefinitions +from sources.options.show_options import ShowCommandDefinitions +from sources.parsers.branches_parser import BranchesParser +from sources.parsers.commits_parser import CommitsParser +from sources.parsers.tags_parser import TagsParser from sources.utils.path_util import PathUtil -class GitCommand: - def __init__(self): - self.__command = 'git' - self.__working_directory = Path() - - @property - def working_directory(self): - return self.__working_directory - - @working_directory.setter - def working_directory(self, working_directory: Union[str, Path]): - class_name = self.__class__.__name__ - if isinstance(working_directory, str): - working_directory = Path(working_directory) - logging.debug(f'Switching {class_name} working directory to "{working_directory.absolute()}"') - self.__working_directory = working_directory - - def __generate_command(self, command: List[Union[str, int]]) -> List[Union[str, int]]: - if isinstance(command, list): - command.insert(0, self.__command) - return command - - def execute(self, command: List[Union[str, int]]): - command = self.__generate_command(command) - logging.debug(command) - with Popen(command, shell=False, stderr=PIPE, stdout=PIPE, cwd=self.__working_directory) as process: - stdout, stderr = process.communicate() - if process.returncode != 0: - raise GitException(stderr.decode('UTF-8')) - return stdout.decode('UTF-8') - - -@dataclass -class Remote: - name: str - url: str - - -class Remotes: - __remotes: List[Remote] - - def __init__(self): - self.__remotes = list() - - @classmethod - def create_from_commits_list(cls, remotes: List[Remote]): - instance = cls() - instance.__remotes = remotes - return instance - - def __getitem__(self, item: Union[int, str]): - if type(item) == int: - return self.__remotes[item] - elif type(item) == str: - for remote in self.__remotes: - if remote.name == item: - return remote - return None - - def __iter__(self): - self.current_index = 0 - return self - - def __next__(self): - if self.current_index < len(self.__remotes): - element = self.__remotes[self.current_index] - self.current_index += 1 - return element - raise StopIteration - - class GitConfig: def __init__(self, configuration_path: Union[str, Path]): configuration_path = PathUtil.convert_to_path(configuration_path) @@ -118,121 +65,6 @@ def remotes(self): return remotes -class Git: - __configuration_folder: Path - - def __init__(self, git_config_folder: Union[str, Path]): - git_config_folder = PathUtil.convert_to_path(git_config_folder) - self.__configuration_folder = git_config_folder - - def __read_configuration(self, configuration_folder: Path): - configuration = GitConfig(configuration_folder) - logging.info(configuration.get('remote')) - - -@dataclass -class Reference: - path: str - - -class Refspec: - DELIMITER: str = ':' - __source: Reference - __destination: Reference - - def __init__(self): - self.__source = None - self.__destination = None - - @classmethod - def create_from_string(cls, refspec: str): - source, destination = refspec.split(cls.DELIMITER) - instance = cls() - instance.__source = source - instance.__destination = destination - return instance - - @property - def source(self) -> Reference: - return self.__source - - @source.setter - def source(self, source: Reference): - self.__source = source - - @property - def destination(self) -> Reference: - return self.__destination - - @destination.setter - def destination(self, destination: Reference): - self.__destination = destination - - @property - def raw(self) -> str: - return self.DELIMITER.join([self.__source.path, self.__destination.path]) - - -class Refspecs: - def __init__(self): - self.__refspecs = list() - - -@dataclass -class Author: - name: str - email: str - - -@dataclass -class Commit: - FORMAT_DELIMITER: ClassVar[str] = '%n' - FORMAT: ClassVar[str] = FORMAT_DELIMITER.join(['%H', '%an', '%ae', '%ad', '%s']) - DATE_FORMAT: ClassVar[str] = '%Y-%m-%d %H:%M:%S' - message: str - author: 'Author' - date: datetime - commit_hash: str = field(default_factory=str) - parents: List['Commit'] = field(default_factory=list, repr=False) - tags: List['Tag'] = field(default_factory=list) - - def add_tag(self, tag: 'Tag'): - self.tags.append(tag) - - -class Commits: - __commits: List[Commit] - - def __init__(self): - self.__commits = list() - - @classmethod - def create_from_commits_list(cls, commits: List[Commit]): - instance = cls() - instance.__commits = commits - return instance - - def __getitem__(self, item: Union[int, str]): - if type(item) == int: - return self.__commits[item] - elif type(item) == str: - for commit in self.__commits: - if commit.commit_hash == item: - return commit - return None - - def __iter__(self): - self.current_index = 0 - return self - - def __next__(self): - if self.current_index < len(self.__commits): - element = self.__commits[self.current_index] - self.current_index += 1 - return element - raise StopIteration - - class GitIgnore: def __init__(self, path: Path): self.__path = path @@ -254,7 +86,7 @@ def __read_file(path: Path): @staticmethod def __read_content(content: List[str]): - exclude_patterns = list() + exclude_patterns = [] for line in content: line = line.strip() if line and not line.startswith('#'): @@ -317,7 +149,7 @@ def __update_files_hash(parent: Path, files_list: Dict[str, str]): @staticmethod def __get_added(start: Dict, end: Dict): result = [] - for key, value in end.items(): + for key, _ in end.items(): if key not in start.keys(): result.append(key) return result @@ -333,7 +165,7 @@ def __get_modified(start: Dict, end: Dict): @staticmethod def __get_removed(start: Dict, end: Dict): result = [] - for key, value in start.items(): + for key, _ in start.items(): if key not in end.keys(): result.append(key) return result @@ -353,10 +185,10 @@ def __get_excluded(files: List, excludes: List): class CheckoutHandler: __new_branch: str - def __init__(self, new_branch: str, git_command: GitCommand, old_branch: Optional[str] = None): + def __init__(self, new_branch: str, repository: 'GitRepository', old_branch: Optional[str] = None): self.__new_branch = new_branch self.__old_branch = old_branch - self.__git_command = git_command + self.__repository = repository self.checkout(self.__new_branch) def __enter__(self): @@ -368,89 +200,9 @@ def __exit__(self, exc_type, exc_val, exc_tb): def checkout(self, branch: Optional[str] = None): if branch is None: branch = self.__new_branch - logging.info(f'Switching to "{branch}" branch.') - self.__git_command.execute(['checkout', branch]) - - -@dataclass -class Tag(Reference): - name: str - commit: Commit - author: Author - - def __post_init__(self): - self.path = '/'.join(['refs', 'tags', self.name]) - self.commit.add_tag(self) - - -@dataclass -class LightweightTag(Tag): - pass - - -@dataclass -class AnnotatedTag(Tag): - tagger: Author - message: str - - -@dataclass -class Branch(Reference): - name: str - commit: Commit - - def __init__(self, name: str, commit: Union[str, Commit]): - self.name = name - self.commit = commit - self.path = '/'.join(['refs', 'heads', self.name]) - - @property - def commit(self): - return self.__commit - - @commit.setter - def commit(self, commit: Union[str, Commit]): - if isinstance(commit, str): - commit = Commit(commit_hash=commit, message='', author=None, date=None) - self.__commit = commit - - -@dataclass -class RemoteBranch(Branch): - pass - - -class Branches: - __branches: List[Branch] - - def __init__(self): - self.__branches = list() - - @classmethod - def create_from_branches_list(cls, branches: List[Branch]): - instance = cls() - instance.__branches = branches - return instance - - def __getitem__(self, item: Union[int, str]): - if type(item) == int: - return self.__branches[item] - elif type(item) == str: - for branch in self.__branches: - if branch.name == item: - return branch - return None - - def __iter__(self): - self.current_index = 0 - return self - - def __next__(self): - if self.current_index < len(self.__branches): - element = self.__branches[self.current_index] - self.current_index += 1 - return element - raise StopIteration + logging.info('Switching to "%s" branch.', branch) + self.__repository.git_command.execute(['checkout', branch]) + self.__repository.refresh_repository(refresh_active_branch=True, refresh_commits=True) class PathsMapping: @@ -502,73 +254,94 @@ def root_path(self, root_path: Union[str, Path]) -> NoReturn: class GitRepository: - __git_command: GitCommand - __git_directory: Path + __repository_information: GitRepositoryPaths + __git_command: GitCommandRunner __git_config: GitConfig __gitignore: GitIgnore - __active_branch: Branch + __active_branch: Optional[Branch] __remotes: Remotes __branches: Branches __commits: Commits + __tags: Tags def __init__(self, path: Union[str, Path]): - self.__git_command = GitCommand() + self.__git_command = GitCommandRunner() self.__git_command.working_directory = path - self.__repository_path = PathUtil.convert_to_path(path) - if not self.__repository_path.exists(): - raise GitRepositoryNotFoundException(f'Git repository: {self.__repository_path} not exists.') - self.__git_directory = self.__repository_path.joinpath('.git') - if not self.__git_directory.exists(): + self.__repository_information = GitRepositoryPaths(path) + if not self.__repository_information.path.exists(): + raise GitRepositoryNotFoundException(f'Git repository: {self.__repository_information.path} not exists.') + if not self.__repository_information.git_directory.exists(): raise NotGitRepositoryException('Provided path is not a git repository, .git directory was not found.') - git_ignore_path = self.__repository_path.joinpath('.gitignore') - self.__gitignore = self.__read_git_ignore(git_ignore_path, self.__git_command) - self.__git_config = GitConfig(self.__git_directory.joinpath('config')) + self.__gitignore = self.__read_git_ignore(self.__repository_information.git_ignore_path, self.__git_command) + self.__git_config = GitConfig(self.__repository_information.git_directory.joinpath('config')) self.__default_author = self.__get_default_author() + self.__initialize_default_values() self.refresh_repository(refresh_active_branch=True, refresh_branches=True, refresh_commits=True, refresh_tags=True, refresh_remotes=True) @property - def git_command(self) -> GitCommand: + def git_command(self) -> GitCommandRunner: return self.__git_command @property def path(self): - return self.__repository_path + return self.__repository_information.path + + @property + def git_path(self): + return self.__repository_information.git_directory @property def gitignore(self): return self.__gitignore @property - def current_branch(self): + def current_branch(self) -> Optional[Branch]: return self.__active_branch @property - def remotes(self): + def remotes(self) -> Remotes: return self.__remotes @property - def branches(self): + def branches(self) -> Branches: return self.__branches @property - def commits(self): + def commits(self) -> Commits: return self.__commits - def refresh_repository(self, refresh_active_branch: bool, refresh_branches: bool, refresh_commits: bool, - refresh_tags: bool, refresh_remotes: bool): - branch_name, branch_last_commit = self.__read_active_branch_raw() + @property + def tags(self) -> Tags: + return self.__tags + + def __initialize_default_values(self): + self.__active_branch = None + self.__remotes = Remotes() + self.__branches = Branches() + self.__commits = Commits() + self.__tags = Tags() + + def refresh_repository(self, refresh_active_branch: bool = False, refresh_branches: bool = False, + refresh_commits: bool = False, refresh_tags: bool = False, refresh_remotes: bool = False): + branches_parser = BranchesParser(self.__repository_information, self.__commits) + branches_parser.refresh_active_branch() if refresh_commits: try: - self.__commits = self.__get_commits(Branch(name=branch_name, commit=branch_last_commit)) + commits_parser = CommitsParser(self.__git_command) + self.__commits = commits_parser.commits except GitException: - self.__commits = Commits.create_from_commits_list([]) + self.__commits = Commits() if refresh_remotes: self.__remotes = self.__read_remotes() if refresh_active_branch: - self.__set_active_branch(branch_name, branch_last_commit) + self.__active_branch = Branch(name=branches_parser.active_branch_name, + commit=self.__commits[branches_parser.active_branch_commit_hash]) if refresh_branches: - self.__branches = self.__read_branches() + self.__branches = branches_parser.branches + if refresh_tags: + tags_parser = TagsParser(self.__git_command, self.__commits) + self.__tags = tags_parser.tags def __get_default_author(self): name = self.__git_command.execute(['config', 'user.name']).strip() @@ -576,190 +349,109 @@ def __get_default_author(self): return Author(name=name, email=email) @staticmethod - def __read_git_ignore(path: Path, git_command: GitCommand) -> Optional[GitIgnore]: + def __read_git_ignore(path: Path, git_command: GitCommandRunner) -> Optional[GitIgnore]: gitignore = None if path.exists(): - commit_content = git_command.execute(['show', f'HEAD:{path.name}']) + options = [ShowCommandDefinitions.Options.OBJECTS.create_option([f'HEAD:{path.name}'])] + commit_content = git_command.execute(options, ShowCommandDefinitions) with path.open('r') as file: content = file.read() if content != commit_content: - gitignore = GitIgnore.create_from_content(commit_content) + gitignore = GitIgnore.create_from_content(path, commit_content) if not gitignore: gitignore = GitIgnore(path) return gitignore def __read_remotes(self): - return Remotes.create_from_commits_list(self.__git_config.remotes) - - def __set_active_branch(self, active_branch, active_branch_commit): - for commit in self.__commits: - if commit.commit_hash == active_branch_commit: - active_branch_commit = commit - break - self.__active_branch = Branch(name=active_branch, commit=active_branch_commit) - - def __read_active_branch_raw(self): - head_file = self.__git_directory.joinpath('HEAD') - with head_file.open('r') as file: - content = file.read().strip() - match = re.match(r'ref:\s*(?Prefs/heads/(?P.*))', content, - flags=re.MULTILINE) - if match: - active_branch = match.group('active_branch') - active_branch_path = match.group('active_branch_path') - active_branch_path = self.__git_directory.joinpath(active_branch_path) - if not active_branch_path.exists(): - return active_branch, None - with active_branch_path.open('r') as branch_file: - commit_hash = branch_file.read().strip() - return active_branch, commit_hash - - def __read_branches(self): - branches = [] - branches.extend(self.__read_local_branches()) - branches.extend(self.__read_packed_branches()) - return Branches.create_from_branches_list(branches) - - def __read_local_branches(self): - branches_path = self.__git_directory.joinpath('refs', 'heads') - branches = [] - for file_path in branches_path.iterdir(): - if file_path.name == '.DS_Store': - continue - with file_path.open('r') as file: - commit = file.read().strip() - branches.append(Branch(name=file_path.name, commit=commit)) - return branches - - def __read_packed_branches(self): - packed_refs_path = self.__git_directory.joinpath('packed-refs') - packed_branch_pattern = re.compile( - r'^\s*(?P[0-9a-fA-F]+)\s+refs/(?Pheads|remotes/[A-Za-z0-9._-]+)/(?P.+)$') - branches = [] - if packed_refs_path.exists(): - with packed_refs_path.open('r') as file: - for line in file.readlines(): - match = packed_branch_pattern.match(line) - if match: - branches.append(Branch(name=match.group('name'), commit=match.group('commit'))) - return branches - - def __get_commits(self, branch: Branch): - commit_attributes = len(Commit.FORMAT.split(Commit.FORMAT_DELIMITER)) - output = self.__git_command.execute( - ['log', f'--pretty=format:{Commit.FORMAT}', f'--date=format:{Commit.DATE_FORMAT}', branch.name]) - lines = output.split('\n') - raw_commits = [lines[row_index:row_index + 5] for row_index in - range(len(lines) - commit_attributes, -1, commit_attributes * -1)] - parents = [] - for raw_commit in raw_commits: - commit_hash = raw_commit[0] - author_name = raw_commit[1] - author_email = raw_commit[2] - commit_date = datetime.strptime(raw_commit[3], Commit.DATE_FORMAT) - commit_message = raw_commit[4] - author = Author(name=author_name, email=author_email) - commit = Commit(commit_hash=commit_hash, message=commit_message, author=author, date=commit_date, - parents=parents.copy()) - parents.append(commit) - commits = list(reversed(parents)) - return Commits.create_from_commits_list(commits) + return Remotes(self.__git_config.remotes) def checkout(self, branch: str): - return CheckoutHandler(branch, self.__git_command, self.__active_branch.name) + return CheckoutHandler(branch, self, self.__active_branch.name) def create_commit(self, message: str, author: Optional[Author] = None, date: Optional[datetime] = None, - commit_hash: str = None): + commit_hash: str = None, parent: Union[str, Commit] = None): if author is None: author = self.__default_author if date is None: date = datetime.now() if commit_hash is None: commit_hash = '' - return Commit(message=message, author=author, date=date, commit_hash=commit_hash) + if parent and isinstance(parent, str): + parent = self.__commits[parent] + return Commit(message=message, author=author, date=date, commit_hash=commit_hash, parent=parent) @classmethod - def init(cls, path: Union[str, Path]): + def init(cls, path: Union[str, Path], *options: GitOption): if isinstance(path, str): path = Path(path) - git_command = GitCommand() - command = ['init', str(path.absolute())] - output = git_command.execute(command) - logging.info(output.strip()) + options = list(options) + options.append(InitCommandDefinitions.Options.DIRECTORY.create_option(str(path.absolute()))) + git_command = GitCommandRunner() + git_command.execute(options, InitCommandDefinitions) return cls(path) @classmethod - def clone(cls, repository: Union[str, Remote], path: Union[str, Path] = None): + def clone(cls, repository: Union[str, Remote], path: Union[str, Path] = None, *options: GitOption): if isinstance(path, str): path = Path(path) - command = ['clone'] if isinstance(repository, Remote): repository = repository.url - command.append(repository) + options = list(options) + options.append(CloneCommandDefinitions.Options.REPOSITORY.create_option(repository)) if path is not None: - command.append(str(path.absolute())) - git_command = GitCommand() - output = git_command.execute(command) - logging.info(output) + options.append(CloneCommandDefinitions.Options.DIRECTORY.create_option(str(path.absolute()))) + git_command = GitCommandRunner() + git_command.execute(options, CloneCommandDefinitions) return cls(path) - def add(self, files: Union[str, Path, List[Union[str, Path]]], force: bool = False, update: bool = False): + def add(self, files: Union[str, Path, List[Union[str, Path]]], *options: GitOption): outputs = [] - additional_options = [] - if force: - additional_options.append('-f') - if update: - additional_options.append('-u') - if isinstance(files, str) or isinstance(files, Path): + if isinstance(files, (str, Path)): files = [files] - for index, file_path in enumerate(files): + for file_path in files: if isinstance(file_path, Path): file_path = str(file_path.absolute()) + options = list(options) + options.append(AddCommandDefinitions.Options.PATHSPEC.create_option(file_path)) try: - command = ['add', *additional_options, file_path] - output = self.__git_command.execute(command) + output = self.__git_command.execute(options, AddCommandDefinitions) outputs.append(output.strip()) except GitException as exception: raise GitAddException(exception.args[0]) from None return '\n'.join(outputs) - def mv(self, mappings: Union[PathsMapping, List[PathsMapping]], force: bool = False): + def mv(self, mappings: Union[PathsMapping, List[PathsMapping]], *options: GitOption): outputs = [] - additional_options = [] - if force: - additional_options.append('-f') if isinstance(mappings, PathsMapping): mappings = [mappings] - for index, mapping in enumerate(mappings): + for mapping in mappings: try: - mapping.root_path = self.__repository_path - command = ['mv', *additional_options, str(mapping.source.absolute()), - str(mapping.destination.absolute())] - output = self.__git_command.execute(command) + mapping.root_path = self.__repository_information.path + options = list(options) + options.append(MvCommandDefinitions.Options.SOURCE.create_option(str(mapping.source.absolute()))) + options.append( + MvCommandDefinitions.Options.DESTINATION.create_option(str(mapping.destination.absolute()))) + output = self.__git_command.execute(options, MvCommandDefinitions) outputs.append(output.strip()) except GitException as exception: raise GitMvException(exception.args[0]) from None return '\n'.join(outputs) - def rm(self, files: Union[str, Path, List[Union[str, Path]]], recursive: bool = False, force: bool = False): + def rm(self, files: Union[str, Path, List[Union[str, Path]]], *options: GitOption): outputs = [] - additional_options = [] - if recursive: - additional_options.append('-r') - if force: - additional_options.append('-f') - if isinstance(files, str) or isinstance(files, Path): + if isinstance(files, (str, Path)): files = [files] - for index, file_path in enumerate(files): - raw_repository_path = str(self.__repository_path.absolute()) + for file_path in files: + raw_repository_path = str(self.__repository_information.path.absolute()) if isinstance(file_path, str) and file_path.startswith(raw_repository_path): file_path = Path(file_path) else: - file_path = self.__repository_path.joinpath(file_path) + file_path = self.__repository_information.path.joinpath(file_path) file_path = str(file_path.absolute()) + options = list(options) + options.append(RmCommandDefinitions.Options.PATHSPEC.create_option(file_path)) try: - command = ['rm', *additional_options, file_path] - output = self.__git_command.execute(command) + output = self.__git_command.execute(options, RmCommandDefinitions) outputs.append(output.strip()) except GitException as exception: raise GitRmException(exception.args[0]) from None @@ -767,37 +459,34 @@ def rm(self, files: Union[str, Path, List[Union[str, Path]]], recursive: bool = @staticmethod def __get_refspec(reference: Optional[Union[Reference, Refspec]]): - refspec = None if isinstance(reference, Reference): refspec = reference.path elif isinstance(reference, Refspec): refspec = reference.raw + else: + refspec = None return refspec def pull(self, remote: Remote, reference: Optional[Union[Reference, Refspec]] = None, *options: GitOption): - # TODO(dl1998): check branch parameter, according to the documentation the last parameter shall be refspec. refspec = self.__get_refspec(reference) try: options = list(options) - options.append(GitOption(name=PullOptionsDefinitions.Options.REPOSITORY.value, value=remote.name)) - options.append(GitOption(name=PullOptionsDefinitions.Options.REFSPEC.value, value=refspec)) - pull_options_definitions = PullOptionsDefinitions() - pull_options = pull_options_definitions.transform_to_command(options) - command = ['pull', *pull_options] - if None in command: - command.remove(None) - output = self.__git_command.execute(command) + options.append(PullCommandDefinitions.Options.REPOSITORY.create_option(remote.name)) + if refspec: + options.append(PullCommandDefinitions.Options.REFSPEC.create_option(refspec)) + output = self.__git_command.execute(options, PullCommandDefinitions) return output except GitException as exception: raise GitPullException(exception.args[0]) from None - def push(self, remote: Remote, reference: Optional[Union[Reference, Refspec]] = None): + def push(self, remote: Remote, reference: Optional[Union[Reference, Refspec]] = None, *options: GitOption): refspec = self.__get_refspec(reference) try: - command = ['push', remote.name, refspec] - if None in command: - command.remove(None) - output = self.__git_command.execute(command) + options = list(options) + options.append(PushCommandDefinitions.Options.REPOSITORY.create_option(remote.name)) + if refspec: + options.append(PushCommandDefinitions.Options.REFSPEC.create_option(refspec)) + output = self.__git_command.execute(options, PushCommandDefinitions) return output except GitException as exception: raise GitPushException(exception.args[0]) from None From 96dc1c67434616d1ec4b8c1ce150870a9f9b730e Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 23:12:48 +0200 Subject: [PATCH 038/119] Refactor documentation modules --- .../conf.py_t | 14 +-- _docs/root_doc.rst_t | 19 ++++ docs/Makefile | 4 +- docs/conf.py | 42 -------- docs/index.rst | 20 ---- docs/make.bat | 35 ++++++ docs/source/conf.py | 71 ++++++++++++ docs/source/index.rst | 19 ++++ docs/{ => source}/modules.rst | 0 docs/source/sources.models.rst | 61 +++++++++++ docs/source/sources.options.rst | 101 ++++++++++++++++++ docs/source/sources.parsers.rst | 37 +++++++ docs/source/sources.rst | 48 +++++++++ docs/source/sources.utils.rst | 21 ++++ docs/sources.rst | 21 ---- 15 files changed, 417 insertions(+), 96 deletions(-) rename {docs/_documentation_templates => _docs}/conf.py_t (92%) create mode 100644 _docs/root_doc.rst_t delete mode 100644 docs/conf.py delete mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst rename docs/{ => source}/modules.rst (100%) create mode 100644 docs/source/sources.models.rst create mode 100644 docs/source/sources.options.rst create mode 100644 docs/source/sources.parsers.rst create mode 100644 docs/source/sources.rst create mode 100644 docs/source/sources.utils.rst delete mode 100644 docs/sources.rst diff --git a/docs/_documentation_templates/conf.py_t b/_docs/conf.py_t similarity index 92% rename from docs/_documentation_templates/conf.py_t rename to _docs/conf.py_t index 0a022d6..93bc311 100644 --- a/docs/_documentation_templates/conf.py_t +++ b/_docs/conf.py_t @@ -10,19 +10,11 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -{% if append_syspath -%} + import os import sys -sys.path.insert(0, {{ module_path | repr }}) -{% else -%} -# import os -# import sys -{% if module_path -%} -# sys.path.insert(0, {{ module_path | repr }}) -{% else -%} -# sys.path.insert(0, os.path.abspath('.')) -{% endif -%} -{% endif %} +sys.path.insert(0, os.path.abspath('./../..')) +sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- diff --git a/_docs/root_doc.rst_t b/_docs/root_doc.rst_t new file mode 100644 index 0000000..2ece958 --- /dev/null +++ b/_docs/root_doc.rst_t @@ -0,0 +1,19 @@ +Welcome to {{ project }}'s documentation! +======================================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. toctree:: + :maxdepth: 2 + :caption: Source Code Documentation: + + modules + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/Makefile b/docs/Makefile index d4bb2cb..d0c3cbf 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -5,8 +5,8 @@ # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build +SOURCEDIR = source +BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index d2a4eed..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,42 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Project information ----------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information - -project = 'py-git' -copyright = '2023, Dmytro Leshchenko' -author = 'Dmytro Leshchenko' - -version = '0.0.1' -release = '0.0.1' - -# -- General configuration --------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration - -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.viewcode', -] - -templates_path = ['_templates'] -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - - - -# -- Options for HTML output ------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output - -html_theme = 'alabaster' -html_static_path = ['_static'] - -# -- Options for intersphinx extension --------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration - -intersphinx_mapping = { - 'python': ('https://docs.python.org/3', None), -} diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 18a2cff..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,20 +0,0 @@ -.. py-git documentation master file, created by - sphinx-quickstart on Sat May 20 20:17:21 2023. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to py-git's documentation! -================================== - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..747ffb7 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..6d35112 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,71 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# + +import os +import sys +sys.path.insert(0, os.path.abspath('./../..')) +sys.path.insert(0, os.path.abspath('.')) + +# -- Project information ----------------------------------------------------- + +project = 'py-git' +copyright = '2023, Dmytro Leshchenko' +author = 'Dmytro Leshchenko' + +# The short X.Y version +version = '0.0.1' + +# The full version, including alpha/beta/rc tags +release = '0.0.1' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.githubpages', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Extension configuration ------------------------------------------------- \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..9c33ba9 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,19 @@ +Welcome to py-git's documentation! +======================================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. toctree:: + :maxdepth: 2 + :caption: Source Code Documentation: + + modules + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` \ No newline at end of file diff --git a/docs/modules.rst b/docs/source/modules.rst similarity index 100% rename from docs/modules.rst rename to docs/source/modules.rst diff --git a/docs/source/sources.models.rst b/docs/source/sources.models.rst new file mode 100644 index 0000000..d2fa836 --- /dev/null +++ b/docs/source/sources.models.rst @@ -0,0 +1,61 @@ +sources.models package +====================== + +Submodules +---------- + +sources.models.base\_classes module +----------------------------------- + +.. automodule:: sources.models.base_classes + :members: + :undoc-members: + :show-inheritance: + +sources.models.branches module +------------------------------ + +.. automodule:: sources.models.branches + :members: + :undoc-members: + :show-inheritance: + +sources.models.commits module +----------------------------- + +.. automodule:: sources.models.commits + :members: + :undoc-members: + :show-inheritance: + +sources.models.remotes module +----------------------------- + +.. automodule:: sources.models.remotes + :members: + :undoc-members: + :show-inheritance: + +sources.models.repository\_information module +--------------------------------------------- + +.. automodule:: sources.models.repository_information + :members: + :undoc-members: + :show-inheritance: + +sources.models.tags module +-------------------------- + +.. automodule:: sources.models.tags + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: sources.models + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/sources.options.rst b/docs/source/sources.options.rst new file mode 100644 index 0000000..ee4f85c --- /dev/null +++ b/docs/source/sources.options.rst @@ -0,0 +1,101 @@ +sources.options package +======================= + +Submodules +---------- + +sources.options.add\_options module +----------------------------------- + +.. automodule:: sources.options.add_options + :members: + :undoc-members: + :show-inheritance: + +sources.options.clone\_options module +------------------------------------- + +.. automodule:: sources.options.clone_options + :members: + :undoc-members: + :show-inheritance: + +sources.options.for\_each\_ref\_options module +---------------------------------------------- + +.. automodule:: sources.options.for_each_ref_options + :members: + :undoc-members: + :show-inheritance: + +sources.options.init\_options module +------------------------------------ + +.. automodule:: sources.options.init_options + :members: + :undoc-members: + :show-inheritance: + +sources.options.log\_options module +----------------------------------- + +.. automodule:: sources.options.log_options + :members: + :undoc-members: + :show-inheritance: + +sources.options.mv\_options module +---------------------------------- + +.. automodule:: sources.options.mv_options + :members: + :undoc-members: + :show-inheritance: + +sources.options.options module +------------------------------ + +.. automodule:: sources.options.options + :members: + :undoc-members: + :show-inheritance: + +sources.options.pull\_options module +------------------------------------ + +.. automodule:: sources.options.pull_options + :members: + :undoc-members: + :show-inheritance: + +sources.options.push\_options module +------------------------------------ + +.. automodule:: sources.options.push_options + :members: + :undoc-members: + :show-inheritance: + +sources.options.rm\_options module +---------------------------------- + +.. automodule:: sources.options.rm_options + :members: + :undoc-members: + :show-inheritance: + +sources.options.show\_options module +------------------------------------ + +.. automodule:: sources.options.show_options + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: sources.options + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/sources.parsers.rst b/docs/source/sources.parsers.rst new file mode 100644 index 0000000..efbbc1b --- /dev/null +++ b/docs/source/sources.parsers.rst @@ -0,0 +1,37 @@ +sources.parsers package +======================= + +Submodules +---------- + +sources.parsers.branches\_parser module +--------------------------------------- + +.. automodule:: sources.parsers.branches_parser + :members: + :undoc-members: + :show-inheritance: + +sources.parsers.commits\_parser module +-------------------------------------- + +.. automodule:: sources.parsers.commits_parser + :members: + :undoc-members: + :show-inheritance: + +sources.parsers.tags\_parser module +----------------------------------- + +.. automodule:: sources.parsers.tags_parser + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: sources.parsers + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/sources.rst b/docs/source/sources.rst new file mode 100644 index 0000000..6c95671 --- /dev/null +++ b/docs/source/sources.rst @@ -0,0 +1,48 @@ +sources package +=============== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + sources.models + sources.options + sources.parsers + sources.utils + +Submodules +---------- + +sources.command module +---------------------- + +.. automodule:: sources.command + :members: + :undoc-members: + :show-inheritance: + +sources.exceptions module +------------------------- + +.. automodule:: sources.exceptions + :members: + :undoc-members: + :show-inheritance: + +sources.git module +------------------ + +.. automodule:: sources.git + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: sources + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/sources.utils.rst b/docs/source/sources.utils.rst new file mode 100644 index 0000000..8d36c5a --- /dev/null +++ b/docs/source/sources.utils.rst @@ -0,0 +1,21 @@ +sources.utils package +===================== + +Submodules +---------- + +sources.utils.path\_util module +------------------------------- + +.. automodule:: sources.utils.path_util + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: sources.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/sources.rst b/docs/sources.rst deleted file mode 100644 index e327104..0000000 --- a/docs/sources.rst +++ /dev/null @@ -1,21 +0,0 @@ -sources package -=============== - -Submodules ----------- - -sources.git module ------------------- - -.. automodule:: sources.git - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: sources - :members: - :undoc-members: - :show-inheritance: From 420ed63cfeddbdebead763d6552b42829c679b56 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 23:13:58 +0200 Subject: [PATCH 039/119] Add docs/build to .gitignore list --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 355f053..9c0fd04 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,7 @@ instance/ # Sphinx documentation docs/_build/ +docs/build/ # PyBuilder .pybuilder/ From 3675492ae28eea8696a7f88354272da7f0cc24ca Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 23:14:41 +0200 Subject: [PATCH 040/119] Change typing for commit parent on Optional[Commit] --- sources/models/commits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/models/commits.py b/sources/models/commits.py index 4dce2d5..023e65b 100644 --- a/sources/models/commits.py +++ b/sources/models/commits.py @@ -14,7 +14,7 @@ class Commit: message: str author: 'Author' date: datetime - parent: 'Commit' = field(repr=False) + parent: Optional['Commit'] = field(repr=False) commit_hash: str = field(default_factory=str) tags: List['Tag'] = field(default_factory=list) From 475675d1f969499c3752e25908a0df7f686a5a03 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 23:15:41 +0200 Subject: [PATCH 041/119] Update docstring for create_from_value method --- sources/options/options.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sources/options/options.py b/sources/options/options.py index ff00742..029ff4c 100644 --- a/sources/options/options.py +++ b/sources/options/options.py @@ -223,7 +223,6 @@ class CommandOptions(Enum): """ Base class for git options enum. """ - @classmethod def create_from_value(cls, value: str) -> Optional['CommandOptions']: """ @@ -231,7 +230,7 @@ def create_from_value(cls, value: str) -> Optional['CommandOptions']: :param value: Enum value from the class. :type value: str - :return: Optional[CommandOptions] + :return: CommandOptions instance. """ for element in cls: if element.value == value: From f9d9366ef6a473a410476cc06724f1621cca2ee9 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 23:17:06 +0200 Subject: [PATCH 042/119] Update python requirements list --- requirements.txt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8a16487..c4212ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ -setuptools==67.8.0 -wheel==0.40.0 -Sphinx==7.0.1 -sphinx-rtd-theme==1.2.0 -behave==1.2.6 \ No newline at end of file +setuptools==68.0.0 +wheel==0.41.2 +Sphinx==5.3.0 +sphinx-rtd-theme==1.3.0 +docs-versions-menu==0.5.2 +behave==1.2.6 +coverage==7.2.7 \ No newline at end of file From 7229237a5c674bacb094dd3b2ea799f6469d64ad Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 23:17:24 +0200 Subject: [PATCH 043/119] Update setup.py configuration --- setup.py | 187 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 120 insertions(+), 67 deletions(-) diff --git a/setup.py b/setup.py index d86258c..e5b9dfc 100644 --- a/setup.py +++ b/setup.py @@ -15,18 +15,10 @@ URL = 'https://github.com/dl1998/PyGit' EMAIL = 'dima.leschenko1998@gmail.com' AUTHOR = 'Dmytro Leshchenko' -REQUIRES_PYTHON = '>=3.6.0' +REQUIRES_PYTHON = '>=3.7.0' VERSION = '0.0.1' RELEASE = VERSION -REQUIRED = [ - -] - -EXTRAS = [ - -] - here = os.path.abspath(os.path.dirname(__file__)) long_description = '' @@ -176,28 +168,39 @@ class BuildDocsCommand(Command): Custom command to build documentation with Sphinx """ doc_dir: Path + source_dir: Path + output_dir: Path user_options = [ - ('doc-dir=', None, 'The documentation output directory'), + ('doc-dir=', None, 'The documentation directory'), + ('source-dir=', None, 'The documentation sources directory'), + ('output-dir=', None, 'The documentation output directory'), ] def initialize_options(self): self.doc_dir = None + self.source_dir = None + self.output_dir = None def finalize_options(self) -> None: self.doc_dir = Path(self.doc_dir) if self.doc_dir else Path(__file__).parent.joinpath('docs') + if self.source_dir is None: + self.source_dir = self.doc_dir.joinpath('source') + if self.output_dir is None: + self.output_dir = self.doc_dir.joinpath('build') def run(self): build_command = [ 'sphinx-build', '-b', 'html', - '-d', str(self.doc_dir.joinpath('_build/doctrees')), + '-d', str(self.doc_dir.joinpath('build/doctrees')), '-j', 'auto', - str(self.doc_dir), - str(self.doc_dir.joinpath('_build/html')) + str(self.source_dir), + str(self.output_dir) ] try: + print(' '.join(build_command)) subprocess.check_call(build_command) except subprocess.CalledProcessError as e: print(f"Error: Failed to build documentation with Sphinx: {e}") @@ -208,39 +211,72 @@ class SphinxGenerate(Command): """ Class responsible for Sphinx project generation. """ + name: str + author: str + version: str + release: str + language: str + template: str + doc_dir: str + user_options = [ ('name', None, 'The project name'), ('author', None, 'The project author'), ('version', None, 'The project version'), ('release', None, 'The project release'), + ('language', None, 'The project language'), + ('template', None, 'The project template'), + ('doc-dir', None, 'The project root'), ] def initialize_options(self) -> NoReturn: """ - If command receive arguments, then method can be used to set default values. + If command receives arguments, then method can be used to set default values. """ self.name = None self.author = None self.version = None self.release = None + self.language = None + self.template = None + self.doc_dir = None def finalize_options(self) -> NoReturn: """ If command receive arguments, then method can be used to set final values for arguments. """ - pass + if self.name is None: + self.name = NAME + if self.author is None: + self.author = AUTHOR + if self.version is None: + self.version = about['__version__'] + if self.release is None: + self.release = about['__version__'] + if self.language is None: + self.language = 'en' + if self.template is None: + self.template = str(Path('_docs').absolute()) + if self.doc_dir is None: + self.doc_dir = str(Path('docs').absolute()) def run(self) -> None: generate_command = [ - 'sphinx-build', - '-b', 'html', - '-d', str(self.doc_dir.joinpath('_build/doctrees')), - '-j', 'auto', - str(self.doc_dir), - str(self.doc_dir.joinpath('_build/html')) + 'sphinx-quickstart', + '--sep', + '-p', self.name, + '-a', self.author, + '-v', self.version, + '-r', self.release, + '-l', self.language, + '--ext-autodoc', + '--ext-githubpages', + '-t', self.template, + str(self.doc_dir) ] try: + print(' '.join(generate_command)) subprocess.check_call(generate_command) except subprocess.CalledProcessError as e: print(f"Error: Failed to generate documentation with Sphinx: {e}") @@ -353,11 +389,11 @@ def run(self) -> NoReturn: Generate Sphinx project and documentation. """ docs_path = os.path.join(here, 'docs') - templates = os.path.join(docs_path, '_documentation_templates') + templates = os.path.join(docs_path, '_docs') sources_path = os.path.join(here, 'sources') rel_sources_path = os.path.relpath(sources_path, here) self.__print_configuration() - cmd = ['sphinx-apidoc', '--templatedir', templates, '--force', '-o', docs_path] + cmd = ['sphinx-apidoc', '--templatedir', templates, '--force', '-o', os.path.join(docs_path, 'source')] if self.generate_project: cmd.extend(['--full', '-a', '-H', NAME, '-A', AUTHOR, '-V', VERSION, '-R', RELEASE]) if self.with_private_methods: @@ -379,57 +415,74 @@ def run(self) -> NoReturn: cmd_class = { - 'prepare-venv': PrepareVirtualEnvironmentCommand, - 'sphinx-generate-project': SphinxGenerate, - 'sphinx-update-modules': SphinxAutoDoc, - 'sphinx-build': BuildDocsCommand + 'prepare_venv': PrepareVirtualEnvironmentCommand, + 'sphinx_generate_project': SphinxGenerate, + 'sphinx_update_modules': SphinxAutoDoc, + 'sphinx_build': BuildDocsCommand } command_options = { - + 'sphinx-generate-project': { + 'name': (NAME, 'Specify the project name'), + 'author': (AUTHOR, 'Specify the project author'), + 'version': (VERSION, 'Specify the project version'), + 'release': (RELEASE, 'Specify the project release'), + 'language': ('en', 'Specify the project language'), + 'template': ('_docs', 'Specify the project template'), + 'doc-dir': ('docs', 'Specify the documentation directory'), + }, } def parse_requirements(file_path): - with open(file_path, 'r') as f: - requirements = f.read().splitlines() + if os.path.exists(file_path): + with open(file_path, 'r', encoding='utf-8') as f: + requirements = f.read().splitlines() + else: + requirements = [] return requirements -# Path to the requirements.txt file -requirements_path = 'requirements.txt' - -# Parse the requirements from requirements.txt -requirements = parse_requirements(requirements_path) - -setup( - name=NAME, - version=about['__version__'], - description=DESCRIPTION, - long_description=long_description, - author=AUTHOR, - author_email=EMAIL, - maintainer=AUTHOR, - maintainer_email=EMAIL, - python_requires=REQUIRES_PYTHON, - install_requires=requirements, - url=URL, - platforms=['any'], - packages=find_packages(exclude=['tests', '*.tests', '*.tests.*', 'tests.*']), - include_package_data=True, - license='MIT', - classifiers=[ - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - ], - keywords=['git', 'version control', 'sources control', 'Git wrapper', 'Git CLI', 'Git commands', 'Git automation', - 'Git interface', 'Git integration', 'Git management', 'Git utility', 'Git interaction', 'Git convenience', - 'Git operations', 'Git workflow'], - cmdclass=cmd_class, - command_options=command_options, -) +def main(): + # Path to the requirements.txt file + requirements_path = 'requirements.txt' + + # Parse the requirements from requirements.txt + requirements = parse_requirements(requirements_path) + + setup( + name=NAME, + version=about['__version__'], + description=DESCRIPTION, + long_description=long_description, + long_description_content_type='text/markdown', + author=AUTHOR, + author_email=EMAIL, + maintainer=AUTHOR, + maintainer_email=EMAIL, + python_requires=REQUIRES_PYTHON, + install_requires=requirements, + url=URL, + platforms=['any'], + packages=find_packages(exclude=['tests', '*.tests', '*.tests.*', 'tests.*']), + include_package_data=True, + license='MIT', + classifiers=[ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + ], + keywords=['git', 'version control', 'sources control', 'Git wrapper', 'Git CLI', 'Git commands', + 'Git automation', 'Git interface', 'Git integration', 'Git management', 'Git utility', + 'Git interaction', 'Git convenience', 'Git operations', 'Git workflow'], + cmdclass=cmd_class, + command_options=command_options, + ) + + +if __name__ == '__main__': + main() From 5e202f3bc9cfdf9a8e0db6401343080360231d3d Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 23:17:45 +0200 Subject: [PATCH 044/119] Add GitHub configuration files --- .github/CODEOWNERS | 4 + .github/workflows/default.yml | 111 ++++++++++++++++++++ .github/workflows/publish-documentation.yml | 33 ++++++ .github/workflows/release.yml | 80 ++++++++++++++ 4 files changed, 228 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/default.yml create mode 100644 .github/workflows/publish-documentation.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..be977a1 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# Changes in .github repository shall always be approved by an owner +.github @dl1998 +setup.py @dl1998 +.pylintrc @dl1998 \ No newline at end of file diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml new file mode 100644 index 0000000..3b1831e --- /dev/null +++ b/.github/workflows/default.yml @@ -0,0 +1,111 @@ +name: Perform Checks +on: + pull_request: + branches: + - main + workflow_dispatch: +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPOSITORY_NAME: 'action-demo' + THRESHOLD: 0.9 +permissions: write-all +jobs: + standard-checks: + name: Perform Standard Checks + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + - name: Install Dependencies + run: python3 -m pip install -r requirements.txt + - name: Run Pylint + uses: dciborow/action-pylint@0.1.0 + with: + github_token: ${{ secrets.github_token }} + # Change reviewdog reporter if you need [github-pr-check,github-check,github-pr-review]. + reporter: github-pr-review + # Change reporter level if you need. + # GitHub Status Check won't become failure with warning. + level: warning + glob_pattern: "sources/**/*.py" + - name: Behave Tests + run: coverage run --source="sources" -m behave tests/qualification_tests + - name: Generate XML coverage report + run: coverage xml + - name: Get Coverage + uses: orgoro/coverage@v3.1 + with: + coverageFile: coverage.xml + token: ${{ secrets.GITHUB_TOKEN }} + thresholdAll: ${{ env.THRESHOLD }} + - name: Retrieve Total Coverage + id: coverage + if: '!cancelled()' + run: echo "TOTAL_COVERAGE=$(coverage report | grep TOTAL | awk '{ print $4 }' | sed 's/%//g')" >> "$GITHUB_ENV" + - name: Retrieve Coverage Color + if: '!cancelled()' + uses: jannekem/run-python-script-action@v1 + id: coverage-color + with: + fail-on-error: false + script: | + if (int("${{ env.TOTAL_COVERAGE }}") / 100.0) < float("${{ env.THRESHOLD }}"): + print("red", end='') + else: + print("green", end='') + - name: Retrieve Release Version + id: version + if: '!cancelled()' + run: echo "VERSION=$(python3 setup.py --version)" >> "$GITHUB_ENV" + - name: Add Badges + if: '!cancelled()' + # You may pin to the exact commit or the version. + # uses: wow-actions/add-badges@43f2c1eaecfb2596b89a8136a3fbda4f18d1d188 + uses: wow-actions/add-badges@v1.1.0 + env: + repo_url: ${{ github.event.repository.html_url }} + repo_name: ${{ github.event.repository.name }} + repo_owner: ${{ github.event.repository.owner.login }} + with: + # Your GitHub token for authentication. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # The badges to add with JSON format + badges: | + [ + { + "badge": "https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge", + "alt": "MIT License", + "link": "${{ env.repo_url }}/blob/main/LICENSE" + }, + { + "badge": "https://img.shields.io/badge/Language-Python-blue?style=for-the-badge&logo=python", + "alt": "Language", + "link": "https://www.python.org/" + }, + { + "badge": "https://img.shields.io/badge/PRs-Welcome-brightgreen.svg?style=for-the-badge", + "alt": "PRs Welcome", + "link": "${{ env.repo_url }}/pulls" + }, + { + "badge": "https://img.shields.io/badge/TestPyPi-${{ env.VERSION }}-brightgreen.svg?style=for-the-badge", + "alt": "TestPyPi ${{ env.VERSION }}", + "link": "https://test.pypi.org/project/${{ env.REPOSITORY_NAME }}/" + }, + { + "badge": "https://img.shields.io/badge/PyPi-${{ env.VERSION }}-brightgreen.svg?style=for-the-badge", + "alt": "PyPi ${{ env.VERSION }}", + "link": "https://pypi.org/project/${{ env.REPOSITORY_NAME }}/" + }, + { + "badge": "https://img.shields.io/badge/Coverage-${{ env.TOTAL_COVERAGE }}%25-${{ steps.coverage-color.outputs.stdout }}.svg?style=for-the-badge", + "alt": "Coverage ${{ env.TOTAL_COVERAGE }}%" + } + ] + # Path and file name to add badges + path: README.md + # Should center align the badges + center: false diff --git a/.github/workflows/publish-documentation.yml b/.github/workflows/publish-documentation.yml new file mode 100644 index 0000000..b129223 --- /dev/null +++ b/.github/workflows/publish-documentation.yml @@ -0,0 +1,33 @@ +name: Publish Documentation +on: + push: + branches: ['main', 'release'] +permissions: + contents: write +jobs: + sphinx-docs: + name: Sphinx Documentation + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: "3.7" + - name: Update pip + run: python3 -m pip install --upgrade pip + - name: Install dependencies + run: python3 -m pip install -r requirements.txt + - name: Generate documentation for modules + run: python3 setup.py sphinx_update_modules + - name: Build Sphinx documentation + run: python3 setup.py sphinx_build + - name: Publish Documentation + uses: peaceiris/actions-gh-pages@v3 + with: + publish_branch: gh-pages + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/build/ + force_orphan: true + keep_files: true \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e0e923b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,80 @@ +name: Release New Version +on: + push: + branches: + - main +env: + REPOSITORY_NAME: action-demo +jobs: + create-release: + name: Create a new release + permissions: write-all + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: "3.7" + - name: Retrieve Release Version + id: version + run: echo "VERSION=$(python3 setup.py --version)" >> "$GITHUB_ENV" + - name: "✏️ Generate release changelog" + uses: heinrichreimer/github-changelog-generator-action@v2.3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + futureRelease: "v${{ env.VERSION }}" + - name: Create GitHub Release + uses: ncipollo/release-action@v1 + with: + tag: "v${{ env.VERSION }}" + name: "Release v${{ env.VERSION }}" + bodyFile: CHANGELOG.md + - name: Install pypa/build + run: python3 -m pip install build --user + - name: Build a binary wheel and a source tarball + run: python3 -m build --sdist --wheel --outdir dist/ + - name: Publish Artifacts + uses: actions/upload-artifact@v3 + with: + name: linux-dist + path: dist/ + publish-library-dev: + name: Publish new release on dev + needs: ['create-release'] + permissions: write-all + runs-on: ubuntu-latest + environment: + name: dev + url: "https://test.pypi.org/project/${{ env.REPOSITORY_NAME }}/" + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + - name: Download Artifacts + uses: actions/download-artifact@v3 + with: + name: linux-dist + path: dist/ + - name: Publish package distributions to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: "https://test.pypi.org/legacy/" + publish-library-prod: + name: Publish new release on prod + needs: ['create-release', 'publish-library-dev'] + permissions: write-all + runs-on: ubuntu-latest + environment: + name: prod + url: "https://pypi.org/project/${{ env.REPOSITORY_NAME }}/" + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + - name: Download Artifacts + uses: actions/download-artifact@v3 + with: + name: linux-dist + path: dist/ + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 \ No newline at end of file From d9a0be69d775b13a240efa5af024ac31f7dcb228 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 23:22:29 +0200 Subject: [PATCH 045/119] Update repository name --- .github/workflows/default.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 3b1831e..81769ce 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPOSITORY_NAME: 'action-demo' + REPOSITORY_NAME: 'py-git' THRESHOLD: 0.9 permissions: write-all jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e0e923b..e812c8b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,7 @@ on: branches: - main env: - REPOSITORY_NAME: action-demo + REPOSITORY_NAME: py-git jobs: create-release: name: Create a new release From 52a68494a2eb2ab343598820f26a8f38411c9d20 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 23:52:58 +0200 Subject: [PATCH 046/119] Fix issues in add and rm commands --- sources/git.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sources/git.py b/sources/git.py index 7f74555..739a4eb 100644 --- a/sources/git.py +++ b/sources/git.py @@ -411,6 +411,8 @@ def add(self, files: Union[str, Path, List[Union[str, Path]]], *options: GitOpti for file_path in files: if isinstance(file_path, Path): file_path = str(file_path.absolute()) + if not isinstance(file_path, list): + file_path = [file_path] options = list(options) options.append(AddCommandDefinitions.Options.PATHSPEC.create_option(file_path)) try: @@ -448,6 +450,8 @@ def rm(self, files: Union[str, Path, List[Union[str, Path]]], *options: GitOptio else: file_path = self.__repository_information.path.joinpath(file_path) file_path = str(file_path.absolute()) + if not isinstance(file_path, list): + file_path = [file_path] options = list(options) options.append(RmCommandDefinitions.Options.PATHSPEC.create_option(file_path)) try: From 5d8abe21c8b9c1cffee01cd1cab8142304d2fe8c Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 30 Sep 2023 23:54:10 +0200 Subject: [PATCH 047/119] Minor code cleanup --- .../features/steps/basic_operations.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/acceptance_tests/features/steps/basic_operations.py b/tests/acceptance_tests/features/steps/basic_operations.py index 4680a7e..9d062bb 100644 --- a/tests/acceptance_tests/features/steps/basic_operations.py +++ b/tests/acceptance_tests/features/steps/basic_operations.py @@ -7,7 +7,7 @@ from behave.runner import Context from sources.exceptions import GitException -from sources.git import GitRepository, Commit +from sources.git import GitRepository @given("new git repository") @@ -29,7 +29,7 @@ def step_impl(context: Context): new_file = repository.path.joinpath('add_file.txt') logging.info(f'Create a new file: {new_file}') with new_file.open('w') as file: - file.write(f'Add new file for testing "git add" command.\r\n') + file.write(r'Add new file for testing "git add" command.\r\n') context.new_file = new_file @@ -65,8 +65,11 @@ def step_impl(context: Context): def step_impl(context: Context): new_file: Path = context.new_file repository: GitRepository = context.repository + successfully_removed = False try: repository.git_command.execute(['ls-files', '--error-unmatch', new_file.name]) - assert False + successfully_removed = False except GitException: - assert True + successfully_removed = True + finally: + assert successfully_removed From afb0009c78cf7226b178d4a5c36026a7dfa921d3 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 00:03:28 +0200 Subject: [PATCH 048/119] Change LICENSE file extension --- LICENSE => LICENSE.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename LICENSE => LICENSE.md (100%) diff --git a/LICENSE b/LICENSE.md similarity index 100% rename from LICENSE rename to LICENSE.md From c78bf1fd53e28ead5f74c82e2ffa1c020247ea5f Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 02:34:47 +0200 Subject: [PATCH 049/119] Add tests for git init, git clone, and git mv commands --- .../features/basic_operations.feature | 22 +++++- .../features/steps/basic_operations.py | 68 ++++++++++++++++++- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/tests/acceptance_tests/features/basic_operations.feature b/tests/acceptance_tests/features/basic_operations.feature index 5755f9d..89fc507 100644 --- a/tests/acceptance_tests/features/basic_operations.feature +++ b/tests/acceptance_tests/features/basic_operations.feature @@ -1,4 +1,14 @@ -Feature: git basic operations +Feature: Git Basic Operations + Scenario: Initialize a new repository + Given new local repository path + When executing 'git init' + Then new empty repository will be created + + Scenario: Clone repository + Given remote repository + When executing 'git clone' + Then new repository will be cloned + Scenario: Add a new file Given new git repository And new file has been created @@ -11,4 +21,12 @@ Feature: git basic operations And new file has been added to tracking And commit changes When executing 'git rm' on file - Then the file will be removed from the git tracking \ No newline at end of file + Then the file will be removed from the git tracking + + Scenario: Move (rename) a file + Given new git repository + And new file has been created + And new file has been added to tracking + And commit changes + When executing 'git mv' on file + Then the file will be renamed \ No newline at end of file diff --git a/tests/acceptance_tests/features/steps/basic_operations.py b/tests/acceptance_tests/features/steps/basic_operations.py index 9d062bb..86abe5f 100644 --- a/tests/acceptance_tests/features/steps/basic_operations.py +++ b/tests/acceptance_tests/features/steps/basic_operations.py @@ -7,7 +7,55 @@ from behave.runner import Context from sources.exceptions import GitException -from sources.git import GitRepository +from sources.git import GitRepository, PathsMapping + + +@given("new local repository path") +def step_impl(context: Context): + hash_name = str(uuid.uuid4()) + if platform.system().lower() == 'windows': + path = Path(r'') + else: + path = Path(fr'/tmp/git-repository-{hash_name}') + logging.info(f'Git Repository: {path}') + context.repository_path = path + + +@when("executing 'git init'") +def step_impl(context: Context): + repository = None + try: + repository = GitRepository.init(path=context.repository_path) + except GitException: + repository = None + context.repository = repository + + +@given("remote repository") +def step_impl(context: Context): + context.remote_repository = 'git@github.com:dl1998/PyGit.git' + + +@when("executing 'git clone'") +def step_impl(context: Context): + repository = None + try: + hash_name = str(uuid.uuid4()) + if platform.system().lower() == 'windows': + path = Path(r'') + else: + path = Path(fr'/tmp/git-repository-{hash_name}') + logging.info(f'Git Repository: {path}') + repository = GitRepository.clone(repository=context.remote_repository, path=path) + except GitException: + repository = None + context.repository = repository + + +@then("new empty repository will be created") +@then("new repository will be cloned") +def step_impl(context: Context): + assert context.repository is not None @given("new git repository") @@ -73,3 +121,21 @@ def step_impl(context: Context): successfully_removed = True finally: assert successfully_removed + + +@when("executing 'git mv' on file") +def step_impl(context: Context): + new_file: Path = context.new_file + repository: GitRepository = context.repository + new_path = new_file.parent.joinpath('renamed_file.txt') + mapping = PathsMapping(new_file, new_path) + repository.mv(mapping) + context.new_path = new_path + + +@then("the file will be renamed") +def step_impl(context: Context): + new_file: Path = context.new_file + new_path: Path = context.new_path + assert not new_file.exists() + assert new_path.exists() From 82805e6e8ac183c4857bef403db97aee0ae86553 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 02:35:49 +0200 Subject: [PATCH 050/119] Update PathsMapping to set default root path --- sources/git.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sources/git.py b/sources/git.py index 739a4eb..c0b167b 100644 --- a/sources/git.py +++ b/sources/git.py @@ -212,7 +212,9 @@ class PathsMapping: __destination: Path __root_path: Path - def __init__(self, source: Union[str, Path], destination: Union[str, Path], root_path: Union[str, Path]): + def __init__(self, source: Union[str, Path], destination: Union[str, Path], root_path: Union[str, Path] = None): + if root_path is None: + root_path = Path() self.root_path = root_path self.__source = source self.__destination = destination From a90be710a7aa0a07c2eb3379c6a06c10b434f7c0 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 15:54:41 +0200 Subject: [PATCH 051/119] Extend GitConfig functionality --- sources/git.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/sources/git.py b/sources/git.py index c0b167b..b8f1a58 100644 --- a/sources/git.py +++ b/sources/git.py @@ -36,9 +36,11 @@ class GitConfig: + __configuration_path: Path + def __init__(self, configuration_path: Union[str, Path]): - configuration_path = PathUtil.convert_to_path(configuration_path) - self.__data = self.__read_configuration(configuration_path) + self.__configuration_path = PathUtil.convert_to_path(configuration_path) + self.__data = self.__read_configuration(self.__configuration_path) @staticmethod def __read_configuration(path: Path) -> ConfigParser: @@ -52,6 +54,14 @@ def get(self, section: str, name: str): def set(self, section: str, name: str, value: str): self.__data.set(section, name, value) + def save(self): + with self.__configuration_path.open('w', encoding='UTF-8') as file: + self.__data.write(file) + + @property + def path(self) -> Path: + return self.__configuration_path + @property def remotes(self): regex = re.compile(r'^remote\s*\"(?P[^"]*)\"$') From ebd84db3b7816037699cc2fc39f2dd5e25badf99 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 15:55:45 +0200 Subject: [PATCH 052/119] Add a new class with definition of the 'git config' command --- sources/options/config_options.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 sources/options/config_options.py diff --git a/sources/options/config_options.py b/sources/options/config_options.py new file mode 100644 index 0000000..87f06ee --- /dev/null +++ b/sources/options/config_options.py @@ -0,0 +1,26 @@ +""" +Module contains classes that defines options that could be configured for 'git config' command. +Reference: https://git-scm.com/docs/git-config +""" +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions + + +class ConfigCommandDefinitions(GitCommand): + """ + Options definitions class for 'git config' command, it contains definitions of the options for 'git config'. + """ + class Options(CommandOptions): + """ + Options class for 'git config' command, it contains options that can be configured. + """ + NAME = 'name' + VALUE = 'value' + VALUE_PATTERN = 'value-pattern' + + def __init__(self): + super().__init__('config') + self.definitions = [ + GitOptionDefinition(name=self.Options.NAME, type=str, positional=True, position=0), + GitOptionDefinition(name=self.Options.VALUE, type=str, positional=True, position=1), + GitOptionDefinition(name=self.Options.VALUE_PATTERN, type=str, positional=True, position=2), + ] From 497e5d181d6af28393dc54eb094c0bf9448ecbe8 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 16:00:09 +0200 Subject: [PATCH 053/119] Update GitRepository class to use ConfigCommandDefinitions --- sources/git.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/sources/git.py b/sources/git.py index b8f1a58..6384786 100644 --- a/sources/git.py +++ b/sources/git.py @@ -22,6 +22,7 @@ from sources.models.tags import Tags from sources.options.add_options import AddCommandDefinitions from sources.options.clone_options import CloneCommandDefinitions +from sources.options.config_options import ConfigCommandDefinitions from sources.options.init_options import InitCommandDefinitions from sources.options.mv_options import MvCommandDefinitions from sources.options.options import GitOption @@ -356,8 +357,14 @@ def refresh_repository(self, refresh_active_branch: bool = False, refresh_branch self.__tags = tags_parser.tags def __get_default_author(self): - name = self.__git_command.execute(['config', 'user.name']).strip() - email = self.__git_command.execute(['config', 'user.email']).strip() + user_name_options = [ + ConfigCommandDefinitions.Options.NAME.create_option('user.name') + ] + name = self.__git_command.execute(user_name_options, ConfigCommandDefinitions).strip() + user_email_options = [ + ConfigCommandDefinitions.Options.NAME.create_option('user.email') + ] + email = self.__git_command.execute(user_email_options, ConfigCommandDefinitions).strip() return Author(name=name, email=email) @staticmethod From 34002efda9cbbdfbe2ee98eccc403e88b1f15e72 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 16:19:19 +0200 Subject: [PATCH 054/119] Add method for saving changes in the GitIgnore --- sources/git.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/sources/git.py b/sources/git.py index 6384786..85d2237 100644 --- a/sources/git.py +++ b/sources/git.py @@ -77,16 +77,16 @@ def remotes(self): class GitIgnore: - def __init__(self, path: Path): - self.__path = path + def __init__(self, path: Union[str, Path]): + self.__path = PathUtil.convert_to_path(path) if not self.__path.exists(): raise FileNotFoundError(self.__path) - self.__exclude_patterns = self.__read_file(self.__path) + self.exclude_patterns = self.__read_file(self.__path) @classmethod def create_from_content(cls, path: Path, content: str): instance = cls(path) - instance.__exclude_patterns = GitIgnore.__read_content(content.split('\n')) + instance.exclude_patterns = GitIgnore.__read_content(content.split('\n')) return instance @staticmethod @@ -105,11 +105,16 @@ def __read_content(content: List[str]): return exclude_patterns def refresh(self): - self.__exclude_patterns = self.__read_file(self.__path) + self.exclude_patterns = self.__read_file(self.__path) - @property - def patterns(self) -> List[str]: - return self.__exclude_patterns + def save(self, path: Union[str, Path, None] = None): + if path is None: + path = self.__path + else: + path = PathUtil.convert_to_path(path) + with path.open('w') as file: + for exclude_pattern in self.exclude_patterns: + file.write(f'{exclude_pattern}\r\n') class FilesChangesHandler: From da38dbc782b108954b7e4773e7deaa2b6f6ea6f5 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 16:29:55 +0200 Subject: [PATCH 055/119] Add docstrings to GitConfig class --- sources/git.py | 48 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/sources/git.py b/sources/git.py index 85d2237..201c202 100644 --- a/sources/git.py +++ b/sources/git.py @@ -37,6 +37,9 @@ class GitConfig: + """ + Class for accessing and modifying git configuration file (config). + """ __configuration_path: Path def __init__(self, configuration_path: Union[str, Path]): @@ -45,26 +48,63 @@ def __init__(self, configuration_path: Union[str, Path]): @staticmethod def __read_configuration(path: Path) -> ConfigParser: + """ + Method reads configuration from the file. + + :param path: Path to the git config file. + :type path: Path + :return: Parsed configuration. + """ parser = ConfigParser() parser.read(path) return parser - def get(self, section: str, name: str): + def get(self, section: str, name: str) -> str: + """ + Method returns value for the requested parameter from the provided section in the parsed file. + + :param section: Section in which searched parameter is located. + :type section: str + :param name: Name of the parameter. + :type name: str + :return: Value of the parameter from the provided section and name. + """ return self.__data.get(section, name) - def set(self, section: str, name: str, value: str): + def set(self, section: str, name: str, value: str) -> NoReturn: + """ + Method overrides value for the parameter in the provided section under provided name in the parsed file. + + :param section: Section in which parameter will be updated. + :type section: str + :param name: Name of the parameter. + :type name: str + :param value: New value for the parameter. + :type value: str + """ self.__data.set(section, name, value) - def save(self): + def save(self) -> NoReturn: + """ + Save changes made to configuration back into the file. + """ with self.__configuration_path.open('w', encoding='UTF-8') as file: self.__data.write(file) @property def path(self) -> Path: + """ + Path to the configuration file. + """ return self.__configuration_path @property - def remotes(self): + def remotes(self) -> List[Remote]: + """ + Method reads all remotes defined in the config file and returns list of the Remote objects. + + :return: List of the Remote objects. + """ regex = re.compile(r'^remote\s*\"(?P[^"]*)\"$') sections = self.__data.sections() remotes = [] From e488d16ba6df22f3fc50ad1d946704418aace510 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 16:33:16 +0200 Subject: [PATCH 056/119] Resolve minor pylint warnings --- sources/git.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sources/git.py b/sources/git.py index 201c202..5b7a1e0 100644 --- a/sources/git.py +++ b/sources/git.py @@ -152,7 +152,7 @@ def save(self, path: Union[str, Path, None] = None): path = self.__path else: path = PathUtil.convert_to_path(path) - with path.open('w') as file: + with path.open('w', encoding='UTF-8') as file: for exclude_pattern in self.exclude_patterns: file.write(f'{exclude_pattern}\r\n') @@ -195,7 +195,7 @@ def files_status(self): @staticmethod def __update_files_hash(parent: Path, files_list: Dict[str, str]): - for root, folders, files in os.walk(parent.absolute()): + for root, _, files in os.walk(parent.absolute()): for file in files: absolute_path = Path(root, file) with absolute_path.open('rb') as binary_file: @@ -229,7 +229,7 @@ def __get_removed(start: Dict, end: Dict): @staticmethod def __get_excluded(files: List, excludes: List): result = [] - for key, value in files.items(): + for key, _ in files.items(): for exclude in excludes: match = fnmatch.fnmatch(key, exclude) if match: From e212c4300a0f87c218bcc2191d8c5a0abe7daf3f Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 16:45:37 +0200 Subject: [PATCH 057/119] Add docstrings to GitIgnore class --- sources/git.py | 47 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/sources/git.py b/sources/git.py index 5b7a1e0..52ab1e7 100644 --- a/sources/git.py +++ b/sources/git.py @@ -117,6 +117,9 @@ def remotes(self) -> List[Remote]: class GitIgnore: + """ + Class for manipulations with .gitignore exclude patterns list. + """ def __init__(self, path: Union[str, Path]): self.__path = PathUtil.convert_to_path(path) if not self.__path.exists(): @@ -124,19 +127,43 @@ def __init__(self, path: Union[str, Path]): self.exclude_patterns = self.__read_file(self.__path) @classmethod - def create_from_content(cls, path: Path, content: str): + def create_from_content(cls, path: Union[str, Path], content: str) -> 'GitIgnore': + """ + Create GitIgnore instance with the provided content. + + :param path: Path to the '.gitignore' file. + :type path: Union[str, Path] + :param content: Content of the '.gitignore' file. + :type content: str + :return: New instance of the GitIgnore class. + """ instance = cls(path) instance.exclude_patterns = GitIgnore.__read_content(content.split('\n')) return instance @staticmethod - def __read_file(path: Path): + def __read_file(path: Path) -> List[str]: + """ + Read exclude patterns from the provided '.gitignore' file. + + :param path: Path to the '.gitignore' file. + :type path: Path + :return: List with exclude patterns. + """ with path.open('r') as file: exclude_patterns = GitIgnore.__read_content(file.readlines()) return exclude_patterns @staticmethod - def __read_content(content: List[str]): + def __read_content(content: List[str]) -> List[str]: + """ + Parse content lines of the '.gitignore' file and return exclude patterns. + + :param content: Content of the file in the list format (each line is represented as the one element of the + list). + :type content: List[str] + :return: List of exclude patterns. + """ exclude_patterns = [] for line in content: line = line.strip() @@ -144,10 +171,20 @@ def __read_content(content: List[str]): exclude_patterns.append(line) return exclude_patterns - def refresh(self): + def refresh(self) -> NoReturn: + """ + Refresh exclude patterns from the file. + """ self.exclude_patterns = self.__read_file(self.__path) - def save(self, path: Union[str, Path, None] = None): + def save(self, path: Union[str, Path, None] = None) -> NoReturn: + """ + Save current list of exclude patterns to the provided file, if no file has been provided, then save to the + current file. + + :param path: Path where content will be saved. + :type path: Union[str, Path, None] + """ if path is None: path = self.__path else: From 2f206182e5bc11ac65578c98af68c8e9576d246e Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 19:10:48 +0200 Subject: [PATCH 058/119] Add classes to handle option name aliases --- sources/options/options.py | 113 +++++++++++++++++++++++++++++++------ 1 file changed, 97 insertions(+), 16 deletions(-) diff --git a/sources/options/options.py b/sources/options/options.py index 029ff4c..c88e4f1 100644 --- a/sources/options/options.py +++ b/sources/options/options.py @@ -18,15 +18,83 @@ class GitOption: value: Any +@dataclass +class GitOptionNameAlias: + """ + Class stores fields necessary to define git option alias. + """ + name: str + short_option: bool = field(default=False) + + +@dataclass +class GitOptionNameAliases: + """ + Class stores all aliases for the git option. + """ + aliases: List[GitOptionNameAlias] = field(default_factory=list) + + def get_aliases(self, short_option: bool) -> List[GitOptionNameAlias]: + """ + Return list of the aliases limited to short options or long options. + + :param short_option: If True, then returns all short option aliases, otherwise returns all long aliases. + :type short_option: bool + :return: Found aliases that satisfies criteria. + """ + found_aliases = [] + for alias in self.aliases: + if alias.short_option == short_option: + found_aliases.append(alias) + return found_aliases + + def has_short_aliases(self) -> bool: + """ + Returns whether aliases list has short options. + + :return: True, if there is at least one short option alias on the list of aliases, otherwise False. + """ + return len(self.get_aliases(short_option=True)) > 0 + + def has_long_aliases(self) -> bool: + """ + Returns whether aliases list has long options. + + :return: True, if there is at least one long option alias on the list of aliases, otherwise False. + """ + return len(self.get_aliases(short_option=False)) > 0 + + def exists(self, name: str) -> bool: + """ + Check is alias with provided name exists on the list of the aliases. + + :return: True, if alias exists, otherwise False. + """ + for alias in self.aliases: + if alias.name == name: + return True + return False + + def get_names(self) -> List[str]: + """ + Collect and return all aliases names. + + :return: List with aliases names. + """ + names = [] + for alias in self.aliases: + names.append(alias.name) + return names + + @dataclass class GitOptionDefinition: """ Class defines git option for the git command. It describes option behaviour and some attributes applicable for this option. """ - name: Union[str, 'CommandOptions'] + name_aliases: Union[GitOptionNameAliases, 'CommandOptions'] type: Union[Type, Tuple] - short_name: str = field(default=None) required: bool = field(default=False) positional: bool = field(default=False) position: int = field(default=None) @@ -34,8 +102,8 @@ class GitOptionDefinition: separator: str = field(default=' ') def __post_init__(self): - if isinstance(self.name, CommandOptions): - self.name = self.name.value + if isinstance(self.name_aliases, CommandOptions): + self.name = self.name_aliases.value def compare_with_option(self, other: GitOption) -> bool: """ @@ -51,7 +119,7 @@ def compare_with_option(self, other: GitOption) -> bool: types = self.type else: types = tuple([self.type]) - if self.name != other.name: + if not self.name_aliases.exists(other.name): is_the_same = False if type(other.value) not in types: is_the_same = False @@ -129,7 +197,8 @@ def validate_positional_list(self) -> Tuple[bool, Optional[str]]: last_position = positions[-1] for definition in self.definitions: if definition.type == list and definition.position != last_position: - return False, definition.name + names = ', '.join(definition.name_aliases.get_names()) + return False, f'[{names}]' return True, None def validate_required(self, options: Union[GitOption, List[GitOption]]) -> Tuple[bool, List[str]]: @@ -142,11 +211,17 @@ def validate_required(self, options: Union[GitOption, List[GitOption]]) -> Tuple otherwise it is set as False. List of string contains name of the options that are required, but they are missing. """ - required_definitions = [definition.name for definition in self.definitions if definition.required] + required_definitions = [definition.name_aliases for definition in self.definitions if definition.required] for option in options: - if option.name in required_definitions: - required_definitions.remove(option.name) - return len(required_definitions) == 0, required_definitions + for required_definition in required_definitions: + if required_definition.exists(option.name): + required_definitions.remove(required_definition) + break + missing_definitions = [] + for required_definition in required_definitions: + aliases = '|'.join(required_definition.get_names()) + missing_definitions.append(f'({aliases})') + return len(required_definitions) == 0, missing_definitions def validate_choices(self, option: GitOption, definition: Optional[GitOptionDefinition] = None) -> bool: """ @@ -176,10 +251,10 @@ def __transform_positional_options_to_command(self, positional_options: List[Git command = [] positional_order = [definition for definition in self.definitions if definition.positional] positional_order = sorted(positional_order, key=lambda positional: positional.position) - positional_order = [positional.name for positional in positional_order] + positional_order = [positional.name_aliases for positional in positional_order] for positional_name in positional_order: for positional_option in positional_options: - if positional_name != positional_option.name: + if not positional_name.exists(positional_option.name): continue if isinstance(positional_option.value, list): values = positional_option.value @@ -207,10 +282,12 @@ def transform_to_command(self, options: Union[GitOption, List[GitOption]]) -> Li positional_options.append(option) continue if not isinstance(option.value, bool) or option.value is not False: - if definition.short_name is not None: - command.append(f'-{definition.short_name}') + if definition.name_aliases.has_short_aliases(): + short_alias = definition.name_aliases.get_aliases(short_option=True)[0] + command.append(f'-{short_alias}') else: - command.append(f'--{definition.name}') + long_alias = definition.name_aliases.get_aliases(short_option=False)[0] + command.append(f'--{long_alias}') if not isinstance(option.value, bool) and definition.separator == ' ': command.append(option.value) elif not isinstance(option.value, bool): @@ -245,4 +322,8 @@ def create_option(self, value: Any) -> GitOption: :type value: Any :return: A new GitOption object for this option with the provided value. """ - return GitOption(name=self.value, value=value) + if self.value.has_long_aliases(): + name_alias = self.value.get_aliases(short_option=False)[0] + else: + name_alias = self.value.get_aliases(short_option=True)[0] + return GitOption(name=name_alias.name, value=value) From 1e46ffbfd1f9350c623b8f025038c5c07ba0d77d Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 19:36:40 +0200 Subject: [PATCH 059/119] Extend git options definitions to use option name aliases --- sources/options/add_options.py | 31 ++++++--- sources/options/clone_options.py | 83 +++++++++++++++++-------- sources/options/config_options.py | 21 ++++--- sources/options/for_each_ref_options.py | 75 ++++++++++++++-------- sources/options/init_options.py | 29 ++++++--- sources/options/log_options.py | 58 +++++++++++------ sources/options/mv_options.py | 29 ++++++--- sources/options/options.py | 6 +- sources/options/pull_options.py | 66 +++++++++++++------- sources/options/push_options.py | 60 ++++++++++++------ sources/options/rm_options.py | 29 ++++++--- sources/options/show_options.py | 22 ++++--- 12 files changed, 346 insertions(+), 163 deletions(-) diff --git a/sources/options/add_options.py b/sources/options/add_options.py index c5fb11c..a158ca6 100644 --- a/sources/options/add_options.py +++ b/sources/options/add_options.py @@ -2,27 +2,40 @@ Module contains classes that defines options that could be configured for 'git add' command. Reference: https://git-scm.com/docs/git-add """ -from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ + GitOptionNameAlias class AddCommandDefinitions(GitCommand): """ Options definitions class for 'git add' command, it contains definitions of the options for 'git add'. """ + class Options(CommandOptions): """ Options class for 'git add' command, it contains options that can be configured. """ - VERBOSE = 'verbose' - FORCE = 'force' - UPDATE = 'update' - PATHSPEC = 'pathspec' + VERBOSE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='verbose', short_option=False), + GitOptionNameAlias(name='v', short_option=True), + ]) + FORCE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='force', short_option=False), + GitOptionNameAlias(name='f', short_option=True), + ]) + UPDATE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='update', short_option=False), + GitOptionNameAlias(name='u', short_option=True), + ]) + PATHSPEC = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='pathspec', short_option=False), + ]) def __init__(self): super().__init__('add') self.definitions = [ - GitOptionDefinition(name=self.Options.VERBOSE, type=bool, short_name='v'), - GitOptionDefinition(name=self.Options.FORCE, type=bool, short_name='f'), - GitOptionDefinition(name=self.Options.UPDATE, type=bool, short_name='u'), - GitOptionDefinition(name=self.Options.PATHSPEC, type=list, positional=True, position=0), + GitOptionDefinition(name_aliases=self.Options.VERBOSE, type=bool), + GitOptionDefinition(name_aliases=self.Options.FORCE, type=bool), + GitOptionDefinition(name_aliases=self.Options.UPDATE, type=bool), + GitOptionDefinition(name_aliases=self.Options.PATHSPEC, type=list, positional=True, position=0), ] diff --git a/sources/options/clone_options.py b/sources/options/clone_options.py index 65adcd3..717384d 100644 --- a/sources/options/clone_options.py +++ b/sources/options/clone_options.py @@ -2,43 +2,76 @@ Module contains classes that defines options that could be configured for 'git clone' command. Reference: https://git-scm.com/docs/git-clone """ -from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAlias, \ + GitOptionNameAliases class CloneCommandDefinitions(GitCommand): """ Options definitions class for 'git clone' command, it contains definitions of the options for 'git clone'. """ + class Options(CommandOptions): """ Options class for 'git clone' command, it contains options that can be configured. """ - VERBOSE = 'verbose' - QUIET = 'quiet' - LOCAL = 'local' - NO_HARDLINKS = 'no-hardlinks' - SHARED = 'shared' - BARE = 'bare' - ORIGIN = 'origin' - BRANCH = 'branch' - NO_TAGS = 'no-tags' - RECURSE_SUBMODULES = 'recurse-submodules' - REPOSITORY = 'repository' - DIRECTORY = 'directory' + VERBOSE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='verbose', short_option=False), + GitOptionNameAlias(name='v', short_option=True), + ]) + QUIET = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='quiet', short_option=False), + GitOptionNameAlias(name='q', short_option=True), + ]) + LOCAL = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='local', short_option=False), + GitOptionNameAlias(name='l', short_option=True), + ]) + NO_HARDLINKS = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='no-hardlinks', short_option=False), + ]) + SHARED = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='shared', short_option=False), + GitOptionNameAlias(name='s', short_option=True), + ]) + BARE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='bare', short_option=False), + ]) + ORIGIN = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='origin', short_option=False), + GitOptionNameAlias(name='o', short_option=True), + ]) + BRANCH = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='branch', short_option=False), + GitOptionNameAlias(name='b', short_option=True), + ]) + NO_TAGS = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='no-tags', short_option=False), + ]) + RECURSE_SUBMODULES = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='recurse-submodules', short_option=False), + ]) + REPOSITORY = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='repository', short_option=False), + ]) + DIRECTORY = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='directory', short_option=False), + ]) def __init__(self): super().__init__('clone') self.definitions = [ - GitOptionDefinition(name=self.Options.VERBOSE, type=bool, short_name='v'), - GitOptionDefinition(name=self.Options.QUIET, type=bool, short_name='q'), - GitOptionDefinition(name=self.Options.LOCAL, type=bool, short_name='l'), - GitOptionDefinition(name=self.Options.NO_HARDLINKS, type=bool), - GitOptionDefinition(name=self.Options.SHARED, type=bool, short_name='s'), - GitOptionDefinition(name=self.Options.BARE, type=bool), - GitOptionDefinition(name=self.Options.ORIGIN, type=bool, short_name='o'), - GitOptionDefinition(name=self.Options.BRANCH, type=bool, short_name='b'), - GitOptionDefinition(name=self.Options.NO_TAGS, type=bool), - GitOptionDefinition(name=self.Options.RECURSE_SUBMODULES, type=(bool, str)), - GitOptionDefinition(name=self.Options.REPOSITORY, type=str, positional=True, position=0, required=True), - GitOptionDefinition(name=self.Options.DIRECTORY, type=str, positional=True, position=1), + GitOptionDefinition(name_aliases=self.Options.VERBOSE, type=bool), + GitOptionDefinition(name_aliases=self.Options.QUIET, type=bool), + GitOptionDefinition(name_aliases=self.Options.LOCAL, type=bool), + GitOptionDefinition(name_aliases=self.Options.NO_HARDLINKS, type=bool), + GitOptionDefinition(name_aliases=self.Options.SHARED, type=bool), + GitOptionDefinition(name_aliases=self.Options.BARE, type=bool), + GitOptionDefinition(name_aliases=self.Options.ORIGIN, type=bool), + GitOptionDefinition(name_aliases=self.Options.BRANCH, type=bool), + GitOptionDefinition(name_aliases=self.Options.NO_TAGS, type=bool), + GitOptionDefinition(name_aliases=self.Options.RECURSE_SUBMODULES, type=(bool, str)), + GitOptionDefinition(name_aliases=self.Options.REPOSITORY, type=str, positional=True, position=0, + required=True), + GitOptionDefinition(name_aliases=self.Options.DIRECTORY, type=str, positional=True, position=1), ] diff --git a/sources/options/config_options.py b/sources/options/config_options.py index 87f06ee..9808824 100644 --- a/sources/options/config_options.py +++ b/sources/options/config_options.py @@ -2,7 +2,8 @@ Module contains classes that defines options that could be configured for 'git config' command. Reference: https://git-scm.com/docs/git-config """ -from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ + GitOptionNameAlias class ConfigCommandDefinitions(GitCommand): @@ -13,14 +14,20 @@ class Options(CommandOptions): """ Options class for 'git config' command, it contains options that can be configured. """ - NAME = 'name' - VALUE = 'value' - VALUE_PATTERN = 'value-pattern' + NAME = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='name', short_option=False), + ]) + VALUE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='value', short_option=False), + ]) + VALUE_PATTERN = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='value-pattern', short_option=False), + ]) def __init__(self): super().__init__('config') self.definitions = [ - GitOptionDefinition(name=self.Options.NAME, type=str, positional=True, position=0), - GitOptionDefinition(name=self.Options.VALUE, type=str, positional=True, position=1), - GitOptionDefinition(name=self.Options.VALUE_PATTERN, type=str, positional=True, position=2), + GitOptionDefinition(name_aliases=self.Options.NAME, type=str, positional=True, position=0), + GitOptionDefinition(name_aliases=self.Options.VALUE, type=str, positional=True, position=1), + GitOptionDefinition(name_aliases=self.Options.VALUE_PATTERN, type=str, positional=True, position=2), ] diff --git a/sources/options/for_each_ref_options.py b/sources/options/for_each_ref_options.py index 0d0e831..351761d 100644 --- a/sources/options/for_each_ref_options.py +++ b/sources/options/for_each_ref_options.py @@ -2,7 +2,8 @@ Module contains classes that defines options that could be configured for 'git for-each-ref' command. Reference: https://git-scm.com/docs/git-for-each-ref """ -from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ + GitOptionNameAlias class ForEachRefCommandDefinitions(GitCommand): @@ -14,32 +15,56 @@ class Options(CommandOptions): """ Options class for 'git for-each-ref' command, it contains options that can be configured. """ - COUNT = 'count' - SORT = 'sort' - FORMAT = 'format' - POINTS_AT = 'points-at' - MERGED = 'merged' - NO_MERGED = 'no-merged' - CONTAINS = 'contains' - NO_CONTAINS = 'no-contains' - IGNORE_CASE = 'ignore-case' - OMIT_EMPTY = 'omit-empty' - EXCLUDE = 'exclude' - PATTERN = 'pattern' + COUNT = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='count', short_option=False), + ]) + SORT = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='sort', short_option=False), + ]) + FORMAT = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='format', short_option=False), + ]) + POINTS_AT = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='points-at', short_option=False), + ]) + MERGED = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='merged', short_option=False), + ]) + NO_MERGED = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='no-merged', short_option=False), + ]) + CONTAINS = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='contains', short_option=False), + ]) + NO_CONTAINS = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='no-contains', short_option=False), + ]) + IGNORE_CASE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='ignore-case', short_option=False), + ]) + OMIT_EMPTY = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='omit-empty', short_option=False), + ]) + EXCLUDE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='exclude', short_option=False), + ]) + PATTERN = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='pattern', short_option=False), + ]) def __init__(self): super().__init__('for-each-ref') self.definitions = [ - GitOptionDefinition(name=self.Options.COUNT, type=str, separator='='), - GitOptionDefinition(name=self.Options.SORT, type=str, separator='='), - GitOptionDefinition(name=self.Options.FORMAT, type=str, separator='='), - GitOptionDefinition(name=self.Options.POINTS_AT, type=str, separator='='), - GitOptionDefinition(name=self.Options.MERGED, type=(bool, str), separator='='), - GitOptionDefinition(name=self.Options.NO_MERGED, type=(bool, str), separator='='), - GitOptionDefinition(name=self.Options.CONTAINS, type=(bool, str), separator='='), - GitOptionDefinition(name=self.Options.NO_CONTAINS, type=(bool, str), separator='='), - GitOptionDefinition(name=self.Options.IGNORE_CASE, type=bool), - GitOptionDefinition(name=self.Options.OMIT_EMPTY, type=bool), - GitOptionDefinition(name=self.Options.EXCLUDE, type=str, separator='='), - GitOptionDefinition(name=self.Options.PATTERN, type=str, positional=True, position=0), + GitOptionDefinition(name_aliases=self.Options.COUNT, type=str, separator='='), + GitOptionDefinition(name_aliases=self.Options.SORT, type=str, separator='='), + GitOptionDefinition(name_aliases=self.Options.FORMAT, type=str, separator='='), + GitOptionDefinition(name_aliases=self.Options.POINTS_AT, type=str, separator='='), + GitOptionDefinition(name_aliases=self.Options.MERGED, type=(bool, str), separator='='), + GitOptionDefinition(name_aliases=self.Options.NO_MERGED, type=(bool, str), separator='='), + GitOptionDefinition(name_aliases=self.Options.CONTAINS, type=(bool, str), separator='='), + GitOptionDefinition(name_aliases=self.Options.NO_CONTAINS, type=(bool, str), separator='='), + GitOptionDefinition(name_aliases=self.Options.IGNORE_CASE, type=bool), + GitOptionDefinition(name_aliases=self.Options.OMIT_EMPTY, type=bool), + GitOptionDefinition(name_aliases=self.Options.EXCLUDE, type=str, separator='='), + GitOptionDefinition(name_aliases=self.Options.PATTERN, type=str, positional=True, position=0), ] diff --git a/sources/options/init_options.py b/sources/options/init_options.py index bd4d44c..1087c81 100644 --- a/sources/options/init_options.py +++ b/sources/options/init_options.py @@ -2,7 +2,8 @@ Module contains classes that defines options that could be configured for 'git init' command. Reference: https://git-scm.com/docs/git-init """ -from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ + GitOptionNameAlias class InitCommandDefinitions(GitCommand): @@ -13,16 +14,26 @@ class Options(CommandOptions): """ Options class for 'git init' command, it contains options that can be configured. """ - QUIET = 'quiet' - BARE = 'bare' - INITIAL_BRANCH = 'initial-branch' - DIRECTORY = 'directory' + QUIET = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='quiet', short_option=False), + GitOptionNameAlias(name='q', short_option=True), + ]) + BARE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='bare', short_option=False), + ]) + INITIAL_BRANCH = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='initial-branch', short_option=False), + GitOptionNameAlias(name='b', short_option=True), + ]) + DIRECTORY = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='directory', short_option=False), + ]) def __init__(self): super().__init__('init') self.definitions = [ - GitOptionDefinition(name=self.Options.QUIET, type=bool, short_name='q'), - GitOptionDefinition(name=self.Options.BARE, type=bool), - GitOptionDefinition(name=self.Options.INITIAL_BRANCH, type=bool, short_name='b'), - GitOptionDefinition(name=self.Options.DIRECTORY, type=str, positional=True, position=0), + GitOptionDefinition(name_aliases=self.Options.QUIET, type=bool), + GitOptionDefinition(name_aliases=self.Options.BARE, type=bool), + GitOptionDefinition(name_aliases=self.Options.INITIAL_BRANCH, type=bool), + GitOptionDefinition(name_aliases=self.Options.DIRECTORY, type=str, positional=True, position=0), ] diff --git a/sources/options/log_options.py b/sources/options/log_options.py index 059f09b..f113982 100644 --- a/sources/options/log_options.py +++ b/sources/options/log_options.py @@ -2,7 +2,8 @@ Module contains classes that defines options that could be configured for 'git log' command. Reference: https://git-scm.com/docs/git-log """ -from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ + GitOptionNameAlias class LogCommandDefinitions(GitCommand): @@ -13,26 +14,45 @@ class Options(CommandOptions): """ Options class for 'git log' command, it contains options that can be configured. """ - MAX_COUNT = 'max-count' - SKIP = 'skip' - BRANCHES = 'branches' - ALL = 'all' - FORMAT = 'format' - PRETTY = 'pretty' - DATE = 'date' - REVISION_RANGE = 'revision-range' - PATH = 'path' + MAX_COUNT = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='max-count', short_option=False), + GitOptionNameAlias(name='n', short_option=True), + ]) + SKIP = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='skip', short_option=False), + ]) + BRANCHES = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='branches', short_option=False), + ]) + ALL = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='all', short_option=False), + ]) + FORMAT = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='format', short_option=False), + ]) + PRETTY = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='pretty', short_option=False), + ]) + DATE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='date', short_option=False), + ]) + REVISION_RANGE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='revision-range', short_option=False), + ]) + PATH = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='path', short_option=False), + ]) def __init__(self): super().__init__('log') self.definitions = [ - GitOptionDefinition(name=self.Options.MAX_COUNT, type=int, short_name='n'), - GitOptionDefinition(name=self.Options.SKIP, type=int), - GitOptionDefinition(name=self.Options.BRANCHES, type=(bool, str)), - GitOptionDefinition(name=self.Options.ALL, type=bool), - GitOptionDefinition(name=self.Options.FORMAT, type=str, separator='='), - GitOptionDefinition(name=self.Options.PRETTY, type=str, separator='='), - GitOptionDefinition(name=self.Options.DATE, type=str, separator='='), - GitOptionDefinition(name=self.Options.REVISION_RANGE, type=str, positional=True, position=0), - GitOptionDefinition(name=self.Options.PATH, type=str, positional=True, position=1), + GitOptionDefinition(name_aliases=self.Options.MAX_COUNT, type=int), + GitOptionDefinition(name_aliases=self.Options.SKIP, type=int), + GitOptionDefinition(name_aliases=self.Options.BRANCHES, type=(bool, str)), + GitOptionDefinition(name_aliases=self.Options.ALL, type=bool), + GitOptionDefinition(name_aliases=self.Options.FORMAT, type=str, separator='='), + GitOptionDefinition(name_aliases=self.Options.PRETTY, type=str, separator='='), + GitOptionDefinition(name_aliases=self.Options.DATE, type=str, separator='='), + GitOptionDefinition(name_aliases=self.Options.REVISION_RANGE, type=str, positional=True, position=0), + GitOptionDefinition(name_aliases=self.Options.PATH, type=str, positional=True, position=1), ] diff --git a/sources/options/mv_options.py b/sources/options/mv_options.py index b029418..2da29e0 100644 --- a/sources/options/mv_options.py +++ b/sources/options/mv_options.py @@ -2,7 +2,8 @@ Module contains classes that defines options that could be configured for 'git mv' command. Reference: https://git-scm.com/docs/git-mv """ -from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ + GitOptionNameAlias class MvCommandDefinitions(GitCommand): @@ -13,16 +14,26 @@ class Options(CommandOptions): """ Options class for 'git mv' command, it contains options that can be configured. """ - FORCE = 'force' - VERBOSE = 'verbose' - SOURCE = 'source' - DESTINATION = 'destination' + FORCE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='force', short_option=False), + GitOptionNameAlias(name='f', short_option=True), + ]) + VERBOSE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='verbose', short_option=False), + GitOptionNameAlias(name='v', short_option=True), + ]) + SOURCE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='source', short_option=False), + ]) + DESTINATION = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='destination', short_option=False), + ]) def __init__(self): super().__init__('mv') self.definitions = [ - GitOptionDefinition(name=self.Options.FORCE, type=bool, short_name='f'), - GitOptionDefinition(name=self.Options.VERBOSE, type=bool, short_name='v'), - GitOptionDefinition(name=self.Options.SOURCE, type=str, positional=True, position=0), - GitOptionDefinition(name=self.Options.DESTINATION, type=str, positional=True, position=1), + GitOptionDefinition(name_aliases=self.Options.FORCE, type=bool), + GitOptionDefinition(name_aliases=self.Options.VERBOSE, type=bool), + GitOptionDefinition(name_aliases=self.Options.SOURCE, type=str, positional=True, position=0), + GitOptionDefinition(name_aliases=self.Options.DESTINATION, type=str, positional=True, position=1), ] diff --git a/sources/options/options.py b/sources/options/options.py index c88e4f1..876f9a5 100644 --- a/sources/options/options.py +++ b/sources/options/options.py @@ -103,7 +103,7 @@ class GitOptionDefinition: def __post_init__(self): if isinstance(self.name_aliases, CommandOptions): - self.name = self.name_aliases.value + self.name_aliases = self.name_aliases.value def compare_with_option(self, other: GitOption) -> bool: """ @@ -283,10 +283,10 @@ def transform_to_command(self, options: Union[GitOption, List[GitOption]]) -> Li continue if not isinstance(option.value, bool) or option.value is not False: if definition.name_aliases.has_short_aliases(): - short_alias = definition.name_aliases.get_aliases(short_option=True)[0] + short_alias = definition.name_aliases.get_aliases(short_option=True)[0].name command.append(f'-{short_alias}') else: - long_alias = definition.name_aliases.get_aliases(short_option=False)[0] + long_alias = definition.name_aliases.get_aliases(short_option=False)[0].name command.append(f'--{long_alias}') if not isinstance(option.value, bool) and definition.separator == ' ': command.append(option.value) diff --git a/sources/options/pull_options.py b/sources/options/pull_options.py index 8f4ae2b..1255a4a 100644 --- a/sources/options/pull_options.py +++ b/sources/options/pull_options.py @@ -2,7 +2,8 @@ Module contains classes that defines options that could be configured for 'git pull' command. Reference: https://git-scm.com/docs/git-pull """ -from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ + GitOptionNameAlias class PullCommandDefinitions(GitCommand): @@ -14,16 +15,39 @@ class Options(CommandOptions): """ Options class for 'git pull' command, it contains options that can be configured. """ - QUIET = 'quiet' - VERBOSE = 'verbose' - RECURSE_SUBMODULES = 'recurse-submodules' - COMMIT = 'commit' - FAST_FORWARD_ONLY = 'ff-only' - FAST_FORWARD = 'ff' - ALL = 'all' - FORCE = 'force' - REPOSITORY = 'repository' - REFSPEC = 'refspec' + QUIET = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='quiet', short_option=False), + GitOptionNameAlias(name='q', short_option=True), + ]) + VERBOSE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='verbose', short_option=False), + GitOptionNameAlias(name='v', short_option=True), + ]) + RECURSE_SUBMODULES = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='recurse-submodules', short_option=False), + ]) + COMMIT = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='commit', short_option=False), + ]) + FAST_FORWARD_ONLY = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='ff-only', short_option=False), + ]) + FAST_FORWARD = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='ff', short_option=False), + ]) + ALL = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='all', short_option=False), + ]) + FORCE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='force', short_option=False), + GitOptionNameAlias(name='f', short_option=True), + ]) + REPOSITORY = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='repository', short_option=False), + ]) + REFSPEC = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='refspec', short_option=False), + ]) class RecurseSubmodulesChoices(CommandOptions): """ @@ -36,15 +60,15 @@ class RecurseSubmodulesChoices(CommandOptions): def __init__(self): super().__init__('pull') self.definitions = [ - GitOptionDefinition(name=self.Options.QUIET, type=bool, short_name='q'), - GitOptionDefinition(name=self.Options.VERBOSE, type=bool, short_name='v'), - GitOptionDefinition(name=self.Options.RECURSE_SUBMODULES, type=str, + GitOptionDefinition(name_aliases=self.Options.QUIET, type=bool), + GitOptionDefinition(name_aliases=self.Options.VERBOSE, type=bool), + GitOptionDefinition(name_aliases=self.Options.RECURSE_SUBMODULES, type=str, choices=self.RecurseSubmodulesChoices, separator='='), - GitOptionDefinition(name=self.Options.COMMIT, type=bool), - GitOptionDefinition(name=self.Options.FAST_FORWARD_ONLY, type=bool), - GitOptionDefinition(name=self.Options.FAST_FORWARD, type=bool), - GitOptionDefinition(name=self.Options.ALL, type=bool), - GitOptionDefinition(name=self.Options.FORCE, type=bool, short_name='f'), - GitOptionDefinition(name=self.Options.REPOSITORY, type=str, positional=True, position=0), - GitOptionDefinition(name=self.Options.REFSPEC, type=str, positional=True, position=1), + GitOptionDefinition(name_aliases=self.Options.COMMIT, type=bool), + GitOptionDefinition(name_aliases=self.Options.FAST_FORWARD_ONLY, type=bool), + GitOptionDefinition(name_aliases=self.Options.FAST_FORWARD, type=bool), + GitOptionDefinition(name_aliases=self.Options.ALL, type=bool), + GitOptionDefinition(name_aliases=self.Options.FORCE, type=bool), + GitOptionDefinition(name_aliases=self.Options.REPOSITORY, type=str, positional=True, position=0), + GitOptionDefinition(name_aliases=self.Options.REFSPEC, type=str, positional=True, position=1), ] diff --git a/sources/options/push_options.py b/sources/options/push_options.py index 92b9b5d..267adfc 100644 --- a/sources/options/push_options.py +++ b/sources/options/push_options.py @@ -2,7 +2,8 @@ Module contains classes that defines options that could be configured for 'git push' command. Reference: https://git-scm.com/docs/git-push """ -from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ + GitOptionNameAlias class PushCommandDefinitions(GitCommand): @@ -14,15 +15,34 @@ class Options(CommandOptions): """ Options class for 'git push' command, it contains options that can be configured. """ - VERBOSE = 'verbose' - RECURSE_SUBMODULES = 'recurse-submodules' - ALL = 'all' - BRANCHES = 'branches' - PRUNE = 'prune' - DELETE = 'delete' - TAGS = 'tags' - REPOSITORY = 'repository' - REFSPEC = 'refspec' + VERBOSE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='verbose', short_option=False), + GitOptionNameAlias(name='v', short_option=True), + ]) + RECURSE_SUBMODULES = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='recurse-submodules', short_option=False), + ]) + ALL = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='all', short_option=False), + ]) + BRANCHES = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='branches', short_option=False), + ]) + PRUNE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='prune', short_option=False), + ]) + DELETE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='delete', short_option=False), + ]) + TAGS = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='tags', short_option=False), + ]) + REPOSITORY = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='repository', short_option=False), + ]) + REFSPEC = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='refspec', short_option=False), + ]) class RecurseSubmodulesChoices(CommandOptions): """ @@ -36,14 +56,14 @@ class RecurseSubmodulesChoices(CommandOptions): def __init__(self): super().__init__('push') self.definitions = [ - GitOptionDefinition(name=self.Options.VERBOSE, type=bool, short_name='v'), - GitOptionDefinition(name=self.Options.RECURSE_SUBMODULES, type=str, choices=self.RecurseSubmodulesChoices, - separator='='), - GitOptionDefinition(name=self.Options.ALL, type=bool), - GitOptionDefinition(name=self.Options.BRANCHES, type=bool), - GitOptionDefinition(name=self.Options.PRUNE, type=bool), - GitOptionDefinition(name=self.Options.DELETE, type=bool), - GitOptionDefinition(name=self.Options.TAGS, type=bool), - GitOptionDefinition(name=self.Options.REPOSITORY, type=str, positional=True, position=0), - GitOptionDefinition(name=self.Options.REFSPEC, type=str, positional=True, position=1), + GitOptionDefinition(name_aliases=self.Options.VERBOSE, type=bool), + GitOptionDefinition(name_aliases=self.Options.RECURSE_SUBMODULES, type=str, + choices=self.RecurseSubmodulesChoices, separator='='), + GitOptionDefinition(name_aliases=self.Options.ALL, type=bool), + GitOptionDefinition(name_aliases=self.Options.BRANCHES, type=bool), + GitOptionDefinition(name_aliases=self.Options.PRUNE, type=bool), + GitOptionDefinition(name_aliases=self.Options.DELETE, type=bool), + GitOptionDefinition(name_aliases=self.Options.TAGS, type=bool), + GitOptionDefinition(name_aliases=self.Options.REPOSITORY, type=str, positional=True, position=0), + GitOptionDefinition(name_aliases=self.Options.REFSPEC, type=str, positional=True, position=1), ] diff --git a/sources/options/rm_options.py b/sources/options/rm_options.py index 3c3ab4e..4be9d52 100644 --- a/sources/options/rm_options.py +++ b/sources/options/rm_options.py @@ -2,7 +2,8 @@ Module contains classes that defines options that could be configured for 'git rm' command. Reference: https://git-scm.com/docs/git-rm """ -from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ + GitOptionNameAlias class RmCommandDefinitions(GitCommand): @@ -13,16 +14,26 @@ class Options(CommandOptions): """ Options class for 'git rm' command, it contains options that can be configured. """ - QUIET = 'quiet' - FORCE = 'force' - RECURSIVE = 'r' - PATHSPEC = 'pathspec' + QUIET = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='quiet', short_option=False), + GitOptionNameAlias(name='q', short_option=True), + ]) + FORCE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='force', short_option=False), + GitOptionNameAlias(name='f', short_option=True), + ]) + RECURSIVE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='r', short_option=True), + ]) + PATHSPEC = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='pathspec', short_option=False), + ]) def __init__(self): super().__init__('rm') self.definitions = [ - GitOptionDefinition(name=self.Options.QUIET, type=bool, short_name='q'), - GitOptionDefinition(name=self.Options.FORCE, type=bool, short_name='f'), - GitOptionDefinition(name=self.Options.RECURSIVE, type=bool, short_name='r'), - GitOptionDefinition(name=self.Options.PATHSPEC, type=list, positional=True, position=0), + GitOptionDefinition(name_aliases=self.Options.QUIET, type=bool), + GitOptionDefinition(name_aliases=self.Options.FORCE, type=bool), + GitOptionDefinition(name_aliases=self.Options.RECURSIVE, type=bool), + GitOptionDefinition(name_aliases=self.Options.PATHSPEC, type=list, positional=True, position=0), ] diff --git a/sources/options/show_options.py b/sources/options/show_options.py index 029877e..53d77a8 100644 --- a/sources/options/show_options.py +++ b/sources/options/show_options.py @@ -2,7 +2,8 @@ Module contains classes that defines options that could be configured for 'git show' command. Reference: https://git-scm.com/docs/git-show """ -from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ + GitOptionNameAlias class ShowCommandDefinitions(GitCommand): @@ -13,14 +14,21 @@ class Options(CommandOptions): """ Options class for 'git show' command, it contains options that can be configured. """ - QUIET = 'quiet' - FORMAT = 'format' - OBJECTS = 'objects' + QUIET = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='quiet', short_option=False), + GitOptionNameAlias(name='q', short_option=True), + ]) + FORMAT = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='format', short_option=False), + ]) + OBJECTS = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='objects', short_option=False), + ]) def __init__(self): super().__init__('show') self.definitions = [ - GitOptionDefinition(name=self.Options.QUIET, type=bool, short_name='q'), - GitOptionDefinition(name=self.Options.FORMAT, type=str, separator='='), - GitOptionDefinition(name=self.Options.OBJECTS, type=list, positional=True, position=0), + GitOptionDefinition(name_aliases=self.Options.QUIET, type=bool), + GitOptionDefinition(name_aliases=self.Options.FORMAT, type=str, separator='='), + GitOptionDefinition(name_aliases=self.Options.OBJECTS, type=list, positional=True, position=0), ] From 3f5a95ebda43fa9b7d1bb0d84b74d38cc0c26627 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 20:47:10 +0200 Subject: [PATCH 060/119] Add docstrings and typing to FilesChangesHandler --- sources/git.py | 68 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/sources/git.py b/sources/git.py index 52ab1e7..78c71a6 100644 --- a/sources/git.py +++ b/sources/git.py @@ -195,6 +195,9 @@ def save(self, path: Union[str, Path, None] = None) -> NoReturn: class FilesChangesHandler: + """ + Class tracks and classifies changes in the files within the repository. + """ START = 'start' END = 'end' @@ -219,6 +222,13 @@ def __exit__(self, exc_type, exc_val, exc_tb): @property def files_status(self): + """ + Method checks status of the files by comparing original files state with current files state. All files changes + are classified as added, modified, or removed. + + :return: Dictionary with changes classified as added, modified, and removed, additionally contains excluded + files. + """ self.__update_files_hash(self.__repository.path.absolute(), self.__files_hashes[self.END]) result = { self.ADDED: self.__get_added(self.__files_hashes[self.START], self.__files_hashes[self.END]), @@ -227,20 +237,37 @@ def files_status(self): } if self.__repository.gitignore: result[self.EXCLUDED] = self.__get_excluded(self.__files_hashes[self.END], - self.__repository.gitignore.patterns) + self.__repository.gitignore.exclude_patterns) return result @staticmethod - def __update_files_hash(parent: Path, files_list: Dict[str, str]): + def __update_files_hash(parent: Path, files_dictionary: Dict[str, str]) -> NoReturn: + """ + Method updates files hashes in the provided dictionary. + + :param parent: Parent path, files hashes will be generated for files under this folder. + :type parent: Path + :param files_dictionary: Dictionary that contains file name and its hash. + :type files_dictionary: Dict[str, str] + """ for root, _, files in os.walk(parent.absolute()): for file in files: absolute_path = Path(root, file) with absolute_path.open('rb') as binary_file: file_hash = hashlib.md5(binary_file.read()).hexdigest() - files_list[str(absolute_path)] = file_hash + files_dictionary[str(absolute_path)] = file_hash @staticmethod - def __get_added(start: Dict, end: Dict): + def __get_added(start: Dict, end: Dict) -> List[str]: + """ + Check which files has been added and return list of file names. + + :param start: Dictionary with files and their hashes at the beginning. + :type start: Dict + :param end: Dictionary with files and their hashes at the end. + :type end: Dict + :return: List of added files. + """ result = [] for key, _ in end.items(): if key not in start.keys(): @@ -248,7 +275,16 @@ def __get_added(start: Dict, end: Dict): return result @staticmethod - def __get_modified(start: Dict, end: Dict): + def __get_modified(start: Dict, end: Dict) -> List[str]: + """ + Check which files has been modified and return list of file names. + + :param start: Dictionary with files and their hashes at the beginning. + :type start: Dict + :param end: Dictionary with files and their hashes at the end. + :type end: Dict + :return: List of modified files. + """ result = [] for key, value in start.items(): if key in end.keys() and value != end[key]: @@ -256,7 +292,16 @@ def __get_modified(start: Dict, end: Dict): return result @staticmethod - def __get_removed(start: Dict, end: Dict): + def __get_removed(start: Dict, end: Dict) -> List[str]: + """ + Check which files has been removed and return list of file names. + + :param start: Dictionary with files and their hashes at the beginning. + :type start: Dict + :param end: Dictionary with files and their hashes at the end. + :type end: Dict + :return: List of removed files. + """ result = [] for key, _ in start.items(): if key not in end.keys(): @@ -264,7 +309,16 @@ def __get_removed(start: Dict, end: Dict): return result @staticmethod - def __get_excluded(files: List, excludes: List): + def __get_excluded(files: Dict[str, str], excludes: List[str]) -> List[str]: + """ + Check which files are on the exclude list and return all excluded files. + + :param files: Dictionary with files and their hashes. + :type files: Dict[str, str] + :param excludes: List of exclude patterns. + :type excludes: List[str] + :return: List of files that are excluded. + """ result = [] for key, _ in files.items(): for exclude in excludes: From d142a5d799c8fe439a2135b2f1a220970bc57315 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 23:16:05 +0200 Subject: [PATCH 061/119] Remove old redundant constants that has been moved to TagsParser --- sources/models/tags.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sources/models/tags.py b/sources/models/tags.py index 025a048..e9fdbe8 100644 --- a/sources/models/tags.py +++ b/sources/models/tags.py @@ -13,8 +13,6 @@ class Tag(Reference): """ Base class that represents git tag. """ - DELIMITER: ClassVar[str] = '%n' - FORMAT: ClassVar[str] = DELIMITER.join(['%aN', '%aE', '%H']) name: str commit: Commit From 266ff5b607644d38e04bab6ee7c9260656435b90 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 23:19:46 +0200 Subject: [PATCH 062/119] Update typing for the raw_tags in the docstring --- sources/parsers/tags_parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sources/parsers/tags_parser.py b/sources/parsers/tags_parser.py index 40e99b9..89c2497 100644 --- a/sources/parsers/tags_parser.py +++ b/sources/parsers/tags_parser.py @@ -55,12 +55,12 @@ def tags(self) -> Tags: tags.extend(self.get_tags()) return Tags(tags) - def parse_tags(self, raw_tags: List[str]) -> List[Union[LightweightTag, AnnotatedTag]]: + def parse_tags(self, raw_tags: List[List[str]]) -> List[Union[LightweightTag, AnnotatedTag]]: """ Parse list of the raw strings with tags returned by 'git for-each-ref' to list of tags objects. :param raw_tags: List of raw strings with tags information. - :type raw_tags: List[str] + :type raw_tags: List[List[str]] :return: List of tags objects. """ tags = [] From 0b612c0a954b27995c6a139a816b078cef753e0f Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 23:20:55 +0200 Subject: [PATCH 063/119] Add exceptions for the git commands: show, config, and checkout --- sources/exceptions.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/sources/exceptions.py b/sources/exceptions.py index 3535dba..f4030d0 100644 --- a/sources/exceptions.py +++ b/sources/exceptions.py @@ -51,6 +51,18 @@ class GitCommandException(GitException): """ +class GitInitException(GitCommandException): + """ + Exception thrown when 'init' operation has failed. + """ + + +class GitCloneException(GitCommandException): + """ + Exception thrown when 'clone' operation has failed. + """ + + class GitAddException(GitCommandException): """ Exception thrown when 'add' operation has failed. @@ -79,3 +91,21 @@ class GitPushException(GitCommandException): """ Exception thrown when 'push' operation has failed. """ + + +class GitShowException(GitCommandException): + """ + Exception thrown when 'show' operation has failed. + """ + + +class GitConfigException(GitCommandException): + """ + Exception thrown when 'config' operation has failed. + """ + + +class GitCheckoutException(GitCommandException): + """ + Exception thrown when 'checkout' operation has failed. + """ From 02d6f7d6856caf16f39dae1b005ff04157eafb27 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 23:21:51 +0200 Subject: [PATCH 064/119] Fix docstrings --- sources/options/options.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sources/options/options.py b/sources/options/options.py index 876f9a5..2d7c3dd 100644 --- a/sources/options/options.py +++ b/sources/options/options.py @@ -161,9 +161,9 @@ def validate(self, options: Union[GitOption, List[GitOption]]) -> NoReturn: :type options: Union[GitOption, List[GitOption]] :raises GitMissingDefinitionException: If option doesn't have definition. :raises GitIncorrectOptionValueException: If option has choices, then check that the value is present on the - choices list. + choices list. :raises GitIncorrectPositionalOptionDefinitionException: If there is a positional option of the list type that - is not defined on the last position. + is not defined on the last position. :raises GitMissingRequiredOptionsException: If not all required options are present. """ if isinstance(options, GitOption): @@ -189,8 +189,8 @@ def validate_positional_list(self) -> Tuple[bool, Optional[str]]: Method checks that there are no positional options of the list type that are defined not on the last position. :return: Tuple with boolean and optional string, boolean contains True if everything is correct and False if - there is an incorrect option. Optional string contains name of the definition which failed the check, where if - all positional options passed the check, then None will be returned. + there is an incorrect option. Optional string contains name of the definition which failed the check, where + if all positional options passed the check, then None will be returned. """ positions = [definition.position for definition in self.definitions if definition.positional] positions = sorted(positions) @@ -208,8 +208,8 @@ def validate_required(self, options: Union[GitOption, List[GitOption]]) -> Tuple :param options: List of provided options for the command. :type options: Union[GitOption, List[GitOption]] :return: Tuple with boolean and list of strings. Boolean is set as True, if all required options are present, - otherwise it is set as False. List of string contains name of the options that are required, but they are - missing. + otherwise it is set as False. List of string contains name of the options that are required, but they are + missing. """ required_definitions = [definition.name_aliases for definition in self.definitions if definition.required] for option in options: From 3f607867f87d4891597db35b61cf5416e49d8bab Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 23:37:29 +0200 Subject: [PATCH 065/119] Add a new class with definition of the 'git checkout' command --- sources/options/checkout_options.py | 44 +++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 sources/options/checkout_options.py diff --git a/sources/options/checkout_options.py b/sources/options/checkout_options.py new file mode 100644 index 0000000..3997346 --- /dev/null +++ b/sources/options/checkout_options.py @@ -0,0 +1,44 @@ +""" +Module contains classes that defines options that could be configured for 'git checkout' command. +Reference: https://git-scm.com/docs/git-checkout +""" +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ + GitOptionNameAlias + + +class CheckoutCommandDefinitions(GitCommand): + """ + Options definitions class for 'git checkout' command, it contains definitions of the options for 'git checkout'. + """ + + class Options(CommandOptions): + """ + Options class for 'git checkout' command, it contains options that can be configured. + """ + QUIET = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='quiet', short_option=False), + GitOptionNameAlias(name='q', short_option=True), + ]) + FORCE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='force', short_option=False), + GitOptionNameAlias(name='f', short_option=True), + ]) + BRANCH = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='branch', short_option=False), + ]) + NEW_BRANCH = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='b', short_option=True), + ]) + START_POINT = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='start-point', short_option=False), + ]) + + def __init__(self): + super().__init__('checkout') + self.definitions = [ + GitOptionDefinition(name_aliases=self.Options.QUIET, type=bool), + GitOptionDefinition(name_aliases=self.Options.FORCE, type=bool), + GitOptionDefinition(name_aliases=self.Options.BRANCH, type=str, positional=True, position=0), + GitOptionDefinition(name_aliases=self.Options.NEW_BRANCH, type=str), + GitOptionDefinition(name_aliases=self.Options.START_POINT, type=str, positional=True, position=0), + ] From 8a19a7f6885854fe05a61ffe98c5772e3a12db53 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 23:39:53 +0200 Subject: [PATCH 066/119] Remove autogenerated .rst files --- docs/source/modules.rst | 7 --- docs/source/sources.models.rst | 61 ------------------- docs/source/sources.options.rst | 101 -------------------------------- docs/source/sources.parsers.rst | 37 ------------ docs/source/sources.rst | 48 --------------- docs/source/sources.utils.rst | 21 ------- 6 files changed, 275 deletions(-) delete mode 100644 docs/source/modules.rst delete mode 100644 docs/source/sources.models.rst delete mode 100644 docs/source/sources.options.rst delete mode 100644 docs/source/sources.parsers.rst delete mode 100644 docs/source/sources.rst delete mode 100644 docs/source/sources.utils.rst diff --git a/docs/source/modules.rst b/docs/source/modules.rst deleted file mode 100644 index aa45929..0000000 --- a/docs/source/modules.rst +++ /dev/null @@ -1,7 +0,0 @@ -sources -======= - -.. toctree:: - :maxdepth: 4 - - sources diff --git a/docs/source/sources.models.rst b/docs/source/sources.models.rst deleted file mode 100644 index d2fa836..0000000 --- a/docs/source/sources.models.rst +++ /dev/null @@ -1,61 +0,0 @@ -sources.models package -====================== - -Submodules ----------- - -sources.models.base\_classes module ------------------------------------ - -.. automodule:: sources.models.base_classes - :members: - :undoc-members: - :show-inheritance: - -sources.models.branches module ------------------------------- - -.. automodule:: sources.models.branches - :members: - :undoc-members: - :show-inheritance: - -sources.models.commits module ------------------------------ - -.. automodule:: sources.models.commits - :members: - :undoc-members: - :show-inheritance: - -sources.models.remotes module ------------------------------ - -.. automodule:: sources.models.remotes - :members: - :undoc-members: - :show-inheritance: - -sources.models.repository\_information module ---------------------------------------------- - -.. automodule:: sources.models.repository_information - :members: - :undoc-members: - :show-inheritance: - -sources.models.tags module --------------------------- - -.. automodule:: sources.models.tags - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: sources.models - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/sources.options.rst b/docs/source/sources.options.rst deleted file mode 100644 index ee4f85c..0000000 --- a/docs/source/sources.options.rst +++ /dev/null @@ -1,101 +0,0 @@ -sources.options package -======================= - -Submodules ----------- - -sources.options.add\_options module ------------------------------------ - -.. automodule:: sources.options.add_options - :members: - :undoc-members: - :show-inheritance: - -sources.options.clone\_options module -------------------------------------- - -.. automodule:: sources.options.clone_options - :members: - :undoc-members: - :show-inheritance: - -sources.options.for\_each\_ref\_options module ----------------------------------------------- - -.. automodule:: sources.options.for_each_ref_options - :members: - :undoc-members: - :show-inheritance: - -sources.options.init\_options module ------------------------------------- - -.. automodule:: sources.options.init_options - :members: - :undoc-members: - :show-inheritance: - -sources.options.log\_options module ------------------------------------ - -.. automodule:: sources.options.log_options - :members: - :undoc-members: - :show-inheritance: - -sources.options.mv\_options module ----------------------------------- - -.. automodule:: sources.options.mv_options - :members: - :undoc-members: - :show-inheritance: - -sources.options.options module ------------------------------- - -.. automodule:: sources.options.options - :members: - :undoc-members: - :show-inheritance: - -sources.options.pull\_options module ------------------------------------- - -.. automodule:: sources.options.pull_options - :members: - :undoc-members: - :show-inheritance: - -sources.options.push\_options module ------------------------------------- - -.. automodule:: sources.options.push_options - :members: - :undoc-members: - :show-inheritance: - -sources.options.rm\_options module ----------------------------------- - -.. automodule:: sources.options.rm_options - :members: - :undoc-members: - :show-inheritance: - -sources.options.show\_options module ------------------------------------- - -.. automodule:: sources.options.show_options - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: sources.options - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/sources.parsers.rst b/docs/source/sources.parsers.rst deleted file mode 100644 index efbbc1b..0000000 --- a/docs/source/sources.parsers.rst +++ /dev/null @@ -1,37 +0,0 @@ -sources.parsers package -======================= - -Submodules ----------- - -sources.parsers.branches\_parser module ---------------------------------------- - -.. automodule:: sources.parsers.branches_parser - :members: - :undoc-members: - :show-inheritance: - -sources.parsers.commits\_parser module --------------------------------------- - -.. automodule:: sources.parsers.commits_parser - :members: - :undoc-members: - :show-inheritance: - -sources.parsers.tags\_parser module ------------------------------------ - -.. automodule:: sources.parsers.tags_parser - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: sources.parsers - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/sources.rst b/docs/source/sources.rst deleted file mode 100644 index 6c95671..0000000 --- a/docs/source/sources.rst +++ /dev/null @@ -1,48 +0,0 @@ -sources package -=============== - -Subpackages ------------ - -.. toctree:: - :maxdepth: 4 - - sources.models - sources.options - sources.parsers - sources.utils - -Submodules ----------- - -sources.command module ----------------------- - -.. automodule:: sources.command - :members: - :undoc-members: - :show-inheritance: - -sources.exceptions module -------------------------- - -.. automodule:: sources.exceptions - :members: - :undoc-members: - :show-inheritance: - -sources.git module ------------------- - -.. automodule:: sources.git - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: sources - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/sources.utils.rst b/docs/source/sources.utils.rst deleted file mode 100644 index 8d36c5a..0000000 --- a/docs/source/sources.utils.rst +++ /dev/null @@ -1,21 +0,0 @@ -sources.utils package -===================== - -Submodules ----------- - -sources.utils.path\_util module -------------------------------- - -.. automodule:: sources.utils.path_util - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: sources.utils - :members: - :undoc-members: - :show-inheritance: From 20f8a0fe5b9c93ca0f4634aa905666ca8c72c504 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 23:43:06 +0200 Subject: [PATCH 067/119] Add an exception class for 'git for-each-ref' command --- sources/exceptions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sources/exceptions.py b/sources/exceptions.py index f4030d0..660d9db 100644 --- a/sources/exceptions.py +++ b/sources/exceptions.py @@ -109,3 +109,9 @@ class GitCheckoutException(GitCommandException): """ Exception thrown when 'checkout' operation has failed. """ + + +class GitForEachRefException(GitCommandException): + """ + Exception thrown when 'for-each-ref' operation has failed. + """ From ecf76a551a7b4e4d39d256a95634899178fac4d6 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 23:44:19 +0200 Subject: [PATCH 068/119] Add an exception class for 'git log' command --- sources/exceptions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sources/exceptions.py b/sources/exceptions.py index 660d9db..97ab25f 100644 --- a/sources/exceptions.py +++ b/sources/exceptions.py @@ -115,3 +115,9 @@ class GitForEachRefException(GitCommandException): """ Exception thrown when 'for-each-ref' operation has failed. """ + + +class GitLogException(GitCommandException): + """ + Exception thrown when 'log' operation has failed. + """ From fd4f29add5d7579c26375a579d93acae9522f4c3 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 23:45:39 +0200 Subject: [PATCH 069/119] Add command wrappers for basic operations --- sources/command.py | 185 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 182 insertions(+), 3 deletions(-) diff --git a/sources/command.py b/sources/command.py index 8133be2..ddc9627 100644 --- a/sources/command.py +++ b/sources/command.py @@ -6,8 +6,21 @@ from subprocess import PIPE, Popen from typing import Union, List, Optional, Type -from sources.exceptions import GitCommandException +from sources.exceptions import GitCommandException, GitException, GitPushException, GitPullException, GitRmException, \ + GitMvException, GitAddException, GitCloneException, GitInitException, GitShowException, GitConfigException, \ + GitCheckoutException, GitForEachRefException +from sources.options.add_options import AddCommandDefinitions +from sources.options.checkout_options import CheckoutCommandDefinitions +from sources.options.clone_options import CloneCommandDefinitions +from sources.options.config_options import ConfigCommandDefinitions +from sources.options.for_each_ref_options import ForEachRefCommandDefinitions +from sources.options.init_options import InitCommandDefinitions +from sources.options.mv_options import MvCommandDefinitions from sources.options.options import GitCommand, GitOption +from sources.options.pull_options import PullCommandDefinitions +from sources.options.push_options import PushCommandDefinitions +from sources.options.rm_options import RmCommandDefinitions +from sources.options.show_options import ShowCommandDefinitions class GitCommandRunner: @@ -50,7 +63,7 @@ def __generate_command(self, command: List[Union[str, int]]) -> List[Union[str, return command def execute(self, commands: List[Union[str, int, GitOption, None]], - definitions_class: Optional[Type[GitCommand]] = None, log_output: bool = False): + definitions_class: Optional[Type[GitCommand]] = None, log_output: bool = False) -> str: """ Method executes git command with options provided as an input. It validates options based on the provided definition. If definitions class has not been provided, then no validation happens and standard list of commands @@ -59,7 +72,7 @@ def execute(self, commands: List[Union[str, int, GitOption, None]], :param commands: List of options for the git command. :type commands: List[Union[str, int, GitOption, None]] :param definitions_class: Definition class that describes options for the git command, shall be derived from - GitCommand class. + GitCommand class. :type definitions_class: Optional[Type[GitCommand]] :param log_output: If this option is True, then stdout of the command will be logged, :type log_output: bool @@ -83,3 +96,169 @@ def execute(self, commands: List[Union[str, int, GitOption, None]], if process.returncode != 0: raise GitCommandException(stderr) return stdout + + def __execute_git_command(self, options: List[GitOption], definition_class: Type[GitCommand], + exception_class: Type[GitCommandException], log_output: bool = False): + """ + Execute git command that takes GitOption, it shall has command definitions class, and exception class. + Optionally method can log stdout in the runtime, if the 'log_output' was set as True. As result of the + successful execution stdout will be returned. + + :param options: List of git options for the command. + :type options: List[GitOption] + :param definition_class: Class that describes git command and its options. + :type definition_class: Type[GitCommand] + :param exception_class: Exception that will be raised if git command failed (returned non-zero exit code). + :type exception_class: Type[GitCommandException] + :param log_output: Select whether it shall log stdout in the runtime. + :type log_output: bool + :return: Stdout returned by the command. + """ + try: + return self.execute(commands=options, definitions_class=definition_class, log_output=log_output) + except GitException as exception: + raise exception_class(exception.args[0]) from None + + def init(self, *options: GitOption, log_output: bool = False) -> str: + """ + Execute 'git init' command with provided options and return stdout of the command. Optionally it can log stdout + in the runtime, if 'log_output' option has been set as True. + + :param options: Options for the 'git init' command. + :type options: Tuple[GitOption] + :param log_output: Set as True, if it shall log stdout of the command in the runtime, otherwise False. + :type log_output: bool + :return: Stdout returned by 'git init' command. + """ + return self.__execute_git_command(list(options), InitCommandDefinitions, GitInitException, log_output) + + def clone(self, *options: GitOption, log_output: bool = False) -> str: + """ + Execute 'git clone' command with provided options and return stdout of the command. Optionally it can log stdout + in the runtime, if 'log_output' option has been set as True. + + :param options: Options for the 'git clone' command. + :type options: Tuple[GitOption] + :param log_output: Set as True, if it shall log stdout of the command in the runtime, otherwise False. + :type log_output: bool + :return: Stdout returned by 'git clone' command. + """ + return self.__execute_git_command(list(options), CloneCommandDefinitions, GitCloneException, log_output) + + def add(self, *options: GitOption, log_output: bool = False) -> str: + """ + Execute 'git add' command with provided options and return stdout of the command. Optionally it can log stdout + in the runtime, if 'log_output' option has been set as True. + + :param options: Options for the 'git add' command. + :type options: Tuple[GitOption] + :param log_output: Set as True, if it shall log stdout of the command in the runtime, otherwise False. + :type log_output: bool + :return: Stdout returned by 'git add' command. + """ + return self.__execute_git_command(list(options), AddCommandDefinitions, GitAddException, log_output) + + def mv(self, *options: GitOption, log_output: bool = False) -> str: + """ + Execute 'git mv' command with provided options and return stdout of the command. Optionally it can log stdout + in the runtime, if 'log_output' option has been set as True. + + :param options: Options for the 'git mv' command. + :type options: Tuple[GitOption] + :param log_output: Set as True, if it shall log stdout of the command in the runtime, otherwise False. + :type log_output: bool + :return: Stdout returned by 'git mv' command. + """ + return self.__execute_git_command(list(options), MvCommandDefinitions, GitMvException, log_output) + + def rm(self, *options: GitOption, log_output: bool = False) -> str: + """ + Execute 'git rm' command with provided options and return stdout of the command. Optionally it can log stdout + in the runtime, if 'log_output' option has been set as True. + + :param options: Options for the 'git rm' command. + :type options: Tuple[GitOption] + :param log_output: Set as True, if it shall log stdout of the command in the runtime, otherwise False. + :type log_output: bool + :return: Stdout returned by 'git rm' command. + """ + return self.__execute_git_command(list(options), RmCommandDefinitions, GitRmException, log_output) + + def pull(self, *options: GitOption, log_output: bool = False) -> str: + """ + Execute 'git pull' command with provided options and return stdout of the command. Optionally it can log stdout + in the runtime, if 'log_output' option has been set as True. + + :param options: Options for the 'git pull' command. + :type options: Tuple[GitOption] + :param log_output: Set as True, if it shall log stdout of the command in the runtime, otherwise False. + :type log_output: bool + :return: Stdout returned by 'git pull' command. + """ + return self.__execute_git_command(list(options), PullCommandDefinitions, GitPullException, log_output) + + def push(self, *options: GitOption, log_output: bool = False) -> str: + """ + Execute 'git push' command with provided options and return stdout of the command. Optionally it can log stdout + in the runtime, if 'log_output' option has been set as True. + + :param options: Options for the 'git push' command. + :type options: Tuple[GitOption] + :param log_output: Set as True, if it shall log stdout of the command in the runtime, otherwise False. + :type log_output: bool + :return: Stdout returned by 'git push' command. + """ + return self.__execute_git_command(list(options), PushCommandDefinitions, GitPushException, log_output) + + def show(self, *options: GitOption, log_output: bool = False) -> str: + """ + Execute 'git show' command with provided options and return stdout of the command. Optionally it can log stdout + in the runtime, if 'log_output' option has been set as True. + + :param options: Options for the 'git show' command. + :type options: Tuple[GitOption] + :param log_output: Set as True, if it shall log stdout of the command in the runtime, otherwise False. + :type log_output: bool + :return: Stdout returned by 'git show' command. + """ + return self.__execute_git_command(list(options), ShowCommandDefinitions, GitShowException, log_output) + + def config(self, *options: GitOption, log_output: bool = False) -> str: + """ + Execute 'git config' command with provided options and return stdout of the command. Optionally it can log + stdout in the runtime, if 'log_output' option has been set as True. + + :param options: Options for the 'git config' command. + :type options: Tuple[GitOption] + :param log_output: Set as True, if it shall log stdout of the command in the runtime, otherwise False. + :type log_output: bool + :return: Stdout returned by 'git config' command. + """ + return self.__execute_git_command(list(options), ConfigCommandDefinitions, GitConfigException, log_output) + + def checkout(self, *options: GitOption, log_output: bool = False) -> str: + """ + Execute 'git checkout' command with provided options and return stdout of the command. Optionally it can log + stdout in the runtime, if 'log_output' option has been set as True. + + :param options: Options for the 'git checkout' command. + :type options: Tuple[GitOption] + :param log_output: Set as True, if it shall log stdout of the command in the runtime, otherwise False. + :type log_output: bool + :return: Stdout returned by 'git checkout' command. + """ + return self.__execute_git_command(list(options), CheckoutCommandDefinitions, GitCheckoutException, log_output) + + def for_each_ref(self, *options: GitOption, log_output: bool = False): + """ + Execute 'git for-each-ref' command with provided options and return stdout of the command. Optionally it can log + stdout in the runtime, if 'log_output' option has been set as True. + + :param options: Options for the 'git for-each-ref' command. + :type options: Tuple[GitOption] + :param log_output: Set as True, if it shall log stdout of the command in the runtime, otherwise False. + :type log_output: bool + :return: Stdout returned by 'git for-each-ref' command. + """ + return self.__execute_git_command(list(options), ForEachRefCommandDefinitions, GitForEachRefException, + log_output) From c8f18afa87e921f64a8276291d6389364c203472 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 23:47:43 +0200 Subject: [PATCH 070/119] Add command wrapper for 'git log' --- sources/command.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/sources/command.py b/sources/command.py index ddc9627..9acda2f 100644 --- a/sources/command.py +++ b/sources/command.py @@ -8,13 +8,14 @@ from sources.exceptions import GitCommandException, GitException, GitPushException, GitPullException, GitRmException, \ GitMvException, GitAddException, GitCloneException, GitInitException, GitShowException, GitConfigException, \ - GitCheckoutException, GitForEachRefException + GitCheckoutException, GitForEachRefException, GitLogException from sources.options.add_options import AddCommandDefinitions from sources.options.checkout_options import CheckoutCommandDefinitions from sources.options.clone_options import CloneCommandDefinitions from sources.options.config_options import ConfigCommandDefinitions from sources.options.for_each_ref_options import ForEachRefCommandDefinitions from sources.options.init_options import InitCommandDefinitions +from sources.options.log_options import LogCommandDefinitions from sources.options.mv_options import MvCommandDefinitions from sources.options.options import GitCommand, GitOption from sources.options.pull_options import PullCommandDefinitions @@ -262,3 +263,16 @@ def for_each_ref(self, *options: GitOption, log_output: bool = False): """ return self.__execute_git_command(list(options), ForEachRefCommandDefinitions, GitForEachRefException, log_output) + + def log(self, *options: GitOption, log_output: bool = False): + """ + Execute 'git log' command with provided options and return stdout of the command. Optionally it can log + stdout in the runtime, if 'log_output' option has been set as True. + + :param options: Options for the 'git log' command. + :type options: Tuple[GitOption] + :param log_output: Set as True, if it shall log stdout of the command in the runtime, otherwise False. + :type log_output: bool + :return: Stdout returned by 'git log' command. + """ + return self.__execute_git_command(list(options), LogCommandDefinitions, GitLogException, log_output) From ffd898a8972a0bc82e41768d6cad6bb019d54126 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 23:49:10 +0200 Subject: [PATCH 071/119] Refactor GitRepository standard methods to use new command wrappers --- sources/git.py | 87 ++++++++++++++++++++------------------------------ 1 file changed, 34 insertions(+), 53 deletions(-) diff --git a/sources/git.py b/sources/git.py index 78c71a6..014e321 100644 --- a/sources/git.py +++ b/sources/git.py @@ -160,7 +160,7 @@ def __read_content(content: List[str]) -> List[str]: Parse content lines of the '.gitignore' file and return exclude patterns. :param content: Content of the file in the list format (each line is represented as the one element of the - list). + list). :type content: List[str] :return: List of exclude patterns. """ @@ -227,7 +227,7 @@ def files_status(self): are classified as added, modified, or removed. :return: Dictionary with changes classified as added, modified, and removed, additionally contains excluded - files. + files. """ self.__update_files_hash(self.__repository.path.absolute(), self.__files_hashes[self.END]) result = { @@ -496,11 +496,11 @@ def __get_default_author(self): user_name_options = [ ConfigCommandDefinitions.Options.NAME.create_option('user.name') ] - name = self.__git_command.execute(user_name_options, ConfigCommandDefinitions).strip() + name = self.__git_command.config(*user_name_options).strip() user_email_options = [ ConfigCommandDefinitions.Options.NAME.create_option('user.email') ] - email = self.__git_command.execute(user_email_options, ConfigCommandDefinitions).strip() + email = self.__git_command.config(*user_email_options).strip() return Author(name=name, email=email) @staticmethod @@ -508,7 +508,7 @@ def __read_git_ignore(path: Path, git_command: GitCommandRunner) -> Optional[Git gitignore = None if path.exists(): options = [ShowCommandDefinitions.Options.OBJECTS.create_option([f'HEAD:{path.name}'])] - commit_content = git_command.execute(options, ShowCommandDefinitions) + commit_content = git_command.show(*options) with path.open('r') as file: content = file.read() if content != commit_content: @@ -542,7 +542,7 @@ def init(cls, path: Union[str, Path], *options: GitOption): options = list(options) options.append(InitCommandDefinitions.Options.DIRECTORY.create_option(str(path.absolute()))) git_command = GitCommandRunner() - git_command.execute(options, InitCommandDefinitions) + git_command.init(*options) return cls(path) @classmethod @@ -556,7 +556,7 @@ def clone(cls, repository: Union[str, Remote], path: Union[str, Path] = None, *o if path is not None: options.append(CloneCommandDefinitions.Options.DIRECTORY.create_option(str(path.absolute()))) git_command = GitCommandRunner() - git_command.execute(options, CloneCommandDefinitions) + git_command.clone(*options) return cls(path) def add(self, files: Union[str, Path, List[Union[str, Path]]], *options: GitOption): @@ -566,15 +566,11 @@ def add(self, files: Union[str, Path, List[Union[str, Path]]], *options: GitOpti for file_path in files: if isinstance(file_path, Path): file_path = str(file_path.absolute()) - if not isinstance(file_path, list): - file_path = [file_path] + file_path = [file_path] options = list(options) options.append(AddCommandDefinitions.Options.PATHSPEC.create_option(file_path)) - try: - output = self.__git_command.execute(options, AddCommandDefinitions) - outputs.append(output.strip()) - except GitException as exception: - raise GitAddException(exception.args[0]) from None + output = self.__git_command.add(*options) + outputs.append(output.strip()) return '\n'.join(outputs) def mv(self, mappings: Union[PathsMapping, List[PathsMapping]], *options: GitOption): @@ -582,38 +578,31 @@ def mv(self, mappings: Union[PathsMapping, List[PathsMapping]], *options: GitOpt if isinstance(mappings, PathsMapping): mappings = [mappings] for mapping in mappings: - try: - mapping.root_path = self.__repository_information.path - options = list(options) - options.append(MvCommandDefinitions.Options.SOURCE.create_option(str(mapping.source.absolute()))) - options.append( - MvCommandDefinitions.Options.DESTINATION.create_option(str(mapping.destination.absolute()))) - output = self.__git_command.execute(options, MvCommandDefinitions) - outputs.append(output.strip()) - except GitException as exception: - raise GitMvException(exception.args[0]) from None + mapping.root_path = self.__repository_information.path + options = list(options) + options.append(MvCommandDefinitions.Options.SOURCE.create_option(str(mapping.source.absolute()))) + options.append( + MvCommandDefinitions.Options.DESTINATION.create_option(str(mapping.destination.absolute()))) + output = self.__git_command.mv(*options) + outputs.append(output.strip()) return '\n'.join(outputs) def rm(self, files: Union[str, Path, List[Union[str, Path]]], *options: GitOption): outputs = [] if isinstance(files, (str, Path)): files = [files] + raw_repository_path = str(self.__repository_information.path.absolute()) + options = list(options) for file_path in files: - raw_repository_path = str(self.__repository_information.path.absolute()) if isinstance(file_path, str) and file_path.startswith(raw_repository_path): file_path = Path(file_path) else: file_path = self.__repository_information.path.joinpath(file_path) - file_path = str(file_path.absolute()) - if not isinstance(file_path, list): - file_path = [file_path] - options = list(options) - options.append(RmCommandDefinitions.Options.PATHSPEC.create_option(file_path)) - try: - output = self.__git_command.execute(options, RmCommandDefinitions) - outputs.append(output.strip()) - except GitException as exception: - raise GitRmException(exception.args[0]) from None + file_path = [str(file_path.absolute())] + command_options = options.copy() + command_options.append(RmCommandDefinitions.Options.PATHSPEC.create_option(file_path)) + output = self.__git_command.rm(*command_options) + outputs.append(output.strip()) return '\n'.join(outputs) @staticmethod @@ -628,24 +617,16 @@ def __get_refspec(reference: Optional[Union[Reference, Refspec]]): def pull(self, remote: Remote, reference: Optional[Union[Reference, Refspec]] = None, *options: GitOption): refspec = self.__get_refspec(reference) - try: - options = list(options) - options.append(PullCommandDefinitions.Options.REPOSITORY.create_option(remote.name)) - if refspec: - options.append(PullCommandDefinitions.Options.REFSPEC.create_option(refspec)) - output = self.__git_command.execute(options, PullCommandDefinitions) - return output - except GitException as exception: - raise GitPullException(exception.args[0]) from None + options = list(options) + options.append(PullCommandDefinitions.Options.REPOSITORY.create_option(remote.name)) + if refspec: + options.append(PullCommandDefinitions.Options.REFSPEC.create_option(refspec)) + return self.__git_command.pull(*options) def push(self, remote: Remote, reference: Optional[Union[Reference, Refspec]] = None, *options: GitOption): refspec = self.__get_refspec(reference) - try: - options = list(options) - options.append(PushCommandDefinitions.Options.REPOSITORY.create_option(remote.name)) - if refspec: - options.append(PushCommandDefinitions.Options.REFSPEC.create_option(refspec)) - output = self.__git_command.execute(options, PushCommandDefinitions) - return output - except GitException as exception: - raise GitPushException(exception.args[0]) from None + options = list(options) + options.append(PushCommandDefinitions.Options.REPOSITORY.create_option(remote.name)) + if refspec: + options.append(PushCommandDefinitions.Options.REFSPEC.create_option(refspec)) + return self.__git_command.push(*options) From 3b4401b588309c86b9670e6993c1989603d9f66b Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 23:50:03 +0200 Subject: [PATCH 072/119] Remove unused import --- sources/models/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/models/tags.py b/sources/models/tags.py index e9fdbe8..30e0ca8 100644 --- a/sources/models/tags.py +++ b/sources/models/tags.py @@ -2,7 +2,7 @@ Module contains models for git tags. """ from dataclasses import dataclass -from typing import List, ClassVar, Union, Optional +from typing import List, Union, Optional from sources.models.base_classes import Author, Reference from sources.models.commits import Commit From ede17bae7590a3029fc7f1572856de2f89f7d7a6 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sun, 1 Oct 2023 23:59:32 +0200 Subject: [PATCH 073/119] Update commits parser to use log command wrapper --- sources/parsers/commits_parser.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sources/parsers/commits_parser.py b/sources/parsers/commits_parser.py index 453ec26..af5f0a8 100644 --- a/sources/parsers/commits_parser.py +++ b/sources/parsers/commits_parser.py @@ -90,8 +90,10 @@ def get_commits(self, reference: Optional[Reference] = None) -> Commits: options.append(LogCommandDefinitions.Options.PRETTY.create_option(f'format:{self.FORMAT_RAW}')) options.append(LogCommandDefinitions.Options.DATE.create_option(f'format:{self.DATE_FORMAT}')) commit_attributes = len(self.FORMAT) - output = self.__git_command.execute(options, LogCommandDefinitions) - lines = output.strip().split('\n') + output = self.__git_command.log(*options).strip() + if not output: + return Commits() + lines = output.split('\n') raw_commits = [lines[row_index:row_index + commit_attributes] for row_index in range(len(lines) - commit_attributes, -1, commit_attributes * -1)] return self.parse_commits(raw_commits) From 6b8ac407d6668bf40cba39e6010395e9096d6859 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Mon, 2 Oct 2023 00:03:30 +0200 Subject: [PATCH 074/119] Update tags parser to use for-each-ref command wrapper --- sources/parsers/tags_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/parsers/tags_parser.py b/sources/parsers/tags_parser.py index 89c2497..9606368 100644 --- a/sources/parsers/tags_parser.py +++ b/sources/parsers/tags_parser.py @@ -90,7 +90,7 @@ def get_tags(self) -> List[Union[LightweightTag, AnnotatedTag]]: ForEachRefCommandDefinitions.Options.FORMAT.create_option(self.RAW_FORMAT), ForEachRefCommandDefinitions.Options.PATTERN.create_option(self.PATTERN), ] - output = self.__git_command.execute(options, ForEachRefCommandDefinitions).strip() + output = self.__git_command.for_each_ref(*options).strip() if not output: return [] lines = output.split('\n') From 2c8880123ebff127928af3dbd5b57d32b50ddbe3 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Mon, 2 Oct 2023 00:24:04 +0200 Subject: [PATCH 075/119] Extend checkout functionality to create a new branch --- sources/git.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/sources/git.py b/sources/git.py index 014e321..0c9a186 100644 --- a/sources/git.py +++ b/sources/git.py @@ -21,6 +21,7 @@ from sources.models.repository_information import GitRepositoryPaths from sources.models.tags import Tags from sources.options.add_options import AddCommandDefinitions +from sources.options.checkout_options import CheckoutCommandDefinitions from sources.options.clone_options import CloneCommandDefinitions from sources.options.config_options import ConfigCommandDefinitions from sources.options.init_options import InitCommandDefinitions @@ -120,6 +121,7 @@ class GitIgnore: """ Class for manipulations with .gitignore exclude patterns list. """ + def __init__(self, path: Union[str, Path]): self.__path = PathUtil.convert_to_path(path) if not self.__path.exists(): @@ -332,11 +334,12 @@ def __get_excluded(files: Dict[str, str], excludes: List[str]) -> List[str]: class CheckoutHandler: __new_branch: str - def __init__(self, new_branch: str, repository: 'GitRepository', old_branch: Optional[str] = None): + def __init__(self, new_branch: str, repository: 'GitRepository', old_branch: Optional[str] = None, + create_if_not_exist: bool = False): self.__new_branch = new_branch self.__old_branch = old_branch self.__repository = repository - self.checkout(self.__new_branch) + self.checkout(self.__new_branch, create_if_not_exist) def __enter__(self): return self @@ -344,11 +347,18 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): self.checkout(self.__old_branch) - def checkout(self, branch: Optional[str] = None): + def checkout(self, branch: Optional[str] = None, create_if_not_exist: bool = False): if branch is None: branch = self.__new_branch logging.info('Switching to "%s" branch.', branch) - self.__repository.git_command.execute(['checkout', branch]) + branch_exist = self.__repository.branches[branch] is not None + options = [] + if not branch_exist and create_if_not_exist: + logging.info('Creating a new branch "%s"', branch) + options.append(CheckoutCommandDefinitions.Options.NEW_BRANCH.create_option(branch)) + else: + options.append(CheckoutCommandDefinitions.Options.BRANCH.create_option(branch)) + self.__repository.git_command.checkout(*options) self.__repository.refresh_repository(refresh_active_branch=True, refresh_commits=True) @@ -520,8 +530,8 @@ def __read_git_ignore(path: Path, git_command: GitCommandRunner) -> Optional[Git def __read_remotes(self): return Remotes(self.__git_config.remotes) - def checkout(self, branch: str): - return CheckoutHandler(branch, self, self.__active_branch.name) + def checkout(self, branch: str, create_if_not_exist: bool = False): + return CheckoutHandler(branch, self, self.__active_branch.name, create_if_not_exist) def create_commit(self, message: str, author: Optional[Author] = None, date: Optional[datetime] = None, commit_hash: str = None, parent: Union[str, Commit] = None): From 3302965e24e5ccb035b7f46dbb1b56fa2e4ee5dd Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Tue, 3 Oct 2023 09:24:10 +0200 Subject: [PATCH 076/119] Extend git command to support 'commit' command --- sources/command.py | 16 ++++++++- sources/exceptions.py | 6 ++++ sources/options/commit_options.py | 58 +++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 sources/options/commit_options.py diff --git a/sources/command.py b/sources/command.py index 9acda2f..23e30c0 100644 --- a/sources/command.py +++ b/sources/command.py @@ -8,10 +8,11 @@ from sources.exceptions import GitCommandException, GitException, GitPushException, GitPullException, GitRmException, \ GitMvException, GitAddException, GitCloneException, GitInitException, GitShowException, GitConfigException, \ - GitCheckoutException, GitForEachRefException, GitLogException + GitCheckoutException, GitForEachRefException, GitLogException, GitCommitException from sources.options.add_options import AddCommandDefinitions from sources.options.checkout_options import CheckoutCommandDefinitions from sources.options.clone_options import CloneCommandDefinitions +from sources.options.commit_options import CommitCommandDefinitions from sources.options.config_options import ConfigCommandDefinitions from sources.options.for_each_ref_options import ForEachRefCommandDefinitions from sources.options.init_options import InitCommandDefinitions @@ -276,3 +277,16 @@ def log(self, *options: GitOption, log_output: bool = False): :return: Stdout returned by 'git log' command. """ return self.__execute_git_command(list(options), LogCommandDefinitions, GitLogException, log_output) + + def commit(self, *options: GitOption, log_output: bool = False): + """ + Execute 'git commit' command with provided options and return stdout of the command. Optionally it can log + stdout in the runtime, if 'log_output' option has been set as True. + + :param options: Options for the 'git commit' command. + :type options: Tuple[GitOption] + :param log_output: Set as True, if it shall log stdout of the command in the runtime, otherwise False. + :type log_output: bool + :return: Stdout returned by 'git commit' command. + """ + return self.__execute_git_command(list(options), CommitCommandDefinitions, GitCommitException, log_output) diff --git a/sources/exceptions.py b/sources/exceptions.py index 97ab25f..56b7799 100644 --- a/sources/exceptions.py +++ b/sources/exceptions.py @@ -121,3 +121,9 @@ class GitLogException(GitCommandException): """ Exception thrown when 'log' operation has failed. """ + + +class GitCommitException(GitCommandException): + """ + Exception thrown when 'commit' operation has failed. + """ diff --git a/sources/options/commit_options.py b/sources/options/commit_options.py new file mode 100644 index 0000000..7303f2c --- /dev/null +++ b/sources/options/commit_options.py @@ -0,0 +1,58 @@ +""" +Module contains classes that defines options that could be configured for 'git commit' command. +Reference: https://git-scm.com/docs/git-commit +""" +from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ + GitOptionNameAlias + + +class CommitCommandDefinitions(GitCommand): + """ + Options definitions class for 'git commit' command, it contains definitions of the options for 'git commit'. + """ + class Options(CommandOptions): + """ + Options class for 'git commit' command, it contains options that can be configured. + """ + VERBOSE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='verbose', short_option=False), + GitOptionNameAlias(name='v', short_option=True), + ]) + QUIET = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='quiet', short_option=False), + GitOptionNameAlias(name='q', short_option=True), + ]) + ALL = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='all', short_option=False), + GitOptionNameAlias(name='a', short_option=True), + ]) + PATCH = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='patch', short_option=False), + GitOptionNameAlias(name='p', short_option=True), + ]) + MESSAGE = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='message', short_option=False), + GitOptionNameAlias(name='m', short_option=True), + ]) + AMEND = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='amend', short_option=False), + ]) + NO_EDIT = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='no-edit', short_option=False), + ]) + PATHSPEC = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='pathspec', short_option=False), + ]) + + def __init__(self): + super().__init__('commit') + self.definitions = [ + GitOptionDefinition(name_aliases=self.Options.VERBOSE, type=bool), + GitOptionDefinition(name_aliases=self.Options.QUIET, type=bool), + GitOptionDefinition(name_aliases=self.Options.ALL, type=bool), + GitOptionDefinition(name_aliases=self.Options.PATCH, type=bool), + GitOptionDefinition(name_aliases=self.Options.MESSAGE, type=str), + GitOptionDefinition(name_aliases=self.Options.AMEND, type=bool), + GitOptionDefinition(name_aliases=self.Options.NO_EDIT, type=bool), + GitOptionDefinition(name_aliases=self.Options.PATHSPEC, type=str, positional=True, position=0), + ] From 6ebc8ea42929d7673f97ecdfd049768cb3f03237 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Tue, 3 Oct 2023 09:35:48 +0200 Subject: [PATCH 077/119] Improve module reference readability --- sources/options/add_options.py | 1 + sources/options/checkout_options.py | 1 + sources/options/clone_options.py | 1 + sources/options/commit_options.py | 1 + sources/options/config_options.py | 1 + sources/options/for_each_ref_options.py | 1 + sources/options/init_options.py | 1 + sources/options/log_options.py | 1 + sources/options/mv_options.py | 1 + sources/options/pull_options.py | 1 + sources/options/push_options.py | 1 + sources/options/rm_options.py | 1 + sources/options/show_options.py | 1 + 13 files changed, 13 insertions(+) diff --git a/sources/options/add_options.py b/sources/options/add_options.py index a158ca6..5ba7fc9 100644 --- a/sources/options/add_options.py +++ b/sources/options/add_options.py @@ -1,5 +1,6 @@ """ Module contains classes that defines options that could be configured for 'git add' command. + Reference: https://git-scm.com/docs/git-add """ from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ diff --git a/sources/options/checkout_options.py b/sources/options/checkout_options.py index 3997346..b334fba 100644 --- a/sources/options/checkout_options.py +++ b/sources/options/checkout_options.py @@ -1,5 +1,6 @@ """ Module contains classes that defines options that could be configured for 'git checkout' command. + Reference: https://git-scm.com/docs/git-checkout """ from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ diff --git a/sources/options/clone_options.py b/sources/options/clone_options.py index 717384d..07c56ad 100644 --- a/sources/options/clone_options.py +++ b/sources/options/clone_options.py @@ -1,5 +1,6 @@ """ Module contains classes that defines options that could be configured for 'git clone' command. + Reference: https://git-scm.com/docs/git-clone """ from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAlias, \ diff --git a/sources/options/commit_options.py b/sources/options/commit_options.py index 7303f2c..cd684ab 100644 --- a/sources/options/commit_options.py +++ b/sources/options/commit_options.py @@ -1,5 +1,6 @@ """ Module contains classes that defines options that could be configured for 'git commit' command. + Reference: https://git-scm.com/docs/git-commit """ from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ diff --git a/sources/options/config_options.py b/sources/options/config_options.py index 9808824..749e125 100644 --- a/sources/options/config_options.py +++ b/sources/options/config_options.py @@ -1,5 +1,6 @@ """ Module contains classes that defines options that could be configured for 'git config' command. + Reference: https://git-scm.com/docs/git-config """ from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ diff --git a/sources/options/for_each_ref_options.py b/sources/options/for_each_ref_options.py index 351761d..8379512 100644 --- a/sources/options/for_each_ref_options.py +++ b/sources/options/for_each_ref_options.py @@ -1,5 +1,6 @@ """ Module contains classes that defines options that could be configured for 'git for-each-ref' command. + Reference: https://git-scm.com/docs/git-for-each-ref """ from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ diff --git a/sources/options/init_options.py b/sources/options/init_options.py index 1087c81..7329987 100644 --- a/sources/options/init_options.py +++ b/sources/options/init_options.py @@ -1,5 +1,6 @@ """ Module contains classes that defines options that could be configured for 'git init' command. + Reference: https://git-scm.com/docs/git-init """ from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ diff --git a/sources/options/log_options.py b/sources/options/log_options.py index f113982..2166bd8 100644 --- a/sources/options/log_options.py +++ b/sources/options/log_options.py @@ -1,5 +1,6 @@ """ Module contains classes that defines options that could be configured for 'git log' command. + Reference: https://git-scm.com/docs/git-log """ from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ diff --git a/sources/options/mv_options.py b/sources/options/mv_options.py index 2da29e0..7fcdaee 100644 --- a/sources/options/mv_options.py +++ b/sources/options/mv_options.py @@ -1,5 +1,6 @@ """ Module contains classes that defines options that could be configured for 'git mv' command. + Reference: https://git-scm.com/docs/git-mv """ from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ diff --git a/sources/options/pull_options.py b/sources/options/pull_options.py index 1255a4a..87a35d4 100644 --- a/sources/options/pull_options.py +++ b/sources/options/pull_options.py @@ -1,5 +1,6 @@ """ Module contains classes that defines options that could be configured for 'git pull' command. + Reference: https://git-scm.com/docs/git-pull """ from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ diff --git a/sources/options/push_options.py b/sources/options/push_options.py index 267adfc..a850931 100644 --- a/sources/options/push_options.py +++ b/sources/options/push_options.py @@ -1,5 +1,6 @@ """ Module contains classes that defines options that could be configured for 'git push' command. + Reference: https://git-scm.com/docs/git-push """ from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ diff --git a/sources/options/rm_options.py b/sources/options/rm_options.py index 4be9d52..83f5760 100644 --- a/sources/options/rm_options.py +++ b/sources/options/rm_options.py @@ -1,5 +1,6 @@ """ Module contains classes that defines options that could be configured for 'git rm' command. + Reference: https://git-scm.com/docs/git-rm """ from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ diff --git a/sources/options/show_options.py b/sources/options/show_options.py index 53d77a8..71db93c 100644 --- a/sources/options/show_options.py +++ b/sources/options/show_options.py @@ -1,5 +1,6 @@ """ Module contains classes that defines options that could be configured for 'git show' command. + Reference: https://git-scm.com/docs/git-show """ from sources.options.options import GitCommand, GitOptionDefinition, CommandOptions, GitOptionNameAliases, \ From df7e2e57fe478bd5cc811f8a7c8f4a10c02b4751 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Wed, 4 Oct 2023 21:28:13 +0200 Subject: [PATCH 078/119] Update commit method used in tests --- tests/acceptance_tests/features/steps/basic_operations.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/acceptance_tests/features/steps/basic_operations.py b/tests/acceptance_tests/features/steps/basic_operations.py index 86abe5f..003cd43 100644 --- a/tests/acceptance_tests/features/steps/basic_operations.py +++ b/tests/acceptance_tests/features/steps/basic_operations.py @@ -8,6 +8,7 @@ from sources.exceptions import GitException from sources.git import GitRepository, PathsMapping +from sources.options.commit_options import CommitCommandDefinitions @given("new local repository path") @@ -91,7 +92,10 @@ def step_impl(context: Context): @given("commit changes") def step_impl(context: Context): repository: GitRepository = context.repository - repository.git_command.execute(['commit', '-m', 'Add new file']) + options = [ + CommitCommandDefinitions.Options.MESSAGE.create_option('Add new file') + ] + repository.git_command.commit(*options) @then("new file will be added to the git tracking") From c892a45c987f2f74de198eb67abf6031d094cff7 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Wed, 4 Oct 2023 21:47:01 +0200 Subject: [PATCH 079/119] Add docstrings and typing to CheckoutHandler --- sources/git.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/sources/git.py b/sources/git.py index 0c9a186..13c2efc 100644 --- a/sources/git.py +++ b/sources/git.py @@ -332,6 +332,10 @@ def __get_excluded(files: Dict[str, str], excludes: List[str]) -> List[str]: class CheckoutHandler: + """ + Class handles checkout to another branch in the context manager. Allowing to switch on the another branch in the + context and then reset it back to the original branch. + """ __new_branch: str def __init__(self, new_branch: str, repository: 'GitRepository', old_branch: Optional[str] = None, @@ -347,7 +351,16 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): self.checkout(self.__old_branch) - def checkout(self, branch: Optional[str] = None, create_if_not_exist: bool = False): + def checkout(self, branch: Optional[str] = None, create_if_not_exist: bool = False) -> NoReturn: + """ + Method checkouts another branch and refresh active branch with commits list. Optionally it can create branch + if it doesn't exist. + + :param branch: A branch on which it will switch. + :type branch: Optional[str] + :param create_if_not_exist: Handles whether it shall create a new branch, if branch doesn't exist. + :type create_if_not_exist: bool + """ if branch is None: branch = self.__new_branch logging.info('Switching to "%s" branch.', branch) From 90dc880ea12ca5daa835c6cbce8d1fb1d91d18d8 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Wed, 4 Oct 2023 21:52:00 +0200 Subject: [PATCH 080/119] Move PathsMapping to path utils --- sources/git.py | 56 ++------------------------------------ sources/utils/path_util.py | 50 ++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/sources/git.py b/sources/git.py index 13c2efc..c42206f 100644 --- a/sources/git.py +++ b/sources/git.py @@ -12,8 +12,8 @@ from typing import Union, List, Optional, Dict, NoReturn from sources.command import GitCommandRunner -from sources.exceptions import GitException, GitPullException, GitRepositoryNotFoundException, \ - NotGitRepositoryException, GitPushException, GitAddException, GitRmException, GitMvException +from sources.exceptions import GitException, GitRepositoryNotFoundException, \ + NotGitRepositoryException from sources.models.base_classes import Reference, Author, Refspec from sources.models.branches import Branch, Branches from sources.models.commits import Commits, Commit @@ -34,7 +34,7 @@ from sources.parsers.branches_parser import BranchesParser from sources.parsers.commits_parser import CommitsParser from sources.parsers.tags_parser import TagsParser -from sources.utils.path_util import PathUtil +from sources.utils.path_util import PathUtil, PathsMapping class GitConfig: @@ -375,56 +375,6 @@ def checkout(self, branch: Optional[str] = None, create_if_not_exist: bool = Fal self.__repository.refresh_repository(refresh_active_branch=True, refresh_commits=True) -class PathsMapping: - DELIMITER: str = ':' - - __source: Path - __destination: Path - __root_path: Path - - def __init__(self, source: Union[str, Path], destination: Union[str, Path], root_path: Union[str, Path] = None): - if root_path is None: - root_path = Path() - self.root_path = root_path - self.__source = source - self.__destination = destination - - @classmethod - def create_from_text(cls, mapping: str, root_path: Union[str, Path] = None): - if root_path is None: - root_path = Path() - source, destination = mapping.strip().split(cls.DELIMITER) - source = source.strip() - destination = destination.strip() - instance = cls(source, destination, root_path) - return instance - - def __normalize_path(self, path: Union[str, Path]) -> Path: - if isinstance(path, str) and str(path).startswith(str(self.__root_path)): - normalized_path = Path(path) - elif isinstance(path, str): - normalized_path = self.__root_path.joinpath(path) - else: - normalized_path = path - return normalized_path - - @property - def source(self) -> Path: - return self.__normalize_path(self.__source) - - @property - def destination(self) -> Path: - return self.__normalize_path(self.__destination) - - @property - def root_path(self) -> Path: - return self.__root_path - - @root_path.setter - def root_path(self, root_path: Union[str, Path]) -> NoReturn: - self.__root_path = root_path - - class GitRepository: __repository_information: GitRepositoryPaths __git_command: GitCommandRunner diff --git a/sources/utils/path_util.py b/sources/utils/path_util.py index 33ad0e1..1d0e7ab 100644 --- a/sources/utils/path_util.py +++ b/sources/utils/path_util.py @@ -34,3 +34,53 @@ def convert_to_string(path: Union[str, Path]) -> str: if isinstance(path, Path): path = str(path.absolute()) return path + + +class PathsMapping: + DELIMITER: str = ':' + + __source: Path + __destination: Path + __root_path: Path + + def __init__(self, source: Union[str, Path], destination: Union[str, Path], root_path: Union[str, Path] = None): + if root_path is None: + root_path = Path() + self.root_path = root_path + self.__source = source + self.__destination = destination + + @classmethod + def create_from_text(cls, mapping: str, root_path: Union[str, Path] = None): + if root_path is None: + root_path = Path() + source, destination = mapping.strip().split(cls.DELIMITER) + source = source.strip() + destination = destination.strip() + instance = cls(source, destination, root_path) + return instance + + def __normalize_path(self, path: Union[str, Path]) -> Path: + if isinstance(path, str) and str(path).startswith(str(self.__root_path)): + normalized_path = Path(path) + elif isinstance(path, str): + normalized_path = self.__root_path.joinpath(path) + else: + normalized_path = path + return normalized_path + + @property + def source(self) -> Path: + return self.__normalize_path(self.__source) + + @property + def destination(self) -> Path: + return self.__normalize_path(self.__destination) + + @property + def root_path(self) -> Path: + return self.__root_path + + @root_path.setter + def root_path(self, root_path: Union[str, Path]) -> NoReturn: + self.__root_path = root_path From de35d090f6954fd3d29cf43e569278c7173b2ea2 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Wed, 4 Oct 2023 22:09:24 +0200 Subject: [PATCH 081/119] Add docstrings and typing to PathsMapping --- sources/utils/path_util.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/sources/utils/path_util.py b/sources/utils/path_util.py index 1d0e7ab..93e1d91 100644 --- a/sources/utils/path_util.py +++ b/sources/utils/path_util.py @@ -2,7 +2,7 @@ Module contains class for manipulations with a path. """ from pathlib import Path -from typing import Union +from typing import Union, NoReturn class PathUtil: @@ -37,6 +37,9 @@ def convert_to_string(path: Union[str, Path]) -> str: class PathsMapping: + """ + Class for paths mapping, maps source path with destination path. + """ DELIMITER: str = ':' __source: Path @@ -51,7 +54,17 @@ def __init__(self, source: Union[str, Path], destination: Union[str, Path], root self.__destination = destination @classmethod - def create_from_text(cls, mapping: str, root_path: Union[str, Path] = None): + def create_from_text(cls, mapping: str, root_path: Union[str, Path] = None) -> 'PathsMapping': + """ + Create a new class instance from the string mapping in the format "source:destination" that is relative to the + repository root. Optionally repository root can be provided, otherwise current directory considered as a root. + + :param mapping: Source to destination mapping, format: "source:destination". + :type mapping: str + :param root_path: Path to the root in which files are located. + :type root_path: Union[str, Path] + :return: A new class instance. + """ if root_path is None: root_path = Path() source, destination = mapping.strip().split(cls.DELIMITER) @@ -61,6 +74,13 @@ def create_from_text(cls, mapping: str, root_path: Union[str, Path] = None): return instance def __normalize_path(self, path: Union[str, Path]) -> Path: + """ + Normalize the path, returns absolute path converted from string to Path, that always starts from the root path. + + :param path: Path that will be normalized. + :type path: Union[str, Path] + :return: Normalized path. + """ if isinstance(path, str) and str(path).startswith(str(self.__root_path)): normalized_path = Path(path) elif isinstance(path, str): @@ -71,14 +91,23 @@ def __normalize_path(self, path: Union[str, Path]) -> Path: @property def source(self) -> Path: + """ + Source path of the mapping. + """ return self.__normalize_path(self.__source) @property def destination(self) -> Path: + """ + Destination path of the mapping. + """ return self.__normalize_path(self.__destination) @property def root_path(self) -> Path: + """ + Root path of the repository, where the files are located. + """ return self.__root_path @root_path.setter From fffd3238de8c1cf3fd109e8bed387371b49e91f5 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Thu, 5 Oct 2023 00:11:43 +0200 Subject: [PATCH 082/119] Add docstrings and typing to GitRepository --- sources/git.py | 214 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 192 insertions(+), 22 deletions(-) diff --git a/sources/git.py b/sources/git.py index c42206f..390b0c9 100644 --- a/sources/git.py +++ b/sources/git.py @@ -376,6 +376,9 @@ def checkout(self, branch: Optional[str] = None, create_if_not_exist: bool = Fal class GitRepository: + """ + Main class that allows to manage git repository. + """ __repository_information: GitRepositoryPaths __git_command: GitCommandRunner __git_config: GitConfig @@ -403,41 +406,71 @@ def __init__(self, path: Union[str, Path]): @property def git_command(self) -> GitCommandRunner: + """ + Git command class instance that allows to execute git commands. + """ return self.__git_command @property def path(self): + """ + Git repository path. + """ return self.__repository_information.path @property def git_path(self): + """ + Path to the '.git' directory in the repository. + """ return self.__repository_information.git_directory @property def gitignore(self): + """ + Path to .gitignore file. + """ return self.__gitignore @property def current_branch(self) -> Optional[Branch]: + """ + Current branch points on the branch to which the repository is currently configured. + """ return self.__active_branch @property def remotes(self) -> Remotes: + """ + List of remotes for the repository. + """ return self.__remotes @property def branches(self) -> Branches: + """ + List of all branches in the repository. + """ return self.__branches @property def commits(self) -> Commits: + """ + List of all commits in the repository. + """ return self.__commits @property def tags(self) -> Tags: + """ + List of all tags in the repository. + """ return self.__tags def __initialize_default_values(self): + """ + Initialize default empty values for the repository. + """ self.__active_branch = None self.__remotes = Remotes() self.__branches = Branches() @@ -445,7 +478,22 @@ def __initialize_default_values(self): self.__tags = Tags() def refresh_repository(self, refresh_active_branch: bool = False, refresh_branches: bool = False, - refresh_commits: bool = False, refresh_tags: bool = False, refresh_remotes: bool = False): + refresh_commits: bool = False, refresh_tags: bool = False, + refresh_remotes: bool = False) -> NoReturn: + """ + Refresh data in the models in the repository. + + :param refresh_active_branch: If True, then refresh current active branch. + :type refresh_active_branch: bool + :param refresh_branches: If True, then refresh list of all branches. + :type refresh_branches: bool + :param refresh_commits: If True, then refresh list of all commits. + :type refresh_commits: bool + :param refresh_tags: If True, then refresh list of all tags. + :type refresh_tags: bool + :param refresh_remotes: If True, then refresh list of all remotes. + :type refresh_remotes: bool + """ branches_parser = BranchesParser(self.__repository_information, self.__commits) branches_parser.refresh_active_branch() if refresh_commits: @@ -465,7 +513,12 @@ def refresh_repository(self, refresh_active_branch: bool = False, refresh_branch tags_parser = TagsParser(self.__git_command, self.__commits) self.__tags = tags_parser.tags - def __get_default_author(self): + def __get_default_author(self) -> Author: + """ + Get default author for the repository, based on your configuration. + + :return: Default author for the repository. + """ user_name_options = [ ConfigCommandDefinitions.Options.NAME.create_option('user.name') ] @@ -478,6 +531,15 @@ def __get_default_author(self): @staticmethod def __read_git_ignore(path: Path, git_command: GitCommandRunner) -> Optional[GitIgnore]: + """ + Read .gitignore file. + + :param path: Path to the '.gitignore' file. + :type path: Path + :param git_command: Git command class that wraps git commands. + :type git_command: GitCommandRunner + :return: Git ignore file object. + """ gitignore = None if path.exists(): options = [ShowCommandDefinitions.Options.OBJECTS.create_option([f'HEAD:{path.name}'])] @@ -490,49 +552,109 @@ def __read_git_ignore(path: Path, git_command: GitCommandRunner) -> Optional[Git gitignore = GitIgnore(path) return gitignore - def __read_remotes(self): + def __read_remotes(self) -> Remotes: + """ + Read remotes to the Remotes object. + + :return: Remotes object with list of all remotes in the repository. + """ return Remotes(self.__git_config.remotes) - def checkout(self, branch: str, create_if_not_exist: bool = False): + def checkout(self, branch: Union[str, Branch], create_if_not_exist: bool = False): + """ + Checkout command that allows to checkout another branch with or without the context manager. + + :param branch: Branch to which it shall switch. + :type branch: Union[str, Branch] + :param create_if_not_exist: If True, then create a new branch if it doesn't exist, otherwise it will fail when + branch doesn't exist. + :type create_if_not_exist: bool + """ + if isinstance(branch, Branch): + branch = branch.name return CheckoutHandler(branch, self, self.__active_branch.name, create_if_not_exist) def create_commit(self, message: str, author: Optional[Author] = None, date: Optional[datetime] = None, - commit_hash: str = None, parent: Union[str, Commit] = None): + commit_hash: str = '', parent: Union[str, Commit, None] = None) -> Commit: + """ + Method creates a new commit object, without creating the real commit itself. + + :param message: Commit message. + :type message: str + :param author: Author of the commit. + :type author: Optional[Author] + :param date: Date of the commit creation. + :type date: Optional[datetime] + :param commit_hash: Commit object hash. + :type commit_hash: str + :param parent: Parent commit of the current commit. + :type parent: Union[str, Commit, None] + :return: A new commit instance. + """ if author is None: author = self.__default_author if date is None: date = datetime.now() - if commit_hash is None: - commit_hash = '' if parent and isinstance(parent, str): parent = self.__commits[parent] return Commit(message=message, author=author, date=date, commit_hash=commit_hash, parent=parent) @classmethod - def init(cls, path: Union[str, Path], *options: GitOption): + def init(cls, path: Union[str, Path], *options: GitOption) -> 'GitRepository': + """ + Initialize new git repository and return a new GitRepository instance for that repository. + + :param path: Path to the new git repository. + :type path: Union[str, Path] + :param options: Additional options for the 'git init' command. + :type options: Tuple[GitOption] + :return: GitRepository instance for the new git repository. + """ if isinstance(path, str): path = Path(path) - options = list(options) - options.append(InitCommandDefinitions.Options.DIRECTORY.create_option(str(path.absolute()))) + command_options = list(options) + command_options.append(InitCommandDefinitions.Options.DIRECTORY.create_option(str(path.absolute()))) git_command = GitCommandRunner() - git_command.init(*options) + git_command.init(*command_options) return cls(path) @classmethod - def clone(cls, repository: Union[str, Remote], path: Union[str, Path] = None, *options: GitOption): + def clone(cls, repository: Union[str, Remote], *options: GitOption, + path: Union[str, Path] = None) -> 'GitRepository': + """ + Clone new git repository from the remote and return a new GitRepository instance for that repository. + + :param repository: Remote url or Remote object that points to the remote repository that will be cloned. + :type repository: Union[str, Remote] + :param options: Additional options for the 'git clone' command. + :type options: Tuple[GitOption] + :param path: Path to the new git repository. + :type path: Union[str, Path] + :return: GitRepository instance for the new git repository that has been cloned locally. + """ if isinstance(path, str): path = Path(path) if isinstance(repository, Remote): repository = repository.url - options = list(options) - options.append(CloneCommandDefinitions.Options.REPOSITORY.create_option(repository)) + command_options = list(options) + command_options.append(CloneCommandDefinitions.Options.REPOSITORY.create_option(repository)) if path is not None: - options.append(CloneCommandDefinitions.Options.DIRECTORY.create_option(str(path.absolute()))) + command_options.append(CloneCommandDefinitions.Options.DIRECTORY.create_option(str(path.absolute()))) git_command = GitCommandRunner() - git_command.clone(*options) + git_command.clone(*command_options) return cls(path) - def add(self, files: Union[str, Path, List[Union[str, Path]]], *options: GitOption): + def add(self, files: Union[str, Path, List[Union[str, Path]]], *options: GitOption) -> str: + """ + Add a new file or list of files to the tracking. + + :param files: A file or list of files that will be added. + :type files: Union[str, Path, List[Union[str, Path]] + :param options: List of additional options for the 'git add' command. + :type options: Tuple[GitOption] + :return: Output from the 'git add' command, if multiple paths were provided, then joined output will be + returned. + """ outputs = [] if isinstance(files, (str, Path)): files = [files] @@ -546,7 +668,17 @@ def add(self, files: Union[str, Path, List[Union[str, Path]]], *options: GitOpti outputs.append(output.strip()) return '\n'.join(outputs) - def mv(self, mappings: Union[PathsMapping, List[PathsMapping]], *options: GitOption): + def mv(self, mappings: Union[PathsMapping, List[PathsMapping]], *options: GitOption) -> str: + """ + Move a file or list of files from one place to another and track this change. + + :param mappings: A file mapping or the list of files mappings that contains information about source and + destination. + :type mappings: Union[PathsMapping, List[PathsMapping]] + :param options: List of additional options for the 'git mv' command. + :type options: Tuple[GitOption] + :return: Output from the 'git mv' command, if multiple paths were provided, then joined output will be returned. + """ outputs = [] if isinstance(mappings, PathsMapping): mappings = [mappings] @@ -560,7 +692,16 @@ def mv(self, mappings: Union[PathsMapping, List[PathsMapping]], *options: GitOpt outputs.append(output.strip()) return '\n'.join(outputs) - def rm(self, files: Union[str, Path, List[Union[str, Path]]], *options: GitOption): + def rm(self, files: Union[str, Path, List[Union[str, Path]]], *options: GitOption) -> str: + """ + Remove a file or list of files and track this change. + + :param files: A file or list of files that will be removed. + :type files: Union[str, Path, List[Union[str, Path]]] + :param options: List of additional options for the 'git rm' command. + :type options: Tuple[GitOption] + :return: Output from the 'git rm' command, if multiple paths were provided, then joined output will be returned. + """ outputs = [] if isinstance(files, (str, Path)): files = [files] @@ -579,7 +720,14 @@ def rm(self, files: Union[str, Path, List[Union[str, Path]]], *options: GitOptio return '\n'.join(outputs) @staticmethod - def __get_refspec(reference: Optional[Union[Reference, Refspec]]): + def __get_refspec(reference: Optional[Union[Reference, Refspec]]) -> Optional[str]: + """ + Extract refspec string from the Reference or Refspec object. + + :param reference: Reference that will be converted to string, if not possible, then None returned. + :type reference: Optional[str] + :return: Reference converted to the string. + """ if isinstance(reference, Reference): refspec = reference.path elif isinstance(reference, Refspec): @@ -588,7 +736,18 @@ def __get_refspec(reference: Optional[Union[Reference, Refspec]]): refspec = None return refspec - def pull(self, remote: Remote, reference: Optional[Union[Reference, Refspec]] = None, *options: GitOption): + def pull(self, remote: Remote, *options: GitOption, reference: Optional[Union[Reference, Refspec]] = None) -> str: + """ + Pull changes from the remote repository into local repository. + + :param remote: Remote repository from which changes will be pulled. + :type remote: Remote + :param options: List of additional options for the 'git pull' command. + :type options: Tuple[GitOption] + :param reference: Specific reference that will be pulled. + :type reference: Optional[Union[Reference, Refspec]] + :return: Output from the 'git pull' command. + """ refspec = self.__get_refspec(reference) options = list(options) options.append(PullCommandDefinitions.Options.REPOSITORY.create_option(remote.name)) @@ -596,7 +755,18 @@ def pull(self, remote: Remote, reference: Optional[Union[Reference, Refspec]] = options.append(PullCommandDefinitions.Options.REFSPEC.create_option(refspec)) return self.__git_command.pull(*options) - def push(self, remote: Remote, reference: Optional[Union[Reference, Refspec]] = None, *options: GitOption): + def push(self, remote: Remote, *options: GitOption, reference: Optional[Union[Reference, Refspec]] = None) -> str: + """ + Push changes to the remote repository from local repository. + + :param remote: Remote repository to which changes will be pushed. + :type remote: Remote + :param options: List of additional options for the 'git push' command. + :type options: Tuple[GitOption] + :param reference: Specific reference that will be pushed. + :type reference: Optional[Union[Reference, Refspec]] + :return: Output from the 'git push' command. + """ refspec = self.__get_refspec(reference) options = list(options) options.append(PushCommandDefinitions.Options.REPOSITORY.create_option(remote.name)) From 542ecd2fd98a43f05176da70a952cdfffe795f8c Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 7 Oct 2023 02:59:50 +0200 Subject: [PATCH 083/119] Add method and class for commit handling --- sources/git.py | 156 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 117 insertions(+), 39 deletions(-) diff --git a/sources/git.py b/sources/git.py index 390b0c9..7d95f4f 100644 --- a/sources/git.py +++ b/sources/git.py @@ -2,7 +2,6 @@ Main module that contains entry points classes for the manipulations with the git repository. """ import fnmatch -import hashlib import logging import os import re @@ -23,6 +22,7 @@ from sources.options.add_options import AddCommandDefinitions from sources.options.checkout_options import CheckoutCommandDefinitions from sources.options.clone_options import CloneCommandDefinitions +from sources.options.commit_options import CommitCommandDefinitions from sources.options.config_options import ConfigCommandDefinitions from sources.options.init_options import InitCommandDefinitions from sources.options.mv_options import MvCommandDefinitions @@ -203,47 +203,42 @@ class FilesChangesHandler: START = 'start' END = 'end' - ADDED = 'added' - MODIFIED = 'modified' - REMOVED = 'removed' - EXCLUDED = 'excluded' + ADDED_TAG = 'added' + MODIFIED_TAG = 'modified' + REMOVED_TAG = 'removed' + EXCLUDED_TAG = 'excluded' def __init__(self, repository: 'GitRepository'): self.__repository = repository self.__files_hashes = {self.START: {}, self.END: {}} - - def __enter__(self): + self.__files_status = {} self.__update_files_hash(self.__repository.path.absolute(), self.__files_hashes[self.START]) - return self - def __exit__(self, exc_type, exc_val, exc_tb): - # TODO(dl1998): Optimize the code. Sort "start" and "end", so it will be faster. Instead of checking using - # "in", walk through lists and classify file by file. Consider to use shared input. - result = self.files_status - logging.debug(result) - - @property - def files_status(self): + def update_files_status(self) -> NoReturn: """ Method checks status of the files by comparing original files state with current files state. All files changes are classified as added, modified, or removed. - - :return: Dictionary with changes classified as added, modified, and removed, additionally contains excluded - files. """ + self.__files_hashes[self.END] = {} self.__update_files_hash(self.__repository.path.absolute(), self.__files_hashes[self.END]) - result = { - self.ADDED: self.__get_added(self.__files_hashes[self.START], self.__files_hashes[self.END]), - self.MODIFIED: self.__get_modified(self.__files_hashes[self.START], self.__files_hashes[self.END]), - self.REMOVED: self.__get_removed(self.__files_hashes[self.START], self.__files_hashes[self.END]) + self.__files_status = { + self.ADDED_TAG: self.__get_added(self.__files_hashes[self.START], self.__files_hashes[self.END]), + self.MODIFIED_TAG: self.__get_modified(self.__files_hashes[self.START], self.__files_hashes[self.END]), + self.REMOVED_TAG: self.__get_removed(self.__files_hashes[self.START], self.__files_hashes[self.END]) } if self.__repository.gitignore: - result[self.EXCLUDED] = self.__get_excluded(self.__files_hashes[self.END], - self.__repository.gitignore.exclude_patterns) - return result + self.__files_status[self.EXCLUDED_TAG] = self.__get_excluded(self.__files_hashes[self.END], + self.__repository.gitignore.exclude_patterns) + + @property + def files_status(self) -> Dict[str, List[str]]: + """ + Dictionary with changes classified as added, modified, and removed, additionally contains excluded files. + """ + return self.__files_status @staticmethod - def __update_files_hash(parent: Path, files_dictionary: Dict[str, str]) -> NoReturn: + def __update_files_hash(parent: Path, files_dictionary: Dict[str, float]) -> NoReturn: """ Method updates files hashes in the provided dictionary. @@ -255,9 +250,8 @@ def __update_files_hash(parent: Path, files_dictionary: Dict[str, str]) -> NoRet for root, _, files in os.walk(parent.absolute()): for file in files: absolute_path = Path(root, file) - with absolute_path.open('rb') as binary_file: - file_hash = hashlib.md5(binary_file.read()).hexdigest() - files_dictionary[str(absolute_path)] = file_hash + absolute_path_str = str(absolute_path) + files_dictionary[absolute_path_str] = os.stat(absolute_path_str).st_mtime @staticmethod def __get_added(start: Dict, end: Dict) -> List[str]: @@ -330,6 +324,81 @@ def __get_excluded(files: Dict[str, str], excludes: List[str]) -> List[str]: break return result + @property + def added(self) -> List[str]: + """ + List of added files. + """ + return self.__files_status.get(self.ADDED_TAG, []) + + @property + def modified(self) -> List[str]: + """ + List of modified files. + """ + return self.__files_status.get(self.MODIFIED_TAG, []) + + @property + def removed(self) -> List[str]: + """ + List of removed files. + """ + return self.__files_status.get(self.REMOVED_TAG, []) + + @property + def excluded(self) -> List[str]: + """ + List of excluded files. + """ + return self.__files_status.get(self.EXCLUDED_TAG, []) + + +class CommitHandler: + """ + Class handles commit operation including tracking of the changed files. + """ + __commit_message: str + __git_repository: 'GitRepository' + __files_changes_handler: FilesChangesHandler + + def __init__(self, message: str, git_repository: 'GitRepository'): + self.__commit_message = message + self.__git_repository = git_repository + self.__files_changes_handler = FilesChangesHandler(git_repository) + + def __enter__(self): + return self.__files_changes_handler + + def __exit__(self, exc_type, exc_val, exc_tb): + changes_found = self.__update_changed_files() + if changes_found: + logging.info('Creating a new commit') + logging.info('Message: %s', self.__commit_message) + options = [ + CommitCommandDefinitions.Options.MESSAGE.create_option(self.__commit_message), + ] + self.__git_repository.git_command.commit(*options) + + def __update_changed_files(self) -> bool: + """ + Add changes done in the files to the tracking. + + :return: True, if any type of modifications happened (add a new file, modify a file, remove a file), otherwise + return False. + """ + self.__files_changes_handler.update_files_status() + changes_found = False + if self.__files_changes_handler.added: + self.__git_repository.add(self.__files_changes_handler.added) + changes_found = True + if self.__files_changes_handler.modified: + self.__git_repository.add(self.__files_changes_handler.modified) + changes_found = True + if self.__files_changes_handler.removed: + self.__git_repository.rm(self.__files_changes_handler.removed) + changes_found = True + return changes_found + class CheckoutHandler: """ @@ -560,6 +629,16 @@ def __read_remotes(self) -> Remotes: """ return Remotes(self.__git_config.remotes) + def commit(self, message: str) -> CommitHandler: + """ + Commit changes to the files. + + :param message: A new commit message. + :type message: str + :return: Commit handler object. + """ + return CommitHandler(message, self) + def checkout(self, branch: Union[str, Branch], create_if_not_exist: bool = False): """ Checkout command that allows to checkout another branch with or without the context manager. @@ -662,9 +741,9 @@ def add(self, files: Union[str, Path, List[Union[str, Path]]], *options: GitOpti if isinstance(file_path, Path): file_path = str(file_path.absolute()) file_path = [file_path] - options = list(options) - options.append(AddCommandDefinitions.Options.PATHSPEC.create_option(file_path)) - output = self.__git_command.add(*options) + command_options = list(options) + command_options.append(AddCommandDefinitions.Options.PATHSPEC.create_option(file_path)) + output = self.__git_command.add(*command_options) outputs.append(output.strip()) return '\n'.join(outputs) @@ -684,11 +763,11 @@ def mv(self, mappings: Union[PathsMapping, List[PathsMapping]], *options: GitOpt mappings = [mappings] for mapping in mappings: mapping.root_path = self.__repository_information.path - options = list(options) - options.append(MvCommandDefinitions.Options.SOURCE.create_option(str(mapping.source.absolute()))) - options.append( + command_options = list(options) + command_options.append(MvCommandDefinitions.Options.SOURCE.create_option(str(mapping.source.absolute()))) + command_options.append( MvCommandDefinitions.Options.DESTINATION.create_option(str(mapping.destination.absolute()))) - output = self.__git_command.mv(*options) + output = self.__git_command.mv(*command_options) outputs.append(output.strip()) return '\n'.join(outputs) @@ -706,14 +785,13 @@ def rm(self, files: Union[str, Path, List[Union[str, Path]]], *options: GitOptio if isinstance(files, (str, Path)): files = [files] raw_repository_path = str(self.__repository_information.path.absolute()) - options = list(options) for file_path in files: if isinstance(file_path, str) and file_path.startswith(raw_repository_path): file_path = Path(file_path) else: file_path = self.__repository_information.path.joinpath(file_path) file_path = [str(file_path.absolute())] - command_options = options.copy() + command_options = list(options) command_options.append(RmCommandDefinitions.Options.PATHSPEC.create_option(file_path)) output = self.__git_command.rm(*command_options) outputs.append(output.strip()) From e6a376ce0678ddea9299a5093662d1637741b68a Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 7 Oct 2023 03:25:17 +0200 Subject: [PATCH 084/119] Cleanup and simplify models --- sources/git.py | 58 ++++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/sources/git.py b/sources/git.py index 7d95f4f..5fb1742 100644 --- a/sources/git.py +++ b/sources/git.py @@ -6,6 +6,7 @@ import os import re from configparser import ConfigParser +from dataclasses import field, dataclass from datetime import datetime from pathlib import Path from typing import Union, List, Optional, Dict, NoReturn @@ -444,6 +445,18 @@ def checkout(self, branch: Optional[str] = None, create_if_not_exist: bool = Fal self.__repository.refresh_repository(refresh_active_branch=True, refresh_commits=True) +@dataclass +class GitObjects: + """ + Class stores different type of git objects, like: remotes, branches, commits, tags. + """ + remotes: Remotes = field(default_factory=Remotes, init=False) + commits: Commits = field(default_factory=Commits, init=False) + tags: Tags = field(default_factory=Tags, init=False) + branches: Branches = field(default_factory=Branches, init=False) + active_branch: Optional[Branch] = field(default=None, init=False) + + class GitRepository: """ Main class that allows to manage git repository. @@ -452,11 +465,7 @@ class GitRepository: __git_command: GitCommandRunner __git_config: GitConfig __gitignore: GitIgnore - __active_branch: Optional[Branch] - __remotes: Remotes - __branches: Branches - __commits: Commits - __tags: Tags + __objects: GitObjects def __init__(self, path: Union[str, Path]): self.__git_command = GitCommandRunner() @@ -506,45 +515,41 @@ def current_branch(self) -> Optional[Branch]: """ Current branch points on the branch to which the repository is currently configured. """ - return self.__active_branch + return self.__objects.active_branch @property def remotes(self) -> Remotes: """ List of remotes for the repository. """ - return self.__remotes + return self.__objects.remotes @property def branches(self) -> Branches: """ List of all branches in the repository. """ - return self.__branches + return self.__objects.branches @property def commits(self) -> Commits: """ List of all commits in the repository. """ - return self.__commits + return self.__objects.commits @property def tags(self) -> Tags: """ List of all tags in the repository. """ - return self.__tags + return self.__objects.tags def __initialize_default_values(self): """ Initialize default empty values for the repository. """ - self.__active_branch = None - self.__remotes = Remotes() - self.__branches = Branches() - self.__commits = Commits() - self.__tags = Tags() + self.__objects = GitObjects() def refresh_repository(self, refresh_active_branch: bool = False, refresh_branches: bool = False, refresh_commits: bool = False, refresh_tags: bool = False, @@ -563,24 +568,25 @@ def refresh_repository(self, refresh_active_branch: bool = False, refresh_branch :param refresh_remotes: If True, then refresh list of all remotes. :type refresh_remotes: bool """ - branches_parser = BranchesParser(self.__repository_information, self.__commits) + branches_parser = BranchesParser(self.__repository_information, self.__objects.commits) branches_parser.refresh_active_branch() if refresh_commits: try: commits_parser = CommitsParser(self.__git_command) - self.__commits = commits_parser.commits + self.__objects.commits = commits_parser.commits except GitException: - self.__commits = Commits() + self.__objects.commits = Commits() if refresh_remotes: - self.__remotes = self.__read_remotes() + self.__objects.remotes = self.__read_remotes() if refresh_active_branch: - self.__active_branch = Branch(name=branches_parser.active_branch_name, - commit=self.__commits[branches_parser.active_branch_commit_hash]) + self.__objects.active_branch = Branch(name=branches_parser.active_branch_name, + commit=self.__objects.commits[ + branches_parser.active_branch_commit_hash]) if refresh_branches: - self.__branches = branches_parser.branches + self.__objects.branches = branches_parser.branches if refresh_tags: - tags_parser = TagsParser(self.__git_command, self.__commits) - self.__tags = tags_parser.tags + tags_parser = TagsParser(self.__git_command, self.__objects.commits) + self.__objects.tags = tags_parser.tags def __get_default_author(self) -> Author: """ @@ -651,7 +657,7 @@ def checkout(self, branch: Union[str, Branch], create_if_not_exist: bool = False """ if isinstance(branch, Branch): branch = branch.name - return CheckoutHandler(branch, self, self.__active_branch.name, create_if_not_exist) + return CheckoutHandler(branch, self, self.__objects.active_branch.name, create_if_not_exist) def create_commit(self, message: str, author: Optional[Author] = None, date: Optional[datetime] = None, commit_hash: str = '', parent: Union[str, Commit, None] = None) -> Commit: @@ -675,7 +681,7 @@ def create_commit(self, message: str, author: Optional[Author] = None, date: Opt if date is None: date = datetime.now() if parent and isinstance(parent, str): - parent = self.__commits[parent] + parent = self.__objects.commits[parent] return Commit(message=message, author=author, date=date, commit_hash=commit_hash, parent=parent) @classmethod From d060873073f53a802968d20516bbda754ff087a1 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 7 Oct 2023 17:38:27 +0200 Subject: [PATCH 085/119] Add pytest dependency --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index c4212ca..6f41296 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,6 @@ wheel==0.41.2 Sphinx==5.3.0 sphinx-rtd-theme==1.3.0 docs-versions-menu==0.5.2 +pytest==7.4.2 behave==1.2.6 coverage==7.2.7 \ No newline at end of file From ffb7db326e50e39260cc7c4dd8c65c1d1b3b4a04 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 22:33:06 +0200 Subject: [PATCH 086/119] Add testing utilities for options --- tests/unit_tests/__init__.py | 0 tests/unit_tests/pygit/__init__.py | 0 tests/unit_tests/pygit/options/__init__.py | 0 .../unit_tests/pygit/options/testing_utils.py | 23 +++++++++++++++++++ 4 files changed, 23 insertions(+) create mode 100644 tests/unit_tests/__init__.py create mode 100644 tests/unit_tests/pygit/__init__.py create mode 100644 tests/unit_tests/pygit/options/__init__.py create mode 100644 tests/unit_tests/pygit/options/testing_utils.py diff --git a/tests/unit_tests/__init__.py b/tests/unit_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit_tests/pygit/__init__.py b/tests/unit_tests/pygit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit_tests/pygit/options/__init__.py b/tests/unit_tests/pygit/options/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit_tests/pygit/options/testing_utils.py b/tests/unit_tests/pygit/options/testing_utils.py new file mode 100644 index 0000000..cf2296e --- /dev/null +++ b/tests/unit_tests/pygit/options/testing_utils.py @@ -0,0 +1,23 @@ +from typing import Type + +from sources.options.options import GitCommand + + +class CommandDefinitionsTestingUtil: + @staticmethod + def test_command_positive(class_type: Type[GitCommand], expected_name: str): + command = class_type() + assert command.command_name == expected_name + + @staticmethod + def test_definitions_positive(class_type: Type[GitCommand]): + command = class_type() + missing_options = [] + for option in command.Options: + found = False + for definition in command.definitions: + if definition.name_aliases == option.value: + found = True + if not found: + missing_options.append(option) + assert len(missing_options) == 0 From 767f02df98583815e8e8efa899e0edfa822e2f27 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 22:33:46 +0200 Subject: [PATCH 087/119] Add tests for options base clases --- .../unit_tests/pygit/options/test_options.py | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 tests/unit_tests/pygit/options/test_options.py diff --git a/tests/unit_tests/pygit/options/test_options.py b/tests/unit_tests/pygit/options/test_options.py new file mode 100644 index 0000000..ed0a287 --- /dev/null +++ b/tests/unit_tests/pygit/options/test_options.py @@ -0,0 +1,171 @@ +from typing import Any, Type + +import pytest + +from sources.exceptions import GitException, GitMissingDefinitionException, GitIncorrectOptionValueException, \ + GitMissingRequiredOptionsException +from sources.options.options import GitOptionNameAliases, GitOptionNameAlias, GitOptionDefinition, GitOption, \ + GitCommand, CommandOptions + + +class DemoCommand(GitCommand): + class Options(CommandOptions): + OPTION = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='option', short_option=False), + GitOptionNameAlias(name='o', short_option=True), + ]) + LONG_OPTION = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='long-option', short_option=False), + ]) + SHORT_OPTION = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='s', short_option=True) + ]) + POSITIONAL_OPTION = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='positional', short_option=True) + ]) + + def __init__(self): + super().__init__('command') + self.definitions = [ + GitOptionDefinition(name_aliases=self.Options.OPTION, type=(str, bool)), + GitOptionDefinition(name_aliases=self.Options.LONG_OPTION, type=str, choices=['first', 'second']), + GitOptionDefinition(name_aliases=self.Options.SHORT_OPTION, type=bool), + GitOptionDefinition(name_aliases=self.Options.POSITIONAL_OPTION, type=str, positional=True, position=0, + required=True), + ] + + +class TestGitOptionNameAliases: + @pytest.mark.parametrize('name,short_option,length', [('option', False, 1), ('o', True, 1)], + ids=('Get long alias', 'Get short alias')) + def test_get_aliases_positive(self, name: str, short_option: bool, length: int): + alias = GitOptionNameAlias(name=name, short_option=short_option) + option_aliases = GitOptionNameAliases(aliases=[ + alias, + ]) + aliases = option_aliases.get_aliases(short_option=short_option) + assert len(aliases) == length + assert aliases[0] == alias + + # noinspection PyTypeChecker + def test_get_aliases_negative(self): + alias = 'option' + try: + option_aliases = GitOptionNameAliases(aliases=[ + alias, + ]) + option_aliases.get_aliases(short_option=False) + test_result = False + except AttributeError: + test_result = True + assert test_result + + @pytest.mark.parametrize('name,short_option', [('option', False), ('o', True)], + ids=("Without short options", 'With short options')) + def test_has_short_aliases_positive(self, name: str, short_option: bool): + alias = GitOptionNameAlias(name=name, short_option=short_option) + option_aliases = GitOptionNameAliases(aliases=[ + alias, + ]) + has_short_aliases = option_aliases.has_short_aliases() + assert has_short_aliases == short_option + + @pytest.mark.parametrize('name,short_option', [('option', False), ('o', True)], + ids=("Without short options", 'With short options')) + def test_has_long_aliases_positive(self, name: str, short_option: bool): + alias = GitOptionNameAlias(name=name, short_option=short_option) + option_aliases = GitOptionNameAliases(aliases=[ + alias, + ]) + has_long_aliases = option_aliases.has_long_aliases() + assert has_long_aliases != short_option + + @pytest.mark.parametrize('name,exists', [('option', True), ('o', True), ('another_option', False)], + ids=("Long option exists", 'Short option exists', 'Missing option')) + def test_exists_positive(self, name: str, exists: bool): + option_aliases = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='option', short_option=False), + GitOptionNameAlias(name='o', short_option=True), + ]) + option_exists = option_aliases.exists(name) + assert option_exists == exists + + def test_get_names_positive(self): + option_aliases = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='option', short_option=False), + GitOptionNameAlias(name='o', short_option=True), + ]) + names = option_aliases.get_names() + assert names == ['option', 'o'] + + class TestGitOptionDefinition: + @pytest.mark.parametrize('name,value,expected', [ + ('option', True, True), ('option', 'value', True), ('o', True, True), ('o', 123, False), + ('another_option', 'value', False) + ], ids=('Correct long boolean option', 'Correct long string option', 'Correct short boolean option', + 'Incorrect short integer option', 'Incorrect another string option')) + def test_compare_with_option_positive(self, name: str, value: Any, expected: bool): + name_aliases = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='option', short_option=False), + GitOptionNameAlias(name='o', short_option=True), + ]) + definition = GitOptionDefinition(name_aliases=name_aliases, type=(str, bool)) + option = GitOption(name=name, value=value) + comparison_result = definition.compare_with_option(option) + assert comparison_result == expected + + class TestGitCommand: + @pytest.mark.parametrize('name,value,expected', [ + ('option', True, True), ('option', 'value', True), ('o', True, True), ('o', 123, False), + ('another_option', 'value', False) + ], ids=("Definition exists for long boolean option", "Definition exists for long string option", + "Definition exists for short boolean option", "Definition doesn't exist for short integer option", + "Definition doesn't exist for another string option")) + def test_get_definition_positive(self, name: str, value: Any, expected: bool): + option = GitOption(name=name, value=value) + command = DemoCommand() + definition = command.get_definition(option) + if expected: + assert definition is not None + else: + assert definition is None + + def test_validate_positive(self): + options = [ + DemoCommand.Options.SHORT_OPTION.create_option(True), + DemoCommand.Options.LONG_OPTION.create_option('first'), + DemoCommand.Options.POSITIONAL_OPTION.create_option('value'), + ] + command = DemoCommand() + fired = False + try: + command.validate(options) + except GitException: + fired = True + assert not fired + + @pytest.mark.parametrize('name,value,expected_exception', [ + ('missing_option', True, GitMissingDefinitionException), + ('long-option', 'third', GitIncorrectOptionValueException), + ('option', True, GitMissingRequiredOptionsException), + ], ids=("Missing option exception", "Incorrect option value exception", "Missing required option exception")) + def test_validate_negative(self, name: str, value: Any, expected_exception: Type[GitException]): + option = GitOption(name=name, value=value) + command = DemoCommand() + fired = False + try: + command.validate(option) + except expected_exception: + fired = True + assert fired + + def test_transform_to_command_positive(self): + options = [ + DemoCommand.Options.SHORT_OPTION.create_option(True), + DemoCommand.Options.LONG_OPTION.create_option('first'), + DemoCommand.Options.POSITIONAL_OPTION.create_option('value'), + ] + command = DemoCommand() + expected_output = ['command', '-s', '--long-option', 'first', 'value'] + transformed_command = command.transform_to_command(options) + assert transformed_command == expected_output From 4c4d767e380b431af9d1c1c596694d301d18a4be Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 22:34:33 +0200 Subject: [PATCH 088/119] Add tests for 'git add' options --- tests/unit_tests/pygit/options/test_add_options.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/unit_tests/pygit/options/test_add_options.py diff --git a/tests/unit_tests/pygit/options/test_add_options.py b/tests/unit_tests/pygit/options/test_add_options.py new file mode 100644 index 0000000..6169def --- /dev/null +++ b/tests/unit_tests/pygit/options/test_add_options.py @@ -0,0 +1,10 @@ +from pygit.options.testing_utils import CommandDefinitionsTestingUtil +from sources.options.add_options import AddCommandDefinitions + + +class TestAddCommandDefinitions: + def test_command_positive(self): + CommandDefinitionsTestingUtil.test_command_positive(AddCommandDefinitions, 'add') + + def test_definitions_positive(self): + CommandDefinitionsTestingUtil.test_definitions_positive(AddCommandDefinitions) From 4eac11ccaaf937bdebaad7ca540b7122ce762a0f Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 22:37:07 +0200 Subject: [PATCH 089/119] Add tests for 'git commit' options --- .../unit_tests/pygit/options/test_checkout_options.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/unit_tests/pygit/options/test_checkout_options.py diff --git a/tests/unit_tests/pygit/options/test_checkout_options.py b/tests/unit_tests/pygit/options/test_checkout_options.py new file mode 100644 index 0000000..7233329 --- /dev/null +++ b/tests/unit_tests/pygit/options/test_checkout_options.py @@ -0,0 +1,10 @@ +from pygit.options.testing_utils import CommandDefinitionsTestingUtil +from sources.options.checkout_options import CheckoutCommandDefinitions + + +class TestCheckoutCommandDefinitions: + def test_command_positive(self): + CommandDefinitionsTestingUtil.test_command_positive(CheckoutCommandDefinitions, 'checkout') + + def test_definitions_positive(self): + CommandDefinitionsTestingUtil.test_definitions_positive(CheckoutCommandDefinitions) From 8e79903457824439506c2e7ecadbc9416a347b5a Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 22:37:25 +0200 Subject: [PATCH 090/119] Add tests for 'git clone' options --- tests/unit_tests/pygit/options/test_clone_options.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/unit_tests/pygit/options/test_clone_options.py diff --git a/tests/unit_tests/pygit/options/test_clone_options.py b/tests/unit_tests/pygit/options/test_clone_options.py new file mode 100644 index 0000000..40b5beb --- /dev/null +++ b/tests/unit_tests/pygit/options/test_clone_options.py @@ -0,0 +1,10 @@ +from pygit.options.testing_utils import CommandDefinitionsTestingUtil +from sources.options.clone_options import CloneCommandDefinitions + + +class TestCloneCommandDefinitions: + def test_command_positive(self): + CommandDefinitionsTestingUtil.test_command_positive(CloneCommandDefinitions, 'clone') + + def test_definitions_positive(self): + CommandDefinitionsTestingUtil.test_definitions_positive(CloneCommandDefinitions) From 35e92e166cf14da139d460f3d94e4d1b9932b613 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 22:37:41 +0200 Subject: [PATCH 091/119] Add tests for 'git commit' options --- tests/unit_tests/pygit/options/test_commit_options.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/unit_tests/pygit/options/test_commit_options.py diff --git a/tests/unit_tests/pygit/options/test_commit_options.py b/tests/unit_tests/pygit/options/test_commit_options.py new file mode 100644 index 0000000..778b283 --- /dev/null +++ b/tests/unit_tests/pygit/options/test_commit_options.py @@ -0,0 +1,10 @@ +from pygit.options.testing_utils import CommandDefinitionsTestingUtil +from sources.options.commit_options import CommitCommandDefinitions + + +class TestCommitCommandDefinitions: + def test_command_positive(self): + CommandDefinitionsTestingUtil.test_command_positive(CommitCommandDefinitions, 'commit') + + def test_definitions_positive(self): + CommandDefinitionsTestingUtil.test_definitions_positive(CommitCommandDefinitions) From ca7ec645a66e7f7f720544e98b4f7046e8d81e56 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 22:37:54 +0200 Subject: [PATCH 092/119] Add tests for 'git config' options --- tests/unit_tests/pygit/options/test_config_options.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/unit_tests/pygit/options/test_config_options.py diff --git a/tests/unit_tests/pygit/options/test_config_options.py b/tests/unit_tests/pygit/options/test_config_options.py new file mode 100644 index 0000000..85e12bc --- /dev/null +++ b/tests/unit_tests/pygit/options/test_config_options.py @@ -0,0 +1,10 @@ +from pygit.options.testing_utils import CommandDefinitionsTestingUtil +from sources.options.config_options import ConfigCommandDefinitions + + +class TestConfigCommandDefinitions: + def test_command_positive(self): + CommandDefinitionsTestingUtil.test_command_positive(ConfigCommandDefinitions, 'config') + + def test_definitions_positive(self): + CommandDefinitionsTestingUtil.test_definitions_positive(ConfigCommandDefinitions) From 2e53f3a2ac7b4af6dfc126854927664661f6f8f1 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 22:38:33 +0200 Subject: [PATCH 093/119] Add tests for 'git for-each-ref' options --- .../pygit/options/test_for_each_ref_options.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/unit_tests/pygit/options/test_for_each_ref_options.py diff --git a/tests/unit_tests/pygit/options/test_for_each_ref_options.py b/tests/unit_tests/pygit/options/test_for_each_ref_options.py new file mode 100644 index 0000000..da95f1b --- /dev/null +++ b/tests/unit_tests/pygit/options/test_for_each_ref_options.py @@ -0,0 +1,10 @@ +from pygit.options.testing_utils import CommandDefinitionsTestingUtil +from sources.options.for_each_ref_options import ForEachRefCommandDefinitions + + +class TestForEachRefCommandDefinitions: + def test_command_positive(self): + CommandDefinitionsTestingUtil.test_command_positive(ForEachRefCommandDefinitions, 'for-each-ref') + + def test_definitions_positive(self): + CommandDefinitionsTestingUtil.test_definitions_positive(ForEachRefCommandDefinitions) From c4add5f59e972f74f42b3ab97f359e2a95939f23 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 22:38:51 +0200 Subject: [PATCH 094/119] Add tests for 'git init' options --- tests/unit_tests/pygit/options/test_init_options.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/unit_tests/pygit/options/test_init_options.py diff --git a/tests/unit_tests/pygit/options/test_init_options.py b/tests/unit_tests/pygit/options/test_init_options.py new file mode 100644 index 0000000..cd98b9d --- /dev/null +++ b/tests/unit_tests/pygit/options/test_init_options.py @@ -0,0 +1,10 @@ +from pygit.options.testing_utils import CommandDefinitionsTestingUtil +from sources.options.init_options import InitCommandDefinitions + + +class TestInitCommandDefinitions: + def test_command_positive(self): + CommandDefinitionsTestingUtil.test_command_positive(InitCommandDefinitions, 'init') + + def test_definitions_positive(self): + CommandDefinitionsTestingUtil.test_definitions_positive(InitCommandDefinitions) From 6cdd458e2393b04fc9b322c7a4e699bf66f071cb Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 22:39:18 +0200 Subject: [PATCH 095/119] Add tests for 'git log' options --- tests/unit_tests/pygit/options/test_log_options.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/unit_tests/pygit/options/test_log_options.py diff --git a/tests/unit_tests/pygit/options/test_log_options.py b/tests/unit_tests/pygit/options/test_log_options.py new file mode 100644 index 0000000..873283e --- /dev/null +++ b/tests/unit_tests/pygit/options/test_log_options.py @@ -0,0 +1,10 @@ +from pygit.options.testing_utils import CommandDefinitionsTestingUtil +from sources.options.log_options import LogCommandDefinitions + + +class TestLogCommandDefinitions: + def test_command_positive(self): + CommandDefinitionsTestingUtil.test_command_positive(LogCommandDefinitions, 'log') + + def test_definitions_positive(self): + CommandDefinitionsTestingUtil.test_definitions_positive(LogCommandDefinitions) From da3d588c8c752992d2d715086e90bb11ee8e42c9 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 22:39:28 +0200 Subject: [PATCH 096/119] Add tests for 'git mv' options --- tests/unit_tests/pygit/options/test_mv_options.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/unit_tests/pygit/options/test_mv_options.py diff --git a/tests/unit_tests/pygit/options/test_mv_options.py b/tests/unit_tests/pygit/options/test_mv_options.py new file mode 100644 index 0000000..b45b37e --- /dev/null +++ b/tests/unit_tests/pygit/options/test_mv_options.py @@ -0,0 +1,10 @@ +from pygit.options.testing_utils import CommandDefinitionsTestingUtil +from sources.options.mv_options import MvCommandDefinitions + + +class TestMvCommandDefinitions: + def test_command_positive(self): + CommandDefinitionsTestingUtil.test_command_positive(MvCommandDefinitions, 'mv') + + def test_definitions_positive(self): + CommandDefinitionsTestingUtil.test_definitions_positive(MvCommandDefinitions) From a0932e969ad67615fd793829d9b3d5c34a529347 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 22:40:33 +0200 Subject: [PATCH 097/119] Add tests for 'git pull' options --- tests/unit_tests/pygit/options/test_pull_options.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/unit_tests/pygit/options/test_pull_options.py diff --git a/tests/unit_tests/pygit/options/test_pull_options.py b/tests/unit_tests/pygit/options/test_pull_options.py new file mode 100644 index 0000000..8c18cd8 --- /dev/null +++ b/tests/unit_tests/pygit/options/test_pull_options.py @@ -0,0 +1,10 @@ +from pygit.options.testing_utils import CommandDefinitionsTestingUtil +from sources.options.pull_options import PullCommandDefinitions + + +class TestPullCommandDefinitions: + def test_command_positive(self): + CommandDefinitionsTestingUtil.test_command_positive(PullCommandDefinitions, 'pull') + + def test_definitions_positive(self): + CommandDefinitionsTestingUtil.test_definitions_positive(PullCommandDefinitions) From f98454de16a85708499df62437f4347c5bdbb8a0 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 22:40:42 +0200 Subject: [PATCH 098/119] Add tests for 'git push' options --- tests/unit_tests/pygit/options/test_push_options.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/unit_tests/pygit/options/test_push_options.py diff --git a/tests/unit_tests/pygit/options/test_push_options.py b/tests/unit_tests/pygit/options/test_push_options.py new file mode 100644 index 0000000..f015cba --- /dev/null +++ b/tests/unit_tests/pygit/options/test_push_options.py @@ -0,0 +1,10 @@ +from pygit.options.testing_utils import CommandDefinitionsTestingUtil +from sources.options.push_options import PushCommandDefinitions + + +class TestPushCommandDefinitions: + def test_command_positive(self): + CommandDefinitionsTestingUtil.test_command_positive(PushCommandDefinitions, 'push') + + def test_definitions_positive(self): + CommandDefinitionsTestingUtil.test_definitions_positive(PushCommandDefinitions) From 09d7b88b92c328b4faba18e1f72dd63dae7f7301 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 22:40:53 +0200 Subject: [PATCH 099/119] Add tests for 'git rm' options --- tests/unit_tests/pygit/options/test_rm_options.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/unit_tests/pygit/options/test_rm_options.py diff --git a/tests/unit_tests/pygit/options/test_rm_options.py b/tests/unit_tests/pygit/options/test_rm_options.py new file mode 100644 index 0000000..2c364d7 --- /dev/null +++ b/tests/unit_tests/pygit/options/test_rm_options.py @@ -0,0 +1,10 @@ +from pygit.options.testing_utils import CommandDefinitionsTestingUtil +from sources.options.rm_options import RmCommandDefinitions + + +class TestRmCommandDefinitions: + def test_command_positive(self): + CommandDefinitionsTestingUtil.test_command_positive(RmCommandDefinitions, 'rm') + + def test_definitions_positive(self): + CommandDefinitionsTestingUtil.test_definitions_positive(RmCommandDefinitions) From 7ff1193f790bca8177b5f13fb6a95f0f3cd2a87a Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 22:41:08 +0200 Subject: [PATCH 100/119] Add tests for 'git show' options --- tests/unit_tests/pygit/options/test_show_options.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/unit_tests/pygit/options/test_show_options.py diff --git a/tests/unit_tests/pygit/options/test_show_options.py b/tests/unit_tests/pygit/options/test_show_options.py new file mode 100644 index 0000000..bfa098d --- /dev/null +++ b/tests/unit_tests/pygit/options/test_show_options.py @@ -0,0 +1,10 @@ +from pygit.options.testing_utils import CommandDefinitionsTestingUtil +from sources.options.show_options import ShowCommandDefinitions + + +class TestShowCommandDefinitions: + def test_command_positive(self): + CommandDefinitionsTestingUtil.test_command_positive(ShowCommandDefinitions, 'show') + + def test_definitions_positive(self): + CommandDefinitionsTestingUtil.test_definitions_positive(ShowCommandDefinitions) From 316b850109d72cdbd82d38d29ec1e6912ff9ecfa Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 22:51:49 +0200 Subject: [PATCH 101/119] Add tests documentation for 'git add' options --- tests/unit_tests/pygit/options/test_add_options.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit_tests/pygit/options/test_add_options.py b/tests/unit_tests/pygit/options/test_add_options.py index 6169def..aad8340 100644 --- a/tests/unit_tests/pygit/options/test_add_options.py +++ b/tests/unit_tests/pygit/options/test_add_options.py @@ -1,10 +1,22 @@ +""" +Module contains unit tests for 'git add' options definition class. +""" from pygit.options.testing_utils import CommandDefinitionsTestingUtil from sources.options.add_options import AddCommandDefinitions class TestAddCommandDefinitions: + """ + Class contains unit tests for 'AddCommandDefinitions' class. + """ def test_command_positive(self): + """ + Method tests that command in the 'AddCommandDefinitions' class has been properly set. + """ CommandDefinitionsTestingUtil.test_command_positive(AddCommandDefinitions, 'add') def test_definitions_positive(self): + """ + Method tests that definitions have been created for all options from the 'AddCommandDefinitions' class. + """ CommandDefinitionsTestingUtil.test_definitions_positive(AddCommandDefinitions) From abad5488245b3599cc8eee812ca696f545392190 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 22:54:26 +0200 Subject: [PATCH 102/119] Add tests documentation for 'git checkout' options --- .../pygit/options/test_checkout_options.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit_tests/pygit/options/test_checkout_options.py b/tests/unit_tests/pygit/options/test_checkout_options.py index 7233329..65c3dcd 100644 --- a/tests/unit_tests/pygit/options/test_checkout_options.py +++ b/tests/unit_tests/pygit/options/test_checkout_options.py @@ -1,10 +1,22 @@ +""" +Module contains unit tests for 'git checkout' options definition class. +""" from pygit.options.testing_utils import CommandDefinitionsTestingUtil from sources.options.checkout_options import CheckoutCommandDefinitions class TestCheckoutCommandDefinitions: + """ + Class contains unit tests for 'CheckoutCommandDefinitions' class. + """ def test_command_positive(self): + """ + Method tests that command in the 'CheckoutCommandDefinitions' class has been properly set. + """ CommandDefinitionsTestingUtil.test_command_positive(CheckoutCommandDefinitions, 'checkout') def test_definitions_positive(self): + """ + Method tests that definitions have been created for all options from the 'CheckoutCommandDefinitions' class. + """ CommandDefinitionsTestingUtil.test_definitions_positive(CheckoutCommandDefinitions) From a85132e57d95757c5f910e8ebb930615389e3d6e Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 22:57:11 +0200 Subject: [PATCH 103/119] Add tests documentation for 'git clone' options --- tests/unit_tests/pygit/options/test_clone_options.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit_tests/pygit/options/test_clone_options.py b/tests/unit_tests/pygit/options/test_clone_options.py index 40b5beb..95cc1c9 100644 --- a/tests/unit_tests/pygit/options/test_clone_options.py +++ b/tests/unit_tests/pygit/options/test_clone_options.py @@ -1,10 +1,22 @@ +""" +Module contains unit tests for 'git clone' options definition class. +""" from pygit.options.testing_utils import CommandDefinitionsTestingUtil from sources.options.clone_options import CloneCommandDefinitions class TestCloneCommandDefinitions: + """ + Class contains unit tests for 'CloneCommandDefinitions' class. + """ def test_command_positive(self): + """ + Method tests that command in the 'CloneCommandDefinitions' class has been properly set. + """ CommandDefinitionsTestingUtil.test_command_positive(CloneCommandDefinitions, 'clone') def test_definitions_positive(self): + """ + Method tests that definitions have been created for all options from the 'CloneCommandDefinitions' class. + """ CommandDefinitionsTestingUtil.test_definitions_positive(CloneCommandDefinitions) From 216870fc9d010615d9e58cd35014b3d7a42d10bc Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 23:00:03 +0200 Subject: [PATCH 104/119] Add tests documentation for 'git commit' options --- .../unit_tests/pygit/options/test_commit_options.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit_tests/pygit/options/test_commit_options.py b/tests/unit_tests/pygit/options/test_commit_options.py index 778b283..1da5841 100644 --- a/tests/unit_tests/pygit/options/test_commit_options.py +++ b/tests/unit_tests/pygit/options/test_commit_options.py @@ -1,10 +1,22 @@ +""" +Module contains unit tests for 'git commit' options definition class. +""" from pygit.options.testing_utils import CommandDefinitionsTestingUtil from sources.options.commit_options import CommitCommandDefinitions class TestCommitCommandDefinitions: + """ + Class contains unit tests for 'CommitCommandDefinitions' class. + """ def test_command_positive(self): + """ + Method tests that command in the 'CommitCommandDefinitions' class has been properly set. + """ CommandDefinitionsTestingUtil.test_command_positive(CommitCommandDefinitions, 'commit') def test_definitions_positive(self): + """ + Method tests that definitions have been created for all options from the 'CommitCommandDefinitions' class. + """ CommandDefinitionsTestingUtil.test_definitions_positive(CommitCommandDefinitions) From b20bdb4563a7ff2aca89ab2081353d25eb08bc05 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 23:01:35 +0200 Subject: [PATCH 105/119] Add tests documentation for 'git config' options --- .../unit_tests/pygit/options/test_config_options.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit_tests/pygit/options/test_config_options.py b/tests/unit_tests/pygit/options/test_config_options.py index 85e12bc..d62c4cd 100644 --- a/tests/unit_tests/pygit/options/test_config_options.py +++ b/tests/unit_tests/pygit/options/test_config_options.py @@ -1,10 +1,22 @@ +""" +Module contains unit tests for 'git config' options definition class. +""" from pygit.options.testing_utils import CommandDefinitionsTestingUtil from sources.options.config_options import ConfigCommandDefinitions class TestConfigCommandDefinitions: + """ + Class contains unit tests for 'ConfigCommandDefinitions' class. + """ def test_command_positive(self): + """ + Method tests that command in the 'ConfigCommandDefinitions' class has been properly set. + """ CommandDefinitionsTestingUtil.test_command_positive(ConfigCommandDefinitions, 'config') def test_definitions_positive(self): + """ + Method tests that definitions have been created for all options from the 'ConfigCommandDefinitions' class. + """ CommandDefinitionsTestingUtil.test_definitions_positive(ConfigCommandDefinitions) From 3e542de87af2783d7f5ad89cb24a980844fb387d Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 23:03:49 +0200 Subject: [PATCH 106/119] Add tests documentation for 'git for-each-ref' options --- .../pygit/options/test_for_each_ref_options.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit_tests/pygit/options/test_for_each_ref_options.py b/tests/unit_tests/pygit/options/test_for_each_ref_options.py index da95f1b..8942126 100644 --- a/tests/unit_tests/pygit/options/test_for_each_ref_options.py +++ b/tests/unit_tests/pygit/options/test_for_each_ref_options.py @@ -1,10 +1,22 @@ +""" +Module contains unit tests for 'git for-each-ref' options definition class. +""" from pygit.options.testing_utils import CommandDefinitionsTestingUtil from sources.options.for_each_ref_options import ForEachRefCommandDefinitions class TestForEachRefCommandDefinitions: + """ + Class contains unit tests for 'ForEachRefCommandDefinitions' class. + """ def test_command_positive(self): + """ + Method tests that command in the 'ForEachRefCommandDefinitions' class has been properly set. + """ CommandDefinitionsTestingUtil.test_command_positive(ForEachRefCommandDefinitions, 'for-each-ref') def test_definitions_positive(self): + """ + Method tests that definitions have been created for all options from the 'ForEachRefCommandDefinitions' class. + """ CommandDefinitionsTestingUtil.test_definitions_positive(ForEachRefCommandDefinitions) From afb12204447bb8ee2b95a9234435dec91dc4ce45 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 23:07:19 +0200 Subject: [PATCH 107/119] Add tests documentation for 'git init' options --- tests/unit_tests/pygit/options/test_init_options.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit_tests/pygit/options/test_init_options.py b/tests/unit_tests/pygit/options/test_init_options.py index cd98b9d..88532d3 100644 --- a/tests/unit_tests/pygit/options/test_init_options.py +++ b/tests/unit_tests/pygit/options/test_init_options.py @@ -1,10 +1,22 @@ +""" +Module contains unit tests for 'git init' options definition class. +""" from pygit.options.testing_utils import CommandDefinitionsTestingUtil from sources.options.init_options import InitCommandDefinitions class TestInitCommandDefinitions: + """ + Class contains unit tests for 'InitCommandDefinitions' class. + """ def test_command_positive(self): + """ + Method tests that command in the 'InitCommandDefinitions' class has been properly set. + """ CommandDefinitionsTestingUtil.test_command_positive(InitCommandDefinitions, 'init') def test_definitions_positive(self): + """ + Method tests that definitions have been created for all options from the 'InitCommandDefinitions' class. + """ CommandDefinitionsTestingUtil.test_definitions_positive(InitCommandDefinitions) From 7f8d14a75e3cc5912059b4ffae9730830a74de7e Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 23:08:15 +0200 Subject: [PATCH 108/119] Add tests documentation for 'git mv' options --- tests/unit_tests/pygit/options/test_mv_options.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit_tests/pygit/options/test_mv_options.py b/tests/unit_tests/pygit/options/test_mv_options.py index b45b37e..a4a3733 100644 --- a/tests/unit_tests/pygit/options/test_mv_options.py +++ b/tests/unit_tests/pygit/options/test_mv_options.py @@ -1,10 +1,22 @@ +""" +Module contains unit tests for 'git mv' options definition class. +""" from pygit.options.testing_utils import CommandDefinitionsTestingUtil from sources.options.mv_options import MvCommandDefinitions class TestMvCommandDefinitions: + """ + Class contains unit tests for 'MvCommandDefinitions' class. + """ def test_command_positive(self): + """ + Method tests that command in the 'MvCommandDefinitions' class has been properly set. + """ CommandDefinitionsTestingUtil.test_command_positive(MvCommandDefinitions, 'mv') def test_definitions_positive(self): + """ + Method tests that definitions have been created for all options from the 'MvCommandDefinitions' class. + """ CommandDefinitionsTestingUtil.test_definitions_positive(MvCommandDefinitions) From 75828fbf8292ea008108e493b288ac30af51ea7b Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 23:09:24 +0200 Subject: [PATCH 109/119] Add tests documentation for 'git pull' options --- tests/unit_tests/pygit/options/test_pull_options.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit_tests/pygit/options/test_pull_options.py b/tests/unit_tests/pygit/options/test_pull_options.py index 8c18cd8..e94094f 100644 --- a/tests/unit_tests/pygit/options/test_pull_options.py +++ b/tests/unit_tests/pygit/options/test_pull_options.py @@ -1,10 +1,22 @@ +""" +Module contains unit tests for 'git pull' options definition class. +""" from pygit.options.testing_utils import CommandDefinitionsTestingUtil from sources.options.pull_options import PullCommandDefinitions class TestPullCommandDefinitions: + """ + Class contains unit tests for 'PullCommandDefinitions' class. + """ def test_command_positive(self): + """ + Method tests that command in the 'PullCommandDefinitions' class has been properly set. + """ CommandDefinitionsTestingUtil.test_command_positive(PullCommandDefinitions, 'pull') def test_definitions_positive(self): + """ + Method tests that definitions have been created for all options from the 'PullCommandDefinitions' class. + """ CommandDefinitionsTestingUtil.test_definitions_positive(PullCommandDefinitions) From 1ab87baf5412de853be1af1eb2210ac7f5d4fab9 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 23:10:45 +0200 Subject: [PATCH 110/119] Add tests documentation for 'git push' options --- tests/unit_tests/pygit/options/test_push_options.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit_tests/pygit/options/test_push_options.py b/tests/unit_tests/pygit/options/test_push_options.py index f015cba..87d86ec 100644 --- a/tests/unit_tests/pygit/options/test_push_options.py +++ b/tests/unit_tests/pygit/options/test_push_options.py @@ -1,10 +1,22 @@ +""" +Module contains unit tests for 'git push' options definition class. +""" from pygit.options.testing_utils import CommandDefinitionsTestingUtil from sources.options.push_options import PushCommandDefinitions class TestPushCommandDefinitions: + """ + Class contains unit tests for 'PushCommandDefinitions' class. + """ def test_command_positive(self): + """ + Method tests that command in the 'PushCommandDefinitions' class has been properly set. + """ CommandDefinitionsTestingUtil.test_command_positive(PushCommandDefinitions, 'push') def test_definitions_positive(self): + """ + Method tests that definitions have been created for all options from the 'PushCommandDefinitions' class. + """ CommandDefinitionsTestingUtil.test_definitions_positive(PushCommandDefinitions) From 3db7aec3d755a2157fd3a12154d317cde53b70d6 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 23:12:00 +0200 Subject: [PATCH 111/119] Add tests documentation for 'git rm' options --- tests/unit_tests/pygit/options/test_rm_options.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit_tests/pygit/options/test_rm_options.py b/tests/unit_tests/pygit/options/test_rm_options.py index 2c364d7..a34db3b 100644 --- a/tests/unit_tests/pygit/options/test_rm_options.py +++ b/tests/unit_tests/pygit/options/test_rm_options.py @@ -1,10 +1,22 @@ +""" +Module contains unit tests for 'git rm' options definition class. +""" from pygit.options.testing_utils import CommandDefinitionsTestingUtil from sources.options.rm_options import RmCommandDefinitions class TestRmCommandDefinitions: + """ + Class contains unit tests for 'RmCommandDefinitions' class. + """ def test_command_positive(self): + """ + Method tests that command in the 'RmCommandDefinitions' class has been properly set. + """ CommandDefinitionsTestingUtil.test_command_positive(RmCommandDefinitions, 'rm') def test_definitions_positive(self): + """ + Method tests that definitions have been created for all options from the 'RmCommandDefinitions' class. + """ CommandDefinitionsTestingUtil.test_definitions_positive(RmCommandDefinitions) From 00bf5d2c5c3bc12c01561e5cee8e38a3b6a36144 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 23:13:13 +0200 Subject: [PATCH 112/119] Add tests documentation for 'git show' options --- tests/unit_tests/pygit/options/test_show_options.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit_tests/pygit/options/test_show_options.py b/tests/unit_tests/pygit/options/test_show_options.py index bfa098d..344ee7b 100644 --- a/tests/unit_tests/pygit/options/test_show_options.py +++ b/tests/unit_tests/pygit/options/test_show_options.py @@ -1,10 +1,22 @@ +""" +Module contains unit tests for 'git show' options definition class. +""" from pygit.options.testing_utils import CommandDefinitionsTestingUtil from sources.options.show_options import ShowCommandDefinitions class TestShowCommandDefinitions: + """ + Class contains unit tests for 'ShowCommandDefinitions' class. + """ def test_command_positive(self): + """ + Method tests that command in the 'ShowCommandDefinitions' class has been properly set. + """ CommandDefinitionsTestingUtil.test_command_positive(ShowCommandDefinitions, 'show') def test_definitions_positive(self): + """ + Method tests that definitions have been created for all options from the 'ShowCommandDefinitions' class. + """ CommandDefinitionsTestingUtil.test_definitions_positive(ShowCommandDefinitions) From 47316fee95b741a07d705bc1b472d8325bb4b590 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Sat, 28 Oct 2023 23:17:32 +0200 Subject: [PATCH 113/119] Add tests documentation for options utils class --- tests/unit_tests/pygit/options/testing_utils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit_tests/pygit/options/testing_utils.py b/tests/unit_tests/pygit/options/testing_utils.py index cf2296e..2912af7 100644 --- a/tests/unit_tests/pygit/options/testing_utils.py +++ b/tests/unit_tests/pygit/options/testing_utils.py @@ -1,16 +1,28 @@ +""" +Module contains testing utilities for options definitions classes. +""" from typing import Type from sources.options.options import GitCommand class CommandDefinitionsTestingUtil: + """ + Class contains unit tests for command definitions class. + """ @staticmethod def test_command_positive(class_type: Type[GitCommand], expected_name: str): + """ + Method tests that command in the command definitions class has been properly set. + """ command = class_type() assert command.command_name == expected_name @staticmethod def test_definitions_positive(class_type: Type[GitCommand]): + """ + Method tests that definitions have been created for all options from the command definitions class. + """ command = class_type() missing_options = [] for option in command.Options: From cf40b5273a960af8fc3ddeec80ec2805d73db960 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Tue, 26 Dec 2023 20:44:19 +0100 Subject: [PATCH 114/119] Add tests documentation for the tests for options module --- .../unit_tests/pygit/options/test_options.py | 190 ++++++++++++++---- 1 file changed, 152 insertions(+), 38 deletions(-) diff --git a/tests/unit_tests/pygit/options/test_options.py b/tests/unit_tests/pygit/options/test_options.py index ed0a287..e99b4db 100644 --- a/tests/unit_tests/pygit/options/test_options.py +++ b/tests/unit_tests/pygit/options/test_options.py @@ -1,3 +1,6 @@ +""" +Module contains unit tests for base options classes of git. +""" from typing import Any, Type import pytest @@ -9,7 +12,13 @@ class DemoCommand(GitCommand): + """ + Class inherits 'GitCommand' class and defines 'dummy' git command for testing purposes. + """ class Options(CommandOptions): + """ + Class inherits 'CommandOptions' class and defines 'dummy' options for the 'dummy' git command. + """ OPTION = GitOptionNameAliases(aliases=[ GitOptionNameAlias(name='option', short_option=False), GitOptionNameAlias(name='o', short_option=True), @@ -36,9 +45,23 @@ def __init__(self): class TestGitOptionNameAliases: + """ + Class contains unit tests for 'GitOptionNameAliases' class. + """ @pytest.mark.parametrize('name,short_option,length', [('option', False, 1), ('o', True, 1)], ids=('Get long alias', 'Get short alias')) def test_get_aliases_positive(self, name: str, short_option: bool, length: int): + """ + Method tests that 'get_aliases' method from 'GitOptionNameAliases' class returns correct values for short and + long aliases. + + :param name: The name of the option. + :type name: str + :param short_option: Whether the tested option is short or long. + :type short_option: bool + :param length: The number fo the available aliases for this option. + :type length: int + """ alias = GitOptionNameAlias(name=name, short_option=short_option) option_aliases = GitOptionNameAliases(aliases=[ alias, @@ -49,6 +72,10 @@ def test_get_aliases_positive(self, name: str, short_option: bool, length: int): # noinspection PyTypeChecker def test_get_aliases_negative(self): + """ + Method tests that 'get_aliases' method from 'GitOptionNameAliases' class throws an exception when an invalid + alias is on the list. + """ alias = 'option' try: option_aliases = GitOptionNameAliases(aliases=[ @@ -63,6 +90,15 @@ def test_get_aliases_negative(self): @pytest.mark.parametrize('name,short_option', [('option', False), ('o', True)], ids=("Without short options", 'With short options')) def test_has_short_aliases_positive(self, name: str, short_option: bool): + """ + Method tests that 'has_short_aliases' method from 'GitOptionNameAliases' class returns True, if there is at + least one short alias for this option and otherwise returns False. + + :param name: The name of the alias. + :type name: str + :param short_option: Whether this alias is short or not. + :type short_option: bool + """ alias = GitOptionNameAlias(name=name, short_option=short_option) option_aliases = GitOptionNameAliases(aliases=[ alias, @@ -71,8 +107,17 @@ def test_has_short_aliases_positive(self, name: str, short_option: bool): assert has_short_aliases == short_option @pytest.mark.parametrize('name,short_option', [('option', False), ('o', True)], - ids=("Without short options", 'With short options')) + ids=("With long options", 'Without long options')) def test_has_long_aliases_positive(self, name: str, short_option: bool): + """ + Method tests that 'has_long_aliases' method from 'GitOptionNameAliases' class returns True, if there is at + least one long alias for this option and otherwise returns False. + + :param name: The name of the alias. + :type name: str + :param short_option: Whether this alias is short or not. + :type short_option: bool + """ alias = GitOptionNameAlias(name=name, short_option=short_option) option_aliases = GitOptionNameAliases(aliases=[ alias, @@ -83,6 +128,15 @@ def test_has_long_aliases_positive(self, name: str, short_option: bool): @pytest.mark.parametrize('name,exists', [('option', True), ('o', True), ('another_option', False)], ids=("Long option exists", 'Short option exists', 'Missing option')) def test_exists_positive(self, name: str, exists: bool): + """ + Method tests that 'exists' method from 'GitOptionNameAliases' class returns True, if there is an alias with + provided name. + + :param name: The name of the alias. + :type name: str + :param exists: Whether this alias exists or not. + :type exists: bool + """ option_aliases = GitOptionNameAliases(aliases=[ GitOptionNameAlias(name='option', short_option=False), GitOptionNameAlias(name='o', short_option=True), @@ -91,6 +145,9 @@ def test_exists_positive(self, name: str, exists: bool): assert option_exists == exists def test_get_names_positive(self): + """ + Method tests that 'get_names' method from 'GitOptionNameAliases' class returns list of aliases for the option. + """ option_aliases = GitOptionNameAliases(aliases=[ GitOptionNameAlias(name='option', short_option=False), GitOptionNameAlias(name='o', short_option=True), @@ -98,51 +155,86 @@ def test_get_names_positive(self): names = option_aliases.get_names() assert names == ['option', 'o'] - class TestGitOptionDefinition: - @pytest.mark.parametrize('name,value,expected', [ - ('option', True, True), ('option', 'value', True), ('o', True, True), ('o', 123, False), - ('another_option', 'value', False) - ], ids=('Correct long boolean option', 'Correct long string option', 'Correct short boolean option', - 'Incorrect short integer option', 'Incorrect another string option')) - def test_compare_with_option_positive(self, name: str, value: Any, expected: bool): - name_aliases = GitOptionNameAliases(aliases=[ - GitOptionNameAlias(name='option', short_option=False), - GitOptionNameAlias(name='o', short_option=True), - ]) - definition = GitOptionDefinition(name_aliases=name_aliases, type=(str, bool)) - option = GitOption(name=name, value=value) - comparison_result = definition.compare_with_option(option) - assert comparison_result == expected - - class TestGitCommand: - @pytest.mark.parametrize('name,value,expected', [ - ('option', True, True), ('option', 'value', True), ('o', True, True), ('o', 123, False), - ('another_option', 'value', False) - ], ids=("Definition exists for long boolean option", "Definition exists for long string option", - "Definition exists for short boolean option", "Definition doesn't exist for short integer option", - "Definition doesn't exist for another string option")) - def test_get_definition_positive(self, name: str, value: Any, expected: bool): - option = GitOption(name=name, value=value) - command = DemoCommand() - definition = command.get_definition(option) - if expected: - assert definition is not None - else: - assert definition is None + +class TestGitOptionDefinition: + """ + Class contains unit tests for 'GitOptionDefinition' class. + """ + @pytest.mark.parametrize('name,value,expected', [ + ('option', True, True), ('option', 'value', True), ('o', True, True), ('o', 123, False), + ('another_option', 'value', False) + ], ids=('Correct long boolean option', 'Correct long string option', 'Correct short boolean option', + 'Incorrect short integer option', 'Incorrect another string option')) + def test_compare_with_option_positive(self, name: str, value: Any, expected: bool): + """ + Method tests that 'compare_with_option' method from class 'GitOptionDefinition' returns True if 'GitOption' is + corresponding to 'GitOptionDefinition', otherwise returns False. + + :param name: The name of the option. + :type name: str + :param value: The value of the option. + :type value: Any + :param expected: The expected value that shall be returned by 'compare_with_option' method. + :type expected: bool + """ + name_aliases = GitOptionNameAliases(aliases=[ + GitOptionNameAlias(name='option', short_option=False), + GitOptionNameAlias(name='o', short_option=True), + ]) + definition = GitOptionDefinition(name_aliases=name_aliases, type=(str, bool)) + option = GitOption(name=name, value=value) + comparison_result = definition.compare_with_option(option) + assert comparison_result == expected + + +class TestGitCommand: + """ + Class contains unit tests for 'GitCommand' class. + """ + @pytest.mark.parametrize("name,value,has_definition", [ + ('option', True, True), ('option', 'value', True), ('o', True, True), ('o', 123, False), + ('another_option', 'value', False) + ], ids=("Definition exists for long boolean option", "Definition exists for long string option", + "Definition exists for short boolean option", "Definition doesn't exist for short integer option", + "Definition doesn't exist for long string option")) + def test_get_definition_positive(self, name: str, value: Any, has_definition: bool): + """ + Method tests that 'get_definition' method from 'GitCommand' class returns definition for the given 'GitOption' + if it exists, otherwise returns None. + + :param name: The name of the option. + :type name: str + :param value: The value of the option. + :type value: Any + :param has_definition: If expected is True, then non None value is expected to be returned by 'get_definition' + method, otherwise None is expected. + :type has_definition: bool + """ + option = GitOption(name=name, value=value) + command = DemoCommand() + definition = command.get_definition(option) + if has_definition: + assert definition is not None + else: + assert definition is None def test_validate_positive(self): + """ + Method tests that 'validate' method from 'GitCommand' class validates the options for the 'GitCommand'. It shall + not raise an exception for the correct options. + """ options = [ DemoCommand.Options.SHORT_OPTION.create_option(True), DemoCommand.Options.LONG_OPTION.create_option('first'), DemoCommand.Options.POSITIONAL_OPTION.create_option('value'), ] command = DemoCommand() - fired = False + raise_exception = False try: command.validate(options) except GitException: - fired = True - assert not fired + raise_exception = True + assert not raise_exception @pytest.mark.parametrize('name,value,expected_exception', [ ('missing_option', True, GitMissingDefinitionException), @@ -150,16 +242,38 @@ def test_validate_positive(self): ('option', True, GitMissingRequiredOptionsException), ], ids=("Missing option exception", "Incorrect option value exception", "Missing required option exception")) def test_validate_negative(self, name: str, value: Any, expected_exception: Type[GitException]): + """ + Method tests that 'validate' method from 'GitCommand' class validates the options for the 'GitCommand'. It shall + raise specified exceptions for the incorrect options. + + Tests the following exceptions: + + - GitMissingDefinitionException: When option is not on the list of defined options for the 'GitCommand', it also + could be that option has different type than defined. + - GitIncorrectOptionValueException: When the value for the option is not the choices list. + - GitMissingRequiredOptionsException: When option that is required is not present. + + :param name: The name of the option to validate. + :type name: str + :param value: The value for the option. + :type value: Any + :param expected_exception: The exception type that shall be thrown for this option. + :type expected_exception: Type[GitException] + """ option = GitOption(name=name, value=value) command = DemoCommand() - fired = False + raise_exception = False try: command.validate(option) except expected_exception: - fired = True - assert fired + raise_exception = True + assert raise_exception def test_transform_to_command_positive(self): + """ + Method tests that 'transform_to_command' method from 'GitOption' class is able to correctly transform + 'GitOption' list to raw command arguments. + """ options = [ DemoCommand.Options.SHORT_OPTION.create_option(True), DemoCommand.Options.LONG_OPTION.create_option('first'), From 49eb2f46e31673597475a9f11b9dcf40b91762ef Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Tue, 26 Dec 2023 23:28:43 +0100 Subject: [PATCH 115/119] Update docstring commit for validate_positional_list --- sources/options/options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sources/options/options.py b/sources/options/options.py index 2d7c3dd..8fdccb7 100644 --- a/sources/options/options.py +++ b/sources/options/options.py @@ -189,8 +189,8 @@ def validate_positional_list(self) -> Tuple[bool, Optional[str]]: Method checks that there are no positional options of the list type that are defined not on the last position. :return: Tuple with boolean and optional string, boolean contains True if everything is correct and False if - there is an incorrect option. Optional string contains name of the definition which failed the check, where - if all positional options passed the check, then None will be returned. + there is an incorrect option. Optional string contains all aliases for the definition which failed the + check, where if all positional options passed the check, then None will be returned. """ positions = [definition.position for definition in self.definitions if definition.positional] positions = sorted(positions) From 009be44931b1c1031ebe763fb3278f0ecf4207f8 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Tue, 26 Dec 2023 23:29:45 +0100 Subject: [PATCH 116/119] Add GitCommand generator and tests for validate_positional_list method --- .../unit_tests/pygit/options/test_options.py | 38 ++++++++++++++++- .../unit_tests/pygit/options/testing_utils.py | 41 ++++++++++++++++++- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/tests/unit_tests/pygit/options/test_options.py b/tests/unit_tests/pygit/options/test_options.py index e99b4db..7860331 100644 --- a/tests/unit_tests/pygit/options/test_options.py +++ b/tests/unit_tests/pygit/options/test_options.py @@ -1,10 +1,11 @@ """ Module contains unit tests for base options classes of git. """ -from typing import Any, Type +from typing import Any, Type, Optional, List, Tuple import pytest +from pygit.options.testing_utils import GitCommandGenerator from sources.exceptions import GitException, GitMissingDefinitionException, GitIncorrectOptionValueException, \ GitMissingRequiredOptionsException from sources.options.options import GitOptionNameAliases, GitOptionNameAlias, GitOptionDefinition, GitOption, \ @@ -15,6 +16,7 @@ class DemoCommand(GitCommand): """ Class inherits 'GitCommand' class and defines 'dummy' git command for testing purposes. """ + class Options(CommandOptions): """ Class inherits 'CommandOptions' class and defines 'dummy' options for the 'dummy' git command. @@ -48,6 +50,7 @@ class TestGitOptionNameAliases: """ Class contains unit tests for 'GitOptionNameAliases' class. """ + @pytest.mark.parametrize('name,short_option,length', [('option', False, 1), ('o', True, 1)], ids=('Get long alias', 'Get short alias')) def test_get_aliases_positive(self, name: str, short_option: bool, length: int): @@ -160,6 +163,7 @@ class TestGitOptionDefinition: """ Class contains unit tests for 'GitOptionDefinition' class. """ + @pytest.mark.parametrize('name,value,expected', [ ('option', True, True), ('option', 'value', True), ('o', True, True), ('o', 123, False), ('another_option', 'value', False) @@ -191,6 +195,7 @@ class TestGitCommand: """ Class contains unit tests for 'GitCommand' class. """ + @pytest.mark.parametrize("name,value,has_definition", [ ('option', True, True), ('option', 'value', True), ('o', True, True), ('o', 123, False), ('another_option', 'value', False) @@ -269,9 +274,38 @@ def test_validate_negative(self, name: str, value: Any, expected_exception: Type raise_exception = True assert raise_exception + @pytest.mark.parametrize('positional_definitions,expected', [ + ([(('option',), str), (('another_option',), list)], (True, None)), + ([(('option',), list), (('another_option',), str)], (False, '[option]')), + ([(('option', 'o'), list), (('another_option',), str)], (False, '[option, o]')), + ], ids=("Correct positional option", "Incorrect positional option with one alias", + "Incorrect positional option with two aliases")) + def test_validate_positional_list_positive(self, positional_definitions: List[Tuple[Tuple[str], type]], + expected: Tuple[bool, Optional[str]]): + """ + Method tests that 'validate_positional_list' method from 'GitCommand' class returns Tuple[status, incorrect + options] when positional arguments of the type list is defined not on the last position. + + :param positional_definitions: The configuration of the positional arguments. + :type positional_definitions: List[Tuple[Tuple[str], type]] + :param expected: The expected value that shall be returned by method. + :type expected: Tuple[bool, Optional[str]] + """ + definitions = [] + for definition in positional_definitions: + aliases = [] + for alias in definition[0]: + aliases.append({'name': alias, 'short-option': not len(alias) > 1}) + definitions.append({'aliases': aliases, 'positional': True, 'type': definition[1]}) + command = GitCommandGenerator.from_dict({ + 'command': 'demo-command', + 'definitions': definitions + }) + assert command.validate_positional_list() == expected + def test_transform_to_command_positive(self): """ - Method tests that 'transform_to_command' method from 'GitOption' class is able to correctly transform + Method tests that 'transform_to_command' method from 'GitCommand' class is able to correctly transform 'GitOption' list to raw command arguments. """ options = [ diff --git a/tests/unit_tests/pygit/options/testing_utils.py b/tests/unit_tests/pygit/options/testing_utils.py index 2912af7..39019f2 100644 --- a/tests/unit_tests/pygit/options/testing_utils.py +++ b/tests/unit_tests/pygit/options/testing_utils.py @@ -3,7 +3,7 @@ """ from typing import Type -from sources.options.options import GitCommand +from sources.options.options import GitCommand, GitOptionNameAlias, GitOptionNameAliases, GitOptionDefinition class CommandDefinitionsTestingUtil: @@ -33,3 +33,42 @@ def test_definitions_positive(class_type: Type[GitCommand]): if not found: missing_options.append(option) assert len(missing_options) == 0 + + +class GitCommandGenerator: + """ + Class contains methods to generate 'GitCommand' class instance. + """ + @staticmethod + def from_dict(parameters: dict) -> 'GitCommand': + """ + Method that create an instance of 'GitCommand' class based on the provided dictionary. + + :param parameters: Dictionary that is used to create 'GitCommand' class instance. + :type parameters: dict + """ + command = GitCommand(parameters['command']) + definitions = [] + positional_index = 0 + for definition in parameters['definitions']: + aliases = [] + for alias in definition['aliases']: + aliases.append(GitOptionNameAlias(name=alias['name'], short_option=alias['short-option'])) + option_aliases = GitOptionNameAliases(aliases=aliases) + positional = definition.get('positional', False) + if positional: + position = positional_index + positional_index += 1 + else: + position = None + option_definition = GitOptionDefinition(name_aliases=option_aliases, type=definition.get('type', str), + positional=positional, position=position) + required = definition.get('required', None) + if required: + option_definition.required = required + choices = definition.get('choices', None) + if choices: + option_definition.choices = choices + definitions.append(option_definition) + command.definitions = definitions + return command From b81ee6315eaf8d08ddcb7a80e797a811fef17345 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Wed, 27 Dec 2023 04:31:57 +0100 Subject: [PATCH 117/119] Add test for validate_required method from GitCommand and update implementation --- sources/options/options.py | 2 +- .../unit_tests/pygit/options/test_options.py | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/sources/options/options.py b/sources/options/options.py index 8fdccb7..a0b9a75 100644 --- a/sources/options/options.py +++ b/sources/options/options.py @@ -220,7 +220,7 @@ def validate_required(self, options: Union[GitOption, List[GitOption]]) -> Tuple missing_definitions = [] for required_definition in required_definitions: aliases = '|'.join(required_definition.get_names()) - missing_definitions.append(f'({aliases})') + missing_definitions.append(f'{aliases}') return len(required_definitions) == 0, missing_definitions def validate_choices(self, option: GitOption, definition: Optional[GitOptionDefinition] = None) -> bool: diff --git a/tests/unit_tests/pygit/options/test_options.py b/tests/unit_tests/pygit/options/test_options.py index 7860331..6a69421 100644 --- a/tests/unit_tests/pygit/options/test_options.py +++ b/tests/unit_tests/pygit/options/test_options.py @@ -303,6 +303,68 @@ def test_validate_positional_list_positive(self, positional_definitions: List[Tu }) assert command.validate_positional_list() == expected + @pytest.mark.parametrize("option_definitions,expected", [ + (['option', 'long-option', 's'], (True, [])), + (['o', 'long-option', 's'], (True, [])), + (['option', 's'], (False, ['long-option'])), + (['option', 'long-option'], (False, ['s'])), + (['long-option', 's'], (False, ['option|o'])), + (['option'], (False, ['long-option', 's'])), + ], ids=("All required options are present (long alias for two aliases option)", + "All required options are present (short alias for two aliases option)", + "Long option is missing", "Short option is missing", "Required option with two aliases is missing", + "Long option and short option are missing")) + def test_validate_required_positive(self, option_definitions: List[str], expected: Tuple[bool, Optional[str]]): + """ + Method tests that 'validate_required' method from 'GitCommand' class returns Tuple[status, missing options], + where missing options is a list of options that are required, but missing. + + :param option_definitions: List of options. + :type option_definitions: List[str] + :param expected: The expected value that shall be returned by method. + :type expected: Tuple[bool, Optional[str]] + """ + options = [] + for option in option_definitions: + options.append(GitOption(name=option, value='value')) + command = GitCommandGenerator.from_dict({ + 'command': 'demo-command', + 'definitions': [ + { + 'aliases': [ + { + 'name': 'option', + 'short-option': False + }, + { + 'name': 'o', + 'short-option': True + } + ], + 'required': True + }, + { + 'aliases': [ + { + 'name': 'long-option', + 'short-option': False + } + ], + 'required': True + }, + { + 'aliases': [ + { + 'name': 's', + 'short-option': True + } + ], + 'required': True + }, + ] + }) + assert command.validate_required(options) == expected + def test_transform_to_command_positive(self): """ Method tests that 'transform_to_command' method from 'GitCommand' class is able to correctly transform From 69c63c8d7b7ff026d7cd10d1255aded347153298 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Wed, 27 Dec 2023 06:05:00 +0100 Subject: [PATCH 118/119] Add test for validate_choices method from GitCommand and update implementation --- sources/options/options.py | 2 +- .../unit_tests/pygit/options/test_options.py | 51 +++++++++++++++- .../unit_tests/pygit/options/testing_utils.py | 58 +++++++++++-------- 3 files changed, 84 insertions(+), 27 deletions(-) diff --git a/sources/options/options.py b/sources/options/options.py index a0b9a75..cab6086 100644 --- a/sources/options/options.py +++ b/sources/options/options.py @@ -97,7 +97,7 @@ class GitOptionDefinition: type: Union[Type, Tuple] required: bool = field(default=False) positional: bool = field(default=False) - position: int = field(default=None) + position: Optional[int] = field(default=None) choices: Union[list, Type[Enum]] = field(default=None) separator: str = field(default=' ') diff --git a/tests/unit_tests/pygit/options/test_options.py b/tests/unit_tests/pygit/options/test_options.py index 6a69421..c8e3a31 100644 --- a/tests/unit_tests/pygit/options/test_options.py +++ b/tests/unit_tests/pygit/options/test_options.py @@ -5,7 +5,7 @@ import pytest -from pygit.options.testing_utils import GitCommandGenerator +from pygit.options.testing_utils import GitModelGenerator from sources.exceptions import GitException, GitMissingDefinitionException, GitIncorrectOptionValueException, \ GitMissingRequiredOptionsException from sources.options.options import GitOptionNameAliases, GitOptionNameAlias, GitOptionDefinition, GitOption, \ @@ -297,7 +297,7 @@ def test_validate_positional_list_positive(self, positional_definitions: List[Tu for alias in definition[0]: aliases.append({'name': alias, 'short-option': not len(alias) > 1}) definitions.append({'aliases': aliases, 'positional': True, 'type': definition[1]}) - command = GitCommandGenerator.from_dict({ + command = GitModelGenerator.generate_git_command({ 'command': 'demo-command', 'definitions': definitions }) @@ -327,7 +327,7 @@ def test_validate_required_positive(self, option_definitions: List[str], expecte options = [] for option in option_definitions: options.append(GitOption(name=option, value='value')) - command = GitCommandGenerator.from_dict({ + command = GitModelGenerator.generate_git_command({ 'command': 'demo-command', 'definitions': [ { @@ -365,6 +365,51 @@ def test_validate_required_positive(self, option_definitions: List[str], expecte }) assert command.validate_required(options) == expected + @pytest.mark.parametrize("defined_choices,value,with_definition,expected", [ + (None, 'first', True, True), + (None, 'first', False, True), + (['one', 'first', 1], 'first', True, True), + (['one', 'first', 1], 1, False, True), + (['one', 'first', 1], 2, True, False), + (['one', 'first', 1], 2, False, False), + ], ids=("Correct option without defined choices with definition", + "Correct option without defined choices without definition", + "Correct option with defined choices with definition", + "Correct option with defined choices without definition", + "Incorrect option with defined choices with definition", + "Incorrect option with defined choices without definition")) + def test_validate_choices_positive(self, defined_choices: List, value: Any, with_definition: bool, expected: bool): + """ + Method tests that 'validate_choices' method from 'GitCommand' class returns True, if it is correct option, + otherwise return False. + + :param defined_choices: List of choices for the definition. + :type defined_choices: List + :param value: Git option value. + :type value: Any + :param with_definition: If true, then test with definition, otherwise test without definition. + :type with_definition: bool + :param expected: The expected value that shall be returned by method. + :type expected: bool + """ + option = GitOption(name='option', value=value) + command = GitModelGenerator.generate_git_command({ + 'command': 'demo-command', + 'definitions': [ + { + 'aliases': [ + { + 'name': 'option', + 'short-option': False + } + ], + 'choices': defined_choices, + 'type': (str, int) + } + ] + }) + assert command.validate_choices(option, command.definitions[0] if with_definition else None) == expected + def test_transform_to_command_positive(self): """ Method tests that 'transform_to_command' method from 'GitCommand' class is able to correctly transform diff --git a/tests/unit_tests/pygit/options/testing_utils.py b/tests/unit_tests/pygit/options/testing_utils.py index 39019f2..0f14bfd 100644 --- a/tests/unit_tests/pygit/options/testing_utils.py +++ b/tests/unit_tests/pygit/options/testing_utils.py @@ -35,40 +35,52 @@ def test_definitions_positive(class_type: Type[GitCommand]): assert len(missing_options) == 0 -class GitCommandGenerator: +class GitModelGenerator: """ Class contains methods to generate 'GitCommand' class instance. """ @staticmethod - def from_dict(parameters: dict) -> 'GitCommand': + def generate_git_command(configuration: dict) -> 'GitCommand': """ Method that create an instance of 'GitCommand' class based on the provided dictionary. - :param parameters: Dictionary that is used to create 'GitCommand' class instance. - :type parameters: dict + :param configuration: Dictionary that is used to create 'GitCommand' class instance. + :type configuration: dict + :return: Instance of the 'GitCommand' class. """ - command = GitCommand(parameters['command']) + command = GitCommand(configuration['command']) definitions = [] positional_index = 0 - for definition in parameters['definitions']: - aliases = [] - for alias in definition['aliases']: - aliases.append(GitOptionNameAlias(name=alias['name'], short_option=alias['short-option'])) - option_aliases = GitOptionNameAliases(aliases=aliases) - positional = definition.get('positional', False) - if positional: - position = positional_index + for definition in configuration['definitions']: + if definition.get('positional', False): + definition['position'] = positional_index positional_index += 1 - else: - position = None - option_definition = GitOptionDefinition(name_aliases=option_aliases, type=definition.get('type', str), - positional=positional, position=position) - required = definition.get('required', None) - if required: - option_definition.required = required - choices = definition.get('choices', None) - if choices: - option_definition.choices = choices + option_definition = GitModelGenerator.generate_git_option_definition(definition) definitions.append(option_definition) command.definitions = definitions return command + + @staticmethod + def generate_git_option_definition(configuration: dict) -> GitOptionDefinition: + """ + Method that create an instance of 'GitOptionDefinition' class based on the provided dictionary. + + :param configuration: Dictionary that is used to create 'GitOptionDefinition' class instance. + :type configuration: dict + :return: Instance of the 'GitOptionDefinition' class. + """ + aliases = [] + for alias in configuration['aliases']: + aliases.append(GitOptionNameAlias(name=alias['name'], short_option=alias['short-option'])) + option_aliases = GitOptionNameAliases(aliases=aliases) + positional = configuration.get('positional', False) + position = configuration.get('position', None) + option_definition = GitOptionDefinition(name_aliases=option_aliases, type=configuration.get('type', str), + positional=positional, position=position) + required = configuration.get('required', None) + if required: + option_definition.required = required + choices = configuration.get('choices', None) + if choices: + option_definition.choices = choices + return option_definition From 932c2b68df8995d2630f733b331a9365f8025851 Mon Sep 17 00:00:00 2001 From: Dmytro Leshchenko Date: Thu, 28 Dec 2023 09:59:54 +0100 Subject: [PATCH 119/119] Add tests for CommandOptions class --- .../unit_tests/pygit/options/test_options.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/unit_tests/pygit/options/test_options.py b/tests/unit_tests/pygit/options/test_options.py index c8e3a31..a7ce65d 100644 --- a/tests/unit_tests/pygit/options/test_options.py +++ b/tests/unit_tests/pygit/options/test_options.py @@ -46,6 +46,12 @@ def __init__(self): ] +class StringOptions(CommandOptions): + first = 'one' + second = 'two' + third = 'three' + + class TestGitOptionNameAliases: """ Class contains unit tests for 'GitOptionNameAliases' class. @@ -424,3 +430,46 @@ def test_transform_to_command_positive(self): expected_output = ['command', '-s', '--long-option', 'first', 'value'] transformed_command = command.transform_to_command(options) assert transformed_command == expected_output + +class TestCommandOptions: + """ + Class contains unit tests for 'CommandOptions' enum class. + """ + @pytest.mark.parametrize("value,expected", [ + ('one', True), ('first', False) + ], ids=("Value exists", "Values not exists")) + def test_create_from_value_positive(self, value: str, expected: bool): + """ + Method tests that 'create_from_value' method from 'CommandOptions' class is able to correctly create + 'CommandOption' instance based on provided value. + + :param value: The value that shall be found in the enum class. + :type value: str + :param expected: Whether the value shall be found in the enum class or not. + :type expected: bool + """ + option = StringOptions.create_from_value(value) + if expected: + assert option is not None + else: + assert option is None + + @pytest.mark.parametrize("short_option", [ + True, False + ], ids=("Short option", "Long option")) + def test_create_option_positive(self, short_option: bool): + """ + Method tests that 'create_option' method from 'CommandOptions' class is able to correctly create 'GitOption' + instance for 'CommandOptions' class with provided value. + + :param short_option: Whether test short option or long option. + :type short_option: bool + """ + if short_option: + option = DemoCommand.Options.SHORT_OPTION.create_option('value') + option_name = 's' + else: + option = DemoCommand.Options.LONG_OPTION.create_option('value') + option_name = 'long-option' + assert option is not None + assert option.name == option_name