diff --git a/src/nancy/cli/__init__.py b/src/nancy/cli/__init__.py index c83fc91..6df259b 100644 --- a/src/nancy/cli/__init__.py +++ b/src/nancy/cli/__init__.py @@ -5,6 +5,7 @@ from ..version import __version__ from . import diff from . import record +from . import show from typing import Optional @@ -51,4 +52,5 @@ def main(log_level: str) -> None: main.add_command(diff.status, name="status") main.add_command(record.record_cli, name="record") +main.add_command(show.show_cli, name="show") main.add_command(version) diff --git a/src/nancy/cli/show.py b/src/nancy/cli/show.py new file mode 100644 index 0000000..ca2b2ea --- /dev/null +++ b/src/nancy/cli/show.py @@ -0,0 +1,135 @@ +import click +from loguru import logger + +from .. import fs, store + +from datetime import datetime +import os +import sys +from typing import Any, Optional, Union +import warnings + + +def print_recorded( + root: fs.FSEntry, + indent: int = 2, + indent_level: int = 0, + use_color: bool = True, + show_hashes: bool = False, +) -> None: + """Pretty print an FSEntry object""" + if use_color: + try: + from colorama import init, Fore, Back, Style # NOQA + except ImportError: + warnings.warn("Could not import colorama library. Color output disabled.") + use_color = False + + filetypecolors = dict( + DIR=Fore.BLUE if use_color else "", + REG="", + LNK=Fore.CYAN if use_color else "", + ) + reset = Style.RESET_ALL if use_color else "" + hashcolor = Fore.MAGENTA if use_color else "" + + def _print_row(entry: fs.FSEntry, level: int) -> None: + if len(entry.versions) == 0: + print(Fore.RED + "NOVERSIONS" + Style.RESET_ALL + entry.sha256) + else: + ver = entry.versions[-1] + + relpath = entry.relpath + + # Format relpath using filetype-based colors + dname, fname = os.path.split(relpath) + if fname == "": # root directory leads to empty fname here + dirstr = filetypecolors["DIR"] + "" + reset + else: + dirstr = ( + (filetypecolors["DIR"] + dname + "/" + reset) if dname != "" else "" + ) + assert ver.filetype is not None + fname = filetypecolors.get(str(ver.filetype), "") + fname + reset + + if ver.filetype == fs.FileType.LNK: # append symlink target + assert ver.symlink_target is not None + fname += " -> " + ver.symlink_target + + relpath = dirstr + fname + + print( + (hashcolor + ver.sha256.hex() + reset) if show_hashes else "", + ver.perms, + relpath, + ) + + for lev, d in root.flatten_tree(): + _print_row(d, lev) + + +@logger.catch +def show( + store_path: Optional[Union[str, "os.PathLike[Any]"]] = None, + show_hashes: bool = False, + use_color: bool = True, +) -> None: + """Unwrapped show command""" + start_time = datetime.now() + + if store_path is None: + curdir = os.path.realpath(os.getcwd()) + logger.info("Looking for store in {}", curdir) + store_path = store.find_store(curdir) + if store_path is None: # If no store found, assume we're creating here + logger.error( + "Could not find nancy.db in any directory containing {}", curdir + ) + sys.exit(1) + logger.info("Found store at {}", store_path) + s = store.Store(store_path) + + # then find a listing covering all the expected paths + recorded = s.fs_entries(shallow=True) + if recorded is None: + logger.warning( + f"No recorded entries in {store_path}. Have you run `nancy record` yet?" + ) + else: + print_recorded(recorded, show_hashes=show_hashes, use_color=use_color) + + +@click.command() +@click.option( + "-H", + "--show-hashes", + is_flag=True, + help="If given, prepend each line in the diff with the new file hash (SHA256).", +) +@click.option( + "--no-color", + is_flag=True, + help="If given, do not print any color output.", +) +@click.option( + "-s", + "--store", + type=str, + default=None, + help="Top-level of store. If omitted, use closest common parent directory " + "of given paths. If given the path to a non-store directory, a new " + "store is initialized there.", +) +def show_cli( + show_hashes: bool, + no_color: bool, + store: str, +) -> None: + """ + Show what's currently recorded in a store. + """ + show( + show_hashes=show_hashes, + use_color=not no_color, + store_path=store, + )