lesson ── git-labs ── ~20 мин ── 7 шагов
The goal is to see the difference between git merge and git rebase by
hand. First you put two branches into the typical "they diverged"
situation, then you join them one way, then you run the same scenario again
the other way and compare the history graphs.
You need no real GitHub. Everything is local, in an ephemeral container with git inside. Afterward the container disappears and the repositories are deleted.
интерактивный sandbox
Поднимется контейнер gitlab/git-base с git, bash, pre-commit. В браузере откроется терминал, можно сразу git init. Каждый шаг проверяется автоматически. Сеть air-gapped, github.com недоступен.
stack ── git · bash · 256 MB RAM · air-gapped · самоуничтожается через 30 мин простоя
You build a minimal repository with one shared commit, then split into two branches and make one commit on each. This is the classic "main moved ahead while I worked on feature" situation.
cd /home/student/work
mkdir merge-vs-rebase && cd merge-vs-rebase
git init
echo "v1" > file.txt
git add file.txt
git commit -m "init"
git switch -c feature # -c = create a branch and switch to it
echo "feature line" >> file.txt # >> append, does not overwrite v1
git commit -am "feature: add line" # -a stage tracked files, -m message
git switch main
echo "main line" >> file.txt
git commit -am "main: add line"
Now main and feature have diverged from the shared init commit.
One command per line. If something is off, read the error.
✓ Two diverging commits from a common ancestor. Ready for the experiment.
The graph shows the shape of the history. Run:
git log --oneline --graph --all # --graph = ASCII graph, --all = show all refs
It should show:
* <sha> (main) main: add line
| * <sha> (feature) feature: add line
|/
* <sha> init
This is a V-shaped divergence from init. Remember the shape; you compare it after merge and after rebase.
If you see nothing, make sure you are in /home/student/work/merge-vs-rebase.
✓ The V shape is visible. Next, merge.
You are on main. Merge feature:
git merge feature # both branches moved, so this is a three-way merge with a conflict
An editor opens with the default merge-commit message. Save it without
edits (Ctrl-O, Enter, Ctrl-X in nano). There is a conflict in file.txt:
the same file was edited in two places. Open file.txt, delete the
markers <<<<<<<, =======, >>>>>>>, and keep both lines in some
order. Save.
git add file.txt # mark the conflict as resolved
git commit # without -m: opens the editor with the merge message
Save the merge-commit message again.
A conflict is normal. Open nano with `nano file.txt`.
✓ The merge commit is created. Look at the new graph.
git log --oneline --graph --all
Now you see:
* <sha> (HEAD -> main) Merge branch 'feature'
|\
| * <sha> (feature) feature: add line
* | <sha> main: add line
|/
* <sha> init
Compare it with what you had before the merge. The old commits are still there, a merge commit was added on top, and the history pulls back into one point. That is a merge: the history is kept exactly as it was, plus a seam.
✓ The graph with the merge commit is visible. Next you roll back and do the same with rebase.
To try rebase on the same situation, return main to its "one commit after init" state. You need two commits: init and main: add line. A merge commit sits on top right now, and it has to go.
git reset --hard HEAD~1 # drop the last commit (merge), wipe the working tree too
git log --oneline
You should be left with main: add line and init. Leave feature
alone, it sits off to the side.
`git reset --hard HEAD~1` drops the single last commit (merge) and loses it.
✓ main is back to where it was before the merge. Ready for rebase.
Now switch to feature and rebase onto main:
git switch feature
git rebase main # replay the feature commits on top of main
Another conflict in file.txt (the same cause as in the merge step). Open the file:
nano file.txt
Inside you see a block with conflict markers: seven angle brackets in a
row (<<<<<<<), equals signs (=======), and angle brackets again
(>>>>>>>). Right now the file looks roughly like this:
v1
<<<<<<< HEAD
main line
=======
feature line
>>>>>>> 699c3b1 (feature: add line)
Delete all three marker lines (<<<<<<<, =======, >>>>>>>) and keep
the main line and feature line lines. You want exactly this, three
lines, with no angle brackets and no equals signs:
v1
main line
feature line
Save (Ctrl-O, Enter, Ctrl-X). Now mark the conflict resolved and continue the rebase:
git add file.txt # mark the conflict as resolved
git rebase --continue # after a conflict use --continue, not commit
An editor opens with the message of the replayed commit. Save it without edits. The rebase finishes.
If verify is red, here are the three most common causes. Check them in order: 1. file.txt still has the markers `<<<<<<<`, `=======`, `>>>>>>>`. Open `nano file.txt`, remove them by hand, and keep three lines with no brackets. Then `git add file.txt` and `git rebase --continue`. 2. You ran `git commit` instead of `git rebase --continue`, which does not finish the rebase. Run `git status`; if you see "interactive rebase in progress", finish it with `git rebase --continue`. 3. The rebase is not done yet (there are uncommitted edits). `git status` tells you what is left.
✓ Rebase finished. The graph changes a lot.
git log --oneline --graph --all
You see:
* <sha> (HEAD -> feature) feature: add line
* <sha> (main) main: add line
* <sha> init
A fully linear history. No merge commit, no V shape. The commit "feature: add line" now sits on top of main: add line, with a different SHA, because its base commit changed.
That is a rebase: your branch is rewritten as if it had branched off the fresh main from the start.
✓ A linear history. You saw the difference by hand. Chapter 8 has more.
Merge keeps the history as it was, at the cost of a merge commit. Rebase
replays on top, at the cost of new SHAs. The graph git log --oneline --graph --all shows the difference at a glance.
команды
git log --oneline --graph --allsee the branches and their shapegit merge featuremerge feature into the current branch (with a merge commit)git rebase mainreplay the current branch on top of maingit reflogif you get lost, roll back through the reflogконцепции