Skip to content

Keep histories consistent

  • Lesson 15
  • 40 min
  • Module 5: Release and hotfix practice
Lesson 15 of 15 · Git and Team Development EssentialsKeep histories consistent

What you will be able to do

Make sure that a fix in a release is also in the next release. After this lesson, you can find release fixes that main does not have, and bring them back. You can also find a fix that is in the history twice.

Before you startWhat to know and to have readyAbout 40 minutes

What you need to know

What you need on your computer

These commands make the practice state again, and repeat the hotfix from the previous lesson. Run them one at a time, from ~/projects:

Terminal
sh nextia-learning-labs/C14/setup.sh hotfix --reset
cd c14-lab/ticket-notes
git switch release/1.0
git switch -c hotfix/1.0.1
git cherry-pick -x aeeb4b6
git push -u origin hotfix/1.0.1
git switch release/1.0
git merge --no-ff --no-edit hotfix/1.0.1
git tag -a v1.0.1 -m "Help desk notes 1.0.1"
git push
git push origin v1.0.1

Then check the state:

Terminal
git status
Output
On branch release/1.0
Your branch is up to date with 'origin/release/1.0'.
nothing to commit, working tree clean

A fix can start in two places:

  • On main first. Sam fixed the network team extension on main. Then Amira copied it to the release with cherry-pick. main already has the fix.
  • On the release first. Sometimes the bug is urgent, or the code on main has changed too much. Then the team fixes the release directly, with a hotfix branch from release/1.0. Now main does not have the fix.

The second case has a risk. The next release, 1.1, starts from main. If main does not have the fix, version 1.1 has the old bug again. A regression is a bug that was fixed, and then comes back.

To prevent it, you bring the fix to main. The usual way is a back merge: you merge the release branch into main, with a pull request from release/1.0 to main. “Back” means back to development. You can also cherry-pick the fix to main, but a merge brings every release fix at the same time, and you cannot forget one.

Two lines of commits. The main line: 465d7ea with the tag v1.0.0, then 3cf73f7, the unfinished export, then aeeb4b6, the fix, then 3f4d833, ticket 105. The release/1.0 line starts at 465d7ea. A dashed arrow labeled cherry-pick -x goes from aeeb4b6 down to 19b64c1 on release/1.0, the copy of the fix. After it, 257b55a has the tag v1.0.1. A dashed arrow labeled merge back goes from 257b55a to a future commit on main.
Fixes go from main to the release with cherry-pick. Release fixes go back to main with a merge.

Teams also talk about a forward merge. It means a merge from an older release line into a newer one, for example from release/1.0 into release/1.1. It carries a fix forward in version order. Teams use these names in different ways. When you join a team, ask which direction each name means there.

Worked example: a fix on the release first

Section titled “Worked example: a fix on the release first”

Current branch: release/1.0. Before the team makes a new release branch, it lists the commits on the release that main does not have:

Terminal
git log --oneline main..release/1.0
Output
257b55a Merge branch 'hotfix/1.0.1' into release/1.0
19b64c1 Fix the network team extension

19b64c1 is the copy of Sam’s fix. main has the same change, in aeeb4b6, but Git compares commits, not changes. The copy is a different commit, so Git lists it.

To compare the changes, use three dots and --cherry-mark. main...release/1.0 lists the commits on either side, but not on both. --left-right marks each one with < for main or > for release/1.0. --cherry-mark shows = when the other side has a commit with the same change:

Terminal
git log --oneline --left-right --cherry-mark main...release/1.0
Output
> 257b55a Merge branch 'hotfix/1.0.1' into release/1.0
= 19b64c1 Fix the network team extension
< 3f4d833 Add ticket 105
= aeeb4b6 Fix the network team extension
< 3cf73f7 Start the ticket export (not finished)

The = marks show that aeeb4b6 and 19b64c1 contain the same change. The same fix is in the history twice: a duplicate fix. This is normal after a cherry-pick. The -x line in 19b64c1 also names aeeb4b6, so a person can see the link.

