add kill-phrase builder
This commit is contained in:
parent
f63f1a61ea
commit
279342bb4f
99
builders.py
99
builders.py
@ -1,4 +1,15 @@
|
|||||||
|
from contextlib import contextmanager
|
||||||
import functools
|
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):
|
def _lazy_evaluable(func):
|
||||||
"""
|
"""
|
||||||
@ -27,9 +38,93 @@ def _lazy_evaluable(func):
|
|||||||
# if the user wants to write her own builder.
|
# if the user wants to write her own builder.
|
||||||
tzk_builder = _lazy_evaluable
|
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
|
@tzk_builder
|
||||||
def printer(username: str):
|
def printer(username: str) -> None:
|
||||||
|
"Display the user's name"
|
||||||
if username == 'Maud':
|
if username == 'Maud':
|
||||||
raise Exception("No Mauds allowed!")
|
raise Exception("No Mauds allowed!")
|
||||||
print(f"Hallelujah, {username} built a wiki!")
|
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))
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
import functools
|
||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -54,4 +55,5 @@ class ConfigurationManager:
|
|||||||
f.write(f'{attr} = "{value}"\n')
|
f.write(f'{attr} = "{value}"\n')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
cm = ConfigurationManager()
|
cm = ConfigurationManager()
|
||||||
|
3
git.py
3
git.py
@ -4,5 +4,8 @@ from typing import Sequence
|
|||||||
def exec(*args: str):
|
def exec(*args: str):
|
||||||
return subprocess.check_call(["git", *args])
|
return subprocess.check_call(["git", *args])
|
||||||
|
|
||||||
|
def rc(*args: str):
|
||||||
|
return subprocess.call(["git", *args])
|
||||||
|
|
||||||
def read(*args: str):
|
def read(*args: str):
|
||||||
return subprocess.check_output(["git", *args], text=True).strip()
|
return subprocess.check_output(["git", *args], text=True).strip()
|
||||||
|
8
tw.py
8
tw.py
@ -7,7 +7,7 @@ import subprocess
|
|||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from typing import Optional, Sequence
|
from typing import Optional, Sequence
|
||||||
|
|
||||||
from config import cm
|
import config
|
||||||
import git
|
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.
|
Return True if the option ended set to wiki_name, False otherwise.
|
||||||
"""
|
"""
|
||||||
print("tzk: Writing new wiki folder to config file...")
|
print("tzk: Writing new wiki folder to config file...")
|
||||||
if not cm.write_attr("wiki_folder", wiki_name):
|
if not config.cm.write_attr("wiki_folder", wiki_name):
|
||||||
if cm.wiki_folder == wiki_name:
|
if config.cm.wiki_folder == wiki_name:
|
||||||
print("tzk: (Looks like it was already there.)")
|
print("tzk: (Looks like it was already there.)")
|
||||||
else:
|
else:
|
||||||
print(f"tzk: WARNING: The wiki_folder option in your config appears "
|
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 "
|
f"you're initializing, {wiki_name}. Please check your config file "
|
||||||
"and update this option if necessary.")
|
"and update this option if necessary.")
|
||||||
return False
|
return False
|
||||||
|
21
tzk.py
21
tzk.py
@ -9,7 +9,7 @@ from typing import Optional
|
|||||||
from config import cm
|
from config import cm
|
||||||
import git
|
import git
|
||||||
import tw
|
import tw
|
||||||
from util import fail
|
from util import BuildError, fail
|
||||||
|
|
||||||
|
|
||||||
class CliCommand(ABC):
|
class CliCommand(ABC):
|
||||||
@ -179,18 +179,29 @@ class BuildCommand(CliCommand):
|
|||||||
print(f"tzk: Found {len(steps)} build steps.")
|
print(f"tzk: Found {len(steps)} build steps.")
|
||||||
|
|
||||||
for idx, step in enumerate(steps, 1):
|
for idx, step in enumerate(steps, 1):
|
||||||
if hasattr(step, 'name'):
|
if hasattr(step, '__doc__'):
|
||||||
print(f"\ntzk: Step {idx}/{len(steps)}: {step.name}")
|
print(f"\ntzk: Step {idx}/{len(steps)}: {step.__doc__}")
|
||||||
else:
|
else:
|
||||||
print(f"\ntzk: Step {idx}/{len(steps)}")
|
print(f"\ntzk: Step {idx}/{len(steps)}")
|
||||||
try:
|
try:
|
||||||
step()
|
step()
|
||||||
except Exception as e:
|
except BuildError as e:
|
||||||
print(f"\ntzk: Build of product '{args.product}' failed on step {failed}. "
|
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:")
|
f"The original error follows:")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
sys.exit(1)
|
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.")
|
print(f"\ntzk: Build of product '{args.product}' completed successfully.")
|
||||||
|
|
||||||
|
|
||||||
|
6
util.py
6
util.py
@ -1,7 +1,11 @@
|
|||||||
import sys
|
import sys
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
|
|
||||||
|
|
||||||
|
class BuildError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def fail(msg: str, exit_code: int = 1) -> NoReturn:
|
def fail(msg: str, exit_code: int = 1) -> NoReturn:
|
||||||
print(msg, file=sys.stderr)
|
print(msg, file=sys.stderr)
|
||||||
sys.exit(exit_code)
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user