Inspired by freeCodeCamp এর "Git Internals" guide আর Octobot এর "How Git Internally Works" থেকে। লেখা আমার নিজের ভাষায়, ছবি Octobot এর article থেকে credit দিয়ে নেওয়া।

এই tutorial এ কী শিখব

Git internals cover illustration

git add, git commit, git push — এগুলো আমরা সবাই দিন রাত চালাই। কিন্তু কখনো ভেবেছ .git folder এর ভিতরে আসলে কী হচ্ছে? git commit চালালে কোথায় কী save হয়? branch switch করলে কোন file update হয়?

এই lesson এ আমরা দুই step এ এগোব:

  1. Theory — Git এর core তিনটা object (blob, tree, commit), branch কী, change record এর তিন stage।
  2. Hardcore manual rebuild — শুধু plumbing command ব্যবহার করে নিজ হাতে .git folder বানাব, object create করব, branch তৈরি করব। কোনো git init, git add, git commit না — সব manually।

এটা যখন শেষ করবে, Git আর কোনো magic থাকবে না। শুধু কিছু text file আর hash এর খেলা।


Part 1: Theory

Git এর তিন object — blob, tree, commit

Git কে key-value store হিসেবে দেখানো diagram

Git এর core আসলে একটা key-value store — content দাও, SHA-1 hash পাও, পরে সেই hash দিয়ে content ফেরত পাও। ব্যস।

এই store এ মাত্র তিন রকম object থাকে:

blob — file এর content

Blob মানে শুধু কোনো file এর কাঁচা bytes। কোনো নাম নাই, কোনো metadata নাই — শুধু content।

দুইটা ভিন্ন file এ যদি একদম একই content থাকে, Git দুইবার store করে না — একই blob দুই জায়গায় reference হয়।

tree — folder এর snapshot

Tree হলো একটা directory listing। প্রতিটা entry তে চারটা জিনিস:

  • mode — permission (100644 মানে regular file, 100755 executable, 040000 subdirectory)
  • type — blob (file) অথবা tree (subdirectory)
  • hash — যাকে point করছে তার SHA-1
  • name — file বা folder এর নাম

Tree আরেকটা tree কে point করতে পারে — এভাবে nested folder structure তৈরি হয়।

Tree object এর স্ট্রাকচার — mode, type, hash, name এর সারি

commit — snapshot + history

Commit object এ থাকে:

  • কোন root tree কে point করছে (পুরো repo এর snapshot)
  • author কে, কখন
  • committer কে, কখন (rebase এ আলাদা হতে পারে)
  • message
  • এক বা একাধিক parent commit এর hash (প্রথম commit এ parent থাকে না, merge commit এ দুইটা থাকে)

Commit object এর ভিতরের ফিল্ড — tree, author, committer, message, parent

Parent pointer এর কারণেই commit গুলো একটা linked list তৈরি করে — এটাই history।

Parent pointer chain দিয়ে commit history linear linked list তৈরি হওয়া

তিনটার সম্পর্ক

Objectকী রাখেকীসে point করে
commitmetadata + messageএকটা tree + parent commit(s)
treedirectory listingঅনেক blob/tree
blobfile contentকিছু না

Git এর তিন object type — commit, tree, blob — এর relationship summary diagram

সব object SHA-1 hash দিয়ে addressed। সব immutable — একবার তৈরি হলে আর পাল্টায় না। কিছু পাল্টালে নতুন object তৈরি হয়, পুরানোটা থেকে যায় (যতদিন কোনো কিছু সেটাকে reference করছে)।

Branch — commit এর উপর মানুষপড়া নাম

40 character এর SHA-1 hash মনে রাখা impossible। তাই Git branch নামে একটা concept দেয় — একটা মানুষপড়া নাম যেটা শুধু একটা commit hash কে point করে।

.git/refs/heads/main — এটা একটা plain text file। ভিতরে শুধু একটা hash লেখা। ব্যস। main branch বলতে এর বেশি কিছু না।

main branch ফাইলের content — শুধু একটা commit hash

নতুন commit করলে Git এই file টা update করে দেয় — নতুন commit এর hash বসিয়ে দেয়। এজন্যই branch "এগিয়ে যায়"।

Branch pointer commit কে কীভাবে পয়েন্ট করে — diagram

HEAD — pointer to pointer

কোন branch এ আমি এখন আছি? সেটা track করে .git/HEAD file। ভিতরে লেখা থাকে:

ref: refs/heads/main

মানে HEAD সরাসরি commit কে point করে না — বরং একটা branch কে point করে, branch কে point করে commit। HEAD = pointer to pointer।

HEAD pointer-to-pointer relationship visualization

