The Guide to Git I wish I had before I started my current job

Table of contents

I've been working with software for well over a decade, and honestly cannot remember when I started using Git. I used to be a bit wary of many commands but thought I had a good grasp on what to do when it came to git. Then I started a new role at a larger company, which had strict git guidelines. No more messy branches, no crappy commit messages, everything rebased.

Here is a list of all the things I wish I knew before starting this role.

Despite Git being very complicated, for day to day use it is really simple. I tend to make the workflow as simple as possible - I don't use aliases for git commands, I use the full arguments (--force) instead of the shortened ones (-f).

Basic concepts of Git I already knew and was very familiar with:

  • git add will 'stage' files, ready to be committed.

    • A 'staged' file is a version of a file that is ready to be committed. If you use a GUI it will probably handle all of that for you, if not you can add files to be staged with git add . (or specific files).
    • git add . to stage all
    • git add --patch to interactively stage chunks of code (useful to review what changes you made)
  • git checkout to change to a branch

    • git checkout - to checkout the previous branch. So you could do git checkout main; git pull; git checkout -; git rebase main to checkout main, pull in changes, switch back to your original branch, and then rebase that original branch on main
    • git checkout -b create-a-new-branch create a new branch.
  • git commit creates a commit from the 'staged' files

    • git commit --amend updates the previous commit with the 'staged' files
  • git status tells you what the current status is (e.g. in the middle of a merge)
  • git diff tells you what changed

    • git diff --staged to see the difference in staged files
    • git diff commit1..commit2 or git diff branch1..branch2 show diff between commits/branches
  • git push will send the current branch to the remote repo (such as Github.com)

    • git push --force overwrite changes when pushing
    • git push --force-with-lease a safer version of git push --force, that will only overwrite if other commits were not added on the remote branch (e.g. by other users).
  • git pull will grab the changes from the remote location (such as Github.com)
  • git log shows a log of commits with their commit hash & messages

    • git log --oneline - easier to digest version
    • git log --graph - useful to see branches
  • git show shows a commit's change
  • git bisect to find out which commit introduced a bug

    • binary search through commits (given a 'good' commit and a 'bad' commit). This is not a commonly used command for me so I won't go into detail but it is extremely handy to know about when needing to figure out when a bug was introduced.
  • The file .gitignore has a list of files to exclude from the git repo
  • git merge I use this so rarely now that I can't remember the syntax at all.

Basic concepts of Git I wish I knew:

  • A commit is like a list of changes to file(s), on a very basic level could be thought of as saying change 5 characters on line 10 to "xxxxx"
  • Branches are like a pointer, they just point to a commit. Multiple branches can point to the same commit. When you are on a branch and you add a new commit, the branch is updated to point to your new commit.

    • I used to think a branch was much more complicated than this.
    • You can change what commit a branch points to by checking out that branch (git checkout some branch) then setting it to point at a new commit hash (git reset --hard 5255841aa).
  • You can checkout single commits (git checkout 5255841aaff441d275122b4abfb099b881de7cb5) just like you can checkout branches (git checkout some-branch)
  • If you do something like git commit --amend to update the previous commit, the old commit is still around. (git gc will garbage collect these, but they stay around for at least a couple of weeks).
  • Every single change (even if you cannot easily find it by other means) can be found by looking at git reflog. It contains every single change, including changing branches, merging, rebasing. If you mess something up you can just look at the reflog and checkout a commit from before your changes.

Undoing all changes since your last commit

(You will lose staged/unstaged changes, this is a destructive command!)

If you have made some changes but decided you want to return to the state of the previous commit you can use git reset --hard to undo everything. This will mean any staged files (from git add .) will be removed.

A less destructive way to do this is to use git stash. It has the same effect, but the 'stashed' changes are still available. (see next section)

Use git stash

Git stash scared me. I used to use PHPStorm's 'shelve changes' as it felt a bit easier. Now I find that more awkward to use, as git stash is so simple.

You can use git stash to take any uncommitted changes and saves it in a temporary location. (This is not sent to Github when you push).

The changes are then undone, so you are back to the same state as your previous commit.