The help desk reports that “more than 2 days” is not clear. It must say “2 working days”. The fix is urgent, so Amira fixes the release first.

Current branch: release/1.0. She makes a hotfix branch from it:

Terminal
git switch -c hotfix/1.0.2

Current branch: hotfix/1.0.2. She changes the last line of escalation.txt in her editor, and checks the diff:

git diff
diff --git a/escalation.txt b/escalation.txt
index 12f9ff4..1b5f8a3 100644
--- a/escalation.txt
+++ b/escalation.txt
@@ -1,3 +1,3 @@
Escalate a ticket when:
- the user cannot work, or
-- the ticket is open for more than 2 days.
+- the ticket is open for more than 2 working days.

She commits, checks and pushes:

Terminal
git commit -am "Fix the escalation wording"
sh check.sh
git push -u origin hotfix/1.0.2
Output
[hotfix/1.0.2 c27858d] Fix the escalation wording
1 file changed, 1 insertion(+), 1 deletion(-)
PASS: 4 ticket lines checked
To /Users/amira/projects/c14-lab/team-remote.git
* [new branch] hotfix/1.0.2 -> hotfix/1.0.2
branch 'hotfix/1.0.2' set up to track 'origin/hotfix/1.0.2'.

PR: source hotfix/1.0.2 → target release/1.0. Sam reviews it and approves.

Current branch: release/1.0. Amira switches to the target, then merges, tags and pushes:

Terminal
git switch release/1.0
git merge --no-ff --no-edit hotfix/1.0.2
git tag -a v1.0.2 -m "Help desk notes 1.0.2"
git push
git push origin v1.0.2
Output (end)
To /Users/amira/projects/c14-lab/team-remote.git
257b55a..03ee8af release/1.0 -> release/1.0
To /Users/amira/projects/c14-lab/team-remote.git
* [new tag] v1.0.2 -> v1.0.2

The help desk has the fix. But main does not:

Terminal
git show main:escalation.txt
Output
Escalate a ticket when:
- the user cannot work, or
- the ticket is open for more than 2 days.

If the team makes release/1.1 from main now, version 1.1 says “2 days” again. That is a regression.

PR: source release/1.0 → target main. The reviewer looks at what the PR brings to main:

Terminal
git diff main...release/1.0 --stat
Output
contacts.txt | 2 +-
escalation.txt | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)

The PR shows both release fixes. Both are fixes, and there is no unfinished work, so the reviewer approves.

Current branch: main. Amira switches to the target of the PR. She pulls first, so that she merges into the newest main:

Terminal
git switch main
git pull
git merge --no-ff --no-edit release/1.0
Output (end)
Already up to date.
Merge made by the 'ort' strategy.
escalation.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

Look at the result. The PR showed two files, but the merge changed only escalation.txt. main already had the network team change from aeeb4b6. Git saw the same change on both sides, so it kept one copy of the line, without a conflict.

Current branch: main. She runs the check and pushes:

Terminal
sh check.sh
git push
Output
PASS: 5 ticket lines checked
To /Users/amira/projects/c14-lab/team-remote.git
3f4d833..9c54d1b main -> main
Terminal
git log --oneline --graph --decorate -9
Output
* 9c54d1b (HEAD -> main, origin/main, origin/HEAD) Merge branch 'release/1.0'
|\
| * 03ee8af (tag: v1.0.2, origin/release/1.0, release/1.0) Merge branch 'hotfix/1.0.2' into release/1.0
| |\
| | * c27858d (origin/hotfix/1.0.2, hotfix/1.0.2) Fix the escalation wording
| |/
| * 257b55a (tag: v1.0.1) Merge branch 'hotfix/1.0.1' into release/1.0
| |\
| | * 19b64c1 (origin/hotfix/1.0.1, hotfix/1.0.1) Fix the network team extension
| |/
* | 3f4d833 Add ticket 105
* | aeeb4b6 Fix the network team extension
* | 3cf73f7 Start the ticket export (not finished)
|/
* 465d7ea (tag: v1.0.0) Add escalation rules