git checkout dev করলে আসলে শুধু HEAD এর ভিতরের লাইন পাল্টায় (ref: refs/heads/dev)। working directory adjust হয়, কিন্তু কোনো object copy হয় না।

Change record এর তিন stage

Git এ যেকোনো change তিনটা area দিয়ে যায়:

Working Directory  →  Staging Area (Index)  →  Repository (.git/objects)
   (modify)              (git add)                   (git commit)
  • Working directory — তোমার actual files। যা edit করছ।
  • Staging area (বা index) — কোন কোন change next commit এ যাবে সেটার একটা "prepared" list। File .git/index এ binary format এ থাকে।
  • Repository — .git/objects/ এর ভিতরের permanent storage।

File এর state দুই রকম: tracked (Git চিনে) আর untracked (নতুন file, Git এখনো জানে না)।


Part 2: Conventional way

আগে normal way তে দেখি কী হয়।

$ mkdir myrepo && cd myrepo
$ git init
$ ls -a .git
HEAD  config  description  hooks/  info/  objects/  refs/

নতুন git repo init এর পর .git ফোল্ডারের ভিতরের স্ট্রাকচার

একটা file বানিয়ে commit করি:

$ echo "hello git" > hello.txt
$ git add hello.txt
$ git commit -m "first commit"

এখন .git/objects/ দেখো:

commit করার পর objects ফোল্ডারে তিনটা নতুন object — blob, tree, commit

তিনটা object তৈরি হয়েছে — blob (file content), tree (folder), commit। প্রতিটা সাব-folder এর নাম hash এর প্রথম 2 character, file এর নাম বাকি 38।

git cat-file -p <hash> দিয়ে যেকোনো object এর content দেখা যায়:

cat-file দিয়ে tree object inspect করা

cat-file দিয়ে commit object inspect করে metadata দেখা

ঠিক যেমন theory তে বলেছিলাম — তিন object, পরস্পরের সাথে যুক্ত।


Part 3: এবার hardcore — manually .git বানাই

এখন আসল মজা। আমরা কোনো git init, git add, git commit চালাব না। শুধু plumbing command ব্যবহার করে নিজ হাতে repo বানাব।

Plumbing vs Porcelain

Git এর command দুই layer এর:

  • Porcelain — user-facing, প্রতিদিনের command: git add, git commit, git log, git branch, git checkout। সুন্দর, easy।
  • Plumbing — low-level command: git hash-object, git cat-file, git update-index, git write-tree, git commit-tree, git update-ref। কাঁচা, কিন্তু শক্তিশালী।

Porcelain command গুলো ভিতরে plumbing command call করে। আমরা porcelain skip করে সরাসরি plumbing দিয়ে কাজ করব।

Step 1: হাতে .git folder বানাই

$ mkdir hardcore-repo && cd hardcore-repo
$ mkdir -p .git/objects .git/refs/heads
$ echo "ref: refs/heads/main" > .git/HEAD

এতটুকুই। তিনটা folder + একটা HEAD file। Git এখন একে valid repo হিসেবে চিনবে:

$ git status
On branch main
No commits yet
nothing to commit

কোনো git init লাগে নাই — Git বুঝে নিয়েছে কারণ .git/ এর shape ঠিক আছে।

Step 2: হাতে blob তৈরি

একটা file এর content কে blob বানাতে git hash-object ব্যবহার করি:

$ echo "hello hardcore" > hello.txt
$ git hash-object -w hello.txt
a1b2c3d4e5f6...
  • -w মানে "শুধু hash না, actually .git/objects/ এ write করো"
  • output টা SHA-1 hash

.git/objects ফোল্ডারে নতুন blob object তৈরি হওয়া

verify করি:

$ git cat-file -t a1b2c3d4
blob
$ git cat-file -p a1b2c3d4
hello hardcore

git cat-file কমান্ড দিয়ে object এর content দেখা

Blob তৈরি হয়ে গেছে। কিন্তু এটা এখনো staging area তে নাই — index এ register করতে হবে।

Step 3: Index এ add করি

git update-index দিয়ে blob কে index file এ entry হিসেবে যোগ করি:

$ git update-index --add --cacheinfo 100644 a1b2c3d4... hello.txt
  • --cacheinfo — manually mode, hash, path দিচ্ছি
  • 100644 — regular file permission
  • এটা porcelain git add এর কাজ — কিন্তু আমরা সরাসরি plumbing দিয়ে করছি
$ git status
Changes to be committed:
  new file:   hello.txt

Step 4: Tree তৈরি

Index এর state কে একটা tree object বানাতে git write-tree:

$ git write-tree
b2c3d4e5...

