Git Basics: commit, branch, and merge

1. Overview

Git is a version control tool that almost every project uses, but many beginners only remember a few commands without understanding the concepts behind them. This article covers three core concepts: commit (saving a snapshot), branch (independent working branches), and merge (combining branches). After reading, you’ll confidently create new branches, commit correctly, handle conflicts during merge, and know when to use merge instead of rebase.

2. Commit – saving a snapshot

2.1. What is a commit

Each commit is a “snapshot” of the entire state of files at a specific point in time, along with a pointer to the parent commit. Git doesn’t store diffs; it stores snapshots — this makes branching and merging fast and cheap.

# Initialize repo and make the first commit
git init order-service
cd order-service
echo "# Order Service" > README.md
git add README.md
git commit -m "docs: add README for Order Service"

Explanation:

  • git init creates a new repo and generates the hidden .git directory.
  • git add README.md moves the file to the staging area (buffer waiting for commit).
  • git commit -m "..." creates a snapshot with a descriptive message of the changes.

Check the history:

git log --oneline
a1b2c3d docs: add README for Order Service

2.2. Write good commit messages

A commit should answer the question “what does this change do, and why?” Common convention (conventional commits):

feat: add API to create orders
fix: fix incorrect total calculation when applying coupon
refactor: extract OrderValidator from OrderService

Note: Avoid vague commit messages like "update" or "fix bug". Months later when looking back at git log, you won’t know what that commit fixed.

2.3. Commit in parts with git add -p

When a file has multiple unrelated changes, don’t commit them all at once.

git add -p src/main/java/com/nonladev/OrderService.java

Git will ask which “hunk” (chunk of changes) you want to stage, allowing you to split them into small, reviewable commits that are easy to revert if needed.

3. Branch – working independently on multiple branches

3.1. Create and switch branches

A branch is just a lightweight pointer to a specific commit. Creating a new branch doesn’t copy all the code; it just adds another pointer.

# Create a new branch and switch to it
git branch feature/add-payment
git checkout feature/add-payment

# Or combine the two commands above (Git ≥ 2.23)
git switch -c feature/add-payment

Explanation:

  • git branch <name> creates a branch but stays on the current branch.
  • git checkout <name> or git switch <name> moves HEAD to that branch.
  • -c in git switch is a shortcut to create-and-switch at once.

3.2. View list of branches

git branch -v
* feature/add-payment  d4e5f6a add PaymentService skeleton
  main                 a1b2c3d docs: add README for Order Service

The * indicates the current branch (where HEAD is pointing).

3.3. Follow naming conventions for branches

Clear branch names help the whole team understand what the branch does:

feature/add-payment       # add new feature
fix/order-total-wrong     # fix bug
refactor/extract-validator
hotfix/prod-null-pointer

3.4. Delete branch after completion

# Delete local branch that has been merged
git branch -d feature/add-payment

# Delete unmerged branch (force delete, careful not to lose commits)
git branch -D feature/add-payment

-d (lowercase) is safer because Git will warn if the branch hasn’t been merged anywhere.

4. Merge – combining branches

4.1. Fast-forward merge

If main has no new commits since you branched off, Git only needs to move the main pointer to the last commit of the feature branch — this is called fast-forward.

git switch main
git merge feature/add-payment
Updating a1b2c3d..d4e5f6a
Fast-forward
 PaymentService.java | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

No new merge commit is created; history remains a straight line.

4.2. Merge commit (3-way merge)

If both branches have separate commits since the branch point, Git creates a merge commit with two parents, merging content from both branches.

git switch main
git commit -am "fix: fix incorrect item quantity validation"   # main has a new commit
git switch feature/add-payment
git switch main
git merge feature/add-payment
Merge made by the 'recursive' strategy.
 PaymentService.java | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

Git automatically creates a merge commit with the default message Merge branch 'feature/add-payment'.

4.3. Handle conflicts during merge

A conflict occurs when two branches modify the same section of code. Example with OrderService class:

