changeset 642:79aefe82c6b6

cm: Move all scripts into a `garcon` package with `invoke` support.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 14 Feb 2016 22:11:24 -0800
parents 35221f5fe0dd
children 81a30a63c431
files dev-requirements.txt garcon/__init__.py garcon/benchsite.py garcon/changelog.py garcon/changelog/category_title.rst garcon/changelog/header.rst garcon/changelog/version_title.rst garcon/documentation.py garcon/messages.py garcon/messages/config.yml garcon/messages/pages/_index.html garcon/messages/pages/critical.html garcon/messages/pages/error.html garcon/messages/pages/error404.html garcon/messages/templates/default.html garcon/messages/templates/error.html garcon/pypi.py tasks.py util/changelog/category_title.rst util/changelog/header.rst util/changelog/version_title.rst util/generate_benchsite.py util/generate_changelog.py util/generate_docs.sh util/generate_messages.cmd util/generate_messages.sh util/messages/config.yml util/messages/pages/_index.html util/messages/pages/critical.html util/messages/pages/error.html util/messages/pages/error404.html util/messages/templates/default.html util/messages/templates/error.html
diffstat 32 files changed, 647 insertions(+), 634 deletions(-) [+]
line wrap: on
line diff
--- a/dev-requirements.txt	Sun Feb 14 22:10:05 2016 -0800
+++ b/dev-requirements.txt	Sun Feb 14 22:11:24 2016 -0800
@@ -1,5 +1,6 @@
 cov-core==1.15.0
 coverage==3.7.1
+invoke==0.12.2
 mock==1.0.1
 pytest==2.7.0
 pytest-cov==1.8.1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/garcon/benchsite.py	Sun Feb 14 22:11:24 2016 -0800
