The Git Workflow That Actually Works for Solo Developers

March 2026 · 16 min read · 3,777 words · Last Updated: March 31, 2026Advanced
# The Git Workflow That Actually Works for Solo Developers I still remember the exact moment I broke. It was 2 AM on a Tuesday, and I was staring at a merge conflict between my `feature/redesign` branch and `main`. The kicker? I was the only developer on the project. I was literally in a merge conflict with myself. Three days of work—gone. Not because of a catastrophic hard drive failure or a deleted repository. No, I lost those three days because I had religiously followed a Git workflow designed for teams of 10+ developers. I had feature branches, hotfix branches, release branches, and a `develop` branch that existed solely to make me feel professional. The irony wasn't lost on me: I had spent more time managing Git than actually writing code. That night, sitting in my home office with cold coffee and a bruised ego, I made a decision. I was going to strip away every piece of Git ceremony that didn't serve me as a solo developer. No more cargo-culting team workflows. No more pretending I needed the same infrastructure as a 50-person engineering team. What emerged from that frustration is the workflow I've used to ship over 40 solo projects since then. It's not sexy. It won't impress anyone at a tech conference. But it works, and more importantly, it gets out of my way so I can actually build things.

Why Most Git Workflows Fail Solo Developers

The problem with most Git tutorials and workflows is that they're written by people working at companies with multiple teams, code review processes, and deployment pipelines that involve at least three different environments. When you're a solo developer, you don't have those constraints—but you also don't have those safety nets. GitFlow, the workflow that dominated the 2010s, is a perfect example. It was designed by Vincent Driessen for a specific problem: managing releases for software that needed to support multiple versions simultaneously. If you're building a desktop application that customers install locally, GitFlow makes sense. If you're a solo developer shipping a SaaS product or a client website, it's complete overkill. The typical solo developer's journey with Git goes something like this: You start with just committing to `main` (or `master`, depending on when you learned Git). Then you read an article about "professional Git workflows" and feel guilty. You implement feature branches. Then you add a `develop` branch because that's what the diagram showed. Before long, you're spending 20 minutes a day just managing branches, and you're not even sure why half of them exist. I've been there. I've had repositories with branches named `feature/new-feature`, `feature/new-feature-2`, `feature/new-feature-actually-final`, and `feature/new-feature-for-real-this-time`. If you've never had a branch naming crisis, you're either lying or you haven't been solo developing long enough. The fundamental issue is that team workflows are designed to solve team problems: coordinating work between multiple people, preventing conflicts, managing code review processes, and maintaining stability in shared environments. When you're working solo, most of these problems simply don't exist. You can't have a merge conflict with someone else if there is no one else.

The Three-Commit Rule That Changed Everything

After my 2 AM disaster, I started analyzing my actual work patterns. I pulled up my Git history for the previous six months and looked at every branch I'd created, every merge I'd done, and every conflict I'd resolved. What I found was illuminating. Ninety percent of my feature branches contained three commits or fewer before merging. These weren't complex, multi-week features that needed isolation. They were small improvements, bug fixes, and incremental changes that I'd artificially separated into branches because I thought that's what "real developers" did. The remaining ten percent—the actual complex features—were where branches made sense. But even then, I noticed something: the branches that caused problems were the ones I'd left open for more than a week. The longer a branch lived, the more likely it was to cause issues when merging back. This led me to what I call the Three-Commit Rule: If a change takes more than three commits, it's probably too big and should be broken down. If it can't be broken down, it's one of the rare cases where a branch actually helps. This rule forced me to think differently about how I work. Instead of creating a branch for "redesign the entire UI," I'd create a branch for "update button styles" or "implement new navigation component." Each branch lived for a day or two maximum, contained focused changes, and merged cleanly. The psychological shift was significant. I stopped thinking in terms of "features" and started thinking in terms of "deployable increments." Every branch had to represent something I could ship to production without breaking existing functionality. This naturally kept branches small and short-lived.

The Day I Shipped a Bug to Production (And Why It Made My Workflow Better)

