Omkar Iyer

Aug 31, 2026 • 16 min read

Program Design #2 - Git without Github for local solo projects

Index

  1. What is Git?

    1. What does Distributed mean?

    2. What is Version Control?

    3. Git's Tracking Phases

      • Working Tree

      • Staging

      • Commit History

  2. What is a Repo?

    • Local Repo

    • Remote Repo

    • Git repos over LAN

  3. Git Internals

    • Git Objects

    • References

  4. HEAD

  5. Working within Branches

  6. Inspecting History

  7. Undoing and Recovering from Mistakes

  8. Project Hygiene & Secrets Management

  9. How is GitHub different from Git?

  10. Advanced Local Workflow

  11. The commit cycle

  12. FINAL git commands cheat-sheet


When you initiate git with git init in your project, it creates a .git directory which will contain git's local repo data like commits, history, branches, configurations and more. The "." hides the folder by-default on many OSes. This initiation gives us the capability of version control .

git init # initiates git in your current project folder

What is Git?

It's a distributed version control system.

It's a time machine for your projects.

1. Distributed

“Distributed” means that each Git repository contains its own copy of the project's history, rather than relying on one central server to provide information.

2. Version Control

Version control means keeping a history of changes to your project so you can see the changes, compare them, and return to earlier versions if needed.

Each saved version is called a "commit". We call these 'checkpoints' in video games. It saves the current state of the project with a name.

.


>> Git's Tracking Phases

Git tracks your projects through 3 places -

Working Tree ----> Staging Area ----> Commit History

01. Working Tree

The files you're currently working on! Say I am working on main.py , so when I edit main.py , I am changing the working tree.

git diff shows the changes you've made but haven't staged.

It's not necesary that it's the files on the MAIN branch. It's what is currently being checked out. If you switch to branch-x, Git changes the working tree to reflect branch-x's current commit.

Older commits are not separate working trees. They're stored as history inside .git/.

You can temporarily make an older commit become your working tree with:

git switch --detach <commit>

git switch main # to return back

--detach allows moving your working tree to a commit without moving the branch. The HEAD will also change to that commit. It's useful for temporarily inspecting, running, or testing an old version without changing your main branch.

NOTE: A commit is an object in Git history. A branch is simply a movable name pointing to a commit. A commit is not contained or tied to a branch, it can exist independently. Git normally makes HEAD follow a branch. Detached HEAD means HEAD temporarily stops following a branch and points directly at a commit.

02. Staging Area

Preparation area or waiting area till the next commit. It contains content you have selected for next commit. You can choose to stage everything or specific files only too.

Use git status to get info about files you've staged, git diff --staged to see what you have staged compared to the latest commit,.

03. Commit History

The permanent project history with all saved states.


One of Personal Usecases

I have started using commits for tutorials or learning from docs, where I incrementally change the code as I learn more but may want to checkout starting phases when needed. I can document my learning without creating backups of previous states of code files like main.py and main.py.bak . Also, I previously used to create a folder called 'debris' to save unused or discarded code which I may wanna reference later. But, then it struck me that I can just use Git.

.


What is a "Repo"?

A Repository is a git-managed project with its history and associated contents.

Here's a detailed breakdown -

project/ ← working tree
├── main.py ← project file
└── .git/ ← Git repository
 ├── objects/ ← commits and file snapshots
 ├── refs/ ← branches/tags
 ├── HEAD ← current location
 └── config ← Git configuration

* Local Repo

The Git repository that Git is currently operating on directly. It is normally stored on the same machine where Git is running, but it can also be on a mounted/network filesystem.

If your project and .git/ are on a server, you can SSH into that server and run Git there. It would still be called local. The network connection is remote, file access is remote but from git's perspective, the repo is local as you're worikng on it directly.

* Remote Repo

A Remote is simply another Git repo that your local repo can talk to.

Remote Repo is commonly used for for team collabs, syncing between machines, open-sourcing & contributions or backing up codebases on the web.

A remote repository is normally intended to represent same project as local repo. But they may be on different versions. They can also contain temporary divergence or different branches.

git remote add origin <github-url>
# git remote add origin https://github.com/me/project.git
# origin is a name/alias for the remote repo

# To send commits to remote origin use:
git push origin main

# To get commits from remote origin use:
git pull origin main

Origin is not fixed. You can use any name to identify remote. You can jokingly even use kiwi - git remote add kiwi <github-url> but we use origin as a convention.

