Git for Beginners: Install, Configure, Commit, Branch and Push to a Remote
Key takeaways
Git is the world's most widely used version control system. This guide walks you through everything you need to get started: installation, commits, branches, and pushing to GitHub.
What is Git?
Git is the world’s most widely used version control system (VCS) — a tool that tracks every change to your files, lets you revert to any previous state, and enables many people to collaborate on the same codebase.
Without Git: With Git:
project_v1.zip git log → full history
project_v2.zip git checkout → any version
project_FINAL.zip git branch → parallel work
project_FINAL2.zip git merge → combine work
This guide covers the core workflow. By the end you will be able to:
- Install and configure Git
- Stage and commit changes
- Create and switch branches
- Push to and pull from GitHub
Installation
macOS
# Option 1: Homebrew (recommended)
brew install git
# Option 2: Xcode Command Line Tools
xcode-select --install
Windows
Download the installer from git-scm.com and follow the wizard. Use Git Bash for all commands in this guide.
Linux (Ubuntu / Debian)
sudo apt update && sudo apt install git
Verify
git --version
# git version 2.44.0
Initial Configuration
Run these once after installation. user.name and user.email are written into every commit you make; they are not login credentials, but platforms like GitHub use the email to link commits to your account, so use the address registered there (or GitHub’s noreply address if you want to keep your email private).
git config --global user.name "Your Name"
git config --global user.email "[email protected]"
# Set default branch name to 'main'
git config --global init.defaultBranch main
# Use VS Code as the default editor
git config --global core.editor "code --wait"
# Line endings: convert CRLF to LF when committing
git config --global core.autocrlf input # macOS/Linux
# git config --global core.autocrlf true # Windows
The line-ending setting prevents a classic cross-platform mess. Windows editors write CRLF line endings, macOS and Linux use LF; without normalization, a Windows teammate opening and saving a file can change every line, and git diff shows the whole file as modified even though nothing visible changed. true on Windows converts to LF in the repository and back to CRLF in your working copy; input on macOS/Linux only fixes stray CRLFs on commit. A .gitattributes file with * text=auto in the repository enforces the same normalization for everyone regardless of their personal config, which is more reliable than asking each developer to set it.
Check your settings:
git config --list
Core Concepts
Working Directory → Staging Area → Repository
(your files) (git add) (git commit)
↓
Remote (git push)
| Term | What it is |
|---|---|
| Repository (repo) | The .git folder that stores all history |
| Working directory | The files you see and edit |
| Staging area (index) | A “draft” of your next commit |
| Commit | A snapshot of your staged changes with a message |
| Branch | A movable pointer to a sequence of commits |
| Remote | A copy of the repo on another machine (e.g. GitHub) |
The staging area is the concept that most confuses people coming from other tools, and it exists for a practical reason: you often change several things at once, but want to record them as separate, understandable commits. Staging lets you choose exactly what goes into the next commit — a whole file, or with git add -p just some of its changes — while the rest stays in your working directory. A commit is then a snapshot of the staged content, not of your working directory. That is why editing a file after git add and then committing records the version you staged, not the latest one; git status shows the file as both “staged” and “modified” in that situation.
Every commit also records the ID of its parent commit, which is how history forms a chain. A branch is just a named pointer to one commit that moves forward when you commit; creating a branch costs a few bytes, which is why Git workflows use them freely.
Basic Workflow
Initialize a Repository
mkdir my-project && cd my-project
git init
# → Initialized empty Git repository in .../my-project/.git/
Or clone an existing repo:
git clone https://github.com/user/repo.git
cd repo
Check Status
git status
Always run git status before staging or committing — it shows:
- Untracked files (new files Git doesn’t know about yet)
- Modified files (changed since last commit)
- Staged files (ready to commit)
Stage Changes
# Stage a specific file
git add README.md
# Stage multiple files
git add src/main.py tests/test_main.py
# Stage all changes in the current directory
git add .
# Interactive staging (choose hunks)
git add -p
git add . stages everything under the current directory, including files you did not mean to commit: an .env with API keys, a 200 MB log file, an IDE’s local settings. Secrets are the dangerous case — once pushed, a key must be treated as leaked even if you delete it in the next commit, because it remains in history and automated scanners watch public repositories for exactly this. A .gitignore written before the first git add . (see below), and a habit of reading git status output before committing, prevent most of these accidents.
Commit
git commit -m "Add README with project overview"
Good commit message formula:
<type>: <short summary> (50 chars max)
<optional body — explain WHY, not what>
Types: feat, fix, docs, refactor, test, chore
# Examples
git commit -m "feat: add user registration endpoint"
git commit -m "fix: prevent duplicate email on signup"
git commit -m "docs: update API authentication section"
The type: prefix follows the Conventional Commits convention; it is optional, but some teams use it to generate changelogs automatically. What matters more is the summary line. Write it as what the commit does when applied (“fix: prevent duplicate email on signup”), not what you did (“fixed stuff”, “WIP”), because you will read these lines months later in git log --oneline, in git blame, and when deciding which commit to revert. Running git commit without -m opens your configured editor, which is the easier way to write a multi-line message with a body explaining why.
View History
# Full log
git log
# One line per commit
git log --oneline
# With graph (useful for branches)
git log --oneline --graph --all
# Show changes in last 3 commits
git log -p -3
Branching
Branches let you work on features or fixes without touching the main codebase.
# Create and switch to a new branch
git checkout -b feat/user-profile
# Modern syntax (Git 2.23+)
git switch -c feat/user-profile
# List all branches
git branch -a
# Switch to an existing branch
git switch main
# Delete a merged branch
git branch -d feat/user-profile
# Force delete (even if unmerged)
git branch -D feat/user-profile
Typical Feature Branch Workflow
# 1. Start from an up-to-date main
git switch main
git pull
# 2. Create a feature branch
git switch -c feat/42-add-search
# 3. Make changes and commit
git add src/search.py
git commit -m "feat: add full-text search with SQLite FTS5"
# 4. Merge back into main
git switch main
git merge feat/42-add-search
# 5. Clean up
git branch -d feat/42-add-search
If main did not move while you worked on the branch, step 4 is a fast-forward: Git just moves the main pointer forward to your last commit, and no merge commit is created. If main gained other commits meanwhile, Git creates a merge commit with two parents. On a team, step 4 is usually replaced by pushing the branch and opening a pull request, so the merge happens on GitHub after review and CI.
git switch requires a clean enough working directory: if you have uncommitted changes that would be overwritten by the other branch’s version of a file, Git refuses with “error: Your local changes to the following files would be overwritten by checkout”. Commit them, stash them (see below), or discard them first. Changes that do not conflict simply come along with you to the other branch, which surprises beginners who expect each branch to have its own uncommitted state.
Merging & Resolving Conflicts
git merge feat/42-add-search
If Git can merge automatically, it creates a merge commit. If two branches changed the same lines, you get a conflict:
<<<<<<< HEAD (main)
def search(query):
return []
=======
def search(query, limit=10):
return db.query(query, limit)
>>>>>>> feat/42-add-search
To resolve:
- Edit the file — keep what you want, delete the markers
git add <file>git commit
The part between <<<<<<< HEAD and ======= is the version on the branch you are on; the part after ======= is the incoming branch. Resolving is not about choosing a side mechanically — in this example the right answer is probably the new signature from the feature branch, but you need to check whether other code on main calls search(query) and still works. After editing, search the file for leftover <<<<<<< markers; committing them is a common beginner mistake and breaks the build. If a merge gets confusing, git merge --abort returns everything to the state before the merge started, so you can try again calmly.
VS Code makes this easier
Open the conflicted file in VS Code and use the “Accept Current / Accept Incoming / Accept Both” buttons in the diff view.
Undoing Changes
# Unstage a file (keep changes in working directory)
git restore --staged README.md
# Discard changes in working directory (IRREVERSIBLE)
git restore README.md
# Undo the last commit, keep changes staged
git reset --soft HEAD~1
# Undo the last commit, keep changes in working directory
git reset --mixed HEAD~1
# Create a new commit that reverses a previous commit (safe for shared branches)
git revert HEAD
# Show what changed in the last commit
git show HEAD
The dividing line is whether the commit has been pushed. reset moves the branch pointer backwards and effectively deletes commits from the branch; that is fine for commits only you have, but if others already pulled them, your history and theirs diverge and the next push is rejected (or, with --force, erases their base). revert adds a new commit that undoes the old one, which is safe on shared branches because it only adds history.
git reset --hard and git restore <file> discard uncommitted work, and Git cannot bring that back. Committed work is much harder to lose than people fear: even after a reset --hard, git reflog lists every position HEAD has recently been at, and git reset --hard <hash-from-reflog> restores a “lost” commit. Knowing about the reflog is the single most reassuring thing to learn early.
Remote Repositories (GitHub)
Push an Existing Repo to GitHub
# 1. Create a repo on github.com (no README, no .gitignore)
# 2. Connect your local repo
git remote add origin https://github.com/you/my-project.git
# 3. Push and set upstream
git push -u origin main
# After the first push, just:
git push
-u (short for --set-upstream) links your local main to origin/main, so later a bare git push or git pull knows where to go. If your local branch is still called master (older Git versions defaulted to it), git push -u origin main fails with “error: src refspec main does not match any” — rename with git branch -M main first. The “no README” advice on step 1 matters too: if GitHub creates an initial commit, your first push is rejected because the two histories are unrelated, and you have to pull and merge it first.
GitHub no longer accepts account passwords for Git over HTTPS; a password prompt followed by “remote: Support for password authentication was removed” means you need a personal access token or, more conveniently, an SSH key or the GitHub CLI (gh auth login), which configures a credential helper for you.
Pull Updates
# Fetch + merge in one step
git pull
# Fetch only (inspect before merging)
git fetch
git log origin/main # see what's new
git merge origin/main
When both you and someone else committed to the same branch, git pull has to combine two lines of history. Recent Git versions refuse to guess how: you see “fatal: Need to specify how to reconcile divergent branches” and a hint listing pull.rebase. Pick a default once — git config --global pull.rebase false (merge, the traditional behavior), true (replay your local commits on top of the remote ones, for a linear history), or pull.ff only (only fast-forward, and fail otherwise so you decide manually). Many teams prefer rebase for pulls on feature branches; the important thing is that the behavior is chosen rather than accidental.
.gitignore
Create a .gitignore file at your repo root to exclude files from tracking:
# Python
__pycache__/
*.pyc
.env
venv/
.venv/
# Node
node_modules/
dist/
.next/
# Editors
.DS_Store
.idea/
.vscode/settings.json
# Build artifacts
*.log
*.tmp
Patterns ending in / match directories only; a leading / anchors a pattern to the repository root; !pattern re-includes something an earlier rule excluded. GitHub’s github/gitignore repository has maintained templates per language and framework, which are a better starting point than writing one from memory. Commit the .gitignore itself, so the rules apply to everyone who clones the project.
Common Troubleshooting
I committed to the wrong branch
# Move last commit to a new branch
git branch feat/new-branch # create branch pointing at current commit
git reset --hard HEAD~1 # remove commit from current branch
git switch feat/new-branch # go to the new branch
This works because the new branch keeps the commit alive before reset removes it from the current branch. Two cautions: --hard also throws away any uncommitted changes in your working directory, so commit or stash them first; and if the wrong branch was already pushed, use git revert there instead of reset, for the reasons explained in “Undoing Changes”.
I want to change the last commit message
git commit --amend -m "New message"
# ⚠️ Only do this if you haven't pushed yet
I accidentally deleted a file
git checkout HEAD -- deleted-file.txt
I need to temporarily set aside work
# Stash uncommitted changes
git stash
# Get them back
git stash pop
# List stashes
git stash list
git stash by default saves only changes to tracked files; new files you have not added yet stay in the working directory. Use git stash push -u -m "description" to include untracked files and label the entry — after a few unlabeled stashes, stash list becomes a list of indistinguishable “WIP on main” lines. pop applies and deletes the entry, but if applying causes a conflict, the entry is kept, so check stash list before assuming it is gone. For anything you will leave for more than a few minutes, a temporary commit on a branch is safer than a stash.
Essential Command Reference
| Command | What it does |
|---|---|
git init | Initialize a new repository |
git clone <url> | Clone a remote repository |
git status | Show working tree status |
git add <file> | Stage a file |
git commit -m "msg" | Create a commit |
git log --oneline | Show compact commit history |
git switch -c <name> | Create + switch to branch |
git merge <branch> | Merge a branch into current |
git push | Push to remote |
git pull | Pull from remote |
git stash | Temporarily shelve changes |
git revert HEAD | Safely undo last commit |
Next Steps
- Branching strategies: Learn Git Flow or GitHub Flow for team collaboration
- Rebase:
git rebasefor a cleaner history - GitHub Actions: Automate CI/CD on push or pull request
Related Articles
Frequently Asked Questions (FAQ)
Q. I added a file to .gitignore, but Git still shows changes to it. Why?
A. .gitignore only stops untracked files from being added; a file that was already committed stays tracked no matter what the ignore rules say. Run git rm --cached <file> (or git rm -r --cached <dir>) to remove it from the index while keeping it on disk, then commit that change. If the file contained a secret, removing it this way does not erase it from earlier commits, so rotate the secret as well.