diff --git a/src/nancy/cli/record.py b/src/nancy/cli/record.py index fb27928..3116ca0 100644 --- a/src/nancy/cli/record.py +++ b/src/nancy/cli/record.py @@ -6,6 +6,7 @@ from .. import store from .common import confirm from .diff import print_diff +from datetime import datetime import os import sys from typing import Any, Optional, Union @@ -21,6 +22,7 @@ def record( skip_confirm: bool = False, ) -> None: """Unwrapped record command""" + start_time = datetime.now() if store_path is None: curdir = os.path.realpath(os.getcwd()) @@ -36,21 +38,34 @@ def record( else: # this is an existing store s = store.Store(store_path) - fsdiff = s.diff() + with s.new_program("RECORD", message) as p: + p.start_time = start_time # reset start to actual start of command + with s.committing() as cur: # entire record operation should be one transaction + # create a task for this operation + task = p.new_task(name="Store._record_recursive", cur=cur) - if show_diff: - print_diff(fsdiff, show_hashes=show_hashes, use_color=use_color) + fsdiff = s.diff() - logger.info("Recording with message: {}", message) + if show_diff: + print_diff(fsdiff, show_hashes=show_hashes, use_color=use_color) - if skip_confirm or confirm("Record the values above into the database?"): - s.record(fsdiff, message=message) - else: - logger.info("Cancelled!") - sys.exit(1) + logger.info("Recording with message: {}", message) + + if skip_confirm or confirm(f"Record to {s.db_path}?"): + # descend the diff, tracking parent filedir IDs, creating them and + # recording new versions of each, when necessary + fsdiff.persist(cur=cur, source_task=task) + else: + logger.info("Cancelled!") + sys.exit(1) @click.command() +@click.option( + "--no-show", + is_flag=True, + help="Do not show what is about to be recorded.", +) @click.option( "-H", "--show-hashes", @@ -62,6 +77,12 @@ def record( is_flag=True, help="If given, do not print any color output.", ) +@click.option( + "-y", + "--yes", + is_flag=True, + help="Do not ask for confirmation.", +) @click.option( "-m", "--message", @@ -78,7 +99,14 @@ def record( "of given paths. If given the path to a non-store directory, a new " "store is initialized there.", ) -def record_cli(show_hashes: bool, no_color: bool, message: str, store: str) -> None: +def record_cli( + no_show: bool, + show_hashes: bool, + yes: bool, + no_color: bool, + message: str, + store: str, +) -> None: """ Initialize tracking or record changes to a tracked directory. """ @@ -87,4 +115,6 @@ def record_cli(show_hashes: bool, no_color: bool, message: str, store: str) -> N show_hashes=show_hashes, use_color=not no_color, store_path=store, + skip_confirm=yes, + show_diff=not no_show, ) diff --git a/src/nancy/fs.py b/src/nancy/fs.py index 91fbe60..ea70e61 100644 --- a/src/nancy/fs.py +++ b/src/nancy/fs.py @@ -154,7 +154,7 @@ class FSEntry: cls: Type[_FSEntryT], store: Optional["Store"] = None, relpath: Optional[str] = None, - exclude: List[str] = ["nancy.db"], + exclude_prefixes: List[str] = ["nancy.db", "."], parent: Optional[_FSEntryT] = None, direntry: Optional["os.DirEntry[str]"] = None, ) -> _FSEntryT: @@ -206,15 +206,23 @@ class FSEntry: for e in direntries ) - children = [ - cls.from_path( - store=store, - relpath=rp, - direntry=e, + children = [] + for rp, e in zip(childrenrelpaths, direntries): + skip = False + for p in exclude_prefixes: + if os.path.basename(rp).startswith(p): + skip = True + break + if skip: + continue + children.append( + cls.from_path( + store=store, + relpath=rp, + direntry=e, + ) ) - for rp, e in zip(childrenrelpaths, direntries) - if rp not in exclude - ] + for c in children: # now hash concatenated sorted hashes # hash on perms+sha256 to enable recursively detecting perm # changes without modifying the hashes of individual files, diff --git a/src/nancy/program.py b/src/nancy/program.py index b5ca5ab..56db257 100644 --- a/src/nancy/program.py +++ b/src/nancy/program.py @@ -129,7 +129,7 @@ class Program: name: str message: str - start_time: Optional[datetime.datetime] = None + start_time: datetime.datetime = datetime.datetime.fromtimestamp(0) evaluated: bool = False uuid: str = "" @@ -195,7 +195,7 @@ class Program: WHERE uuid = ? """, - (self.start_time, end_time, self.uuid), + (self.start_time.timestamp(), end_time.timestamp(), self.uuid), ) self.evaluated = True # prevent re-running assert self.start_time is not None diff --git a/src/nancy/store.py b/src/nancy/store.py index ddb6b47..89515c4 100644 --- a/src/nancy/store.py +++ b/src/nancy/store.py @@ -78,7 +78,6 @@ class Store: @classmethod def init(cls: Type[_StoreT], message: str, directory: fs.PathStr) -> _StoreT: - start_time = datetime.datetime.now() if not os.path.isdir(directory): raise FileNotFoundError( f"Directory {directory} must exist before initializing a store there.", @@ -107,10 +106,6 @@ class Store: (new_store.uuid,), ) - with new_store.new_program("INIT", message) as p: - # set the timing to the actual times it took to initialize the db - p.start_time = start_time - return new_store def filedir_root_key(self, cur: Optional[sqlite3.Cursor] = None) -> Optional[str]: @@ -188,26 +183,6 @@ class Store: return fs.FSDiff.compute(recorded, current) - def record( - self, - diff: fs.FSDiff, - message: str, - parent_id: Optional[str] = None, - cur: Optional[sqlite3.Cursor] = None, - ) -> None: - if cur is None: - assert self.conn is not None - cur = self.conn.cursor() - - with self.new_program("RECORD", message) as p: - with self.committing() as cur: # entire record operation is one transaction - # create a task for this operation - task = p.new_task(name="Store._record_recursive", cur=cur) - - # descend the diff, tracking parent filedir IDs, creating them and - # recording new versions of each, when necessary - diff.persist(cur=cur, source_task=task) - def find_store(path: Union[str, "os.PathLike[str]"]) -> Optional[str]: """ diff --git a/tests/cli/test_record.py b/tests/cli/test_record.py index aa0dd0d..89cd67b 100644 --- a/tests/cli/test_record.py +++ b/tests/cli/test_record.py @@ -22,6 +22,22 @@ def junk_dir() -> Iterator[Path]: def test_record(junk_dir: Path) -> None: runner = CliRunner() + result = runner.invoke( + main, + [ + "record", + "-s", + str(junk_dir), + "-m", + "This is just a test recording", + "--yes", # don't ask for confirmation + ], + ) + print(result.output) + assert result.exit_code == 0 + assert "ERROR" not in result.output + + # test again with confirmation result = runner.invoke( main, [