Git & Version Control

Chapter 6
Branching, Merging, and Integration

Certificate in DevOps
Git & Version Control   •   Certificate

Branching, Merging, and Integration

Introduction

Branching is one of Git’s most important capabilities. It allows several lines of development to exist in the same repository without requiring every change to be released immediately. A team can develop a feature, correct a defect, prepare a release, or experiment with an idea while preserving the stability of another branch.

Branching alone, however, does not integrate work. Integration occurs when commits from one line of development are incorporated into another. Git provides several integration techniques, especially merging and rebasing. Choosing between them affects project history, collaboration, traceability, and the risk of errors.

This chapter explains how Git represents branches, how fast-forward and three-way merges work, when rebasing is appropriate, how conflicts are resolved, and how teams can maintain an understandable history.

Learning Objectives

By the end of this chapter, you should be able to:

  • create, inspect, switch, and delete branches;
  • explain branches as references to commits;
  • distinguish fast-forward and three-way merges;
  • merge a development branch safely;
  • identify, resolve, and verify merge conflicts;
  • rebase a branch appropriately and explain its risks;
  • compare merge, rebase, and squash integration;
  • select a practical branching strategy;
  • maintain a readable and trustworthy project history.

1. Git Branches as Movable References

A Git commit is a snapshot of tracked files accompanied by metadata, including its parent commit, author, message, and timestamp. A branch is not a separate copy of a directory. It is a movable reference—usually a name pointing to one commit.

If the branch main points to commit C, and a new commit is created, Git moves main to the new commit. Earlier commits remain available through their parent relationships. A branch therefore identifies the current tip of a line of development.

A repository may contain branches such as:

BranchTypical purpose
mainStable, releasable history
developShared integration, where used by the team
feature/loginWork on a specific feature
bugfix/cache-errorCorrection of a defect
release/2.4Stabilization of a planned release
hotfix/payment-timeoutUrgent production correction

The names are conventions, not Git requirements. A team should choose names that communicate purpose and use them consistently.

Inspecting and creating branches

Common commands include:

git branch                 # list local branches
git branch --all           # list local and remote-tracking branches
git switch -c feature/login
                            # create and switch to a new branch
git switch main             # switch to an existing branch
git log --oneline --graph --decorate --all
                            # inspect history and branch positions

Older repositories may commonly use git checkout -b feature/login; git switch is clearer because it separates branch switching from file restoration.

Before switching branches, inspect or save local changes. Switching with uncommitted changes can be safe in some situations, but it can also produce confusing results or prevent the operation. A clean working tree is usually the safest starting point.

Remote-tracking branches

A remote-tracking branch such as origin/main records the state of a branch at the last fetch. It is not automatically updated every time another developer pushes. Use:

git fetch origin

to update remote-tracking information. Fetching downloads information without changing the current branch or working files. A local branch may track a remote branch, enabling commands such as git pull and git push to infer the destination.

2. A Simple Branching Model

Suppose main points to commit B. A developer creates feature/report and makes commits C and D:

A — B — C — D   feature/report
        main → B

The feature branch contains the history of main plus two new commits. The two branch names are references to different commits; the commits themselves are shared objects.

If another commit E is added to main, the history becomes divergent:

C — D   feature/report
       /
A — B
       \
        E       main

Neither branch is simply a continuation of the other. Integration is now required if the feature is to become part of main.

A useful concept here is the common ancestor, also called the merge base. In this example, B is the common ancestor of main and feature/report. Git uses this point when performing a three-way merge.

3. Fast-Forward Merges

A fast-forward merge occurs when the target branch is an ancestor of the source branch. For example:

A — B — C — D   feature/report
        main → B

If feature/report is merged into main, Git does not need to create a new merge commit. It simply moves the main reference from B to D:

A — B — C — D   main, feature/report

This is called “fast-forward” because Git advances the target reference without combining two divergent histories.

A fast-forward merge preserves a linear history and is easy to read. It does not, however, record that a separate feature branch existed. If that information matters for auditability or release analysis, a team may deliberately use a merge commit:

git switch main
git merge --no-ff feature/report

