Git Workflow Guide: From Basics to Advanced Branching Strategies
Updated March 2026 · 13 min read
Why Version Control Matters
Every software project, whether it is a weekend side project or a production system serving millions of users, eventually reaches a point where tracking changes becomes critical. Version control is the discipline of recording every modification to your codebase over time so that you can recall specific versions later, understand who changed what and why, and collaborate with others without overwriting each other's work.
Before modern version control systems existed, developers relied on manual methods: copying entire project folders with date-stamped names, emailing zip files back and forth, or maintaining shared network drives where only one person could edit a file at a time. These approaches were fragile, error-prone, and collapsed under the weight of even modest team sizes.
Git, created by Linus Torvalds in 2005 to manage Linux kernel development, solved these problems decisively. It introduced a distributed model where every developer has a complete copy of the repository history. This means you can work offline, commit freely, and synchronize only when ready. There is no single point of failure, and branching—once an expensive operation in older systems like Subversion—is nearly instant.
Version control matters for several concrete reasons:
- Accountability: Every change is attributed to an author with a timestamp and message explaining the intent.
- Reversibility: If a deployment breaks production, you can revert to the last known-good state in seconds.
- Parallel work: Multiple developers can work on different features simultaneously without stepping on each other's code.
- Code reviews: Changes can be proposed, discussed, and refined before they are merged into the main codebase.
- Auditability: Regulated industries often require a complete history of every change to source code, and Git provides this natively.
Whether you are a solo developer or part of a large engineering organization, understanding Git deeply will save you time, prevent data loss, and make collaboration far smoother.
Git Fundamentals
Before exploring branching strategies and advanced techniques, you need a solid understanding of the core Git commands that form the foundation of every workflow. These commands are what you will use dozens of times a day.
Initializing a repository. Every Git project starts with git init. This creates a hidden .git directory in your project root that stores all version history, configuration, and metadata. If you are joining an existing project, you will use git clone <url> instead, which copies the entire repository including its history to your machine.
The staging area. Git has a unique concept called the staging area (also known as the index). When you modify files, those changes exist only in your working directory. You must explicitly stage them with git add before they can be committed. This intermediate step lets you craft precise commits by selecting exactly which changes to include.
To stage a specific file:
git add src/auth/login.js
To stage all changed files:
git add .
Committing changes. A commit is a snapshot of your staged changes. Each commit gets a unique SHA-1 hash and stores the author, timestamp, and a message describing the change. Write clear, descriptive commit messages—they are the narrative of your project's evolution.
git commit -m "Add email validation to login form"
Checking status. The git status command is your most-used diagnostic tool. It tells you which files are modified, which are staged, and which are untracked. Run it frequently to stay oriented:
git status
Typical output shows three sections: changes to be committed (staged), changes not staged for commit (modified but not yet added), and untracked files (new files Git does not know about yet).
Viewing history. The git log command shows the commit history. The default output can be verbose, so most developers use formatting flags:
git log --oneline --graph --decorate --all
This shows a compact, visual representation of your commit history across all branches. It is one of the most useful commands for understanding the state of a repository at a glance.
Other essential commands in the fundamentals category include git diff to see what has changed, git rm to remove tracked files, and git mv to rename or move files while preserving history.
Branching and Merging
Branching is where Git truly shines. A branch in Git is simply a lightweight, movable pointer to a commit. Creating a branch is nearly instantaneous regardless of repository size, which encourages developers to branch freely and frequently.
Creating a branch. To create a new branch and switch to it:
git checkout -b feature/user-authentication
Or using the newer switch command, introduced in Git 2.23:
git switch -c feature/user-authentication
Switching between branches. Move between branches with:
git switch main
git switch feature/user-authentication
Git will update your working directory to reflect the state of that branch. If you have uncommitted changes that would conflict, Git will warn you and refuse to switch until you commit or stash your changes.
Merging. Once your feature is complete and tested, you merge it back into the target branch, typically main or develop:
git switch main
git merge feature/user-authentication
If the branches have diverged (both have new commits since they split), Git performs a three-way merge and creates a merge commit. If the target branch has no new commits, Git performs a fast-forward merge, simply moving the branch pointer forward without creating an extra commit.
Deleting branches. After a branch has been merged, clean it up:
git branch -d feature/user-authentication
The -d flag only deletes the branch if it has been fully merged. Use -D to force-delete an unmerged branch, but be careful—you could lose commits that exist only on that branch.
A typical workflow cycle looks like this:
- Pull the latest changes from the remote main branch.
- Create a feature branch from main.
- Make your changes, committing frequently with meaningful messages.
- Push your feature branch to the remote.
- Open a pull request for code review.
- After approval, merge back into main.
- Delete the feature branch.
Git Flow vs GitHub Flow vs Trunk-Based Development
Choosing a branching strategy is one of the most impactful decisions a team makes. The right strategy depends on your team size, release cadence, and deployment model. Here are the three most widely adopted approaches.
Git Flow was popularized by Vincent Driessen in 2010 and introduces a structured branching model with clearly defined roles for each branch type:
main— always reflects the production-ready state.develop— the integration branch where features are combined for the next release.feature/*— branched from develop, merged back into develop when complete.release/*— branched from develop when preparing a release, merged into both main and develop.hotfix/*— branched from main for urgent production fixes, merged into both main and develop.
Git Flow works well for projects with scheduled releases, such as mobile apps or desktop software where you ship versioned artifacts. However, it can be overly complex for web applications that deploy continuously. The overhead of maintaining parallel long-lived branches and performing multiple merge operations per feature adds friction that may not be justified.
GitHub Flow is a radically simpler alternative. There is one long-lived branch: main. Every change goes through a feature branch and a pull request. After review and CI checks pass, the branch is merged into main, and main is deployed immediately.
The rules are straightforward:
- Anything on main is deployable.
- Create descriptively named branches off main for new work.
- Push to your branch regularly and open a pull request when ready for feedback.
- After review, merge to main.
- Deploy immediately after merging.
GitHub Flow is ideal for teams practicing continuous deployment. It minimizes branching complexity and keeps the entire team focused on a single source of truth.
Trunk-Based Development takes simplicity even further. Developers commit directly to the trunk (main branch), or use extremely short-lived feature branches that last no more than a day or two. The key principles are:
- Keep branches short-lived—merge within 24 hours if possible.
- Use feature flags to hide incomplete work rather than long-lived branches.
- Rely heavily on automated testing and CI to catch regressions immediately.
- Deploy from trunk frequently, sometimes multiple times per day.
This model is used by many high-performing engineering teams at companies like Google, Facebook, and Netflix. It requires a mature testing culture and strong CI infrastructure but rewards teams with fewer merge conflicts, faster feedback loops, and simpler mental models.
When deciding between these three, consider your release frequency. If you ship monthly releases, Git Flow provides useful structure. If you deploy daily, GitHub Flow strikes a good balance. If you deploy multiple times a day and have strong CI, trunk-based development offers the least friction.
Pull Requests and Code Reviews
Pull requests (PRs) are the mechanism through which most teams propose, discuss, and review changes before merging them. While not a Git feature per se (they are provided by platforms like GitHub, GitLab, and Bitbucket), they are central to modern Git workflows.
A good pull request has several characteristics:
- Small scope: PRs should address a single concern. A PR that touches 50 files across unrelated features is difficult to review and prone to bugs slipping through.
- Clear description: Explain what changed, why it changed, and how to test it. Include screenshots for UI changes and link to related issues or tickets.
- Passing CI: All automated checks (linting, tests, type checking, security scanning) should pass before requesting review.
- Self-reviewed: Before requesting others' time, review your own diff. You will often catch issues yourself.
On the reviewing side, focus on these aspects:
- Correctness: Does the code do what it claims? Are edge cases handled?
- Readability: Will a future developer understand this code without the PR description as context?
- Architecture: Does the change fit within the existing patterns and conventions of the codebase?
- Security: Are inputs validated? Are secrets hardcoded? Does the change introduce vulnerabilities?
- Performance: Could this cause N+1 queries, memory leaks, or unnecessary re-renders?
Use comments for questions and suggestions, but also approve work that is good enough even if you would have done it differently. Blocking PRs over style preferences erodes trust and slows the team down.
Resolving Merge Conflicts
Merge conflicts occur when two branches modify the same lines of the same file, or when one branch deletes a file that the other modified. Git cannot automatically determine which change should win, so it asks you to resolve the conflict manually.
When a conflict occurs during a merge, Git marks the affected files with conflict markers:
<<<<<<< HEAD
const apiUrl = '/api/v2/users';
=======
const apiUrl = '/api/v3/users';
>>>>>>> feature/upgrade-api
The section between <<<<<<< HEAD and ======= is your current branch's version. The section between ======= and >>>>>>> is the incoming branch's version.
To resolve the conflict:
- Open the file and find the conflict markers.
- Decide which version to keep, or combine both changes manually.
- Remove the conflict markers entirely.
- Stage the resolved file with
git add <file>. - Complete the merge with
git commit.
Most modern editors and IDEs provide merge conflict resolution tools that display both versions side by side and let you accept one, the other, or both with a single click. VS Code, for instance, highlights conflicts and offers inline "Accept Current," "Accept Incoming," and "Accept Both" buttons.
To minimize merge conflicts in the first place:
- Pull and merge main into your feature branch frequently.
- Keep branches short-lived—the longer a branch lives, the more it diverges.
- Coordinate with teammates when working on the same files.
- Prefer small, focused commits over large, sweeping changes.
If a merge conflict feels too complex, you can always abort and start over:
git merge --abort
Git Best Practices
Adopting consistent Git practices across your team dramatically improves collaboration and makes your repository a reliable historical record. These practices are not just suggestions—they are the difference between a repository that tells a clear story and one that is an impenetrable tangle.
Write meaningful commit messages. A good commit message has a short summary line (50 characters or fewer), a blank line, and then a detailed explanation if needed. Use the imperative mood: "Add user authentication" not "Added user authentication" or "Adding user authentication."
feat: Add rate limiting to API endpoints
Implement token bucket algorithm with configurable
limits per endpoint. Default is 100 requests per
minute per user.
Closes #234
Many teams adopt the Conventional Commits specification, which prefixes messages with a type (feat, fix, docs, refactor, test, chore). This enables automated changelog generation and semantic versioning.
Use a .gitignore file. Every repository should have a .gitignore file that excludes files that should not be committed: build artifacts, dependency directories, environment files with secrets, editor-specific files, and OS-generated files. GitHub maintains a comprehensive collection of templates at github.com/github/gitignore.
A typical .gitignore for a Node.js project might include:
node_modules/
dist/
.env
.env.local
*.log
.DS_Store
coverage/
.idea/
.vscode/
Make atomic commits. Each commit should represent one logical change. Do not commit a bug fix, a feature addition, and a formatting change all in one commit. Atomic commits make it easy to revert specific changes, cherry-pick fixes to other branches, and understand the history with git log or git bisect.
Use git add -p (patch mode) to stage individual hunks within a file rather than the entire file. This is invaluable when you have been working on multiple concerns and need to split them into separate commits:
git add -p src/server.js
Git will show you each chunk of changes and ask whether to stage it, skip it, or split it further.
Never commit secrets. API keys, database passwords, and private certificates should never appear in version history. Even if you remove them in a later commit, they remain in the Git history. Use environment variables, secret management tools (Vault, AWS Secrets Manager), or encrypted configuration files instead. If you accidentally commit a secret, consider the key compromised, rotate it immediately, and use tools like git filter-branch or BFG Repo-Cleaner to purge it from history.
Tag releases. Use annotated tags to mark releases:
git tag -a v1.2.0 -m "Release version 1.2.0"
git push origin v1.2.0
Tags create permanent reference points in your history and are used by many CI/CD systems to trigger release pipelines.
Advanced Git
Once you are comfortable with the fundamentals, these advanced techniques will help you maintain a cleaner history and debug issues more efficiently.
Interactive rebase lets you rewrite commit history before sharing it. This is powerful for cleaning up a messy series of work-in-progress commits into a coherent narrative:
git rebase -i HEAD~5
This opens an editor showing the last 5 commits. You can reorder them, squash multiple commits into one, edit commit messages, or drop commits entirely. The most common operations are:
pick— keep the commit as-is.squash— combine with the previous commit, keeping both messages.fixup— combine with the previous commit, discarding this commit's message.reword— keep the commit but edit the message.drop— remove the commit entirely.
A critical rule: never rebase commits that have been pushed to a shared branch. Rebasing rewrites commit hashes, which will cause problems for anyone who has already based their work on those commits.
Rebasing vs merging. While git merge preserves the true history of when branches diverged and converged, git rebase replays your changes on top of the target branch, producing a linear history. Many teams prefer a "rebase locally, merge via PR" workflow: developers rebase their feature branches on main to keep history clean, but the final merge into main creates a merge commit that clearly marks when the feature was integrated.
# Update your feature branch with latest main
git switch feature/search
git fetch origin
git rebase origin/main
Cherry-pick applies a specific commit from one branch to another without merging the entire branch. This is useful for applying a critical bug fix that was committed to a feature branch before the full feature is ready:
git switch main
git cherry-pick a1b2c3d
This creates a new commit on main with the same changes as commit a1b2c3d. Use cherry-pick sparingly—overuse leads to duplicate commits and confusing history.
Stash temporarily shelves your uncommitted changes so you can work on something else, then re-apply them later:
# Save current work
git stash push -m "WIP: refactor validation logic"
# Do other work, switch branches, etc.
git switch main
git pull
# Come back and restore your stash
git switch feature/validation
git stash pop
You can maintain multiple stashes and list them with git stash list. Apply a specific stash by index: git stash apply stash@{2}. The stash is a stack, so the most recent stash is always at index 0.
Bisect is Git's built-in binary search tool for finding the commit that introduced a bug. If you know a bug exists now but did not exist 100 commits ago, bisect will find the exact commit in about 7 steps instead of 100:
git bisect start
git bisect bad # current commit has the bug
git bisect good v1.0.0 # this version was known to work
# Git checks out a commit in the middle.
# Test it and mark it:
git bisect good # or git bisect bad
# Repeat until Git identifies the offending commit.
git bisect reset # return to your original branch
You can even automate bisect by providing a test script:
git bisect start HEAD v1.0.0
git bisect run npm test
Git will automatically run your test suite at each bisect step and mark commits as good or bad based on the exit code. This is extremely powerful when you have reliable automated tests. When working with code formatting across different commits, tools like DevTools Pro can help ensure consistent formatting that will not interfere with your bisect results.
Reflog is your safety net. Git records every change to branch tips and HEAD in the reflog, even operations that are not part of the commit history (like resets and rebases). If you accidentally delete a branch or reset too far back, you can often recover:
git reflog
# Find the commit you want to return to
git reset --hard HEAD@{3}
Reflog entries are kept for 90 days by default. Think of the reflog as Git's undo history—it tracks everything you did, even the mistakes.
Conclusion
Git is far more than a tool for saving code—it is a collaboration framework, a safety net, and a historical record of every decision your team has made. The fundamentals of add, commit, and push will get you started, but the real power of Git emerges when you understand branching strategies, master conflict resolution, and leverage advanced features like interactive rebase and bisect.
Start with a branching model that matches your team's release cadence. If you are deploying frequently, GitHub Flow or trunk-based development will serve you well. If you need structured release cycles, Git Flow provides the guardrails. As your team grows, invest in pull request conventions, consistent commit messages, and automated CI checks to keep quality high.
The most important thing is to practice. Create a throwaway repository, experiment with rebasing, intentionally create and resolve merge conflicts, and run a bisect session to find a deliberately introduced bug. The muscle memory you build in a safe environment will prove invaluable when you face these scenarios in production under pressure.
Version control is a skill that compounds over time. Every hour you invest in understanding Git more deeply will pay dividends across your entire career as a developer.
Level Up Your Development Workflow
Format, minify, and transform your code with DevTools Pro—a free suite of developer utilities designed for speed and privacy.
Try DevTools Pro