Also you can have multiple remotes. You can add say a backup remote URL with git remote add backup <url> then use git push origin main git push backup main to push updates to those URLs.

.

Git repos over LAN

LAN is local area network. You can connect devices together within a locality's range.

You can connect 2 repos on 2 different PCs and from each other's perspective local and remote repo definition changes -

From Computer A's perspective:

  • Repository A = local

  • Repository B = remote

From Computer B's perspective, if it communicates with A:

  • Repository B = local

  • Repository A = remote

So, remote doesn't mean “global internet”. It just means “another repository I'm communicating with”.

.


Git Internals

ESSENCE: Git stores objects in .git/objects/. The main object types are blobs, trees, and commits.

Object - An object is a piece of data stored by Git and identified by a hash.

blob → stores file contents
tree → stores directory structure
commit → project snapshot + history information
commit
├── tree → project's directory structure
├── parent → previous commit
├── author
├── timestamp
└── message

Inspect Objects :-

git cat-file -t <hash>
git cat-file -p <hash>

.

References

A reference (ref) is a human-friendly name that points to a Git object. eg. commit, tags, branches.

Instead of remembering or using hashes, we use names.

cat .git/HEAD
# ref: refs/heads/main

.

HEAD
 ↓
main
 ↓
commit
 ↓
tree
 ├── hello.py → blob
 └── src → tree
 └── app.py → blob

.


HEAD

HEAD tells Git where you currently are in the repository's history.

It usually points to a commit on the current branch.

It can also point to a commit without being attached to a branch. It's called detached HEAD. This is useful for temporary checkout but dangerous if you make edits. Git will eventually garbage-collect (remove) any commit which has no branch pointing to it. To continue development from a detached commit, you can create a branch pointing to it with git switch -c <new-branch-name> .

To see where the HEAD is use git status command.

.


Working within Branches

Branches are a tool to isolate parts of your work.

You can use it to build a feature without affecting rest of the code, test it and merge with main if it works out. You also can use it for risky experiments and revert back & delete the branch if it fails.

A branch gives you an independent line of development without creating another copy of your project or files within it.

A branch is a name pointing to a commit.

A → B → C
 ↑
 main

A → B → C → D
 ↑
 main

=> As D got committed, main moved from C to D

.

List branches

git branch

.

Create a new Branch

git branch <branch-name>

.

Switch Branches

We used to use git checkout but it also had many ambiguous jobs so git switch was introduced to make branch operations specific, clear and safer.

git switch <some-branch-name>
# assume you wanna move from 'main' to 'some-branch-name'
# branch must already exist

git switch -c <new-branch-name>
# switch from current branch to a new branch git just created
# if it already exists and you use '-c' flag, it will fail

git switch main # switch back to main
DIFFERENCES BETWEEN SWITCH AND CHECKOUT

| Command | Main purpose |
| ------------------------- | ------------------------------- |
| `git switch main` | Switch branches |
| `git switch -c feature` | Create + switch branch |
| `git checkout main` | Switch branches (older command) |
| `git checkout <commit>` | Enter detached HEAD |
| `git checkout -- file.py` | Restore a file |

.

Merging

Say you have a branch called 'auth' and you finished working on it and wanna merge the changes with main branch. Then first switch to main branch with git switch main then while on main do -

git merge login

On an IDE like VS Code, this can open the diff view - showing changes or 'conflicts to resolve' if both branches changed the same part of the same file in incompatible ways. Then git will ask you to resolve conflicts manually. Though, It can automatically resolve minor conflicts.

.

Deleting a branch

git branch -d auth # safely delete branch

This deletes the branch name not the commits as branch doesn't owns commits.

If you try to delete without merging the changes/commits, git will warn you.

⚠️ Deleting a branch and its unique commits -

git branch -D auth

This force-deletes the branch even if it hasn't been merged. The commits are then unreferenced and get garbage-collected eventually. ☠️ : once the commits become unreachable, recovery is harder.

-d = delete only if Git considers it safe
-D = delete even if commits may be lost

To clean-up abandoned commits immediately -

git gc

.


Inspecting History

git log --oneline

git show <commit-id> # see changes in a selected commit

git diff # shows changes between 2 states
git diff <commit-1-id> <commit-2-id>
git diff HEAD # Compare your current files with the latest commit