এটা index এ যা যা আছে সব নিয়ে একটা tree object তৈরি করে objects/ এ write করে, hash return করে।

cat-file দিয়ে tree object inspect করা

$ git cat-file -p b2c3d4e5
100644 blob a1b2c3d4...  hello.txt

দেখো — tree এর ভিতরে exactly সেই info: mode, type, blob-hash, name।

Step 5: Commit তৈরি

Tree কে wrap করে commit object বানাই git commit-tree দিয়ে:

$ echo "first hardcore commit" | git commit-tree b2c3d4e5
c3d4e5f6...
  • input হিসেবে commit message stdin দিয়ে
  • output — commit object এর hash
$ git cat-file -p c3d4e5f6
tree b2c3d4e5...
author Nazrul <...> 1716...
committer Nazrul <...> 1716...

first hardcore commit

প্রথম commit, তাই কোনো parent line নাই।

Step 6: Branch কে point করানো

Commit তৈরি হলো কিন্তু main branch এখনো এটাকে চেনে না — কারণ refs/heads/main file ই নাই। তৈরি করি:

$ git update-ref refs/heads/main c3d4e5f6...

অথবা সরাসরি:

$ echo "c3d4e5f6..." > .git/refs/heads/main

দুইটাই same কাজ — branch file এ commit hash বসিয়ে দেওয়া।

নতুন commit এর পর branch pointer auto-advance

verify:

$ git log
commit c3d4e5f6... (HEAD -> main)
Author: Nazrul <...>

    first hardcore commit

🎉 কোনো porcelain command ছাড়া আমরা পুরা commit তৈরি করে ফেললাম।

Step 7: দ্বিতীয় commit (parent দিয়ে)

File edit করে আবার একই process:

$ echo "second line" >> hello.txt
$ git hash-object -w hello.txt              # নতুন blob
d4e5f6...
$ git update-index --add --cacheinfo 100644 d4e5f6... hello.txt
$ git write-tree                            # নতুন tree
e5f6a7...
$ echo "second commit" | git commit-tree e5f6a7 -p c3d4e5f6
f6a7b8...
$ git update-ref refs/heads/main f6a7b8...

লক্ষ্য করো — -p c3d4e5f6 দিয়ে আগের commit কে parent হিসেবে দিচ্ছি। এটাই history তৈরি করে।

নতুন commit object inspect — parent field এ আগের commit এর hash

অপরিবর্তিত ফাইলের blob দুই commit এ same hash দেখাচ্ছে

Step 8: নতুন branch হাতে বানাই

dev branch তৈরি করতে শুধু একটা file বানালেই হয়:

$ cp .git/refs/heads/main .git/refs/heads/dev

ব্যস। dev এখন main এর same commit কে point করছে।

.git/refs/heads ফোল্ডারের ভিতর branch ফাইলগুলো

switch করতে HEAD পাল্টাই:

$ echo "ref: refs/heads/dev" > .git/HEAD
$ git branch
* dev
  main

দেখলে? Branch switch মানে শুধু একটা text file এর content পাল্টানো। আর কিছু না।

git checkout দিয়ে branch switch — HEAD update হচ্ছে

Step 9: Tag — branch এর immutable cousin

Tag আর branch এর difference একটাই: tag advance করে না।

$ mkdir -p .git/refs/tags
$ echo "f6a7b8..." > .git/refs/tags/v1.0

ব্যস। v1.0 permanently সেই commit কে point করবে।

Tag আর branch এর তুলনা — tag স্থির, branch চলমান


সারসংক্ষেপ

আমরা এই tutorial এ যা শিখলাম:

  1. Git core = key-value store — content দাও, SHA-1 hash পাও।
  2. তিন object type:
    • blob = file content
    • tree = directory listing (mode + type + hash + name)
    • commit = tree pointer + metadata + parent
  3. Branch = .git/refs/heads/<name> text file, ভিতরে commit hash।
  4. HEAD = pointer to current branch (ref: refs/heads/main)।
  5. Tag = immutable branch।
  6. Plumbing command দিয়ে পুরো repo manually বানানো যায়:
    • git hash-object -w → blob
    • git update-index --add --cacheinfo → staging
    • git write-tree → tree
    • git commit-tree -p → commit
    • git update-ref → branch

এতটুকু মাথায় থাকলে rebase, cherry-pick, reset, reflog — সব কিছু পরিষ্কার লাগবে। কারণ এগুলো আসলে শুধু pointer গুলো এদিক-ওদিক সরানো ছাড়া আর কিছুই না।

Git আসলে version control না — version control হলো content-addressable object store এর উপর বসানো একটা পাতলা লেয়ার।


আরো পড়তে