Source code for gitws.clone

# Copyright 2022-2023 c0fec0de
#
# This file is part of Git Workspace.
#
# Git Workspace is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Git Workspace is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with Git Workspace. If not, see <https://www.gnu.org/licenses/>.

"""
A Clone.

A :any:`Clone` is just the assembly of a :any:`Project` and its corresponding git interface (:any:`Git`).
"""

import logging
from contextlib import suppress
from pathlib import Path
from typing import Callable, Generator, List, Optional, Tuple

from ._util import get_repr
from .appconfig import AppConfig
from .datamodel import Project
from .git import Git
from .workspace import Workspace

_LOGGER = logging.getLogger("git-ws")


[docs]class Clone: """ Clone - A Pair Of Project Definition And Git Interface. Args: project: Project Definition. git: Git Interface. """ def __init__(self, project: Project, git: Git): self.project = project self.git = git
[docs] @staticmethod def from_project(workspace: Workspace, project: Project, secho=None) -> "Clone": """Create :any:`Clone` for ``project`` in ``workspace``.""" project_path = workspace.get_project_path(project, relative=True) clone_cache = AppConfig().options.clone_cache git = Git(project_path, clone_cache=clone_cache, secho=secho) return Clone(project, git)
[docs] def check(self, exists=True, diff=True): """ Check Clone And Emit Warnings On Logger. The following checks are processed: * Check that the git clone exists (if ``exists=True``). * Check that the current git clone revision matches the ``project.revision`` (if ``diff=True``). * The revision check is skipped on the main project. Keyword Args: exists: Check if cloned. diff: Check if revisions differ """ git = self.git # exists if exists: git.check() project = self.project info = project.info # revision if project.revision: if diff: self._check_revision() elif not project.is_main: _LOGGER.warning("Clone %s has no revision!", info) # URL if diff and project.url: cloneurl = None with suppress(FileNotFoundError): cloneurl = git.get_url() if project.url != cloneurl: if cloneurl: _LOGGER.warning("Clone %s remote origin is %r but intends to be: %r", info, cloneurl, project.url) else: _LOGGER.warning("Clone %s has no remote origin but intends to be: %r", info, project.url)
def _check_revision(self) -> None: git = self.git project = self.project revs: Tuple[str, ...] = () with suppress(FileNotFoundError): # We cannot determine if we checked out a tag or SHA, so we need to be careful here branch = git.get_branch() if branch: revs = (branch,) else: tag = git.get_tag() sha = git.get_sha() if tag and sha: revs = (tag, sha) elif tag: revs = (tag,) elif sha: revs = (sha,) if revs and project.revision not in revs: akas = " aka ".join(repr(rev) for rev in revs) _LOGGER.warning("Clone %s is on different revision: %s", project.info, akas) def __eq__(self, other): if isinstance(other, Clone): return (self.project, self.git) == (other.project, other.git) return NotImplemented def __repr__(self): return get_repr(self, (self.project, self.git)) @property def info(self): """ `repr`-like info string but more condensed. >>> import gitws >>> project = gitws.Project(name='pname', level=0, path='ppath', revision='prevision') >>> git = gitws.Git('gpath') >>> clone = gitws.Clone(project, git) >>> clone.info "gpath ('pname', revision='prevision')" """ project = self.project path = str(self.git.path) options = get_repr( args=(project.name,), kwargs=( ("revision", project.revision, None), ("groups", ",".join(project.groups), ""), ("submodules", project.submodules, True), ), ) if project.is_main: options = f"MAIN {options}" return f"{path} ({options})"
ClonePaths = Tuple[Clone, Tuple[Path, ...]]
[docs]def map_paths(clones: Tuple[Clone, ...], paths: Optional[Tuple[Path, ...]]) -> Generator[ClonePaths, None, None]: """ Map ``paths`` to ``clones``. Associate the given ``paths`` to the corresponding ``clones``. * If ``paths`` is not empty, the corresponding clone and paths pairs are yielded. * If ``paths`` is empty, just all clones with an empty paths tuple are yielded. """ if paths: clonepaths: Tuple[Tuple[Clone, List[Path]], ...] = tuple((clone, []) for clone in clones) # We operate on the absolute paths, but keep track of the specified ones. origpaths: List[Path] = list(paths) abspaths: List[Path] = [path.resolve() for path in paths] # Start path matching at the clones with the deepest folder clonepathssorted = sorted(clonepaths, key=lambda item: item[0].git.path.resolve().parts, reverse=True) # Matching for item in clonepathssorted: clone: Clone = item[0] cpaths: List[Path] = item[1] clonepath = clone.git.path.resolve() for path in tuple(abspaths): try: relpath = path.relative_to(clonepath) except ValueError: continue # pop file from list idx = abspaths.index(path) origpaths.pop(idx) abspaths.pop(idx) cpaths.append(relpath) # Report non-matching for path in origpaths: raise ValueError(f"{str(path)!r} cannot be associated with any clone.") # Return for clone, cpaths in clonepaths: if cpaths: yield clone, tuple(cpaths) else: nopaths: Tuple[Path, ...] = () for clone in clones: yield clone, nopaths
CloneFilter = Callable[[Clone], bool]
[docs]def filter_clone_on_branch(clone: Clone) -> bool: """Return `True` on branches.""" return bool(clone.git.get_branch())