Git
February 16, 2026 | 19 min read
gitoneshotHow would you understand and learn git? read this 2-3 times if you want and then open your terminal and run this command man git. this is all you need to learn git.
Linus Torvalds created git in 5 days because one of the contributors of Linux broke the BitKeeper's license & the whole drama (go read if you want). but this is how git was created. to manage the linux codebase from going nuclear.
MAN GIT
this is a Linux command. go read my Linux article to learn more.
q => quit the manual
j => jump line down
k => jump line above
d => half page down
u => half page up
/term => search for 'term'
n => next search term
N => previous search term
PORCELAIN & PLUMBING
In git, the commands are divided into high-level commands (known as 'porcelain' commands) and low-level commands (known as 'plumbing' commands).
porcelain commands are the ones you will mostly use as a developer.
some porcelain commands
git status
git add
git commit
git push
git pull
git log
some plumbing commands
git apply
git commit-tree
git hash-object
GIT CONFIG
git was intended to track who changed code, when and how. therefore before starting to use git, you need to configure it to contain your information to ensure you get the proper credits for the code that you wrote. you need to set your name and email.
git comes with a configuration, both at the global and the repository level.
repository (repo) is a project folder tracked by git.
- check if your name & email are already set.
git config --get user.name
git config --get user.email
- if not set, you can set it using below commands.
git config --add --global user.name 'nikhar savaliya'
git config --add --global user.email 'nikhar123@gmail.com'
# --global sets the global (user level) configuration for git,
# remove it to set repo level config.
- you can change the default branch using,
git config --add --global init.defaultBranch main
GIT CLONE
Clone copies a remote repository to your local machine.
git clone <URL>
git clone <URL> <folder_name>
GIT INIT
A git repository represents a single git project. you'll typically have one repo per project unless it's a monorepo.
A repo is just a directory that contains a project (i.e., directories & files). The only difference is a hidden .git directory. This hidden directory is where git stores all of its internal tracking & versioning info for the project.
- create a new directory
mkdir first-project
cd first-project
- make it a git repo
git init
use ls -a command to see the hidden .git directory.
GIT STATUS
A file can be in one of several statuses in a git repo. Here are a few important ones,
- untracked: not being tracked by git (git does not know this file yet).
- staged: marked for inclusion in the next commit (so git can track this file).
- committed: saved to the git history (git now knows who created/changed this file).
GIT ADD
git add command is used to stage the untracked files in your repository.
Without staging, every file in your repo would be included in every commit, but that isn't efficient or needed.
git add FILE_PATH
use git add . to add all untracked files in the current working directory.
use git add -A to add all untracked files in the current git repo.
GIT COMMIT
A commit is a snapshot of the repository at a given point in time. It's a way to save the state of the repository and it is how git keeps track of changes made to the repo.
git commit -m 'COMMIT_MESSAGE'
GIT LOG
A command that shows the history of the commits in your repo. This is what makes git a version control system. you can see who made a commit, when the commit was made and what was changed.
COMMIT HASH
Each commit has a unique identifier called "commit hash". This is a long string of characters that uniquely identifies the commit. For example, this is a commit hash - f27caa0bc65b114e2d5322838503435189458b3c.
you can use the first 7 characters of a hash to refer to the whole commit hash (e.g., f27caa0)
Git uses a cryptographic hash function called SHA-1 to generate commit hashes.
git --no-pager log -n 10 # last 10 commits
# --no-pager => print directly to stdout instead of a second screen
git log --oneline # nice & compact
git log --parents # with parent commits hash
git log --graph # shows nice graph
You and I will have different hashes even when we have the same content in the repository. This hash is affected by,
- the commit message
- the author's name and email
- the date and time
- parent commit hashes
Theoretically it's possible to get the same hash for commits if for you and I the above factors stay the same.
THE PLUMBING
IT'S JUST FILES ALL THE WAY DOWN!
All the data in a git repository is stored directly in the .git directory. That includes all the commits, branches, tags and other objects we'll learn about later.
Git is made up of objects that are stored in the .git/objects directory.
For your commit hash f27caa0 you would find .git/objects/f2/7caa0
WHY /f2/7caa0?
In Linux there is a situation called 'inode busting'. When a file system runs out of inodes (data structure that stores metadata about files and directories); this file convention is used to prevent inode busting.
THE OBJECT FILES
Let's take a look at what's inside this suspicious object file.
cat PATH_TO_FILE
It prints total BS, because the contents have been compressed to raw bytes!
Let's try again with the xxd command
xxd PATH_TO_FILE
GIT CAT-FILE
Luckily, Git has a built-in plumbing command cat-file that allows us to see the content of a commit without needing to futz around with the object files directly.
git cat-file -p <HASH>
The output is something like this
tree <HASH1>
parent <HASH2>
author <NAME><EMAIL> <TIMESTAMP>
committer <NAME><EMAIL> <TIMESTAMP>
<COMMIT_MESSAGE>
If you run the same command with the hash of the tree it lists all the blob objects with hash & file name
Ex. 100644 blob <HASH> <FILENAME>
You can think of "tree" as a directory & "blob" as a file
WHY IS THIS USEFUL?
You can go back to the history of any file and look or grab any file at any time.
WHAT IF?
You committed 'Readme.md' file and then added a new file 'TOC.md' and committed.
Now there are two tree objects for these two commits.
- T1 (for commit 1) points to hash of Readme.md
- T2 (for commit 2) points to hash of Readme.md of T1 (if file not changed) and also points to hash of blob of TOC.md
Note that git did not create a new blob object for the unchanged files, it references those files via hash.
This is why git is efficient.
STORING DATA
We know that git stores an entire snapshot of files on a pre-commit level. This was a surprise.
While git stores an entire snapshot of the repo; it has performance optimizations (as we noted above). It compresses & Packs files to store them more efficiently. Git deduplicates files that are the same across different commits. If a file doesn't change between commits, git only stores it once.
CONFIG (ADVANCED)
git config --add --global user.name "foo" # appends user.name even if it exists
# ↑ ↑ ↑ ↑
# add scope section.key value
git config --global user.name "foo" # adds/replaces user.name
git config --get user.name
git config --list --local
git config --unset user.name
Git doesn't care! It allows you to store duplicate keys. (It takes the last one as final). --unset-all flag is useful if you ever really want to purge all instances of a key from your config. Conversely, --unset only works with a single instance of a key.
git config --unset-all example.key
If you try to unset an entire section it fails because it can't, it needs a key. use --remove-section
git config --remove-section user
There are several locations where git can be configured.
system:
/etc/gitconfig- A git config for all users on the system
global:
~/.gitconfig- A file that configures git for all projects of the user.
local:
.git/config- For a repository
worktree
WORKTREE > LOCAL > GLOBAL > SYSTEM
- Worktree config overrides local, local overrides global and global overrides system for the same keys.
BRANCHING
A git branch allows you to keep track of different changes separately.
Under the hood, a branch is just a named pointer to a specific commit. When you create a branch, you are creating a new pointer to a specific commit.
The commit that the branch points to is called the tip of the branch or HEAD.
# 1. to see what branch you are working on
git branch
# 2. rename a branch
git branch -M <old> <new>
# 3. create a branch
git branch <name>
# 4. create and switch to a new branch
git checkout -b <name> # normy coder
git switch -c <name> # chad coder
# create & switch to new branch from specific commit
git switch -c <name> <HASH>
When you create a branch, it points to the latest commit of that branch.
# after `git switch -c feat`
A --- B --- C (main)
\
(feat, HEAD)
GIT LOG FLAGS
Git log shows you the history of commits in the repo. There are a few flags I like to use from time to time to make the output easier to read.
--decorate: can be short, full, or no. Shows full ref name of branch instead of just branch name. A ref is just a pointer to a commit.
All branches are refs, but not all refs are branches.
git log --decorate=full # shows full ref name instead of branch name
--oneline: super spacious.
git log --oneline
use this as a chad developer.
git --no-pager log --oneline --decorate --graph --parents
GIT FILES
Git stores all info about the files in the .git folder. The heads of branches are stored in the .git/refs/heads directory (one file per branch)
If you cat any of the files in this directory, you will find the commit hash that the branch points to.
MERGING
(merge base)
A --- B --- C (main)
\
D --- E (feat, HEAD)
To merge branch feat into main, run the following command while on the main branch.
git merge feat
WHAT DOES ABOVE COMMAND DO?
- finds the 'merge base', also known as 'best common ancestor', of the two branches (main & feat).
- replays the changes from 'feat' into 'main' starting from the merge base.
- records the changes as a new commit (F) (F is a special commit because it has 2 parents)
(merge base)
A --- B --- C -------- F (main, HEAD)
\ /
D --- E ---------- (feat)
Fast-Forward Merge
The branch that you are merging into, if its HEAD is the merge base then you can do a fast-forward merge.
A --- B --- C (main)
\
D --- E (feat, HEAD)
Here, because the feat branch has all commits from main, no merge commit is created, this is called a fast-forward merge (git automatically does this)
REBASE
Say we have this commit history
A --- B --- C (main)
\
D --- E (feat, HEAD)
We are working on 'feat' and want to bring in the changes our team added on the main branch, but merge would add an additional merge commit.
Rebase avoids a merge commit by replaying the commits from feature on top of main. After the rebase, the history would look like this
REBASE JUST MOVES MERGE BASE.
A --- B --- C (main)
\
D' --- E' (feat, HEAD)
NOTE: after rebase, now commit D & E would have new commit hashes because D has a new parent. Because D has a new hash, E would have a new hash (remember that we talked about SHA-1 hash and what affects it).
How rebase works internally
- git checks out to main (target branch)
- replays one commit at a time from feature branch onto main
- updates feature branch to point to the last replayed commit
- rebase doesn't affect main while feature gets all changes from main
GIT RESET
1. Soft Reset
Moves HEAD to specified commit but keeps all changes staged. useful when you want to redo the commit message or combine commits.
git reset --soft <HASH>
git reset --soft HEAD~1
2. Mixed Reset (default)
Moves HEAD and unstages changes, but keeps them in working directory. useful when you want to re-stage selectively.
git reset <HASH>
git reset HEAD~1
3. Hard Reset
Moves HEAD and discards all changes completely. dangerous - changes are gone.
Untracked files are not in the worktree yet, hence git reset --hard doesn't work on them.
Resetting your working directory and index to the state of that commit will remove all commits after that (those are not lost forever, you can cherry-pick them using reflog).
GIT REMOTE
In git, another repo is called a remote. The standard convention is that when you're treating the remote as the authoritative source of truth (such as GitHub) you would name it the origin.
By the 'authoritative source of truth' we mean that it's the one you and your team treat as the true repo. It's the one that contains the most up-to-date version of the accepted code.
git remote add <NAME> <URI>
Origin is your fork of the Original Repo & Upstream is the Original repo. a useful command is git ls-remote
FETCH
Downloads commits from the remote but doesn't merge them.
git fetch
git fetch origin main
PULL
Fetch + merge in one command. gets remote changes and integrates them.
git pull # fetch and merge current branch
git pull origin main # fetch and merge specific branch
you can merge branches between local & remote repo manually too.
git merge origin/branch
YOUR IDEAL GIT/GITHUB WORKFLOW
- Keep real shat on GitHub so if you fry your machine, it is not lost.
- configure git to rebase by default on pull, rather than merge so we can keep linear history.
git config --global pull.rebase true
while doing solo feature development,
- main branch -> git add -> git commit -> git push.
Working with team
- git pull origin main
- git switch -c feat/xyz
- git add .
- git commit
- git push origin feat/xyz.
- make pr from feat/xyz into main.
- merged by Team Lead.
- delete feat/xyz from remote.
GIT IGNORE
.gitignore lets you ignore files/directories in a working git repo. It is excluded in code staging & committing. you can have multiple .gitignore in your repo for different directories for directory level.
PATTERNS
In addition to exact filepath section names, you have some patterns to allow more robust gitignore.
- wildcards (
*.tsx, *.env) - Rooted pattern
- patterns starting with a '/' are anchored to the directory containing the .gitignore.
- example: ignore a page.tsx at the root (/page.tsx)
- Negation
- ignore all .tsx except this.tsx
*.tsx
!imp.tsx
- comments (
# this is a comment)
REFLOG
Git reflog is kinda like git log but stands for reference log. It specifically logs the changes to a 'reference' that have happened over time.
Reflog uses a different format to show the history of a branch or HEAD: one that's concerned with the number of steps back in time.
HEAD@{0} => where head is Right Now.
HEAD@{1} => Where head was 1 move ago.
You have 3 commits (A => b => c (HEAD))
git reflog
# output
c3f9a1e (HEAD -> main) HEAD@{0}: commit: C
b2e8d7f HEAD@{1}: commit: B
a1d2c3b HEAD@{2}: commit (initial): A
for some reason you have to reset to commit B.
git reset --hard HEAD@{1}
# or
git reset --hard b2e8d7f
now the reflog is this,
git reflog
# output
b2e8d7f (HEAD -> main) HEAD@{0}: reset: moving to HEAD@{1}
c3f9a1e HEAD@{1}: commit: C
b2e8d7f HEAD@{2}: commit: B
a1d2c3b HEAD@{3}: commit (initial): A
This may help you understand why i said we can recover lost content even when we do hard reset.
Reflog tracks the changes of a branch or other references, which means it can track HEAD across branch changes & commit changes. Create a feature branch, merge & push to main. now delete your branch. Now someone force pushed to main reverting your changes. (FIRED BTW). You deleted your branch but we have reflogs.
git reflog => get hash of your commit => you can merge this commit to your branch (git merge HEAD@1). Or you can do this,
git reflog
git cat-file -p <hash> # get tree hash
git cat-file -p <tree_hash> # get file hash
git cat-file -p <file_hash> # you see your file
HOMEWORK: look up FORK, FORK NETWORK, and SUPER REPO.
MERGE CONFLICTS
2 commits change the same line in the same file, it would create merge conflict when your feature branch merges back into main.
<<<<<<< HEAD
Main change
=======
Feature change
>>>>>>> feat
Here HEAD is your current branch.
REBASE CONFLICT
Rebase conflict is different from merge conflicts. In rebase conflict,
<<<<<<< HEAD
Main change
=======
Feature change
>>>>>>> feat
Here if you did rebase of main branch into your feature branch, you would notice that HEAD is main branch's changes (OPPOSED TO MERGE). This is because when you rebase, you switch to target branch (i.e., main) then you replay your source branch changes one by one from merge base. (i.e., feat)
Override feat branch's changes with the changes from main using
git checkout --ours <file_name>
Unlike a merge conflict, we don't commit to resolve the conflict, we use --continue
git rebase --continue
During rebase, if there is a commit that deleted one line and we rebase; it would ignore those changes and git doesn't keep empty commits so it would be removed.
during rebase conflicts you're in a "no branch" state. use git branch to see it.
ReReRe - REPEAT RESOLUTION SETUP
There are times when you have to manually resolve the same conflicts over and over again. git rerere is a bit of a hidden feature.
rerere - records resolution. It allows you to ask git to remember how you've resolved a hunk conflict so that next time it sees the same conflict, git can resolve it for you automatically.
git config --local rerere.enabled true
SQUASHING
We take all the changes from several commits and 'squash' them into a single commit for better commit history. you have 3 commits a,b,c and you want to squash them all into a.
# before squash
A --- B --- C (HEAD -> main)
git rebase -i HEAD~3 # interactive rebase of 3 commits
# rebase-todo will open
# pick <hash> a
# squash <hash> b
# squash <hash> c
# after squash
A' (HEAD -> main)
STASH
You want to pull some changes but don't want to commit a WIP feature & don't want to lose it, use stash. It puts your changes in a special FILO stack.
git stash
git stash -u
git stash list
git stash pop/apply
GIT DIFF
git diff shows you the difference between stuff. git diff shows the changes between working tree & commit.
git diff # unstaged changes
git diff --staged # staged changes
git diff HEAD~1 # between previous commit and current state
git diff <hash> <hash> # between two commits
GIT REVERT
Unlike git reset, git revert undoes a commit but creates a new commit instead of changing history. safe for shared branches.
git revert <HASH>
CHERRY PICK
When you want to have a specific commit from another branch into the current branch, use cherry pick.
git cherry-pick <commitish>
before cherry-pick make sure there are no uncommitted changes.
BISECT
Let's say you have a bug introduced in main but don't know which commit introduced it. You can use bisect, set good commit (a commit without this bug) & bad commit (duh). Bisect does a binary search to find the commit. It's not only for bugs, it's for everything you want to find.
- git bisect start
- git bisect good HASH
- git bisect bad HASH
- git checkout to a commit between good & bad
- Execute git bisect good or git bisect bad to say the current commit is good or bad.
- loop back to step 4 (until git bisect completes)
- exit bisect mode with git bisect reset
TAGS
Tags are named pointers to specific commits. used for releases.
git tag v1.0.0 # lightweight tag
git tag -a v1.0.0 -m "msg" # annotated tag (recommended)
git tag # list tags
git push origin v1.0.0 # push tag to remote
git push origin --tags # push all tags
WORKTREE
Worktrees let you have multiple branches checked out simultaneously in different directories. useful when you need to work on a hotfix while keeping your feature branch intact.
git worktree add ../hotfix main # create worktree from main
git worktree list # list worktrees
git worktree remove ../hotfix # remove worktree