Writing Your Own Changelog Generator with Git

Turn your commit messages into release notes for your users

Jacky Efendi
Oct 20, 2019 · 8 min read
Image for post
Image for post
Photo by Yancy Min on Unsplash

Changelogs are great. They give us a record of what changes in our project were made at a particular time. If you’re writing a library, it also gives your users awareness of what changed and gives the impression that your library is well-maintained and can be relied on. Tools such as conventional-changelog allow us to generate a very neat changelog easily, provided we adhere to their conventions on how to write commit messages. One of the most popular ones is Conventional Commits.

Image for post
Image for post
Example of a generated changelog in Angular’s repo

Under the hood, these tools use Git to do this. In this article, we’ll go through some steps to write our own fully-functional changelog generator!

What is Git?

Git is really powerful software. It’s a tool for collaborative software development. It’s used by almost every developer in the world! Even though I also use Git all the time, I’d be the first to admit that I don’t really know a whole lot about Git. I can do the basic stuff and understand some of the concepts, enough for me to be productive. But for the more advanced things you can actually do with Git? I have a long, long way to go. Seeing tweets like this makes me feel better about myself because it means that other developers are also discovering new stuff about Git.

Setting Up Our Repository

Let’s create a directory, and initialize it as a Git repository.

mkdir changelog-generator
cd changelog-generator
git init

In it, we’ll create a simple package.json file that only contains the version field. Think of this as a very simple JavaScript project repository. To make it simple, let’s just set "1" as the version.

"version": "1"

After that, let’s create a CHANGELOG.md file. Just leave it empty for now.

Image for post
Image for post

This should be how our project looks; we’ll commit this for now. In a conventional commit, that each commit message is required to be prefixed with one of the available prefixes like feat, fix, perf, refactor, chore, etc. For our repository, let’s say we have to use two prefixes, chore and feature. For our initial commit, let’s use the following command:

git add .
git commit -m "chore: Initial commit for changelog generator"

Now, just so we can have two commits in our repo, let’s create a file named index.js. Leave it empty; we’ll write code there later. Do another commit, and for this one, set feature: Added index.js script as the commit message.

Writing the Changelog Generator

Now we’re getting to the main part! If we type git log into our terminal, we’ll see git provides us with a list of the commits we have made so far in the repo.

commit 1c3ec7c03f2796790eaf7271ef47b2141b22cb63 (HEAD -> master)
Author: Jacky Efendi <not-a-real@email.com>
Date: Sun Oct 20 14:39:42 2019 +0700
feature: Added index.js scriptcommit 05fbe5c5eee29cc33065474800e5401370e7e929
Author: Jacky Efendi <not-a-real@email.com>
Date: Sun Oct 20 14:39:37 2019 +0700
chore: Initial commit for changelog generator

Whoa, that’s a lot of stuff! Remember, we want to create a changelog that is similar to the CHANGELOG.md in Angular’s repository. This means for every commit, we only need the commit message and the commit SHA1 hash. Fortunately, git log can be configured with different formats. If we type git log --format=%B%H, Git will give us only the raw commit body and the hash.

feature: Added index.js script
chore: Initial commit for changelog generator

Now, we can create a script that just runs this command, gets the string output and then transforms it into an array. Let’s write some code in index.js:

Basically, we are just running the git log while adding a —--—DELIMITER—--— string to help us split the string. We also filtered out the commit if it doesn’t have a SHA hash. If we run the script, we will see the following output.

➜ node index.js
{ commitsArray:
[ { sha: '1c3ec7c03f2796790eaf7271ef47b2141b22cb63',
message: 'feature: Added index.js script' },
{ sha: '05fbe5c5eee29cc33065474800e5401370e7e929',
message: 'chore: Initial commit for changelog generator' } ] }

Nice, now we have an array of commits, which are just objects with sha and message. We can use this array to write things into our CHANGELOG.md. Let’s do that for now. A lot of the code is just reading from a file, manipulating the string, and then writing the new string into a file, so I won’t bore you with the details. Here is the new code that can generate a changelog for us.

If we run our new code, we’ll see a nice changelog:

Image for post
Image for post

It works! Let’s manually bump the version in our package.json file to "2" and commit everything.

git commit -m "chore: Bump to version 2"

But, we have a problem. If we run the script again, we’ll see we have repeated items in the list:

Image for post
Image for post
Items in version 2 list should not appear again in version 3 list…

