Source code for law.contrib.git.task

# coding: utf-8

"""
Git-related tasks.
"""

from __future__ import annotations

__all__ = ["BundleGitRepository"]

import os
import pathlib
import threading
import subprocess
from abc import abstractmethod

import luigi  # type: ignore[import-untyped]

from law.task.base import Task
from law.target.file import FileSystemFileTarget, get_path
from law.target.local import LocalFileTarget
from law.parameter import NO_STR, CSVParameter
from law.decorator import log
from law.util import rel_path, interruptable_popen, quote_cmd


[docs] class BundleGitRepository(Task): task_namespace = "law.git" exclude_files = CSVParameter( default=(), description="patterns of files to exclude, default: ()", ) include_files = CSVParameter( default=(), description="patterns of files to force-include, takes precedence over .gitignore, " "default: ()", ) custom_checksum = luigi.Parameter( default=NO_STR, description="a custom checksum to use, default: NO_STR", ) def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self._checksum: str | None = None self._checksum_lock = threading.RLock() @abstractmethod def get_repo_path(self) -> str | pathlib.Path | FileSystemFileTarget: ... @property def checksum(self) -> str: if self.custom_checksum != NO_STR: return self.custom_checksum # type: ignore[return-value] with self._checksum_lock: if self._checksum is None: cmd = quote_cmd([ rel_path(__file__, "scripts", "repository_checksum.sh"), get_path(self.get_repo_path()), "1", # recursive flag " ".join(self.include_files), # type: ignore[arg-type] " ".join(self.exclude_files), # type: ignore[arg-type] ]) out: str code, out, _ = interruptable_popen( # type: ignore[assignment] cmd, shell=True, executable="/bin/bash", stdout=subprocess.PIPE, ) if code != 0: raise Exception("repository checksum calculation failed") self._checksum = out.strip() return self._checksum
[docs] def output(self) -> FileSystemFileTarget: repo_base = os.path.basename(get_path(self.get_repo_path())) repo_base = os.path.abspath(os.path.expandvars(os.path.expanduser(repo_base))) return LocalFileTarget(f"{repo_base}.{self.check_sum}.tgz") # type: ignore[attr-defined]
[docs] @log def run(self) -> None: with self.output().localize("w") as tmp: with self.publish_step("bundle git repository ..."): self.bundle(tmp.path)
def bundle(self, dst_path: str | pathlib.Path | FileSystemFileTarget) -> None: # helper to filter out files that are not actually present in the repository repo_path = os.path.abspath(os.path.expandvars(os.path.expanduser(str(self.get_repo_path())))) is_in_repo = lambda p: not os.path.relpath(os.path.join(str(p)), repo_path).startswith("..") # build the command cmd: list | str cmd = [ rel_path(__file__, "scripts", "bundle_repository.sh"), get_path(repo_path), get_path(dst_path), " ".join(filter(is_in_repo, self.include_files)), # type: ignore[call-overload] " ".join(filter(is_in_repo, self.exclude_files)), # type: ignore[call-overload] ] cmd = quote_cmd(cmd) code = interruptable_popen(cmd, shell=True, executable="/bin/bash")[0] if code != 0: raise Exception("repository bundling failed")