Git is amazing and you’ll learn only by using it. Like in any other version control system, in git, you commit files, review the history of changes, maybe even create a merge branch. Git is more powerful than any other version control system. Here, you will learn how to take advantage of Git’s advanced, and therefore lesser-known, features to help you improve your daily workflow.
I’ll be using Git entirely from the command line. If you feel more comfortable using a graphical user interface, you can certainly run one alongside the command line as you follow along. However, I highly recommend that you make the command line your primary tool when using Git. Why? Because GUIs, regardless of how well they are designed, always impose some kind of limitation on what you can and can’t do with Git. It doesn’t mean that you’ll have to sacrifice user-friendliness. There are a few utilities designed specifically to make Git easier to use from the command line eg. Posh Git for Powershell, Oh-my-zsh for Z shell. A quick hack for Git bash is adding this to ~/.bashrc
git branch 2> /dev/null | sed -e ‘/^[^*]/d’ -e ‘s/* \(.*\)/(\1)/’
export PS1=”\u@\h \[\e[32m\]\w \[\e[91m\]\$(parse_git_branch)\[\e[00m\]$”
Sometimes even with autocompletion on you just can’t seem to type fast enough. For those situations, having short aliases for the most common git commands can be quite handy. Defining an alias is easy. You simply say:
git config –global –add alias.st status
git config alias.ch checkout
Merging vs Rebase
Git is all about working with divergent history. Its git merge and git rebase commands offer alternative ways to integrate commits from different branches, and both options come with their own advantages.
Consider what happens when you start working on a new feature in a dedicated branch, then another team member updates the master branch with new commits. This results in a forked history, which should be familiar to anyone who has used Git as a collaboration tool.
Now, let’s say that the new commits in master are relevant to the feature that you’re working on. To incorporate the new commits into your feature branch, you have two options: merging or rebasing.
The merge option
The easiest option is to merge the master branch into the feature branch using something like the following:
git checkout feature
git merge master
This creates a new “merge commit” in the feature branch that ties together the histories of both branches, giving you a branch structure that looks like this:
The Rebase Option
As an alternative to merging, you can rebase the feature branch onto master branch using the following commands:
git checkout feature
git rebase master
This moves the entire feature branch to begin on the tip of the master branch, effectively incorporating all of the new commits in master. But, instead of using a merge commit, rebasing re-writes the project history by creating brand new commits for each commit in the original branch.
The major benefit of rebasing is that you get a much cleaner project history. First, it eliminates the unnecessary merge commits required by git merge. Second, as you can see in the above diagram, rebasing also results in a perfectly linear project history—you can follow the tip of feature all the way to the beginning of the project without any forks.
The golden rule of rebasing is to never use it on public branches
Since history is something we’ll be wanting to look at all the time every tiny bit of improvement that we can make is worth the effort. By periodically performing an interactive rebase, you can make sure each commit in your feature branch is focused and meaningful. This lets you write your code without worrying about breaking it up into isolated commits—you can fix it up after the fact. For example, the following command begins an interactive rebase of only the last 3 commits.
git checkout feature
git rebase -i HEAD~3
By specifying HEAD~3 as the new base, you’re not actually moving the branch—you’re just interactively re-writing the 3 commits that follow it.
Note: This will not incorporate upstream changes into the feature branch.
If you try to push the rebased feature branch back to a remote repository, Git will prevent you from doing so because it conflicts with the remote feature branch. But, you can force the push to go through by passing the –force flag, like so:
git push –force
This overwrites the remote feature branch to match the rebased one from your repository and makes things very confusing for the rest of your team. So, be very careful to use this command only when you know exactly what you’re doing.
Resetting is a way to move the tip of a branch to a different commit. This can be used to remove commits from the current branch. For example, the following command moves the hotfix branch backward by two commits.
git checkout hotfix
git reset HEAD~2
The two commits that were on the end of hotfix are now dangling, or orphaned commits. This means they will be deleted the next time Git performs a garbage collection. It can be visualized as:
In addition to moving the current branch, you can also get
git reset to alter the staged snapshot and/or the working directory by passing it one of the following flags:
- –soft – The staged snapshot and working directory are not altered in any way.
- –mixed – The staged snapshot is updated to match the specified commit, but the working directory is not affected. This is the default option.
- –hard – The staged snapshot and the working directory are both updated to match the specified commit.
Reverting a specific commit
Reverting undoes a commit by creating a new commit. This is a safe way to undo changes, as it has no chance of re-writing the commit history. For example, the following command will figure out the changes contained in the 2nd to the last commit, create a new commit undoing those changes, and tack the new commit onto the existing project.
git checkout hotfix
git revert HEAD~2
This can be visualized as the following:
Contrast this with git reset which does alter the existing commit history. For this reason, git revert should be used to undo changes on a public branch, and git reset should be reserved for undoing changes on a private branch.
You can also think of git revert as a tool for undoing committed changes, while git reset HEAD is for undoing uncommitted changes.