git blame src/utils.py
# tells which commit performed that specific change in a file
# use `git show <commit-id>` to investigate that commit

Undoing and Recovering from Mistakes

Undoing Mistakes

1 Restore Working Files

When you have changed a file but haven't committed or staged yet and want to discard all changes to go back to last commit state -

git restore main.py

⚠️You cannot recover the discarded changes on you run this, by-the-way.

.

2 Unstage Files

git add main.py

git restore --staged main.py

This will unstage the staged files without discarding the edits you made. Nothing is lost/deleted.

.

3 Amend Commits

Change commits already committed.

git commit -m 'add auth' # you already ran this

# suppose you wanna add login.py to same commit -
# first stage the changes
git add login.py

git commit --amend

Git replaces recent commit with the new commit containing extra changes.

.

4 Revert Commits

Say you started with

print("hello")

then commited it with say git commit -m "Create program" ,then changed it to

print("Hello")
print("Welcome")

then commited it again with say git commit -m "Add welcome message" ,

But now you think the second commit was a mistake and you wanna go back. Then use :

git revert HEAD

HEAD was pointing to current commit so you targeted it.

Git creates another commit that reverses the changes from the previous commit.

File becomes again -

print("Hello")

but history has all 3 commits -

1 Create program
2 Add welcome message
3 Revert "Add welcome message"

So, revert undoes an earlier commit but by creating a new commit with old state.

.

5 Reset

git reset moves a branch's pointer to another commit.

The default reset mode is --mixed .

git reset HEAD~1
# OR
git reset --mixed HEAD~1

There are --mixed, --soft, --hard modes.

git reset --soft HEAD~1

Soft can make the move from commit 2 to commit 1 but changes introduced in commit 2 remain staged, ready to commit again.

git reset --hard HEAD~1

Hard makes the move from commit 2 to commit 1 but changes introduced in commit 2 are REMOVED.

--soft → move commit back, keep changes staged
--mixed → move commit back, keep changes unstaged
--hard → move commit back, discard changes

to move main back by one commit -

git reset HEAD~1

.


Recovering from Mistakes

1 Git reflog

reflog is a local record of where HEAD and branch references have recently moved.

git reflog

# example output
#abc123 HEAD@{0}: switch: moving from auth to main
#def456 HEAD@{1}: commit: Add login
#ghi789 HEAD@{2}: switch: moving from main to auth

.

2 Detached HEAD

HEAD can move between commits without moving the branch - we already saw this! If you make a commit while being on detached HEAD and move away, that commit may become unreachable. But reflog can help find it.

It can be recovered using

git switch -c recovered <commit>

Now the commit is safely referenced again.

.

3 Recovering a deleted branch

If you ran by-mistake git branch -D auth the branch would disappear but the commits would be still there if garbage collection hasn't occured.

# find the old position
git reflog

# it will show the commit with an ID like this
abc123 HEAD@{4}: commit: Add authentication

# use that ID to restore the commit by attaching it to a branch
git switch -c auth-recovered abc123 # -c creates new branch

Deleting a branch deletes the pointer to commits not any commit itself. These dangling commits can be recovered.


Project Hygiene and Secrets Management

  • .gitignore tells Git which files/folders it should ignore.

  • If you accidently commit .env use git rm --cached .env then add .env to .gitignore .

  • Secrets include passwords, API keys, tokens, private certificates.

  • If secrets get pushed to remote, they are to be assumed as compromised. Hence, revoke them.

  • Generated files + Dependencies and build artifacts like .venv/ , __pycache__/ , dist/ , build/ , coverage/ , target/ , node_modules/ must be excluded from commits and gitingored.


How is GitHub different from Git?

GitHub is a hosted platform or service that adds features around Git repositories, such as:

  • Web-based repository browsing

  • Contributions via Pull requests and code review

  • Issues and project management

  • Access control and permissions

  • CI/CD through GitHub Actions

  • Collaboration

  • Public repository discovery and sharing

  • Web backup

It's not a part of git but more like an add-on.


Advanced Local Workflows

Basic : status, diff, branches, commits

1) Stashing

Stashing temporarily saves and removes uncommitted changes from the working tree and restores it to the last commit.

So last commit + changes in working tree ----> last commit only + (changes saved in stash)

To stash your changes run -

git stash

To bring back the changes -

git stash pop

If any merge conflict happens, stash is not lost. Git stops and marks conflicts to resolve manually.

