DEV Community

Alex Chen
Alex Chen

Posted on

The Git Workflow That Saved My Sanity as a Solo Developer

The Git Workflow That Saved My Sanity as a Solo Developer

I used to dread git conflicts and messy commit histories. This workflow fixed everything.

The Problem

My old git history:

a1b2c3d fix
e4f5g6h fix again
i7j8k9l wip
m1n2o3p wip fix
q4r5s6t merge branch 'main' of ...
u7v8w9x conflict resolution
y0z1a2b another fix

❌ 50+ commits for a 2-day feature
❌ "fix" and "wip" everywhere
❌ No idea which commit does what
❌ Scary to push because of mess
Enter fullscreen mode Exit fullscreen mode

The Solution: My Current Workflow

Branch Naming Convention

# Format: type/ticket-description
# Types: feat/ fix/ refactor/ docs/ chore/ hotfix/

git checkout -b feat/auth-login-page
git checkout -b fix/api-timeout-error
git checkout -b refactor/user-service-cleanup
git checkout -b docs/readme-installation
git checkout -b hotfix/security-patch-v1.0.1
Enter fullscreen mode Exit fullscreen mode

Commit Message Convention (Conventional Commits)

# Format: type(scope): subject
# body (optional)
# footer (optional)

git commit -m "feat(auth): add OAuth2 Google login"
git commit -m "fix(api): handle timeout errors gracefully"
git commit -m "refactor(db): replace raw queries with ORM"
git commit -m "docs(readme): add installation instructions"
Enter fullscreen mode Exit fullscreen mode

Types I use:

Type When
feat New feature
fix Bug fix
refactor Code restructuring (no behavior change)
docs Documentation only
style Formatting, semicolons, etc. (no code change)
perf Performance improvement
test Adding or updating tests
chore Build process, dependencies, config
ci CI/CD changes

The Daily Development Loop

Start of day:
  git pull origin main          # Get latest
  git checkout -b feat/my-feature  # New branch

During work:
  # Make changes
  git add .
  git commit -m "feat(feature): description of progress"

  # Need to share with teammate?
  git push origin feat/my-feature

Ready for review:
  # Rebase on latest main first!
  git fetch origin
  git rebase origin/main         # Clean history

  # Push (force needed after rebase)
  git push --force-with-lease origin feat/my-feature

  # Create PR on GitHub/GitLab

After review + merge:
  git checkout main
  git pull origin main
  git branch -d feat/my-feature  # Clean up local branch
Enter fullscreen mode Exit fullscreen mode

The Magic of Interactive Rebase

# Squash 15 "wip" commits into 3 meaningful ones
git rebase -i HEAD~15

# Opens editor with:
pick a1b2c3d feat: initial setup
pick e4f5g6h wip
pick i7j8k9l wip
pick m1n2o3p wip
pick q4r5s6t wip
pick u7v8w9x fix
pick y0z1a2b wip

# Change to:
pick a1b2c3d feat: initial setup
squash e4f5g6h
squash i7j8k9l
squash m1n2o3p
squash q4r5s6t
fixup u7v8w9x
squash y0z1a2b

# Result: 1 clean commit instead of 7 messy ones!

# Or use the shortcut:
git reset --soft HEAD~7    # Keep all changes staged
git commit -m "feat: complete feature implementation with fixes"  # One clean commit
Enter fullscreen mode Exit fullscreen mode

Handling Common Scenarios

Scenario 1: "I committed to the wrong branch"

# Undo last commit (keep changes)
git reset HEAD~1

# Stash the changes
git stash

# Switch to correct branch
git checkout correct-branch

# Apply changes
git stash pop

git commit -m "feat: ..."
Enter fullscreen mode Exit fullscreen mode

Scenario 2: "I need to pull in changes from main mid-feature"

# Option A: Merge (creates merge commit)
git merge main

# Option B: Rebase (cleaner linear history) ← PREFERRED
git rebase main

