Git Branching and Collaboration
A branch is a movable reference to a commit.
What this module covers
Branches, HEAD, and detached HEAD Create, switch, rename, and delete branches Merge commits and fast-forward merges Three-way merge and conflict markers Conflict resolution workflow Rebase and replaying commits Merge versus rebase
Remote repositories, fetch, pull, and push Upstream tracking branches Pull/merge requests and code review
Trunk-based development and short-lived branches Release branches and hotfixes Tags and release notes
10.1 Branches and HEAD
A branch is a movable reference to a commit. HEAD usually identifies the current branch, or it can directly identify a commit in detached-HEAD state.
Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible):
A branch does not duplicate every file; it points into the commit graph. Switching updates the working tree and index to the chosen commit, subject to local changes and conflict rules.
git switch -c feature/can-timeout
git branch --list
git switch main
git branch -d feature/can-timeout$ git switch -c feature/can-timeout
Switched to a new branch 'feature/can-timeout'
$ git branch --list
* feature/can-timeout
main
$ git switch main
Switched to branch 'main'
$ git branch -d feature/can-timeout
Deleted branch feature/can-timeout (was 86adc2d).10.2 Merge
Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible), after a commit on the branch:
A fast-forward merge moves the target branch pointer forward when possible. A three-way merge combines changes using a common ancestor. Conflicts occur when Git cannot determine a safe combined result automatically.
git switch main
git merge feature/can-timeout$ git switch -c feature/can-timeout
Switched to a new branch 'feature/can-timeout'
$ sed -i 's/return 100;/return 250;/' src/can.c && git commit -qam 'fix: raise CAN timeout to 250 ms'
$ git switch main
Switched to branch 'main'
$ git merge feature/can-timeout
Updating 5fc0362..79b8deb
Fast-forward
src/can.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline -3
79b8deb fix: raise CAN timeout to 250 ms
5fc0362 feat: add CAN timeout
86adc2d chore: add ignore rules10.3 Conflict workflow
Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible): two branches change the same line, then the status and diff commands above (the resolution follows below): Typical conflict markers:
Do not merely delete markers without understanding the intended behavior. Run relevant tests after resolving.
Terminal session continued — resolving the conflict shown above:
The resolution keeps the slow-bus value and records why. With GIT_EDITOR set to accept the default, git commit created the merge commit with Git's prepared message.
git status
# Open each conflicted file and resolve conflict markers.
git diff
git add path/to/resolved-file
git commit$ git switch -c feature/can-retry
Switched to a new branch 'feature/can-retry'
$ sed -i 's/return 250;/return 500; \/* slow bus *\//' src/can.c && git commit -qam 'fix: allow 500 ms on slow buses'
$ git switch main
Switched to branch 'main'
$ sed -i 's/return 250;/return 300;/' src/can.c && git commit -qam 'fix: tune timeout to 300 ms'
$ git merge feature/can-retry
Auto-merging src/can.c
CONFLICT (content): Merge conflict in src/can.c
Automatic merge failed; fix conflicts and then commit the result.
[exit status 1]
$ git status
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: src/can.c
no changes added to commit (use "git add" and/or "git commit -a")
$ git diff
diff --cc src/can.c
index 2f78fe8,85e303b..0000000
--- a/src/can.c
+++ b/src/can.c
@@@ -1,4 -1,4 +1,8 @@@
int can_timeout_ms(void)
{
++<<<<<<< HEAD
+ return 300;
++=======
+ return 500; /* slow bus */
++>>>>>>> feature/can-retry
}
$ cat src/can.c
int can_timeout_ms(void)
{
<<<<<<< HEAD
return 300;
=======
return 500; /* slow bus */
>>>>>>>
…<<<<<<< HEAD
current branch content
=======
incoming branch content
>>>>>>> feature/can-timeout$ printf 'int can_timeout_ms(void)\n{\n return 500; /* slow bus; covers the 300 ms tuning case */\n}\n' > src/can.c
$ git add src/can.c
$ git commit
[main 12ae103] Merge branch 'feature/can-retry'
$ git log --oneline --graph -5
* 12ae103 Merge branch 'feature/can-retry'
|\
| * 6973c6c fix: allow 500 ms on slow buses
* | 148d11d fix: tune timeout to 300 ms
|/
* 79b8deb fix: raise CAN timeout to 250 ms
* 5fc0362 feat: add CAN timeout10.4 Rebase
Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible). A local bare repository stands in for the server, and a teammate has pushed a change to the same function:
Rebase replays commits onto a new base and creates new commit IDs for rewritten commits. It can make local history easier to read, but rewriting shared/public history can disrupt collaborators.
During a conflict:
Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible), continuing: Resolving the conflict and continuing
Terminal session continued — inspect, resolve, git add, git rebase --continue:
--abort instead returns the branch to exactly where it was before the rebase:
Terminal session (a separate experiment branch):
git switch feature/can-timeout
git fetch origin
git rebase origin/main$ git log --oneline -2
5f3e353 docs: point to CAN notes
6241c7c feat: add CAN retry count
$ git switch feature/can-timeout
Already on 'feature/can-timeout'
$ git fetch origin
From /tmp/bsvc/remote/project
* [new branch] feature/logging -> origin/feature/logging
12ae103..23a51d1 main -> origin/main
$ git rebase origin/main
Rebasing (1/2)
Auto-merging src/can.c
CONFLICT (content): Merge conflict in src/can.c
error: could not apply 6241c7c... feat: add CAN retry count
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 6241c7c... feat: add CAN retry count
[exit status 1]git status
# Resolve files, then:
git add path/to/file
git rebase --continue
# Or abandon the operation:
git rebase --abort$ git status
interactive rebase in progress; onto 23a51d1
Last command done (1 command done):
pick 6241c7c feat: add CAN retry count
Next command to do (1 remaining command):
pick 5f3e353 docs: point to CAN notes
(use "git rebase --edit-todo" to view and edit)
You are currently rebasing branch 'feature/can-timeout' on '23a51d1'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: src/can.c
no changes added to commit (use "git add" and/or "git commit -a")$ git diff --name-only --diff-filter=U; sed -n '6,14p' src/can.c
src/can.c
int can_retries(void)
{
<<<<<<< HEAD
return 5;
=======
return 3;
>>>>>>> 6241c7c (feat: add CAN retry count)
}
$ printf 'int can_timeout_ms(void)\n{\n return 500; /* slow bus; covers the 300 ms tuning case */\n}\n\nint can_retries(void)\n{\n return 5; /* main chose 5; this branch proposed 3 */\n}\n' > src/can.c
$ git add src/can.c
$ git rebase --continue
[detached HEAD 7dfe85b] feat: add CAN retry count
1 file changed, 1 insertion(+), 1 deletion(-)
Rebasing (2/2)
Successfully rebased and updated refs/heads/feature/can-timeout.
$ git log --oneline -4
bc036e5 docs: point to CAN notes
7dfe85b feat: add CAN retry count
23a51d1 feat: retry CAN frames five times
12ae103 Merge branch 'feature/can-retry'$ git switch -q -c experiment origin/main && sed -i 's/return 5;/return 9;/' src/can.c && git commit -qam 'try nine retries'
$ git rebase feature/can-timeout | grep CONFLICT
Rebasing (1/1)
error: could not apply 89f7258... try nine retries
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 89f7258... try nine retries
CONFLICT (content): Merge conflict in src/can.c
$ git status --short
UU src/can.c
$ git rebase --abort && git status --short && git log --oneline -1
89f7258 try nine retries
$ git switch -q feature/can-timeout10.5 Merge versus rebase
Merge: records a merge relationship and preserves existing commit identities.
Rebase: replays commits and creates a different linearized history.
Choose based on team policy, traceability, review requirements, and whether commits have already been shared.
Neither approach automatically validates the resulting code.
Try it: commit graph
10.6 Remote collaboration
Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible). The placeholder URL was replaced by a local bare repository, /tmp/bsvc/remote/project.git:
The example URL is a placeholder. Use your organization's actual remote. fetch downloads remote references and objects; pull performs a fetch followed by an integration operation determined by configuration/options. --ff-only refuses a non-fast-forward integration rather than creating a merge commit.
git clone https://example.com/team/project.git
git remote -v
git fetch origin
git switch -c feature/logging
git push -u origin feature/logging
git pull --ff-only$ git init -q --bare /tmp/bsvc/remote/project.git
$ git remote add origin /tmp/bsvc/remote/project.git
$ git push -q -u origin main
$ git clone /tmp/bsvc/remote/project.git teammate && cd teammate
Cloning into 'teammate'...
done.
$ git remote -v
origin /tmp/bsvc/remote/project.git (fetch)
origin /tmp/bsvc/remote/project.git (push)
$ git fetch origin
$ git switch -c feature/logging
Switched to a new branch 'feature/logging'
$ git push -u origin feature/logging
To /tmp/bsvc/remote/project.git
* [new branch] feature/logging -> feature/logging
Branch 'feature/logging' set up to track remote branch 'feature/logging' from 'origin'.
$ git pull --ff-only
Already up to date.10.7 Cherry-pick
Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible): a fix on main back-ported to release/1.x:
Cherry-pick applies the change introduced by a commit onto the current branch, usually creating a new commit. It is useful for backports and selective fixes, but repeated cherry-picks can complicate history and conflict resolution.
git cherry-pick abc1234$ git log --oneline -1
9286323 fix: reject NULL input in parser
$ git switch release/1.x
Switched to branch 'release/1.x'
$ git cherry-pick 9286323
[release/1.x e7bdd56] fix: reject NULL input in parser
Date: Thu Jan 1 00:22:00 2026 +0000
1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline -2
e7bdd56 fix: reject NULL input in parser
148d11d fix: tune timeout to 300 ms
$ git switch -q main10.8 Tags
Terminal session (Git 2.34.1, Ubuntu 22.04 on WSL2; the same practice repository throughout the Git modules, with identity Example Developer and commit dates fixed to 1 January 2026 so the IDs are reproducible):
Annotated tags contain tag metadata and are commonly used for releases. Protect release tags according to team policy; a tag name alone is not proof that an artifact was built securely from that source.
git tag -a v1.2.0 -m "Release 1.2.0"
git show v1.2.0
git push origin v1.2.0$ git tag -a v1.2.0 -m "Release 1.2.0"
$ git show v1.2.0
tag v1.2.0
Tagger: Example Developer <[email protected]>
Date: Thu Jan 1 00:24:00 2026 +0000
Release 1.2.0
commit 928632399905e96799eaabe7c41cef94b9cf64a8
Author: Example Developer <[email protected]>
Date: Thu Jan 1 00:22:00 2026 +0000
fix: reject NULL input in parser
diff --git a/src/parse.c b/src/parse.c
index 84c4345..29c8be0 100644
--- a/src/parse.c
+++ b/src/parse.c
@@ -1,6 +1,6 @@
int parse(const char *s)
{
- if (s[0] == 0)
+ if (s == 0 || s[0] == 0)
return -1;
return s[0] - 0x30;
}
$ git push origin v1.2.0
To /tmp/bsvc/remote/project.git
* [new tag] v1.2.0 -> v1.2.0
$ git tag -n
v1.2.0 Release 1.2.010.9 Collaboration workflow example
Update local main.
Create a short-lived feature branch.
Make a small, focused change.
Run build and tests.
Inspect staged diff and commit.
Push branch and open a review.
Address review feedback and CI failures.
Merge according to repository policy.
Delete the feature branch when no longer needed.
Confirm the released artifact corresponds to the reviewed commit.
Quiz
? In the rebase session, the branch's commits had new IDs after `git rebase origin/main`. Why?
+ Rebase creates new commits with new parents; the ID hashes the parent too. :: Same change, different commit.
- Git renumbers commits periodically. :: IDs are content hashes.
- The author changed. :: The author was the same.
- Rebase compresses commits. :: It replays them.
? `git push` was rejected with “fetch first”. What does that mean?
+ The remote branch has commits you do not have locally. :: Integrate them, then push.
- You lack permission to push. :: That gives a different error.
- The branch is protected. :: Also a different error.
- Your commits are invalid. :: They were fine.
? When is a fast-forward merge possible?
+ When the target branch has no commits that the source branch lacks :: The pointer simply moves forward.
- When there are conflicts :: Conflicts require a real merge.
- Only with `--no-ff` :: That option *prevents* fast-forwarding.
- Always :: Diverged histories need a merge commit or rebase.