Excluding ‘Note to Self’ Code Comments from Commits with Bash
Tags: bash, git, productivity, tutorial
Published: 2020-03-22 22:41:30


# The Premise
A lot of times when I'm working on a feature, I'll put these little 'note to self' comments in my code to remind me to do something later, like handle some future condition or link in some future aspect of the feature. 

Something like this:
```
// once we have LoadType, we should probably
// refactor to return Observable<LoadType>
func load() { 
```
Or even less flattering:
```
controller!.load() // DANGER!! ADD BETTER NULL HANDLING HERE
```

Sure, these reminders could go somewhere else, but putting them right in the code eliminates the otherwise huge probability of me fuddying it up.

The issue with this approach arises when it's time to make a commit, but I've still got these 'note to self' (n2s) comments all over the place. I don't want my scatterbrained self-instructions to be immortalized in the repo, and I don't want to subject the reviewer to them either. So at this point I usually either:

1. Painstakingly search for and delete all of these comments before making the commit, often forgetting to re-add them, occasionally causing my future self to fuddy it up in the ways I was trying to prevent.

2. Put off making the commit until I've implemented all the things the comments are reminding me to do, resulting in a commit that's larger than it should be.

1 wastes time and is error-prone, and 2 is unfair to the reviewer and the history of the repo.

I've recently vowed to improve my bash skills, so I decided to try to come up with a solution to this problem using bash. This tutorial goes through my process of building out the script, which will be available at the end.

*Disclaimers before diving in:*
- There are some shortcomings to my approach that I'll outline at the end. I have potential solutions to most of them which I might explore in a follow up post, but I want to keep this post from getting too long.
- I don't claim to be a bash expert, any suggestions/improvements to this approach are greatly appreciated!

# Building the Solution in Bash
The goal is to use git commands and bash programs to somehow:
1. Remove the n2s comments from the code
2. Make the commit
3. Reapply the n2s comments to the code

Let's say we've got our code ready, all the files are added to git, and we're ready to commit but we want to exclude our n2s comments.

The first step is to get a list of files that we need to process i.e. any files that were created or modified. This git command does just that:
```
git diff --cached --name-only --diff-filter=MA
```
Which returns something like:
```
src/ComedicTimer.swift
src/PrankDialer.swift
```
`--cached` tells git to only report changes in the staging area, `--name-only` tells git to return just the relative paths of files to be included in the commit, and `--diff-filter=MA` tells git to only report changes that are modifications or additions. This is helpful because we don't need to worry about deleted or renamed files.

Now that we've got our paths to operate on, we're going to start by focusing on one of these paths. In the end, we'll just loop through all the paths applying the solution to each file one at a time. For now, let's look at `src/PrankDialer.swift`.

First we're going to copy the file somewhere before we start operating on it. This will make it easy to restore the commented-state of the file after we've made the commit:

```
cp src/PrankDialer.swift src/PrankDialer.swift.precommentstrip
```

Now that we've got our backup, it's time to strip out the comments from the original file. We're going to do this by:
1. Getting all the additions to the file that have a single line comment in them
2. Slicing the comment off of the change
3. Replacing the original addition with our new comment-free addition

Ok, let's take a look at what changes we've got in src/PrankDialer.swift with `git diff --cached src/PrankDialer.swift`:

```
diff --git a/src/PrankDialer.swift b/src/PrankDialer.swift
index 7f11ff9..2f7281b 100644
--- a/src/PrankDialer.swift
+++ b/src/PrankDialer.swift
@@ -2,9 +2,12 @@
 class PrankDialer {
        let pranks: [Prank]
        let phoneNumbers: [PhoneNumber]
+       let comedicTimer: ComedicTimer // we might want to pass this in as a parameter instead of retaining it here
 
        func prankDial(with number: PhoneNumber) {
                let prank = pranks.random()
-               prank.dial()
+                // Dial should probably eventually return an Observable when we implement calling,
+                // if we do that we'll want to return the Observable from this function as well.
+               prank.dial(number, with: comedicTimer)
        }
 }
```
That's a mess, but it's got the information we need. Time to clean it up. Let's pipe that output to `awk`, and filter for lines that start with a `+` (signifying an addition) and contain `//`.

