Diagram credit: nikkiandchris.io (@nikki.and.chris, @NikkiSiapno, @ChrisStaud)
A visual guide to understanding Git's core workflow and the relationship between your working directory, staging area, local repository, and remote repository.
Before using Git, you need to configure it properly:
# Set your identity (required for commits)
git config --global user.name "Your Full Name"
git config --global user.email "[email protected]"
# Verify your configuration
git config --global --list
SSH keys provide secure authentication without entering passwords:
# Generate SSH key pair
ssh-keygen -t ed25519 -C "[email protected]"
# Press Enter for default location (~/.ssh/id_ed25519)
# Optionally set a passphrase
# Add key to SSH agent
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
# Copy public key to clipboard (macOS)
pbcopy < ~/.ssh/id_ed25519.pub
# Copy public key to clipboard (Linux)
cat ~/.ssh/id_ed25519.pub | xclip -selection clipboard
# Copy public key to clipboard (Windows)
type %USERPROFILE%\.ssh\id_ed25519.pub | clip
Add the public key to your Git platform:
- GitHub: Settings → SSH and GPG keys → New SSH key
- GitLab: Preferences → SSH Keys → Add key
- Bitbucket: Personal settings → SSH keys → Add key
Test your SSH connection:
# Test GitHub connection
ssh -T [email protected]
# Test GitLab connection
ssh -T [email protected]
Git tracks all files in your repository by default, but some files shouldn't be committed (temporary files, secrets, build artifacts, etc.). The .gitignore
file tells Git which files to ignore.
# Create .gitignore in your repository root
touch .gitignore
# Edit with your preferred editor
code .gitignore
vim .gitignore
Basic syntax:
# This is a comment
# Ignore specific file
secret.txt
# Ignore all files with extension
*.log
*.tmp
*.cache
# Ignore entire directory
node_modules/
build/
.env/
# Ignore files in any directory
**/temp.txt
# Ignore files only in root directory
/config.local
# Negate a rule (don't ignore this file)
!important.log
Real-world examples:
Node.js project:
# Dependencies
node_modules/
npm-debug.log*
# Environment variables
.env
.env.local
# Build output
dist/
build/
# IDE files
.vscode/
.idea/
# OS files
.DS_Store
Thumbs.db
Python project:
# Python cache
__pycache__/
*.pyc
*.pyo
# Virtual environment
venv/
env/
.env
# IDE
.vscode/
.idea/
# Jupyter notebooks checkpoints
.ipynb_checkpoints/
Java project:
# Compiled class files
*.class
# Build directories
target/
build/
# IDE files
.idea/
.eclipse/
*.iml
# OS files
.DS_Store
Files you should typically ignore:
- Secrets: API keys, passwords, certificates
- Dependencies:
node_modules/
,vendor/
, package files - Build artifacts:
dist/
,build/
, compiled files - IDE files:
.vscode/
,.idea/
, workspace files - OS files:
.DS_Store
,Thumbs.db
,desktop.ini
- Logs:
*.log
, debug files - Temporary files:
*.tmp
,*.cache
, backup files
Check what's being ignored:
# See which files are ignored
git status --ignored
# Check if specific file is ignored
git check-ignore -v filename.txt
Already committed files:
# If you accidentally committed a file that should be ignored
git rm --cached filename.txt
git commit -m "Remove file from tracking"
# For directories
git rm -r --cached directory/
git commit -m "Remove directory from tracking"
Global .gitignore:
# Create global gitignore for your machine
git config --global core.excludesfile ~/.gitignore_global
# Add common files you never want to commit
echo ".DS_Store" >> ~/.gitignore_global
echo "*.tmp" >> ~/.gitignore_global
Do ignore:
- Environment-specific files (
.env
, config files with secrets) - Build artifacts and compiled files
- Package manager directories (
node_modules/
,vendor/
) - IDE and editor files
- OS-generated files
- Log files and temporary files
Don't ignore:
- Source code files
- Configuration templates (
.env.example
) - Documentation files
- Package manager lock files (
package-lock.json
,Gemfile.lock
)
Pro tips:
- Add
.gitignore
early in your project - Use templates from gitignore.io for your tech stack
- Commit your
.gitignore
file so the whole team uses the same rules - Be specific rather than overly broad with patterns
Quick setup:
# Generate .gitignore for your tech stack
curl -o .gitignore https://www.toptal.com/developers/gitignore/api/node,python,java,visualstudiocode
# Or visit gitignore.io and search for your languages/tools
GitHub templates: GitHub provides .gitignore
templates when creating repositories for popular languages and frameworks.
For repositories with large files (images, videos, datasets, binaries):
# Install Git LFS (if not already installed)
# macOS: brew install git-lfs
# Ubuntu: sudo apt install git-lfs
# Windows: Download from git-lfs.github.io
# Initialize LFS in your user account
git lfs install
# Track large file types in your repository
git lfs track "*.jpg"
git lfs track "*.png"
git lfs track "*.mp4"
git lfs track "*.zip"
git lfs track "*.pdf"
# The above commands create/update .gitattributes file
git add .gitattributes
git commit -m "Track large files with LFS"
# Check what's being tracked
git lfs track
# View LFS file info
git lfs ls-files
When to use Git LFS:
- Files larger than 100MB (GitHub's limit)
- Binary files that change frequently
- Media files (images, videos, audio)
- Compiled binaries or archives
- Large datasets
# Set default branch name
git config --global init.defaultBranch main
# Set default editor (optional)
git config --global core.editor "code --wait" # VS Code
git config --global core.editor "vim" # Vim
# Enable colored output
git config --global color.ui auto
# Set up credential helper (HTTPS only)
git config --global credential.helper store # Stores credentials in plain text
git config --global credential.helper cache # Caches for 15 minutes
# Configure line endings (important for cross-platform teams)
git config --global core.autocrlf input # macOS/Linux
git config --global core.autocrlf true # Windows
# Check all global configuration
git config --global --list
# Test with a new repository
mkdir test-repo
cd test-repo
git init
echo "Hello Git" > README.md
git add README.md
git commit -m "Initial commit"
# Should show your name and email in the commit
git log --oneline
Your project files as they exist on your file system. This is where you make changes, edit files, and create new content.
A holding area for changes that are ready to be committed. Think of it as a "draft" of your next commit.
Your local Git database containing all commits, branches, and history stored in the .git
folder.
The shared repository (e.g., on GitHub, GitLab, Bitbucket) where your team collaborates.
# Stage changes from working directory to staging area
git add <file> # Add specific file
git add . # Add all changes
# Commit staged changes to local repository
git commit -m "message" # Create commit with message
# Send commits to remote repository
git push # Push to remote
# Download changes without merging
git fetch # Updates local repo with remote changes
# Download and merge changes
git pull # Equivalent to git fetch + git merge
The stash is a temporary storage area for uncommitted changes:
# Save current changes to stash
git stash # Stash with auto-generated message
git stash save "WIP: working on login feature" # Stash with custom message
# Apply stashed changes back
git stash apply # Apply most recent stash
git stash pop # Apply and remove most recent stash
# List all stashes (shows like this)
git stash list
# Output example:
# stash@{0}: WIP on feature-branch: 1a2b3c4 Add user validation
# stash@{1}: On main: 5d6e7f8 Fix header styling
# stash@{2}: WIP: working on login feature
# Apply specific stash by index
git stash apply stash@{1}
git stash pop stash@{0}
# See what's in a stash without applying
git stash show stash@{0} # Summary of changes
git stash show -p stash@{0} # Full diff
# Delete specific stash
git stash drop stash@{1}
# Clear all stashes
git stash clear
Stash identification tips:
- Most recent stash is always
stash@{0}
- Stashes include branch name and last commit message
- Use descriptive messages:
git stash save "fixing CSS before switching to bug branch"
git stash show
lets you preview before applying
- Make changes in working directory
git add
to stage changesgit commit
to save to local repositorygit push
to share with team
git pull # Get latest changes from remote
# Make your changes
git add .
git commit -m "Your changes"
git push
# You're working on feature A, but need to switch to fix a bug
git stash # Save current work
git checkout main # Switch to main branch
# Fix the bug, commit, push
git checkout feature-a # Back to your feature
git stash pop # Restore your work
- Commit often: Make small, logical commits with clear messages
- Pull before push: Always
git pull
before pushing to avoid conflicts - Use stash wisely: Great for temporarily saving work when switching contexts
- Stage selectively: Use
git add <file>
to stage only related changes together
The diagram shows how each command moves your code between the different areas:
- Rightward arrows: Moving code toward the remote (add → commit → push)
- Leftward arrows: Getting code from remote (pull, fetch)
- Stash operations: Temporary storage for work-in-progress
Beyond the basic workflow, teams use different branching strategies to organize their development process:
main ← feature-branch-1
← feature-branch-2
← feature-branch-3
Simple and effective for continuous deployment:
# Start new feature
git checkout main
git pull
git checkout -b feature/user-authentication
# Develop feature
# ... make changes, add, commit ...
# Push and create pull request
git push -u origin feature/user-authentication
# Create PR to merge into main
# After review, merge to main
Best for: Small teams, continuous deployment, rapid iterations
main ← test ← develop ← feature-branch-1
← feature-branch-2
← feature-branch-3
Structured approach with quality gates:
# Start feature from develop
git checkout develop
git pull
git checkout -b feature/payment-system
# Develop feature
# ... make changes, add, commit ...
# Merge to develop
git checkout develop
git merge feature/payment-system
# When ready for testing
git checkout test
git merge develop
# After QA approval
git checkout main
git merge test
git tag v1.2.0 # Tag the release
Branch purposes:
- main: Production-ready code only
- test: Pre-production testing and QA
- develop: Integration of all features
- feature/*: Individual feature development
Best for: Larger teams, formal QA process, scheduled releases
main ← release ← develop ← feature branches
← hotfix branches
Full Gitflow with release branches:
- main: Production releases
- develop: Integration branch
- feature/*: New features
- release/*: Preparing releases (bug fixes only)
- hotfix/*: Emergency production fixes
Best for: Complex projects with formal release cycles
main (trunk) ← very short-lived feature branches
High-velocity approach:
- Everyone commits to main frequently (multiple times daily)
- Feature branches live for hours/1-2 days maximum
- Heavy use of feature flags to hide incomplete features
- Requires excellent CI/CD and automated testing
Best for: High-velocity teams with strong automation
main ← feature branches
↓
staging ← (deploy by merging)
↓
production
Environment-based deployments:
- Deploy by merging between environment branches
- Can have multiple environment branches
- Combines simplicity with deployment control
Consider these factors:
Team Size & Experience:
- Small team (2-5): Feature Branch Workflow
- Medium team (5-15): Gitflow with Testing Branch
- Large team (15+): Classic Gitflow or Trunk-based
Release Frequency:
- Continuous deployment: Feature Branch or Trunk-based
- Weekly/monthly releases: Gitflow variations
- Scheduled releases: Classic Gitflow
Quality Requirements:
- High-risk applications: Gitflow with testing stages
- Rapid prototyping: Feature Branch Workflow
- Enterprise software: Classic Gitflow
Automation Level:
- High automation: Trunk-based Development
- Manual testing: Gitflow variations
- Mixed: Feature Branch with CI/CD
# Feature branches
feature/user-authentication
feature/payment-integration
feat/dashboard-redesign
# Bug fixes
bugfix/login-validation
fix/memory-leak
# Hotfixes
hotfix/security-patch
hotfix/v1.2.1
# Release branches (if using Gitflow)
release/v1.3.0
release/2024-q1
Rebase is a powerful tool that allows you to rewrite commit history for a cleaner project timeline.
Scenario: You're working on a feature branch, but main has moved forward with new commits.
Before (your situation):
main: A---B---C---F---G (main moved forward while you worked)
\
feature: D---E (your feature commits)
Option 1 - Merge main into feature:
main: A---B---C---F---G
\ \
feature: D---E---M (merge commit M combines both)
Option 2 - Rebase your feature branch to catch up:
feature: A---B---C---F---G---D'---E' (your commits moved to new base)
main: A---B---C---F---G (main unchanged)
What rebase does: Takes your commits (D, E) and replays them on top of the latest main (after G), creating new commits (D', E') with the same changes but different commit hashes. Your feature branch is now up-to-date with main.
Update your feature branch with latest main (this is probably what you've done):
# You're on your feature branch, main has moved forward
git checkout feature-branch
git fetch origin # Get latest changes
git rebase origin/main # Replay your commits on top of latest main
# Or if you have local main up to date
git rebase main feature-branch
Common scenario: You made commits on your feature branch, but someone else pushed to main (or you made changes in GitHub UI). Rebasing puts your feature commits on top of the latest main.
Clean up your own commits before pushing:
# Clean up your last 3 commits on current branch
git rebase -i HEAD~3
# This is useful when you have commits like:
# - "Add user login"
# - "Fix typo"
# - "Actually fix the typo"
# - "Add validation"
# You can squash the typo fixes into the main commits
Use Rebase when:
- Cleaning up local commits before pushing
- Creating linear history for features
- You haven't shared the commits yet (local only)
Use Merge when:
- Preserving the true history of development
- Working with shared/public branches
- Merging completed features
Golden Rule: Never rebase commits that have been pushed to shared repositories unless your team explicitly agrees to it.
Conflicts occur when Git can't automatically merge changes. Here's how to resolve them:
# During merge
git merge feature-branch
# Auto-merging failed; fix conflicts and then commit
# During rebase
git rebase main
# CONFLICT: Merge conflict in file.txt
1. Identify conflicted files:
git status
# Shows files with conflicts in red
2. Open conflicted files and look for markers:
<<<<<<< HEAD
Your current branch content
=======
The incoming change content
>>>>>>> feature-branch
3. Resolve conflicts by editing the file:
# Remove conflict markers and choose/combine content
Final resolved content here
4. Mark conflicts as resolved:
git add <resolved-file>
5. Complete the merge/rebase:
# For merge
git commit
# For rebase
git rebase --continue
Use merge tools:
# Configure a merge tool (like VS Code)
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'
# Use the tool during conflicts
git mergetool
Abort if needed:
# Abort merge
git merge --abort
# Abort rebase
git rebase --abort
View conflict details:
# See what caused the conflict
git diff
# See both sides of the conflict
git log --merge --oneline
Prevention strategies:
- Pull frequently from main branch
- Keep feature branches small and short-lived
- Communicate with team about file changes
- Use consistent code formatting
During conflicts:
- Don't panic - conflicts are normal
- Take time to understand both changes
- Test your resolution before committing
- Ask for help if you're unsure about the intent of conflicting changes
Example conflict resolution workflow:
# Working on feature branch
git checkout feature/new-header
git rebase main
# CONFLICT in header.html
# 1. Check what's conflicted
git status
# 2. Open header.html, see:
# <<<<<<< HEAD
# <h1>Welcome to Our Site</h1>
# =======
# <h1>Welcome to My Amazing Site</h1>
# >>>>>>> feature/new-header
# 3. Decide on resolution:
# <h1>Welcome to Our Amazing Site</h1>
# 4. Mark as resolved
git add header.html
# 5. Continue rebase
git rebase --continue