4. Branching and Merging

Branches are one of the most powerful features of Git. They let you create a version of the main code to work on new features, fix bugs, or simply experiment, without affecting the original code. Afterward, you can download the changes or include the new code in the "official" version.

Imagine your project is a tree:

  • The main branch (usually called main or master), which is the trunk of the tree.
  • The development branches (or new features), which are the branches that grow out of the trunk.

Each branch can evolve independently, and you can even create other branches from them. If a change is interesting and adds value, the new changes can be merged into the trunk.

This way you can:

  • Work on new features without breaking the stable code.
  • Multiple people can work in parallel on the same code base.
  • Experiment with ideas without consequences. When you delete a branch, everything you did in it disappears without consequences.

To manage branches we'll use the following commands:

# View all branches
git branch

# View all branches (including remote ones)
git branch -a

# Create a new branch
git branch new-feature

# Create and switch to a branch
git branch -c my-branch

# Delete a branch
git branch -d branch-name

# Force delete a branch
git branch -D branch-name

# Rename a branch
git branch -m old-name new-name

git checkout

Switches between branches or restores files.

# Switch to an existing branch
git checkout main

# Create a branch and switch to it
git checkout -b new-branch

# Switch to a specific commit
git checkout abc1234

# Restore a file from the last commit
git checkout -- file.txt

git switch

A more modern and specific command for switching branches (since Git 2.23).

# Switch to a branch
git switch main

# Create and switch to a new branch
git switch -c new-feature

# Go back to the previous branch
git switch -

Recommendation: Use git switch to switch branches and git checkout for operations with files.

Branch workflow

# 1. Create a branch for your feature
git switch -c feature/login

# 2. Make changes
echo "Login component" > login.js
git add login.js
git commit -m "Add login component"

# 3. Make more commits as needed
echo "Tests" > login.test.js
git add login.test.js
git commit -m "Add tests for login"

# 4. Go back to main
git switch main

# 5. Merge your branch
git merge feature/login

git merge

Merges a branch into the current branch.

# Merge a branch
git merge branch-name

# Merge without fast-forward (creates a merge commit)
git merge --no-ff branch-name

# Abort a merge
git merge --abort

Types of merge:

Fast-forward: When there are no new commits on the base branch.

%%{init: {'theme':'base', 'themeVariables': { 'git0': '#ff6b6b', 'git1': '#4ecdc4'}}}%%
gitGraph
    commit id: "A"
    commit id: "B"
    branch feature
    commit id: "C"
    commit id: "D"
    checkout main
    merge feature tag: "Fast-forward"

Three-way merge: When there are changes on both branches.

%%{init: {'theme':'base', 'themeVariables': { 'git0': '#ff6b6b', 'git1': '#4ecdc4', 'git2': '#95e1d3'}}}%%
gitGraph
    commit id: "A"
    commit id: "B"
    branch develop
    checkout develop
    commit id: "C"
    branch feature/new-feature
    checkout feature/new-feature
    commit id: "D"
    commit id: "E"
    checkout develop
    commit id: "F"
    merge feature/new-feature tag: "M1"
    checkout main
    commit id: "G"
    merge develop tag: "M2"

Squash merge: Combines all the branch's commits into a single one.

%%{init: {'theme':'base', 'themeVariables': { 'git0': '#ff6b6b', 'git1': '#4ecdc4'}}}%%
gitGraph
    commit id: "A"
    commit id: "B"
    branch feature
    checkout feature
    commit id: "C"
    commit id: "D"
    commit id: "E"
    checkout main
    commit id: "F"
    commit id: "S (C+D+E)" type: HIGHLIGHT tag: "Squash"

Merge vs Rebase vs Fast-forward

There are different ways to integrate changes from one branch into another. Each one has its advantages and use cases.

Fast-forward merge

It's the simplest type of merge. It happens when the target branch has had no new commits since the branch you want to merge was created. Git simply moves the branch pointer forward.

  • Doesn't create a merge commit
  • The history stays linear
  • It's as if you had never created a branch
# main is at commit B
# You create feature and make commits C and D
# main is still at B (no one has committed there)
git switch main
git merge feature  # Automatic fast-forward