// In main
public class OrderService {
    public BigDecimal calculateTotal(Order order) {
        return order.getSubtotal().multiply(BigDecimal.valueOf(1.1)); // add 10% tax
    }
}
// In feature/add-payment
public class OrderService {
    public BigDecimal calculateTotal(Order order) {
        return order.getSubtotal().subtract(order.getDiscount()); // subtract discount
    }
}

During merge, Git can’t decide which line to keep, so markers appear in the file:

public class OrderService {
    public BigDecimal calculateTotal(Order order) {
<<<<<<< HEAD
        return order.getSubtotal().multiply(BigDecimal.valueOf(1.1));
=======
        return order.getSubtotal().subtract(order.getDiscount());
>>>>>>> feature/add-payment
    }
}

You need to manually fix the logic to combine correctly (for example: subtract discount first, then add tax), remove the markers <<<<<<<, =======, >>>>>>>, then:

git add OrderService.java
git commit

Note: When committing after resolving a conflict, you don’t need -m; Git will open an editor with a default message clearly stating this is a merge commit — keep that message as-is unless you want to add notes.

4.4. Merge vs Rebase

Criteria merge rebase
History Preserved with merge commit Rewrites history, branch is linear
Safety Doesn’t modify old commits Modifies old commits (changes hashes)
Use when Public shared branches (main, develop) Personal branch, want clean history before creating PR
# Rebase feature branch onto latest main
git switch feature/add-payment
git rebase main

Note: Don’t rebase a branch that’s already pushed and others are pulling from — you’ll corrupt their history and cause massive conflicts when they pull again.

5. Test a merge scenario with JUnit

Although Git isn’t Java code, you can completely write tests to verify that business logic doesn’t break after merging two branches — this is why CI always runs tests on merge commits. Example of OrderService after resolving the conflict in section 4.3:

public class OrderService {

    public BigDecimal calculateTotal(Order order) {
        BigDecimal afterDiscount = order.getSubtotal().subtract(order.getDiscount());
        return afterDiscount.multiply(BigDecimal.valueOf(1.1));
    }
}
class OrderServiceTest {

    private final OrderService orderService = new OrderService();

    @Test
    void shouldApplyDiscountThenTax() {
        Order order = new Order(
                BigDecimal.valueOf(100),  // subtotal
                BigDecimal.valueOf(10)    // discount
        );

        BigDecimal total = orderService.calculateTotal(order);

        assertThat(total).isEqualByComparingTo(BigDecimal.valueOf(99.0));
    }
}

Explanation: subtotal 100 minus discount 10 leaves 90, multiply by 10% tax gives 99. This test is a “safety net” ensuring the merge of two logics (tax and discount) don’t cancel each other out.

✓ shouldApplyDiscountThenTax()

6. Suggested workflow for small teams

  1. Always create a new branch from main for each task: git switch -c feature/xxx.
  2. Small, frequent commits with clear messages.
  3. Before creating a Pull Request, update your branch with the latest main:
    git switch feature/add-payment
    git fetch origin
    git merge origin/main
  4. Resolve conflicts (if any) on your local machine, re-run tests.
  5. Push and create a PR, letting CI run tests on the merge commit before merging into main.

Note: Don’t commit directly to main in a multi-person team — always go through a branch + PR for review and CI checks first.

7. Common mistakes

  • Forgetting git add before commit: Git reports nothing to commit, working tree clean even though you’ve edited files — because they haven’t been staged.
  • Merging the wrong branch: always run git status and git branch to check where you are before merge.
  • Leftover conflict markers in code: forgetting to remove <<<<<<< prevents compilation — always build/test immediately after resolving conflicts.
  • Force push to shared branch: git push --force after rebasing a shared branch deletes your colleague’s history. Only use --force-with-lease and only on personal branches.

8. Conclusion

Commit, branch, and merge are three operations you use daily with Git. Mastering commits keeps your code history clear, branches allow parallel work without interference, and understanding merge (including conflict resolution) gives you confidence when combining code from multiple people. Practice small commits, follow branch naming conventions, and always run tests before merging into main.

gitversion-controlbranchmergedevops