The --no-ff option forces a merge commit even when fast-forwarding is possible. It is useful when the team wants feature boundaries to remain visible, although excessive use can make history noisy.

4. Three-Way Merges

When branches have diverged, Git performs a three-way merge. It compares three snapshots:

  1. the merge base, or common ancestor;
  2. the current tip of the target branch;
  3. the tip of the branch being merged.

For the earlier example:

C — D   feature/report
       /
A — B
       \
        E       main

Git compares B with E and D. It calculates the changes made on each branch since B, then attempts to combine them.

If the changes affect different files or different non-overlapping lines, Git can usually combine them automatically. It creates a new merge commit with two parents:

C — D
       /     \
A — B       M   main
       \     /
        E ———

A merge commit records that two histories were joined. Its first parent is normally the previous tip of the target branch, and its second parent is the tip being merged. The exact graph display depends on the command and options used, but the conceptual relationship is stable.

A three-way merge is not the same as applying only the latest file versions. Git uses the common ancestor to determine what changed on each side. This is why the merge base is central to accurate integration.

Basic merge procedure

A disciplined procedure is:

git status
git switch main
git pull --ff-only origin main
git merge --no-ff feature/report
git status
git log --oneline --graph --decorate --all

The --ff-only option on the update step prevents an unexpected merge from being created while updating local main. Teams may instead fetch and inspect before integrating, especially in protected repositories.

After a successful merge, run the project’s tests and inspect the resulting diff. A merge that completes without conflict can still introduce a logical defect.

5. Merge Conflicts

A conflict occurs when Git cannot determine a safe automatic result. This commonly happens when both branches modify the same lines, delete and modify the same file, or rename files in incompatible ways.

During a conflict, Git marks the affected file with sections such as:

<<<<<<< HEAD
version from the current branch
=======
version from the branch being merged
>>>>>>> feature/report

The labels indicate alternatives, not instructions about which version is correct. The developer must edit the file into the intended final state and remove all conflict markers.

A safe conflict-resolution procedure is:

git status
# edit each conflicted file
git add path/to/resolved-file
git diff --cached
                      # inspect the staged resolution
git commit             # complete the merge, if Git requests a commit

For a merge conflict, use:

git merge --abort

to return to the state before the merge, provided no unrelated work has been mixed into the operation. Aborting is preferable to guessing when the integration approach is unclear.

Conflict resolution requires domain judgment. Git can identify textual disagreement, but it cannot determine whether a business rule, security requirement, data migration, or test expectation should prevail. After resolution, run unit tests, integration tests, static analysis, and relevant manual checks.

6. Rebasing

Rebasing moves or replays commits onto a new base. Suppose:

C — D   feature/report
       /
A — B — E       main

Rebasing the feature onto main conceptually extracts C and D, places the feature base at E, and replays the changes as new commits C′ and D′:

A — B — E — C′ — D′   feature/report

The resulting history is linear, but C′ and D′ are new commits with new identities. A commit’s identity depends on its content, parent, and metadata; changing its parent therefore creates a different commit.

Typical commands are:

git switch feature/report
git fetch origin
git rebase origin/main

If a conflict occurs:

# edit the conflicted files
git add path/to/resolved-file
git rebase --continue

To abandon the rebase:

git rebase --abort

When rebase is appropriate

Rebase is often appropriate for a private or personal branch that has not been shared, because it can update the branch onto the latest target and produce a clean sequence of commits. It is also useful before submitting a change for review, where a current base exposes integration problems early.

Rebase is dangerous when used on commits that other people have already based work on. Rewriting published history changes commit identities and can force collaborators to repair their branches. A practical rule is: do not rebase a shared branch unless the team has explicitly agreed on the procedure and understands the consequences.

After rebasing a branch previously pushed to a remote, a normal push may be rejected. If the rewritten branch is known to be safe to update, use:

git push --force-with-lease origin feature/report

--force-with-lease is safer than --force because it refuses to overwrite the remote branch if it has changed unexpectedly since the local repository last observed it. It is not a substitute for communication or review.

Rebase versus merge

CriterionMergeRebase
HistoryPreserves original topologyCreates a new linear sequence
Published commitsGenerally safeRewrites commit identities
Conflict timingUsually at integration timeDuring the rebase
Branch boundariesVisible with merge commitsMay disappear
AuditabilityStrong record of joining linesClean but rewritten story
Main riskExtra graph complexityDisrupting collaborators

