DEV Community

Cover image for Another list of tips and tricks to improve your Git workflow
Jody Heavener
Jody Heavener

Posted on

Another list of tips and tricks to improve your Git workflow

I did somewhat of a deep dive into Git recently, and I figured I could share some of the things that helped me greatly improve my workflow and overall understanding of how Git works. This article draws from various videos and articles, so be sure to check out the resources at the bottom of this post for even more details into each topic!

This post assumes a basic understanding of Git.

Sign your commits

As I started working on open source projects I discovered that a common rule for contributing is that you need to sign your commits. This makes sense, as projects want to maintain the integrity of commits and the code contributed to it. It makes sense to do it even if you’re not working on open source projects, too.

Commit signing is a way to verify that the person who pushed commits to the repository is in fact the actual person who created the commit. Here’s a more detailed explanation of what commit signing helps protect against:

There are a number of ways in which a git repository can be compromised (this isn’t a security flaw, just a fact of life—one should not avoid using git because of this). For example, someone may have pushed to your repository claiming to be you. Or for that matter, someone could have pushed to someone else’s repository claiming to be you (someone could push to their own repository claiming to be you too).

Rather than rewrite the wheel here I’m going to link to a fantastic post from Rémi Lavedrine, where they break down how to set up commit signing and getting it set up with the major Git services.

Find the commit that broke everything by bisecting

The feature’s broken? It was working just fine two months ago! What changed?

In order to effectively use bisect you need three things:

  1. A test to determine if things are broken. If it’s a manual test that’s fine, but this is also where good test coverage can save you.
  2. A commit where things were working. You can use git log to go back in history, checking out older commits to see where the thing that is broken works again.
  3. A commit where things are broken. This would generally be the most recent commit.

We can now use bisect to perform a binary search that will find the commit where things went from good to bad. Step by step, here’s how it works:

# Tell Git we’re entering bisect mode
git bisect start
# Check out the commit where things are bad
git checkout 8ebbad
# Tell Git that this is a commit where things are bad
git bisect bad
# Check out the commit where things are good
git checkout a526ff
# Tell Git that this is a commit where things are good
git bisect good
Enter fullscreen mode Exit fullscreen mode

Now Git will check out the commits in between the two you’ve provided, asking you to test it and determine it’s working or still broken each time. If it’s working, you’ll run git bisect good, and if not you’ll run git bisect bad. From this you gain more information about which commit introduced a breaking change.

