Handbooks / Git / Chapter 1

Git Basics: Commit, Branch, Push

49 pages · ~68 min✓ Reviewed

Start here. Next up: Stash & Worktrees.

Part 1 · Git Basics: Commit, Branch, Push

Git Basics: Save, Branch and Share Your Work

Every project changes over time. You fix a bug, try a new idea, rename a file, and a week later you cannot remember what the code looked like when it last worked. Git is a tool that records those changes for you, so you can look back at any earlier state, see exactly what changed and why, and try risky ideas without fear of breaking what already works. It is the standard way software teams keep track of their work, which makes it one of the first tools worth learning properly.

This chapter builds Git up from the ground. You will see how Git thinks about your files through its three areas, the working tree, the staging area and the repository, and how a commit turns a set of edits into a permanent snapshot. From there you will read your history with git status, git log and git diff, split work into branches so experiments never disturb your main line, and share everything with other people through a remote such as GitHub.

By the end you will be able to start or clone a repository, make clean commits with clear messages, move between branches, push and pull your work, keep unwanted files out with .gitignore, and undo a mistake without panic. The last pages collect the errors beginners hit most often and a cheat sheet you can keep beside you.

Before you start

You need Git installed (check with git --version in a terminal) and a place to type commands, such as Terminal on macOS and Linux or Git Bash and PowerShell on Windows. Set your identity once with git config --global user.name "Your Name" and git config --global user.email "you@example.com". A free GitHub account is needed only for the sharing section. No programming experience is required, just comfort opening a terminal and moving between folders.

Part 2 · Version Control and Why Git

What version control gives you

Every project changes over time. You fix a bug, rewrite a paragraph, try a risky idea, then wish you could undo it. Version control is a system that records every change to your files, along with who made it and when. Because each change is recorded, you can return to any earlier state of the project.

Think of it as an unlimited undo button that lasts for the whole life of the project, not just the current editing session. It also lets you ask questions such as what did this file look like last month? and who changed this line, and why?

Life without it

Most people start by copying things by hand. You end up with a folder full of report_final.docx, report_final_v2.docx and report_final_v2_REAL.zip. It works for a day or two, then it falls apart: you cannot tell which copy is newest, what differs between two copies, or which one holds the fix you made last Tuesday.

Copying folders by handVersion control
Finding an old stateGuess from file names and datesPick any recorded change from the history
What changedOpen both copies and compare by eyeThe tool shows the exact lines that differ
Who and whyUnknown unless you wrote it downEvery change carries an author, a date and a message
Working with othersEmail zips and merge by hand, overwriting each other's editsCombine edits safely and share them through one history
Disk useWhole project duplicated for every copyOnly recorded changes are stored
Common mistake

Treating a backup folder as version control. Copies tell you nothing about what changed or why, and nothing stops two people from silently overwriting each other's work.

Distributed, with snapshots

Git is a distributed version control system. Older systems kept the history on one central server, and you needed a connection to it for almost everything. With Git, when you copy a project (a clone), you get the full history too, not just the latest files. Every clone is a complete repository.

Central server modelGit (distributed)
Where history livesOnly on the serverOn every clone
Viewing history or comparing versionsNeeds the serverWorks offline
Committing a changeNeeds the serverWorks offline, saved on your machine
If the server is lostHistory may be goneAny clone can restore it

In practice, committing, browsing history, comparing versions and creating branches all work on a plane with no Wi-Fi. You only need a network when you want to exchange changes with someone else, such as pushing to a hosting service.

Snapshots, not diffs

Many people assume a version control tool stores a list of file-by-file differences. Git thinks differently. Each time you commit, Git records a snapshot of the whole project as it looks at that moment. Files that did not change are not copied again; Git just points to the identical stored content. Each snapshot also points back to the one before it, which forms the history.

History is a line of snapshots
  1. 1Commit 1Snapshot: index.html, style.css
  2. 2Commit 2Snapshot: style.css changed, index.html reused
  3. 3Commit 3Snapshot: new file app.js added

Because every commit is a complete picture, going back to an old state means asking for that picture, with no need to replay a long chain of differences. Git can still show you the differences between two snapshots whenever you ask.

Remember

Distributed means every clone has the full history. Snapshots mean every commit is a complete picture of the project.

Git, GitHub and first-time setup

People often say Git and GitHub as if they were one thing. They are not. Git is the tool you run on your own computer. GitHub, GitLab and Bitbucket are hosting services built around Git: websites that store a copy of your repository online and add sharing and review features on top.

GitGitHub, GitLab, Bitbucket
What it isA program on your computerWebsites that host Git repositories
Needs internetOnly to exchange changesYes
Main jobRecord and manage historyShare, back up and collaborate
Required to use GitYesNo, optional
Common mistake

Thinking you need a GitHub account to use Git. Git works fine on its own; a hosting service is just one place to share the result.

Check the install and set your identity

Before your first commit, confirm Git is installed by asking for its version. Any version number means it is ready; an error such as command not found means you must install it first.

bash
git --version

Your exact version number will differ

output
git version 2.45.1

Every commit is stamped with a name and an email address, so Git needs to know yours. You set them once with --global, which saves them for every repository on this computer. Use the same email you plan to use on your hosting service so your commits are linked to your account.

bash
git config --global user.name "Asha Rao"
git config --global user.email "asha@example.com"
git config --global user.name

The last line reads the name back to confirm it was saved

output
Asha Rao

Part 3 · init and clone: Starting a Repository

git init: Turning a Folder into a Repository

Git does not watch every folder on your computer. A folder only becomes a Git project after you tell Git to start tracking it. The command that does this for a brand new project is git init.

Run git init inside a folder and Git creates a hidden folder named .git right there. That one folder is what turns an ordinary directory into a repository. Your existing files are not touched, and nothing is saved yet. Git has only prepared a place to keep history.

bash
mkdir notes
cd notes
git init
ls -a

Start a new project from scratch

output
Initialized empty Git repository in /home/sam/notes/.git/
.  ..  .git

The ls -a flag shows hidden entries, so you can see .git sitting next to . and ... A normal ls hides it because the name starts with a dot.

Check where you are first

Before running git init, type pwd (or look at your prompt) to confirm which folder you are in. Git will happily initialise whatever folder you are standing in.

git clone: Joining an Existing Project

Often the project already exists somewhere else, for example on GitHub. Instead of starting empty, you copy it with git clone <url>. Clone downloads the files and also the full history: every commit ever made, so you can browse the past offline.

Clone also records where the copy came from. It sets up a remote named origin that points at the URL you cloned from. Later, when you share work, origin is the name you will use to talk to that server.

bash
git clone https://github.com/example/recipes.git
cd recipes
git remote -v

Copy a project and check the remote

output
origin  https://github.com/example/recipes.git (fetch)
origin  https://github.com/example/recipes.git (push)

By default Git names the new folder after the repository, here recipes. If you want a different folder name, add it after the URL.

bash
git clone https://github.com/example/recipes.git my-recipes
cd my-recipes

Clone into a folder name you choose

The contents are identical. Only the local folder name changes, and origin still points at the same URL.

What git clone does
  1. 1Downloadfiles and full history
  2. 2Create folderrepo name or your own
  3. 3Add .githistory and config inside
  4. 4Set originpoints at the URL

Choosing, and What .git Really Is

The two commands answer different situations. The question to ask is whether the project already exists somewhere.

git initgit clone <url>
Use it whenYou are starting a new local projectYou are joining an existing project
Starting historyEmptyComplete copy of the existing history
Remote originNone until you add oneSet automatically
ResultA new .git folder in the current folderA new folder containing files and .git
Which command do I need?

Everything Git knows about your project lives in the .git folder: all the history and the repository settings, including the remote. Your visible files are just the current snapshot sitting beside it. This explains a surprising fact: if you delete .git, the repository is gone, history and all, but your files stay exactly as they were. They simply become an ordinary folder again.

