posts: git-basics: add history manipulation

This commit is contained in:
Bruno BELANYI 2020-09-05 18:49:25 +02:00
parent eac8b1505c
commit 2ce60ab160

View file

@ -90,19 +90,202 @@ give you the "*second parent*" of your commit.
## History manipulation
* Cherry-picking
* Easy to do
* Most likely not what you want
* CONFLICTS
Once you start using `git` for non-trivial projects, using some of the
practices that I aim to teach you, rewriting history will become your secret
weapon for productivity.
* The power of the rebase, Luke
* Work on your own
* Commit early, commit often
* Clean-up merge requests
I have to insist on one point though, which is that re-writing history that was
published and used by other people is often seen as a *faux-pas*, or worse! You
should only use it on private branches, making sure to never rewrite published
history unless absolutely necessary.
* Lost? Here's a map
* History manipulation can lose commits and other work
* `reflog` can help you find it again
### Picking cherries
The easiest way to manipulate history is the `cherry-pick` command. It allows
you to "*lift*" a commit any other place in history, and plop it down in your
current branch.
It's the easiest way to manipulate history, allowing you for example to pick a
commit which fixes a bug in another branch and apply it onto yours: simply do
`git cherry-pick <my-commit-with-the-bugfix>`.
It is however most likely not what you want to do if you later intend to merge
your branch with the one you lifted the commit from. Both sets of commits will
have the exact same change, and `git` will not be able to resolve the conflict.
In those cases, consider merging from a common branch whose purpose is applying
the fix. In that case, `git` will happily merge your branches later on without
making a fuss.
### All your rebase are belong to us
This is probably the single best command in all of `git` in my mind. Having the
access to `git rebase` allows you to commit as you work, without caring about
atomicity, commit messages, or even having working/compiling code.
Rebasing allows you to make various changes to your branch's history:
* Rewording a commit's message.
* Reordering commits
* Removing commits
* Squashing: merging a commit into another one
This tool allows you to work on your own, commit early and commit often as you
work on your changes, and keep a clean result before merging back into the main
branch.
#### Fixup, a practical example
A specific kind of squashing which I use frequently is the notion of `fixup`s.
Say you've commited a change (*A*), and later on notice that it is missing
a part of the changeset. You can decide to commit that missing part (*A-bis*)
and annotate it to mean that it is linked to *A*.
Let's say you have this history:
```none
42sh$ git log --oneline
* 787dd36 (HEAD -> master) Add README
* 8d08529 Add baz
* 7188fb1 Frobulate bar
* 961d8fb Fix foo
```
And notice that missed a change that belongs to `Add baz`. You can `add` it to
your staged changes, and issue `commit --fixup @~`. This will create a commit
named `fixup! Add baz`.
```none
42sh$ git log --oneline
* 92912ee (HEAD -> master) fixup! Add baz
* 787dd36 Add README
* 8d08529 Add baz
* 7188fb1 Frobulate bar
* 961d8fb Fix foo
```
If you then rebase using `-i --autosquash` will result in this interactive
rebase screen.
```none
pick 961d8fb Fix foo
pick 7188fb1 Frobulate bar
pick 8d08529 Add baz
fixup 92912ee fixup! Add baz
pick 787dd36 Add README
```
After applying the rebase, you find yourself with the complete change inside
`Add baz`, which can be confirmed with another `git log`
```none
* 0174e54 (HEAD -> master) Add README
* b0a47ae Add baz
* 7188fb1 Frobulate bar
* 961d8fb Fix foo
```
This is especially useful when you want to apply suggestion on a merge request
after it was reviewed. You can keep a clean history without those pesky `Apply
suggestion ...` commmits being part of your history.
### Lost commits and the reflog
When doing this kind of history manipulation, you might end up making a mistake
and lose a commit that was **very important**.
Obviously, `git` has a way to save us in this situation. If we look at the man
page for `git reflog`, we can read the following sentence:
```none
Reference logs, or "reflogs", record when the tips of branches and other
references were updated in the local repository.
```
What does this mean exactly? Simply put, you can use it to checkout a previous
version of your repository, in the state it was in before you manipulated the
history. Let's illustrate with a small example.
#### Mapping lost commits: a practical example
Let's say you have this repository state at the beginning.
```none
42sh$ git log --oneline
* 524de22 (HEAD -> master) Documentation update
* d60ddb5 USELESS COMMIT
* e81b5fb Remove baz dependency
* 44cea7d VERY IMPORTANT COMMIT
* 58eb2d9 Use foo without bar
* dab7792 Simplify frobulation
```
And decide to drop `c581d4d` (**`USELESS COMMIT`**), but inadvertently drop
`377921c` (**`VERY IMPORTANT COMMIT`**) at the same time. For this example,
I simply `dropped` both commits in a `rebase` operation.
I notice now that I am missing my **`VERY IMPORTANT COMMIT`** in my history:
```none
42sh$ git log --oneline
* ec8508b (HEAD -> master) Documentation update
* 3866067 Remove baz dependency
* 58eb2d9 Use foo without bar
* dab7792 Simplify frobulation
```
If I now use try to see what happened to my `HEAD` reference using `reflog`,
I can find the last update I did before starting my `rebase` to cancel the
whole operation.
```none
42sh$ git reflog
ec8508b (HEAD -> master) HEAD@{0}: rebase (finish): returning to refs/heads/master
ec8508b (HEAD -> master) HEAD@{1}: rebase (pick): Documentation update
3866067 HEAD@{2}: rebase (pick): Remove baz dependency
58eb2d9 HEAD@{3}: rebase: fast-forward
dab7792 HEAD@{4}: rebase: fast-forward
612e6f5 HEAD@{5}: rebase (start): checkout 612e6f5a055280aac1d7608af2dd2443aed6875c
524de22 HEAD@{6}: commit: Documentation update
d60ddb5 HEAD@{7}: commit: USELESS COMMIT
e81b5fb HEAD@{8}: commit: Remove baz dependency
44cea7d HEAD@{9}: commit: VERY IMPORTANT COMMIT
58eb2d9 HEAD@{10}: commit: Use foo without bar
dab7792 HEAD@{11}: commit (initial): Simplify frobulation
```
By reading the `reflog`, I can see that my `rebase` started at `HEAD@{5}`
(reads: *`HEAD`'s fifth prior value*). If I want to return to the state of my
repository before starting that rebase, I can simply do `git checkout HEAD@6`
which will take me back to the state prior to the `rebase`.
```none
42sh$ git checkout HEAD@{6} # Checkout my `HEAD`'s 6th prior value
42sh$ git log --oneline # Are we back before the rebase?
* 524de22 (HEAD) Documentation update
* d60ddb5 USELESS COMMIT
* e81b5fb Remove baz dependency
* 44cea7d VERY IMPORTANT COMMIT
* 58eb2d9 Use foo without bar
* dab7792 Simplify frobulation
```
Now, I want to make sure that I have my `master` branch back to that state too,
and not simply my disembodied `HEAD`.
```none
42sh$ git branch -f master # Change where `master` is pointing at
42sh$ git checkout master # Checkout `master` branch
42sh$ git log --oneline # Is everything in order?
* 524de22 (HEAD -> master) Documentation update
* d60ddb5 USELESS COMMIT
* e81b5fb Remove baz dependency
* 44cea7d VERY IMPORTANT COMMIT
* 58eb2d9 Use foo without bar
* dab7792 Simplify frobulation
```
And voila! I can now try my `rebase` again, and be careful not to lose **`VERY
IMPORTANT COMMIT`** this time.
## Tips and tricks