Result: main now points to D, the history is A → B → C → D

%%{init: {'theme':'base', 'themeVariables': { 'git0': '#ff6b6b', 'git1': '#4ecdc4'}}}%%
gitGraph
    commit id: "A"
    commit id: "B"
    branch feature
    commit id: "C"
    commit id: "D"
    checkout main
    merge feature tag: "Fast-forward (main → D)"

Force or avoid fast-forward:

# Avoid fast-forward (always create a merge commit)
git merge --no-ff feature

# Force fast-forward (fails if not possible)
git merge --ff-only feature

Three-way merge (Traditional merge)

It happens when both branches have new commits. Git creates a new commit that has two parents: the last commit of each branch.

  • Creates a merge commit (a commit with two parents)
  • Preserves the entire history
  • Clearly shows when branches were merged
  • The history is not linear

When to use:

  • When you want to keep the context that there was a branch
  • On long-lived branches (develop, release)
  • When you work in a team and want to see who merged what

Advantages:

  • Complete and honest history
  • Easy to revert (you only revert the merge commit)
  • Doesn't rewrite history

Disadvantages:

  • History can become complex with many branches
  • Merge commits "pollute" the history
git switch main
git merge feature  # Creates a merge commit if there are changes on main
%%{init: {'theme':'base', 'themeVariables': { 'git0': '#ff6b6b', 'git1': '#4ecdc4'}}}%%
gitGraph
    commit id: "A"
    commit id: "B"
    branch feature
    checkout feature
    commit id: "C"
    commit id: "D"
    checkout main
    commit id: "E"
    commit id: "F"
    merge feature tag: "M (merge commit)"

Squash merge

Combines all the commits of a branch into a single new commit before merging them. It's like taking all the branch's work and "compressing" it into a single commit.

  • Creates a single commit with all the changes
  • Loses the individual commit history of the branch
  • The history on main stays clean and simple
  • Doesn't keep the relationship with the original branch

When to use:

  • Pull Requests with many small or development commits
  • When the detailed branch history is not important
  • You want to keep main with a clean history
  • Each commit on main represents a complete feature

Advantages:

  • Very clean and readable main history
  • One commit = one complete feature
  • Easy to revert (a single commit)
  • Removes intermediate commits like "fix typo", "WIP", etc.

Disadvantages:

  • You lose the detailed development history
  • You can't cherry-pick individual commits
  • Makes it harder to understand how the feature was developed
git switch main
git merge --squash feature
git commit -m "feat: add complete new feature"
%%{init: {'theme':'base', 'themeVariables': { 'git0': '#ff6b6b', 'git1': '#4ecdc4'}}}%%
gitGraph
    commit id: "A"
    commit id: "B"
    branch feature
    checkout feature
    commit id: "C (WIP)"
    commit id: "D (fix typo)"
    commit id: "E (add tests)"
    checkout main
    commit id: "F"
    commit id: "S (C+D+E squashed)" type: HIGHLIGHT tag: "Squash"

GitHub uses squash merge as the default option in many projects. It's very common in open source development.

Rebase

Rebase "relocates" your commits onto the tip of another branch. Instead of merging, it rewrites history as if you had started your work from the most recent commit of the target branch.

  • Rewrites the history
  • Creates new commits (with different hashes)
  • The history stays linear
  • There are no merge commits
# Before the rebase:
# main: A → B → E → F
# feature: A → B → C → D

git switch feature
git rebase main

# After the rebase:
# main: A → B → E → F
# feature: A → B → E → F → C' → D'
# (C' and D' are commits C and D "relocated")

When to use:

  • Local feature branches before pushing
  • You want a clean, linear history
  • You update your branch with recent changes from main

Advantages:

  • Linear history that's easy to read
  • No merge commits
  • Makes it easier to bisect and review changes

Disadvantages:

  • Rewrites history (never rebase public commits)
  • Can be confusing for beginners
  • Conflicts are resolved commit by commit

NEVER rebase commits that are already in shared repositories (pushed to origin).

Practical comparison

Scenario: You have a feature branch with commits C and D. Meanwhile, main has commits E and F.

With merge:

git switch main
git merge feature
# Result: A → B → E → F → M (merge of C and D)
#                    ↘ C → D ↗

With rebase + merge:

git switch feature
git rebase main      # Relocates C and D after F
git switch main
git merge feature    # Fast-forward
# Result: A → B → E → F → C' → D'

Which one to use?

Use Fast-forward (letting it happen naturally):

  • Small, quick changes
  • You don't need to preserve the branch context

Use Merge (three-way):

  • Important branches (develop → main, release → main)
  • You want to preserve the historical context
  • Collaborative work where the history matters
  • You don't want to rewrite history

Use Squash merge:

  • Pull Requests in open source projects
  • Features with many intermediate development commits
  • You want a clean history on main where each commit is a feature
  • You don't need the detailed history of how it was developed

Use Rebase:

  • Update your local branch with changes from main
  • Before creating a PR (to have a clean history)
  • Personal feature branches
  • You want a linear history

Recommended workflow:

# While working on your feature
git switch feature

# Update with changes from main using rebase
git fetch origin
git rebase origin/main

# Resolve conflicts if any
git add file.txt
git rebase --continue

# When done, create a PR
# The maintainer will decide whether to merge or squash

Complete example:

# Strategy 1: Traditional merge
git switch main
git merge feature/login --no-ff -m "Merge: add login feature"

# Strategy 2: Rebase + Fast-forward
git switch feature/login
git rebase main           # Update the branch
git switch main
git merge feature/login   # Automatic fast-forward

# Strategy 3: Squash merge (combines everything into one commit)
git switch main
git merge --squash feature/login
git commit -m "feat: add complete login system"

# Strategy 4: Fast-forward only (fails if not possible)
git switch main
git merge --ff-only feature/login

Conflict resolution

A conflict occurs when two branches modify the same lines of code.

Example of a conflict:

# On branch main
echo "main version" > file.txt
git add file.txt
git commit -m "Update on main"

# Create and switch to a new branch from an earlier commit
git switch -c feature
echo "feature version" > file.txt
git add file.txt
git commit -m "Update on feature"

# Try to merge
git switch main
git merge feature
# Conflict!

Git will mark the conflict in the file:

<<<<<<< HEAD
main version
=======
feature version
>>>>>>> feature

Resolve the conflict:

  1. Open the file and decide which version to keep
  2. Remove the conflict markers (<<<<<<<, =======, >>>>>>>)
  3. Save the file
  4. Add it to staging: git add file.txt
  5. Complete the merge: git commit

Tools to resolve conflicts:

# View files with conflicts
git status

# Use a visual merge tool
git mergetool

# Accept all changes from one branch
git checkout --theirs file.txt  # The branch you're merging
git checkout --ours file.txt    # The current branch

Branching strategies

Feature Branch

The simplest strategy: one branch per feature.

main
  ├── feature/login
  ├── feature/dashboard
  └── bugfix/header-typo
# Create a feature branch
git switch -c feature/new-feature

# Develop
git add .
git commit -m "Implement feature"

# Merge when ready
git switch main
git merge feature/new-feature
Activity 1
  1. Create a branch called feature/new-page.
  2. On that branch, create a page.html file with basic HTML content.
  3. Commit the changes.
  4. Go back to the main branch.
  5. Merge the feature/new-page branch into main.
  6. Delete the feature/new-page branch.
Activity 2

Conflict practice:

  1. On the main branch, edit the first line of README.md.
  2. Commit.
  3. Create a conflict-test branch from an earlier commit.
  4. Edit the same line of the README with different text.
  5. Commit.
  6. Try to merge conflict-test into main.
  7. Resolve the conflict manually.
  8. Complete the merge.

This work is under a Attribution-NonCommercial-NoDerivatives 4.0 International license.

Desafíos de programación atemporales y multiparadigmáticos

Desafíos de programación atemporales y multiparadigmáticos

Te encuentras ante un librillo de actividades, divididas en 2 niveles de dificultad. Te enfrentarás a los casos más comunes que te puedes encontrar en pruebas técnicas o aprender conceptos elementales de programación.

Buy the book

Will you buy me a coffee?

This is how I keep writing without ads or paywalls.

Comments

There are no comments yet.