Skip to content

The Feature Branch Workflow: From Idea to Merge

This is the workflow you'll use 90% of the time. It's the standard process for adding new functionality to the codebase. Mastering this flow is the key to contributing effectively and keeping our project history clean.

Working on bugfixes, chores, or any other task for main follows the exact same process, so pay close attention: you'll be using these commands constantly.

The Golden Rule: Minimal Viable Change (MVC)

A feature branch should represent the smallest, complete change possible that is stable and testable. If a user story feels too big, break it down!

Small branches lead to faster development, easier code reviews, fewer merge conflicts, and a much lower chance of introducing bugs.

The 5 Steps of Feature Development

The entire lifecycle of a feature branch can be broken down into five clear steps.

Step 1: 🌿 Start Fresh

Before writing a single line of code, ensure you're starting from the latest version of the main branch. This is like cleaning your workbench before starting a new project: it prevents a world of pain later.

bash
# 1. Switch to the main branch
git checkout main

# 2. Get the latest updates from the remote server
git pull

Now that your local main branch is up to date, create your new feature branch from it.

:::important Branch Naming Convention: feature/<Ticket-ID>_<short-description>

  • Prefix: The feature/ prefix helps organize branches in GUIs and the command line.
  • Ticket ID: Linking the code directly to the requirement is mandatory for traceability. For this course, I'll use the "Git Gud [GG]" project as an example and a common ticket naming convention: GG-123.
  • Description: A short, 2-3 word summary makes the branch's purpose obvious at a glance. :::

What About Bug Fixes?

The process for a bug fix is exactly the same. The only differences are:

  • You'll name your branch bugfix/<Ticket-ID>_<short-description>.
  • Your commit message will start with fix:, like fix(GG-456): resolve login button issue.

That's it! The five steps remain the same.

bash
# Create your new branch and switch to it in one command
git checkout -b feature/GG-123_user-login-form

You are now in a safe, isolated branch, ready to code.

git switch

checkout and switch are interchangeable, so the following command will behave exactly the same.

bash
git switch -c feature/GG-123_user-login-form

Just take caution with the -c (create), which is the only difference.

Step 2: 💻 Do the Work & Commit Locally

This is your personal sandbox. As you work, commit your changes frequently. Don't worry about making your commits perfect at this stage; we will clean them up later.

Think of local commits as "save points" in a video game. They let you experiment freely, knowing you can always revert to a previous state if you go down the wrong path.

bash
# 1. Stage your changes for the next commit
git add .

# 2. Create a commit with a descriptive message (following Conventional Commits!)
git commit -m "feat(GG-123): add email and password fields"

# If the file you're editing is already indexed (i.e., it has already been committed before)
# You can do both commands in one go:
git commit -am "feat(GG-123): add email and password fields"

After your first commit, you can either commit your next modifications again or you can add them to your previous commit. This is called "amending" a commit:

bash
# 1. Stage your changes...
git add .

# 2. ...and add them to the last commit
git commit --amend

You'll be offered to modify the commit message. Change it or leave it. You're a grown kid.

Vim

By default, your text editor for this commit message will be displayed in your terminal, and it will be Vim. To start typing, press insert. When you're done editing your message, press escape, then type :wq, then press enter.

If this was too scary of an experience for you, you can change the editor globally:

bash
# for VSCode :
git config --global core.editor "code --wait"

Step 3: 🚀 Share Your Work & Get Feedback

Once you have a meaningful chunk of work done, push your branch to the remote server. This makes it visible to others and allows you to open a Pull Request (PR) or Merge Request (MR).

bash
# The -u flag sets the remote branch as the "upstream" 
# for your local branch the first time you push
git push -u origin feature/GG-123_user-login-form

Pushing Updates

After the first push, you no longer need the extra arguments. Git now knows your local branch is tied to the remote one.

bash
# After making new commits to address feedback...
git push

Step 4: 🔄 Clean Up & Update

While you were working, main has likely moved forward. It is your responsibility to update your branch with these new changes. This is where you will handle any "conflicts": instances where changes on main overlap with your own.

