July 22nd, 2010 @ 01:18

A school friend, namely p4bl0, mentioned the idea of maintaining blog posts with git and a set of hooks which would produce the blog html from the contents of the repo. I loved the idea, but thought I could push it a little further : a blog engine which would use no other storage than git, with the post subject and contents being the commit message subject and contents. A post-commit or post-receive hook then produces the html. As simple as that !

You can find the source in BloGit git repo, and see an example at BloGit example. To use the source, you first have to pack it (using the pack script), which will merge the raw_post and raw_produce, producing a single post script (which I also included at the end of this post), which you can simply put in an empty directory and run it. It will unpack the other script (produce), initialize the git repo, and set the hooks. It’ll then prompt you for your post title and then open an editor for you to set your post contents. Save the file, and you’re done with your first post : check the index.html file which has been produced in the same directory. You can write your own stylesheet in the blogit-style.css file. Further posts can be done with the same post script.

Yet, the best way is probably just to use the usual git workflow. To initialize the repo and all, run post --unpack, and to post post --raw or git commit --allow-empty (when using git commit, leave a blank line between the subject line and the rest of the post). You can also amend existing commits (using git commit --amend), use the GIT_AUTHOR_* environment variables to change the author, and so on. Since merge commits are skipped by the html generator, it should work just great for multi author blogging !

PS : I know this is JUST a git log pretty printer, and that the whole thing is pretty much trivial. I also know that using versionned files to store the posts would allow a lot of extra bonuses (such as automatically adding “Updated on …” mentions based on the commit log of each single file). I just thought the idea was fun :p There are probably a lot of things to improve, or a lot of smart git features to use there that I overlooked. Feel free to leave a line :)

#!/usr/bin/env python
"""
 bloggit
 Author : Guillaume "iXce" Seguin
 Email  : guillaume@segu.in (or guillaume.seguin@ens.fr)
 Copyright (C) 2010 Guillaume Seguin
 This program is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License
 as published by the Free Software Foundation; either version 2
 of the License, or (at your option) any later version.
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.
 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 51 Franklin Street, Fifth Floor,
 Boston, MA  02110-1301, USA.
"""
import subprocess, os, sys
import base64, bz2
CONTENTS_FILE = "POST_CONTENTS"
repo = os.path.dirname (os.path.abspath (__file__))
os.chdir (repo)
if not os.path.exists (os.path.join (repo, ".git")):
    subprocess.call (["git", "init"])
produce_path = os.path.join (repo, "produce")
produce_script = bz2.decompress (base64.b64decode ("""
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
6MrbOTHwIixlYyW5xO7y/+LuSKcKEgD+5O2A==
""".strip ().replace ("\n", "")))
if not os.path.exists (produce_path):
    open (produce_path, "w").write (produce_script)
    os.chmod (produce_path, 0755)
for hook in ("post-receive", "post-commit"):
    hook_path = os.path.join (repo, ".git", "hooks", hook)
    if not os.path.exists (hook_path) \
     or os.path.realpath (hook_path) != produce_path:
        if os.path.lexists (hook_path): os.remove (hook_path)
        os.symlink (produce_path, hook_path)
if "--unpack" in sys.argv:
    print "Unpack only"
    raise SystemExit
if "--raw" in sys.argv:
    p = subprocess.Popen (["git", "commit", "--allow-empty"])
    p.communicate ()
else:
    title = raw_input ("Post title ? ")
    editor = "vi"
    if "VISUAL" in os.environ:
        editor = os.environ["VISUAL"]
    elif "EDITOR" in os.environ:
        editor = os.environ["EDITOR"]
    contents_path = os.path.join (repo, CONTENTS_FILE)
    if os.path.exists (contents_path):
        os.remove (contents_path)
    ret = subprocess.call ([editor, contents_path])
    if ret == 0 and os.path.exists (contents_path):
        contents = open (contents_path).read ()
        os.remove (contents_path)
    else:
        print "A problem occured while editing the contents file"
        raise SystemExit
    p = subprocess.Popen (["git", "commit", "--allow-empty", "-F", "-"],
                          stdin = subprocess.PIPE)
    p.communicate ("%s\n\n%s" % (title, contents))
if p.returncode == 0: print "New post successfully added"
else:
    print "Couldn't add new post"
    sys.exit (1)