The check from step 1 is now empty. main has every commit of the release, so this command prints nothing:

Terminal
git log --oneline main..release/1.0

The duplicate is still there. main now has two commits with the same change:

Terminal
git log --oneline --grep "network team" main
Output
19b64c1 Fix the network team extension
aeeb4b6 Fix the network team extension

--grep lists only the commits whose message contains the text. Here the duplicate is harmless: both commits make the same change, so the file is correct.

Two copies of the same change merge without trouble. Two different fixes for the same bug do not. For example, Amira writes “2 working days” on the release, and Sam writes “48 hours” on main. Then one of two things happens:

  • Both fixes change the same line. The back merge stops with a conflict. The first common mistake below shows it.
  • The fixes change different lines. Git merges them without a conflict, and the file now has both fixes. For example, a missing contact is added twice, in two places.

To prevent this, use one fix with one source:

  • Agree a team rule, and use only one of these:
    • Fix on main first, then cherry-pick to the release (the previous lesson).
    • Fix on the release first, then merge the release back into main (this lesson).
  • Before you write a fix, check whether someone already fixed the bug, for example with git log --oneline --grep "escalation" --all.
  • Always use cherry-pick -x, so that each copy names its original.
  • Merge the release back into main after each release, not weeks later.

Guided practice: fix the release, then merge back

Section titled “Guided practice: fix the release, then merge back”
  1. Start from Get the starting state. Current branch: release/1.0.

  2. Run git log --oneline --left-right --cherry-mark main...release/1.0. Two lines start with =.

  3. Make the hotfix branch from the release:

    Terminal
    git switch -c hotfix/1.0.2

    Current branch: hotfix/1.0.2.

  4. In escalation.txt, change more than 2 days to more than 2 working days. Save the file.

  5. Commit, check and push:

    Terminal
    git commit -am "Fix the escalation wording"
    sh check.sh
    git push -u origin hotfix/1.0.2
  6. Review the PR, source hotfix/1.0.2 → target release/1.0, with git diff release/1.0...hotfix/1.0.2. The diff changes only escalation.txt.

  7. Merge it into the target, and release v1.0.2:

    Terminal
    git switch release/1.0
    git merge --no-ff --no-edit hotfix/1.0.2
    git tag -a v1.0.2 -m "Help desk notes 1.0.2"
    git push
    git push origin v1.0.2

    Current branch: release/1.0.

  8. Run git show main:escalation.txt. It still says 2 days.

  9. Run git log --oneline main..release/1.0. The output lists four commits, including Fix the escalation wording.

  10. Review the back-merge PR, source release/1.0 → target main:

    Terminal
    git diff main...release/1.0 --stat

    The output lists contacts.txt and escalation.txt, and nothing else.

  11. Switch to main, and pull:

    Terminal
    git switch main
    git pull

    Current branch: main.

  12. Merge the release into main:

    Terminal
    git merge --no-ff --no-edit release/1.0

    The output lists only escalation.txt.

  13. Run sh check.sh. The output starts with PASS. Then run git push.

  14. Run git log --oneline main..release/1.0. The output is empty.

  15. Run git log --oneline --grep "network team" main. The output lists two commits with the same message.

Your turn: bring a fix back with cherry-pick

Section titled “Your turn: bring a fix back with cherry-pick”

The help desk extension is wrong in the release. It must be 4110, not 4100. Start from the end of the guided practice.

  1. Release the fix as v1.0.3. Make it on the release first, with a hotfix branch.
  2. Bring the fix to main with a cherry-pick, not a back merge. Use a branch called port-help-desk-fix, made from main, and a PR with the target main.
  3. Run git log --oneline main..release/1.0. Is the output empty? Explain why, and show a command that proves main has the fix.

Write the current branch and the PR source and target at each step.