```
git diff --cached src/PrankDialer.swift | awk '($1 ~ /^\+/) && (/\/\//)'
```
Which gives us:
```
+	let comedicTimer: ComedicTimer // we might want to pass this in as a parameter instead of retaining it here
+		 // Dial should probably eventually return an Observable when we implement calling,
+		 // if we do that we'll want to return the Observable from this function as well.
```
Much better! Now we've identified the new lines with comments in them, but we still need to cut off the leading `+` as this will mess up our replacing logic. Let's pipe the output once more to `cut`:
```
git diff --cached src/PrankDialer.swift | awk '($1 ~ /^\+/) && (/\/\//)' |  cut -c 2-
```
Now we've got:
```
	let comedicTimer: ComedicTimer // we might want to pass this in as a parameter instead of retaining it here
		 // Dial should probably eventually return an Observable when we implement calling,
		 // if we do that we'll want to return the Observable from this function as well.
```

Perfect! We've got the lines in the file that we need to strip comments from. 

We'll loop through these lines when we make our script, but for now,  we're just going to focus on the first line. Let's assign it to a variable to make our upcoming commands more readable:
```
addition="       let comedicTimer: ComedicTimer // we might want to pass this in as a parameter instead of retaining it here"
```

Next, let's use `awk` with a custom field separator to split that comment off:
```
echo "$addition" | awk -F'//' '{print $1}'
```
Which yields:
```
       let comedicTimer: ComedicTimer 
```
Looking good, but there's still a pesky trailing space that we'd prefer not to include in the commit. Let's take care of that by trimming all trailing whitespace with `sed`:
```
echo "$addition" | awk -F'//' '{print $1}' | sed 's|[[:blank:]]*$||'
```
Which gives us a nice clean output to use in our replacement:
```
       let comedicTimer: ComedicTimer
```

Again, for readability, let's set a variable:
```
commentFreeAddition="$(echo "$addition" | awk -F'//' '{print $1}' | sed 's|[[:blank:]]*$||')"
```

