B. Software Development · Software Development: Managing a Team Software ProjectTeamwork with Git: Branches, Merges, Conflicts and Code Review
B1.7 B2.1About 75 minutes
Learning goals- I can create a branch for a task, commit on it, and merge it back into main.
- I can explain the difference between a fast-forward merge and a merge commit.
- I can resolve a merge conflict by talking to my teammate and editing the conflict markers.
- I can use a shared repository with clone, pull and push, following team rules that keep main working.
- I can review a teammate's code with a checklist and respond to a review of my own code.
Words to know
branch
A separate line of development. In Git a branch is a movable label pointing to its newest commit.
main
The branch that holds the team's current working version. Team rule: main always runs and passes its tests.
merge
Combining the commits from one branch into another.
fast-forward
A merge where main has not changed since the branch was made, so Git simply moves main forward to the branch's newest commit.
merge conflict
When both branches changed the same lines differently, so Git cannot decide which to keep and asks a person.
remote (origin)
A copy of the repository on another computer, usually a shared hosted repository that everyone pushes to and pulls from.
push / pull
Push sends your commits to the remote; pull fetches teammates' commits from the remote and merges them into your copy.
pull request (merge request)
A request, on a hosting site, to merge a branch into main; teammates review the changes and comment before it is merged.
Four people, one program
In the last lesson Git kept your history. The real power of version control appears when four people work on the same program at the same time. Jordan is halfway through the loans module, which does not run yet. Luc wants to finish the search feature. Amara needs a working version to test. If everyone edited the same files in one place, nobody could test anything, ever.
Branches solve this. Each task gets its own branch. Work on a branch does not affect main until it is finished, tested and merged. This lesson also covers sharing a repository between computers, what to do when two people change the same line, and code review, which is how a team keeps its code quality high. All of this is part of B1.7 (managing source code with shared resources) and B2.1 (contributing as a team member).
A branch splits off main, collects commits for one task (C3, C4), and is merged back. Meanwhile main gets other work (C5). The merge commit M has two parents.Branch, commit, merge
The basic cycle for one task:
git switch -c feature/search creates a branch called feature/search and switches to it. (The feature/ prefix is just a naming habit.)- Edit, test and commit on the branch as usual.
git switch main goes back to main. Your branch's files disappear from the folder, but they are safe in the branch.git merge feature/search brings the branch's commits into main.git branch -d feature/search deletes the label once the work is merged.
If main has not changed since the branch was created, the merge is a fast-forward: Git just moves the main label forward. Here is Luc's search feature, which was merged that way.
$ git switch -c feature/search
Switched to a new branch 'feature/search'
(Luc writes search.py and tests it)
$ git add search.py
$ git commit -m "Add search by partial name (R6)"
[feature/search 9d4ef03] Add search by partial name (R6)
1 file changed, 2 insertions(+)
create mode 100644 search.py
$ git switch main
Switched to branch 'main'
$ git merge feature/search
Updating 50e76a7..9d4ef03
Fast-forward
search.py | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 search.py
$ git branch -d feature/search
Deleted branch feature/search (was 9d4ef03).
Terminal session (not Python). Because main had not moved, the merge was a fast-forward. Your commit IDs will be different.
If main has changed (someone else merged work meanwhile), Git makes a merge commit with two parents, combining both lines of work. Usually the changes are in different files or different parts of a file, and Git merges them automatically.
When Git cannot decide: merge conflicts
Git merges line by line, comparing each branch with the common ancestor (the commit where they split). This is called a three-way merge:
- If only one side changed a line, keep that change.
- If both sides made the same change, keep it.
- If both sides changed the same line differently, Git cannot know which is right. That is a conflict, and a person must decide.
Here is the idea in Python. It works on files with the same number of lines; Git's real algorithm also handles added and deleted lines, but the decision rule is the same.
def merge3(base, ours, theirs):
"""Merge two edited copies of a file line by line (same number of lines)."""
result = []
conflicts = 0
for b, o, t in zip(base, ours, theirs):
if o == t: # both the same (changed the same way, or not at all)
result.append(o)
elif o == b: # only they changed this line
result.append(t)
elif t == b: # only we changed this line
result.append(o)
else: # both changed it, differently
result += ["<<<<<<< ours", o, "=======", t, ">>>>>>> theirs"]
conflicts += 1
return result, conflicts
base = ["LOAN_DAYS = 14", "MAX_ITEMS = 3", "FEE_PER_DAY = 0.25"]
ours = ["LOAN_DAYS = 7", "MAX_ITEMS = 3", "FEE_PER_DAY = 0.25"]
theirs = ["LOAN_DAYS = 21", "MAX_ITEMS = 3", "FEE_PER_DAY = 0.50"]
merged, n = merge3(base, ours, theirs)
print("\n".join(merged))
print(n, "conflict(s)")
Output: a conflict block for LOAN_DAYS (both sides changed it differently), then MAX_ITEMS = 3 (unchanged), FEE_PER_DAY = 0.50 (only theirs changed it, so theirs is kept automatically), and 1 conflict(s).
A real conflict on the LendIt team
A volunteer told Luc that two weeks is too long, so on main he changed LOAN_DAYS to 7. Meanwhile, on a branch, Jordan changed it to 21 because a parent asked for longer ski loans. When Jordan merges, Git stops:
$ git merge feature/longer-loans
Auto-merging config.py
CONFLICT (content): Merge conflict in config.py
Automatic merge failed; fix conflicts and then commit the result.
$ cat config.py
<<<<<<< HEAD
LOAN_DAYS = 7
=======
LOAN_DAYS = 21
>>>>>>> feature/longer-loans
MAX_ITEMS = 3
(edit config.py: keep LOAN_DAYS = 14 as agreed with the client,
and delete the three marker lines)
$ git add config.py
$ git commit -m "Merge feature/longer-loans: keep 14 days as agreed with client"
[main ce0d2d5] Merge feature/longer-loans: keep 14 days as agreed with client
$ git log --oneline --graph
* ce0d2d5 Merge feature/longer-loans: keep 14 days as agreed with client
|\
| * f96d338 Lend items for 21 days
* | 18c1e63 Lend items for 7 days
|/
* 9d4ef03 Add search by partial name (R6)
* 50e76a7 Add config
Terminal session (not Python). Between <<<<<<< HEAD and ======= is the version on main (where you are); between ======= and >>>>>>> is the branch's version. After resolving, git log --graph shows the two lines of work joining. Your commit IDs will be different.
Worked exampleExample 1: Resolving a conflict properly
How should Jordan resolve the LOAN_DAYS conflict?
- Do not just pick your own version. A conflict usually means two people had different ideas. Here, both Luc and Jordan changed a business rule without asking.
- Talk to the people involved. Jordan messages Luc; they realize the loan period is set by requirement R2 (14 days), which the client signed. Neither of them should have changed it.
- Edit the file: keep
LOAN_DAYS = 14, and delete all three marker lines (<<<<<<<, ======= and >>>>>>>). A file with markers left in it will not even run. - Run the tests, then
git add config.py and git commit with a message that explains the decision. - Follow up: Priya records the volunteer's and parent's requests as change requests for Denise to decide (see the lesson on keeping a project on track).
Keep 14 days (the signed requirement), remove the markers, test, commit with an explanation, and send both suggestions through the change process. Merge conflicts are often communication problems in disguise.
Seeing differences like Git does
A diff is the list of line changes between two versions. Python's difflib module produces the same "unified" format Git uses: - for a removed line, + for an added one, and a space for an unchanged context line. The @@ -1,2 +1,3 @@ header means "lines 1 to 2 of the old file became lines 1 to 3 of the new".
import difflib
old = ["LOAN_DAYS = 14", "FEE_PER_DAY = 0.25"]
new = ["LOAN_DAYS = 14", "FEE_PER_DAY = 0.25", "MAX_FEE = 5.00"]
for line in difflib.unified_diff(old, new, "a/loans.py", "b/loans.py", lineterm=""):
print(line)
Output: --- a/loans.py, +++ b/loans.py, @@ -1,2 +1,3 @@, the two unchanged lines (each starting with a space), and +MAX_FEE = 5.00. Compare it with the git diff in the previous lesson: the format is the same.
Sharing a repository
For a team, the repository lives in a shared place: a private repository on a hosting site, a school server, or a shared network folder. It is called a remote, and Git names it origin by default. Each member has a full clone on their own computer.
git clone <address> makes your copy (once).git pull brings in teammates' new commits (do it every time you start work).git push sends your commits to the shared repository.
The workflow below keeps main working. Each command was checked with a shared repository; the address is an example.
$ git clone https://example.org/maple-grove/lendit.git (once: copy the shared repository)
$ git pull (start of every session: get teammates' work)
$ git switch -c feature/overdue-list (a branch for one task)
... edit, test, git add, git commit (several small commits) ...
$ git push -u origin feature/overdue-list (send the branch to the shared repository)
... open a pull request on the hosting site; a teammate reviews it ...
$ git switch main
$ git pull (after the merge, get the new main)
A typical team session (terminal, not Python). The comments in brackets are explanations, not part of the commands.
Every team member has a full clone. The shared repository is where everyone's work meets. Pull before you start; push small, tested commits.Pull requests and code review
On most hosting sites, instead of merging into main yourself, you open a pull request (some sites call it a merge request): "Please merge feature/overdue-list into main." A teammate reviews the changes, comments on specific lines, and approves or asks for changes. Often the hosting site also runs the tests automatically. Only then is the branch merged.
Code review is one of the most effective ways teams catch bugs, and it spreads knowledge: after reviewing the loans module, Amara understands it too. A review should be specific, kind and about the code, not the person. "This loop can miss the last loan; try range(len(loans))" is useful. "This is messy" is not.
The pull-request workflow. If the reviewer requests changes, the author fixes them on the same branch and pushes again; the pull request updates automatically.Worked exampleExample 2: Reviewing a pull request
Luc opens a pull request for the overdue list (R4). Amara reviews it with the team's checklist. What does she check, and what does she write?
- Does it meet the requirement? R4 asks for the item, the member and days late. The list shows the member number but not the name that Denise asked for at the design review. Comment: "Design doc section 5 says the overdue list shows the member's name too; could you add it?"
- Do the tests pass, and are there new tests? Tests pass, but none checks an item that is due today. Comment: "Please add a test where today is the due date; that item is not overdue yet, so it should not be listed."
- Readable? Names are clear; every public function has a docstring with the requirement ID. Comment: "Nice docstrings." (Say what is good, too.)
- Safe? No passwords or real data are included; only
sample_items.csv is used. - Size? 60 changed lines: small enough to review properly. She clicks Request changes, Luc fixes both points and pushes, and she approves.
Two specific requests (member name, due-date boundary test), one compliment, and an approval after the fixes. The bug is caught before it reaches main.
Team Git rules for LendIt
main always runs and passes all tests.- One branch per task, named
feature/... or fix/..., merged within about two days. git pull at the start of every session; push at the end.- Every change goes through a pull request reviewed by someone else.
- Commit messages are imperative and name a requirement or task.
- Never commit secrets or real personal data.
Just for funGit's conflict markers, <<<<<<< and >>>>>>>, are each seven characters long. Seven arrows pointing straight at the problem: subtle, they are not.
Hands-on activityProject step 7: Branch, merge and review
You need: your Git repository from project step 6. Optional: a teammate, or a free private repository on a hosting site.
- Create a branch for one task from your schedule, such as
feature/<task>. Make at least two small commits on it, then merge it into main. - Make a conflict on purpose (safe practice): create branch
practice/conflict, change one line of a file and commit. Switch to main, change the same line differently and commit. Merge the branch, read the markers, resolve them, and commit. Write two sentences in docs/plan.md about what you did. - Write your team's Git rules (use the LendIt rules as a model) in
README.md. - Code review: swap one module with a teammate (or, working alone, review your own module the next day, wearing the tester's hat). Use the checklist from Example 2. Write at least one compliment and one specific request.
- With a teammate and a hosting site: clone the shared repository on both computers, push a branch, open a pull request, review it and merge it.
What to look for: git log --oneline --graph shows at least one branch joining main; no conflict markers left in any file; review comments that are specific and kind.
Your turn
1Which command creates a branch called fix/fee-rounding and switches to it?
git switch -c fix/fee-rounding.
2When is a merge a fast-forward?
When main has not changed since the branch was created, so Git just moves the main label forward to the branch's newest commit (no merge commit is needed).
3Base line: MAX_ITEMS = 3. Ours: MAX_ITEMS = 3. Theirs: MAX_ITEMS = 4. What does a three-way merge keep?
Only theirs changed it, so the merge keeps MAX_ITEMS = 4 automatically, with no conflict.
4Base: FEE = 0.25. Ours: FEE = 0.30. Theirs: FEE = 0.35. What happens?
Both changed the same line differently: a conflict. A person must decide (and here, since the fee is a client rule, ask the client).
5What must you remove from a file after resolving a conflict?
The three marker lines: <<<<<<< ..., ======= and >>>>>>> ... (and the version you are not keeping).
6Why should you git pull at the start of every work session?
To get teammates' latest commits, so you work on the current version. It reduces conflicts and avoids building on old code.
7Give two things a reviewer checks in a pull request.
Any two: it meets the requirement; tests pass and new tests were added; code is readable with docstrings; no secrets or real data; the change is small enough to review.
More practice: warm-up
Sends your commits to the remote (shared) repository.
Makes a full copy of a repository, including its history, on your computer.
3What does a merge commit have that an ordinary commit does not?
Two parents (one from each branch).
4What is the usual default name of the shared remote?
5Finish the team rule: "main always..."
"...runs and passes all its tests."
More practice: on your own
1Use merge3 on base ["a", "b", "c"], ours ["a", "B", "c"], theirs ["a", "b", "C"]. What is the result?
Line 1 unchanged: a. Line 2 only we changed: B. Line 3 only they changed: C. Result ["a", "B", "C"] with 0 conflicts.
2Use merge3 on base ["x"], ours ["y"], theirs ["y"]. Is there a conflict? Why?
No. Both sides made the same change, so o == t and the merge keeps y.
3Why is "just keep my version" a bad way to resolve conflicts?
It throws away a teammate's work without telling them, and the conflict may mean the team disagrees about a requirement. The right fix is to understand both changes, talk to the teammate, and check the requirements.
4Rewrite this review comment so it is useful: "This code is bad."
Example: "In overdue(), an item that is due today is listed as overdue, because the comparison uses >=. Requirement R4 says only items past their due date. Could you change it to > and add a test for that case?" It is specific, points to a line and a requirement, and suggests a fix.
5In the difflib example, what would the output be if old and new were identical?
Nothing. unified_diff produces no lines at all (not even the --- and +++ headers) when there are no differences.
Challenge
1merge3 assumes the three versions have the same number of lines. Describe a situation where this fails and what Git does differently.
If one side adds or deletes a line (for example, theirs inserts MAX_FEE = 5.00 in the middle), the lines no longer line up by position, and comparing line 2 with line 2 would compare unrelated lines. Git first aligns each version with the base using a diff (finding the longest common subsequence of lines), then applies the three-way rule to the matching regions.
2Two teammates keep getting conflicts in main.py because both add menu options. Suggest two changes to the team's process or design that would reduce these conflicts.
- Merge more often: small branches merged within a day or two, and
git pull before starting, so both work on the latest version. 2. Design change: store menu options in a list or dictionary built from separate entries (or let each module register its own option), so adding an option touches a different line or file each time. Also, talk: "I'm editing the menu today."
3A teammate pushed a commit directly to main that breaks the tests, right before the client demo. Describe what the team should do now and what rule would prevent it next time.
Now: use git revert <commit> to undo it with a new commit (do not rewrite shared history), run the tests to confirm main works, push, then let the teammate redo the change on a branch. Prevention: protect main so that changes arrive only through pull requests that pass the tests and are approved by a reviewer (most hosting sites have a setting for this).
Check your understandingQuiz
1. Why do teams develop each task on its own branch?
2. Base: X = 1. Ours: X = 2. Theirs: X = 1. What does a three-way merge produce?
3. What does a conflict marker ======= separate?
4. Which command should you run at the start of every work session on a shared project?
5. Which is the most useful code-review comment?
6. A merge is a fast-forward when:
For parents and teachersBig idea: B1.7 asks students to manage source code with shared resources and proper version control, and B2.1 asks them to contribute as team members. This lesson covers Git branches, merging, resolving conflicts, shared repositories (clone, pull, push) and code review through pull requests.
How to help: If your teen is working alone, offer to be the "reviewer": they can walk you through a change and explain why it is correct, which often reveals bugs. Encourage the deliberate-conflict exercise; resolving a conflict calmly once makes the real ones far less stressful.
Common difficulties:
- Leaving conflict markers in a file (the program will not run).
- Long-lived branches that drift far from main and then conflict badly. Merge little and often.
- Forgetting to pull before starting.
- Review comments that are vague or personal. Encourage "specific, kind, about the code".