You have two main tools for this: merge and rebase. One is simple but can create a messy history, while the other keeps things clean but has a reputation for being tricky.

There is a whole page dedicated to this great debate, but the short version is: we strongly recommend a squash-then-rebase approach. It makes things easier than you think and leads to a much cleaner project.

➡️ Learn more in Merge vs. Rebase

Here are the commands for both methods. Pick your poison, but know that in the end, your branch should be squashed into a single commit on main regardless.

Rebase (The Slow, Clean Way)

1. First, squash your commits.

Let's say you made 3:

bash
# This opens the interactive rebase tool for your last 3 commits
git rebase -i HEAD~3

You will be presented with the following screen with your chosen text editor

bash
pick 4eb6289 feat(GG-2): Added walls 
pick 8617cf6 feat(GG-3): Added doors 
pick 136d167 feat(GG-4): Added locks 

# ... (help text omitted for brevity) ...

To squash them, leave the first commit as pick and change the others to s (squash) or f (fixup). fixup is great because it discards the commit message, leaving you with a cleaner history to edit. You can also choose d to remove a commit entirely. Don't worry if you don't remember what each command does; the help text is always there.

bash
pick 4eb6289 feat(GG-2): Added walls 
f 8617cf6 feat(GG-3): Added doors 
f 136d167 feat(GG-4): Added locks

If you haven't changed your editor, just a quick refresher for Vim: insert to start editing, Esc then :wq to save and quit.

Save and close the editor. If you chose at least one squash or reword, Git will then ask you to write the final commit message for the single, combined commit.

2. Rebase onto main:

Now that you have one clean commit, pull the latest changes from main using the --rebase flag.

bash
# This fetches the latest main and replays your single squashed commit on top
git pull --rebase origin main

If there are any conflicts, Git will pause and let you resolve them. Once you're done, you'll have a clean, linear history.

Since you rewrote the history of your branch, Git cannot accept your branch as it is. You'll need to force-push to make it accept the new history. Do it safely:

bash
# Use the safe force-push command (or your `git pf` alias!)
git push --force-with-lease --force-if-includes

(Don't forget the git pf alias from my Golden Rules page! 😉)

Merge (The Quick & Dirty Way)

This single command will fetch the latest state of main from the remote and merge it into your current branch right away.

bash
git pull --merge origin main
git push

Step 5: ✅ Finalize and Merge

Your feature is complete, reviewed, and up-to-date with main. It's time to land it!

The command line behind it

It should be handled by your GitLab/GitHub/Bitbucket, but if you're curious, here's how it's done on the command line:

shell
git checkout main
git merge --squash feature/GG-123_user-login-form
git commit -am "feat(GG-123): User login form"

Pre-Merge Checklist:

  • ☑️ Perfect the MR. Ensure your title follows Conventional Commits. Write the description as if it were the body of the final commit on main.
  • ☑️ Get it Reviewed. Have a peer review your code. Don't take feedback personally; apply the principles of Egoless Programming and remember: it's not your code, it's...
    Bugs Bunny communist meme stating 'our code'
  • ☑️ Configure the Merge. Verify the MR is set to Squash commits and Delete source branch. We don't want WIP messages or spaghetti merges polluting main.
  • ☑️ Check the Checks. Confirm that all CI/CD pipelines have passed and all feedback is resolved.
  • ☑️ Test. Unit tests, integration tests, E2E tests, manual QA... whatever you have on hand, and everything your team can handle before merging. Don't let a buggy mess land on main for everyone else to deal with. A good goal is: "anything merged into main should be releasable right away."

Once all boxes are checked, merge it! Your single, well-written commit will land on main.

What is the SemVer for this commit?

GitVersion will see the feat: prefix and calculate the new SemVer, for example, 1.1.0-beta.1.

Congratulations, you've successfully shipped a feature!

Going further

Dealing with complex features, working on a monorepository and you're not allowed to merge until all layers of the solution (backend, frontend, tests...) are merged? Take a look at Task Branches & Merge Trains.