Helping Git overcome blocks
PREREQUISITES
- Knowledge of how to run shell commands in a terminal
- Familiarity with Git concepts of commits, commit messages, and commit ancestors
- A Git repo to work in (see Create a Git playground)
- Familiarity with Git relative references
- Familiarity with
git cherry-pickandgit rebase - An understanding of what ends up in commits created by cherry picking and rebasing (see Reapplying work)
TIP
I recommend visualizing the commit graph while working through this. See Visualizing commit graphs.
HOLD UP
Before expanding toggles, try things out on your own!
Introduction
In what the Git docs call the "easy case", Git is able to determine how to apply the changes introduced by an existing commit to an arbitrary ancestor. That is, Git can tell what your files should look like after running a git-cherry-pick or git-rebase command. It can tell, so it makes the necessary and commits it. There's also a "hard case", in which the correct end state is ambiguous. When Git hits a hard case it doesn't make a commit; instead it asks the user to step in and make a decision.
Our focus is to understand how conflicts arise. Since we're here we'll also look at the practical workflow of resolving and moving past them.
Set the stage
This is similar to what we did in Reapplying work:
Create a new branch called
conflicting-basepointing to the same commit asmainToggle to reveal my solutions
Click a tab to see other solutions
git checkout main git branch conflicting-basegit branch conflicting-base mainCreate a new branch called
conflicting-1pointing to that same commit, and check it outToggle to reveal my solutions
Click a tab to see other solutions
git checkout conflicting-base git branch conflicting-1 git checkout conflicting-1git checkout -b conflicting-1 conflicting-basegit switch -c conflicting-1 conflicting-baseCreate a new file called "conflicting" and give it content
a b cand commit it with the commit message
conflicting-abcToggle to reveal my solution
Create, edit, and save the file, then
git add conflicting git commit -m "conflicting-abc"From where you are, create and check out a new branch called
conflicting-2Change the file
conflictingto have this content:d b eSave, add, commit with the message "conflicting-dbe"
Check out
conflicting-1Change the file
conflictingto have this content:f b cSave, add, commit with the message "fbc"
This is what we've built. You could also use a Git GUI to visualize it as you go.
(conflicting-base) < conflicting-abc < conflicting-fbc(conflicting-1*)
\
conflicting-dbe(conflicting-2)
Create conflict
Rebase
conflicting-2offconflicting-1(or attempt to…).PAUSE
It's a good habit to read the rebase output. Git's output is generally clear and helpful. In this case the error output includes "hints" for what to do next.
This time read it but don't yet do what it says to do! First we'll look into what happened.
Toggle to reveal my solutions
Click a tab to see other solutions
git checkout conflicting-2 git rebase conflicting-1git rebase @ conflicting-2
Shine a light on the conflict
git statustells you the current Git state. Run it, and read the output.- The status shows that something is up with the file
conflicting. But what?git diffwithout any other options outputs your unstaged changes. Run it. - The output shows us a one line conflict. Between the
<<< HEADline and the===line is the version of this line atHEAD. Between the===line and the>>> a-commit-id (conflicting-2)line is the version of this line atconflicting-2. The<<<,===, and>>>are "start of before / start of conflict", "end of before / start of after", and "end of after / end of conflict".
Back in the "easy case" of cherry picking and rebasing, we got into how Git determines what changes a reapplied commit needs to make. Part of what Git does is check whether it can make the same change in this new context. We saw before what happens if there's no change to be made: it doesn't make a change. But what if the change can't be made?
The difference introduced by the conflicting-2 commit is to change line one of conflicting from a to f. Git asks "can the a-to-f change be applied to the branch conflicting-1"? No: in conflicting-1 we've already changed a to d, so there's no a to change to f.
Resolving conflicts
Clearing this up is straight forward provided you know what the desired final result is.
In this case let's assume we really want line 1 of conflicting to be a.
git-diff shows file differences. That <<< === >>> business is actually in the file, inserted by Git. To resolve the conflict we'll delete the version we don't want, delete the three conflict marker lines, save, stage the file, commit, and then —as "hinted" in the rebase output— run git rebase --continue.
Open the
conflictingfile. VS Code makes this even easier, with buttons to "accept current," "accept both," and "accept incoming". Click one of them. Undo. Click one of the others. Undo. Click the other. Save.Stage the file and commit.
Toggle to reveal my solution
git add conflicting git commit -m "(your-message-here)"Tell Git you've unblocked it and that it can continue rebasing.
Toggle to reveal my solution
git rebase --continue
If the branch we were rebasing had multiple commits, Git would reapply each of them in turn now. But we just had the one, so Git cleans up its "there's a rebase in progress" files, and the rebase is done.
TODO
What's up with the two lines after the >>> line?
Add exercise of
- a conflict on the first and second lines
- a conflict on the first and third lines
Consider adding an exercise before the existing three-line-file one: a single-line file.
TODO
The exact same thing applies when merging
Add section explaining conflicts in the context of merge commits.
How this comes up
There's a good chance —don't say risk, it's no big deal— of conflicts coming up when working on multiple features in the same part of a codebase.
Maybe you've been given two tasks, you worked on them in separate branches, and the work for both made a change to a shared component. They're both ready to merge. You merge the first without a hitch. You go to merge the second… and conflict! At least you understand both changes.
It can be trickier if the conflict is related to collaboration. You and a collaborator work independently on a separate features, and you both change shared component. Your collaborator's feature merges without a hitch. You go to merge your feature… and conflict! Now you'll have to figure out why your collaborator made the change they made, and figure out the change that will work for your needs and theirs.
In either case, take out the commit markers, update the file so that both features are as expected, commit, rebase --continue.