Mistake: git init inside another repository

If you run git init in a folder that already sits inside a repository, you create a second, nested repo. The outer project then treats that folder as something odd, and commits go to the inner repo instead of the one you expected. Run git status first; if Git already reports a repository, you do not need init.

Mistake: git init in your home folder

Running git init in your home folder makes Git responsible for everything under it: documents, downloads, other projects. Status becomes huge and slow, and you may track files you never meant to. Always cd into a project folder first. If it happened, deleting the .git folder in your home directory undoes it without touching your files.

Part 4 · Working Tree, Staging Area and Repository

Three places your work lives

Git does not simply save whatever is on your disk. It keeps your work in three separate places, and a change moves through them one step at a time. Once you know these places, most of Git's commands stop feeling mysterious.

The working tree

The working tree is the folder of files you see and edit every day. When you open notes.txt in your editor and change a line, you are changing the working tree. Git notices the change, but it has not saved anything yet.

The staging area

The staging area, also called the index, is a holding area. It lists exactly what the next commit will contain. Think of it as packing a box before you seal it: you choose what goes in, and only those items are shipped.

The repository

The repository is the hidden .git folder inside your project. It stores the full history of commits, and every commit is a permanent snapshot you can return to later. Delete .git and the project stays on disk, but the history is gone.

PlaceWhere it livesWhat it holdsYou change it with
Working treeThe project folderThe files you editYour editor
Staging areaInside .gitA list of what goes in the next commitgit add
RepositoryInside .gitCommitted snapshots, the historygit commit

How a change travels

A change always moves in the same direction. You edit a file, you choose it with git add, and you save it with git commit. Each command is one step of the journey.

Here is the whole trip in a terminal. We create a file, stage it, then commit it. The git status --short command prints a two-letter code for each file, and A means the file is staged as a new addition.

output
$ echo "hello" > notes.txt
$ git add notes.txt
$ git status --short
A  notes.txt
$ git commit -m "Add notes file"
[main (root-commit) 3f2a1c9] Add notes file
 1 file changed, 1 insertion(+)
 create mode 100644 notes.txt
$ git status --short

After the commit, status prints nothing: all three places agree.

Common mistake: thinking git add commits

git add only stages. Nothing is saved to history until you run git commit. If you add a file and then lose your work, the staged copy is not a safe backup of your history.

Staging gives you control

You might wonder why Git has a staging step at all. Why not commit everything at once? The reason is that real work is messy. In one afternoon you might fix a bug, tidy a comment and start a new feature. Staging lets you split those unrelated edits into separate, focused commits.

Suppose you edited app.py to fix a bug and README.md to correct a typo. You can stage and commit them one at a time, so the history tells a clear story, and each commit can be understood or undone on its own.

output
$ git add app.py
$ git commit -m "Fix crash when input is empty"
$ git add README.md
$ git commit -m "Correct typo in install steps"
$ git log --oneline
a81c4d2 Correct typo in install steps
5be90f7 Fix crash when input is empty

Two edits, two commits, each with one clear purpose.

Remember

The working tree is what you edit, the staging area is what you picked, and the repository is what you saved.

File states and taking files back out of staging

At any moment, each file in your project is in one of four states. Git tracks which state a file is in, and git status shows it to you. When you are unsure what is going on, run git status first.

StateMeaningHow it appears in git status
UntrackedGit has never been told about this fileListed under Untracked files
ModifiedTracked, and changed since the last commitListed under Changes not staged for commit
StagedMarked to go into the next commitListed under Changes to be committed
CommittedSafely stored, no changes sinceNot listed at all

A file can loop around this cycle many times. Edit a committed file and it becomes modified, stage it and it is staged, commit it and it is committed again.

Unstaging a file

If you staged a file by accident, you can take it back out with git restore --staged <file>. The file moves from staging back to modified or untracked, but your edits stay exactly as they were. Nothing is lost.

output
$ git add app.py
$ git status --short
M  app.py
$ git restore --staged app.py
$ git status --short
 M app.py

The first column is staging, the second is the working tree. The edit is still there, only unstaged.

Reading the two columns

In short status, a letter in the left column means staged and a letter in the right column means changed but not staged. A new untracked file shows as ??.

Part 5 · add and commit

Choosing what goes into a commit

Editing a file does not save it into Git's history. A commit is a deliberate step in two moves: first you pick which changes belong together with git add, then you record them with git commit. The first move puts them in the staging area, a waiting room for the next snapshot.

The simplest form names the file you want. git add notes.txt stages that one file and leaves every other changed file alone. If you want everything that changed, git add . stages every change in the current folder and all folders below it. The dot means "here".

bash
git add notes.txt
git add .
git status --short

Stage one file, then everything under the current folder

output
A  notes.txt
A  src/app.py
A  src/util.py
CommandWhat it stagesUse it when
git add notes.txtOnly that fileYou want a small, focused commit
git add src/Everything inside that folderOne folder holds the whole change
git add .All changes in the current folder treeEverything you touched belongs together
git add -pChosen pieces of filesOne file holds two unrelated changes
Look before you stage

Run git status before git add .. It is the quickest way to notice a stray file, such as a scratch note, that you did not mean to include.

Staging only part of a file

Sometimes one file holds two unrelated edits: a bug fix and a half-finished experiment. Committing both makes history hard to read and hard to undo. git add -p (the p is for patch) walks you through the file one hunk at a time. A hunk is a contiguous block of changed lines. For each hunk you answer a question: stage it or skip it.

bash
git add -p app.py

Git shows each hunk and waits for your answer

output
@@ -10,6 +10,7 @@ def total(items):
     result = 0
     for item in items:
-        result += item.price
+        result += item.price * item.qty
     return result
(1/2) Stage this hunk [y,n,q,a,d,s,e,?]? y

@@ -30,3 +31,4 @@ def debug():
+    print('experiment')
(2/2) Stage this hunk [y,n,q,a,d,s,e,?]? n
KeyMeaning
yStage this hunk
nSkip this hunk for now
sSplit the hunk into smaller hunks
qQuit; hunks already answered with y stay staged

After this session the price fix is staged and the debug print is not. The file is now in two states at once: the staged part will go into the next commit, and the rest stays as an ordinary edit in your working tree. You can confirm this with git status, which lists the same file under both "Changes to be committed" and "Changes not staged".

Making the commit

Once the right changes are staged, git commit -m "message" saves them. Git stores a snapshot of everything that is staged, together with your author name and email, the time, and the message you wrote. The -m flag lets you give the message right on the command line.

bash
git commit -m "Fix item total to use quantity"

Saves the staged snapshot

output
[main 3f9a1c2] Fix item total to use quantity
 1 file changed, 1 insertion(+), 1 deletion(-)

The 3f9a1c2 in that output is the start of the commit's ID. Every commit gets a unique 40-character SHA-1 hash computed from its contents, its message, its author and time, and its parent. Git shows only the first seven characters by default, which is enough to tell commits apart in a small project.

Each commit also records which commit came right before it, called its parent. That link is what turns separate snapshots into a history: starting from the newest commit, Git can follow parent after parent back to the first one.

Each commit points back to its parent
  1. 1a41b7e0Add notes.txt
  2. 2c82d5f3Add src/app.py
  3. 33f9a1c2Fix item total (newest)

Because the hash depends on the parent as well, changing an old commit would change its hash and every hash after it. This is why history is treated as something you add to rather than edit.

Editor messages, empty commits and amending

If you run git commit without -m, Git opens your configured text editor with a template. Type the message on the first line, save and close the file, and the commit is made. Lines starting with # are comments and are ignored. If you close the editor with an empty message, Git cancels the commit. This is the comfortable way to write a message with a subject line and a longer explanation.

