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 commit directly to main and regret it every time. Here's the system that fixed everything.

The Problem With "Just Commit to Main"

We've all done it:

# Make a quick fix
git add -A
git commit -m "fixed bug"
git push origin main

# ... 2 hours later ...
# Oh no, that broke something else
# And now production is broken
# And I can't remember what I changed
Enter fullscreen mode Exit fullscreen mode

When you're a solo developer, there's no code review partner to catch your mistakes. You need your own process to be your safety net.

My Current Workflow

Here's the exact system I use for every project:

Branch Strategy

main (protected)
  ├── feature/xxx (new features)
  ├── fix/xxx (bug fixes)  
  └── refactor/xxx (code cleanup)

release/v*.*.* (for versioned releases, optional)
Enter fullscreen mode Exit fullscreen mode

Rule: Never push directly to main. Ever.

The Complete Flow

# 1. Start work
git checkout -b feature/user-authentication

# 2. Work normally (commit often, messy commits are OK)
git add -A
git commit -m "wip: auth controller skeleton"
git add -A
git commit -m "wip: login endpoint"
git add -A
git commit -m "wip: jwt token generation"

# 3. Before merging: clean up history
git rebase -i main
# Pick, squash, reword into clean commits:
#   "feat: add user authentication with JWT"
#   "fix: handle expired tokens gracefully"

# 4. Run checks (automated)
npm test
npm run lint

# 5. Merge to main
git checkout main
git merge --no-ff feature/user-authentication
git push origin main

# 6. Clean up
git branch -d feature/user-authentication
Enter fullscreen mode Exit fullscreen mode

Why --no-ff?

# --no-ff (no fast-forward) creates a merge commit
# This preserves branch history in git log:

git log --oneline --graph
*   a1b2c3d (HEAD -> main) Merge pull request #42 from feature/user-auth
|\
| * e4f5g6h feat: add user authentication with JWT
| * f6g7h8i fix: handle expired tokens gracefully
|/
* j8k9l0m Previous main commit

# vs fast-forward (loses context):
* e4f5g6h (HEAD -> main) feat: add user authentication with JWT
* f6g7h8i fix: handle expired tokens gracefully
* j8k9l0m Previous main commit
Enter fullscreen mode Exit fullscreen mode

The merge commit tells you why these changes were made. Six months later, you'll thank yourself.

Pre-Push Hook (My Safety Net)

This is the single most valuable thing in my setup:

#!/bin/sh
# .git/hooks/pre-push
# Runs before every push. Blocks push if any check fails.

echo "🔍 Running pre-push checks..."

# 1. Don't allow pushing to main directly
PROTECTED_BRANCHES="main master develop"
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)

for BRANCH in $PROTECTED_BRANCHES; do
  if [ "$CURRENT_BRANCH" = "$BRANCH" ]; then
    echo "❌ ERROR: Direct pushes to '$BRANCH' are not allowed!"
    echo "   Create a feature branch and merge instead."
    exit 1
  fi
done

# 2. Check for secrets (basic pattern match)
if git diff --cached --name-only | grep -qE '\.env$|credentials|secret'; then
  echo "⚠️  WARNING: Possible secret file detected!"
  read -p "   Continue anyway? (y/N) " confirm
  [ "$confirm" != "y" ] && exit 1
fi

# 3. Run tests if available
if [ -f "package.json" ] && grep -q '"test"' package.json; then
  echo "🧪 Running tests..."
  npm test
  if [ $? -ne 0 ]; then
    echo "❌ Tests failed. Push aborted."
    exit 1
  fi
fi

# 4. Run linter if available
if [ -f "package.json" ] && grep -q '"lint"' package.json; then
  echo "✨ Running linter..."
  npm run lint
  if [ $? -ne 0 ]; then
    echo "❌ Lint errors found. Fix them or --no-verify (not recommended)."
    exit 1
  fi
fi

echo "✅ All checks passed. Pushing..."
exit 0
Enter fullscreen mode Exit fullscreen mode

Make it executable: chmod +x .git/hooks/pre-push

This has saved me from pushing broken code more times than I can count.

Pre-Commit Hook (Automatic Cleanup)

#!/bin/sh
# .git/hooks/pre-commit
# Runs before every commit.