Neither method is universally superior. The correct choice depends on whether history is shared, whether branch topology matters, and whether the team values a linear presentation over preservation of original relationships.

7. Squashing and Integration Policies

A squash combines several commits into one logical commit when integrating a branch. On a hosting platform, a “squash merge” commonly creates one new commit on the target branch and does not preserve the feature branch’s individual commits in the target history.

Squashing is useful when a branch contains work-in-progress commits such as “fix test,” “try again,” and “address review.” It can make the target history easier to understand. Its disadvantage is loss of the individual development sequence, which may matter for debugging or audit purposes.

Teams should define policies rather than applying commands inconsistently. A policy might state:

  • protected branches cannot receive direct pushes;
  • changes require review and passing automated checks;
  • feature branches may be rebased before review;
  • shared branches are merged rather than rebased;
  • every integration must pass the test suite;
  • merge messages identify the change or issue clearly;
  • force-pushing is restricted and uses --force-with-lease when permitted.

8. Branching Strategies

Feature branching

Each feature or defect is developed on a short-lived branch and integrated into the main line through review. This is simple and suitable for many small teams. The main risk is long-lived branches, which accumulate divergence and produce difficult conflicts.

Release branches

A release branch is created to stabilize a planned version while new development continues elsewhere. It can be useful when releases require formal testing or regulatory approval. The cost is additional coordination and the possibility that fixes must be propagated to multiple branches.

Trunk-based development

In trunk-based development, developers integrate small changes frequently into a shared main line. Features may be hidden behind feature flags until complete. This minimizes divergence and supports continuous integration, but it requires reliable automated tests, disciplined review, and careful deployment controls.

Git-flow-style models

Git-flow-style models use long-lived branches for development, releases, and hotfixes. They can provide explicit release structure, but they may be unnecessarily complex for teams deploying frequently. Branching strategy should reflect delivery frequency, team size, compliance needs, and test maturity rather than fashion.

9. Maintaining Understandable History

A useful history should help a future engineer answer: what changed, why did it change, when was it integrated, and which release contained it?

Good practices include:

  1. Use meaningful branch names. Include the purpose, such as feature/482-login-timeout.
  2. Write focused commits. A commit should represent one coherent change where practical.
  3. Use informative messages. Begin with a concise imperative summary and add context when necessary.
  4. Keep branches short-lived. Integrate regularly to reduce divergence.
  5. Separate formatting from behavior changes. This makes review and conflict resolution easier.
  6. Review the final diff. A successful merge is not proof of correctness.
  7. Preserve traceability. Link commits or pull requests to issues, requirements, or change records where appropriate.
  8. Do not hide uncertainty. If a conflict resolution involved a design decision, document that decision.

History should not be optimized only for visual neatness. A perfectly linear history may conceal the fact that two independently developed changes interacted. Conversely, a history containing every temporary experiment may overwhelm readers. The appropriate objective is an accurate, useful, and proportionate record.

10. Applied Case: Integrating a Payment Timeout Fix

Background

This illustrative case concerns a small online service. The protected main branch represents production-ready code. A developer creates hotfix/payment-timeout after users report that payments remain pending when a payment provider responds slowly.

Problem

While the hotfix is being developed, another developer changes the same service method to improve logging. Both branches edit adjacent sections of the same file. The team must integrate the correction without losing the logging improvement or weakening payment safety.

Analysis

The hotfix branch was created from commit R1. Before integration, main receives commit R2, which contains the logging change. The histories have diverged, so a fast-forward is impossible. A three-way merge or rebase is required.

The team chooses to rebase the private hotfix branch onto the current main. This exposes the interaction before review and avoids adding a merge commit to the small hotfix branch.

Approach and solution

The developer runs:

git switch hotfix/payment-timeout
git fetch origin
git rebase origin/main

Git reports a conflict in the payment service. The developer consults the issue and test requirements, then creates a result that retains the new logging while adding a bounded timeout and explicit failure handling. The developer runs:

git add src/payment/service.py
git rebase --continue

