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:

  1. Install and configure Git
  2. Stage and commit changes
  3. Create and switch branches
  4. 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)
TermWhat it is
Repository (repo)The .git folder that stores all history
Working directoryThe files you see and edit
Staging area (index)A “draft” of your next commit
CommitA snapshot of your staged changes with a message
BranchA movable pointer to a sequence of commits
RemoteA 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:

  1. Edit the file — keep what you want, delete the markers
  2. git add <file>
  3. 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

CommandWhat it does
git initInitialize a new repository
git clone <url>Clone a remote repository
git statusShow working tree status
git add <file>Stage a file
git commit -m "msg"Create a commit
git log --onelineShow compact commit history
git switch -c <name>Create + switch to branch
git merge <branch>Merge a branch into current
git pushPush to remote
git pullPull from remote
git stashTemporarily shelve changes
git revert HEADSafely undo last commit

Next Steps

  • Branching strategies: Learn Git Flow or GitHub Flow for team collaboration
  • Rebase: git rebase for a cleaner history
  • GitHub Actions: Automate CI/CD on push or pull request

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.