It will only stash files that are:

  • either already being tracked by Git (in other words: files that exist elsewhere in your git repo)
  • and other files not yet tracked by Git (not in any commits) but are staged (e.g. from git add .)

Git stash will not stash ignored files (from .gitignore), or new files that are not already elsewhere in your git repo and have not been staged (use git add to include them first)

You can then run git stash apply to replay those changes.

You can list all 'stashes' with git stash list, and then use git apply 4 to apply the 4th one.

Use git rebase and git rebase -i

I see git rebase and interactive git rebase (git rebase -i) as two completely separate commands. I'll cover them separately.

Git rebase

If you have 3 commits on the main branch:

 mainCommit1
 mainCommit2
 mainCommit3

and then make a branch from mainCommit 3, and add a couple of commits:

 mainCommit1
 mainCommit2
 mainCommit3
   \_______ branch1Commit1
            branch1Commit2
            branch1Commit3

But then on the main branch more commits are added:

 mainCommit1
 mainCommit2
 mainCommit3
   \_______ branch1Commit1
            branch1Commit2
            branch1Commit3
 mainCommit4
 mainCommit5

You may now want to 'rebase' your branch1 commits on top of main. This means you want to 'play' the changes in branch1Commit1, branch1Commit2, and branch1Commit3 on top of mainCommit5

If you run git checkout branch1; git rebase main; it will check out the branch1 branch, and rebase it on top of the most recent commit on main.

So now it would look like this:

 mainCommit1
 mainCommit2
 mainCommit3
 mainCommit4
 mainCommit5
 branch1Commit1
 branch1Commit2
 branch1Commit3

When is this useful?

If you are working on your branch (working-branch), you realise a dependency needs updating. Maybe you create a new branch (fix-dependency), open up a new PR to update that dependency, and it gets merged into the main branch. Now you want to go back to working-branch and use the dependency. But of course, your branch was created before the dependency update was in main:

 someCommit
    \________ working-branch-commit1
              working-branch-commit2
 fixDepencencyCommit

So you would checkout working-branch; rebase main to rebase your commits in working-branch on top of main, and it would look like this:

 someCommit
 fixDepencencyCommit
 working-branch-commit1
 working-branch-commit2

So you now have access to that branch.

Before understanding how git rebase worked I would have used git cherry-pick, which lets you get a specific commit and run those changes.

Note: You will often get merge conflicts when it tries to rebase. I love PHPstorm's interface for this. I think people who deal with complicated merge conflicts with just the command line tools are mental.

Interactive rebase git rebase -i

This used to be the scariest thing for me to do, but now I am used to it interactive rebase is simple.

The typical syntax is: git rebase -i $FROM_COMMIT, where $FROM_COMMIT is the oldest commit you want to look at. I almost always just type git rebase -i HEAD~10 which will be the 10th oldest one.