# If there are conflicts:
# 1. Git will pause at each conflict
# 2. Open the files, look for <<<<<<< markers
# 3. Fix the conflict
# 4. git add <file>
# 5. git rebase --continue
# Repeat until done
Enter fullscreen mode Exit fullscreen mode

Scenario 3: "I want to undo this commit but keep the changes"

# Soft reset: keeps changes staged
git reset --soft HEAD~1

# Mixed reset: keeps changes unstaged (default)
git reset HEAD~1

# Hard reset: LOSES changes completely
git reset --hard HEAD~1   # ⚠️ Destructive!
Enter fullscreen mode Exit fullscreen mode

Scenario 4: "I pushed something I shouldn't have"

# If it's YOUR branch and nobody else uses it:
git push --force-with-lease origin my-branch

# If it's SHARED (main, develop, team branches):
# DO NOT force push! Instead:
git revert <commit-hash>    # Creates a new commit that undoes the change
git push origin main        # Safe push
Enter fullscreen mode Exit fullscreen mode

My .gitconfig

[user]
    name = Alex Chen
    email = alex@example.com

[init]
    defaultBranch = main

[pull]
    rebase = true              # Always rebase instead of merge on pull

[core]
    editor = code --wait       # VS Code as default editor
    autocrlf = input           # Handle line endings properly

[alias]
    co = checkout
    br = branch
    ci = commit
    st = status -sb            # Short status format
    lg = log --oneline --graph --all -20  # Pretty graph view
    unstage = reset HEAD --
    amend = commit --amend     # Edit last commit message
    save = stash push -m       # Stash with message
    pop = stash pop
    recent = branch --sort=-committerdate -10  # Recent branches

[rerere]
    enabled = true             # Remember conflict resolutions!
Enter fullscreen mode Exit fullscreen mode

rerere is a game-changer — if you resolve the same conflict twice, git remembers your resolution and auto-applies it the third time.

The Pre-Push Checklist

Before every push, I run:

#!/bin/bash
# pre-push-check.sh

echo "=== Pre-push checks ==="

# 1. Branch name follows convention
BRANCH=$(git branch --show-current)
if ! echo "$BRANCH" | grep -qE '^(feat|fix|refactor|docs|chore|hotfix)/'; then
  echo "⚠️  Branch name doesn't follow convention: $BRANCH"
fi

# 2. No "wip" or "fix" commits left
if git log --oneline origin/main..HEAD | grep -qiE '\b(wip|todo|asdf|fix)\b'; then
  echo "⚠️  Found WIP/Todo commits. Consider squashing."
fi

# 3. Tests pass
if [ -f "package.json" ] && grep -q '"test"' package.json; then
    npm test
    if [ $? -ne 0 ]; then
        echo "❌ Tests failed! Aborting push."
        exit 1
    fi
fi

# 4. No large files accidentally included
if git diff --cached --stat | grep -E '^\s+\d+\.\d+\s+[MG]B'; then
    echo "⚠️  Large file detected!"
fi

# 5. No secrets
if git diff --cached --name-only -z | xargs -0 grep -l -E '(password|secret|key|token)\s*[:=]' 2>/dev/null; then
    echo "❌ Possible secret detected! Aborting push."
    exit 1
fi

echo "✅ All checks passed!"
Enter fullscreen mode Exit fullscreen mode

Hook it up: .git/hooks/pre-push → make executable.

What Changed For Me

Before this workflow:
- Dreaded git operations
- Messy history that scared me to push
- Spent hours resolving confusing conflicts
- Couldn't find anything in history

After this workflow:
- Git is a tool, not an obstacle
- Clean history tells a story
- Conflicts are rare and easy when they happen
- Can find any change instantly
- Confidence in every push
Enter fullscreen mode Exit fullscreen mode

What's your git workflow? Any tips I'm missing?

Follow @armorbreak for more developer content.

Top comments (0)