Overview
Git is the essential version control system that tracks changes in your code and enables collaboration with other developers.
We could do without GitHub (a service to host your git repositories), but we cannot do without Git - that's the tool you should know how to use.
How I Started Learning Git
Git can feel intimidating at first - all the commands seem strange and obscure. But with practice and the right understanding of core concepts, Git becomes an incredibly powerful tool for managing code changes and collaborating with others.
The best way to learn Git is through hands-on practice. Start with the basic commands and gradually work your way up to more advanced features like rebasing and interactive staging.
Basic Git Workflow
The fundamental Git workflow involves four main commands:
Getting Started
Clone an existing repository:
git clone https://github.com/davdma/davdma.github.io.git
Or start a new repository:
git init
Core Workflow
-
Check file status:
git status
-
Stage changes:
git add file.py
-
Commit changes:
git commit -m "my first commit"
-
Push to remote:
git push
-
Pull changes:
git pull
You can add a remote repository later using git remote add <name> <url>
if you started with git init
.
Concepts
File States
Files are either tracked or untracked. Tracked means Git knows about the file and can recover it. Untracked files are everything else in the directory.
Git's Three Areas
The mental model: working tree → index → git history
- Working Directory (Working Tree): The directory on your file system where project files live
- Staging Area (Index): Temporary area where you prepare changes for the next commit
- Git History: The permanent record of commits
Understanding the flow between areas
git add
moves changes from working tree → indexgit commit
moves changes from index → git history- The index allows you to selectively stage changes without committing everything at once
Remotes
Remotes are versions of your repository hosted online (like on GitHub). They enable collaboration by providing a central location where team members can share code changes.
# View remotes
git remote -v
# Add a remote
git remote add <name> <url>
# Push to specific remote
git push <remote> <branch>
Working with Remotes
Remote Refs and Tracking Branches
When working with remote repositories, Git maintains several types of references:
Think of remote refs as Git's way of keeping track of what the remote repository looked like the last time you communicated with it.
Remote tracking branches are local references to the state of remote branches. They follow the naming pattern <remote>/<branch>
(e.g., origin/main
, origin/feature-branch
). These refs are automatically updated when you fetch from the remote and represent Git's local cache of the remote repository state.
# View all remote tracking branches
git branch -r
# View both local and remote branches
git branch -a
# See detailed tracking information
git branch -vv
# See what remote refs exist
git ls-remote origin
Upstream tracking creates a connection between your local branch and a remote branch. When you push a local branch to remote for the first time, you set up this tracking relationship:
# Push and set upstream tracking in one command
git push -u origin feature-branch
# Equivalent to:
git push origin feature-branch
git branch --set-upstream-to=origin/feature-branch
Understanding what upstream tracking enables
Once upstream tracking is established:
git status
shows how many commits you're ahead/behind the remotegit pull
knows which remote branch to merge fromgit push
knows where to push without specifying remote/branch- Git can warn you if your local branch has diverged from remote
Remote refs are stored in .git/refs/remotes/
and are read-only from your perspective. You cannot directly commit to origin/main
- you commit to your local main
branch, then push to update the remote.
Working with Multiple Remotes
You can have multiple remotes (e.g., origin
, upstream
, fork
) each with their own set of remote refs:
# Add additional remotes
git remote add upstream https://github.com/original/repo.git
# Fetch from all remotes
git fetch --all
Each remote maintains its own namespace of tracking branches:
origin/main
- main branch from origin remoteupstream/main
- main branch from upstream remotefork/experimental
- experimental branch from fork remote
Fetching vs Pulling
Fetching downloads the latest changes from remote repositories and updates your remote tracking refs (like origin/main
) without affecting your local branches.
# Fetch updates from all remotes
git fetch
# Fetch from specific remote
git fetch origin
# View what was fetched
git log HEAD..origin/main
Pulling combines fetch and merge in one command:
# git pull = git fetch + git merge
git pull
# Equivalent to:
git fetch
git merge origin/main
Because git pull
runs git fetch
+ git merge
, pulling can sometimes create merge commits.
Use git pull --rebase
to avoid merge commits and keep a linear history when pulling updates.
Undoing Changes
Often times you'll find yourself in the situation of having run a git command you didn't want to actually run, and you want to go back. This can be tricky if you do not know the appropriate undo commands (some might unknowingly run a dangerous operation like git clean
and lose precious work, which I've seen happen!).
Unstaging Files
Modern way (preferred):git restore --staged <file>
git reset HEAD <file>
Discarding File Changes
Modern way (preferred):git restore <file>
git checkout -- <file>
HEAD is not moved when you revert a single file.
Reset Modes
Be careful with reset commands - they can permanently discard changes!
git reset <mode> <commit>
Understanding reset modes
--soft
: Moves HEAD only, keeps index and working tree unchanged--mixed
(default): Resets index, keeps working tree unchanged--hard
: Resets both index and working tree - DANGEROUS!
Git Reset vs Git Restore
git reset
moves the HEAD, while git restore
only modifies your working directory. This makes git restore
safer.
Viewing Differences
A very useful command is git diff
to look at what changes you have compared to a saved reference point. You have options between looking at changes between your working directory and index, index and commit (by default HEAD), or working directory and commit.
# Working tree vs index
git diff
# Index vs commit
git diff --staged <commit>
# Working tree vs commit
git diff --merge-base <commit>
# Show specific commit changes
git show <commit-sha>
Branching
Branching is Git's most powerful feature. It allows you to work on code separately without affecting the main codebase.
Branching enables multiple developers to work on different features simultaneously. The typical workflow protects the main branch, requiring developers to:
- Branch off of
main
- Work on the feature branch
- Submit a pull request (PR)
- Get code reviewed
- Merge into
main
This process is essential for Continuous Integration (CI), preventing integration conflicts by frequently merging work.
Branch Commands
# Create new branch
git branch <name>
# OR create and switch in one command
git checkout -b <name>
# Switch branches
git checkout <name>
# Set upstream for first push
git push -u <remote> <branch>
# Delete local branch
git branch -d <branch>
# Delete remote branch
git push origin --delete <branch>
Moving Around
Git checkout allows you to move around your project's history, examining different commits and branches.
# Jump to a specific commit
git checkout <commit-hash>
# Go back 3 commits from current HEAD
git checkout HEAD~3
# Return to latest commit on current branch
git checkout <branch-name>
When you checkout a specific commit (not a branch), you enter a "detached HEAD" state where HEAD points directly to a commit rather than a branch.
Detached HEAD State
When you checkout a specific commit instead of a branch, Git enters detached HEAD state. This happens because HEAD points directly to a commit rather than to a branch reference.
Understanding detached HEAD
Normal state: HEAD → main → commit ABC123
Detached state: HEAD → commit ABC123
(no branch involved)
This is useful for:
- Exploring historical versions of your code
- Testing how your project behaved at a specific point
- Investigating when a bug was introduced
If you make commits from a detached HEAD state, those commits can be lost! Always create a new branch first if you intend to make commits from a specific commit.
# Safe way to work from a specific commit
git checkout <commit-hash>
git checkout -b new-feature-branch # Create branch from this point
# Now you can safely make commits
Merging
Merge commits combine changes from two branches using a three-way merge (common ancestor + both branch tips). The primary purpose of merging is to integrate your feature branch back into the main branch once your work is complete and reviewed.
Merging preserves the complete history of both branches, showing exactly when and how features were developed in parallel.
When you're ready to combine your feature work back into main
, you typically:
- Ensure your feature branch is up-to-date with
main
- Create a pull request for code review
- Merge the feature branch into
main
(often done through GitHub/GitLab UI)
Why merge conflicts happen
Merge conflicts occur when Git can't automatically combine changes because:
- Same lines modified: Both branches changed the same lines of code differently
- File moved/deleted: One branch modified a file while another deleted or moved it
- Structural conflicts: Changes that affect the same logical code structure
These conflicts are normal in collaborative development - they're Git's way of asking you to manually decide how to combine conflicting changes.
To merge latest main
changes into your feature branch: checkout main
→ git pull
→ checkout feature branch → git merge main
Handling Merge Conflicts
You don't have to choose "current" or "incoming" - you can rewrite the conflicted lines however you want!
Resolving merge conflicts
- Look for conflict markers:
<<<<<<<
,=======
,>>>>>>>
- Edit the file to resolve conflicts (you can combine, modify, or rewrite)
- Remove all conflict markers
- Save and stage the file (
git add <file>
) - Continue with
git merge --continue
Flags & Commands
Here are a list of useful commands with flags that makes my life a lot easier:
# Clean status output (hide untracked files)
git status -uno
# Add and commit everything modified
git commit -a
# Interactive adding for selective commits
git add -i
git add --patch # For partial file changes
# Rebase instead of merge on pull
git pull --rebase
# Enhanced log viewing
git log -p # Show diffs
git log --stat # Show stats
git log --graph # ASCII graph
git log -- <file> # Limit to file
git log -S <string> # Search for string changes
# More diff context
git diff -U8 # 8 lines context instead of 3
# List tracked files
git ls-files .
# Column branch output
git branch --column
# Branch status vs remote
git branch -vv
Advanced commands
# File authorship
git blame -w -C -C -C <file>
git blame -L 59,100 <file> # Specific lines
git log -L 59,100:<file> # Line evolution
# Safe force push
git push --force-with-lease
# Performance optimization
git maintenance start
Test Your Knowledge
Resources
- Interactive Git Tutorial - Visual, hands-on learning
- Pro Git book - Comprehensive reference
- UChicago Student Resource Guide
- Learn Git Rebase
- Scott Chacon at FOSDEM