From 279342bb4f3b2b585bcc6bc4fea38350872b20ea Mon Sep 17 00:00:00 2001 From: "Soren I. Bjornstad" Date: Thu, 26 Aug 2021 14:04:09 -0500 Subject: [PATCH] add kill-phrase builder --- builders.py | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++-- config.py | 2 ++ git.py | 3 ++ tw.py | 8 ++--- tzk.py | 21 +++++++++--- util.py | 6 +++- 6 files changed, 127 insertions(+), 12 deletions(-) diff --git a/builders.py b/builders.py index 427fad1..5317811 100644 --- a/builders.py +++ b/builders.py @@ -1,4 +1,15 @@ +from contextlib import contextmanager import functools +from pathlib import Path +import re +import shutil +import tempfile +from typing import Set + +import git +import tw +from util import BuildError + def _lazy_evaluable(func): """ @@ -27,9 +38,93 @@ def _lazy_evaluable(func): # if the user wants to write her own builder. tzk_builder = _lazy_evaluable + +def stop(message: str) -> None: + "Stop the build due to an error condition." + raise BuildError(message) + + +# Global state available to all builders. +build_state = {} + + @tzk_builder -def printer(username: str): +def printer(username: str) -> None: + "Display the user's name" if username == 'Maud': raise Exception("No Mauds allowed!") print(f"Hallelujah, {username} built a wiki!") -printer.name = "Display the user's name" + + +@tzk_builder +def require_branch(branchname: str) -> None: + "Require a specific Git branch to be checked out" + if git.read("branch", "--show-current") != branchname: + stop(f"You may only run this build from the {branchname} branch.") + + +@tzk_builder +def require_clean_working_tree() -> None: + "Require the working tree of the Git repository to be clean" + pleasecommit = "Please commit or stash them before publishing." + if git.rc("diff-index", "--quiet", "--cached", "HEAD", "--") != 0: + stop(f"There are staged changes. {pleasecommit}") + if git.rc("diff-files", "--quiet") != 0: + stop(f"There are uncommitted changes. {pleasecommit}") + + +@tzk_builder +def new_output_folder(): + "Create a new temporary folder to hold output being built" + assert 'public_wiki_folder' not in build_state + build_state['public_wiki_folder'] = tempfile.mkdtemp() + +new_output_folder.cleaner = lambda: shutil.rmtree(build_state['public_wiki_folder']) + + +@tzk_builder +def export_public_tiddlers(export_filter: str) -> None: + "Export public tiddlers to public wiki folder" + assert 'public_wiki_folder' in build_state + tw.exec(( + ("savewikifolder", build_state['public_wiki_folder'], export_filter), + )) + + +def _find_kill_phrases(phrases: Set[str]): + regexes = [re.compile(phrase) for phrase in phrases] + failures = [] + tid_files = (Path(build_state['public_wiki_folder']) / "tiddlers").glob("**/*.tid") + + for tid_file in tid_files: + with tid_file.open() as f: + for line in f: + for regex in regexes: + if re.search(regex, line): + failures.append((regex, str(tid_file), line)) + + return failures + +@tzk_builder +def check_for_kill_phrases(kill_phrase_file: str = None): + "Fail build if any of a series of regexes appears in a tiddler's source" + if kill_phrase_file is None: + kill_phrase_file = "tiddlers/_system/config/zettelkasten/Build/KillPhrases.tid" + + kill_phrases = set() + with open(kill_phrase_file) as f: + reading = False + for line in f: + if not line.strip(): + reading = True + if line.strip() and reading: + kill_phrases.add(line) + + failures = _find_kill_phrases(kill_phrases) + if failures: + result = ["Kill phrases were found in your public wiki:"] + for failed_regex, failed_file, failed_line in failures: + trimmed_file = failed_file.replace(build_state['public_wiki_folder'], '') + result.append(f"'{failed_regex.pattern}' matched file {trimmed_file}:\n" + f" {failed_line.strip()}") + stop('\n'.join(result)) diff --git a/config.py b/config.py index 84a1d54..611cc66 100644 --- a/config.py +++ b/config.py @@ -1,4 +1,5 @@ import datetime +import functools import importlib import os from pathlib import Path @@ -54,4 +55,5 @@ class ConfigurationManager: f.write(f'{attr} = "{value}"\n') return True + cm = ConfigurationManager() diff --git a/git.py b/git.py index b429da3..6cb4f73 100644 --- a/git.py +++ b/git.py @@ -4,5 +4,8 @@ from typing import Sequence def exec(*args: str): return subprocess.check_call(["git", *args]) +def rc(*args: str): + return subprocess.call(["git", *args]) + def read(*args: str): return subprocess.check_output(["git", *args], text=True).strip() diff --git a/tw.py b/tw.py index 56b04a1..2c5ab05 100644 --- a/tw.py +++ b/tw.py @@ -7,7 +7,7 @@ import subprocess from textwrap import dedent from typing import Optional, Sequence -from config import cm +import config import git @@ -96,12 +96,12 @@ def _save_wikifolder_to_config(wiki_name: str) -> bool: Return True if the option ended set to wiki_name, False otherwise. """ print("tzk: Writing new wiki folder to config file...") - if not cm.write_attr("wiki_folder", wiki_name): - if cm.wiki_folder == wiki_name: + if not config.cm.write_attr("wiki_folder", wiki_name): + if config.cm.wiki_folder == wiki_name: print("tzk: (Looks like it was already there.)") else: print(f"tzk: WARNING: The wiki_folder option in your config appears " - f"to be set to '{cm.wiki_folder}', rather than the wiki folder " + f"to be set to '{config.cm.wiki_folder}', rather than the wiki folder " f"you're initializing, {wiki_name}. Please check your config file " "and update this option if necessary.") return False diff --git a/tzk.py b/tzk.py index dcb731a..c232367 100644 --- a/tzk.py +++ b/tzk.py @@ -9,7 +9,7 @@ from typing import Optional from config import cm import git import tw -from util import fail +from util import BuildError, fail class CliCommand(ABC): @@ -179,18 +179,29 @@ class BuildCommand(CliCommand): print(f"tzk: Found {len(steps)} build steps.") for idx, step in enumerate(steps, 1): - if hasattr(step, 'name'): - print(f"\ntzk: Step {idx}/{len(steps)}: {step.name}") + if hasattr(step, '__doc__'): + print(f"\ntzk: Step {idx}/{len(steps)}: {step.__doc__}") else: print(f"\ntzk: Step {idx}/{len(steps)}") try: step() - except Exception as e: - print(f"\ntzk: Build of product '{args.product}' failed on step {failed}. " + except BuildError as e: + print(f"tzk: ERROR: {str(e)}") + print(f"\ntzk: Build of product '{args.product}' failed on step {idx}, " + f"backed by builder '{step.__name__}'.") + sys.exit(1) + except Exception: + print(f"\ntzk: Build of product '{args.product}' failed on step {idx}: " + f"unhandled exception. " f"The original error follows:") traceback.print_exc() sys.exit(1) + for idx, step in enumerate(steps, 1): + if hasattr(step, 'cleaner'): + print(f"\ntzk: Running cleanup routine for step {idx}...") + step.cleaner() + print(f"\ntzk: Build of product '{args.product}' completed successfully.") diff --git a/util.py b/util.py index 11ec9f8..fd92a59 100644 --- a/util.py +++ b/util.py @@ -1,7 +1,11 @@ import sys from typing import NoReturn + +class BuildError(Exception): + pass + + def fail(msg: str, exit_code: int = 1) -> NoReturn: print(msg, file=sys.stderr) sys.exit(exit_code) -