tzk/tzk.py

237 lines
8.5 KiB
Python
Raw Normal View History

2021-08-25 19:36:40 +00:00
from abc import ABC, abstractmethod, abstractclassmethod
import argparse
import os
import shutil
2021-08-25 20:38:26 +00:00
import sys
2021-08-26 16:51:11 +00:00
import traceback
from typing import Optional
from config import cm
import git
2021-08-25 20:25:50 +00:00
import tw
2021-08-26 19:04:09 +00:00
from util import BuildError, fail
2021-08-25 19:36:40 +00:00
class CliCommand(ABC):
cmd = None # type: str
help = None # type: str
2021-08-25 19:36:40 +00:00
@abstractclassmethod
def setup_arguments(cls, parser: argparse.ArgumentParser) -> None:
2021-08-25 19:36:40 +00:00
raise NotImplementedError
@abstractmethod
def execute(self, parser: argparse.Namespace) -> None:
2021-08-25 19:36:40 +00:00
raise NotImplementedError
class CommitCommand(CliCommand):
cmd = "commit"
help = "Commit all changes to the wiki repository."
2021-08-25 19:36:40 +00:00
@classmethod
def setup_arguments(cls, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
2021-08-25 20:06:42 +00:00
"-m", "--message",
metavar="MSG",
help="Commit message to use.",
2021-08-25 20:38:26 +00:00
default=(cm.commit_message or "checkpoint")
)
parser.add_argument(
"-r", "--remote",
metavar="REMOTE",
help="Name of the configured Git remote to push to.",
default=(cm.commit_remote or "origin"),
2021-08-25 20:06:42 +00:00
)
parser.add_argument(
"-l", "--local",
help="Don't push the results to any configured remote repository.",
2021-08-25 20:38:26 +00:00
action="store_true",
2021-08-25 20:06:42 +00:00
)
2021-08-25 19:36:40 +00:00
def execute(self, args: argparse.Namespace) -> None:
2021-08-25 20:38:26 +00:00
if cm.commit_require_branch:
current_branch = git.read("rev-parse", "--abbrev-ref", "HEAD")
if current_branch != cm.commit_require_branch:
fail(f"You are on the '{current_branch}' branch, "
f"but your TZK configuration requires you to be on the "
f"'{cm.commit_require_branch}' branch to commit.")
2021-08-25 20:38:26 +00:00
git.exec("add", "-A")
git.exec("commit", "-m", args.message)
2021-08-25 20:06:42 +00:00
if not args.local:
2021-08-25 20:38:26 +00:00
git.exec("push", args.remote)
2021-08-25 20:06:42 +00:00
2021-08-25 20:25:50 +00:00
class ListenCommand(CliCommand):
cmd = "listen"
help = "Start a TiddlyWiki server in the current directory."
@classmethod
def setup_arguments(cls, parser: argparse.ArgumentParser) -> None:
2021-08-25 20:25:50 +00:00
parser.add_argument(
"-p", "--port",
metavar="PORT",
help="Port to listen on.",
default=str(cm.listen_port or "8080"),
)
parser.add_argument(
"--username",
metavar="USERNAME",
default=cm.listen_username or "",
help="Username to use for basic authentication, if any.",
)
parser.add_argument(
"--password",
metavar="PASSWORD",
default=cm.listen_password or "",
help="Password to use for basic authentication, if any.",
)
def execute(self, args: argparse.Namespace) -> None:
try:
tw.exec(
[
("listen",
f"port={args.port}",
f"username={args.username}",
f"password={args.password}")
]
)
except KeyboardInterrupt:
# We'll terminate anyway now that we're at the end of execute() --
# no need to display the traceback to the user.
pass
class InitCommand(CliCommand):
cmd = "init"
help = "Set up a new TZK directory."
@classmethod
def setup_arguments(cls, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"-n", "--wiki-name",
metavar="NAME",
help="The wiki will be installed in a subfolder of the current directory, called NAME.",
default="wiki",
)
parser.add_argument(
"-v", "--tiddlywiki-version-spec",
metavar="SPEC",
help="NPM version spec for the version of TiddlyWiki to start your package.json at.",
default="^5.1.23",
)
parser.add_argument(
"-a", "--author",
metavar="AUTHOR_NAME",
help="The author to be credited in the package.json, if not your current username.",
default=None,
2021-08-25 20:25:50 +00:00
)
def _precheck(self):
if shutil.which("npm") is None:
fail("TZK requires NPM. Please install NPM and make it available on your PATH.\n"
"https://docs.npmjs.com/downloading-and-installing-node-js-and-npm")
if shutil.which("git") is None:
fail("TZK requires Git. Please install Git and make it available on your PATH.\n"
"https://git-scm.com/book/en/v2/Getting-Started-Installing-Git")
if os.path.exists("package.json"):
fail("A 'package.json' file already exists in the current directory. "
"Perhaps you've already initialized a TZK repository here?")
def execute(self, args: argparse.Namespace) -> None:
self._precheck()
tw.install(args.wiki_name, args.tiddlywiki_version_spec, args.author)
2021-08-25 20:25:50 +00:00
2021-08-25 19:36:40 +00:00
2021-08-26 16:51:11 +00:00
class BuildCommand(CliCommand):
cmd = "build"
help = ("Build another wiki or derivative product, "
"such as a public version of the wiki, "
"from this TZK repository.")
@classmethod
def setup_arguments(cls, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"product",
metavar="PRODUCT",
help="Name of the product you want to build (defined in your config file).",
)
def _precheck(self, product: str) -> None:
if cm.products is None:
fail("No 'products' dictionary is defined in your config file.")
if product not in cm.products:
fail(f"No '{product}' product found in the products dictionary "
f"in your config file. (Available: {', '.join(cm.products.keys())})")
if not cm.products[product]:
fail(f"No build steps are defined in the '{product}' product "
f"in your config file.")
def execute(self, args: argparse.Namespace) -> None:
self._precheck(args.product)
steps = cm.products[args.product]
print(f"tzk: Starting build of product '{args.product}'.")
print(f"tzk: Found {len(steps)} build steps.")
for idx, step in enumerate(steps, 1):
2021-08-26 19:04:09 +00:00
if hasattr(step, '__doc__'):
print(f"\ntzk: Step {idx}/{len(steps)}: {step.__doc__}")
2021-08-26 16:51:11 +00:00
else:
print(f"\ntzk: Step {idx}/{len(steps)}")
try:
step()
2021-08-26 19:04:09 +00:00
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. "
2021-08-26 16:51:11 +00:00
f"The original error follows:")
traceback.print_exc()
sys.exit(1)
2021-08-26 19:11:56 +00:00
# TODO: This should run in a finally() block; leaving for now as it's convenient for development :)
2021-08-26 19:04:09 +00:00
for idx, step in enumerate(steps, 1):
if hasattr(step, 'cleaner'):
print(f"\ntzk: Running cleanup routine for step {idx}...")
step.cleaner()
2021-08-26 16:51:11 +00:00
print(f"\ntzk: Build of product '{args.product}' completed successfully.")
2021-08-25 19:36:40 +00:00
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
for command in sorted(CliCommand.__subclasses__(), key=lambda i: i.__name__):
2021-08-25 19:36:40 +00:00
subparser = subparsers.add_parser(command.cmd, help=command.help)
subparser.set_defaults(_cls=command)
command.setup_arguments(subparser) # type: ignore
2021-08-25 19:36:40 +00:00
args = parser.parse_args()
# For all operations except 'init', we start in the wiki folder.
if not args._cls.cmd == "init":
if not cm.wiki_folder:
fail("No 'wiki_folder' option found in config. Set this option to the name "
"of the wiki subfolder within the current directory.")
try:
os.chdir(cm.wiki_folder)
except FileNotFoundError:
fail(f"Tried to change directory into the wiki_folder '{cm.wiki_folder}' "
f"specified in your config file, but that directory does not exist.")
if not os.path.exists("tiddlywiki.info"):
fail(f"After changing directory into {cm.wiki_folder} per your config file: "
f"Expected a 'tiddlywiki.info' file in {os.getcwd()}. "
f"Please check that your wiki is initialized "
f"and you specified the correct wiki_folder_name.")
2021-08-25 19:36:40 +00:00
args._cls().execute(args)