And now the replacement becomes trivial with `sed`:
```
sed -i '' "s|$addition|$commentFreeAddition|" src/PrankDialer.swift
```
Note that we're using the `-i` flag here to tell `sed` to do the replacement in place i.e. actually edit the file. The empty `''` is to support Mac's `sed`, which requires a backup extension (even if it's empty) when using `-i`.

Now that we've made the replacement, let's take a look at the change we've made by running `git diff src/PrankDialer.swift`:

```
diff --git a/src/PrankDialer.swift b/src/PrankDialer.swift
index 2f7281b..8e146c1 100644
--- a/src/PrankDialer.swift
+++ b/src/PrankDialer.swift
@@ -2,7 +2,7 @@
 class PrankDialer {
        let pranks: [Prank]
        let phoneNumbers: [PhoneNumber]
-       let comedicTimer: ComedicTimer // we might want to pass this in as a parameter instead of retaining it here
+       let comedicTimer: ComedicTimer
 
        func prankDial(with number: PhoneNumber) {
                let prank = pranks.random()
```
We've successfully lopped off the comment!

There is one complication, on lines where it's just a comment by itself, this replacement approach will leave an empty line. It's probably better to just delete the line in that case.

So if we detect an empty `$commentFreeAddition`, we'll just delete it with:
```
sed -i '' "\|$addition|d" "$path"
```

After we've done this replacement/deletion for all additions in all of our paths, we'll simply `git add` the paths, make the commit, and `mv` our `.precommentstrip` backups back to their original paths.

Ok, now it's time to put all these pieces together into a script that we can call.

# The Script
The script looks like this:
```
#!/bin/bash

paths="$(git diff --name-only --cached --diff-filter=MA)"

echo "$paths" |
while IFS= read -r path; do
  cp "$path" "$path.precommentstrip"
  git diff --cached "$path" | awk '($1 ~ /^\+/) && (/\/\//)' | cut -c 2- |
  while IFS= read -r addition; do 
    commentFreeAddition="$(echo "$addition" | awk -F'//' '{print $1}' | sed 's|[[:blank:]]*$||')"
    if [[ -z "$commentFreeAddition" ]]
    then 
      sed -i '' "\|$addition|d" "$path"
    else
      sed -i '' "s|$addition|$commentFreeAddition|" "$path"
    fi
  done
  git add "$path"
done

git commit

echo "$paths" |
while IFS= read -r path; do
  mv "$path.precommentstrip" "$path" 
done

```
We _could_ put this script in a standard `.sh` file and call directly to it, but wouldn't it be nice if we could make this an extension of git somehow?

Good news, we can! By putting this in a file named `git-<command_name>`, making the file executable, and putting it somewhere accessible from `$PATH`, we'll be able to call it with git like `git <command_name>`. 

For example, I put the script in a file at `/usr/local/bin/git-commitn2s` and made it executable with `chmod +x /usr/local/bin/git-commitn2s`.

Now if I enter:

```
git commitn2s
```

And input a commit message, the commit will be made with 'note to self' comments not in it, but still preserved in the unstaged area!

`git show` will show the commit without comments:
```
commit 91ea7543acf2acb0f3c38a918377803660688298
Author: <omitted>
Date:   Sun Mar 22 14:33:42 2020 -0400

    Commit without n2s!

diff --git a/src/ComedicTimer.swift b/src/ComedicTimer.swift
new file mode 100644
index 0000000..882c00d
--- /dev/null
+++ b/src/ComedicTimer.swift
@@ -0,0 +1,5 @@
+
+class ComedicTimer {
+       let internalTimer: Timer
+       let audienceTimer: Timer
+}
diff --git a/src/PrankDialer.swift b/src/PrankDialer.swift
index 7f11ff9..0dc7744 100644
--- a/src/PrankDialer.swift
+++ b/src/PrankDialer.swift
@@ -2,9 +2,10 @@
 class PrankDialer {
        let pranks: [Prank]
        let phoneNumbers: [PhoneNumber]
+       let comedicTimer: ComedicTimer
 
        func prankDial(with number: PhoneNumber) {
                let prank = pranks.random()
-               prank.dial()
+               prank.dial(number, with: comedicTimer)
        }
 }

```
And `git diff` will show the comments still present in the unstaged area:
```
diff --git a/src/ComedicTimer.swift b/src/ComedicTimer.swift
index 882c00d..69296db 100644
--- a/src/ComedicTimer.swift
+++ b/src/ComedicTimer.swift
@@ -1,5 +1,5 @@
 
 class ComedicTimer {
        let internalTimer: Timer
-       let audienceTimer: Timer
+       let audienceTimer: Timer // make this a subclass of Timer 
 }
diff --git a/src/PrankDialer.swift b/src/PrankDialer.swift
index 0dc7744..c06e602 100644
--- a/src/PrankDialer.swift
+++ b/src/PrankDialer.swift
@@ -2,10 +2,12 @@
 class PrankDialer {
        let pranks: [Prank]
        let phoneNumbers: [PhoneNumber]
-       let comedicTimer: ComedicTimer
+       let comedicTimer: ComedicTimer // we might want to pass this in as a parameter instead of retaining it here
 
        func prankDial(with number: PhoneNumber) {
                let prank = pranks.random()
+               // Dial should probably eventually return an Observable when we implement calling,
+               // if we do that we'll want to return the Observable from this function as well.
                prank.dial(number, with: comedicTimer)
        }
 }
```

Pretty cool! We've accomplished what we set out to do and hopefully picked up some interesting bash knowledge along the way.

Soo problem totally solved, right? Absolutely no room for improvement, right?? Well...

# Caveats and Future Improvements
Here are some limitations of the current script, roughly ordered in decreasing order of egregiousness:

1. This approach strips out all single line comments, even ones that you might intend to include in the commit. One workaround could be to establish a different style for legitimate comments and n2s comments. Maybe something like:
`// This comment has a leading space signifying it's meant to be committed`
`//This comment does not, signifying it's a n2s`
Then the script would need updated logic to only strip single line comments without a leading space.

2. Right now, the delimiter we use in `sed` must not be present in lines being replaced. The script uses `|` but that is a valid character in lots of programming languages. We need to explicitly escape any valid `|` in our `sed` inputs to properly sanitize them.

3. The script has `//` hardcoded as the single line comment characters. To better support languages with different comment characters, we need to make this configurable.

3. If comment characters exist in a string literal, the script will mistakenly cut off mid-string literal. Consider this problematic scenario:
`let url = URL("https://server.com") // A comment`
The script should ideally use a more sophisticated regex to detect comment characters that are outside of quotes.

4. If a file contains other instances of the line we are replacing, `sed` will also replace it. I don't have great solution for this at the moment, I'm open to suggestions.

5. It'd be nice if the script accepted a `-m 'commit message'` parameter.

6. I'm sure there are other issues that I'm overlooking, let me know of anything you can think of!

The good thing is even with these limitations, this script shouldn't be destructive and you should at least be able to get back to your pre-commit state fairly easily if something goes wrong.

If I get some time, I'll make a follow up post implementing some of these proposed solutions and any other improvements people suggest.

# Shameless Newsletter Plug
I'm starting an educational bash newsletter to help share my learnings along the way as I improve my bash skills. It's totally free, check it out:
[https://incommand.dev](https://incommand.dev/)

# Gist
[Gist available here](https://gist.github.com/bfichter/4cb40793d315025d6e0bb3aa980aaecc), comments/suggestions are welcome!