This document gives a concise account of the essential Git practices used at Makimo. Before reading this document, ensure you've read the following:
Git-flow is a standard practice at Makimo. As such, its solid understanding is required. Supplementary reading:
-
Whether implementing features or fixing bugs - always use a new branch. Whenever branching, choose short and descriptive names. Branch names are of the form
<type>/<name>, where the name is comprised of words separated with hyphens (not underscores). -
Most common branch types are
feature,improvement,fix,docs,refactor,tests,style,chore(updating config files, build tasks and the like). The name should be human-readable, and briefly but clearly describe the work the branch contains. Example:# Good, specific name. git checkout -b fix/rabbitmq-dist-config # Bad, too vague. git checkout -b fix/logging # Bad, too specific. It's better to use more human-readable # name and put issue reference in the title/description instead. git checkout -b feature/PROJ-1234 -
When feature branch is removed from the remote (usually origin) it is a good idea to also remove local branches to keep repository clean and fast. To remove stale branches from local repository first we need to prune it (in other words: remove upstream references to branches that have been deleted on the
origin):
git fetch --prune <remote_name>
This will fetch all the refs from <remote_name> and prune the stale
references. There is also possibility to prune references without
fetching:
git remote prune <remote_name>
It is important to remember that pruning will not remove local
branches. To do this, checkout one of the main branches (master or
develop) and run the following script:
git fetch --prune <remote_name> && git branch -vv \
| grep ': gone]' \
| awk '{print $1}' \
| xargs git branch -dNOTE: this will only work on systems on which
grep,xargsandawkare available.
- Commit represents atomic change (on a conceptual level) made to the repository. It goes without saying that single commit should record a single, self-contained change. Readability of the git history crucially depends on that. Therefore, commits such as these ones should be avoided:
3676983 Various fixes
b6d9a02 Add email template; add dist config
e62d25a Tests, tests, tests
And these:
e62d25a Ups, forgot about print statement
b6d9a02 Add that feature
-
If you've made a mistake or forgot about something and pushed to the origin, you can rewrite published history but only if you're working on a private branch and are positive that no one is using it.
-
Generally speaking, you should avoid commiting removing forgotten prints and the like. You can either clean-up before publishing or made tweaks in batches.
There are two accepted commit formats: short and long.
This format is used when commit needs explanation and/or is important. Anatomy of the full commit is as follows:
<title>
# Commit title is a *title* - capitalized, short (70 chars or less) summary
# without the dot at the end, written in imperative present tense: "Fix bug",
# not "Fixed a bug." or "fixed a bug".
<description>
# More detailed explanatory text, if necessary. Usually about few sentences,
# but may require more explainig (e.g. if solving intricate bug or
# describing new feature). Further paragraphs come after blank lines.
# Bullet points are okay too, examples:
#
# - A fix to a quite embarassing issue where raw_copy_to_user() was
# implemented with asm_copy_from_user() (and vice versa).
#
# - Improvements to our makefile to allow flat binaries to be generated.
#
# NOTE: Description, including bullet points, consists of normal sentences.
# Therefore, usual English grammar applies.
<meta-information>
# This section is reserved for additional information, such as who signed-off
# or reviewed the change:
# Reviewed-by: <Franz Kafka fkafka@email.de>
# It is in good taste to include related issues, e.g.:
# Fixes: SLUG-77
# Most basic tags are "Fixes", "Resolves", "Reviewed-by".
Example:
Ensure we return the error if someone kills a waiting layoutget
If someone interrupts a wait on one or more outstanding layoutgets in
pnfs_update_layout() then return the ERESTARTSYS/EINTR error.
Reviewed-by: Franz Kafka <fkafka@email.de>
Fixes: PNFS-191
The blank line separating the summary from the body is critical; tools like rebase can get confused if you run the two together.
For other examples of excellent commit messages see Linux kernel git history.
In case of short commit, used for small patches and tweaks, the format is as follows:
<title> [<issue>,] [<command>,]
where <command> is a smart commit command (see 3. in supplementary reading
above). For example:
Fix nasty little bug ISSUE-51 #resolve #time 2h30m
-
Do not rewrite published history. If it happens that someone already pulled a copy of the history being rewritten, conflicts emerge. To save everyone a headache, stick by this rule.
-
However, you can rewrite private branches before publishing it to
developormaster. It's easy to make mistakes - if you're 100% positive that your branch is not used by anyone on the team, feel free to tweak it. -
Never rewrite special branches, e.g.
master(or any other used by CI). -
Keep the history clean and simple. Before pushing any branch to the public always ensure that it conforms to the style-guide. If it doesn't: squash, rebase, reword commits as necessary.
Perhaps the most straightforward history rewrite is changing the commit message. If the commit in question is the last one, the task is trivial:
git commit --amend
This command will open an editor (configured with git config core.editor)
with last commit for you to modify. After the changes are saved, git will
update your local repository.
Please note that this will only work if you didn't push your changes to the remote. In other cases, you will have to use push --force which is invasive and dangerous operation. Before pushing your changes do your best to make sure they are ready to be published to avoid the necessity to rewrite the history.
If you forgot to include a change in the last commit, or if you want to remove something, you can do it with the following commands:
git add [<file>,]
git rm [<file>,]
git commit --amend --no-edit
As was the case in the previous paragraph, in this situation too you have to be sure that commit was not pushed to the remote.
In order to change commits further back in the history, we will have to use
more advanced tools. Enter rebase. git rebase re-applies commits,
one by one, from your current branch onto the HEAD they were originally based
on. When used in the interactive mode, rebase works as follows:
- You specify how far into history you want to go. You can either
a specify single commit or a relative count from the
HEAD(istands for interactive):
git rebase -i d8b9bd # Rewrite from the specified commit to HEAD
git rebase -i HEAD~3 # Rewrite three last commits
- In the next step,
gitopens up an editor with list of commits which looks something like this (forgit rebase -i HEAD~3):
pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file
# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
#
# 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
Here you can tell git how you want to proceed with your rewrite. Think of it as a script. In this script you can:
-
pick- To "pick" a commit means to include it in the rewrite. If you were to remove the linepick a5f4a0d added cat-filefrom the script, git would remove this commit from the history. -
edit- This tells the script to stop at the relevent commit and let you apply changes you make to make to it. When git stops the script, you will see a message similar to the following:
$ git rebase -i HEAD~3
Stopped at f7f3f6d... changed my name a bit
You can amend the commit now, with
git commit --amend
Once you’re satisfied with your changes, run
git rebase --continue
At which point you can whatever you want with the commit and all changes will be included in the rebase.
-
reword- This is a shorthand foreditwhich, instead of giving you full control, asks only for the new message for the commit. -
squash- Squashing a commit means "melding" it with the previous one. When you squash two commits, git will combine the changes and the messages and ask you for the new one. Withsquashit is crucial to understand the order of operations. If you're unsure which commit gets squashed with which commit, please be sure to read section 4.4. -
fixup- Version onsquashwhich completely discards message of the commit being squashed. This is handy when you made a change, realised you forgot about something and added the fix in the next commit. In such case you want to discard "fixed typo from the previous commit"-type message. -
exec- This option allows to run a command between two commits in the rebase script. As such, it does not tag any specific commit - it is inserted between the two. Let's say we want to runecho "Woah, this is cool"after some commit. Then, we would write:
edit 310154e updated README formatting and added blame
exec echo "Woah, this is cool"
squash a5f4a0d added cat-file
exec can also be given the option to rebase with --exec. In such case,
specified command will be run after every step in the rebase script.
Additionally, by re-ordering commits in the rebase script, you can re-order commits in the history (if applicable).
It is important to keep in mind that commits in the rebase script are listed
in the reverse order (with respect to git log):
git log --pretty=format:"%h %s" HEAD~3..HEAD
a5f4a0d added cat-file
310154e updated README formatting and added blame
f7f3f6d changed my name a bit
Notice the reverse order of the rebase script in the previous paragrah.
The interactive rebase generates a script which is going to run starting
with the commit at the up and ending with the commit at the bottom. Therefore,
for options such as squash and fixup, previous commit it the commit above.
Because git rebase changes hashes of the commits being rewritten, it is only
safe when working with your local repository.
Generally, you should not be rewriting remote branches. However, if you're sure
that no one is using the branch (e.g. you're working on a private feature
branch) you can apply rebase this with push --force (!).
Sometimes during work on a large feature, there is a necessity to commit
partially finished job (for example: because the workday is ending and
you want to back up your work in case of an outage). In such cases, the
commits should be marked with WIP: prefix in the title. It would be
a good idea to include the short summary for finished parts of the task
in the commit description.
WIP: Add some long, mundane feature
So far the I have refactored the `MainView` and `SecondaryView` to get
them ready for this awesome new feature.
Such commits can be only pushed to the private feature branches and
must be removed (with rebase or amending commits) before the pull
request is created. The existence of WIP: commits in the history of
revised changes may be a reason of declining the pull request.
Although the existence of WIP: commits on the private feature branches
is acceptable, you are strongly encouraged to keep the parts of your job
as simple and short as possible to avoid the need to commit partially
finished job.
git tag <tag-name>
git push --tags
git tag -d <tag-name>
git push --delete <origin> <tag-name>
-
Always test before you push. Do not push something that may break.
-
Don't push work half-done. If you're working on a private branch and want to back-up your data, clean-up before opening pull-request.
-
Always check where are you pushing.