Git also refuses to commit when the staging area is empty. It will not create a snapshot with nothing new in it. The message tells you whether there is nothing changed at all, or whether you changed files but forgot to stage them.

bash
git commit -m "Tweak app"

Edited app.py but never ran git add

output
On branch main
Changes not staged for commit:
  modified:   app.py

no changes added to commit (use "git add" and/or "git commit -a")

The fix is to stage first, then commit. If instead the output says nothing to commit, working tree clean, there is simply nothing new to save.

Now suppose you committed and then spotted a typo in the message, or realised a file was missing. git commit --amend replaces the latest commit with a new one. Stage anything forgotten first, then amend. Adding --no-edit keeps the old message.

bash
git add forgotten.py
git commit --amend --no-edit

Fold a forgotten file into the last commit

output
[main 9d04e7b] Fix item total to use quantity
 Date: Fri Oct 9 20:10:00 2026 +0530
 2 files changed, 5 insertions(+), 1 deletion(-)

Notice the hash changed from 3f9a1c2 to 9d04e7b. Amend does not edit the old commit; it builds a new one and moves your branch onto it. That is harmless on your own machine, but it matters once the commit has been shared.

Should I amend or make a new commit?
Common mistake: amending a pushed commit

After you push, teammates may already have the old commit. An amended commit has a different hash, so Git rejects your next push as non-fast-forward. The only way through is a force push, which rewrites shared history and can erase or conflict with their work. Fix a pushed mistake with a new commit instead.

Part 6 · Writing Good Commit Messages

The shape of a message

Every commit carries a message, and months from now that message is the only clue to why the change exists. Git does not care what you write, but your teammates and your future self will. A good message has a fixed shape: a short subject line, then a blank line, then an optional body.

The subject is written in the imperative mood, as if giving an order to the codebase: "Fix login redirect", not "Fixed login redirect" or "Fixes login redirect". A handy test is to complete the sentence "If applied, this commit will ...". Keep it to about 50 characters so it fits in one line of git log --oneline, and leave off the trailing period.

bash
git commit -m "Fix login redirect"

A short subject is often all a small change needs

When the change needs explaining, leave a blank line after the subject and write a body. The body should answer why the change was made, because the diff already shows what changed. Without the blank line, Git tools treat the whole text as one long subject.

Anatomy of a commit message
  1. 1SubjectImperative, about 50 characters, no period
  2. 2Blank lineSeparates subject from body
  3. 3Body (optional)Explains why, not what

Running git commit with no -m opens your editor, which is the easiest way to write a multi-line message. Here is what one looks like:

text
Fix crash when cart is empty

Checkout read the first item without checking the list, so an
empty cart raised an IndexError. Return early and show the
"Your cart is empty" page instead.

Subject, blank line, then a body about the reason

PartRuleExample
MoodImperative, like a commandAdd retry to upload
LengthAbout 50 charactersFix login redirect
EndingNo trailing periodFix login redirect
BodyOptional, explains whyWhy the old behavior was wrong

Good, bad and one change at a time

A message is only useful if it says something specific. Messages like "stuff", "fix" or "changes" could describe any commit ever made, so they tell the reader nothing. Compare them with messages that name the problem or the feature.

MessageVerdictWhy
stuffBadSays nothing about the change
fixBadFix what? Where?
changesBadEvery commit contains changes
Fix crash when cart is emptyGoodNames the bug and the situation
Add password reset emailGoodNames the feature

Good messages are easier to write when each commit holds one logical change. If you fixed a bug and also renamed a variable and also tweaked a color, no single short message can describe all three honestly. You end up with a vague message such as "updates", or a long one joined with "and". Splitting the work into separate commits keeps every message short and true.

Is this commit ready?

The staging area from earlier sections is what makes this possible. Use git add on just the files, or the parts of files, that belong together, commit them, then stage the next group.

bash
git add cart.py
git commit -m "Fix crash when cart is empty"
git add theme.css
git commit -m "Darken the checkout button"

Two unrelated edits become two honest commits

Prefixes and a readable history

Some teams add a short label to the front of the subject, following the Conventional Commits style. The prefix tells you the kind of change at a glance, so scanning a long history is much faster. Use it only if your team has agreed on it; otherwise plain imperative subjects are perfectly fine.

PrefixMeaningExample
feat:A new featurefeat: add password reset email
fix:A bug fixfix: handle empty cart at checkout
docs:Documentation onlydocs: explain how to run tests
refactor:Restructure without changing behaviorrefactor: split cart totals into a function
chore:Upkeep such as toolingchore: update dependencies

Small, focused commits pay off whenever you read the history. Each line of git log --oneline becomes a clear step in the story of the project:

bash
git log --oneline
output
c3d4e5f docs: explain how to run tests
b2c3d4e fix: handle empty cart at checkout
a1b2c3d feat: add password reset email

They also make undoing safe. If the empty-cart fix turns out to be wrong, you can revert exactly that commit and nothing else. With a giant commit, reverting would throw away unrelated work along with the mistake.

bash
git revert b2c3d4e

Creates a new commit that undoes only that one change

Mistakes to avoid

Huge commits that mix everything

One commit that contains a refactor, a new feature and a pile of formatting changes cannot have an honest message. Reviewers cannot see which lines matter, and a revert removes all of it at once. Do the refactor, the feature and the formatting as three separate commits.

Secrets and debug prints the message never mentions

It is easy to run git add . and sweep in a .env file, an API key or a leftover print("here"). If the message says "Fix login redirect" but the commit also contains a password, nobody reading it will know to look. Check git status and git diff --staged before every commit. A secret that reaches history stays there even after you delete it in a later commit, so treat it as leaked and replace the key.

A quick routine before you commit keeps both mistakes away:

  • Run git diff --staged and read every line you are about to record.
  • Look for keys, passwords and debug prints that do not belong.
  • Ask whether one short subject describes it all; if not, split the commit.
  • Write the subject in the imperative, about 50 characters, with no period.
Remember

A good commit is one logical change with a short imperative subject, and a body only when the reason needs explaining. Do that every time and your history stays readable and easy to revert.

Part 7 · status, log and diff

git status: Where Things Stand

Git gives you three commands for looking at your work without changing anything. The first is git status. It answers one question: what is the state of my project right now? It compares the three areas you met earlier (working tree, staging area and repository) and reports the differences.

The report has four parts. It names the branch you are on, lists the staged changes that will go into the next commit, lists the unstaged changes (files Git tracks that you edited but did not stage), and lists untracked files that Git has never been told about.

bash
git status

Run it from anywhere inside the repository

output
On branch main
Changes to be committed:
  (use "git restore --staged <file>" to unstage)
	modified:   app.py

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
	modified:   README.md

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	notes.txt

Read it from top to bottom. Here app.py is staged and ready, README.md was edited but not staged, and notes.txt is brand new to Git. If all three lists are empty, Git prints nothing to commit, working tree clean.

Staged
Section
Changes to be committed
app.py
Unstaged
Section
Changes not staged
README.md
Untracked
Section
Untracked files
notes.txt

The long form is friendly but takes many lines. Once you know the layout, git status -s (short for --short) prints one line per file in two columns. The left column is the staging area compared with the last commit. The right column is the working tree compared with the staging area.

bash
git status -s
output
M  app.py
 M README.md
?? notes.txt
Short codeMeaning
M (letter on the left)Modified and staged
M (letter on the right)Modified but not staged
MMStaged, then edited again after staging
A New file that is staged
??Untracked file
Which column is which?

Left means ready to commit, right means still only in your working tree. A line with a letter in the left column is already staged.

git log: Reading History

git status looks at the present. git log looks at the past. It lists the commits reachable from where you are, newest first. Each entry shows the full commit hash (the unique ID), the author, the date and the message.

