Module cdev.utils.git_safe.utils

Expand source code
from enum import Enum
import json
from typing import Optional, Dict, List
import os

import git
from git import Repo
from git.exc import InvalidGitRepositoryError, NoSuchPathError

from pydantic import DirectoryPath, FilePath

from cdev.default.project import local_project_info


########################################
##### Exceptions
########################################
class ProjectNotMerged(Exception):
    pass


class ProjectFileEdited(Exception):
    pass


class ProjectFileReadingError(Exception):
    pass


class PreserveResourceStatesError(Exception):
    pass


########################################
##### Global Config and Enums
########################################

CDEV_PROJECT_FILE_LOCATION = ".cdev/cdev_project.json"


class GitMergeFileStates(str, Enum):
    UNMERGED = "UNMERGED"
    MODIFIED = "MODIFIED"
    ADD = "ADD"
    DELETE = "DELETED"
    RENAMED = "RENAMED"


_change_str_to_GMFS = {
    "U": GitMergeFileStates.UNMERGED,
    "M": GitMergeFileStates.MODIFIED,
    "A": GitMergeFileStates.ADD,
    "D": GitMergeFileStates.DELETE,
}


########################################
##### Apis
########################################


def get_repo(dir: DirectoryPath) -> Optional[Repo]:
    _git_dir = os.path.join(dir, ".git")
    if is_repo(_git_dir):
        return Repo(_git_dir)

    return None


def is_repo(dir: DirectoryPath) -> bool:
    try:
        _ = Repo(dir).git_dir
        return True
    except InvalidGitRepositoryError:
        return False
    except NoSuchPathError:
        return False
    except Exception:
        return False


def create_repo(dir: DirectoryPath) -> Repo:
    return Repo.init(dir)


def clean_up_resource_states_util() -> None:
    repo = Repo()
    staged_files = repo.index.diff("HEAD")
    working_directory_files = repo.index.diff(None)

    if not staged_files:
        # This is a best effort fix any mistakes with deleting resource states, so if we are not it the correct place within the context
        # of git, then we can just pass through and let a downstream process decide what to do
        return

    _staged_files_information = _reverse_deletes_adds(_process_git_diff(staged_files))
    _working_directory_files_information = _process_git_diff(working_directory_files)

    if (
        CDEV_PROJECT_FILE_LOCATION in _staged_files_information
        and _staged_files_information.get(CDEV_PROJECT_FILE_LOCATION)
        == GitMergeFileStates.UNMERGED
    ):
        # If the project state has not been cleaned up (fixed merge conflicts) and staged, we should not proceed because it could be in a bad state
        raise ProjectNotMerged

    if CDEV_PROJECT_FILE_LOCATION in _working_directory_files_information:
        # if the project state is staged but the working directory has a changes on it, then it is potentially not safe to read
        raise ProjectFileEdited

    try:
        staged_project_info = _load_local_project_information(
            CDEV_PROJECT_FILE_LOCATION
        )
    except Exception:
        # If we can not read the working directory cdev project file and it is the same as the staged version, then we should not proceed.
        raise ProjectFileReadingError

    _resource_states_to_preserve = _compute_cleanup_resource_state_actions(
        staged_project_info, _staged_files_information
    )

    if _resource_states_to_preserve:
        try:
            repo.index.reset("HEAD", paths=_resource_states_to_preserve)
            repo.index.checkout(paths=_resource_states_to_preserve)
        except Exception:
            raise PreserveResourceStatesError


# duplicate from __main__.py, will refactor to a central location
def _load_local_project_information(
    project_info_location: FilePath,
) -> local_project_info:
    """Help function to load the project info json file

    Args:
        project_info_location (FilePath): location of project info json

    Returns:
        local_project_info
    """
    with open(project_info_location, "r") as fh:
        json_information = json.load(fh)

        local_project_info_model = local_project_info(**json_information)

    return local_project_info_model


def _process_git_diff(staged_files: git.DiffIndex) -> Dict[str, GitMergeFileStates]:
    return {
        x.b_path: _change_str_to_GMFS.get(x.change_type)
        if not x.renamed_file
        else GitMergeFileStates.RENAMED
        for x in staged_files
    }


def _compute_resource_state_file_location(resource_state_uuid: str) -> str:
    return os.path.join(".cdev", "state", f"resource_state_{resource_state_uuid}.json")


def _compute_cleanup_resource_state_actions(
    project_info: local_project_info, file_diffs: Dict[str, GitMergeFileStates]
):
    rv = []

    for environment in project_info.environment_infos:
        _rs_file_location = _compute_resource_state_file_location(
            environment.workspace_info.resource_state_uuid
        )

        if (
            _rs_file_location in file_diffs
            and file_diffs.get(_rs_file_location) == GitMergeFileStates.DELETE
        ):
            rv.append(_rs_file_location)

    return rv