Let me tell you about the worst deployment of my career. I was working on a client project—a booking system for a small hotel chain. I'd been working on a feature branch for two weeks (yes, I know, I broke my own rule) that added a new payment integration. The branch had diverged significantly from `main`. I'd been so focused on the payment feature that I hadn't been keeping up with the small fixes and improvements I'd been making directly to `main` for urgent client requests. When it came time to merge, I had conflicts in 14 files. I resolved the conflicts, ran the tests (they passed), and deployed to production. Within an hour, I got a panicked call from the client. The booking form was broken. Not the new payment integration—the basic booking form that had been working fine for months. What happened? In resolving one of the merge conflicts, I'd accidentally kept the wrong version of a function. The tests didn't catch it because I hadn't written tests for that particular edge case (lesson learned). The bug was entirely my fault, but the workflow had made it easy to make that mistake. That incident taught me something crucial: as a solo developer, my biggest risk isn't other people's code—it's my own code from two weeks ago. When I'm context-switching between different parts of a project, I'm essentially collaborating with past versions of myself. And past me is often an unreliable colleague. This realization led to the second pillar of my workflow: the One-Week Rule. No branch lives longer than one week. If a feature is going to take longer than that, I break it into smaller pieces that can each be completed and merged within a week. If that's not possible, I use feature flags to merge incomplete code to `main` while keeping it hidden from users. The One-Week Rule has saved me countless times. It forces me to stay close to `main`, which means I'm always working with the most current version of the codebase. It prevents the kind of divergence that leads to complex merge conflicts. And it keeps me honest about scope—if I can't finish something in a week, I'm probably trying to do too much at once.

The Real Cost of Branch Complexity

Let's talk numbers. I tracked my Git-related activities for three months before and after simplifying my workflow. The results were stark:
Activity Before (hours/week) After (hours/week) Time Saved
Creating and managing branches 2.5 0.5 2 hours
Resolving merge conflicts 3.0 0.3 2.7 hours
Deciding which branch to work on 1.5 0.1 1.4 hours
Cleaning up stale branches 1.0 0.1 0.9 hours
Rebasing and syncing branches 2.0 0.2 1.8 hours
Total Git overhead 10.0 1.2 8.8 hours
Nearly nine hours a week. That's more than a full workday that I was spending on Git ceremony instead of writing code. Over a year, that's 450+ hours—more than 11 full work weeks. But the time savings weren't even the most significant benefit. What really changed was my mental overhead. Before, I'd often find myself staring at my terminal, trying to remember which branch had which changes, whether I'd already merged something, or if I needed to rebase before merging. These micro-decisions added up to significant cognitive load. After simplifying my workflow, those decisions disappeared. I always knew where my work was because there were only ever one or two branches at most. I never had to think about rebasing strategies or merge strategies because the branches were so short-lived that conflicts were rare. The data also revealed something I hadn't expected: my commit frequency increased by 40%. With less friction in my workflow, I was committing more often, which meant smaller, more focused commits. This made it easier to understand my history and, when necessary, revert specific changes without undoing a bunch of unrelated work.

Why "Commit Early, Commit Often" Is Wrong for Solo Developers

Here's a controversial take: the advice to "commit early, commit often" is actually harmful for solo developers. I know this goes against conventional wisdom, but hear me out.
The purpose of commits isn't to create a detailed log of every keystroke. It's to create meaningful checkpoints that tell a story about how your code evolved. When you're working solo, you're the only person who needs to understand that story.
I used to commit constantly. Every time I got something working, even partially, I'd commit it. My history was full of commits like "fix typo," "actually fix typo," "remove console.log," and "WIP." This felt productive—I was following best practices, right? Wrong. What I ended up with was a commit history that was essentially useless. When I needed to understand why I'd made a particular change, I'd have to dig through dozens of tiny commits to piece together the actual story. When I wanted to revert a feature, I'd have to revert 15 commits instead of one. The turning point came when I was working on a project with a client who wanted to review my work weekly. I'd send them a link to the GitHub repository, and they'd look at the commit history to see what I'd been working on. After the first week, they asked me if I could "make the commits more understandable" because they couldn't follow what I'd done. That's when I realized: commits should represent complete thoughts, not individual keystrokes. Each commit should be something I could explain to someone (or to future me) in a single sentence. "Add user authentication" is a good commit. "Fix bug in authentication" is a good commit. "Update auth.js" is not a good commit. This led me to embrace interactive rebasing and commit squashing in a way I never had before. Now, my typical workflow looks like this: I work on a feature, committing frequently to save my progress (these are essentially save points, not real commits). When I'm done, I use `git rebase -i` to squash all those work-in-progress commits into one or two meaningful commits that tell the story of what I built.
Your commit history should read like a changelog, not a diary. Each entry should be something you'd want to tell someone about, not a record of every time you hit save.
This approach has another benefit: it makes it much easier to use Git as a debugging tool. When something breaks, I can use `git bisect` to quickly find the commit that introduced the bug. With meaningful commits, this process takes minutes. With dozens of tiny commits, it's nearly impossible.

The Myth of the Perfect Linear History

