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.
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 hand | Version control | |
|---|---|---|
| Finding an old state | Guess from file names and dates | Pick any recorded change from the history |
| What changed | Open both copies and compare by eye | The tool shows the exact lines that differ |
| Who and why | Unknown unless you wrote it down | Every change carries an author, a date and a message |
| Working with others | Email zips and merge by hand, overwriting each other's edits | Combine edits safely and share them through one history |
| Disk use | Whole project duplicated for every copy | Only recorded changes are stored |
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 model | Git (distributed) | |
|---|---|---|
| Where history lives | Only on the server | On every clone |
| Viewing history or comparing versions | Needs the server | Works offline |
| Committing a change | Needs the server | Works offline, saved on your machine |
| If the server is lost | History may be gone | Any 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.
- 1Commit 1Snapshot: index.html, style.css
- 2Commit 2Snapshot: style.css changed, index.html reused
- 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.
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.
| Git | GitHub, GitLab, Bitbucket | |
|---|---|---|
| What it is | A program on your computer | Websites that host Git repositories |
| Needs internet | Only to exchange changes | Yes |
| Main job | Record and manage history | Share, back up and collaborate |
| Required to use Git | Yes | No, optional |
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.
git --versionYour exact version number will differ
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.
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
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.
mkdir notes cd notes git init ls -a
Start a new project from scratch
Initialized empty Git repository in /home/sam/notes/.git/
. .. .gitThe 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.
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.
git clone https://github.com/example/recipes.git cd recipes git remote -v
Copy a project and check the remote
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.
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.
- 1Downloadfiles and full history
- 2Create folderrepo name or your own
- 3Add .githistory and config inside
- 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 init | git clone <url> | |
|---|---|---|
| Use it when | You are starting a new local project | You are joining an existing project |
| Starting history | Empty | Complete copy of the existing history |
| Remote origin | None until you add one | Set automatically |
| Result | A new .git folder in the current folder | A new folder containing files and .git |
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.
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.
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.
| Place | Where it lives | What it holds | You change it with |
|---|---|---|---|
| Working tree | The project folder | The files you edit | Your editor |
| Staging area | Inside .git | A list of what goes in the next commit | git add |
| Repository | Inside .git | Committed snapshots, the history | git 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.
$ 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.
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.
$ 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.
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.
| State | Meaning | How it appears in git status |
|---|---|---|
| Untracked | Git has never been told about this file | Listed under Untracked files |
| Modified | Tracked, and changed since the last commit | Listed under Changes not staged for commit |
| Staged | Marked to go into the next commit | Listed under Changes to be committed |
| Committed | Safely stored, no changes since | Not 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.
$ 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.
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".
git add notes.txt
git add .
git status --shortStage one file, then everything under the current folder
A notes.txt A src/app.py A src/util.py
| Command | What it stages | Use it when |
|---|---|---|
git add notes.txt | Only that file | You want a small, focused commit |
git add src/ | Everything inside that folder | One folder holds the whole change |
git add . | All changes in the current folder tree | Everything you touched belongs together |
git add -p | Chosen pieces of files | One file holds two unrelated changes |
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.
git add -p app.py
Git shows each hunk and waits for your answer
@@ -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
| Key | Meaning |
|---|---|
y | Stage this hunk |
n | Skip this hunk for now |
s | Split the hunk into smaller hunks |
q | Quit; 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.
git commit -m "Fix item total to use quantity"Saves the staged snapshot
[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.
- 1a41b7e0Add notes.txt
- 2c82d5f3Add src/app.py
- 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.
git commit -m "Tweak app"Edited app.py but never ran git add
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.
git add forgotten.py
git commit --amend --no-editFold a forgotten file into the last commit
[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.
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.
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.
- 1SubjectImperative, about 50 characters, no period
- 2Blank lineSeparates subject from body
- 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:
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
| Part | Rule | Example |
|---|---|---|
| Mood | Imperative, like a command | Add retry to upload |
| Length | About 50 characters | Fix login redirect |
| Ending | No trailing period | Fix login redirect |
| Body | Optional, explains why | Why 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.
| Message | Verdict | Why |
|---|---|---|
| stuff | Bad | Says nothing about the change |
| fix | Bad | Fix what? Where? |
| changes | Bad | Every commit contains changes |
| Fix crash when cart is empty | Good | Names the bug and the situation |
| Add password reset email | Good | Names 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.
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.
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.
| Prefix | Meaning | Example |
|---|---|---|
| feat: | A new feature | feat: add password reset email |
| fix: | A bug fix | fix: handle empty cart at checkout |
| docs: | Documentation only | docs: explain how to run tests |
| refactor: | Restructure without changing behavior | refactor: split cart totals into a function |
| chore: | Upkeep such as tooling | chore: 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:
git log --onelinec3d4e5f 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.
git revert b2c3d4e
Creates a new commit that undoes only that one change
Mistakes to avoid
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.
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 --stagedand 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.
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.
git status
Run it from anywhere inside the repository
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.
| Section |
|---|
| Changes to be committed |
| app.py |
| Section |
|---|
| Changes not staged |
| README.md |
| 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.
git status -s
M app.py M README.md ?? notes.txt
| Short code | Meaning |
|---|---|
M (letter on the left) | Modified and staged |
M (letter on the right) | Modified but not staged |
MM | Staged, then edited again after staging |
A | New file that is staged |
?? | Untracked file |
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.
git log
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.
git log --oneline --graph --all* 5e8f0d2 (feature/login) Add login form
| * a41c9b7 (HEAD -> main) Fix typo in README
|/
* 7c3e1fa Add greeting to home page
* 1b9d604 Initial commitThis 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.
| Flag | What it changes |
|---|---|
--oneline | One line per commit with a short hash |
--graph | Draws the branch and merge shape |
--all | Shows 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.
git diff
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.
git add app.py
git diff --stageddiff --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")
| Command | Compares | Answers |
|---|---|---|
git diff | Working tree vs staging area | What have I changed but not staged? |
git diff --staged | Staging area vs last commit | What will my next commit contain? |
git diff main..feature | Two branches or commits | How 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.
git diff main..feature/login
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.
git show 7c3e1fa
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")
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.
- 1git statusRight files staged? Anything untracked?
- 2git diff --stagedRead the exact lines going in
- 3Fix or restageOnly if something looks wrong
- 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.
git status -s git diff --staged git commit -m "Add welcome message" git log --oneline
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
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.
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.
- 1Commit Afirst commit
- 2Commit Bmain points here
- 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.
| Pointer | What it points to | Moves when |
|---|---|---|
| main | The latest commit on main | You commit while on main, or merge into it |
| feature | The latest commit on feature | You commit while on feature |
| HEAD | The branch you are on | You switch branches |
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.
git branch git branch login-form git branch
List, create, list again
* 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.
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
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.
| Command | What it does |
|---|---|
| git branch | Lists local branches, marking the current one with a star |
| git branch name | Creates a branch but stays where you are |
| git switch name | Moves HEAD to an existing branch |
| git switch -c name | Creates 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.
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.
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.
| Fast-forward | Merge commit | |
|---|---|---|
| When | Target branch has no new commits | Both branches have new commits |
| New commit | None | Yes, with two parents |
| History shape | One straight line | Two 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.
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.
<<<<<<< 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.
git add title.txt git commit
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.
git branch -d login-form
Deleted branch login-form (was 4f2a9c1).
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.
git branch fix-login
git reset --hard HEAD~1
git switch fix-loginRescue a commit made on main by mistake
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.
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.
AbortingThe 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.
git remote add origin https://github.com/you/notes.git git remote -v
Connect the local repo to GitHub, then check the connection
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.
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.
| Command | What it does |
|---|---|
git remote add origin <url> | Records a remote called origin pointing at the URL |
git remote -v | Lists remotes with their fetch and push URLs |
git push -u origin main | Uploads main and sets origin/main as its upstream |
git push | Uploads the current branch to its upstream |
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.
- 1git fetchdownload new commits, files untouched
- 2git mergecombine origin/main into your branch
- 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 pull | git pull --rebase | |
|---|---|---|
| Steps | fetch, then merge | fetch, then replay your commits on top |
| Extra merge commit | Yes, if both sides have new work | No |
| History shape | Branches that join back together | One straight line |
| Your commits | Keep their original hashes | Get new hashes because they are replayed |
git fetch git log --oneline main..origin/main git pull --rebase
Look first, then bring the changes in
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.
! [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 behindThe 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.
| HTTPS | SSH | |
|---|---|---|
| URL looks like | https://github.com/you/notes.git | git@github.com:you/notes.git |
| Proves who you are with | A personal access token | A key pair |
| Setup | Create a token once | Generate a key and upload the public half |
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.
git switch -c feat/login
git add .
git commit -m "Add login form"
git push -u origin feat/loginEverything 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.
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 lack | Overwrites them | Refuses to push |
| Safe on a shared branch | No | Much 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.
# dependencies node_modules/ # any log file, in any folder *.log # one specific file .env
A small .gitignore at the root of a web project
| Pattern | What it ignores | Example match |
|---|---|---|
node_modules/ | A whole folder (the trailing slash means directory) | node_modules/express/index.js |
*.log | Every file ending in .log, at any depth | debug.log, logs/server.log |
.env | Any 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.
*.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.
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.
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.
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.
- 1Add the patternput it in .gitignore
- 2git rm --cached app.logfile stays on disk
- 3git commitrecords the removal
git rm --cached app.log git commit -m "Stop tracking app.log"
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.
git status --ignored
git check-ignore -v debug.logIgnored 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
| Command | Question it answers |
|---|---|
git status --ignored | Which 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.
git config --global core.excludesFile ~/.gitignore_globalThen put .DS_Store, Thumbs.db and similar lines in ~/.gitignore_global
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.
- 1git add . and commit.env goes into history
- 2Push to GitHubanyone with access can read it
- 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.
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.
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.
git status --short git restore notes.txt git status --short
The file is clean again after the restore
M notes.txt
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.
| Command | Staging area | Your file's contents |
|---|---|---|
git restore notes.txt | Unchanged | Reset to the last commit (edits lost) |
git restore --staged notes.txt | File removed from it | Unchanged (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.
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.
git log --oneline git revert 4f9c2ab git log --oneline
A new commit appears on top; the old one stays
4f9c2ab Change tax rate b21d7e0 Add checkout page
--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.
| Command | Commit | Staged changes | Working tree files |
|---|---|---|---|
git reset --soft HEAD~1 | Undone | Kept, still staged | Kept |
git reset --hard HEAD~1 | Undone | Discarded | Discarded (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.
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.
git reflog
git reset --hard HEAD@{1}HEAD@{1} is where HEAD was one move ago
b21d7e0 HEAD@{0}: reset: moving to HEAD~1
4f9c2ab HEAD@{1}: commit: Change tax rateChoosing 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.
| Situation | Use | Safe for shared history? |
|---|---|---|
| Edits not wanted | git restore <file> | Yes, but edits are lost |
| Added too early | git restore --staged <file> | Yes |
| Last commit slightly wrong, unpushed | git commit --amend | No, rewrites |
| Bad commit already pushed | git revert <hash> | Yes |
| Local commit to redo | git reset --soft HEAD~1 | No, rewrites |
| Local commit to throw away | git reset --hard HEAD~1 | No, and destructive |
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.
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.
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.
- 1Revoke and rotateThe old key must stop working immediately
- 2Stop tracking the filegit rm --cached .env
- 3Ignore itAdd .env to .gitignore and commit
- 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.
node_modules/ dist/ build/ __pycache__/ .env
A starter .gitignore for the first commit
git rm -r --cached node_modules git commit -m "Stop tracking node_modules"
Already committed it? Untrack it, keeping the files on disk
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 push | git push --force | |
|---|---|---|
| When the remote has newer commits | Rejected, so you pull first | Remote history is replaced by yours |
| Teammates' work | Safe | Can be lost or orphaned |
| Fine on | Any branch | Only 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.
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 habit | Better habit | |
|---|---|---|
| Message | update | Validate email before saving a user |
| Size | A week of work in one commit | One idea, with its tests |
| Undoing | Revert everything or nothing | Revert exactly one change |
| Review | Teammates skim and hope | Each 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.
git switch main git pull git switch -c feat/profile-page
Update first, then branch
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.
<<<<<<< HEAD timeout = 30 ======= timeout = 60 retries = 3 >>>>>>> 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.
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.
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.
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
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.
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.
| Command | What it does | Use it when |
|---|---|---|
git init | Creates a new empty repository in the current folder | You have a project folder with no history yet |
git clone <url> | Downloads a full copy of a remote repository, with its history | The project already exists on GitHub or a server |
git init git clone https://github.com/example/notes.git
Start from scratch, or copy an existing project
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.
- 1git statussee what changed
- 2git add <file>stage the files for one change
- 3git commit -m "msg"save the snapshot
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.
| Command | Compares or shows | Answers the question |
|---|---|---|
git log --oneline --graph | One line per commit, with branch lines drawn | What happened in this project, and how did branches split? |
git diff | Working tree against staging | What have I changed but not staged yet? |
git diff --staged | Staging against the last commit | What will go into my next commit? |
git show <hash> | One commit's message and its changes | What exactly did that commit do? |
git log --oneline --graph git diff git diff --staged git show a1b2c3d
Replace a1b2c3d with a hash from your own log
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.
- 1git switch -c namecreate and move onto it
- 2add and commitwork as usual
- 3git switch maingo back to the target branch
- 4git merge namebring the work in
- 5git branch -d nametidy up
| Command | Effect |
|---|---|
git switch -c name | Creates a new branch and switches to it |
git switch name | Moves to an existing branch |
git merge name | Brings the commits of name into the branch you are on |
git branch -d name | Deletes 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.
| Command | Direction | What happens |
|---|---|---|
git remote -v | Look only | Lists the remotes and their URLs |
git fetch | Download | Gets new commits but does not change your files |
git pull | Download and merge | Fetches, then merges into your current branch |
git push -u origin name | Upload | Sends the branch and remembers it for later pushes |
git switch -c add-search
git commit -am "Add search box"
git push -u origin add-searchAfter -u, a plain git push or git pull works on this branch
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.
| Command | What it does | Safe? |
|---|---|---|
git restore <file> | Throws away your unsaved edits to a file | Destructive: the edits are gone |
git restore --staged <file> | Moves a file back out of staging, edits kept | Yes |
git revert <hash> | Adds a new commit that cancels an old one | Yes, history is kept |
git reset --soft HEAD~1 | Removes the last commit but keeps its changes staged | Yes, 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.
# .gitignore
node_modules/
*.log
.envOne pattern per line
git rm --cached config.local
git check-ignore -v debug.logStop tracking a file but keep it on disk, then ask which rule ignores a path
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.
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 statuslistsapp.pyas both staged and modified. - Run
git add app.pyagain 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, andmainstill points at the commit before yours. - The fix appears on
mainonly aftergit merge fix-login. Becausemainhas 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
.envis 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 .envand 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(orgit pull --rebasefor a straight history), resolve any conflicts, then push again. --forceoverwrites the remote branch with yours and erases your teammates' commits. If you truly need it,--force-with-leaserefuses 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 --hardthrows the commit away locally, so your branch no longer matches the remote and you would need a force push.- Reserve
resetfor commits that are still local, and checkgit statusfirst so you do not lose unsaved work. If you reset by accident,git reflogshows where HEAD was.
Summary
- Changes move working tree → staging (
git add) → repository (git commit) → remote (git push), andgit addalone never commits. - Run
git statusandgit diff --stagedbefore 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 pullis fetch plus merge; if a push is rejected, pull first and avoid force pushing over shared work..gitignoreonly affects untracked files, so usegit rm --cachedfor tracked ones and rotate any secret that was ever committed.- Use
git revertfor pushed commits,resetonly for local ones, andgit reflogto recover after a mistake.