@@ -0,0 +1,227 @@
+import io
+import os
+import os.path
+import string
+import random
+import datetime
+import argparse
+
+
+def generateWord(min_len=1, max_len=10):
+    length = random.randint(min_len, max_len)
+    word = ''.join(random.choice(string.ascii_letters) for _ in range(length))
+    return word
+
+
+def generateSentence(words):
+    return ' '.join([generateWord() for i in range(words)])
+
+
+def generateDate():
+    year = random.choice(range(1995, 2015))
+    month = random.choice(range(1, 13))
+    day = random.choice(range(1, 29))
+    hours = random.choice(range(0, 24))
+    minutes = random.choice(range(0, 60))
+    seconds = random.choice(range(0, 60))
+    return datetime.datetime(
+            year, month, day, hours, minutes, seconds)
+
+
+def generateTitleAndSlug():
+    title = generateSentence(8)
+    slug = title.replace(' ', '-').lower()
+    slug = ''.join(c for c in slug if c.isalnum() or c == '-')
+    return title, slug
+
+
+class BenchmarkSiteGenerator(object):
+    def __init__(self, out_dir):
+        self.out_dir = out_dir
+        self.all_tags = []
+
+    def generatePost(self):
+        post_info = {}
+        title, slug = generateTitleAndSlug()
+        post_info.update({
+            'title': title,
+            'slug': slug})
+        post_info['description'] = generateSentence(20)
+        post_info['tags'] = random.choice(self.all_tags)
+        post_info['datetime'] = generateDate()
+
+        buf = io.StringIO()
+        with buf:
+            para_count = random.randint(5, 10)
+            for i in range(para_count):
+                buf.write(generateSentence(random.randint(50, 100)))
+                buf.write('\n\n')
+            post_info['text'] = buf.getvalue()
+
+        self.writePost(post_info)
+
+    def initialize(self):
+        pass
+
+    def writePost(self, post_info):
+        raise NotImplementedError()
+
+
+class PieCrustBechmarkSiteGenerator(BenchmarkSiteGenerator):
+    def initialize(self):
+        posts_dir = os.path.join(self.out_dir, 'posts')
+        if not os.path.isdir(posts_dir):
+            os.makedirs(posts_dir)
+
+    def writePost(self, post_info):
+        out_dir = os.path.join(self.out_dir, 'posts')
+        slug = post_info['slug']
+        dtstr = post_info['datetime'].strftime('%Y-%m-%d')
+        with open('%s/%s_%s.md' % (out_dir, dtstr, slug), 'w',
+                  encoding='utf8') as f:
+            f.write('---\n')
+            f.write('title: %s\n' % post_info['title'])
+            f.write('description: %s\n' % post_info['description'])
+            f.write('tags: [%s]\n' % post_info['tags'])
+            f.write('---\n')
+
+            para_count = random.randint(5, 10)
+            for i in range(para_count):
+                f.write(generateSentence(random.randint(50, 100)))
+                f.write('\n\n')
+
+
+class OctopressBenchmarkSiteGenerator(BenchmarkSiteGenerator):
+    def initialize(self):
+        posts_dir = os.path.join(self.out_dir, 'source', '_posts')
+        if not os.path.isdir(posts_dir):
+            os.makedirs(posts_dir)
+
+    def writePost(self, post_info):
+        out_dir = os.path.join(self.out_dir, 'source', '_posts')
+        slug = post_info['slug']
+        dtstr = post_info['datetime'].strftime('%Y-%m-%d')
+        with open('%s/%s-%s.markdown' % (out_dir, dtstr, slug), 'w',
+                  encoding='utf8') as f:
+            f.write('---\n')
+            f.write('layout: post\n')
+            f.write('title: %s\n' % post_info['title'])
+            f.write('date: %s 12:00\n' % dtstr)
+            f.write('comments: false\n')
+            f.write('categories: [%s]\n' % post_info['tags'])
+            f.write('---\n')
+
+            para_count = random.randint(5, 10)
+            for i in range(para_count):
+                f.write(generateSentence(random.randint(50, 100)))
+                f.write('\n\n')
+
+
+class MiddlemanBenchmarkSiteGenerator(BenchmarkSiteGenerator):
+    def initialize(self):
+        posts_dir = os.path.join(self.out_dir, 'source')
+        if not os.path.isdir(posts_dir):
+            os.makedirs(posts_dir)
+
+    def writePost(self, post_info):
+        out_dir = os.path.join(self.out_dir, 'source')
+        slug = post_info['slug']
+        dtstr = post_info['datetime'].strftime('%Y-%m-%d')
+        with open('%s/%s-%s.html.markdown' % (out_dir, dtstr, slug), 'w',
+                  encoding='utf8') as f:
+            f.write('---\n')
+            f.write('title: %s\n' % post_info['title'])
+            f.write('date: %s\n' % post_info['datetime'].strftime('%Y/%m/%d'))
+            f.write('tags: %s\n' % post_info['tags'])
+            f.write('---\n')
+
+            para_count = random.randint(5, 10)
+            for i in range(para_count):
+                f.write(generateSentence(random.randint(50, 100)))
+                f.write('\n\n')
+
+
+class HugoBenchmarkSiteGenerator(BenchmarkSiteGenerator):
+    def initialize(self):
+        posts_dir = os.path.join(self.out_dir, 'content', 'post')
+        if not os.path.isdir(posts_dir):
+            os.makedirs(posts_dir)
+
+    def writePost(self, post_info):
+        out_dir = os.path.join(self.out_dir, 'content', 'post')
+        dtstr = post_info['datetime'].strftime('%Y-%m-%d_%H-%M-%S')
+        post_path = os.path.join(out_dir, '%s.md' % dtstr)
+        with open(post_path, 'w', encoding='utf8') as f:
+            f.write('+++\n')
+            f.write('title = "%s"\n' % post_info['title'])
+            f.write('description = "%s"\n' % post_info['description'])
+            f.write('categories = [\n  "%s"\n]\n' % post_info['tags'])
+            f.write('date = "%s"\n' % post_info['datetime'].strftime(
+                    "%Y-%m-%d %H:%M:%S-00:00"))
+            f.write('slug ="%s"\n' % post_info['slug'])
+            f.write('+++\n')
+            f.write(post_info['text'])
+
+
+generators = {
+        'piecrust': PieCrustBechmarkSiteGenerator,
+        'octopress': OctopressBenchmarkSiteGenerator,
+        'middleman': MiddlemanBenchmarkSiteGenerator,
+        'hugo': HugoBenchmarkSiteGenerator
+        }
+
+
+def main():
+    parser = argparse.ArgumentParser(
+            prog='generate_benchsite',
+            description=("Generates a benchmark website with placeholder "
+                         "content suitable for testing."))
+    parser.add_argument(
+            'engine',
+            help="The engine to generate the site for.",
+            choices=list(generators.keys()))
+    parser.add_argument(
+            'out_dir',
+            help="The target directory for the website.")
+    parser.add_argument(
+            '-c', '--post-count',
+            help="The number of posts to create.",
+            type=int,
+            default=100)
+    parser.add_argument(
+            '--tag-count',
+            help="The number of tags to use.",
+            type=int,
+            default=30)
+
+    result = parser.parse_args()
+    generate(result.engine, result.out_dir,
+             post_count=result.post_count,
+             tag_count=result.tag_count)
+
+
+def generate(engine, out_dir, post_count=100, tag_count=10):
+    print("Generating %d posts in %s..." % (post_count, out_dir))
+
+    if not os.path.exists(out_dir):
+        os.makedirs(out_dir)
+
+    gen = generators[engine](out_dir)
+    gen.all_tags = [generateWord(3, 12) for _ in range(tag_count)]
+    gen.initialize()
+
+    for i in range(post_count):
+        gen.generatePost()
+
+
+if __name__ == '__main__':
+    main()
+else:
+    from invoke import task
+
+    @task
+    def genbenchsite(engine, out_dir, post_count=100, tag_count=10):
+        generate(engine, out_dir,
+                 post_count=post_count,
+                 tag_count=tag_count)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/garcon/changelog.py	Sun Feb 14 22:11:24 2016 -0800
@@ -0,0 +1,183 @@
+import os
+import os.path
+import re
+import time
+import argparse
+import subprocess
+
+
+hg_log_template = ("{if(tags, '>>{tags};{date|shortdate}\n')}"
+                   "{desc|firstline}\n\n")
+
+re_add_tag_changeset = re.compile('^Added tag [^\s]+ for changeset [\w\d]+$')
+re_merge_pr_changeset = re.compile('^Merge pull request')
+re_tag = re.compile('^\d+\.\d+\.\d+([ab]\d+)?(rc\d+)?$')
+re_change = re.compile('^(\w+):')
+re_clean_code_span = re.compile('([^\s])``([^\s]+)')
+
+category_commands = [
+        'chef', 'bake', 'find', 'help', 'import', 'init', 'paths', 'plugin',
+        'plugins', 'prepare', 'purge', 'root', 'routes', 'serve',
+        'showconfig', 'showrecord', 'sources', 'theme', 'themes', 'admin',
+        'publish']
+category_core = [
+        'internal', 'bug', 'templating', 'formatting', 'performance',
+        'data', 'config', 'rendering', 'render', 'debug', 'reporting',
+        'linker', 'pagination', 'routing', 'caching', 'cli']
+category_project = ['build', 'cm', 'docs', 'tests', 'setup']
+categories = [
+        ('commands', category_commands),
+        ('core', category_core),
+        ('project', category_project),
+        ('miscellaneous', None)]
+category_names = list(map(lambda i: i[0], categories))
+
+
+def generate(out_file, last=None):
+    print("Generating %s" % out_file)
+
+    if not os.path.exists('.hg'):
+        raise Exception("You must run this script from the root of a "
+                        "Mercurial clone of the PieCrust repository.")
+    hglog = subprocess.check_output([
+        'hg', 'log',
+        '--rev', 'reverse(::master)',
+        '--template', hg_log_template])
+    hglog = hglog.decode('utf8')
+
+    templates = _get_templates()
+
+    with open(out_file, 'w') as fp:
+        fp.write(templates['header'])
+
+        skip = False
+        in_desc = False
+        current_version = 0
+        current_version_info = None
+        current_changes = None
+
+        if last:
+            current_version = 1
+            cur_date = time.strftime('%Y-%m-%d')
+            current_version_info = last, cur_date
+            current_changes = {}
+
+        for line in hglog.splitlines():
+            if line == '':
+                skip = False
+                in_desc = False
+                continue
+
+            if not in_desc and line.startswith('>>'):
+                tags, tag_date = line[2:].split(';')
+                if re_tag.match(tags):
+                    if current_version > 0:
+                        _write_version_changes(
+                                templates,
+                                current_version, current_version_info,
+                                current_changes, fp)
+
+                    current_version += 1
+                    current_version_info = tags, tag_date
+                    current_changes = {}
+                    in_desc = True
+                else:
+                    skip = True
+                continue
+
+            if skip or current_version == 0:
+                continue
+
+            if re_add_tag_changeset.match(line):
+                continue
+            if re_merge_pr_changeset.match(line):
+                continue
+
+            m = re_change.match(line)
+            if m:
+                ch_type = m.group(1)
+                for cat_name, ch_types in categories:
+                    if ch_types is None or ch_type in ch_types:
+                        msgs = current_changes.setdefault(cat_name, [])
+                        msgs.append(line)
+                        break
+                else:
+                    assert False, ("Change '%s' should have gone in the "
+                                   "misc. bucket." % line)
+            else:
+                msgs = current_changes.setdefault('miscellaneous', [])
+                msgs.append(line)
+
+        if current_version > 0:
+            _write_version_changes(
+                    templates,
+                    current_version, current_version_info,
+                    current_changes, fp)
+
+
+def _write_version_changes(templates, version, version_info, changes, fp):
+    tokens = {
+            'num': str(version),
+            'version': version_info[0],
+            'date': version_info[1]}
+    tpl = _multi_replace(templates['version_title'], tokens)
+    fp.write(tpl)
+
+    for i, cat_name in enumerate(category_names):
+        msgs = changes.get(cat_name)
+        if not msgs:
+            continue
+
+        tokens = {
+                'sub_num': str(i),
+                'category': cat_name.title()}
+        tpl = _multi_replace(templates['category_title'], tokens)
+        fp.write(tpl)
+
+        for msg in msgs:
+            msg = msg.replace('`', '``').rstrip('\n')
+            msg = re_clean_code_span.sub(r'\1`` \2', msg)
+            fp.write('* ' + msg + '\n')
+
+
+def _multi_replace(s, tokens):
+    for token in tokens:
+        s = s.replace('%%%s%%' % token, tokens[token])
+    return s
+
+
+def _get_templates():
+    tpl_dir = os.path.join(os.path.dirname(__file__), 'changelog')
+    tpls = {}
+    for name in os.listdir(tpl_dir):
+        tpl = _get_template(os.path.join(tpl_dir, name))
+        name_no_ext, _ = os.path.splitext(name)
+        tpls[name_no_ext] = tpl
+    return tpls
+
+
+def _get_template(filename):
+    with open(filename, 'r', encoding='utf8') as fp:
+        return fp.read()
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser(description='Generate CHANGELOG file.')
+    parser.add_argument(
+            'out_file',
+            nargs='?',
+            default='CHANGELOG.rst',
+            help='The output file.')
+    parser.add_argument(
+            '--last',
+            help="The version for the last few untagged changes.")
+    args = parser.parse_args()
+
+    generate(args.out_file, last=args.last)
+else:
+    from invoke import task
+
+    @task
+    def genchangelog(out_file='CHANGELOG.rst', last=None):
+        generate(out_file, last)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/garcon/changelog/category_title.rst	Sun Feb 14 22:11:24 2016 -0800
@@ -0,0 +1,4 @@
+
+1.%sub_num% %category%
+----------------------
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/garcon/changelog/header.rst	Sun Feb 14 22:11:24 2016 -0800
@@ -0,0 +1,10 @@
+
+#########
+CHANGELOG
+#########
+
+This is the changelog for PieCrust_.
+
+.. _PieCrust: http://bolt80.com/piecrust/
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/garcon/changelog/version_title.rst	Sun Feb 14 22:11:24 2016 -0800
@@ -0,0 +1,5 @@
+
+==================================
+%num%. PieCrust %version% (%date%)
+==================================
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/garcon/documentation.py	Sun Feb 14 22:11:24 2016 -0800
@@ -0,0 +1,41 @@
+import os
+import os.path
+from invoke import task, run
+
+
+@task
+def gendocs(tmp_dir=None, out_dir=None):
+    if not tmp_dir:
+        tmp_dir = '_docs-counter'
+
+    print("Updating virtual environment")
+    run("pip install -r requirements.txt --upgrade")
+
+    print("Update Bower packages")
+    run("bower update")
+
+    print("Generate PieCrust version")
+    run('python setup.py version')
+    from piecrust.__version__ import APP_VERSION
+    version = APP_VERSION
+
+    print("Baking documentation for version: %s" % version)
+    args = [
+            'python', 'chef.py',
+            '--root', 'docs',
+            '--config', 'dist',
+            '--config-set', 'site/root', '/piecrust/en/%s' % version,
+            'bake',
+            '-o', tmp_dir
+            ]
+    run(' '.join(args))
+
+    if out_dir:
+        print("Synchronizing %s" % out_dir)
+        if not os.path.isdir(out_dir):
+            os.makedirs(out_dir)
+
+        tmp_dir = tmp_dir.rstrip('/') + '/'
+        out_dir = out_dir.rstrip('/') + '/'
+        run('rsync -av --delete %s %s' % (tmp_dir, out_dir))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/garcon/messages.py	Sun Feb 14 22:11:24 2016 -0800
@@ -0,0 +1,11 @@
+import os
+from invoke import task, run
+
+
+@task
+def genmessages():
+    root_dir = 'garcon/messages'
+    out_dir = 'piecrust/resources/messages'
+    run('python chef.py --root %s bake -o %s' % (root_dir, out_dir))
+    os.unlink('piecrust/resources/messages/index.html')
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/garcon/messages/config.yml	Sun Feb 14 22:11:24 2016 -0800
@@ -0,0 +1,2 @@
+site:
+    title: PieCrust System Messages
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/garcon/messages/pages/_index.html	Sun Feb 14 22:11:24 2016 -0800
@@ -0,0 +1,12 @@
+---
+title: PieCrust System Messages
+---
+
+Here are the **PieCrust** system message pages:
+
+* [Requirements Not Met]({{ pcurl('requirements') }})
+* [Error]({{ pcurl('error') }})
+* [Not Found]({{ pcurl('error404') }})
+* [Critical Error]({{ pcurl('critical') }})
+
+This very page you're reading, however, is only here for convenience.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/garcon/messages/pages/critical.html	Sun Feb 14 22:11:24 2016 -0800
@@ -0,0 +1,6 @@
+---
+title: The Whole Kitchen Burned Down!
+layout: error
+---
+Something critically bad happened, and **PieCrust** needs to shut down. It's probably our fault.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/garcon/messages/pages/error.html	Sun Feb 14 22:11:24 2016 -0800
@@ -0,0 +1,5 @@
+---
+title: The Cake Just Burned!
+layout: error
+---
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/garcon/messages/pages/error404.html	Sun Feb 14 22:11:24 2016 -0800
@@ -0,0 +1,6 @@
+---
+title: Can't find the sugar!
+layout: error
+---
+It looks like the page you were trying to access does not exist around here. Try going somewhere else.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/garcon/messages/templates/default.html	Sun Feb 14 22:11:24 2016 -0800
@@ -0,0 +1,70 @@
+<!doctype html>
+<html>
+<head>
+    <title>{{ page.title }}</title>
+    <meta name="generator" content="PieCrust" />
+	<link rel="stylesheet" type="text/css" href="http://fonts.googleapis.com/css?family=Lobster">
+	<style>
+		body {
+			margin: 0;
+			padding: 1em;
+			background: #eee;
+			color: #000;
+			font-family: Georgia, serif;
+		}
+		h1 {
+			font-size: 4.5em;
+			font-family: Lobster, 'Trebuchet MS', Verdana, sans-serif;
+			text-align: center;
+			font-weight: bold;
+            margin-top: 0;
+			color: #333;
+			text-shadow: 0px 2px 5px rgba(0,0,0,0.3);
+		}
+		h2 {
+			font-size: 2.5em;
+            font-family: 'Lobster', 'Trebuchet MS', Verdana, sans-serif;
+		}
+		code {
+			background: #ddd;
+			padding: 0 0.2em;
+		}
+        #preamble {
+            font-size: 1.2em;
+            font-style: italic;
+            text-align: center;
+            margin-bottom: 0;
+        }
+		#container {
+			margin: 0 20%;
+		}
+        #content {
+            margin: 2em 1em;
+        }
+        .error-details {
+            color: #d11;
+        }
+		.note {
+			margin: 3em;
+			color: #888;
+			font-style: italic;
+		}
+	</style>
+</head>
+<body>
+    <div id="container">
+        <div id="header">
+            <p id="preamble">A Message From The Kitchen:</p>
+			<h1>{{ page.title }}</h1>
+		</div>
+        <hr />
+        <div id="content">
+            {% block content %}
+			{{ content|safe }}
+            {% endblock %}
+		</div>
+        <hr />
+        {% block footer %}{% endblock %}
+	</div>
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/garcon/messages/templates/error.html	Sun Feb 14 22:11:24 2016 -0800
@@ -0,0 +1,27 @@
+{% extends "default.html" %}
+
+{% block content %}
+{{content|safe}}
+
+{# The following is `raw` because we want it to be in the
+   produced page, so it can then be templated on the fly 
+   with the error messages #}
+{% raw %}
+{% if details %}
+<div class="error-details">
+    <p>Error details:</p>
+    <ul>
+    {% for desc in details %}
+        <li>{{ desc }}</li>
+    {% endfor %}
+    </ul>
+</div>
+{% endif %}
+{% endraw %}
+{% endblock %}
+
+{% block footer %}
+{% pcformat 'textile' %}
+p(note). You're seeing this because something wrong happend. To see detailed errors with callstacks, run chef with the @--debug@ parameter, append @?!debug@ to the URL, or initialize the @PieCrust@ object with @{'debug': true}@. On the other hand, to see you custom error pages, set the @site/display_errors@ setting  to @false@.
+{% endpcformat %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/garcon/pypi.py	Sun Feb 14 22:11:24 2016 -0800
@@ -0,0 +1,22 @@
+from invoke import task, run
+
+
+@task
+def makerelease(version, notag=False, noupload=False):
+    if not version:
+        raise Exception("You must specify a version!")
+
+    # FoodTruck assets.
+    run("gulp")
+
+    # CHANGELOG.rst
+    run("invoke changelog --last %s" % version)
+
+    # Tag in Mercurial, which will then be used for PyPi version.
+    if not notag:
+        run("hg tag %s" % version)
+
+    # PyPi upload.
+    if not noupload:
+        run("python setup.py sdist upload")
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tasks.py	Sun Feb 14 22:11:24 2016 -0800
@@ -0,0 +1,15 @@
+from invoke import Collection, task, run
+from garcon.benchsite import genbenchsite
+from garcon.changelog import genchangelog
+from garcon.documentation import gendocs
+from garcon.messages import genmessages
+from garcon.pypi import makerelease
+
+
+ns = Collection()
+ns.add_task(genbenchsite, name='benchsite')
+ns.add_task(genchangelog, name='changelog')
+ns.add_task(gendocs, name='docs')
+ns.add_task(genmessages, name='messages')
+ns.add_task(makerelease, name='release')
+
--- a/util/changelog/category_title.rst	Sun Feb 14 22:10:05 2016 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-
-1.%sub_num% %category%
-----------------------
-
--- a/util/changelog/header.rst	Sun Feb 14 22:10:05 2016 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-
-#########
-CHANGELOG
-#########
-
-This is the changelog for PieCrust_.
-
-.. _PieCrust: http://bolt80.com/piecrust/
-
-
--- a/util/changelog/version_title.rst	Sun Feb 14 22:10:05 2016 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-
-==================================
-%num%. PieCrust %version% (%date%)
-==================================
-
--- a/util/generate_benchsite.py	Sun Feb 14 22:10:05 2016 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,215 +0,0 @@
-import io
-import os
-import os.path
-import string
-import random
-import datetime
-import argparse
-
-
-def generateWord(min_len=1, max_len=10):
-    length = random.randint(min_len, max_len)
-    word = ''.join(random.choice(string.ascii_letters) for _ in range(length))
-    return word
-
-
-def generateSentence(words):
-    return ' '.join([generateWord() for i in range(words)])
-
-
-def generateDate():
-    year = random.choice(range(1995, 2015))
-    month = random.choice(range(1, 13))
-    day = random.choice(range(1, 29))
-    hours = random.choice(range(0, 24))
-    minutes = random.choice(range(0, 60))
-    seconds = random.choice(range(0, 60))
-    return datetime.datetime(
-            year, month, day, hours, minutes, seconds)
-
-
-def generateTitleAndSlug():
-    title = generateSentence(8)
-    slug = title.replace(' ', '-').lower()
-    slug = ''.join(c for c in slug if c.isalnum() or c == '-')
-    return title, slug
-
-
-class BenchmarkSiteGenerator(object):
-    def __init__(self, out_dir):
-        self.out_dir = out_dir
-        self.all_tags = []
-
-    def generatePost(self):
-        post_info = {}
-        title, slug = generateTitleAndSlug()
-        post_info.update({
-            'title': title,
-            'slug': slug})
-        post_info['description'] = generateSentence(20)
-        post_info['tags'] = random.choice(self.all_tags)
-        post_info['datetime'] = generateDate()
-
-        buf = io.StringIO()
-        with buf:
-            para_count = random.randint(5, 10)
-            for i in range(para_count):
-                buf.write(generateSentence(random.randint(50, 100)))
-                buf.write('\n\n')
-            post_info['text'] = buf.getvalue()
-
-        self.writePost(post_info)
-
-    def initialize(self):
-        pass
-
-    def writePost(self, post_info):
-        raise NotImplementedError()
-
-
-class PieCrustBechmarkSiteGenerator(BenchmarkSiteGenerator):
-    def initialize(self):
-        posts_dir = os.path.join(self.out_dir, 'posts')
-        if not os.path.isdir(posts_dir):
-            os.makedirs(posts_dir)
-
-    def writePost(self, post_info):
-        out_dir = os.path.join(self.out_dir, 'posts')
-        slug = post_info['slug']
-        dtstr = post_info['datetime'].strftime('%Y-%m-%d')
-        with open('%s/%s_%s.md' % (out_dir, dtstr, slug), 'w',
-                  encoding='utf8') as f:
-            f.write('---\n')
-            f.write('title: %s\n' % post_info['title'])
-            f.write('description: %s\n' % post_info['description'])
-            f.write('tags: [%s]\n' % post_info['tags'])
-            f.write('---\n')
-
-            para_count = random.randint(5, 10)
-            for i in range(para_count):
-                f.write(generateSentence(random.randint(50, 100)))
-                f.write('\n\n')
-
-
-class OctopressBenchmarkSiteGenerator(BenchmarkSiteGenerator):
-    def initialize(self):
-        posts_dir = os.path.join(self.out_dir, 'source', '_posts')
-        if not os.path.isdir(posts_dir):
-            os.makedirs(posts_dir)
-
-    def writePost(self, post_info):
-        out_dir = os.path.join(self.out_dir, 'source', '_posts')
-        slug = post_info['slug']
-        dtstr = post_info['datetime'].strftime('%Y-%m-%d')
-        with open('%s/%s-%s.markdown' % (out_dir, dtstr, slug), 'w',
-                  encoding='utf8') as f:
-            f.write('---\n')
-            f.write('layout: post\n')
-            f.write('title: %s\n' % post_info['title'])
-            f.write('date: %s 12:00\n' % dtstr)
-            f.write('comments: false\n')
-            f.write('categories: [%s]\n' % post_info['tags'])
-            f.write('---\n')
-
-            para_count = random.randint(5, 10)
-            for i in range(para_count):
-                f.write(generateSentence(random.randint(50, 100)))
-                f.write('\n\n')
-
-
-class MiddlemanBenchmarkSiteGenerator(BenchmarkSiteGenerator):
-    def initialize(self):
-        posts_dir = os.path.join(self.out_dir, 'source')
-        if not os.path.isdir(posts_dir):
-            os.makedirs(posts_dir)
-
-    def writePost(self, post_info):
-        out_dir = os.path.join(self.out_dir, 'source')
-        slug = post_info['slug']
-        dtstr = post_info['datetime'].strftime('%Y-%m-%d')
-        with open('%s/%s-%s.html.markdown' % (out_dir, dtstr, slug), 'w',
-                  encoding='utf8') as f:
-            f.write('---\n')
-            f.write('title: %s\n' % post_info['title'])
-            f.write('date: %s\n' % post_info['datetime'].strftime('%Y/%m/%d'))
-            f.write('tags: %s\n' % post_info['tags'])
-            f.write('---\n')
-
-            para_count = random.randint(5, 10)
-            for i in range(para_count):
-                f.write(generateSentence(random.randint(50, 100)))
-                f.write('\n\n')
-
-
-class HugoBenchmarkSiteGenerator(BenchmarkSiteGenerator):
-    def initialize(self):
-        posts_dir = os.path.join(self.out_dir, 'content', 'post')
-        if not os.path.isdir(posts_dir):
-            os.makedirs(posts_dir)
-
-    def writePost(self, post_info):
-        out_dir = os.path.join(self.out_dir, 'content', 'post')
-        dtstr = post_info['datetime'].strftime('%Y-%m-%d_%H-%M-%S')
-        post_path = os.path.join(out_dir, '%s.md' % dtstr)
-        with open(post_path, 'w', encoding='utf8') as f:
-            f.write('+++\n')
-            f.write('title = "%s"\n' % post_info['title'])
-            f.write('description = "%s"\n' % post_info['description'])
-            f.write('categories = [\n  "%s"\n]\n' % post_info['tags'])
-            f.write('date = "%s"\n' % post_info['datetime'].strftime(
-                    "%Y-%m-%d %H:%M:%S-00:00"))
-            f.write('slug ="%s"\n' % post_info['slug'])
-            f.write('+++\n')
-            f.write(post_info['text'])
-
-
-generators = {
-        'piecrust': PieCrustBechmarkSiteGenerator,
-        'octopress': OctopressBenchmarkSiteGenerator,
-        'middleman': MiddlemanBenchmarkSiteGenerator,
-        'hugo': HugoBenchmarkSiteGenerator
-        }
-
-
-def main():
-    parser = argparse.ArgumentParser(
-            prog='generate_benchsite',
-            description=("Generates a benchmark website with placeholder "
-                         "content suitable for testing."))
-    parser.add_argument(
-            'engine',
-            help="The engine to generate the site for.",
-            choices=list(generators.keys()))
-    parser.add_argument(
-            'out_dir',
-            help="The target directory for the website.")
-    parser.add_argument(
-            '-c', '--post-count',
-            help="The number of posts to create.",
-            type=int,
-            default=100)
-    parser.add_argument(
-            '--tag-count',
-            help="The number of tags to use.",
-            type=int,
-            default=30)
-
-    result = parser.parse_args()
-
-    print("Generating %d posts in %s..." % (
-            result.post_count, result.out_dir))
-
-    if not os.path.exists(result.out_dir):
-        os.makedirs(result.out_dir)
-
-    gen = generators[result.engine](result.out_dir)
-    gen.all_tags = [generateWord(3, 12) for _ in range(result.tag_count)]
-    gen.initialize()
-
-    for i in range(result.post_count):
-        gen.generatePost()
-
-
-if __name__ == '__main__':
-    main()
-
--- a/util/generate_changelog.py	Sun Feb 14 22:10:05 2016 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,177 +0,0 @@
-import os
-import os.path
-import re
-import time
-import argparse
-import subprocess
-
-
-hg_log_template = ("{if(tags, '>>{tags};{date|shortdate}\n')}"
-                   "{desc|firstline}\n\n")
-
-re_add_tag_changeset = re.compile('^Added tag [^\s]+ for changeset [\w\d]+$')
-re_merge_pr_changeset = re.compile('^Merge pull request')
-re_tag = re.compile('^\d+\.\d+\.\d+([ab]\d+)?(rc\d+)?$')
-re_change = re.compile('^(\w+):')
-re_clean_code_span = re.compile('([^\s])``([^\s]+)')
-
-category_commands = [
-        'chef', 'bake', 'find', 'help', 'import', 'init', 'paths', 'plugin',
-        'plugins', 'prepare', 'purge', 'root', 'routes', 'serve',
-        'showconfig', 'showrecord', 'sources', 'theme', 'themes', 'admin',
-        'publish']
-category_core = [
-        'internal', 'bug', 'templating', 'formatting', 'performance',
-        'data', 'config', 'rendering', 'render', 'debug', 'reporting',
-        'linker', 'pagination', 'routing', 'caching', 'cli']
-category_project = ['build', 'cm', 'docs', 'tests', 'setup']
-categories = [
-        ('commands', category_commands),
-        ('core', category_core),
-        ('project', category_project),
-        ('miscellaneous', None)]
-category_names = list(map(lambda i: i[0], categories))
-
-
-def generate():
-    parser = argparse.ArgumentParser(description='Generate CHANGELOG file.')
-    parser.add_argument(
-            'out_file',
-            nargs='?',
-            default='CHANGELOG.rst',
-            help='The output file.')
-    parser.add_argument(
-            '--last',
-            help="The version for the last few untagged changes.")
-    args = parser.parse_args()
-
-    print("Generating %s" % args.out_file)
-
-    if not os.path.exists('.hg'):
-        raise Exception("You must run this script from the root of a "
-                        "Mercurial clone of the PieCrust repository.")
-    hglog = subprocess.check_output([
-        'hg', 'log',
-        '--rev', 'reverse(::master)',
-        '--template', hg_log_template])
-    hglog = hglog.decode('utf8')
-
-    templates = _get_templates()
-
-    with open(args.out_file, 'w') as fp:
-        fp.write(templates['header'])
-
-        skip = False
-        in_desc = False
-        current_version = 0
-        current_version_info = None
-        current_changes = None
-
-        if args.last:
-            current_version = 1
-            cur_date = time.strftime('%Y-%m-%d')
-            current_version_info = args.last, cur_date
-            current_changes = {}
-
-        for line in hglog.splitlines():
-            if line == '':
-                skip = False
-                in_desc = False
-                continue
-
-            if not in_desc and line.startswith('>>'):
-                tags, tag_date = line[2:].split(';')
-                if re_tag.match(tags):
-                    if current_version > 0:
-                        _write_version_changes(
-                                templates,
-                                current_version, current_version_info,
-                                current_changes, fp)
-
-                    current_version += 1
-                    current_version_info = tags, tag_date
-                    current_changes = {}
-                    in_desc = True
-                else:
-                    skip = True
-                continue
-
-            if skip or current_version == 0:
-                continue
-
-            if re_add_tag_changeset.match(line):
-                continue
-            if re_merge_pr_changeset.match(line):
-                continue
-
-            m = re_change.match(line)
-            if m:
-                ch_type = m.group(1)
-                for cat_name, ch_types in categories:
-                    if ch_types is None or ch_type in ch_types:
-                        msgs = current_changes.setdefault(cat_name, [])
-                        msgs.append(line)
-                        break
-                else:
-                    assert False, ("Change '%s' should have gone in the "
-                                   "misc. bucket." % line)
-            else:
-                msgs = current_changes.setdefault('miscellaneous', [])
-                msgs.append(line)
-
-        if current_version > 0:
-            _write_version_changes(
-                    templates,
-                    current_version, current_version_info,
-                    current_changes, fp)
-
-
-def _write_version_changes(templates, version, version_info, changes, fp):
-    tokens = {
-            'num': str(version),
-            'version': version_info[0],
-            'date': version_info[1]}
-    tpl = _multi_replace(templates['version_title'], tokens)
-    fp.write(tpl)
-
-    for i, cat_name in enumerate(category_names):
-        msgs = changes.get(cat_name)
-        if not msgs:
-            continue
-
-        tokens = {
-                'sub_num': str(i),
-                'category': cat_name.title()}
-        tpl = _multi_replace(templates['category_title'], tokens)
-        fp.write(tpl)
-
-        for msg in msgs:
-            msg = msg.replace('`', '``').rstrip('\n')
-            msg = re_clean_code_span.sub(r'\1`` \2', msg)
-            fp.write('* ' + msg + '\n')
-
-
-def _multi_replace(s, tokens):
-    for token in tokens:
-        s = s.replace('%%%s%%' % token, tokens[token])
-    return s
-
-
-def _get_templates():
-    tpl_dir = os.path.join(os.path.dirname(__file__), 'changelog')
-    tpls = {}
-    for name in os.listdir(tpl_dir):
-        tpl = _get_template(os.path.join(tpl_dir, name))
-        name_no_ext, _ = os.path.splitext(name)
-        tpls[name_no_ext] = tpl
-    return tpls
-
-
-def _get_template(filename):
-    with open(filename, 'r', encoding='utf8') as fp:
-        return fp.read()
-
-
-if __name__ == '__main__':
-    generate()
-
--- a/util/generate_docs.sh	Sun Feb 14 22:10:05 2016 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-#!/bin/sh
-
-set -e
-
-VERSION=
-ROOT_URL=
-TEMP_DIR=
-OUTPUT_DIR=
-
-ShowUsage() {
-    echo "Usage:"
-    echo "    $PROG_NAME <options>"
-    echo ""
-    echo "    -v [version]"
-    echo "    -r [root_url]"
-    echo "    -t [tmp_dir]"
-    echo "    -o [out_dir]"
-    echo ""
-}
-
-while getopts "h?v:r:t:o:" opt; do
-    case $opt in
-        h|\?)
-            ShowUsage
-            exit 0
-            ;;
-        v)
-            VERSION=$OPTARG
-            ;;
-        r)
-            ROOT_URL=$OPTARG
-            ;;
-        t)
-            TEMP_DIR=$OPTARG
-            ;;
-        o)
-            OUTPUT_DIR=$OPTARG
-            ;;
-    esac
-done
-
-if [ "$VERSION" = "" ]; then
-    echo "You need to specify a version number or label."
-    exit 1
-fi
-if [ "$OUTPUT_DIR" = "" ]; then
-    echo "You need to specify an output directory."
-    exit 1
-fi
-if [ "$TEMP_DIR" = "" ]; then
-    TEMP_DIR=_counter-docs
-fi
-
-echo "Updating virtual environment..."
-venv/bin/pip install -r requirements.txt --upgrade
-
-echo "Generate PieCrust version..."
-venv/bin/python3 setup.py version
-
-echo "Update Bower packages..."
-bower update
-
-echo "Baking documentation for version $VERSION..."
-CHEF_ARGS="--root docs --config dist"
-if [ ! "$ROOT_URL" = "" ]; then
-    CHEF_ARGS="$CHEF_ARGS --config-set site/root $ROOT_URL"
-fi
-venv/bin/python3 chef.py $CHEF_ARGS bake -o $TEMP_DIR
-
-echo "Synchronizing $OUTPUT_DIR"
-if [ ! -d $OUTPUT_DIR ]; then
-    mkdir -p $OUTPUT_DIR
-fi
-rsync -av --delete-after $TEMP_DIR/ $OUTPUT_DIR/
-
--- a/util/generate_messages.cmd	Sun Feb 14 22:10:05 2016 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-@echo off
-setlocal
-
-set CUR_DIR=%~dp0
-set CHEF=%CUR_DIR%..\bin\chef
-set OUT_DIR=%CUR_DIR%..\piecrust\resources\messages
-set ROOT_DIR=%CUR_DIR%messages
-
-%CHEF% --root=%ROOT_DIR% bake -o %OUT_DIR%
-del %OUT_DIR%\index.html
-
--- a/util/generate_messages.sh	Sun Feb 14 22:10:05 2016 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-#!/bin/sh
-
-CUR_DIR="$( cd "$( dirname "$0" )" && pwd )"
-CHEF=${CUR_DIR}/../bin/chef
-OUT_DIR=${CUR_DIR}/../piecrust/resources/messages
-ROOT_DIR=${CUR_DIR}/messages
-
-$CHEF --root=$ROOT_DIR bake -o $OUT_DIR
-rm ${OUT_DIR}/index.html
--- a/util/messages/config.yml	Sun Feb 14 22:10:05 2016 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-site:
-    title: PieCrust System Messages
--- a/util/messages/pages/_index.html	Sun Feb 14 22:10:05 2016 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
----
-title: PieCrust System Messages
----
-
-Here are the **PieCrust** system message pages:
-
-* [Requirements Not Met]({{ pcurl('requirements') }})
-* [Error]({{ pcurl('error') }})
-* [Not Found]({{ pcurl('error404') }})
-* [Critical Error]({{ pcurl('critical') }})
-
-This very page you're reading, however, is only here for convenience.
--- a/util/messages/pages/critical.html	Sun Feb 14 22:10:05 2016 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
----
-title: The Whole Kitchen Burned Down!
-layout: error
----
-Something critically bad happened, and **PieCrust** needs to shut down. It's probably our fault.
-
--- a/util/messages/pages/error.html	Sun Feb 14 22:10:05 2016 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
----
-title: The Cake Just Burned!
-layout: error
----
-
--- a/util/messages/pages/error404.html	Sun Feb 14 22:10:05 2016 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
----
-title: Can't find the sugar!
-layout: error
----
-It looks like the page you were trying to access does not exist around here. Try going somewhere else.
-
--- a/util/messages/templates/default.html	Sun Feb 14 22:10:05 2016 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-<!doctype html>
-<html>
-<head>
-    <title>{{ page.title }}</title>
-    <meta name="generator" content="PieCrust" />
-	<link rel="stylesheet" type="text/css" href="http://fonts.googleapis.com/css?family=Lobster">
-	<style>
-		body {
-			margin: 0;
-			padding: 1em;
-			background: #eee;
-			color: #000;
-			font-family: Georgia, serif;
-		}
-		h1 {
-			font-size: 4.5em;
-			font-family: Lobster, 'Trebuchet MS', Verdana, sans-serif;
-			text-align: center;
-			font-weight: bold;
-            margin-top: 0;
-			color: #333;
-			text-shadow: 0px 2px 5px rgba(0,0,0,0.3);
-		}
-		h2 {
-			font-size: 2.5em;
-            font-family: 'Lobster', 'Trebuchet MS', Verdana, sans-serif;
-		}
-		code {
-			background: #ddd;
-			padding: 0 0.2em;
-		}
-        #preamble {
-            font-size: 1.2em;
-            font-style: italic;
-            text-align: center;
-            margin-bottom: 0;
-        }
-		#container {
-			margin: 0 20%;
-		}
-        #content {
-            margin: 2em 1em;
-        }
-        .error-details {
-            color: #d11;
-        }
-		.note {
-			margin: 3em;
-			color: #888;
-			font-style: italic;
-		}
-	</style>
-</head>
-<body>
-    <div id="container">
-        <div id="header">
-            <p id="preamble">A Message From The Kitchen:</p>
-			<h1>{{ page.title }}</h1>
-		</div>
-        <hr />
-        <div id="content">
-            {% block content %}
-			{{ content|safe }}
-            {% endblock %}
-		</div>
-        <hr />
-        {% block footer %}{% endblock %}
-	</div>
-</body>
-</html>
--- a/util/messages/templates/error.html	Sun Feb 14 22:10:05 2016 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-{% extends "default.html" %}
-
-{% block content %}
-{{content|safe}}
-
-{# The following is `raw` because we want it to be in the
-   produced page, so it can then be templated on the fly 
-   with the error messages #}
-{% raw %}
-{% if details %}
-<div class="error-details">
-    <p>Error details:</p>
-    <ul>
-    {% for desc in details %}
-        <li>{{ desc }}</li>
-    {% endfor %}
-    </ul>
-</div>
-{% endif %}
-{% endraw %}
-{% endblock %}
-
-{% block footer %}
-{% pcformat 'textile' %}
-p(note). You're seeing this because something wrong happend. To see detailed errors with callstacks, run chef with the @--debug@ parameter, append @?!debug@ to the URL, or initialize the @PieCrust@ object with @{'debug': true}@. On the other hand, to see you custom error pages, set the @site/display_errors@ setting  to @false@.
-{% endpcformat %}
-{% endblock %}