Don Hamilton III
Posted on June 7, 2023
Using Git and Github Effectively
Git and Github are very powerful tools for managing collections of files. Most people are familiar with using Git for programming version control and Github for maintaining remote versions of these programs. Git can be used for more than version control for programming, but it can also be a tool to maintain any project or directory of files that it would be helpful to have snapshots of over time.
Following are some tips and practices that have helped me use git to not only more efficiently manage projects, but to also work with teams of developers on a code base. All of these tips should be able to be applied as best practices to any project that you are using git and Github to maintain. This will not be a comprehensive guide to using git. This article will provide useful tips that I have found useful to me in my projects.
General Tips
HEAD
just refers to the most recent commit on whatever branch you're on.
HEAD
was a concept that took me a really long time to understand. HEAD
simply refers to your most recent commit on whatever branch you are currently on. Grasping that makes things like "detached HEAD
states" much less confusing
Always use git add -p
instead of git add .
to avoid committing unintended files.
The git add -p
command goes through the changes that you haven't staged and asks you about changes one by one and asks you if you want to include them on the current commit. This might seem tedious, but it can save you a lot of time by avoiding commits that contain changes that should be saved for later, smaller commits.
Generally use git commit -v
instead of git commit -m <message>
to type a long form commit.
Admittedly, one of the first reasons I started doing this was to get more familiar with Vim and editing files (especially small changes) from the command line. While using Vim is certainly a noble pursuit in and of itself, it is definitely a much better practice to have some sort of commit template in place when you are working with teams and sticking to it as much as possible. Examples of commit templates can be found here.
Always do a git rebase -i
before submitting a feature branch for PR.
More on this in a later section, but this allows you to clean up your commits before showing people your work. Which means you can get rid of things like work-in-progress commits, merge commits that may be related to one another, rename commits after having more context for what they actually do, etc. It helps clean up your changes and gives your team, and future members of your team, a better idea of what work was done.
Stashing
Stashing is a phenomenal resource in git that I feel gets underused. Here is an example scenario. You are working on a branch in which you have progress you aren't ready to commit. A co-worker reaches out and asks you to review some work on another branch before they make a PR. You don't want to muddy up your commit history with a work-in-progress commit, but you don't want to move this work to your co-workers branch either. What do you do? Use git stash
instead! This saves a version of your current work in the stash list. Then when you finish and return to your original branch, simply reapply your previous changes using git stash pop
. Now you're back where you left off with no unneeded commits!
Time Travel and Undoing
It's really useful to be familiar with restoring, resetting, and reverting. Now if you stick to good git practices, you can usually avoid these. USUALLY. But there are times when you or your teammates might add some code into a commit that shouldn't be there and you will need to do a little time traveling to work things out. The best thing about git is that it is truly almost impossible to blow work away forever. There is almost always a way to go back to previous states or recover code that might accidentally get deleted. So if you think you've irrecoverably broken something or lost all the work you did on that new feature, CALM DOWN. You can fix it.
note: HEAD~#
refers to a way to specify a number of commits behind the current HEAD
. For example, if you simply want to go back one commit from where you are now in the commit tree, simply type HEAD~1
and go back one commit relative to where you are.
git restore --source [<commit hash or HEAD~#>] <file-name>
Unstage a file that is currently staged for commit or return a file to a state at a previous commit. Restore is usually used before you make a commit and you stage something you didn't mean to. (read: use git add -p
and not git add .
)
git reset <commit-hash or HEAD~#>
Reset is what you want to use when you need to undo an existing commit. Using the command above takes HEAD
back to specific commit but DOES NOT remove changes. It brings files back to a new HEAD
for you to do as you please. So your files will almost look like you have a merge conflict and allows you to fix up your files as you need.
git reset --hard <commit-hash or HEAD~#>
Removes all commits following the specific commit and DOES remove changes. All work from removed commits is lost. So essentially, this takes you back in time to whatever commit you specify and removes all of the commits you made after that point. BE CAREFUL especially if you are going to be removing commits that have been pushed and may already be merged. This will not be fun to deal with in teams.
git revert <commit-hash or HEAD~#>
Removes changes to a given commit very much like git reset
BUT creates a new commit with those changes instead of removing commits and changing git history. Almost all of the time, you want to use git revert
over git reset
, but there are exceptions. You can pretty safely use git reset
if you are working locally and the changes to the commit history will not affect anyone else that may be working on the branch you will be changing.
Fetch/Pull
Fetch
Fetching retrieves any changes from the remote and puts them in the local repository. It does not apply these changes to any files and, as such, doesn't alter any work that you may have in a given file. For example, if you're working in a file called Button.tsx
and you've made changes to 30 lines and then perform a git fetch
and another 20 lines have been changed in that file on the remote, you will now have those new lines, but you will not have them moved into your local file until you either choose to git merge
or git rebase
. This is preferable to git pull
as it gives you options.
Speaking of...
git pull
on the other hand combines the commands git fetch
and git merge
. This is more dangerous because you may end up with merge conflicts that you do not anticipate and give you no way to deal with them other than fixing the merge and making a new commit. (note: the new commit is the problem albeit not a horrible one if you stick to the rule of doing a clean-up rebase before pushing)
Tips
It is generally a good idea to use
git fetch
alongsidegit rebase
which will come later. A colleague on a previous project got me in the habit of doing agit fetch
followed by agit rebase
every morning from our development branch to our feature branches. (the development branch is the one we would make PRs to from our feature branches). This is a tremendous help because it severely cuts down on merge conflicts later on when you PR said feature branch as you've already worked through any file changes in much smaller chunks. (At least most of the time...)Use
git checkout origin/<branch-name>
after usinggit fetch
to take a look at changes before rebasing. This can give you a good idea of any conflicts you may be fixing ahead of time and mitigating them in your code first. An ounce of prevention is worth a pound of cure.
Why Feature Branches
I've mentioned feature branches. So a quick aside on what I mean. The concept of feature branches comes from trunk-based development. (note: I'm not going to be going into detail about that as it is extensive. But you can read more about it here if you'd like.)
Feature branches are short lived branches that only exist for the duration of a feature's development. Once the given feature is done, the branch is merged back into the main branch and then deleted. This process usually includes a code review before merging back into the main branch.
This is the very short version of how feature branches work. Feature branches are useful because they prevent developers from working on the same branch while making the new features. This is a good work flow because it prevents developers doing a bunch of work on one branch all at the same time. It is a very important topic when working with teams and I encourage you to seek out more information about it and talk to your fellow developers about best practices.
Rebasing
Golden Rule: Don't rebase if other people are working with your branch! Since rebasing creates new commits that are identical to the commits you had previously, it does remove the commits that were in place. BUT, to reiterate, if you are using feature branches properly, this should never become an issue. Don't share your branches. Covet them. You are Smeegle, it is your precious ring. Don't merge it until it is done. Once a branch is merged, work should not be done on the feature. If work does need to be done, log a bug or new feature.
Rebasing is essentially a way to put your feature branch at the end of the current branch instead of dealing with merges intermittently in that branch. The best way to think of this is in comparison to merging. Imagine you are working on a code base with a team of developers and you want to keep in sync with the main branch once a day (a very typical workflow). You can do this by pulling down the main branch each morning and then merging it with your feature and then cleaning up any subsequent changes. The issue with this practice is that the merges then muddy up your branch with commits that do nothing other than get the most current version of the development branch.
On the other hand, if you do the same practice with rebase, you are simply updating your branch with the changes and shifting where your changes exist in relation to the current HEAD of the main branch. Which keeps things a lot cleaner and doesn't muddy up your commit history.
Interactive Rebasing
Interactive rebasing is the process by which you can alter commits before they are remade by git rebase
. To do this we use the command git rebase -i [<branch-name>] [<HEAD-#]>
. Notice that both the branch name and number of commits is optional. But one or both is highly recommended. Now what can you do with it?
Rewording
You can use rebase to reword commit messages before git replaces that commit. This is especially useful with hindsight because you might have a better idea of what that commit was actually doing with respect to the finished work.
Fixing Up and Squashing
fixup
gives you the ability to squash commits into one another but also gives you the ability to give a new commit name if you'd like. While squash
will just fold commits into the previous commit without giving you the rename option. Opt for fixup
so you don't have to remember more keywords and they do the same thing.
Both of these maintain the code changes from removed commits. They just clean up history.
Drop
This is dangerous. It gets rid of the commit and any changes in it. It's like it never happened.
Conclusion
Hopefully these tips can help you use git a little more efficiently. Git is an amazing tool and one that we as developers should be using as ambitiously as possible. Thanks for reading!
Posted on June 7, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.