A Better Way to Write Commit Messages in VS Code

Posted:
Tags:

Visual Studio Code is my favorite software application. (Is that weird?)

Here are some things it’s got going for it:

It also has the best Git GUI that I’ve used.1 Being, as I am, a staunch advocate of the command line,2 I was surprised to find myself more and more consistently opening VS Code to review and stage non-trivial commits, even when I had done the actual editing in a different application. And sure, it’s all well and good to leverage the strengths of multiple applications in concert; but the trouble was, as much as I appreciated VS Code for preparing commits, I found the experience of actually writing commits to be an exercise in frustration.

It’s fine if you’re writing a one-line message. It even tells you if you exceed the traditional 50 character limit! But for anything longer than that—especially when adhering to the tenets of conventional formatting—it starts to feel substantially inferior to good ol’ Vim; and if my changes are substantial enough that I’m using VS Code to review them, odds are I’m going to have a few things to say. I may be willing to use one application to write code and another to stage commits, but firing up a third just to write commit messages is crossing a line.

I’m clearly not alone in this opinion, as there’s been a Github issue open since 2017 requesting the ability to write commits with a full editor tab, like any other file. It finally saw some momentum in April with the appearance of a pull request; and, indeed, I’ve held off writing on this topic in the expectation that it would soon be merged and such a post would be immediately redundant. Alas, four months (and four releases) have now elapsed, with no indication that the feature will be integrated anytime soon, so I have decided at last to share my workaround.

In January, VS Code v1.42 introduced user-level tasks; in March, I realized I could use one to invoke git commit using VS Code as the editor:

{
  // See https://go.microsoft.com/fwlink/?LinkId=733558
  // for the documentation about the tasks.json format
  "version": "2.0.0",
  "tasks": [
    {
      "label": "git commit --verbose",
      "type": "shell",
      "command": "GIT_EDITOR=\"code --wait\" git commit --verbose",
      "presentation": {
        "echo": false,
        "reveal": "silent",
        "focus": false,
        "panel": "shared",
        "showReuseMessage": false,
        "clear": false
      },
      "problemMatcher": []
    }
  ]
}

The GIT_EDITOR=\"code --wait\" bit temporarily sets an environment variable which tells Git to use VS Code as its default editor, rather than my actual default (which is Vim). Unfortunately, this is a Bash convention that doesn’t work in Windows, which I am forced to use at work, and so for the next five months I patiently continued to execute my awkward dance between Visual Studio, VS Code, and the terminal. Then finally, in July, I chanced upon the realization that I could use a process task rather than a shell task, and pass the GIT_EDITOR variable in a shell-agnostic fashion:

{
  "label": "Git: Commit",
  "detail": "git commit --verbose",
  "type": "process",
  "command": "git",
  "args": [
    "commit",
    "--verbose"
  ],
  "options": {
    "env": {
      "GIT_EDITOR": "code --wait"
    }
  },
  "presentation": {
    "echo": false,
    "reveal": "silent",
    "focus": false,
    "panel": "shared",
    "showReuseMessage": false,
    "clear": false
  },
  "problemMatcher": []
}

And there was much rejoicing.

The really neat thing about this approach—and the one reason why it might stay relevant if and when that pull request finally merges—is that you can change up the args for different scenarios. For instance, I added another task (otherwise identical) to amend my last commit:

"args": [
  "commit",
  "--verbose",
  "--amend"
],

Occasionally, I’ll receive some updated code, documentation, or configuration files from a coworker or contractor unfamiliar with Git, so I made another task that prompts me for an author. This one is paired with an inputs array elsewhere in the file, outside of the tasks array:

"tasks": [
  ... other tasks ...
  {
    "label": "Git: Commit as Different Author",
    ... same content as above ...
    "args": [
      "commit",
      "--verbose",
      "--author",
      "${input:gitAuthor}"
    ],
    ... same content as above ...
  }
],
"inputs": [
  {
    "id": "gitAuthor",
    "description": "Commit Author:",
    "type": "promptString"
  }
]

When I invoke this task, an input box appears at the top of the window, allowing me to enter the name of whomever actually wrote the changes which I’m committing on their behalf.

For the best experience, you can add a custom keybinding to invoke a specific task, or just to pull up the list of tasks. I repurposed Ctrl+Shift+T to open the task list (T for task), as running tasks is something I do far more often than reopening closed editors.

I hope that someday, the option to use a full editor tab for commit messages will indeed be part of VS Code’s native Git experience. In the meantime, however, my humble tasks are working great!

Footnotes

  1. Caveat: the only other Git GUI I’ve used is Visual Studio’s. Also, very briefly, the GUI that comes with Git for Windows. ↩︎

  2. I really really like typing, okay? ↩︎