Introduction
Git can feel overwhelming when you're starting out, but once you establish good habits, it becomes second nature. After working on various projects and seeing what works (and what doesn't), I've found that following certain practices makes collaboration smoother and debugging much easier.
Let me walk you through the essential Git practices that will improve your workflow and save you from those late-night "how do I undo this" moments.
Write Clear Commit Messages
Your commit messages are like breadcrumbs for your future self and your teammates. A good commit message answers "what changed and why" without requiring anyone to dig through the code.
The Conventional Commit Format
Using a consistent format helps everyone understand changes at a glance:
type(scope): description
# Examples:
feat(auth): add password reset functionality
fix(api): handle null response in user endpoint
docs(readme): update installation instructions
refactor(utils): simplify date formatting logic
Common types include:
feat: New featurefix: Bug fixdocs: Documentation changesstyle: Code formatting (no logic changes)refactor: Code restructuring without changing behaviortest: Adding or updating testschore: Maintenance tasks
💡 Tip: Keep the first line under 50 characters. If you need more detail, add a blank line and then a longer description.
What Makes a Good Commit Message
# Bad
git commit -m "fix stuff"
git commit -m "updates"
# Good
git commit -m "fix(cart): prevent duplicate items when clicking add quickly"
git commit -m "feat(search): add autocomplete for product names"
Master Your Branching Strategy
A good branching strategy prevents conflicts and keeps your codebase organized. Here's what I've found works well for most teams:
Main Branch Protection
Your main branch should always be deployable. Protect it with these rules:
# Never commit directly to main
git checkout -b feature/user-profile
# Make your changes, then create a pull request
Feature Branch Naming
Use descriptive names that include the type of work and a brief description:
# Good branch names
feature/user-authentication
fix/payment-gateway-timeout
hotfix/critical-security-patch
docs/api-documentation
# Avoid generic names
feature/new-stuff
fix/bug
update/changes
Branch Lifecycle
- Create a feature branch from main
- Make your changes with clear commits
- Push and create a pull request
- After review and merge, delete the branch
# Create and switch to new branch
git checkout -b feature/shopping-cart
# Push new branch to remote
git push -u origin feature/shopping-cart
# After merge, clean up
git branch -d feature/shopping-cart
git push origin --delete feature/shopping-cart
Keep Your History Clean
A clean Git history makes it easier to track changes, find bugs, and understand project evolution.
Interactive Rebase for Cleanup
Before merging, clean up your commits using interactive rebase:
# Rebase last 3 commits
git rebase -i HEAD~3
# In the editor, you can:
# - squash commits together
# - reword commit messages
# - reorder commits
Example of squashing commits:
pick 1a2b3c4 feat(cart): add item to cart
squash 5d6e7f8 fix typo in cart logic
squash 9g0h1i2 remove console.log
# Results in single clean commit:
# feat(cart): add item to cart functionality
⚠️ Warning: Never rebase commits that have been pushed to shared branches. Only rebase your local feature branches.
When to Squash vs Keep Commits
| Scenario | Action | Reason |
|---|---|---|
| Multiple small fixes for same feature | Squash | Creates cleaner history |
| Distinct logical changes | Keep separate | Easier to review and revert |
| Work-in-progress commits | Squash | Removes noise from history |
| Bug fix + new feature | Keep separate | Different types of changes |
Handle Merge Conflicts Like a Pro
Merge conflicts happen when Git can't automatically combine changes. Here's how to resolve them efficiently:
Understanding Conflict Markers
. <<<<<<<< HEAD
. const API_URL = 'https://api.production.com';
. =======
. const API_URL = 'https://api.staging.com';
. >>>>>>> feature/api-updates
<<<<<<< HEAD: Current branch version=======: Separator>>>>>>> branch-name: Incoming changes
Resolution Strategy
# Start merge or rebase
git merge feature/api-updates
# Edit conflicted files, removing markers and choosing correct code
const API_URL = process.env.NODE_ENV === 'production'
? 'https://api.production.com'
: 'https://api.staging.com';
# Stage resolved files
git add conflicted-file.js
# Complete merge
git commit
📌 Note: Use a good merge tool like VS Code's built-in resolver or GitKraken for complex conflicts.
Use .gitignore Effectively
A proper .gitignore file prevents committing files that shouldn't be in version control:
# Dependencies
node_modules/
vendor/
# Build outputs
dist/
build/
*.min.js
# Environment files
.env
.env.local
.env.production
# IDE files
.vscode/
.idea/
*.swp
# OS files
.DS_Store
Thumbs.db
# Logs
*.log
logs/
Common Mistakes to Avoid
# Don't ignore with wildcards that are too broad
# *.json # This would ignore package.json!
# Instead, be specific
config.json
secrets.json
# Don't commit then ignore
# If file is already tracked, ignoring won't work
git rm --cached unwanted-file.txt
# Then add to .gitignore
Leverage Git Hooks for Quality
Git hooks automatically run scripts at certain Git events. They help maintain code quality and consistency:
Pre-commit Hook Example
Create .git/hooks/pre-commit:
#!/bin/sh
# Run linting before commit
npm run lint
if [ $? -ne 0 ]; then
echo "Linting failed. Commit aborted."
exit 1
fi
# Run tests
npm test
if [ $? -ne 0 ]; then
echo "Tests failed. Commit aborted."
exit 1
fi
Using Husky for Team Consistency
# Install husky
npm install --save-dev husky
# Set up pre-commit hook
npx husky add .husky/pre-commit "npm run lint && npm test"
This ensures everyone on your team runs the same checks before committing.
Useful Git Commands for Daily Work
Here are commands that make Git workflow smoother:
Quick Status and Staging
# See what's changed
git status --short
# Stage specific parts of files
git add --patch filename.js
# Stage all changes
git add .
# Unstage files
git reset HEAD filename.js
Viewing History
# Pretty log with graph
git log --oneline --graph --decorate
# See changes in commits
git log -p
# Find specific changes
git log --grep="bug fix" --oneline
Undoing Changes
# Undo last commit (keep changes)
git reset HEAD~1
# Undo last commit (discard changes)
git reset --hard HEAD~1
# Revert commit (creates new commit)
git revert commit-hash
# Discard working directory changes
git checkout -- filename.js
Working with Remotes
# See remote URLs
git remote -v
# Fetch updates without merging
git fetch origin
# Pull with rebase instead of merge
git pull --rebase origin main
# Push new branch
git push -u origin feature-branch
Advanced Workflow Tips
Atomic Commits
Make each commit represent one logical change:
# Bad: mixing different types of changes
git add .
git commit -m "fix login and update readme"
# Good: separate commits
git add src/auth/
git commit -m "fix(auth): handle expired token properly"
git add README.md
git commit -m "docs: update authentication section"
Using Git Stash
Stash helps when you need to switch branches with uncommitted changes:
# Stash current changes
git stash push -m "work in progress on user profile"
# Switch branches
git checkout main
# Come back and restore
git checkout feature/user-profile
git stash pop
# List stashes
git stash list
Cherry-picking Changes
Sometimes you need specific commits from other branches:
# Pick specific commit
git cherry-pick commit-hash
# Pick multiple commits
git cherry-pick commit1 commit2
# Pick without committing (to modify first)
git cherry-pick --no-commit commit-hash
Common Pitfalls to Avoid
Force Push Dangers
# Dangerous on shared branches
git push --force
# Safer alternative
git push --force-with-lease
⚠️ Warning: Force pushing can overwrite others' work. Only do it on your own feature branches.
Large File Issues
Git doesn't handle large files well. For assets over 50MB:
# Use Git LFS for large files
git lfs track "*.psd"
git lfs track "*.mp4"
git add .gitattributes
Merge vs Rebase Decision
| Use Merge When | Use Rebase When |
|---|---|
| Working with shared branches | Cleaning up feature branch |
| Want to preserve branch history | Want linear history |
| Collaborating with team | Working alone |
| Ready to merge to main | Preparing for merge |
Setting Up Your Git Environment
Global Configuration
# Set up user info
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"
# Set default editor
git config --global core.editor "code --wait"
# Set default branch name
git config --global init.defaultBranch main
# Enable color output
git config --global color.ui auto
Useful Aliases
# Add shortcuts for common commands
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.st status
git config --global alias.unstage 'reset HEAD --'
git config --global alias.visual '!gitk'
Conclusion
Good Git practices aren't just about following rules - they make your development life easier and your team more productive. Start with clear commit messages and a simple branching strategy, then gradually adopt more advanced techniques as they become useful.
The key is consistency. Pick practices that work for your team and stick with them. Your future self will thank you when you can easily track down that bug fix from three months ago or when a new team member can understand your project's history at a glance.
Remember, Git is a tool to help you, not hinder you. If a practice feels too complicated or doesn't add value, adjust it to fit your workflow better.