This is similar to merging branches but in this case we work on the same branch and stashed changed are merged with changes in current working tree.

To see stashes -

git stash list

.

2) Interactive Staging

Normal adding/staging stages the whole file but you can stage specific parts of a file as well. Git will show each chunk and ask what to stage.

git add utils.py # normal, stages whole file

git add -p

When to use this -

  • When two unrelated changes get mixed up in one file

  • To stage only the bug fix and commit it separately

.

3) Interactive rebase

Rewrite local commit history. Git opens editor showing last X commits with options for actions to perform.

git rebase -i HEAD~3
pick keep commit
reword change message
edit stop and modify commit
squash combine with previous commit
drop remove commit

.

4) Squashing Commits

Squashing means combining multiple commits into fewer commits.

As we were using the 'auth' example: each step like adding 'login page', 'signup page', 'validation engine' can be a commit on its own. But let's say I only want 'added auth' I can squash group of commits -

git rebase -i HEAD~4

This interactive rebase approach can do this.

Squashing changes history, it not just created an "undo" commit.

You'll have to pick "squash" instead of default "pick"

.

5) Cleaning up History

git log --oneline # see the history
git rebase -i HEAD~N # make modifications with rebase
# N is the no. of commits to select

The Commit Cycle

CHANGE FILES
 ↓
git status # get list of stages changes
 ↓
git diff # compare changes between versions
 ↓
git add # adds files to commit to new version
 ↓
git commit # creates and saves the new version
 ↓
repeat

GIT Commands Cheat-sheet

// Setup & Basic Workflow
---
git init // Initialize Git in the current project
git status // Show current changes and staging state
git diff // Show unstaged changes
git diff --staged // Show staged changes
git add <filename> // Stage a specific file
git add . // Stage all changes
git add -p // Stage selected parts of files
git commit -m "message" // Create a commit

// Branches
---
git branch // List branches
git branch <branch-name> // Create a branch
git switch <branch-name> // Switch to an existing branch
git switch -c <branch-name> // Create and switch to a new branch
git merge <branch-name> // Merge a branch into the current branch
git branch -d <branch-name> // Safely delete a merged branch
git branch -D <branch-name> // Force-delete a branch

// Inspecting History
---
git log --oneline // Show compact commit history
git show <commit-id> // Show a commit and its changes
git diff <commit-1> <commit-2> // Compare two commits
git diff HEAD // Compare working files with latest commit
git diff HEAD~1 // Compare working files with previous commit
git blame <filename> // Show which commit changed each line

// Inspecting Git Internals
---
git cat-file -t <hash> // Show the type of a Git object
git cat-file -p <hash> // Show the contents of a Git object
cat .git/HEAD // Show what HEAD currently points to

// Detached HEAD
---
git switch --detach <commit> // Temporarily inspect a specific commit
git switch main // Return to the main branch

// Undoing Changes
---
git restore <filename> // Discard unstaged changes in a file
git restore --staged <filename> // Unstage a file without discarding changes
git commit --amend // Replace the latest commit with updated changes
git revert <commit> // Undo a commit by creating a new commit

// Resetting History
---
git reset HEAD~1 // Move branch back one commit, keep changes unstaged
git reset --mixed HEAD~1 // Move branch back, keep changes unstaged
git reset --soft HEAD~1 // Move branch back, keep changes staged
git reset --hard HEAD~1 // Move branch back and discard changes

// Recovering from Mistakes
---
git reflog // Show recent movements of HEAD and references
git switch -c <branch> <commit> // Create a branch from a specific commit
git gc // Clean up unreachable Git objects

// Remote Repositories
---
git remote add origin <url> // Add a remote repository
git push origin main // Send local commits to a remote branch
git pull origin main // Fetch and integrate remote changes

// Stashing
---
git stash // Temporarily save uncommitted changes
git stash pop // Restore the latest stash
git stash list // List saved stashes

// Interactive History Editing
---
git rebase -i HEAD~N // Interactively modify the last N commits

// Removing Tracked Files
---
git rm --cached <filename> // Stop tracking a file but keep it locally

That's it for this article.

.

Join Omkar on Peerlist!

Join amazing folks like Omkar and thousands of other builders on Peerlist.

peerlist.io/

It’s available... this username is available! 😃

Claim your username before it's too late!

This username is already taken, you’re a little late.😐

0

0

0