From the translator: this article does not describe git commands, it implies that you are already familiar with it. It describes a completely sensible, in my opinion, approach to keeping public history clean and tidy.If you don’t understand what prompted git to do just that, then suffering awaits you. Using a lot of flags (--flag), you can make git work the way you think it should work, instead of working the way git wants it. It's like nailing with a screwdriver. The work is done, but worse, slower, and the screwdriver spoils.
Let's look at how the usual approach to development with git is falling apart.
')
We bud off a branch from master, we work, we merge back when we are done.
Most of the time it works, as expected, because the master changes after you have made a branch
(It means that your colleagues commit to master - note of the translator.) . Once you merge the feature branch into master, but the master hasn't changed. Instead of a merge commit, git simply moves the master pointer to the last commit, fast forward occurs.
To explain the fast forward mechanism, I borrowed a picture from one famous article. Note translator.Unfortunately, your feature branch contained intermediate commits — frequent commits that back up the work, but capture the code when it’s not working. Now these commits are indistinguishable from stable commits in master. You can easily roll back into such a disaster.
So, you add a new rule: “Use --no-ff when merging branches of a feature”. This solves the problem and you move on.
Then one day you find a critical bug in production and you need to track the moment when it appeared. You run
bisect , but you constantly hit intermediate commits. You give up and look for your hands.
You localize the bug down to the file. Run blame to see changes in the last 48 hours. You know that this is not possible, but blame reports that the file has not been changed for several weeks. It turns out that blame gives the time of the original commit instead of the time of merging the branch
(logical, because merge commit is empty - comment of the translator) . Your first interim commit changed this file a few weeks ago, but the change was only injected today.
A no-ff crutch, a broken bisect and slurred blame are symptoms that you hammer nails with a screwdriver.
Rethinking version control
Version control is needed for two things.
The first is to help write code. There is a need to synchronize edits with your team and regularly backup your work.
The second reason is
configuration management . Includes parallel development management. For example, work on the next release version and parallel bug-fixes of the existing production version. Configuration management implies the ability to find out when something has been changed. An invaluable tool for diagnosing errors.
Traditionally, these two reasons come into conflict.
When developing some functionality, you will need regular intermediate commits. However, these commits usually break the build.
In a perfect world, every change in version history is concise and stable. There are no intermediate commits that interfere. There are no giant commits for 10,000 lines. A neat story allows you to roll back edits or flip them between branches with the help of
cherry-pick . A neat story is easier to learn and analyze. However, maintaining the purity of the story implies bringing all edits to the ideal state.
So which approach do you choose? Frequent commits or a neat story?
If you are working together on a pre-release startup, a tidy story doesn’t bribe a lot. You can commit everything to master and release releases whenever you like.
As soon as the significance of the changes increases, be it the growth of the development team or the size of the user base, you will need tools to maintain order. This includes automated testing, code review, and a neat story.
The feature branches look like a good compromise. They solve simple parallel development problems. You are thinking of integration at the least important point in time when you write code, but this will help you for a while.
When your project grows large enough, the simple approach of branch / commit / merge will fall apart. Adhesive tape application time is over. You need a neat change history.
Git is revolutionary because it gives you the best of both worlds. You can make frequent commits in the development process and clean the history at the end. If this is your approach, then the git defaults seem more meaningful
(by default, fast-forward when merging branches - comment of the translator) .
Sequencing
Think about branches in the context of two categories: public branches and private.
Public branches are the official history of the project. A commit to a public branch should be concise, atomic and have a good description. It must be linear. It should be unchanged. Public branches are master and release.
Private thread for yourself. This is your draft at the time of solving the problem.
The safest way to store private branches locally. If you need to push to synchronize work and home computers, for example, tell your colleagues that the branch is yours and that you should not rely on it.
It is not necessary to inject a private branch into a public simple merge. First, clean up your thread with tools like reset, rebase, merge --squash and commit --amend.
Imagine yourself a writer, and commits as chapters of a book. Writers do not publish drafts. Michael Crichton said: "Great books are not written - they are rewritten."
If you come from other VCS, changing the history will be taboo. You proceed from the fact that any commit is carved in stone. Following this logic, you need to remove "undo" from text editors.
Pragmatists take care of edits only until these edits become annoying. For configuration management, only global changes are important to us. Intermediate commits are just a lightweight buffer with the possibility of cancellation.
If history is viewed as something clean, then fast-forward merging is not only safe but also preferable. It maintains a linear history, it is easier to track
The only remaining argument for --no-ff is documentation. You can use merge commits to associate with the latest production code. This is an anti-pattern. Use tags.
Recommendations and examples
I use 3 simple approaches, depending on the size of the change, the time spent working on it and how far the branch has gone aside.
Quick edit
Most of the time cleaning is just a squash commit.
Suppose I created a feature branch and made several intermediate commits within an hour.
git checkout -b private_feature_branch touch file1.txt git add file1.txt git commit -am "WIP"
As soon as I finish, instead of a simple merge, I do the following:
git checkout master git merge --squash private_feature_branch git commit -v
Then I spend a minute writing a more detailed comment to the commit.
Edit more
At times, the implementation of the feature grows into a multi-day project with many small commits.
I decide that my edit should be divided into smaller pieces, so squash is too coarse a tool. (As a daily rule, I ask myself: “Will it be easy to make a code review?”)
If my intermediate commits were a logical move forward, then you can use
rebase interactively.
Interactive mode is powerful. You can use it to edit old commits, split or organize them and, in this case, to combine several.
In feature branch:
git rebase --interactive master
The editor opens with a list of commits. Each line is: a command to be executed, a SHA1 hash, and a comment to the commit. Below is a list of possible commands.
By default, each commit is “pick”, which means “commit not to change”.
pick ccd6e62 Work on back button pick 1c83feb Bug fixes pick f9d0c33 Start work on toolbar
I am changing the squash command, which combines the current commit with the previous one.
pick ccd6e62 Work on back button squash 1c83feb Bug fixes pick f9d0c33 Start work on toolbar
I save, now another editor is requesting a comment to the joint commit. All is ready.
Failed branches
Perhaps the feature branch existed for a long time and other branches merged into it to maintain its relevance. The story is complicated and confusing. The simplest solution is to take a rough diff and create a new branch.
git checkout master git checkout -b cleaned_up_branch git merge --squash private_feature_branch git reset
Now the working directory is full of my edits and no legacy from the previous branch. Now we take and add pens and commit edits.
We summarize
If you are struggling with defaults in git, ask yourself why.
Consider the public history unchanged, atomic and easily traceable.
Consider your private history as changeable and flexible.
The procedure is as follows:
- We create a private branch of the public branch.
- Methodically commit work to this private thread.
- Once the code has reached perfection, put the story in order.
- We merge the ordered branch back to the public one.