bash
git log
output
commit 7c3e1fa94b0d52e8a6f1c3d7b9e04a2f58d61b3c (HEAD -> main)
Author: Asha Rao <asha@example.com>
Date:   Mon Oct 5 10:12:44 2026 +0530

    Add greeting to home page

commit 1b9d604e7a35c8f20d94b6a1e3c7f58092ad4e61
Author: Asha Rao <asha@example.com>
Date:   Mon Oct 5 09:30:02 2026 +0530

    Initial commit

The text in brackets after the first hash is a label. HEAD -> main means your checkout currently sits on the main branch at that commit. For a long history, git log opens in a pager: press q to leave it.

Full entries use a lot of space. Add --oneline to squeeze each commit onto one line with a short hash and the message. Add --graph to draw the branch shape with ASCII lines, and --all to include every branch rather than only the one you are on.

bash
git log --oneline --graph --all
output
* 5e8f0d2 (feature/login) Add login form
| * a41c9b7 (HEAD -> main) Fix typo in README
|/
* 7c3e1fa Add greeting to home page
* 1b9d604 Initial commit

This picture shows two lines of work that split after 7c3e1fa. The * marks a commit, and the | and / characters show where lines run side by side and where they join. You will use this command constantly once branches enter the picture.

FlagWhat it changes
--onelineOne line per commit with a short hash
--graphDraws the branch and merge shape
--allShows every branch, not just the current one

git diff and git show: Seeing the Actual Changes

Status tells you which files changed, but not what changed inside them. git diff shows the lines themselves. Run on its own, it compares your working tree against the staging area, so it shows only changes you have not staged yet.

bash
git diff
output
diff --git a/README.md b/README.md
index 3b18e51..c7a9f20 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,3 @@
 # Demo
-A small app.
+A small app for learning Git.
+Run it with python app.py.

Lines starting with - were removed and lines starting with + were added. The @@ line tells you where in the file the change sits. The --- and +++ lines name the old and new versions.

Once you run git add, that change leaves the plain git diff output, because it is no longer unstaged. To review what is about to be committed, use git diff --staged (the older spelling --cached does the same). It compares the staging area against the last commit.

bash
git add app.py
git diff --staged
output
diff --git a/app.py b/app.py
index 91d4a3c..e20b7f4 100644
--- a/app.py
+++ b/app.py
@@ -1 +1,2 @@
 print("hello")
+print("welcome back")
CommandComparesAnswers
git diffWorking tree vs staging areaWhat have I changed but not staged?
git diff --stagedStaging area vs last commitWhat will my next commit contain?
git diff main..featureTwo branches or commitsHow does one line of work differ from the other?

The third form takes two references. git diff main..feature shows what you would need to change on main to arrive at the feature branch. Hashes work too, so git diff 1b9d604..7c3e1fa compares two commits.

bash
git diff main..feature/login
output
diff --git a/login.py b/login.py
new file mode 100644
index 0000000..4f6a2d8
--- /dev/null
+++ b/login.py
@@ -0,0 +1,2 @@
+def login(user):
+    return user.is_active

To inspect a single commit from history, use git show <hash>. It prints the commit header (author, date, message) followed by the diff that commit introduced. A short hash from --oneline is enough, as long as it is unique.

bash
git show 7c3e1fa
output
commit 7c3e1fa94b0d52e8a6f1c3d7b9e04a2f58d61b3c
Author: Asha Rao <asha@example.com>
Date:   Mon Oct 5 10:12:44 2026 +0530

    Add greeting to home page

diff --git a/app.py b/app.py
index 5b2c0aa..91d4a3c 100644
--- a/app.py
+++ b/app.py
@@ -1 +1 @@
-print("hi")
+print("hello")
Common mistake: git diff shows nothing

You staged the file with git add, then ran plain git diff and saw empty output. Nothing is wrong. The change is staged, so look at it with git diff --staged.

The Habit Before Every Commit

Each command answers a different question, and together they form a short check you should run before every commit. It takes seconds and catches the most common slips: a forgotten file, a stray debug line, or a change you did not mean to include.

Before you commit
  1. 1git statusRight files staged? Anything untracked?
  2. 2git diff --stagedRead the exact lines going in
  3. 3Fix or restageOnly if something looks wrong
  4. 4git commitWrite a clear message

First git status confirms that the files you expect are in the staged list and that nothing important is left untracked. Then git diff --staged lets you read the precise change. If you spot a leftover print or a password, fix it and stage again before committing, when it is cheap to correct.

bash
git status -s
git diff --staged
git commit -m "Add welcome message"
git log --oneline
output
M  app.py
 M README.md
diff --git a/app.py b/app.py
index 91d4a3c..e20b7f4 100644
--- a/app.py
+++ b/app.py
@@ -1 +1,2 @@
 print("hello")
+print("welcome back")
[main d83f1c9] Add welcome message
 1 file changed, 1 insertion(+)
d83f1c9 (HEAD -> main) Add welcome message
7c3e1fa Add greeting to home page
1b9d604 Initial commit
Common mistake: committing blind

Running git add . and git commit without looking sweeps in everything, including files you did not intend to share. Check git status and git diff --staged first.

Remember

status tells you which files, diff tells you which lines, log tells you what already happened, and show opens one commit. None of them change your project, so run them as often as you like.

Part 8 · Branches: Parallel Lines of Work

What a Branch Really Is

Imagine you want to try a risky redesign of a login form while the rest of the project keeps working. Git's answer is the branch. A branch is a separate line of work, so you can experiment without disturbing the code your teammates, or your future self, rely on.

The surprising part is how cheap this is. A branch is not a copy of your files. It is a lightweight, movable pointer to one commit, stored as a tiny file holding a commit ID. Creating a branch writes one small pointer, so it is instant even in a huge project. When you commit, the branch you are on simply moves forward to the new commit.

Two branches pointing into one history
  1. 1Commit Afirst commit
  2. 2Commit Bmain points here
  3. 3Commit Cfeature points here

Git also needs to know which branch you are working on right now. That is the job of HEAD, a special pointer to the branch (or, rarely, a specific commit) you currently have checked out. When you commit, Git adds the commit to whatever HEAD points at, and that branch moves forward. Other branches stay where they were.

PointerWhat it points toMoves when
mainThe latest commit on mainYou commit while on main, or merge into it
featureThe latest commit on featureYou commit while on feature
HEADThe branch you are onYou switch branches
Remember

Branches are cheap labels on commits. Make one for every task, however small.

Creating and Switching Branches

Running git branch with no arguments lists your local branches and puts a star next to the one HEAD points at. Giving it a name, as in git branch name, creates a new branch at your current commit, but it does not move you onto it.

bash
git branch
git branch login-form
git branch

List, create, list again

output
* main
  login-form
* main

Look at the output: the star is still on main, so creating a branch did not change where you are. To move, use git switch name. Since you nearly always create a branch because you want to work on it, git switch -c name does both in one step: it creates the branch and moves HEAD onto it.

bash
git switch login-form
echo "<form>" > login.html
git add login.html
git commit -m "Add login form markup"
git switch main
ls

Commit on the branch, then go back to main

output
Switched to branch 'login-form'
[login-form 4f2a9c1] Add login form markup
 1 file changed, 1 insertion(+)
 create mode 100644 login.html
Switched to branch 'main'
README.md

Notice that login.html vanished when you returned to main. Git rewrote your working files to match the commit that main points to. Switch back and the file returns. Nothing was lost; the work is safe on the other branch.

CommandWhat it does
git branchLists local branches, marking the current one with a star
git branch nameCreates a branch but stays where you are
git switch nameMoves HEAD to an existing branch
git switch -c nameCreates a branch and moves to it in one step

Merging: Bringing Work Together

When the feature is ready, you bring it back with a merge. The rule to remember is that git merge name combines the named branch into the branch you are on. So you first switch to the branch that should receive the work, usually main, and then merge.

bash
git switch main
git merge login-form

What Git does next depends on the shape of the history. If main has not moved since you branched off, there is nothing to reconcile, and Git simply slides the main pointer forward to the feature's latest commit. This is a fast-forward merge. No new commit is created.

output
Updating 8b1d3e0..4f2a9c1
Fast-forward
 login.html | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 login.html

If both branches gained new commits, the histories have diverged, and a pointer move cannot combine them. Git instead builds a merge commit, a special commit with two parents that joins both lines. Git opens your editor with a default message, which you can normally accept.

What kind of merge will Git do?
Fast-forwardMerge commit
WhenTarget branch has no new commitsBoth branches have new commits
New commitNoneYes, with two parents
History shapeOne straight lineTwo lines joined together

To see the shape for yourself, git log --oneline --graph draws the branches with ASCII lines.

Merge Conflicts

Git merges most changes automatically, because edits in different files or different parts of a file do not collide. A merge conflict happens only when both branches changed the same lines. Git cannot guess which version you want, so it stops, leaves the merge half done and asks you to decide.

output
Auto-merging title.txt
CONFLICT (content): Merge conflict in title.txt
Automatic merge failed; fix conflicts and then commit the result.

Open the named file and you will find both versions wrapped in markers. Everything between <<<<<<< and ======= is your current branch's version, and everything between ======= and >>>>>>> comes from the branch you are merging in.

text
<<<<<<< HEAD
Welcome back!
=======
Hello again, friend
>>>>>>> login-form

The conflicted region in title.txt

Resolving means editing the file into the text you actually want, and deleting all three marker lines. The result might keep one side, the other, or a blend of both. Then you tell Git the conflict is settled by staging the file and committing.

bash
git add title.txt
git commit
Common mistake: leaving markers in the file

If you stage a file that still contains <<<<<<< lines, Git happily commits them and they end up in your program. Search the file for the markers before running git add.

Cleaning Up and Avoiding Two Classic Mistakes

Once a branch has been merged, its pointer has done its job and the commits stay in history. Delete it with git branch -d name. The lowercase -d is the safe option: Git refuses if the branch holds commits that are not merged anywhere, so you cannot lose work by accident.

bash
git branch -d login-form
output
Deleted branch login-form (was 4f2a9c1).
Common mistake: committing on main by accident

You start coding, commit, and only then notice you are on main instead of a feature branch. The commit is not lost, but it is on the wrong line. Check your branch with git branch before you begin, and create the feature branch first with git switch -c name.

If it already happened, the fix is easy because branches are cheap. Create a branch at the current commit, then move main back one commit. The work now lives safely on the new branch. Only do the second command while the commit exists on that new branch, since reset --hard discards changes.

bash
git branch fix-login
git reset --hard HEAD~1
git switch fix-login

Rescue a commit made on main by mistake

Common mistake: switching with conflicting uncommitted changes

Git carries uncommitted edits along when you switch, but if the target branch has a different version of the same file, Git refuses rather than overwrite your work. You will see an error about local changes that would be overwritten by checkout.

output
error: Your local changes to the following files would be overwritten by checkout:
        title.txt
Please commit your changes or stash them before you switch branches.
Aborting

The message gives you the two ways out. Either commit your changes first, or park them with git stash, switch, and later bring them back with git stash pop. Doing neither and forcing the switch would throw the edits away, so avoid that.

Part 9 · Remotes: push, pull and GitHub

Remotes and your first push

So far everything has lived on your own machine. A remote is a named copy of your repository hosted somewhere else, such as GitHub. Remotes give you a backup, a way to share work with teammates, and a place to review changes. By convention the first remote is called origin, but that is only a default name, not something special.

If you started with git clone, origin is already set up for you. If you started with git init, you connect your local repository to an empty repository you created on GitHub using git remote add. You can then list what is configured with git remote -v, which shows each remote's name and URL for fetching and pushing.

bash
git remote add origin https://github.com/you/notes.git
git remote -v

Connect the local repo to GitHub, then check the connection

output
origin  https://github.com/you/notes.git (fetch)
origin  https://github.com/you/notes.git (push)

Adding a remote only records a name and a URL. Nothing is uploaded yet. To upload your commits you use git push. The first time, add -u so Git remembers which remote branch your local branch tracks. That link is called the upstream.

bash
git push -u origin main

Upload main and set its upstream

After that one command, a plain git push or git pull on this branch knows where to go, so you no longer have to type the remote and branch names.

CommandWhat it does
git remote add origin <url>Records a remote called origin pointing at the URL
git remote -vLists remotes with their fetch and push URLs
git push -u origin mainUploads main and sets origin/main as its upstream
git pushUploads the current branch to its upstream
Only once

You need -u only the first time you push a branch. Every later push on that branch can be just git push.

Fetch, pull and rebase

Teammates push too, so the remote can move ahead of your copy. git fetch downloads the new commits and updates your remote-tracking branches such as origin/main, but it does not touch your files or your current branch. It is the safe way to look at what changed before deciding what to do.

git pull is a shortcut for two steps: it runs git fetch and then merges the fetched branch into the branch you are on. Because it changes your files, it can produce a merge or a conflict.

What git pull does
  1. 1git fetchdownload new commits, files untouched
  2. 2git mergecombine origin/main into your branch
  3. 3Updated branchfiles now include teammates' work

When both you and the remote have new commits, a normal pull creates an extra merge commit that joins the two lines of history. If you prefer a straight, linear history, use git pull --rebase. It fetches, then replays your own commits on top of the remote's latest commit instead of merging.

git pullgit pull --rebase
Stepsfetch, then mergefetch, then replay your commits on top
Extra merge commitYes, if both sides have new workNo
History shapeBranches that join back togetherOne straight line
Your commitsKeep their original hashesGet new hashes because they are replayed
bash
git fetch
git log --oneline main..origin/main
git pull --rebase

Look first, then bring the changes in

Fetch is look, pull is look and change

Use git fetch when you only want to see what is new. Use git pull when you want those changes in your current branch.

Rejected pushes and authentication

Sometimes a push fails with a message like rejected ... (non-fast-forward). It means the remote branch has commits that you do not have locally, so Git refuses to overwrite them. Nothing is broken. You just need to bring those commits in first and then push again.

output
! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to 'github.com/you/notes.git'
hint: Updates were rejected because the tip of your current branch is behind
Fixing a rejected push

The other thing that stops beginners is signing in. GitHub no longer accepts your account password for Git operations over HTTPS. Instead you create a personal access token in your GitHub settings and paste it where Git asks for a password. The alternative is SSH, where you generate a key pair, keep the private key on your machine and add the public key to GitHub. After that, no password or token is needed on each push.

HTTPSSSH
URL looks likehttps://github.com/you/notes.gitgit@github.com:you/notes.git
Proves who you are withA personal access tokenA key pair
SetupCreate a token onceGenerate a key and upload the public half
Common mistake

Typing your GitHub password when Git asks for one over HTTPS. It will fail. Use a personal access token, or switch the remote to an SSH URL.

The team workflow and force pushing

On a shared project you rarely push straight to main. Instead you work on a branch, push that branch, and ask for it to be merged through a pull request on GitHub. A pull request is a page where teammates can read your changes, comment and approve before anything reaches main.

bash
git switch -c feat/login
git add .
git commit -m "Add login form"
git push -u origin feat/login

Everything before opening the pull request

Once the pull request is merged, switch back to main and run git pull so your local copy includes the merged work.

Finally, a warning about git push --force. It makes the remote branch match yours exactly, discarding any commits on the remote that you do not have. If a teammate pushed work you have not pulled, a force push erases it from the shared history.

Common mistake

Running git push --force to get past a rejected push. That overwrites your teammates' commits. Pull first instead. If you truly must rewrite a branch you own, for example after a rebase, use git push --force-with-lease, which refuses to push if the remote has commits you have not seen.

--force--force-with-lease
Remote has commits you lackOverwrites themRefuses to push
Safe on a shared branchNoMuch safer, but still use with care

Part 10 · .gitignore: Keeping Files Out

Telling Git what to leave alone

Some files in a project should never become part of its history: downloaded dependencies, log files, compiled output, and files holding passwords. A .gitignore file is a plain text file in your repository that lists patterns for the files Git should not track. Once a pattern matches, the file stops showing up in git status and git add . skips it.

Each line is one pattern. Blank lines are skipped, and a line starting with # is a comment. The three patterns you will write most often target a folder, a file extension, or one specific file.

gitignore
# dependencies
node_modules/

# any log file, in any folder
*.log

# one specific file
.env

A small .gitignore at the root of a web project

PatternWhat it ignoresExample match
node_modules/A whole folder (the trailing slash means directory)node_modules/express/index.js
*.logEvery file ending in .log, at any depthdebug.log, logs/server.log
.envAny file named exactly .env.env, api/.env

Negating and anchoring

Two small symbols give you finer control. A leading ! negates a pattern, which means a file that an earlier line ignored is tracked again. A leading / anchors the pattern to the folder that holds the .gitignore, so it no longer matches in subfolders.

gitignore
*.log
!keep.log

/build

Ignore all logs except keep.log, and ignore only the build folder at the repo root

Here keep.log is tracked even though *.log comes first, because the later !keep.log line wins. Without the slash, build would also ignore docs/build. With /build, only the top-level folder is skipped.

Order matters

Git reads the file top to bottom and the last matching line wins. Put a ! exception after the broad pattern it relaxes. Git also cannot re-include a file if a parent folder is ignored, so ignore *.log rather than logs/ when you want an exception inside it.

Sharing the rules and the tracked-file trap

The .gitignore is a normal file, and you should commit it like any other. Then everyone who clones the project gets the same rules, and nobody on the team accidentally commits node_modules/ or their own log files.

bash
git add .gitignore
git commit -m "Add gitignore for dependencies, logs and env file"

There is one rule that surprises almost every beginner. Ignoring only affects untracked files. If Git is already tracking a file, adding it to .gitignore changes nothing: Git keeps recording its changes. The ignore list is only consulted for files Git has never seen.

Will .gitignore hide this file?

Stop tracking a file but keep it on disk

To fix an already tracked file, tell Git to forget it while leaving your copy alone. The --cached flag limits the removal to the staging area and the repository, so the file stays in your working folder. Then commit the removal, and the ignore rule takes over from there.

Untracking a file you meant to ignore
  1. 1Add the patternput it in .gitignore
  2. 2git rm --cached app.logfile stays on disk
  3. 3git commitrecords the removal
bash
git rm --cached app.log
git commit -m "Stop tracking app.log"
Common mistake

Leaving off --cached. Plain git rm app.log deletes the file from your disk as well. For a folder, add -r, as in git rm -r --cached node_modules.

Checking rules and ignoring junk everywhere

When a file is missing from git status, or surprisingly present, you can ask Git directly. git status --ignored adds a section listing everything currently ignored. When you want to know why, git check-ignore -v <path> prints the exact rule that matched, along with the file and line number it came from.

bash
git status --ignored
git check-ignore -v debug.log
output
Ignored files:
  (use "git add -f <file>..." to include in what will be committed)
        .env
        debug.log
        node_modules/
.gitignore:2:*.log	debug.log

Sample output: the last line reads file:line:pattern, then the path

CommandQuestion it answers
git status --ignoredWhich files is Git skipping right now?
git check-ignore -v <path>Which rule, in which file and line, ignores this path?

A global ignore file for your own machine

Some junk belongs to your computer or editor, not to the project: .DS_Store from macOS, Thumbs.db from Windows, or .vscode/ settings. Putting these in every project's .gitignore clutters shared rules. Instead, create one personal list and point Git at it with the core.excludesFile setting. It then applies to every repository you use.

bash
git config --global core.excludesFile ~/.gitignore_global

Then put .DS_Store, Thumbs.db and similar lines in ~/.gitignore_global

Which file for which rule

Project-wide rules (dependencies, build output, secrets files) go in the committed .gitignore. Personal rules (editor and OS junk) go in your global ignore file, which is never shared.

The classic mistake: ignoring .env too late

A .env file usually holds secrets such as database passwords and API keys. The safe order is to add .env to .gitignore before the first commit. The painful order is to commit it first and ignore it later. Because ignoring does nothing for tracked files, the file keeps being tracked, and every earlier commit still contains the secret in history.

How the leak happens
  1. 1git add . and commit.env goes into history
  2. 2Push to GitHubanyone with access can read it
  3. 3Add .env to .gitignoretoo late: file is still tracked

Running git rm --cached .env stops future commits from including it, but the old commits keep the content, and anyone who cloned the repository already has a copy. Deleting the secret from history is hard and cannot reach clones, so treat the secret as public.

Common mistake

Adding .env to .gitignore after committing it and assuming the problem is solved. The secret is still in history. Rotate it: create a new password or key, revoke the old one, and update your app. Only then clean up with git rm --cached .env.

Habit to build

Create .gitignore as one of the first files in a new project, before your first git add. You can still store a safe .env.example with dummy values so teammates know which settings they need.

Part 11 · Undoing Things Safely

Discarding and Unstaging

Everyone makes mistakes while working, so Git gives you several ways to take things back. The right tool depends on where the mistake lives: in your working tree, in the staging area, or already in a commit. We start with the two smallest undo commands, which deal with files that have not been committed yet.

Discarding edits with git restore

Suppose you changed notes.txt, decided the edit was a bad idea, and want the file back the way it was at the last commit. git restore notes.txt replaces the file in your working tree with the committed version. Your unstaged edits are thrown away.

bash
git status --short
git restore notes.txt
git status --short

The file is clean again after the restore

output
 M notes.txt
No safety net here

Edits discarded with git restore <file> were never saved by Git, so they cannot be recovered. Be sure you really do not want them.

Unstaging with git restore --staged

Sometimes you ran git add on a file too early. git restore --staged notes.txt moves the file out of the staging area, so it will not be part of the next commit. The file itself is not touched: your edits stay in the working tree, just no longer staged.

CommandStaging areaYour file's contents
git restore notes.txtUnchangedReset to the last commit (edits lost)
git restore --staged notes.txtFile removed from itUnchanged (edits kept)

Fixing and Reversing Commits

Once a change is committed, you have more options. Which one is right depends mostly on one question: has the commit been pushed to a shared remote yet?

Fixing the last commit with --amend

If you made a typo in the last commit message, or forgot to add a file, you do not need a new commit. Stage whatever is missing, then run git commit --amend. Git replaces the last commit with a corrected one. Because it rewrites that commit (it gets a new hash), only use it while the commit is still local and unpushed.

bash
git add forgotten.txt
git commit --amend -m "Add login form and its styles"

Replaces the last commit instead of adding a new one

Undoing an old commit with git revert

git revert <hash> leaves history alone and adds a new commit that does the exact opposite of the chosen one. The bad commit stays in the log, followed by the commit that cancels it. Because nothing is rewritten, teammates who already pulled the old commit can pull the revert without trouble. That makes it the safe choice for shared history.

bash
git log --oneline
git revert 4f9c2ab
git log --oneline

A new commit appears on top; the old one stays

output
4f9c2ab Change tax rate
b21d7e0 Add checkout page
Amend vs revert

--amend edits history, so keep it for unpushed work. revert adds to history, so it is fine for anything.

Reset and Reflog

git reset moves your branch backwards, so commits drop off the end of it. It comes in two flavours that differ in what happens to the changes those commits contained. HEAD~1 means one commit before the current one.