One of the most persistent myths in Git culture is that you should maintain a perfectly linear history. No merge commits, only rebases. A clean, straight line of commits that looks beautiful in your Git log. I bought into this myth hard. I spent hours rebasing branches, resolving conflicts multiple times as I rebased commit by commit, all in service of maintaining that perfect linear history. I felt like a Git master, wielding the power of rebase with confidence. Then I realized something: I was the only person who ever looked at my Git history. And even I rarely looked at it beyond the last few commits. I was spending hours maintaining something that provided zero value. Here's the truth that took me years to accept: merge commits are fine. They're actually good. They clearly show when a feature was integrated, and they make it easy to revert an entire feature if needed. The `--no-ff` flag (no fast-forward) that creates a merge commit even when a fast-forward is possible isn't a sign of amateur hour—it's a useful tool for maintaining context. Now, my workflow is simple: I use merge commits for anything that was developed on a branch, and I let Git fast-forward for simple, single-commit changes. My history isn't perfectly linear, but it's readable and useful. And I save hours every week by not rebasing.
A messy history that accurately reflects how the code evolved is infinitely more valuable than a pristine history that's been rewritten so many times it bears no resemblance to reality.
This doesn't mean I never rebase. I still use interactive rebase to clean up my commits before merging. But I don't rebase feature branches onto `main` anymore. I just merge them. The merge commit serves as a clear marker: "This is when feature X was integrated." The exception is when I'm working on open source projects or contributing to other people's repositories. In those cases, I follow whatever workflow the project uses. But for my own solo projects? Merge commits all the way.

The Actual Workflow: Simple, Boring, Effective

After all that context, here's the actual workflow I use. It's going to seem almost disappointingly simple, but that's the point. 1. I work directly on `main` for small changes. If it's a bug fix, a typo correction, or a small improvement that I can complete in one sitting, I just commit directly to `main`. No branch, no ceremony. Commit, push, done. 2. I create a branch for anything that takes more than an hour. If I'm building something that requires multiple commits or that I might not finish in one session, I create a branch. The branch name is simple and descriptive: `add-dark-mode`, `fix-payment-bug`, `update-dependencies`. No prefixes like `feature/` or `bugfix/`—those are just noise. 3. I commit frequently while working, but I clean up before merging. While I'm working on a branch, I commit whenever I reach a stable point. These commits are messy and informal—they're just save points. Before I merge, I use `git rebase -i main` to squash and clean up these commits into one or two meaningful commits. 4. I merge with a merge commit. When the branch is ready, I merge it back to `main` with `git merge --no-ff branch-name`. This creates a merge commit that clearly shows when the feature was integrated. Then I delete the branch immediately. 5. I deploy from `main`. Always. There's no separate `production` branch or `release` branch. `main` is always deployable. If it's not deployable, I don't merge to it. 6. I use tags for releases. When I deploy a significant update, I tag the commit with a version number. This makes it easy to see what was deployed when and to roll back if needed. 7. I never let a branch live longer than a week. If I can't finish something in a week, I break it down further or use feature flags to merge incomplete work to `main` while keeping it hidden from users. That's it. Seven rules. No complex branching strategies, no release branches, no hotfix branches, no `develop` branch. Just `main` and short-lived feature branches.

When This Workflow Breaks Down (And What to Do About It)

I'd be lying if I said this workflow is perfect for every situation. There are times when it breaks down, and it's important to know when to adapt. Multiple environments with different code: If you're maintaining different versions of your code for different clients or environments, you might need long-lived branches. In this case, I use branches named after the environment (`client-a`, `client-b`) and cherry-pick commits between them as needed. It's not ideal, but it's better than trying to maintain multiple repositories. Breaking changes that need testing: Sometimes you need to make a breaking change that requires extensive testing before it can go to production. In these cases, I'll keep a branch open longer than a week, but I'm very careful to regularly merge `main` into the branch to prevent divergence. I also try to avoid this situation by using feature flags whenever possible. Open source contributions: When contributing to other people's projects, I follow their workflow, whatever it is. This workflow is for my own solo projects, not for collaborative work. Client review requirements: Some clients want to review work before it goes to production. In these cases, I'll create a `staging` branch that I deploy to a staging environment. But I still keep my feature branches short-lived—they merge to `staging`, not to `main`. Once the client approves, I merge `staging` to `main`. The key is to recognize that these are exceptions, not the rule. For 90% of my work, the simple workflow is sufficient. For the other 10%, I adapt as needed, but I always try to keep things as simple as possible.

The Tools That Make This Workflow Possible

