posts: git-basics: add history manipulation
This commit is contained in:
parent
eac8b1505c
commit
2ce60ab160
|
@ -90,19 +90,202 @@ give you the "*second parent*" of your commit.
|
||||||
|
|
||||||
## History manipulation
|
## History manipulation
|
||||||
|
|
||||||
* Cherry-picking
|
Once you start using `git` for non-trivial projects, using some of the
|
||||||
* Easy to do
|
practices that I aim to teach you, rewriting history will become your secret
|
||||||
* Most likely not what you want
|
weapon for productivity.
|
||||||
* CONFLICTS
|
|
||||||
|
|
||||||
* The power of the rebase, Luke
|
I have to insist on one point though, which is that re-writing history that was
|
||||||
* Work on your own
|
published and used by other people is often seen as a *faux-pas*, or worse! You
|
||||||
* Commit early, commit often
|
should only use it on private branches, making sure to never rewrite published
|
||||||
* Clean-up merge requests
|
history unless absolutely necessary.
|
||||||
|
|
||||||
* Lost? Here's a map
|
### Picking cherries
|
||||||
* History manipulation can lose commits and other work
|
|
||||||
* `reflog` can help you find it again
|
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
|
## Tips and tricks
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue