Best Git Practices For Managing Your Project
Having troubles managing git history in your project? You should read this.
In this article we’ll talk about best git practices for managing your project. Follow these rules to keep your project well-documented and to avoid future problems with your project’s git tree.
First things first
✉️ Android Dev Newsletter
If you enjoy learning about Android like I do and want to stay up to date with the latest, worth reading articles, programming news and much more, consider subscribing to my newsletter 👇
https://androiddevnews.com/
🎙 Android Talks Podcast
If you’re a Polish speaker and want to listen to what I have to say about Android, architecture, security and other interesting topics, check out my podcast 👇
https://androidtalks.buzzsprout.com/
Creating new branches
Main branches
At the core, the development model is greatly inspired by existing models out there. The central repository holds two main branches with an infinite lifetime:
- master branch
- develop branch
Support branches
Next to the main branches master and develop, our development model uses a variety of supporting branches to aid parallel development between team members, ease tracking of features, prepare for production releases and to assist in quickly fixing live production problems. Unlike the main branches, these branches always have a limited lifetime, since they will be removed eventually.
The different types of branches you may want to use are:
- Feature branches (for ex. feature/<TASK_ID>-Short-feature-description)
- Defect branches (for ex. defect/<TASK_ID>-Short-defect-description)
- Release branches (for ex. release/<release_version>)
- Hotfix branches (for ex. hotfix/<short_hotfix_description>)
Committing
Use proper naming!
Generally, there are a few approaches for commit naming. You should choose one of them and stick to it. Remember to tell exactly what you did in a commit. Bad naming can cause difficulties in finding changes/bugs in your git tree.
For example, naming like this IS UNNACEPTABLE:
- “added ui”
- “added logic”
- “resolved PR comments part 2”
- “rebasing”
- etc.
Trust me, I’ve seen things like this in a few projects and the git tree was a mess…
Preferably, use one of these approaches:
1.Imperative mood
[If applied, this commit will] Remove deprecated methods from extensions
2. Past tense
[By applying this commit, I] Removed deprecated methods from extensions
3. What does the commit do?
[This commit] Removes deprecated methods from extensions
REMEMBER TO ALWAYS ADD TASK ID, IF YOU HAVE ONE:
So finally, the commit would look like this:
“ABC-123 Removes deprecated methods from extensions“
Amending commits
If you forgot about something (for example reformatting, removing whitespaces, etc.) in the last commit or you want to change it’s name DO NOT create a new commit if it is not necessary. Just amend you last commit, by using:
git commit --amend
Once you do that, a new commit is created and we will push it ON TOP of the old commit. That’s why we need to use push force:
git push -f origin feature/ABC-123_Some_cool_feature
Merging VS Rebasing
The first thing to understand about git rebase
is that it solves the same problem as git merge
. Both of these commands are designed to integrate changes from one branch into another branch—they just do it in very different ways.
Consider what happens when you start working on a new feature in a dedicated branch, then another team member updates the develop
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 develop
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.
Merging
The easiest option is to merge the develop
branch into the feature branch. 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:
Merging is nice because it’s a non-destructive operation. The existing branches are not changed in any way. This avoids all of the potential pitfalls of rebasing (discussed below).
On the other hand, this also means that the feature
branch will have an extraneous merge commit every time you need to incorporate upstream changes. If master
is very active, this can pollute your feature branch’s history quite a bit. While it’s possible to mitigate this issue with advanced git log
options, it can make it hard for other developers to understand the history of the project.
On the other hand, this also means that the feature
branch will have an extraneous merge commit every time you need to incorporate upstream changes. If develop
is very active, this can pollute your feature branch’s history quite a bit. While it’s possible to mitigate this issue with advanced git log
options, it can make it hard for other developers to understand the history of the project.
Rebasing
This moves the entire feature
branch to begin on the tip of the develop
branch, effectively incorporating all of the new commits in develop
. 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. This makes it easier to navigate your project with commands like git log
, git bisect
, and gitk
.
But, there are two trade-offs for this pristine commit history: safety and traceability. If you don’t follow the Golden Rule of Rebasing , re-writing project history can be potentially catastrophic for your collaboration workflow. And, less importantly, rebasing loses the context provided by a merge commit — you can’t see when upstream changes were incorporated into the feature.
Remember that after rebasing if commits are added to our past then you have to PUSH FORCE them to your branch. If commits are added on the top (for example rebasing feature branch to develop, then you can use a simple push).
The Verdict
Use rebase if you want to get the latest changes from develop or other branch. It does not create a mess in git tree like merge, so the project is easier to read/learn and old unwanted changes are easier to edit too.
BUT
If you’re not the only one working on a branch and for example someone changed the history/amended commit/squashed them/etc. it’s better to use merge. If you’d want to use rebase in this situation you’d have to reset your branch and cherry pick the changes from you other brach for example.
Useful git commands that you MUST learn to save yourself in critical situations
Git stash
The git stash
temporarily shelves (or stashes) changes you’ve made to your working copy so you can work on something else, and then come back and re-apply them later on. Stashing is handy if you need to quickly switch context and work on something else, but you’re mid-way through a code change and aren’t quite ready to commit.
Git status
The git status
command displays the state of the working directory and the staging area. It lets you see which changes have been staged, which haven’t, and which files aren’t being tracked by Git.
Git log
The git log
command displays committed snapshots. It lets you list the project history, filter it, and search for specific changes. While git status
lets you inspect the working directory and the staging area, git log
only operates on the committed history.
Git reflog
Reflogs track when Git refs were updated in the local repository. In addition to branch tip reflogs, a special reflog is maintained for the Git stash. Reflogs are stored in directories under the local repository’s .git
directory. git reflog
directories can be found at .git/logs/refs/heads/.
, .git/logs/HEAD
, and also .git/logs/refs/stash
if the git stash
has been used on the repo.
Git reset (soft/mixed/hard)
The git reset
command is a complex and versatile tool for undoing changes. It has three primary forms of invocation. These forms correspond to command line arguments --soft, --mixed, --hard
. The three arguments each correspond to Git’s three internal state management mechanism’s, The Commit Tree (HEAD
), The Staging Index, and The Working Directory.
Git rebase interactive
To modify a commit that is farther back in your history, you must move to more complex tools. Git doesn’t have a modify-history tool, but you can use the rebase tool to rebase a series of commits onto the HEAD they were originally based on instead of moving them to another one. With the interactive rebase tool, you can then stop after each commit you want to modify and change the message, add files, or do whatever you wish. You can run rebase interactively by adding the -i
option to git rebase
. You must indicate how far back you want to rewrite commits by telling the command which commit to rebase onto.
Running this command gives you a list of commits in your text editor that looks something like this:
pick f7f3f6d Change my name a bit
pick 310154e Update README formatting and add blame
pick a5f4a0d Add cat-file# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# 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
Git cherry pick
git cherry-pick
is a powerful command that enables arbitrary Git commits to be picked by reference and appended to the current working HEAD. Cherry picking is the act of picking a commit from a branch and applying it to another. git cherry-pick
can be useful for undoing changes. For example, say a commit is accidentally made to the wrong branch. You can switch to the correct branch and cherry-pick the commit to where it should belong.
Wow, that’s a lot of new git commands
I know that for for some of you these commands can be hard to understand. That’s why I encourage you to go, create a simple repository on Github, GitLab, Bitbucket or whatever you like. And then just.. PLAY WITH IT. Test every command and every option it has. You can either use the command line or software like Github Desktop, Sourcetree or GitKraken. Just learn how these commands work and when is the good time to use them. It will be very helpful for you, I assure you.
If you have any questions about Git in general, don’t be scared and reach out to me, cheers!