CommandCommitStaged changesWorking tree files
git reset --soft HEAD~1UndoneKept, still stagedKept
git reset --hard HEAD~1UndoneDiscardedDiscarded (destructive)

Use --soft when the commit was fine in content but you want to redo it, for example to split it or reword it: all your work is waiting in the staging area. Use --hard only when you truly want the commit and its changes gone.

Check git status before reset --hard

Running git reset --hard without first looking at git status can wipe out uncommitted work you forgot about. Unsaved changes cannot be brought back by any Git command.

Recovering with git reflog

If you reset too far, committed work is usually not lost. git reflog lists every place HEAD has been recently, including commits that are no longer on any branch. Find the hash from before the reset, then move back to it.

bash
git reflog
git reset --hard HEAD@{1}

HEAD@{1} is where HEAD was one move ago

output
b21d7e0 HEAD@{0}: reset: moving to HEAD~1
4f9c2ab HEAD@{1}: commit: Change tax rate

Choosing the Right Undo

The golden rule is simple: revert for pushed commits, reset only for local ones. Resetting rewrites history, and if others have already based work on those commits, their copies will disagree with yours and cause painful conflicts. Revert simply adds a new commit, so everyone stays in sync.

How do I undo this?
SituationUseSafe for shared history?
Edits not wantedgit restore <file>Yes, but edits are lost
Added too earlygit restore --staged <file>Yes
Last commit slightly wrong, unpushedgit commit --amendNo, rewrites
Bad commit already pushedgit revert <hash>Yes
Local commit to redogit reset --soft HEAD~1No, rewrites
Local commit to throw awaygit reset --hard HEAD~1No, and destructive
Habit worth building

Run git status before any command that discards things. It takes a second and shows exactly what would be lost.

Part 12 · Common Beginner Mistakes

Wrong Branch, Secrets and Junk Files

Almost every Git user makes the same handful of mistakes in their first months. None of them is fatal, and most are easy to fix if you notice early. This section walks through nine of them, starting with three that are about where a commit goes and what goes into it.

1. Committing on the wrong branch

You start fixing a bug, then realise you are still on main instead of a feature branch. If you have not committed yet, the fix is easy: git switch -c fix/login-bug creates a new branch at your current position and moves you onto it. Your uncommitted changes come along, and the next commit lands on the new branch, leaving main untouched.

bash
git branch --show-current
git switch -c fix/login-bug
git add app.py
git commit -m "Fix crash when the password is empty"

Check where you are, then branch before committing

If you already committed on main, create the branch first so the commit is safe, then use git cherry-pick to copy it where it belongs, or simply move the branch pointer. The flow below shows the cherry-pick route.

Common mistake

Starting to edit without running git status or git branch --show-current. Make it a habit to look at the branch name before your first commit of the day. Also be careful with reset --hard: only use it on main once you have confirmed the commit exists on the new branch.

2. Committing secrets

A .env file, a database password or an API key committed to a repository is the most costly beginner mistake. Once a commit is pushed, assume the secret is public: bots scan GitHub for keys within minutes, and deleting the file in a later commit does not remove it from history.

The order of response matters. Rotate the key first, by revoking it at the provider and issuing a new one. Then remove the file from tracking and add it to .gitignore. Rewriting history with a tool such as git filter-repo is a last resort, because it changes every later commit hash for the whole team, and it still does not help once the key has been copied elsewhere.

Leaked secret: what to do, in order
  1. 1Revoke and rotateThe old key must stop working immediately
  2. 2Stop tracking the filegit rm --cached .env
  3. 3Ignore itAdd .env to .gitignore and commit
  4. 4Rewrite history only if neededLast resort, coordinate with the team

3. Committing node_modules and build output

Dependency folders and generated output can be thousands of files and hundreds of megabytes. They bloat the repository, make every clone slow and create noisy diffs. They can always be rebuilt from your source and lockfile, so they do not belong in Git. Put a .gitignore in the very first commit, because ignoring a file only works for files Git is not yet tracking.

gitignore
node_modules/
dist/
build/
__pycache__/
.env

A starter .gitignore for the first commit

bash
git rm -r --cached node_modules
git commit -m "Stop tracking node_modules"

Already committed it? Untrack it, keeping the files on disk

Common mistake

Adding a line to .gitignore after the folder is already tracked and expecting it to disappear. Git keeps tracking files it already knows about until you run git rm --cached on them.

Pushing, Pulling and Commit Habits

The next three mistakes are about working with other people: how you share commits, how often you refresh your copy, and how you describe what you did.

4. Force pushing to a shared branch

When a normal git push is rejected, Git is protecting you: the remote has commits you do not have. Adding --force tells the remote to throw its version away and use yours. If a teammate pushed work in the meantime, those commits are overwritten on the server, and their next pull becomes a confusing mess.

git pushgit push --force
When the remote has newer commitsRejected, so you pull firstRemote history is replaced by yours
Teammates' workSafeCan be lost or orphaned
Fine onAny branchOnly your own private branch

If a push is rejected, run git pull, resolve anything that comes up, then push again. If you truly must force a private branch, prefer git push --force-with-lease, which refuses to overwrite commits you have not seen.

Common mistake

Reaching for --force just to make the red error go away. The error is information. Read it before overriding it.

5. Giant, vague commits

A commit called update that touches forty files tells nobody anything. You cannot review it, you cannot revert one part of it, and git log becomes useless. Commit small and often, with one logical change per commit, and write a subject line that says what changed.

Vague habitBetter habit
MessageupdateValidate email before saving a user
SizeA week of work in one commitOne idea, with its tests
UndoingRevert everything or nothingRevert exactly one change
ReviewTeammates skim and hopeEach diff is short enough to read

6. Skipping git pull before you start

If you begin work on a stale copy, you may spend an hour editing code that a teammate already changed. The conflict appears only when you finally push, when it is most painful. Pulling at the start of a work session keeps your base current and shrinks the gap that can conflict.

bash
git switch main
git pull
git switch -c feat/profile-page

Update first, then branch

A simple routine

Pull before you start, commit small while you work, pull again before you push. Fewer surprises, smaller conflicts.

Conflicts, Detached HEAD and Identity

7. Panic-resolving a conflict

A merge conflict means Git could not decide between two versions of the same lines, so it asks you. Beginners often panic and delete one whole side, which silently throws away a teammate's work or their own. Instead, read both versions and keep the right lines, which is often a mix of the two.

text
<<<<<<< HEAD
timeout = 30
=======
timeout = 60
retries = 3
&gt;&gt;&gt;&gt;&gt;&gt;&gt; feat/retry

Above the ======= is your side, below is theirs

Here the right answer might keep the longer timeout and the new retries line. Delete the three marker lines, save the file, then git add it and finish with git commit. Run the tests before you do.

Resolving a conflict calmly
Common mistake

Leaving <<<<<<< markers in the file, or accepting one side without reading. Search the project for the marker text before you commit.

8. Detached HEAD

Checking out a commit hash instead of a branch puts you in detached HEAD state: HEAD points straight at a commit, not at a branch name. That is fine for looking around. But if you commit there and then switch away, those commits belong to no branch and become hard to find. To keep the work, give it a branch while you are still there.

bash
git checkout 3f9a2c1
git switch -c experiment/old-version

Git warns about detached HEAD; naming a branch saves anything you commit

If you only wanted to look, run git switch main to return. If you already switched away and lost commits, git reflog lists where HEAD has been, so you can find the hash and branch from it.

9. Not setting user.name and user.email

Every commit records an author. If you never configured your identity, Git either refuses to commit or guesses a name and address from your computer, so history shows a stranger, and GitHub cannot link the commits to your account. Set it once for all repositories.

bash
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --global --list

Use the same email as your GitHub account

The nine in one breath