The branch is pushed for review using a protected workflow. Automated tests include successful payment, provider timeout, retry behavior, duplicate request handling, and log verification. Reviewers inspect both the code and the conflict resolution.

After approval, the change is integrated into main. The deployment system records the commit and issue identifier.

Outcome

The service now fails safely when the provider exceeds the configured response limit, preserves useful operational logs, and avoids duplicate payment attempts. The team has also documented the timeout behavior for future maintenance.

Learning points

  • A textual conflict can conceal a significant business or safety decision.
  • Rebasing a private branch can reveal integration problems early.
  • Tests must cover behavior, not merely successful compilation.
  • Conflict resolution should preserve requirements from both lines of development.
  • Traceability from issue to commit to deployment supports operational accountability.

11. Challenges, Governance, and Contemporary Practice

Branching introduces technical and organizational risks. Long-lived branches increase integration cost. Unreviewed force-pushes can destroy useful references. Poor conflict resolution can introduce security vulnerabilities or data corruption. A repository may also contain sensitive credentials accidentally committed during branch experimentation; branches do not provide security isolation once data has entered repository history.

Use protected branches, mandatory reviews, automated checks, least-privilege permissions, and secret-scanning tools. If a secret is committed, removing it from the latest file is insufficient: the historical object may remain accessible. The credential should be revoked or rotated, followed by an approved history-remediation process.

Continuous integration changes the economics of branching. Small, frequently integrated branches reduce the period during which assumptions diverge. Feature flags can separate deployment from release, allowing incomplete code to be integrated safely when the flags and fallback behavior are well designed.

AI-assisted coding can generate changes rapidly, but it does not remove the need for branch discipline. AI-generated code should be reviewed, tested, checked for licensing and security concerns, and integrated through the same controlled workflow as human-written code. Automated tools can assist with conflict explanations, but responsibility for the final result remains with the development team.

Best-Practice Checklist

Before merging or rebasing, ask:

  • Is the working tree understood and, preferably, clean?
  • Is the target branch up to date?
  • Is the branch shared with other developers?
  • Does the chosen operation preserve the team’s history policy?
  • Have conflicts been resolved according to requirements rather than convenience?
  • Has the resulting diff been inspected?
  • Have automated and relevant manual tests passed?
  • Is the commit or pull request traceable to a requirement or issue?
  • Has the branch been removed or retained intentionally after integration?

Chapter Summary

Git branches are movable references to commits, not independent copies of a repository. They enable parallel development while preserving a shared object history. A fast-forward merge moves the target reference when the target is an ancestor of the source. A three-way merge uses the common ancestor and both branch tips to combine divergent histories, often producing a merge commit.

Rebase replays commits onto a new base and creates new commit identities. It is useful for private or carefully controlled branches, but it should not casually rewrite shared history. Squashing can produce a concise integration record at the cost of detailed branch history. Merge, rebase, and squash are policy choices with different effects on collaboration and traceability.

Conflict resolution is a technical and domain activity: Git identifies textual disagreement, but developers determine the correct business, security, and operational behavior. Short-lived branches, focused commits, protected integration branches, automated testing, review, and explicit history policies help teams integrate safely and maintain an understandable record of change.

Key Terms

Branch: A movable reference that identifies the tip of a line of development.

Commit: A recorded project snapshot with metadata and parent references.

Branch tip: The commit currently identified by a branch reference.

Target branch: The branch into which another branch is integrated.

Source branch: The branch whose changes are being integrated.

Common ancestor: A commit reachable from both branches.

Merge base: The common ancestor selected by Git for a three-way merge.

Fast-forward merge: Integration that advances the target reference without creating a merge commit.

Three-way merge: Integration based on the merge base and the two branch tips.

Merge commit: A commit with multiple parents that records the joining of histories.

Merge conflict: A situation in which Git cannot automatically determine a safe combined result.

Rebase: Replaying commits onto a different base, creating new commit identities.

Squash: Combining several changes into one logical commit during integration.

Remote-tracking branch: A local reference recording a remote branch’s last fetched state.

Protected branch: A branch governed by restrictions such as review or status checks.

Force-with-lease: A guarded force push that checks whether the remote reference changed unexpectedly.