def _reverse_deletes_adds(
    file_diffs: Dict[str, GitMergeFileStates]
) -> Dict[str, GitMergeFileStates]:
    _map = {
        GitMergeFileStates.UNMERGED: GitMergeFileStates.UNMERGED,
        GitMergeFileStates.MODIFIED: GitMergeFileStates.MODIFIED,
        GitMergeFileStates.RENAMED: GitMergeFileStates.RENAMED,
        GitMergeFileStates.ADD: GitMergeFileStates.DELETE,
        GitMergeFileStates.DELETE: GitMergeFileStates.ADD,
    }

    return {k: _map.get(v) for k, v in file_diffs.items()}

Functions

def clean_up_resource_states_util() ‑> None
Expand source code
def clean_up_resource_states_util() -> None:
    repo = Repo()
    staged_files = repo.index.diff("HEAD")
    working_directory_files = repo.index.diff(None)

    if not staged_files:
        # This is a best effort fix any mistakes with deleting resource states, so if we are not it the correct place within the context
        # of git, then we can just pass through and let a downstream process decide what to do
        return

    _staged_files_information = _reverse_deletes_adds(_process_git_diff(staged_files))
    _working_directory_files_information = _process_git_diff(working_directory_files)

    if (
        CDEV_PROJECT_FILE_LOCATION in _staged_files_information
        and _staged_files_information.get(CDEV_PROJECT_FILE_LOCATION)
        == GitMergeFileStates.UNMERGED
    ):
        # If the project state has not been cleaned up (fixed merge conflicts) and staged, we should not proceed because it could be in a bad state
        raise ProjectNotMerged

    if CDEV_PROJECT_FILE_LOCATION in _working_directory_files_information:
        # if the project state is staged but the working directory has a changes on it, then it is potentially not safe to read
        raise ProjectFileEdited

    try:
        staged_project_info = _load_local_project_information(
            CDEV_PROJECT_FILE_LOCATION
        )
    except Exception:
        # If we can not read the working directory cdev project file and it is the same as the staged version, then we should not proceed.
        raise ProjectFileReadingError

    _resource_states_to_preserve = _compute_cleanup_resource_state_actions(
        staged_project_info, _staged_files_information
    )

    if _resource_states_to_preserve:
        try:
            repo.index.reset("HEAD", paths=_resource_states_to_preserve)
            repo.index.checkout(paths=_resource_states_to_preserve)
        except Exception:
            raise PreserveResourceStatesError
def create_repo(dir: pydantic.types.DirectoryPath) ‑> git.repo.base.Repo
Expand source code
def create_repo(dir: DirectoryPath) -> Repo:
    return Repo.init(dir)
def get_repo(dir: pydantic.types.DirectoryPath) ‑> Optional[git.repo.base.Repo]
Expand source code
def get_repo(dir: DirectoryPath) -> Optional[Repo]:
    _git_dir = os.path.join(dir, ".git")
    if is_repo(_git_dir):
        return Repo(_git_dir)

    return None
def is_repo(dir: pydantic.types.DirectoryPath) ‑> bool
Expand source code
def is_repo(dir: DirectoryPath) -> bool:
    try:
        _ = Repo(dir).git_dir
        return True
    except InvalidGitRepositoryError:
        return False
    except NoSuchPathError:
        return False
    except Exception:
        return False

Classes

class GitMergeFileStates (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code
class GitMergeFileStates(str, Enum):
    UNMERGED = "UNMERGED"
    MODIFIED = "MODIFIED"
    ADD = "ADD"
    DELETE = "DELETED"
    RENAMED = "RENAMED"

Ancestors

  • builtins.str
  • enum.Enum

Class variables

var ADD
var DELETE
var MODIFIED
var RENAMED
var UNMERGED
class PreserveResourceStatesError (*args, **kwargs)

Common base class for all non-exit exceptions.

Expand source code
class PreserveResourceStatesError(Exception):
    pass

Ancestors

  • builtins.Exception
  • builtins.BaseException
class ProjectFileEdited (*args, **kwargs)

Common base class for all non-exit exceptions.

Expand source code
class ProjectFileEdited(Exception):
    pass

Ancestors

  • builtins.Exception
  • builtins.BaseException
class ProjectFileReadingError (*args, **kwargs)

Common base class for all non-exit exceptions.

Expand source code
class ProjectFileReadingError(Exception):
    pass

Ancestors

  • builtins.Exception
  • builtins.BaseException
class ProjectNotMerged (*args, **kwargs)

Common base class for all non-exit exceptions.

Expand source code
class ProjectNotMerged(Exception):
    pass

Ancestors

  • builtins.Exception
  • builtins.BaseException