diff options
Diffstat (limited to 'cli/management.py')
-rw-r--r-- | cli/management.py | 221 |
1 files changed, 221 insertions, 0 deletions
diff --git a/cli/management.py b/cli/management.py new file mode 100644 index 00000000..9d35c643 --- /dev/null +++ b/cli/management.py @@ -0,0 +1,221 @@ +#/bin/python3 + +import subprocess +import sys +import re +import time +import signal + +from config.config import * +from utils import utils +from generators import generators + + +def generate(args): + """Wrapper for generate_blog(fresh=False).""" + # pylint: disable=unused-argument + exit(generators.generate_blog(fresh=False)) + + +def regenerate(args): + """Wrapper for generate_blog(fresh=True).""" + # pylint: disable=unused-argument + exit(generators.generate_blog(fresh=True)) + + +def deploy(args): + """Deploys build directory to origin/master without regenerating. + + Returns + ------- + 0 + On success. Exit early with nonzero status otherwise. + + """ + + # pylint: disable=unused-argument,too-many-statements + + # check whether root is dirty + os.chdir(ROOTDIR) + dirty = subprocess.check_output(["git", "status", "--porcelain"]) + if dirty: + sys.stderr.write(YELLOW) + sys.stderr.write("Project root is dirty.\n") + sys.stderr.write("You may want to commit in your changes " + "to the source branch, since the SHA and title " + "of the latest commit on the source branch will be " + "incorporated into the commit message on " + "the deployment branch. Type s[hell] on the " + "next prompt to open an interactive shell.\n") + sys.stderr.write(RESET) + while True: + sys.stderr.write("Continue? [yNs] ") + answer = input() + if not answer: + # default + abort = True + break + elif answer.startswith(('y', 'Y')): + abort = False + break + elif answer.startswith(('n', 'N')): + abort = True + break + elif answer.startswith(('s', 'S')): + shell = (os.environ['SHELL'] if 'SHELL' in os.environ and os.environ['SHELL'] + else 'zsh') + subprocess.call(shell) + stilldirty = subprocess.check_output(["git", "status", "--porcelain"]) + if stilldirty: + sys.stderr.write(YELLOW) + sys.stderr.write("Project root is still dirty.\n") + sys.stderr.write(RESET) + else: + sys.stderr.write("Please answer yes or no.\n") + if abort: + sys.stderr.write("%saborting deployment%s\n" % (RED, RESET)) + return 1 + + # extract latest commit on the source branch + source_commit = subprocess.check_output( + ["git", "log", "-1", "--pretty=oneline", "source", "--"]).decode('utf-8').strip() + + # cd into BUILDDIR and assemble commit message + sys.stderr.write("%scommand: cd '%s'%s\n" % (BLUE, BUILDDIR, RESET)) + os.chdir(BUILDDIR) + + # extract updated time from atom.xml + if not os.path.exists("atom.xml"): + sys.stderr.write("atom.xml not found, cannot deploy\naborting\n") + return 1 + atomxml = ET.parse("atom.xml").getroot() + updated = atomxml.find('{http://www.w3.org/2005/Atom}updated').text + + commit_message = ("Site updated at %s\n\nsource branch was at:\n%s\n" % + (updated, source_commit)) + + # commit changes in BUILDDIR + sys.stderr.write("%scommand: git add --all%s\n" % (BLUE, RESET)) + subprocess.check_call(["git", "add", "--all"]) + sys.stderr.write("%scommand: git commit --no-verify --gpg-sign --message='%s'%s\n" % + (BLUE, commit_message, RESET)) + try: + subprocess.check_call(["git", "commit", "--gpg-sign", + "--message=%s" % commit_message]) + except subprocess.CalledProcessError: + sys.stderr.write("\n%serror: git commit failed%s\n" % (RED, RESET)) + return 1 + + # check dirty status + dirty = subprocess.check_output(["git", "status", "--porcelain"]) + if dirty: + sys.stderr.write(RED) + sys.stderr.write("error: failed to commit all changes; " + "build directory still dirty\n") + sys.stderr.write("error: please manually inspect what was left out\n") + sys.stderr.write(RESET) + return 1 + + # push to origin/master + sys.stderr.write("%scommand: git push origin master%s\n" % (BLUE, RESET)) + try: + subprocess.check_call(["git", "push", "origin", "master"]) + except subprocess.CalledProcessError: + sys.stderr.write("\n%serror: git push failed%s\n" % (RED, RESET)) + return 1 + return 0 + + +def gen_deploy(args): + """Regenerate and deploy.""" + # pylint: disable=unused-argument,too-many-branches + + # try to smartly determine the latest post, and prompt to touch it + current_time = time.time() + latest_post = None + latest_postdate = 0 + latest_mtime = 0 + for name in os.listdir(POSTSDIR): + matchobj = re.match(r"^([0-9]{4})-([0-9]{2})-([0-9]{2})-.*\.md", name) + if not matchobj: + continue + fullpath = os.path.join(POSTSDIR, name) + mtime = os.path.getmtime(fullpath) + # get post date from the date metadata field of the post + postdate = 0 + with open(fullpath) as postobj: + for line in postobj: + dateregex = r"^date: (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}-\d{2}:?\d{2})" + datematch = re.match(dateregex, line.rstrip()) + if datematch: + postdate = dateutil.parser.parse(datematch.group(1)).timestamp() + break + # skip the post if it is dated more than three days ago + if current_time - postdate > 3 * 24 * 3600: + continue + if mtime > latest_mtime: + latest_post = name + latest_postdate = postdate + latest_mtime = mtime + # prompt for touching if the latest post determined above was + # modified within the last hour but the date registered in the post + # isn't within the last ten minutes + if ((latest_post is not None and current_time - latest_mtime < 3600 and + current_time - latest_postdate > 600)): + sys.stderr.write("%sIt appears that %s might be a new post.\n" + "Do you want to touch its timestamp?%s\n" % + (GREEN, latest_post, RESET)) + while True: + yesnoquit = input("[ynq]: ") + if yesnoquit.startswith(("Y", "y")): + yesno = True + break + elif yesnoquit.startswith(("N", "n")): + yesno = False + break + elif yesnoquit.startswith(("Q", "q")): + sys.stderr.write("%saborting gen_deploy%s\n" % (RED, RESET)) + return 1 + else: + sys.stderr.write("Please answer yes, no, or quit.\n") + if yesno: + sys.stderr.write("%stouching %s%s\n" % (BLUE, latest_post, RESET)) + touch(latest_post) + sys.stderr.write("\n") + + generators.generate_blog(fresh=True) + deploy(None) + + +def preview(args): + """Serve the blog and auto regenerate upon changes.""" + + # pylint: disable=unused-argument + + server_process = utils.HTTPServerProcess(BUILDDIR) + server_process.start() + sys.stderr.write("watching for changes\n") + sys.stderr.write("send SIGINT to stop\n") + + # install a SIGINT handler only for this process + sigint_raised = False + + def sigint_mitigator(signum, frame): + """Translate SIGINT to setting the sigint_raised flag.""" + nonlocal sigint_raised + sigint_raised = True + + signal.signal(signal.SIGINT, sigint_mitigator) + + # Watch and auto-regen. + # No need to actually implement watch separately, since + # generate_blog(fresh=False, report_total_errors=False) already + # watches for modifications and only regens upon changes, and it is + # completely silent when there's no change. + while not sigint_raised: + generators.generate_blog(fresh=False, report_total_errors=False) + time.sleep(0.5) + + sys.stderr.write("\nSIGINT received, cleaning up...\n") + server_process.join() + return 0 |