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 rules

10.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 timeout

10.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-timeout

10.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 main

10.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.0

10.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.