A simple workflow doesn't mean doing everything manually. I rely on a few tools and practices that make this workflow smooth and efficient. Git aliases: I have a handful of Git aliases that eliminate the friction of common operations. More on these in the final section. Pre-commit hooks: I use pre-commit hooks to run linters and formatters automatically. This ensures that every commit meets basic quality standards without me having to think about it. I use Husky for JavaScript projects and pre-commit for Python projects. Automated testing: I run tests before every merge. This is non-negotiable. Even for solo projects, automated tests are the safety net that makes it safe to move fast. I use GitHub Actions for this, but any CI/CD tool works. Feature flags: For larger features that can't be completed in a week, I use feature flags to merge incomplete code to `main` while keeping it hidden from users. I use a simple environment variable approach for most projects—nothing fancy, just a boolean flag that I can toggle. Conventional commits: I follow the Conventional Commits specification for my commit messages. This makes it easy to generate changelogs and understand what each commit does at a glance. A commit message like `feat: add dark mode toggle` or `fix: resolve payment processing error` is immediately understandable. Branch protection: Even though I'm the only developer, I use branch protection on `main` in GitHub. This forces me to create a pull request for any branch, which gives me a chance to review my own code before merging. It sounds silly, but reviewing my own PR has caught bugs more times than I can count.

The Exact Git Aliases I Use Every Day

Here are the Git aliases that make this workflow fast and frictionless. I've refined these over years of solo development, and they've saved me countless hours. Add these to your `~/.gitconfig` file under the `[alias]` section: ``` [alias] # Quick status s = status -sb # Add all and commit with message ac = !git add -A && git commit -m # Amend the last commit without editing the message amend = commit --amend --no-edit # Create a new branch and switch to it nb = checkout -b # Switch to main and pull latest mm = !git checkout main && git pull # Delete a branch locally and remotely nuke = !git branch -D $1 && git push origin --delete $1 # Interactive rebase on main tidy = rebase -i main # Merge with no fast-forward (creates merge commit) merge-branch = merge --no-ff # Show the last 10 commits in a compact format recent = log -10 --oneline --decorate # Undo the last commit but keep the changes undo = reset HEAD~1 --soft # Stash with a message save = stash push -m # List all stashes stashes = stash list # Create a work-in-progress commit wip = !git add -A && git commit -m "WIP: work in progress" # Push current branch to origin push-branch = !git push -u origin $(git branch --show-current) ``` Here's how I use these in my daily workflow: Starting a new feature: ```bash git mm # Switch to main and pull latest git nb add-feature-name # Create and switch to new branch ``` Working on the feature: ```bash git wip # Quick save point # ... more work ... git ac "Implement feature X" # Add all and commit with message ``` Cleaning up before merge: ```bash git tidy # Interactive rebase to clean up commits # Squash WIP commits, reword messages, etc. ``` Merging the feature: ```bash git mm # Switch to main and pull latest git merge-branch add-feature-name # Merge with merge commit git push git nuke add-feature-name # Delete branch locally and remotely ``` Quick fixes: ```bash git mm git ac "Fix typo in documentation" git push ``` The `wip` alias deserves special mention. It's my most-used alias by far. Whenever I need to switch contexts, take a break, or just want to save my progress, I run `git wip`. It creates a commit with all my changes and a clear "WIP" message. Later, when I'm ready to merge, I use `git tidy` to squash all those WIP commits into proper commits. The `amend` alias is also incredibly useful. If I commit something and immediately realize I forgot to include a file or made a typo, I can fix it and run `git amend` to add it to the last commit without changing the commit message. These aliases might seem simple, but they eliminate the friction that makes Git feel like a chore. Instead of typing out long commands or looking up syntax, I can execute common operations with just a few keystrokes. This keeps me in flow and focused on writing code, not managing Git. --- This workflow isn't revolutionary. It's not going to win any awards for sophistication. But it works. It's gotten me through 40+ solo projects without a single catastrophic Git disaster (after that initial one, anyway). It keeps Git in the background where it belongs, letting me focus on building things instead of managing branches. The best workflow is the one you'll actually use consistently. For me, that means keeping things simple, avoiding ceremony, and optimizing for the reality of solo development rather than cargo-culting team practices. Your mileage may vary, but if you're drowning in branches and merge conflicts despite being the only developer on your project, maybe it's time to simplify.

Disclaimer: This article is for informational purposes only. While we strive for accuracy, technology evolves rapidly. Always verify critical information from official sources. Some links may be affiliate links.

C

Written by the Cod-AI Team

Our editorial team specializes in software development and programming. We research, test, and write in-depth guides to help you work smarter with the right tools.

Share This Article

Twitter LinkedIn Reddit HN

Related Tools

CSS Minifier - Compress CSS Code Free Knowledge Base — cod-ai.com How to Encode Base64 — Free Guide

Related Articles

What is an API? The Complete Beginner's Guide with Examples - COD-AI.com Base64 Image Converter: Encode & Decode — cod-ai.com Git Workflow for Teams: Branching Strategies That Work — cod-ai.com

Put this into practice

Try Our Free Tools →