# Auto-fix formatting issues
if command -v npx &> /dev/null && [ -f "package.json" ]; then
  # Prettier (if installed)
  if npx prettier --version &> /dev/null; then
    npx prettier --write . 2>/dev/null
    git update-index -a
  fi

  # ESLint auto-fix (if configured)
  if npx eslint --version &> /dev/null; then
    npx eslint --fix . 2>/dev/null
    git update-index -a
  fi
fi

# Don't commit files larger than 1MB
MAX_SIZE=$((1024 * 1024))
FILES=$(git diff --cached --name-only | while read file; do
  SIZE=$(stat -c%s "$file" 2>/dev/null || echo 0)
  if [ "$SIZE" -gt "$MAX_SIZE" ]; then
    echo "$file ($((SIZE / 1024))KB)"
  fi
done)

if [ -n "$FILES" ]; then
  echo "⚠️  Large files detected:"
  echo "$FILES"
  echo "Consider using Git LFS for binary files."
fi

exit 0
Enter fullscreen mode Exit fullscreen mode

Useful Git Aliases

# Add these to ~/.gitconfig

[alias]
  # Quick status
  s = status -sb

  # Nice log view
  lg = log --oneline --graph --decorate

  # Better diff (with stats)
  d = diff --stat -p

  # Undo last commit (keep changes staged)
  undo = reset --soft HEAD~1

  # Delete all merged branches
  clean-branches = "!git branch --merged | grep -v '\\*\\|main\\|master\\|develop' | xargs git branch -d"

  # Show files changed in a branch
  branch-changes = "!git log --oneline ..main --name-only | sort | uniq"

  # Find a string in all commits
  find-text = "!git rev-list --all | xargs git grep -F"

  # Quick amend (for fixing last commit)
  amend = commit --amend --no-edit

  # Save uncommitted work temporarily
  save = stash push -u -m 'WIP'
  pop = stash pop
Enter fullscreen mode Exit fullscreen mode

Handling "Oh No" Moments

"I pushed something broken to main"

# Option 1: Revert (safe, preserves history)
git revert HEAD
git push origin main

# Option 2: If you MUST rewrite history (dangerous with shared repos)
git reset --hard HEAD~1
git push --force-with-lease origin main
# Note: --force-with-lease is safer than --force
Enter fullscreen mode Exit fullscreen mode

"I committed to the wrong branch"

# Move the last N commits to a new branch
git branch new-branch       # Create branch from current state
git reset --hard HEAD~N     # Move main back N commits
git checkout new-branch     # Switch to branch with your work
Enter fullscreen mode Exit fullscreen mode

"I need to get one file from another branch"

# Get config.js from staging branch
git checkout staging -- path/to/config.js
git commit -m "chore: sync config from staging"
Enter fullscreen mode Exit fullscreen mode

"I want to see what changed between two points"

# Since last release tag
git log v1.0.0..HEAD --oneline

# Stats since last week
git diff --stat "@{1 week ago}" HEAD

# Who changed this file?
git log --follow -p -- src/server.js
Enter fullscreen mode Exit fullscreen mode

Release Workflow

For projects with versions:

#!/bin/bash
# release.sh — creates a new versioned release

# Bump version in package.json
VERSION=$(node -p "require('./package.json').version")
read -p "Current version: $VERSION. New version: " NEW_VERSION

npm version $NEW_VERSION --no-git-tag-version
git add package.json package-lock.json
git commit -m "chore: bump version to $NEW_VERSION"

# Create annotated tag
git tag -a v$NEW_VERSION -m "Release v$NEW_VERSION"

# Push tag (triggers CI/CD deployment)
git push origin main --tags

echo "✅ Released v$NEW_VERSION"
Enter fullscreen mode Exit fullscreen mode

What This Gives Me

Before After
Broken main every other day Clean main, always deployable
"What did I change?" confusion Clear git log tells the story
Messy commit messages Consistent conventional commits
Secrets accidentally committed Pre-push hook blocks it
Spent hours finding bugs Tests catch them before push

The Minimum Viable Setup

If you take nothing else from this article, do these three things:

  1. Protect main branch — use pre-push hook or GitHub branch rules
  2. Commit on branches — never work directly on main
  3. Clean up before merge — rebase -i to make history readable

Everything else is optimization. These three habits alone will prevent 90% of "oh crap" moments.


Git is a tool. Like any tool, it's only as good as your workflow.

Follow @armorbreak for more developer productivity tips.

Top comments (0)