Skip to main content
Signocore
WordPress Web Development Productivity 9 min read Daniel Nielsen

Git for Solo Developers: Beyond the Basics

Stop losing power with lazy commits. Master git stash, interactive rebase, bisect, and conventional commits to work smarter on WordPress projects.

Git for Solo Developers: Beyond the Basics

Most solo developers use Git as a time machine with one button. Write code, run git add . && git commit -m "update", push, repeat. The repo has history, technically - but it's the kind of history that tells you nothing when something breaks at 11pm and you need to know exactly what changed three days ago.

Git has a depth that most tutorials never reach, and for solo WordPress plugin developers in particular, the gap between "knows Git" and "uses Git well" is where hours of debugging time get lost. This guide covers the features that close that gap - not comprehensively, but practically.

Why "git add . && git commit -m 'update'" is leaving power on the table

The problem with mass-adding everything and writing vague commit messages isn't laziness - it's that it collapses context. A commit should answer one question: what changed, and why? When you lump three unrelated fixes into one commit called "update", you've made that question unanswerable.

This matters most when you're using git log to understand why a line of code exists, when you're reverting a specific change without touching others, or when you're using git bisect to hunt a regression. All of those workflows depend on commits that represent single, coherent units of intent.

Start with staged hunks instead of staging everything:

git add -p

This drops you into an interactive prompt where you can stage individual chunks of a file. A single file can contain changes that belong to two different commits - git add -p lets you split them cleanly. It takes an extra minute. It saves hours later.

Meaningful commit messages: the Conventional Commits format

Conventional Commits is a lightweight specification for structuring commit messages. The format looks like this:

type(scope): short description

Optional longer body explaining the why, not the what.

Common types include feat (new feature), fix (bug fix), refactor (code change with no behavior change), docs, chore (build process, dependencies), and perf. For a WordPress plugin project, real commit messages look like:

feat(schema): add FAQ schema output for Gutenberg blocks
fix(admin): correct nonce verification on settings save
chore(deps): update tested-up-to to WordPress 6.5
refactor(api): extract REST endpoint registration to separate class

The scope is optional but valuable. For a plugin with distinct subsystems - an admin UI, a REST API, a schema layer - scopes make git log --oneline scannable at a glance.

The real payoff is for future-you. Six months from now, when a user reports that a certain setting stopped working after a version bump, you'll scan the log and immediately spot fix(admin): correct nonce verification on settings save rather than staring at a wall of "update" and "fix stuff".

Conventional Commits also enables automated changelog generation and semantic versioning tooling - useful once a plugin grows past a certain size.

Branching strategy for solo work

Feature branches aren't just for teams. They're for any situation where you want to keep your main branch deployable while experimenting. For solo plugin development, a lightweight rule works well: main is always stable, anything speculative lives on a branch.

Creating a feature branch takes seconds:

git checkout -b feat/rest-api-endpoint

When the feature is done and tested, merge it back - and consider using --no-ff to preserve the branch history as a distinct unit:

git checkout main
git merge --no-ff feat/rest-api-endpoint
git branch -d feat/rest-api-endpoint

That said, branches aren't always worth it. For a quick two-line bugfix that you're confident about, committing directly to main is fine. The overhead of branching should be proportional to the uncertainty of the work. Exploratory rewrites, API integrations, anything that might get abandoned - branch it. Typo fixes and minor style corrections - don't bother.

git stash: your escape hatch for context switching

git stash solves a specific, recurring problem: you're mid-feature when something urgent comes up - a production bug, a client request - and you need a clean working tree without committing half-finished work.

git stash push -m "wip: schema output refactor"

The -m flag is important. Without it, stash entries get default names like "WIP on main: a3f2b1c feat(schema)..." which is technically informative but harder to scan when you have multiple stashes. Named stashes are easier to recover intentionally:

git stash list
git stash pop stash@{0}

A few things to know to avoid losing work. git stash pop applies the stash and removes it from the list. git stash apply applies it but keeps it in the list - useful if you want to apply the same stash to multiple branches. Untracked files are not stashed by default; use git stash push -u to include them. And stashes are local - they don't push to remote, so if you nuke your local repo without applying a stash, it's gone.

Interactive rebase: clean up before you push

Working on a feature branch often produces a commit history that reflects how you worked, not what you built. You'll end up with commits like "fix typo", "actually fix the typo", "WIP", and "ok this works now". Before merging that into main, interactive rebase lets you rewrite that history into something coherent.

git rebase -i HEAD~5