(HEAD points to the currently checked out branch's commit or the current commit).

Once you have typed git rebase -i HEAD~4 you will see something like this (the commit summary messages are going to be whatever you enter - I've added the filenames to make this easier to explain):

pick 913f120 feat: new blog - add blog.php
pick 2c2631b feat: add contact form - add contact.php
pick 67da3b4 fix: replace contact captcha - fix contact.php
pick 076c54a feat: update navbar with blog link - update navbar.php

# Rebase 913f120..2c2631b onto 5dc1032 (4 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

What this is saying is that it will 'pick' those 4 commits, and run them in that order.

pick 913f120 feat: new blog - add blog.php
pick 2c2631b feat: add contact form - add contact.php
pick 67da3b4 fix: replace contact captcha - fix contact.php
pick 076c54a feat: update navbar with blog link - update navbar.php

If you were to save and exit (you are probably in vim with git rebase, so press escape, then :x to write/quit) it would replay those commits, but they are the exact same as they were so nothing will change. All commit hash IDs will remain the same.

The order presented is oldest at the top, newest at the bottom. (This is different than git log).

Editing (reword) a previous commit message

After typing git rebase -i HEAD~10 you can change one of the pick to reword to edit a commit message.

pick 913f120 feat: new blog - add blog.php
pick 2c2631b feat: add contact form - add contact.php
reword 67da3b4 fix: replace contact captcha - fix contact.php
pick 076c54a feat: update navbar with blog link - update navbar.php

Once you save/quit that editor (probably vim), you will be shown an editor to edit the commit message. Make your changes, save/quit that message, then run git rebase --continue and it will then run the subsequent (picked) commit changes in order. (All commits after the one you edited will have a new commit hash).

Removing a git commit in interactive rebase:

What you can do though is rearrange (or even delete) those lines. If you changed it to the following (deleting the 3rd commit, replacing the contact captcha commit):

pick 913f120 feat: new blog - add blog.php
pick 2c2631b feat: add contact form - add contact.php
pick 076c54a feat: update navbar with blog link - update navbar.php

And then saved, the first two commits would be the exact same (they didn't change). But then it will look at 076c54a, replay those changes but create a new commit, and then point the current branch at that new commit.

(The old commit that was not used 67da3b4 and the original commit after that (076c54a) will still exist: check out git reflog to see it).

Rearranging the git commit order in interactive rebase:

As well as deleting lines, you can rearrange them. If you do the following (rearrange the order, and put the 'fix contact.php' commit above the 'add contact.php' commit):

pick 913f120 feat: new blog - add blog.php
pick 67da3b4 fix: replace contact captcha - fix contact.php
pick 2c2631b feat: add contact form - add contact.php
pick 076c54a feat: update navbar with blog link - update navbar.php

You will get a merge conflict. If you run git status it will tell you want to do (fix the conflict, then git rebase --continue. Or just git rebase --abort to reset everything back to what it was)

"squashing" or "fixup"-ing commits into one

The first word on each line (pick in all of the examples above) can be replaced with one of the following:

# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message

(either s or the full word squash, and f or fixup)

fixup to combine a commit with the previous one (only using parent commit message)

If you run this:

pick 913f120 feat: new blog - add blog.php
pick 2c2631b feat: add contact form - add contact.php
fixup 67da3b4 fix: replace contact captcha - fix contact.php
pick 076c54a feat: update navbar with blog link - update navbar.php

Then it will replay the first two commits (no changes at all), then look at the 3rd commit's changes, run them, and then combine those changes into one commit. It will throw away the 3rd commit's message, and just use the parent commit. The commits will now look like this:

913f120 feat: new blog - add blog.php
aabbccd feat: add contact form - add contact.php
eeffggh feat: update navbar with blog link - update navbar.php

note the now 2nd / 3rd commit have new hashes. And the current branch will now point to the new 3rd commit.

squash to combine a commit with a parent commit, but keep the commit messages

This is very similar to fixup, but instead of throwing away the newer commit's message (keeping only the parent commit message), it will combine the newer commit message with the parent one (and let you edit it)

pick 913f120 feat: new blog - add blog.php
pick 2c2631b feat: add contact form - add contact.php
squash 67da3b4 fix: replace contact captcha - fix contact.php
pick 076c54a feat: update navbar with blog link - update navbar.php

Once you save and quit that, it will prompt you to edit the git commit message with it prefilled in with both commit messages. Then you can do whatever you want to the commit message.

Using fixup and squash on multiple commits

I personally like to work by making very frequent commits, so while working on something I will end up with something like:

913f120 feat: some new feature
2c2631b WIP: adding button
67da3b4 WIP: new input field
076c54a WIP: add form submission handler

But when I want to push it to GitHub I don't want my WIP commits showing up. So I will do git rebase -i HEAD~5 and fixup all into the first commit (which I write out neatly)

pick 913f120 feat: some new feature
fixup 2c2631b WIP: adding button
fixup 67da3b4 WIP: new input field
fixup 076c54a WIP: add form submission handler

And then I have just one neat commit, ready to push to GitHub and pretend I do all my work in one well thought out commit.

Editing the changes within a commit

Sometimes you may have made changes in a commit several commits behind the current commit (so you can't just git commit --amend the most recent one). In that case, you have to use interactive rebase to edit that commit.

This is the one change I dislike doing in interactive rebase as if you edit a commit and then a subsequent commit has also changed the same file(s) you get very annoying conflicts that you have to manually resolve.

To do this, just change pick to edit

pick 913f120 feat: new blog - add blog.php
pick 2c2631b feat: add contact form - add contact.php
edit 67da3b4 fix: replace contact captcha - fix contact.php
pick 076c54a feat: update navbar with blog link - update navbar.php

Then you can make your changes, stage them (git commit add), and then run git rebase --continue to run the rest of the picked commits.

Committing on the wrong branch is not a problem

Years ago I used to have a little panic when committing on the wrong branch. It is very easy to do, and also very easy to fix. There are a few ways to do it. I will just add my favourite as it tends to be the easiest.

  • 1) Ok, you made your commit and realised it was on the wrong branch. Copy the hash (the one from the top of the list on git log --oneline) - this guide I'll pretend it is aabbccd
  • 2) create the branch you should have been on (or switch to it). git checkout main; git checkout -b the-branch-you-should-be-on
  • 3) Cherry pick that commit with git cherry-pick aabbccd. This will then grab that commit you made elsewhere, and run the same changes. Hopefully, there is no conflict. If there is use git status to see what to do
  • 4) Now you have the commit on the right branch. But you still have the 'old' commit on the previous branch. So checkout that branch, and then use git rebase -i HEAD~3 to enter interactive rebase mode and remove that commit. (See guide above)

