Day 4: Merge Requests, Merge Strategies, and Resolving Merge Conflicts
This chapter covers the core mechanics of Git merge requests (MRs) within GitLab. It explores the lifecycle of a merge request, compares the four primary merge strategies (Merge Commit, Squash Merge, Fast-Forward, and Rebase), and provides step-by-step practical workflows for configuring branch protection, executing squash merges, and safely resolving merge conflicts.
1. Why Do We Need Merge Requests?
In production software development, a Merge Request (MR) is not merely a raw Git operation; it is a governance and process layer built on top of Git.
In a naive development workflow, a developer might run git merge locally to combine their feature branch directly into the production branch. However, in enterprise environments, this approach is highly risky because the code has not been validated.
To mitigate this risk, teams use trunk-based development or feature-branching workflows where direct pushes to production branches are blocked. Instead, developers propose changes via a Merge Request.
An MR acts as a quality gate that ensures:
- Code Review: Peers can inspect, discuss, and suggest modifications to the code.
- Automated Testing: Continuous Integration (CI) pipelines run automatically to validate that the new code does not break existing functionality.
- Auditability: A historical record of who proposed, reviewed, and approved the changes is preserved.
2. The Core Mental Model: The Control Gateway
Real-Life Analogy
Think of your production branch (e.g.,
masterormain) as a highly secure facility. You cannot allow visitors (untested code) to walk straight through the front door unescorted.A Merge Request is the security checkpoint at the entrance. At this checkpoint:
- The visitor's ID is verified (Code Review).
- Their bags are scanned for hazardous items (CI Pipeline Execution).
- Security personnel sign off on their entry (Approvals).
Only after passing all these checks is the visitor allowed to step inside the facility (Merging).
3. The 6-Step Merge Request Lifecycle
The journey of code from a developer's local machine to the production branch follows a structured six-step lifecycle:
Step 1: Feature Branch Creation
Developers isolate their work by creating a dedicated branch from the latest state of the production branch. This prevents unstable code from breaking the production environment.
git checkout -b feature/add-login
Step 2: Push and Propose
Once the code is written and committed locally, the branch is pushed to the remote GitLab repository, and a Merge Request is opened.
git push origin feature/add-login
- Source Branch: The branch containing the new changes (
feature/add-login). - Target Branch: The stable branch where changes will be integrated (
masterormain).
Step 3: Set MR State (Draft vs. Ready)
Developers can flag the status of their MR to signal its readiness to the team:
- Draft: Indicates the work is still in progress. It prevents accidental merges and signals to reviewers that the code is not yet finalized.
- Ready: Signals that the work is complete, pipelines are passing, and the code is ready for formal peer review.
Step 4: Review and Discussion
Reviewers analyze the diff (the changes between the source and target branches). They can leave inline comments, suggest specific code fixes, and discuss implementation details.
Step 5: Pipeline Execution
GitLab CI/CD pipelines run automated tests, linters, and security scanners. The pipeline status (Pass/Fail) is displayed directly inside the MR widget, serving as an automated quality gate.
Step 6: The Merge Decision
Once peer reviews are completed, discussions are resolved, and pipelines pass, the authorized maintainer executes the merge, integrating the feature branch into the target branch.
4. Code Review & Approvals: Free Tier vs. Enterprise
GitLab handles code reviews and approvals differently depending on your subscription tier:
Note: In the GitLab Free Tier, approvals serve as a social contract and a visual indicator of readiness rather than a hard security restriction.
5. Deep Dive into Merge Strategies
When integrating a feature branch into a target branch, Git can execute the merge using different strategies. Choosing the right strategy depends on how clean you want your commit history to be.
Strategy 1: Merge Commit
This is the default Git merge behavior. It preserves every single commit made on the feature branch and adds an explicit "Merge commit" to tie the histories together.
- Result: Commits
CandDremain visible in the main branch history, alongside a new merge commitM.
Strategy 2: Squash Merge
Squash merging condenses all commits from the feature branch into a single, unified commit before applying it to the target branch.
- Result: The target branch history remains clean and linear. The intermediate "noise" commits (e.g., "fixed typo", "test commit") are discarded, leaving only one high-quality commit
S. - Mathematical Representation: If a feature branch has commits , they are squashed into a single commit :
Strategy 3: Fast-Forward Merge
A fast-forward merge occurs when the target branch has not received any new commits since the feature branch was created. Instead of creating a merge commit, Git simply moves the target branch pointer (HEAD) forward to the latest commit of the feature branch.
- Limitation: This only works if the target branch has not diverged. If a new commit
Fis added to the main branch while you are working on the feature branch, a pure fast-forward merge is impossible.
Strategy 4: Rebase + Fast-Forward
When the target branch has diverged, you can use git rebase to rewrite the history of your feature branch. Rebasing takes the new commits from the target branch and applies them as the new base of your feature branch.
6. Hands-on Lab 1: Setting Up the Repository & Overcoming Branch Protection
In this lab, we configure a local repository, push changes to the remote, and resolve a common real-world error: pushing to a protected branch.
Step 1: Initialize and Push Initial Commits
Create a file locally, commit it, and attempt to push it directly to the remote master branch.
# Create the first test file
echo "Aditya Jaiswal" > test1.txt
# Stage and commit
git add test1.txt
git commit -m "added test1.txt"
# Attempt to push to remote master
git push origin master
Step 2: Troubleshooting the Protected Branch Error
If the master branch is protected in GitLab, you will receive an error message:
remote: GitLab: You are not allowed to push code to protected branches on this project.
fatal: Could not read from remote repository.
Resolution Workflow:
To allow developers to push directly to the master branch during initial setups:
- Navigate to your GitLab project.
- Go to Settings -> Repository.
- Expand the Protected branches section.
- Locate the protected branch (e.g.,
masterormain). - Change the Allowed to push and merge dropdown to include Developers + Maintainers.
- Return to your terminal and retry the push:
git push origin master
Now, add a second file to establish a baseline commit history:
echo "Second file content" > test2.txt
git add test2.txt
git commit -m "added test2.txt"
git push origin master
Verify your commit history:
git log --oneline
Output:
7. Hands-on Lab 2: Executing a Squash Merge
This lab demonstrates how to clean up a messy commit history by configuring and executing a Squash Merge in GitLab.
Step 1: Configure GitLab to Encourage Squashing
- In your GitLab project, navigate to Settings -> Merge requests.
- Scroll down to the Squash commits when merging section.
- Select Encourage (this displays a checkbox during the MR process that is selected by default).
- Click Save changes.
Step 2: Create a Feature Branch and Make Multiple Commits
Create a new branch and simulate a developer making multiple small, noisy commits to a file named app.txt.
# Create and switch to the squash demo branch
git checkout -b feature/squash-demo
# Commit 1: Add line 1
echo "line 1" >> app.txt
git add app.txt
git commit -m "add line 1"
# Commit 2: Add line 2
echo "line 2" >> app.txt
git add app.txt
git commit -m "add line 2"
# Commit 3: Fix a typo
echo "fix typo" >> app.txt
git add app.txt
git commit -m "fix typo"
# Commit 4: Final change
echo "final change" >> app.txt
git add app.txt
git commit -m "final change"
# Push the branch to the remote repository
git push origin feature/squash-demo
Verify the local commit history:
git log --oneline
Output:
Step 3: Create and Merge the Squash Merge Request
- Go to GitLab and click New merge request.
- Set the Source branch to
feature/squash-demoand the Target branch tomaster. - Title the MR:
Day 4 Squash Merge Demo. - Ensure the Squash commits when merging checkbox is checked.
- Approve and click Merge.
Step 4: Verify the Cleaned History
Pull the merged changes back to your local master branch and inspect the log:
git checkout master
git pull origin master
git log --oneline
Output:
Observation: The four individual commits (
add line 1,add line 2,fix typo, andfinal change) have been squashed into a single clean commit:Day 4 Squash Merge Demo.
8. Hands-on Lab 3: Simulating and Resolving Merge Conflicts
Merge conflicts occur when two different branches make conflicting changes to the same line of the same file, and Git cannot automatically determine which version to keep.
Step 1: Clean Up Local Branches
# Delete the old local feature branch
git branch -D feature/squash-demo
Step 2: Create Branch A and Modify app.txt
# Create and switch to Branch A
git checkout -b feature/update-app
# Modify app.txt
echo "feature change" > app.txt
git add app.txt
git commit -m "added feature change"
git push origin feature/update-app
Step 3: Create Branch B and Modify app.txt on the Same Line
# Switch back to master and pull latest changes
git checkout master
git pull origin master
# Create and switch to Branch B
git checkout -b feature/conflict-test
# Modify the exact same file with conflicting content
echo "conflicting change" > app.txt
git add app.txt
git commit -m "added conflicting change"
git push origin feature/conflict-test
Step 4: Merge Branch A into Master
Go to GitLab, create a Merge Request for feature/update-app into master, and merge it. This change is now live on the master branch.
Step 5: Trigger the Conflict with Branch B
Create a Merge Request for feature/conflict-test into master.
GitLab will display a warning:
Merge blocked: Merge conflict.
Because feature/update-app already changed app.txt to "feature change", Git cannot automatically overwrite it with "conflicting change".
Step 6: Safe Conflict Resolution Workflow
To resolve the conflict safely, pull the latest changes from the target branch into your local feature branch, resolve the conflict manually, and push the update.
1. Pull Master into the Conflicting Branch
Switch to your conflicting branch locally and pull the latest changes from the remote master branch:
git checkout feature/conflict-test
git pull origin master
2. Locate and Resolve the Conflict Markers
Open app.txt in your editor. You will see Git conflict markers:
<<<<<<< HEAD: Contains the changes from your current local branch (feature/conflict-test).=======: The divider between the conflicting changes.>>>>>>> master: Contains the changes from the incoming branch (master).
Resolve the conflict by deleting the markers and writing the final desired content:
hello updated by Aditya
3. Commit and Push the Resolution
Stage the resolved file, commit the changes, and push them back to the remote repository:
git add app.txt
git commit -m "Fix conflict"
git push origin feature/conflict-test
4. Verify in GitLab
Return to your GitLab Merge Request page. The conflict warning will disappear, and the status will update to Ready to merge. Click Merge to complete the integration.
Summary and Key Takeaways
- Merge Requests are Quality Gates: An MR is a process layer on top of Git that enforces peer reviews, discussions, and CI/CD pipeline validations before code is integrated into production.
- Draft vs. Ready States: Use the Draft status to protect work-in-progress code from being merged prematurely while still allowing early feedback.
- GitLab Free Tier Limitations: In the free tier, approvals are non-mandatory and cannot be programmatically enforced. Enterprise tiers allow you to enforce mandatory approval rules.
- Squash Merging Keeps History Clean: Squashing combines multiple intermediate commits into a single, high-quality commit, keeping the target branch's history clean and readable.
- Fast-Forward Requires a Linear Path: Fast-forward merges simply move the branch pointer forward. They only work if the target branch has not diverged since the feature branch was created.
- Rebase Rewrites History: Rebasing reapplies your feature branch commits on top of the latest commit of a diverged target branch, enabling a clean fast-forward merge.
- Resolving Conflicts Locally is Safest: When a merge conflict occurs, pull the target branch into your local feature branch, resolve the conflict markers manually, commit the changes, and push them to unblock the Merge Request.