Pavel Panchekha


Share under CC-BY-SA.

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/-]\'

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?]] :prog:
* [[file:blog/][Paper Statistics with TimeTracker]] :tools:
* [[file:blog/][How CSS Floats Work]] :prog:algs:
* [[file:blog/][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/:

So in total I have:

published_posts () { grep 'file:blog/[a-z0-9/-]\' | 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/") "\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 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!



Unless they go looking, in which case, uhh, sure?


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.