Check your branch, keep secrets and generated folders out, never force push shared work, commit small with clear messages, pull before you start, read both sides of a conflict, name a branch when HEAD is detached and set your identity once.

Part 13 · Git Cheat Sheet

The Big Picture and First Setup

Every Git command you use moves changes between a few places. If you keep that map in your head, the commands stop feeling like a list to memorise. Your edits start in the working tree, move to the staging area when you add them, become a permanent snapshot in the repository when you commit, and reach your teammates on the remote when you push.

Before the first commit, tell Git who you are. The name and email are stamped on every commit you make. The --global flag stores them once for your whole computer, so you only do this a single time.

bash
git config --global user.name "Asha Rao"
git config --global user.email "asha@example.com"

One-time setup, used on every commit afterwards

Then you need a repository to work in. You either turn an existing folder into one, or copy one that already lives somewhere else.

CommandWhat it doesUse it when
git initCreates a new empty repository in the current folderYou have a project folder with no history yet
git clone <url>Downloads a full copy of a remote repository, with its historyThe project already exists on GitHub or a server
bash
git init
git clone https://github.com/example/notes.git

Start from scratch, or copy an existing project

Remember the route

A change travels working tree, then staging with add, then repository with commit, then remote with push. Nearly every other command inspects that route or moves something back along it.

The Daily Loop and Looking Around

Most days you repeat one small loop: check what changed, choose what belongs together, and record it. Run git status first and often. It tells you which files are modified, which are staged and which Git has never seen.

The daily loop
  1. 1git statussee what changed
  2. 2git add <file>stage the files for one change
  3. 3git commit -m "msg"save the snapshot
bash
git status
git add notes.txt
git commit -m "Add shopping list notes"

One pass through the loop

Once you have a few commits, you will want to look at them. Four commands cover almost every question. The two diff commands differ only in which areas they compare, which is where beginners usually trip.

CommandCompares or showsAnswers the question
git log --oneline --graphOne line per commit, with branch lines drawnWhat happened in this project, and how did branches split?
git diffWorking tree against stagingWhat have I changed but not staged yet?
git diff --stagedStaging against the last commitWhat will go into my next commit?
git show <hash>One commit's message and its changesWhat exactly did that commit do?
bash
git log --oneline --graph
git diff
git diff --staged
git show a1b2c3d

Replace a1b2c3d with a hash from your own log

Common mistake: an empty diff

You ran git add and then git diff shows nothing. That is normal. The change is now staged, so look at it with git diff --staged.

Branches and Remotes

A branch is a separate line of work, so you can try something without disturbing the main line. The usual life of a branch is short: create it, commit on it, merge it back, then delete it.

Life of a feature branch
  1. 1git switch -c namecreate and move onto it
  2. 2add and commitwork as usual
  3. 3git switch maingo back to the target branch
  4. 4git merge namebring the work in
  5. 5git branch -d nametidy up
CommandEffect
git switch -c nameCreates a new branch and switches to it
git switch nameMoves to an existing branch
git merge nameBrings the commits of name into the branch you are on
git branch -d nameDeletes a branch that has already been merged

Branches live on your computer until you share them. The remote is the copy on GitHub or another server. Git never talks to it behind your back: you decide when to download and when to upload.

CommandDirectionWhat happens
git remote -vLook onlyLists the remotes and their URLs
git fetchDownloadGets new commits but does not change your files
git pullDownload and mergeFetches, then merges into your current branch
git push -u origin nameUploadSends the branch and remembers it for later pushes
bash
git switch -c add-search
git commit -am "Add search box"
git push -u origin add-search

After -u, a plain git push or git pull works on this branch

Common mistake: deleting too early

git branch -d refuses to delete a branch that has unmerged work. Read that refusal as a warning, not an obstacle, and merge first.

Undo, Ignore and Quick Choices

Undoing in Git depends on how far the change has travelled. A change that is only in your working tree is easy to drop. One that is staged can be unstaged. One that is already committed is best undone with a new commit if other people have it.

Which undo do I need?
CommandWhat it doesSafe?
git restore <file>Throws away your unsaved edits to a fileDestructive: the edits are gone
git restore --staged <file>Moves a file back out of staging, edits keptYes
git revert <hash>Adds a new commit that cancels an old oneYes, history is kept
git reset --soft HEAD~1Removes the last commit but keeps its changes stagedYes, for unpushed commits

The last piece is keeping files out of Git. A .gitignore file lists patterns for things like build output, secrets and editor folders. Ignore rules only affect files Git is not tracking yet, so a file you already committed needs to be untracked first.

bash
# .gitignore
node_modules/
*.log
.env

One pattern per line

bash
git rm --cached config.local
git check-ignore -v debug.log

Stop tracking a file but keep it on disk, then ask which rule ignores a path

Common mistake: ignoring after committing

Adding .env to .gitignore does not remove a .env that was already committed. Run git rm --cached .env and commit that change. If it held real secrets, change them too, because they are still in the history.

Keep this page handy

Setup once, then loop through status, add and commit. Inspect with log and diff, branch for experiments, push to share, and pick the undo that matches where the change sits.

Part 14 · Check yourself

Quiz

Try each question before opening the answer. They ask you to predict what Git will do, not to repeat definitions.

You edit app.py, run git add app.py, then edit app.py again and run git commit -m "Add greeting". Does the second edit end up in the commit?
  • No. The commit saves what was in the staging area at the moment you ran git add.
  • The second edit is still in the working tree, so git status lists app.py as both staged and modified.
  • Run git add app.py again before committing if you want the second edit included.
git status -s
MM app.py
You run git switch -c fix-login, then git commit -m "Fix login redirect", then git switch main. You open login.py. Which version do you see, and why?
  • You see the old version, without the fix.
  • The commit was made on fix-login. A branch is a pointer to a commit, and main still points at the commit before yours.
  • The fix appears on main only after git merge fix-login. Because main has no new commits, that merge is a fast-forward and just moves the pointer.
You committed .env by mistake, then added .env to .gitignore. Running git status shows nothing about .env. Is the problem solved?
  • No. Ignoring only affects untracked files, and .env is already tracked, so Git quietly keeps tracking it.
  • The secret also remains in earlier commits, so push and share nothing before you act.
  • Run git rm --cached .env and commit to stop tracking it, then rotate every key that was in the file. Rewriting history is a last resort.
git rm --cached .env
git commit -m "Stop tracking .env"
Your push is rejected with a non-fast-forward error. A teammate suggests git push --force. What should you do instead, and what does force do?
  • The rejection means the remote has commits you do not have yet.
  • Run git pull (or git pull --rebase for a straight history), resolve any conflicts, then push again.
  • --force overwrites the remote branch with yours and erases your teammates' commits. If you truly need it, --force-with-lease refuses when the remote changed since you last fetched.
You pushed a commit that breaks the build. You want it undone. Do you pick git reset --hard HEAD~1 or git revert <hash>?
  • Use git revert <hash>. It adds a new commit that undoes the bad one, so history that others already have stays intact.
  • reset --hard throws the commit away locally, so your branch no longer matches the remote and you would need a force push.
  • Reserve reset for commits that are still local, and check git status first so you do not lose unsaved work. If you reset by accident, git reflog shows where HEAD was.

Summary

  • Changes move working tree → staging (git add) → repository (git commit) → remote (git push), and git add alone never commits.
  • Run git status and git diff --staged before each commit, and keep every commit to one logical change with an imperative subject line.
  • A branch is just a movable pointer to a commit, so creating and switching branches is cheap; merge it back when the work is done.
  • git pull is fetch plus merge; if a push is rejected, pull first and avoid force pushing over shared work.
  • .gitignore only affects untracked files, so use git rm --cached for tracked ones and rotate any secret that was ever committed.
  • Use git revert for pushed commits, reset only for local ones, and git reflog to recover after a mistake.