A funny thing happened on the way to this commit message

Posted by Romain Pouclet on March 26, 2018

A few years ago, I was working on a backend application that created a lot of temporary files. In an attempt to add a command to remove those files automatically, I created a script that removed everything… including itself. As I’m writing this, I’m pretty proud to announce that I outdid myself.

My commit message editor

“Commitment” is a tiny macOS app that I created for myself to write pretty commit messages. Most of my use of git happens in the command line because that’s what I know best, but I’ve never been happy using a regular text editor to write commit messages.

It’s a window that lets me write commit messages but it does a couple of cool things like:

  • It highlights issues and pull request references in the editor, as well as mentions of GitHub users.
  • For all the references found, it performs request to the GitHub API and display details about the issues or the user.

Commitment lets me tinker with Cocoa programming while also making me write better commit messages.

Parsing command line tools argument

Creating a custom editor to write your git commit is fairly simple. Git will invoke your application with some arguments, one of those arguments is a path to a file named COMMIT_EDITMSG. Once your editor of choice exits, git reads the content of this file and use it as your commit message. This approach lets you use any tool that can write into a file.

Command line arguments in Swift can be read from the Commandline class, like so:

CommandLine.arguments.forEach { print("Argument: \($0)") }

My very first approach was to look for the first argument that contained the path to this COMMIT_EDITMSG file.

It roughly looked like this:

let path = CommandLine.arguments.first { $0.components(separatedBy: "/").last == "COMMIT_EDITMSG" }

While this worked, there were some cases where this wasn’t the best approach. Sometimes, the file isn’t named COMMIT_EDITMSG, for other tools like hub, that uses the editor configured for git to create a pull-request. There were also sometimes when I wished I could have used Commitment to write a tiny bit of text with references to GitHub users and pull-requests I had worked on.

Trusting my test suite, I though I could just tweak the part of my code that was looking for the file, and instead of looking for the first file named COMMIT_EDITMSG, I would just return the first file that existed, like so:

let path = CommandLine.arguments.first(where: FileManager.default.fileExists)

I made sure my updated test suite passed and started using my own app which, for some reason, only worked after a fresh install.

What happened?

It is quite funny to take all these programming notion for granted and make dumb mistakes like this one. I say a lot of (bad) things about bash scripting, but I’ve been relying on it to automate most of my tasks. The first thing they teach you in bash school is that the first argument passed to a script or a program, is always the path to the program itself.

My clever code always use the first argument that contained the path to a file that existed, which in my case, always was Commitment itself! Once I was done writing my commit message, it wrote it into… the binary itself.

The content of my binary has been replaced with the word coucou

No wonder my app stopped working the second time.

Conclusion

This was an easy fix once I realized my mistake. I figured it would make an interesting story for people new to development. I’m sure that the student that I was in school would have loved it.