DEV Community

Cover image for From 'src refspec' Errors to Clean PRs: The Practical Git Branching Workflow You Need
Alan Varghese
Alan Varghese

Posted on

From 'src refspec' Errors to Clean PRs: The Practical Git Branching Workflow You Need

We’ve all been there: pushing straight to main, writing commit messages like "fix stuff", and dreading the moment someone mentions merge conflicts or branch topology.

When you're building solo, you can get away with git add . and git push origin main. But the moment you collaborate with a team, work in an open-source project, or maintain production software, messy Git habits quickly turn into broken builds and lost code.

In this post, I’ll walk you through a practical, lightweight Git branching workflow modeled on real-world team practices (often called Git Flow Light). We'll set up a project, create feature branches, use the GitHub CLI (gh) for Pull Requests, intentionally create and resolve a merge conflict, and cover the most common Git errors you will face along the way.


🧠 1. The Mental Model: What's Actually Happening in Git?

Before typing commands into the terminal, let’s demystify two core Git mechanics that make everything click:

The Three-Tree Architecture

Git doesn’t just copy your files to the cloud; it manages changes across three distinct zones:

[ Working Directory ]  --->  `git add`  --->  [ Staging Area (Index) ]  --->  `git commit`  --->  [ Repository History ]
 (Live files on disk)                           (The box you're packing)                             (Committed snapshots)
Enter fullscreen mode Exit fullscreen mode
  • Working Directory: The actual physical files you are editing in VS Code.
  • Staging Area (Index): The draft snapshot. It holds a copy of your staged changes, not a live reference.
  • Repository (.git): The permanent history of committed snapshots.

Branches Are Just Cheap Pointers

In Git, creating a branch does not duplicate your entire codebase or take up disk space. A branch is merely a 40-character text file containing the commit SHA it points to.

  • When you create a branch, Git creates a new pointer.
  • HEAD simply indicates which pointer your working directory is currently tracking.
(develop) ─── [ C1 ] ─── [ C2 ] ─── [ C3 ] (HEAD -> feature/todo-ui)
Enter fullscreen mode Exit fullscreen mode

🌳 2. The Branching Strategy (Git Flow Light)

For most teams and personal projects, a 3-tier structure provides the perfect balance between stability and agility without the over-engineering of full Git Flow:

Branch Purpose Rule
main Production-ready releases Only receives tested code from develop. Tagged with version numbers (e.g., v1.0.0).
develop Integration branch Where all finished features land and are tested together.
feature/* Isolated task or component Forked from develop, merged back via Pull Request, and deleted after merge.

🚀 3. Step-by-Step Workflow in Action

Let’s trace a real project scenario: setting up a clean repository and building a small Todo App.

Step 1: Initialize and Configure

# Initialize git in your project directory
git init

# Configure identity (if not already set globally)
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"
Enter fullscreen mode Exit fullscreen mode

Step 2: Establish main and develop

Always make sure you have an initial commit before trying to manage branches:

# Create the initial files
touch index.html style.css app.js

# Stage and commit baseline
git add .
git commit -m "feat: initial project structure"

# Ensure main is the active branch
git branch -M main

# Connect to GitHub remote
git remote add origin https://github.com/your-username/todo-app.git

# Push main and set upstream
git push -u origin main

# Create and switch to develop branch
git checkout -b develop
git push -u origin develop
Enter fullscreen mode Exit fullscreen mode

Step 3: Work on an Isolated Feature Branch

Never write code directly on develop or main. Whenever you start a new task, fork a feature branch from develop:

# Ensure you are on latest develop
git checkout develop
git pull origin develop

# Cut a new feature branch
git checkout -b feature/add-task-ui
Enter fullscreen mode Exit fullscreen mode

Make your code updates in index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>To-Do List</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="app">
        <h1>My To-Do List</h1>
        <input type="text" id="new-task" placeholder="Add a new task...">
        <button id="add-btn">Add</button>
        <ul id="task-list"></ul>
    </div>
    <script src="app.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Stage and commit with a descriptive message:

# Check what changed
git status

# Stage & commit
git add index.html
git commit -m "feat(ui): add basic task input structure and markup"

# Push feature branch to remote
git push -u origin feature/add-task-ui
Enter fullscreen mode Exit fullscreen mode

📬 4. Pull Requests as Quality Gates (Using GitHub CLI)

Instead of manually clicking through the GitHub web UI, you can streamline the review process using the GitHub CLI (gh):

Creating the PR to develop:

gh pr create --base develop --head feature/add-task-ui --title "feat: Add Task UI" --body "Implements initial HTML structure for task input and listing."
Enter fullscreen mode Exit fullscreen mode

Merging and Cleaning Up:

Once the PR passes checks (or code review):

# Merge with a merge commit to preserve branch topology and delete the remote branch
gh pr merge --merge --delete-branch

# Switch back locally and sync
git checkout develop
git pull origin develop

# Clean up the local feature branch
git branch -d feature/add-task-ui
Enter fullscreen mode Exit fullscreen mode

💥 5. Merge Conflicts: Demystified & Conquered

Merge conflicts occur when Git cannot automatically reconcile two different changes made to the same lines of a file across different branches.

The Conflict Setup Scenario

Imagine two developers (or two branches) modify the heading in index.html at the same time:

  • Branch develop has:
  <h1>My Daily To-Do List</h1>
Enter fullscreen mode Exit fullscreen mode
  • Branch feature/update-ui has:
  <h1>My To-Do Tasks</h1>
Enter fullscreen mode Exit fullscreen mode

When you attempt to merge feature/update-ui into develop:

git checkout develop
git merge feature/update-ui
Enter fullscreen mode Exit fullscreen mode

Git halts the process and warns:

CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
Enter fullscreen mode Exit fullscreen mode

Anatomy of Conflict Markers

Open index.html, and you will see Git's conflict markers:

<<<<<<< HEAD (Current change on develop)
        <h1>My Daily To-Do List</h1>
=======
        <h1>My To-Do Tasks</h1>
>>>>>>> feature/update-ui (Incoming change)
Enter fullscreen mode Exit fullscreen mode

How to Resolve It:

  1. Decide on the correct code (e.g., combine or pick one version).
  2. Delete the marker lines (<<<<<<<, =======, >>>>>>>).
  3. Save the file:
   <h1>My Daily To-Do Tasks</h1>
Enter fullscreen mode Exit fullscreen mode
  1. Stage and commit the resolution:
   git add index.html
   git commit -m "fix(merge): resolve heading text conflict between develop and feature/update-ui"
Enter fullscreen mode Exit fullscreen mode

🏷️ 6. Releasing to Production (main) & Tagging

Once develop contains a stable set of merged features and has been tested, merge it into main and tag the release:

# Switch to main and merge develop
git checkout main
git pull origin main
git merge develop --no-ff -m "chore(release): merge develop into main for v1.0.0"

# Tag the milestone
git tag -a v1.0.0 -m "Release version 1.0.0 - Initial working Todo App"

# Push main and tags to remote
git push origin main
git push origin v1.0.0
Enter fullscreen mode Exit fullscreen mode

⚠️ 7. The Wall of Common Git Errors (& Exact Fixes)

Save this cheat sheet for when terminal errors strike:

Error Message Why It Happened How to Fix It
fatal: not a git repository You ran a git command outside the project folder. cd into your actual repository root directory.
src refspec main does not match any You tried to push main before creating at least one commit. Create an initial commit: git commit --allow-empty -m "init" then push.
base ref must be a branch / base sha can't be blank Opening a PR to --base develop when develop hasn't been pushed to remote. Push the base branch first: git push -u origin develop.
no commits between dev and feat The feature branch has no unique commits ahead of develop. Make changes and commit them on the feature branch first.
CONFLICT (content): Merge conflict in <file> Concurrent edits to the exact same lines across branches. Open the file, remove <<<<<<</=======/>>>>>>>, pick the final code, git add ., and git commit.
default branch is invalid Tried to change default branch to main before main exists remotely. Push main to remote first: git push -u origin main, then update repo settings.
git diff shows nothing after editing You forgot to save the file in your code editor. Hit Ctrl+S / Cmd+S in your editor, then re-run git diff.

💡 Pro Tips for a Smoother Git Life

  1. git status is your reflex: Run it before git add and before git commit. Always know what is staged versus unstaged.
  2. Use visual history logs: Run git log --oneline --graph --all to see a ASCII branch tree of your entire repository history.
  3. Keep branches short-lived: Large, long-living feature branches are conflict magnets. Break work into small, bite-sized PRs.
  4. Prune stale remote references: Run git fetch --prune periodically to remove local references to branches that were deleted on GitHub.
  5. Write semantic commit messages: feat:, fix:, docs:, refactor:. Your future self and teammates will thank you.

💬 Over to You!

What is your team's favorite Git workflow? Have you ever had a legendary merge conflict disaster? Drop your thoughts or questions in the comments below! 👇

Top comments (0)