This happens because we are only running git log, which will return all the commits. What we want is to only get the logs from a particular commit, up to the current state. We need some kind of versioning.


The easiest way to achieve versioning using Git is by using Git tags. Basically, the purpose of Git tags is to tag a particular commit (duh). Let’s try creating one. Run git log in your terminal again:

commit 50df6552c5e709b38dfd915aad3fa8e07e2b86e1 (HEAD -> master)
Author: Jacky Efendi <not-a-real@email.com>
Date: Sun Oct 20 15:27:36 2019 +0700
chore: Bump to version 2commit 1c3ec7c03f2796790eaf7271ef47b2141b22cb63
Author: Jacky Efendi <not-a-real@email.com>
Date: Sun Oct 20 14:39:42 2019 +0700
feature: Added index.js scriptcommit 05fbe5c5eee29cc33065474800e5401370e7e929
Author: Jacky Efendi <not-a-real@email.com>
Date: Sun Oct 20 14:39:37 2019 +0700
chore: Initial commit for changelog generator

We see that the latest commit in our repo is the one with the hash 50df6552c5e709b38dfd915aad3fa8e07e2b86e1. yours will be different, so check it on your machine. Let’s tag this latest commit as version2. The way to do it is with the following command:

git tag -a -m "Tag for version 2" version2

The command will create an annotated tag, with “Tag for version 2” as the annotation, and version2 as the tag name. Now, let’s try running git log again:

commit 50df6552c5e709b38dfd915aad3fa8e07e2b86e1 (HEAD -> master, tag: version2)
Author: Jacky Efendi <not-a-real@email.com>
Date: Sun Oct 20 15:27:36 2019 +0700
chore: Bump to version 2

You can see that our commit now has tag: version2 written beside it. This means that the tag version2 is now referring to this particular commit, just like our current HEAD. Now, we can run git describe --long, and Git will tell us the latest tag we have in our Git repository.

➜ git describe --long

The output might seem a bit cryptic but is actually very simple. The string has three parts in it, all delimited by the character. Here is the explanation for each part:

  1. version2: The latest tag found in this Git commit history
  2. 0: The number of refs between the latest tag and the current HEAD
  3. g50df655: The abbreviated commit SHA of the current HEAD, prefixed with “g”.

What we care about here is the name of the latest tag. We can pass this information to git log. The git log command can accept two Git refs, and return only the logs between those two refs. For example, we can do git log version2..HEAD. Git will return nothing, because version2 and HEAD currently refer to the same Git commit. Let’s try adding a dummy.txt file and just commit it.

touch dummy.txt
git add .
git commit -m "chore: Added dummy.txt file"

Now, if we run git log version2..HEAD again, we will see only one commit.

commit b9491e3c9a22bddff528e48fd599b08c0eafcce1 (HEAD -> master)
Author: Jacky Efendi <not-a-real@email.com>
Date: Sun Oct 20 15:45:19 2019 +0700
chore: Added dummy.txt file

I hope now you get the idea of what we are going to do with these commands; let’s get back to coding!

Writing the Changelog Generator (Continued)

In the first part of our code, we now want to run git describe --long to get the latest tag, and only run git log from that tag until our current HEAD.

In the last part of our code, we want to now automatically update the version number in the package.json file, create a commit, and tag that commit as the new version:

Now, let’s commit our new changelog generator:

git commit -m "feature: Implemented versioning for the changelog generator"

And let’s run the script again to generate another changelog:

Image for post
Image for post
It works!

Now, we have a fully functional changelog generator with support for versioning as well.

To see if this is really working, let’s put some text into the dummy.txt file and commit it. Then, we’ll run the script again.

Image for post
Image for post
Yep, everything still work!

Now you can just push all the tags to your remote repository so that they appear there as well. Simply run git push --tags to do so. In GitHub web, you can see the list of tags on the tags page.


We basically only needed git log, git describe, and git tag combined with some scripting to make this work. This is actually very similar to what tools like conventional-changelog do under the hood to give you that simple generated changelog. Of course, our generator is very simple and definitely doesn’t handle as many cases as conventional-changelog does, but the core idea is similar. By enforcing a specific format of writing the commit message, we can automate changelog generation.

When handling monorepo though, things get more difficult. lerna, a tool to manage a monorepo, has its own scripts, combined with conventional-changelog to handle monorepo uses cases. If you are curious and want to learn more, try improving on this generator to handle monorepo as well. I am sure you will learn much more in the process!

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store