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.
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.
The feature’s broken? It was working just fine two months ago! What changed?
In order to effectively use
bisect you need three things:
- 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.
- A commit where things were working. You can use
git logto go back in history, checking out older commits to see where the thing that is broken works again.
- 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
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 test.sh) and it will use this to automatically find the culprit.
Instead of adding and committing in two steps:
git add . git commit -m "Big changes"
For commits to changes on existing files it’s as simple as:
git commit -am "Big changes"
-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.”
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
This will simply update the message of the commit, but not any of the changes.
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.
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:
--force-with-leasedoes 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.
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
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
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
Another, sometimes quicker, way to step back in time is to use the tilde (
~) with HEAD:
HEAD~5 # the same as HEAD^^^^^
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.
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.
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, pickYou’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, rewordPlace 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, editYou 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, squashIf 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, fixupThis is similar to squash, except it automatically uses the previous commit’s message.
d, dropThis 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,
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
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
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
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]
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
- 5 Git Tips and Tricks 2019 - Git Commands With Examples - Git Tutorial
- David Baumgold - Advanced Git - PyCon 2015
- Signing commits
- –force considered harmful; understanding git’s –force-with-lease
- How to Use GPG to Sign your Commits on Github, Gitlab, Bitbucket
- What are the advantages and disadvantages of cryptographically signing commits and tags in Git?