Unpublished blog posts in Magit
I use Emacs Org-mode to maintain this blog, and I keep those Org-mode source files in git (with Gitolite). I love this setup, because to me it is transparent, nearly fool-proof, and still gives me many of the benefits of a modern publishing system. In particular, I can write draft posts by just committing them to git without linking them from the main page. These get rendered and published, so I can share them with friends and ask for feedback; yet, since they're not linked, no one is going to read them before they're ready.1 [1 Unless they go looking, in which case, uhh, sure?]
One downside of this system is that I sometimes forget to later link the blog post to the main page. I wondered: how many such posts are there?
Finding unlinked pages
Since all of the posts are plain text, it's pretty easy to figure out
what links to what else. Since I want to find pages that exist but
aren't linked from the main page, I need to figure out what is linked
from the main page. grep
does that fine:
grep 'file:blog/[a-z0-9/-]\+.org' rss.org
This finds every link to an org
file in blog/
, which is roughly every
blog post. The output produced looks like this:
* [[file:blog/why-equality.org][Why Equality?]] :prog: * [[file:blog/paper-time-stats.org][Paper Statistics with TimeTracker]] :tools: * [[file:blog/css-floats.org][How CSS Floats Work]] :prog:algs: * [[file:blog/multi-command-line.org][Multi-command-line in Racket]] :prog:
The formatting is pretty regular because the RSS file has a pretty strict format, but I want to toss out the tags and page titles:
cut -d] -f1 | cut -d/ -f2-
The first cut
cuts off the post title and everything past that, while
the second one splits off everything up to blog/
:
why-equality.org paper-time-stats.org css-floats.org multi-command-line.org
So in total I have:
published_posts () { grep 'file:blog/[a-z0-9/-]\+.org' rss.org | cut -d] -f1 | cut -d/ -f2- ; }
Now, I also need the list of all blog posts:
all_posts () { find blog -name '*.org' -type f | cut -d/ -f2- ; }
I'm using find
instead of the more intuitive ls
because I sometimes
have blog series, like the Zippers series, that live in their own
folders.
Now that I have the list of published and all posts, I can get the
unpublished posts using set subtraction. And set subtraction is easy
in bash
using the little-known comm
command:
comm -13 <(published_posts | sort | uniq) <(all_posts | sort | uniq)
The comm
command takes two files as input (here using pipe
redirection) and splits lines into three buckets: those that appear
only in the first file, those that appear only in the second, and
those that appear in both. With the -13
flags it only outputs that
second category, lines that appear only in the second file, and in
this case that is blog posts but not published ones.
Making the list easy to access
The shell script was useful to audit the unpublished blog posts once in a while, but it was still too easy to put these unpublished posts out of mind. I wanted to push myself to make those final steps toward publication.
Every time I interact with my blog, I use Magit, which is by the way a
fantastic tool for interacting with git
, and I say this as someone
fully comfortable with the command-line interface. I wondered whether
it might be possible to make Magit display the unpublished blog posts
in the repository status page. That way, every time I interacted with
Magit, I'd see the list of unpublished posts and that would push me to
publish some.
Luckily, Magit has what it calls status sections. Each status section can write itself to the status page, and it can contain subsections; plus, each subsection has a type, and that type comes with automatic interactions (like clicking on a file to go to that file). To put my unpublished posts in Magit, I just needed to write my own status section.
A status section is just a function:
(defun my-magit-insert-blog-posts () "Insert section detailing my unpublished blog posts"
I wanted the status section to only show up if I have unpublished posts, looking forward to that glorious day when all my posts are published:
(-when-let (unpublished-posts (split-string (shell-command-to-string "bash etc/drafts.sh") "\n" t))
I don't actually know where -when-let
comes from (the dash
package,
perhaps?), but it's pretty useful! In this case it is executing the
drafts.sh
script I described earlier, splitting it on newlines, and
binding that to unpublished-posts
, but it is only executing the body
of the -when-let
when that bound value is non-empty, in other words
when I have unpublished posts.2 [2 The final t
argument to
split-string
tells Emacs to drop empty strings from the results;
without it, the final newline would always introduce an empty string
of a file.]
Now that I have my data, I have to draw it to the Magit status
section, and here I use a lot of special Magit
features. First, I
insert the section and its heading:
(magit-insert-section (blog-posts)
(magit-insert-heading "Unpublished blog posts:")
Here, blog-posts
is the section type; I made this one up, it doesn't
do anything. Now that I have the heading I need to insert a line for
each unpublished post. Here, Magit is pretty clever. Each unpublished
post is going to be its own status section, nested inside the
blog-posts
section and with the special file
type. The file
type comes
with some built-in behaviors, most importantly that hitting RET
while
on the file opens that file up.
(dolist (post unpublished-posts) (magit-insert-section (file (concat "blog/" post)) (insert (propertize (concat "blog/" post) 'face 'magit-filename) ?\n))) (insert ?\n))))
If you're not familiar with Emacs Lisp, dolist
is a for-in
loop, and
insert
writes text to the current window (buffer). For each post
, I
insert a file
section, and I pass that section an argument with the
file path (here, (concat "blog/" post)
computes that path). Inside the
section, I just write out the file name with a text property that
tells Emacs to use the magit-filename
text style (face
) to draw that text.
With this function all written, I need to ask Magit to actually use
this blog. It is not too hard! Magit has a special
magit-add-section-hook
function to add status sections; I just need to
make sure it is only called for my blog posts directory, /home/www/
:
(defun my-magit-setup-blog-posts () (when (equal default-directory "/home/www/") (magit-add-section-hook 'magit-status-sections-hook 'my-magit-insert-blog-posts 'magit-insert-untracked-files 'after t))) (add-hook 'magit-mode-hook 'my-magit-setup-blog-posts)
There's a lot going on here! magit-status-sections-hook
is what I pass
to make the status section show up for the repository status page. (I
assume you can also make status sections show up on diff, log, and
other screens.) I tell Magit to put the section after
the
untracked-files section. And finally, the t
that I pass to
magit-add-section-hook
tells Magit to make the modification locally. I
make sure that I am in the blog post directory before running that
command so the net effect is only changing how Magit works for that
one repository. Finally, I make Magit run this check every time I
start Magit; adding a section to is idempotent, so it can't hurt.
The result is quite nice, and I've already started thinking about finishing editing on some of those posts and publishing them—stay tuned!