Show a model answer
Step Current branch PR source → target
Make the hotfix branch release/1.0 → hotfix/1.0.3 none
Fix, commit, check, push hotfix/1.0.3 none
Review hotfix/1.0.3 hotfix/1.0.3 → release/1.0
Merge, tag v1.0.3, push release/1.0 hotfix/1.0.3 → release/1.0 (merged)
Make the port branch main → port-help-desk-fix none
Cherry-pick, check, push port-help-desk-fix none
Review port-help-desk-fix port-help-desk-fix → main
Merge, push main port-help-desk-fix → main (merged)
Terminal
git switch release/1.0
git switch -c hotfix/1.0.3
git commit -am "Fix the help desk extension"
sh check.sh
git push -u origin hotfix/1.0.3
git switch release/1.0
git merge --no-ff --no-edit hotfix/1.0.3
git tag -a v1.0.3 -m "Help desk notes 1.0.3"
git push
git push origin v1.0.3
git switch main
git switch -c port-help-desk-fix
git cherry-pick -x hotfix/1.0.3
sh check.sh
git push -u origin port-help-desk-fix
git switch main
git merge --no-ff --no-edit port-help-desk-fix
git push

Before the commit, change Help desk: ext. 4100 to Help desk: ext. 4110 in contacts.txt. git cherry-pick -x hotfix/1.0.3 copies the newest commit of hotfix/1.0.3, which is the fix.

The check is not empty:

Terminal
git log --oneline main..release/1.0
Output
2810d0a Merge branch 'hotfix/1.0.3' into release/1.0
642f5ce Fix the help desk extension

main has a copy of 642f5ce, not 642f5ce itself, so Git lists it. --cherry-mark proves that the change is on both sides:

Terminal
git log --oneline --left-right --cherry-mark main...release/1.0
Output (first lines)
< 211a8eb Merge branch 'port-help-desk-fix'
= 147f70e Fix the help desk extension
> 2810d0a Merge branch 'hotfix/1.0.3' into release/1.0
= 642f5ce Fix the help desk extension

git show main:contacts.txt also shows Help desk: ext. 4110.

This is one reason to prefer a back merge: after a merge, the check is empty when everything is on main. After a cherry-pick, the check lists commits that main already has, and you must check each one with --cherry-mark.

Common mistake

The bug came back in version 1.1

Symptom: the team finished the export, and made release/1.1 from main. The help desk reports that 1.1 says “2 days” again:

Terminal
git show release/1.1:escalation.txt
Output
Escalate a ticket when:
- the user cannot work, or
- the ticket is open for more than 2 days.

Cause: the fix went onto release/1.0 first, and nobody merged it back into main. release/1.1 came from main, so it does not have the fix. The check shows the missing commits. The hashes are different from the worked example, because this output comes from a separate practice run.

Terminal
git log --oneline release/1.1..release/1.0
Output
89962f3 Merge branch 'hotfix/1.0.2' into release/1.0
cafc5f2 Fix the escalation wording
e9e566e Merge branch 'hotfix/1.0.1' into release/1.0
4ef81fb Fix the network team extension

Fix:

  1. Merge the old release into the new one, with a PR from release/1.0 to release/1.1. This is a forward merge. Current branch: release/1.1.

    Terminal
    git merge --no-ff --no-edit release/1.0
    Output
    Merge made by the 'ort' strategy.
    escalation.txt | 2 +-
    1 file changed, 1 insertion(+), 1 deletion(-)
  2. Run git log --oneline release/1.1..release/1.0 again. The output is empty.

  3. Merge release/1.0 back into main as well, as in the worked example. Otherwise release/1.2 has the same problem.

To prevent this, run git log --oneline main..release/1.0 before you make a new release branch. Merge back until the output is empty, or until --cherry-mark shows = for every commit.

Common mistake

The back merge stops with a conflict

Symptom: Amira released “2 working days” in v1.0.2. At the same time, Sam fixed the same bug on main with “48 hours”. The back merge into main stops:

Output
Auto-merging escalation.txt
CONFLICT (content): Merge conflict in escalation.txt
Automatic merge failed; fix conflicts and then commit the result.

Cause: two people fixed the same bug in different ways. The two fixes change the same line, so Git cannot choose. git diff shows both versions and the original:

Output (end)
Escalate a ticket when:
- the user cannot work, or
++<<<<<<< HEAD
+- the ticket is open for more than 48 hours.
++||||||| 465d7ea
++- the ticket is open for more than 2 days.
++=======
- the ticket is open for more than 2 working days.
++>>>>>>> release/1.0

Fix:

  1. Talk to Sam. Agree on one wording. Here the team keeps “2 working days”, because the help desk already uses it in v1.0.2.

  2. Edit escalation.txt so that it contains only the agreed line, with no conflict markers.

  3. Stage the file, and finish the merge:

    Terminal
    git add escalation.txt
    git commit --no-edit
    Output
    [main f2744e8] Merge branch 'release/1.0'
  4. Run sh check.sh, then git push.

To prevent this, agree on one source for each fix, as in When duplicate fixes cause trouble.

The module asks you to show two things: both branches contain the fix, and unrelated work was not released. Run these commands at the end of the guided practice. If you also did “Your turn”, the help desk line shows ext. 4110.

Both branches have the fixed line. Show the file on each branch:

Terminal
git show release/1.0:contacts.txt
git show main:contacts.txt
Output
Help desk: ext. 4100
Network team: ext. 4401
Accounts team: ext. 4300
Help desk: ext. 4100
Network team: ext. 4401
Accounts team: ext. 4300

Both branches have a commit with the fix. --grep finds the commits by message:

Terminal
git log --oneline --grep "network team" release/1.0
Output
19b64c1 Fix the network team extension

You may know git branch --contains, which lists the branches that contain a commit. It does not work for a cherry-picked fix:

Terminal
git branch --contains aeeb4b6
Output
* main

release/1.0 is not in the list, but it has the fix. It contains the copy, 19b64c1, not aeeb4b6. git branch --contains looks for one exact commit, not for a change. So check the file contents, or use --grep and --cherry-mark.

Unrelated work was not released. Compare the first release with the release branch now:

Terminal
git diff v1.0.0 release/1.0 --stat
Output
contacts.txt | 2 +-
escalation.txt | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)

Only the two fixed files changed. export.txt and tickets.txt are not in the list, so the unfinished export and ticket 105 were not released.

Check your understanding

Predict: Amira fixes a bug directly on release/1.0 and releases v1.0.4. Nobody merges back. Two weeks later, the team makes release/1.1 from main. Does version 1.1 have the fix? Which command shows the problem before the team makes release/1.1?

Show the answer

No. release/1.1 comes from main, and main never got the fix, so the bug comes back: a regression. Run git log --oneline main..release/1.0 before you make release/1.1. It lists the fix commit, because main does not have it. Merge release/1.0 back into main first, with a PR from release/1.0 to main.

Check your understanding

Practical check: show that main and release/1.0 both contain the network team fix, and that the export was not released. Why does git branch --contains aeeb4b6 not list release/1.0?

Show the answer
  • git show main:contacts.txt and git show release/1.0:contacts.txt both show Network team: ext. 4401.
  • git log --oneline --grep "network team" release/1.0 lists 19b64c1, the copy with the -x line.
  • git diff v1.0.0 release/1.0 --stat lists only contacts.txt and escalation.txt. git show release/1.0:export.txt fails.

git branch --contains looks for the exact commit aeeb4b6. The release has a copy with a different hash, so the command does not list it, although the change is there.

Recap

  • A fix that starts on the release branch must also reach main. If not, the next release brings the bug back: a regression.
  • A back merge brings the release into main with a PR from release/1.0 to main. A forward merge goes from an older release line into a newer one. Names vary between teams.
  • Before you make a new release branch, run git log --oneline main..release/1.0.
  • A cherry-pick makes a duplicate: the same change in two commits. --cherry-mark shows them with =. Same changes merge cleanly. Different fixes for one bug cause conflicts.
  • Use one source for each fix, cherry-pick -x, and a team rule: fix on main first, or fix on the release and merge back.
  • You can now release a version, fix it with a traceable cherry-pick, and keep main and the release consistent.

Next: Final assignment