Feature flag: A control that separates code deployment from user-visible activation.

Branching strategy: A team’s systematic policy for creating, integrating, and retiring branches.

History rewriting: Changing commit identities or ancestry, commonly through rebase.

Traceability: The ability to connect a change with its rationale, review, tests, and release.

Review Questions

Short Answer Questions

  1. What is a Git branch, and how does it differ from a separate copy of a project?
  2. Under what condition does a fast-forward merge occur?
  3. What three snapshots are compared in a three-way merge?
  4. Why can rebasing a shared branch disrupt collaborators?
  5. Give two practices that help maintain an understandable history.

Long Answer Questions

  1. Explain fast-forward and three-way merges using commit graphs and describe when each occurs.
  2. Describe a complete procedure for resolving a merge conflict, including verification after resolution.
  3. Compare merge, rebase, and squash integration with respect to history, collaboration, and traceability.
  4. Explain how trunk-based development, feature branching, and release branches address different delivery conditions.
  5. Discuss why conflict resolution requires domain knowledge in addition to Git commands.

Analytical / Application Questions

  1. A private feature branch is three commits behind main, and main has changed in the same service file. Which integration approach would you choose and why? Describe the commands and safeguards you would use.
  2. A developer force-pushes a rebased branch that several colleagues have used. Analyze the likely consequences and propose a recovery and communication plan.
  3. Design a branching and integration policy for a team that deploys weekly, requires code review, and must preserve an auditable relationship between changes and production releases.

Multiple Choice Questions with Answers

Question 1

What does a Git branch primarily represent?

A. A complete independent copy of the repository

B. A movable reference to a commit

C. A compressed archive of source files

D. A list of unresolved conflicts

Correct Answer: B. A movable reference to a commit

Question 2

When does a fast-forward merge occur?

A. When both branches modify the same line

B. When the target branch is an ancestor of the source branch

C. When Git always creates a merge commit

D. When a rebase has failed

Correct Answer: B. When the target branch is an ancestor of the source branch

Question 3

What does Git use as the third input in a three-way merge, besides the two branch tips?

A. The oldest commit in the repository

B. The remote server timestamp

C. The common ancestor or merge base

D. The most recently tagged release

Correct Answer: C. The common ancestor or merge base

Question 4

Which command abandons an in-progress rebase and returns to its previous state?

A. git merge --abort

B. git rebase --continue

C. git rebase --abort

D. git reset --remote

Correct Answer: C. git rebase --abort

Question 5

Why is rebasing a shared branch risky?

A. It permanently deletes all working files

B. It changes commit identities and can disrupt collaborators

C. It prevents all future merges

D. It automatically exposes repository secrets

Correct Answer: B. It changes commit identities and can disrupt collaborators

Question 6

Which command is generally safer than a plain force push after rewriting a branch?

A. git push --force-with-lease

B. git push --delete-all

C. git merge --force-only

D. git fetch --overwrite-local

Correct Answer: A. git push --force-with-lease

Question 7

What should a developer normally do after manually resolving a merge conflict?

A. Leave conflict markers for the reviewer

B. Stage the resolved files, complete the operation, and run tests

C. Delete the target branch immediately

D. Reinitialize the repository

Correct Answer: B. Stage the resolved files, complete the operation, and run tests

Question 8

What is a principal benefit of squashing a feature branch during integration?

A. It preserves every temporary commit exactly

B. It creates a concise logical change in the target history

C. It guarantees that conflicts cannot occur

D. It prevents code review

Correct Answer: B. It creates a concise logical change in the target history

Question 9

Which practice usually reduces integration difficulty?

A. Keeping branches open indefinitely

B. Avoiding automated tests

C. Integrating small changes frequently

D. Force-pushing protected branches without notice

Correct Answer: C. Integrating small changes frequently

Question 10

What does fetching a remote repository normally do?

A. Updates remote-tracking information without changing the current working files

B. Automatically merges all remote branches into the current branch

C. Deletes local branches that are absent remotely

D. Rewrites all local commit identities

Correct Answer: A. Updates remote-tracking information without changing the current working files

Git & Version Control
Chapter 6 of 14