This opens an editor listing the last five commits. Each line starts with a command word - the most useful ones are:

  • pick - keep the commit as-is

  • reword - keep the commit but edit the message

  • squash - merge this commit into the previous one, combining messages

  • fixup - like squash, but discard this commit's message entirely

  • drop - delete the commit completely

A typical cleanup session involves squashing several "wip" and "fix" commits into the meaningful feat or fix commit they belong to, and rewording anything that doesn't follow the commit message format you're using.

One hard rule: never rebase commits that have already been pushed to a shared remote. Interactive rebase rewrites history - if someone else (or another machine) has pulled those commits, you'll create a divergence that's painful to reconcile. On a solo project with a personal remote, this is rarely a problem, but it's worth internalizing early.

git bisect: surgical bug hunting

You notice a bug. You know it wasn't there two weeks ago. You have 40 commits between then and now. Reading through all of them manually is slow and error-prone. git bisect performs a binary search through your commit history to find the exact commit that introduced the regression.

git bisect start
git bisect bad                  # current commit is broken
git bisect good v1.2.0          # this tag was working fine

Git checks out the commit halfway between good and bad. You test - does the bug exist? Then:

git bisect good   # or
git bisect bad

Git keeps halving the range. With 40 commits, you'll find the culprit in at most 6 steps. When bisect identifies the commit, Git tells you exactly which one it is and shows you the diff. Run git bisect reset to return to your original HEAD when you're done.

For WordPress plugin work - where a bug might be a subtle interaction between a hook, a filter, and a database write - bisect turns what could be a two-hour debug session into a ten-minute one.

Tags and releases: versioning your plugins meaningfully

WordPress plugins follow semantic versioning conventions: MAJOR.MINOR.PATCH. A breaking change bumps the major version, new backward-compatible features bump minor, and bug fixes bump patch. When you're ready to tag a release:

git tag -a v1.3.0 -m "feat: add REST API endpoint for schema retrieval"
git push origin v1.3.0

Annotated tags (created with -a) store metadata - the tagger, the date, and a message. Lightweight tags are just pointers. For releases, always use annotated tags.

Tags give you stable reference points for git bisect, meaningful anchors in the log, and a clean way to check out exactly what was running in production at any given time:

git checkout v1.2.1

If you're distributing through the WordPress.org plugin directory, your SVN tags should match your Git tags. Keeping them in sync eliminates version confusion when users report bugs against "the latest version".

.gitignore for WordPress projects: what to exclude

A WordPress plugin repo should contain only the plugin's code - not WordPress core, not other plugins, not generated files, not secrets. A solid .gitignore for a plugin project living inside a local WordPress install:

# WordPress core and other plugins/themes
/wp-admin/
/wp-includes/
/wp-content/plugins/!(your-plugin-name)/
/wp-content/themes/
/wp-content/uploads/

# Environment and config
wp-config.php
.env
*.log

# Dependencies (if using Composer)
/vendor/

# Build artifacts
/node_modules/
/dist/
*.min.js
*.min.css

# OS and editor noise
.DS_Store
Thumbs.db
.idea/
.vscode/
*.swp

A common mistake is committing vendor/ directories. If your plugin uses Composer, commit composer.json and composer.lock - not the vendor directory itself. Anyone cloning the repo runs composer install to reconstruct it. This keeps the repo lean and avoids committing third-party code you don't own.

Never commit wp-config.php. It contains database credentials. Even in a private repo, it's a habit worth forming - repos get made public, get cloned to other machines, get shared. The password generator and hash generator in the Signocore developer tools are handy when rotating credentials or generating auth keys for local configs.

Putting it together in a plugin workflow

A practical day-to-day workflow for a solo plugin developer might look like this: feature branches for anything non-trivial, git add -p to stage only what belongs in each commit, Conventional Commits format for every message, interactive rebase to clean up the branch before merging, annotated tags at every release. When something breaks, git bisect cuts the investigation time dramatically.

None of these practices require a team to justify them. They're investments in your own future productivity - the kind that compound quietly until the day you need to roll back a specific change without touching anything else, and you can do it in under a minute because your history actually means something.

The Diff Checker in Signocore's developer tools is worth bookmarking alongside these workflows - useful for comparing file states outside of Git when you're working across environments or reviewing changes before staging them.

Git's power for solo developers isn't about complexity for its own sake. It's about having enough control over your own history that the repo works for you - not just as a backup, but as a precise record of every decision you made and why.

// keep reading

Have questions about this article?

Get in touch if you'd like to learn more about this topic.