If you’ve got an automated test you can actually set up a script to pass to Git (git bisect run and it will use this to automatically find the culprit.

Stage and commit in a single command:

Instead of adding and committing in two steps:

git add .
git commit -m "Big changes"
Enter fullscreen mode Exit fullscreen mode

For commits to changes on existing files it’s as simple as:

git commit -am "Big changes"
Enter fullscreen mode Exit fullscreen mode

The -a flag will “tell the command to automatically stage files that have been modified and deleted,” but importantly, “new files you have not told Git about are not affected.”

Quickly update your most recent commit

Let’s say you forgot to add the ticket number to a commit message. Or maybe you forgot to include a file in your commit. You could create a new commit, or interactive rebase to reword the message, sure, but if it’s a simple change just “amend” the commit:

git commit -am "Big changes" # original commit
git add some-file.txt # add your missing file, if there is one
git commit --amend -m "#2947 - Big changes" # amended commit
Enter fullscreen mode Exit fullscreen mode

This will simply update the message of the commit, but not any of the changes.

Fun tip:
Every commit in your repository has a long string of characters assigned to it, called a commit hash. The commit hash is generated based on the parent, the contents, and the message of that commit. In theory, because of the specificity involved in generating it, no two commits ever in existence should have the same hash.

Stop forcing it. Lease it, instead.

You’ve probably been burned by git push —force. If you haven’t maybe you should. Or at the very least someone should have yelled at you about it by now.

When you force push you’re effectively saying, “I don’t care what you think happened here, and I don’t care if others have pushed here since I last pulled - the timeline I’m presenting is fact.” This can sometimes be helpful (maybe you’ve just rebased), but at worst can be damaging as there is little recourse for this action. Unless you want to spend time trawling around the reflog you might be out of luck if you force push when you shouldn’t have.

Instead, you should consider force pushing with a lease. I think this Atlassian blog post put it best:

What --force-with-lease does is refuse to update a branch unless it is the state that we expect; i.e. nobody has updated the branch upstream. In practice this works by checking that the upstream ref is what we expect, because refs are hashes, and implicitly encode the chain of parents into their value.

What this means is that when you go to force push Git will check the local ref head against the remote ref and if they do not match the force will not continue. Magic.

Move something you accidentally committed to master over to a feature branch

Let’s say you were working on a cool new feature, meant for a feature branch, but you accidentally committed it to master. Oh no!

Fear not. You just need to cherry pick it. The first thing you’ll want to do is find the commit hash of the commit you just made. You can use git log to see this.

Once you’ve copied your commit hash, go ahead and check out your feature branch. The next step is to perform something called a cherry pick, which essentially clones your commit to the branch you’re on:

git log # retrieve your commit hash
# Let’s say your commit hash is
# 3e59227225678186a93efe0f3bc207d6483989af
git checkout my-new-feature # check out the feature branch
git cherry-pick 3e5922
Enter fullscreen mode Exit fullscreen mode

You’ll notice we only supplied the first 6 characters of the commit hash. This is because Git is smart enough to identify your commit with just the first few characters of its hash.

Note that we only just cloned the commit to the feature branch, we did not delete the original commit on master. So how do we remove that accidental commit from the master branch? Let’s reset!

git reset changes the branch pointer to point to a different commit in your repository. We can use the HEAD reference to determine which commit it should point to. HEAD represents the commit you’re currently sitting on, and we can use the carat (^) to step back in commits:

HEAD # the commit we’re currently sitting on
HEAD^ # the parent of the commit we’re currently sitting on
HEAD^^ # the grandparent of the commit we’re currently sitting on

# You can use ^ to step back further and further in commits
Enter fullscreen mode Exit fullscreen mode

Another, sometimes quicker, way to step back in time is to use the tilde (~) with HEAD:

HEAD~5 # the same as HEAD^^^^^
Enter fullscreen mode Exit fullscreen mode

In our case, since we just want to remove the most recent commit, we can run git reset --hard HEAD^ to take us one step back. Our erroneous commit is no longer being pointed at.

Fun tip:
In Git, there is no such thing as a branch object. Branches are merely labels that point to commits. Over time, using various commands, you can change where those labels point to in order to change how those branches are structured.

Get a little interactive with your commits

There are some ideas discussed in this post about rewording and rearranging commits, and they are indeed helpful in their own way, but there’s an even more advanced way of modifying and manipulating your commits, and it’s called an Interactive Rebase.

You can enter interactive rebase mode by passing the -i flag to your rebase command: git rebase -i HEAD~5. This will open a list of the last 5 commits in your designated Git editor.

You can rearrange these this list of commits if you need to change the order in which they occur. The commits will be re-executed from top to bottom. Below your list of commits there will also be a list of keywords you can prepend your commit with in order to perform that action. Here are a few of them in a little more detail:

  • p, pick You’ll notice this is the default keyword before each commit, and it’s simply meant to signify that this commit is good to use. You can leave it in place if you’re not performing another action on that commit.
  • r, reword Place this keyword before a commit to reword the commit message. When you hit save on the file Git will re-open again, asking you to enter a new commit message for the specified commit.
  • e, edit You want to use this commit, but you want to make changes first. When you hit save on the file Git will, in the command line, prompt you to make your changes and then continue with git rebase --continue.
  • s, squash If you’re familiar with squashing this will sound familiar; use this keyword to squash this commit into the previous commit. Once you hit save on the file Git will prompt you to enter a new commit message for this newly melded commit.
  • f, fixup This is similar to squash, except it automatically uses the previous commit’s message.
  • d, drop This will drop the commit entirely. You can alternatively delete the line in your editor. As a reminder, interactive rebases are re-executed from top to bottom, so you need to be careful that the file changes in the commit you’re dropping aren’t further operated on where the initial change no longer exists.

There are additional options, x, exec, l, label, t, reset, and m, merge, that can also be handy, but are a little more advanced and probably won’t be something you use every day, so I’ll leave those for now.

Bonus: Git Extras

I’m probably preaching to the choir with this, but if you’re not already using Git Extras in your workflow, do yourself a favour and give it a whirl. This extension to the git CLI provides you with over 60 additional tools that range from novelty to daily use.

Here are some examples:

# Run git commands without typing 'git'
git repl
# Delete branches that have been merged
git delete-merged-branches
# Merge commits from new_feature into master
git graft new_feature master
# Output jodyheavener’s contributions to a project:
git contrib jodyheavener
Enter fullscreen mode Exit fullscreen mode

Bonus: Node GH

Yeah, yeah, not everyone uses GitHub. We get it. But if you do you might benefit from this nifty little CLI that allows you to interact directly with GitHub.

The utility has a variety of helpful commands that allow you to drastically reduce the need to leave your command line. Here are some examples:

# List all your open Pull Requests
gh pr --list --me
# Comment on PR #52
gh pr 52 --comment "This is amazing!"
# Create an issue using your default editor to type the message
gh is --new --title "Error occurs when…" --message
# Close issue #81
gh is 81 --close
# List all gists
gh gi --list
# Create a new repo and clone it into the current dir
gh re --new booyah --clone
Enter fullscreen mode Exit fullscreen mode

Update: Right as I posted this I saw that the official GitHub CLI entered beta. Give it a spin for me, would ya?

There appears to also be a gitlab CLI! But I haven't tried it, so GLHF.

Final Bonus: Conventional Commits

This isn’t a tool so much as it’s an approach to uniform, understandable Git commits (well, there is a tool). The Conventional Commits method defines a spec so that each commit’s message belongs to a type and an optional scope, followed by your commit’s description, body, and footer.

Some common types include fix, feat, chore, docs, style, refactor, perf, test, and others.

This pattern is useful in any repository’s history, but can be especially useful in open source projects where it helps to have everyone held to the same contribution standard.

As the project’s website outlines, commits should be structured like so:

<type>[optional scope]: <description>

[optional body]

[optional footer]
Enter fullscreen mode Exit fullscreen mode

Here are what these could look like in practice (pulled from mozilla/fxa):

feat(oauth): added redis scripts to store oauth access tokens
fix(email): Minor CSS tweaks for subscription email
chore(CI): update three jobs to use node 12 
Enter fullscreen mode Exit fullscreen mode

Resources and further reading:

Top comments (0)