An alternative way to get rid of the commit on the original branch is to get the commit before that one (I'll pretend it is ffeeddc). Then when on the original branch (git checkout main) run git reset --hard ffeeddc. Now that branch is pointing to the commit ffeeddc and it is like the branch never knew about aabbccd.

You can also use git revert to undo a change from a commit but it makes the git history messier. If you haven't pushed and those changes are not in main/master, I do not see the need for git revert.

Understanding what a detached head is

A detached head is where HEAD is pointing to a commit, not a branch. This can be the case when you checkout a specific commit. If you make a new commit, the commit can go through but it won't be associated with a branch.

Git hooks / Husky

Git supports a way to trigger scripts after certain actions. There are client-side and server-side hooks. I'll focus just on the client side ones.

The main one I use is pre-commit.

pre-commit git hook

Pre commit runs before a commit. If the scripts from that hook return an error code, the commit will not pass. (You can always do git commit --no-verify to skip running these commits).

The easiest way to set up pre-commit hooks that I've seen is to use Husky. In my example, I will use Husky to add a pre-commit hook to run some linting via lint-staged. You install Husky/lint-staged/eslint/stylelint with yarn or npm, then in your package.json file add something like:

  {
    "lint-staged": {
      "src/**/*.{js,jsx}": [
        "eslint . --fix", "git add"
      ],
      "src/**/*.scss": [
        "stylelint --syntax scss --fix", "git add"
      ]
    },
    "husky": {
      "hooks": {
        "pre-commit": "lint-staged"
      }
    }
  } 

Now it will run the linting checks on every commit and will not let the commit progress if there is an error. Find documentation on it here

Copy a file from another branch

I still have to Google this one every time to get the syntax correct, but if you are on branch1 and you want to get a file from branch2 you can do this:

git checkout branch2 src/index.js

You can also use git restore --source branch2 src/index.js

Git aliases/shortcuts

I am not a fan of adding too many aliases locally, as I have such bad memory I will not remember what they're an alias for and am then unable to use git on any computer other than my one. But I do have the following which I find quite useful.

# git log
gl=$'git log --oneline --decorate --graph '
# git log graph all
glga=$'git log --graph --oneline --all --decorate '

I can then use gl to show a simple git log, and gla to show it with branches. I very rarely need to use git log unless I need to read the commit messages.

git switch and git restore

I only recently learned about these commands. They are kind of like aliases for some git checkout and git reset commands

git switch to switch to a branch

git switch another-branch, same as git checkout another-branch

git restore to restore a file

git restore some-file.php will restore a non-committed change back to its original state. This is similar to some of the git reset commands.

Comments The Guide to Git I wish I had before I started my current job